// @vitest-environment node import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ events: [] as string[], updateMany: vi.fn(async () => ({ count: 1 })), encryptTenantField: vi.fn(async () => "sealed-credentials"), createAuditEvent: vi.fn(async () => undefined), validateConnection: vi.fn(async () => ({ ok: true, health: "active" as const, messageKey: "integrations.health.active", })), })); vi.mock("~/utils/db/.server/tenantRls.server", () => ({ withTenantRLS: async (_tenantId: string, callback: (tx: unknown) => Promise) => { mocks.events.push("transaction"); return callback({ dataConnection: { updateMany: mocks.updateMany } }); }, })); vi.mock("@hivegrid/db/crypto/tenant-envelope.server", () => ({ decryptTenantField: vi.fn(), encryptTenantField: async (...args: unknown[]) => { mocks.events.push("encrypt"); return mocks.encryptTenantField(...args); }, })); vi.mock("@hivegrid/db/audit-events.server", () => ({ createAuditEvent: mocks.createAuditEvent })); vi.mock("@hivegrid/db/notifications.server", () => ({ createNotificationDelivery: vi.fn() })); vi.mock("~/utils/db.server", () => ({ db: {} })); vi.mock("@hivegrid/integrations", () => ({ resolveAdapter: vi.fn(() => ({ key: "kwp", connectionType: "oauth2", validateConnection: async (...args: unknown[]) => { mocks.events.push("validate"); return mocks.validateConnection(...args); }, })), })); import { activateOAuthConnection, AppsEngineServiceError } from "./appsEngineService.server"; describe("activateOAuthConnection", () => { beforeEach(() => { mocks.events.length = 0; mocks.updateMany.mockClear(); mocks.encryptTenantField.mockClear(); mocks.createAuditEvent.mockClear(); mocks.validateConnection.mockClear(); mocks.validateConnection.mockResolvedValue({ ok: true, health: "active", messageKey: "integrations.health.active", }); mocks.updateMany.mockResolvedValue({ count: 1 }); }); it("validates outside the transaction and persists encrypted OAuth credentials with expiry", async () => { const tokenExpiresAt = new Date("2026-07-20T18:30:00.000Z"); await activateOAuthConnection({ tenantId: "tenant-1", connectionId: "connection-1", appId: "kwp", credentials: { accessToken: "access", refreshToken: "refresh" }, tokenExpiresAt, actorUserId: "user-1", expectedUpdatedAt: new Date("2026-07-20T12:00:00.000Z"), }); expect(mocks.events).toEqual(["validate", "encrypt", "transaction"]); expect(mocks.encryptTenantField).toHaveBeenCalledWith( "tenant-1", JSON.stringify({ accessToken: "access", refreshToken: "refresh" }), ); expect(mocks.updateMany).toHaveBeenCalledWith({ where: { id: "connection-1", tenantId: "tenant-1", appId: "kwp", archivedAt: null, updatedAt: new Date("2026-07-20T12:00:00.000Z"), }, data: expect.objectContaining({ credentialsCiphertext: "sealed-credentials", tokenExpiresAt, connectionStatus: "active", healthMessageKey: "integrations.health.active", }), }); expect(mocks.createAuditEvent).toHaveBeenCalledWith( expect.objectContaining({ tenantId: "tenant-1", actorId: "user-1", action: "INTEGRATION_OAUTH_CONNECT", targetId: "connection-1", }), ); }); it("does not persist credentials when the provider validation fails", async () => { mocks.validateConnection.mockResolvedValueOnce({ ok: false, health: "broken", messageKey: "integrations.health.scopeMissing", }); await expect( activateOAuthConnection({ tenantId: "tenant-1", connectionId: "connection-1", appId: "kwp", credentials: {}, tokenExpiresAt: null, actorUserId: "user-1", expectedUpdatedAt: new Date("2026-07-20T12:00:00.000Z"), }), ).rejects.toEqual( expect.objectContaining>({ messageKey: "integrations.health.scopeMissing", }), ); expect(mocks.events).toEqual(["validate"]); expect(mocks.updateMany).not.toHaveBeenCalled(); }); it("rejects a slot that is not owned by the tenant and app", async () => { mocks.updateMany.mockResolvedValueOnce({ count: 0 }); await expect( activateOAuthConnection({ tenantId: "tenant-1", connectionId: "foreign-slot", appId: "kwp", credentials: { accessToken: "access" }, tokenExpiresAt: null, actorUserId: "user-1", expectedUpdatedAt: new Date("2026-07-20T12:00:00.000Z"), }), ).rejects.toMatchObject({ status: 404 }); expect(mocks.createAuditEvent).not.toHaveBeenCalled(); }); });