Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/integrations/appsEngineService.oauth.test.ts
T

145 lines
4.8 KiB
TypeScript

// @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<unknown>) => {
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<Partial<AppsEngineServiceError>>({
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();
});
});