// @vitest-environment node import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ connectionFindFirst: vi.fn(), connectionUpdateMany: vi.fn(async () => ({ count: 1 })), syncRunCreate: vi.fn(), syncRunFindMany: vi.fn(async () => []), syncRunFindFirst: vi.fn(), syncItemFindMany: vi.fn(async () => []), syncRunLogFindMany: vi.fn(async () => []), encryptTenantField: vi.fn(async () => "sealed"), createAuditEvent: vi.fn(async () => undefined), notify: vi.fn(async () => undefined), validateConnection: vi.fn(async () => ({ ok: true, health: "active" as const, messageKey: "integrations.health.active", })), })); vi.mock("@hivegrid/foundation", async () => ({ ...(await vi.importActual("@hivegrid/foundation")), notify: mocks.notify, })); vi.mock("~/utils/db/.server/tenantRls.server", () => ({ withTenantRLS: async (_tenantId: string, callback: (tx: unknown) => Promise) => callback({ dataConnection: { findFirst: mocks.connectionFindFirst, updateMany: mocks.connectionUpdateMany, }, appSyncRun: { create: mocks.syncRunCreate, findMany: mocks.syncRunFindMany, findFirst: mocks.syncRunFindFirst, }, appSyncItem: { findMany: mocks.syncItemFindMany }, appSyncRunLog: { findMany: mocks.syncRunLogFindMany }, }), })); vi.mock("@hivegrid/db/crypto/tenant-envelope.server", () => ({ decryptTenantField: vi.fn(), encryptTenantField: mocks.encryptTenantField, })); vi.mock("@hivegrid/db/audit-events.server", () => ({ createAuditEvent: mocks.createAuditEvent })); vi.mock("~/utils/db.server", () => ({ db: {} })); vi.mock("@hivegrid/integrations", () => ({ resolveAdapter: vi.fn(() => ({ validateConnection: mocks.validateConnection })), })); import { disconnectConnection, checkConnectionHealth, getConnectionOrThrow, getSyncRunWithItems, listSyncRuns, reconnectConnection, runSyncForConnection, } from "./appsEngineService.server"; const CONNECTION_ROW = { id: "connection-1", tenantId: "tenant-1", appId: "sample-data-sync", label: "Sample", connectionStatus: "active", lastHealthCheck: null, healthMessageKey: null, tokenExpiresAt: null, createdByUserId: "user-1", isDemoData: false, createdAt: new Date("2026-07-20T10:00:00.000Z"), archivedAt: null, } as const; describe("Apps Engine app/connection binding", () => { beforeEach(() => { vi.clearAllMocks(); mocks.connectionUpdateMany.mockResolvedValue({ count: 1 }); mocks.syncRunFindMany.mockResolvedValue([]); mocks.syncItemFindMany.mockResolvedValue([]); mocks.syncRunLogFindMany.mockResolvedValue([]); mocks.encryptTenantField.mockResolvedValue("sealed"); mocks.validateConnection.mockResolvedValue({ ok: true, health: "active", messageKey: "integrations.health.active", }); }); it("loads a connection only when tenant, id, and expected app all match", async () => { mocks.connectionFindFirst.mockResolvedValueOnce(CONNECTION_ROW); await expect(getConnectionOrThrow("tenant-1", "connection-1", "sample-data-sync")).resolves.toMatchObject({ id: "connection-1", appId: "sample-data-sync", }); expect(mocks.connectionFindFirst).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "connection-1", tenantId: "tenant-1", appId: "sample-data-sync" }, }), ); }); it("binds reconnect and disconnect reads and writes to the expected app", async () => { mocks.connectionFindFirst.mockResolvedValue({ id: "connection-1", appId: "sample-data-sync" }); await reconnectConnection({ tenantId: "tenant-1", connectionId: "connection-1", expectedAppId: "sample-data-sync", credentials: { apiKey: "fixture" }, actorUserId: "user-1", }); await disconnectConnection({ tenantId: "tenant-1", connectionId: "connection-1", expectedAppId: "sample-data-sync", actorUserId: "user-1", }); expect(mocks.connectionFindFirst).toHaveBeenCalledTimes(2); expect(mocks.connectionFindFirst).toHaveBeenNthCalledWith( 1, expect.objectContaining({ where: { id: "connection-1", tenantId: "tenant-1", appId: "sample-data-sync" }, }), ); expect(mocks.connectionFindFirst).toHaveBeenNthCalledWith( 2, expect.objectContaining({ where: { id: "connection-1", tenantId: "tenant-1", appId: "sample-data-sync" }, }), ); expect(mocks.connectionUpdateMany).toHaveBeenCalledTimes(2); for (const [input] of mocks.connectionUpdateMany.mock.calls) { expect(input).toEqual( expect.objectContaining({ where: { id: "connection-1", tenantId: "tenant-1", appId: "sample-data-sync" }, }), ); } expect(mocks.connectionUpdateMany).toHaveBeenNthCalledWith( 2, expect.objectContaining({ data: expect.objectContaining({ credentialsCiphertext: null, tokenExpiresAt: null, connectionStatus: "revoked" }), }), ); }); it("rejects a manual sync before creating a run when the expected app does not match", async () => { mocks.connectionFindFirst.mockResolvedValueOnce(null); await expect( runSyncForConnection({ tenantId: "tenant-1", connectionId: "kwp-connection", expectedAppId: "sample-data-sync", kind: "manual", }), ).rejects.toMatchObject({ status: 404 }); expect(mocks.connectionFindFirst).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "kwp-connection", tenantId: "tenant-1", appId: "sample-data-sync" }, }), ); expect(mocks.syncRunCreate).not.toHaveBeenCalled(); }); it("binds sync history and run detail to both the connection and its expected app", async () => { mocks.syncRunFindFirst.mockResolvedValueOnce(null); await expect(listSyncRuns("tenant-1", "connection-1", "sample-data-sync")).resolves.toEqual([]); expect(mocks.syncRunFindMany).toHaveBeenCalledWith({ where: { tenantId: "tenant-1", dataConnectionId: "connection-1", dataConnection: { tenantId: "tenant-1", appId: "sample-data-sync" }, }, orderBy: { createdAt: "desc" }, take: 50, }); await expect( getSyncRunWithItems("tenant-1", "run-1", "connection-1", "sample-data-sync"), ).rejects.toMatchObject({ status: 404 }); expect(mocks.syncRunFindFirst).toHaveBeenCalledWith({ where: { id: "run-1", tenantId: "tenant-1", dataConnectionId: "connection-1", dataConnection: { tenantId: "tenant-1", appId: "sample-data-sync" }, }, }); expect(mocks.syncItemFindMany).not.toHaveBeenCalled(); expect(mocks.syncRunLogFindMany).not.toHaveBeenCalled(); }); it("links an unhealthy-connection notification to the affected connection settings", async () => { mocks.connectionFindFirst.mockResolvedValueOnce({ id: "connection-1", appId: "aircall", connectionStatus: "active", credentialsCiphertext: null, createdByUserId: "user-1", }); await expect( checkConnectionHealth({ tenantId: "tenant-1", connectionId: "connection-1", notificationDeeplink: "/acme/settings/integrations/telephony", }), ).resolves.toEqual({ status: "broken", messageKey: "integrations.health.notConnected" }); expect(mocks.notify).toHaveBeenCalledWith( expect.objectContaining({ tenantId: "tenant-1", userId: "user-1", messageKey: "integrations.notify.connectionUnhealthy", sourceKey: "integrations:connection-unhealthy:connection-1", payloadMetadata: expect.objectContaining({ connectionId: "connection-1", health: "broken", deeplink: "/acme/settings/integrations/telephony", }), }), expect.anything(), ); }); });