// @vitest-environment node import { beforeEach, describe, expect, it, vi } from "vitest"; import { CatalogError, foundationErrors } from "@hivegrid/foundation"; const mocks = vi.hoisted(() => ({ AppsEngineServiceError: class AppsEngineServiceError extends Error { readonly messageKey: string; constructor(messageKey: string) { super(messageKey); this.name = "AppsEngineServiceError"; this.messageKey = messageKey; } }, KwpOidcClientError: class KwpOidcClientError extends Error { readonly code: string; readonly diagnosticCode?: string; constructor(code: string, options?: { diagnosticCode?: string }) { super(code); this.name = "KwpOidcClientError"; this.code = code; this.diagnosticCode = options?.diagnosticCode; } }, KwpError: class KwpError extends Error { readonly code: string; readonly httpStatus?: number; constructor(code: string, httpStatus?: number) { super(code); this.name = "KwpError"; this.code = code; this.httpStatus = httpStatus; } }, context: { tenantId: "tenant-1", userId: "user-1" } as { tenantId?: string; userId?: string }, executeRaw: vi.fn(async () => 1), findMany: vi.fn(), findFirst: vi.fn(), create: vi.fn(), updateMany: vi.fn(), createAuditEvent: vi.fn(async () => undefined), activateOAuthConnection: vi.fn(async () => undefined), encryptTenantField: vi.fn(), decryptTenantField: vi.fn(), discoverKwpMandants: vi.fn(), readConfig: vi.fn(), buildAuthorizationUrl: vi.fn(), exchangeAuthorizationCode: vi.fn(), createChallenge: vi.fn(), })); vi.mock("@hivegrid/db/rls-context.server", () => ({ getRlsExecutionContext: () => mocks.context })); vi.mock("@hivegrid/db/audit-events.server", () => ({ createAuditEvent: mocks.createAuditEvent })); vi.mock("@hivegrid/db/crypto/tenant-envelope.server", () => ({ encryptTenantField: mocks.encryptTenantField, decryptTenantField: mocks.decryptTenantField, })); vi.mock("@hivegrid/integrations", async (importOriginal) => ({ ...(await importOriginal()), KwpError: mocks.KwpError, discoverKwpMandants: mocks.discoverKwpMandants, })); vi.mock("~/utils/db/.server/tenantRls.server", () => ({ withTenantRLS: (_tenantId: string, callback: (tx: unknown) => Promise) => callback({ $executeRaw: mocks.executeRaw, dataConnection: { findMany: mocks.findMany, findFirst: mocks.findFirst, create: mocks.create, updateMany: mocks.updateMany, }, }), })); vi.mock("~/utils/integrations/appsEngineService.server", () => ({ AppsEngineServiceError: mocks.AppsEngineServiceError, activateOAuthConnection: mocks.activateOAuthConnection, })); vi.mock("./kwpOidcClient.server", () => ({ KwpOidcClientError: mocks.KwpOidcClientError, readKwpOidcRuntimeConfig: mocks.readConfig, buildKwpAuthorizationUrl: mocks.buildAuthorizationUrl, exchangeKwpAuthorizationCode: mocks.exchangeAuthorizationCode, })); vi.mock("./kwpOAuthChallenge.server", () => ({ KWP_OAUTH_CHALLENGE_TTL_MS: 10 * 60_000, createKwpOAuthChallenge: mocks.createChallenge, })); import { beginKwpOAuthConnection, cancelKwpOAuthConnectionAttempt, completeKwpOAuthConnection, finalizeKwpOAuthConnection, KwpConnectError, loadKwpMandantSelection, } from "./KwpConnectionService.server"; const ACCESS_TOKEN = "access-token-sensitive"; const REFRESH_TOKEN = "refresh-token-sensitive"; const CLIENT_SECRET = "client-secret-sensitive"; const TOKEN_EXPIRES_AT = new Date("2099-07-20T13:05:00.000Z"); const PENDING_VERSION = new Date("2026-07-20T12:01:00.000Z"); const PENDING_CIPHERTEXT = "sealed-pending-bundle"; const PREVIOUS_CIPHERTEXT = "sealed-working-credentials"; const RESERVATION_EXPIRES_AT = new Date("2099-07-20T13:10:00.000Z"); const OAUTH_CREDENTIALS = { accessToken: ACCESS_TOKEN, refreshToken: REFRESH_TOKEN, expiresAt: TOKEN_EXPIRES_AT.toISOString(), issuer: "https://id.kwp-online.de/realms/kwp-federated", clientId: "_extern-geerkens2", clientSecret: CLIENT_SECRET, customerCode: "geerkens2", scopes: ["openid", "adresse-extern", "archiv-extern", "termin-extern", "vorgang-extern", "aktivitaet-extern", "offline_access"], } as const; const AVAILABLE_BUNDLE = { kind: "kwp_mandant_selection_v2", previous: null, oauth: OAUTH_CREDENTIALS, discovery: { kind: "available", options: [ { id: 1, code: "001", label: "Geerkens GmbH" }, { id: 2, code: "002", label: "Geerkens Service GmbH" }, ], }, issuedAt: "2026-07-20T12:00:30.000Z", } as const; const PREVIOUS_CONNECTION = { credentialsCiphertext: PREVIOUS_CIPHERTEXT, connectionStatus: "active", tokenExpiresAt: "2099-07-20T13:00:00.000Z", healthMessageKey: "integrations.kwp.health.active", lastHealthCheck: "2026-07-20T11:59:00.000Z", createdByUserId: "original-owner", } as const; const RESERVATION_BUNDLE = { kind: "kwp_oauth_reservation_v1", customerCode: "geerkens2", previous: PREVIOUS_CONNECTION, issuedAt: "2099-07-20T13:00:00.000Z", expiresAt: RESERVATION_EXPIRES_AT.toISOString(), } as const; const MANUAL_BUNDLE = { ...AVAILABLE_BUNDLE, discovery: { kind: "manual" }, } as const; const RECONNECT_SELECTION_BUNDLE = { ...AVAILABLE_BUNDLE, previous: PREVIOUS_CONNECTION, } as const; const EMPTY_AVAILABLE_BUNDLE = { ...AVAILABLE_BUNDLE, discovery: { kind: "available", options: [] }, } as const; const CHALLENGE = { tenantId: "tenant-1", tenantSlug: "acme", userId: "user-1", connectionId: "connection-1", connectionVersion: "2026-07-20T12:00:00.000Z", customerCode: "geerkens2", state: "a".repeat(64), nonce: "b".repeat(64), codeVerifier: "v".repeat(64), codeChallenge: "c".repeat(43), createdAt: 1_725_000_000_000, } as const; function pendingRow(overrides: Record = {}) { return { id: "connection-1", updatedAt: PENDING_VERSION, credentialsCiphertext: PENDING_CIPHERTEXT, tokenExpiresAt: TOKEN_EXPIRES_AT, healthMessageKey: "integrations.kwp.health.mandantSelectionRequired", ...overrides, }; } function reservationRow(overrides: Record = {}) { return pendingRow({ updatedAt: new Date(CHALLENGE.connectionVersion), tokenExpiresAt: RESERVATION_EXPIRES_AT, healthMessageKey: "integrations.kwp.health.oauthPending", ...overrides, }); } function mockPendingBundle(bundle: unknown = AVAILABLE_BUNDLE): void { mocks.findFirst.mockResolvedValue(pendingRow()); mocks.decryptTenantField.mockResolvedValue(JSON.stringify(bundle)); } beforeEach(() => { vi.clearAllMocks(); mocks.context = { tenantId: "tenant-1", userId: "user-1" }; mocks.findMany.mockResolvedValue([]); mocks.findFirst.mockResolvedValue(reservationRow()); mocks.create.mockResolvedValue({ id: "connection-1", updatedAt: new Date(CHALLENGE.connectionVersion) }); mocks.updateMany.mockResolvedValue({ count: 1 }); mocks.encryptTenantField.mockResolvedValue(PENDING_CIPHERTEXT); mocks.decryptTenantField.mockResolvedValue(JSON.stringify(RESERVATION_BUNDLE)); mocks.discoverKwpMandants.mockResolvedValue(AVAILABLE_BUNDLE.discovery.options); mocks.readConfig.mockReturnValue({ customerCode: "geerkens2", hiveGridTenantId: "tenant-1" }); mocks.buildAuthorizationUrl.mockResolvedValue("https://id.kwp-online.de/auth?state=state"); mocks.createChallenge.mockReturnValue({ payload: CHALLENGE, cookie: "__Host-kwp=challenge; Secure; HttpOnly" }); mocks.exchangeAuthorizationCode.mockResolvedValue({ credentials: OAUTH_CREDENTIALS, tokenExpiresAt: TOKEN_EXPIRES_AT }); }); describe("beginKwpOAuthConnection", () => { it("requires the entered customer code to match the tenant's provisioned pilot code", async () => { await expect(beginKwpOAuthConnection({ tenantId: "tenant-1", tenantSlug: "acme", userId: "user-1", customerCode: "other" })).rejects.toEqual( expect.objectContaining>({ code: "not_configured" }) ); await expect(beginKwpOAuthConnection({ tenantId: "tenant-1", tenantSlug: "acme", userId: "user-1", customerCode: "bad/code" })).rejects.toEqual( expect.objectContaining>({ code: "not_configured" }) ); expect(mocks.findMany).not.toHaveBeenCalled(); expect(mocks.create).not.toHaveBeenCalled(); }); it("serializes and reserves one pending slot before creating the bound challenge", async () => { await expect(beginKwpOAuthConnection({ tenantId: "tenant-1", tenantSlug: "acme", userId: "user-1", customerCode: "geerkens2" })).resolves.toEqual({ authorizationUrl: "https://id.kwp-online.de/auth?state=state", setCookie: "__Host-kwp=challenge; Secure; HttpOnly", }); expect(mocks.executeRaw).toHaveBeenCalledTimes(1); expect(mocks.create).toHaveBeenCalledWith({ data: expect.objectContaining({ tenantId: "tenant-1", appId: "kwp", credentialsCiphertext: PENDING_CIPHERTEXT, connectionStatus: "pending", healthMessageKey: "integrations.kwp.health.oauthPending", tokenExpiresAt: expect.any(Date), createdByUserId: "user-1", }), select: { id: true, updatedAt: true }, }); expect(JSON.parse(mocks.encryptTenantField.mock.calls[0]?.[1] as string)).toEqual( expect.objectContaining({ kind: "kwp_oauth_reservation_v1", customerCode: "geerkens2", previous: null }) ); expect(mocks.createChallenge).toHaveBeenCalledWith(expect.objectContaining({ customerCode: "geerkens2", connectionId: "connection-1" })); expect(mocks.buildAuthorizationUrl).toHaveBeenCalledWith(expect.objectContaining({ customerCode: "geerkens2" })); expect(mocks.createAuditEvent).toHaveBeenCalledWith(expect.objectContaining({ action: "INTEGRATION_OAUTH_RESERVE" })); }); it("invalidates an old pending attempt with an exact new generation before starting another", async () => { const previousVersion = PENDING_VERSION; mocks.findMany.mockResolvedValueOnce([{ id: "connection-1", updatedAt: previousVersion, connectionStatus: "pending" }]); await beginKwpOAuthConnection({ tenantId: "tenant-1", tenantSlug: "acme", userId: "user-1", customerCode: "geerkens2" }); expect(mocks.create).not.toHaveBeenCalled(); const invalidation = mocks.updateMany.mock.calls[0]?.[0]; expect(invalidation).toEqual({ where: expect.objectContaining({ id: "connection-1", tenantId: "tenant-1", appId: "kwp", archivedAt: null, connectionStatus: "pending", updatedAt: previousVersion, }), data: expect.objectContaining({ credentialsCiphertext: PENDING_CIPHERTEXT, tokenExpiresAt: expect.any(Date), connectionStatus: "pending", healthMessageKey: "integrations.kwp.health.oauthPending", createdByUserId: "user-1", updatedAt: expect.any(Date), }), }); const nextVersion = invalidation?.data.updatedAt as Date; expect(nextVersion.getTime()).toBeGreaterThan(previousVersion.getTime()); expect(mocks.createChallenge).toHaveBeenCalledWith(expect.objectContaining({ connectionVersion: nextVersion.toISOString() })); expect(mocks.createAuditEvent).toHaveBeenCalledWith( expect.objectContaining({ action: "INTEGRATION_OAUTH_RESERVE", metadata: { appId: "kwp", created: false } }) ); mocks.findFirst.mockResolvedValueOnce(null); await expect( finalizeKwpOAuthConnection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1", version: previousVersion.toISOString(), mandantBinding: { kind: "none" }, }) ).rejects.toEqual(expect.objectContaining>({ code: "invalid_state" })); expect(mocks.activateOAuthConnection).not.toHaveBeenCalled(); }); it("reserves an active slot while preserving its full working snapshot", async () => { mocks.findMany.mockResolvedValueOnce([{ id: "connection-1", updatedAt: PENDING_VERSION, connectionStatus: "active", credentialsCiphertext: PREVIOUS_CIPHERTEXT, tokenExpiresAt: new Date(PREVIOUS_CONNECTION.tokenExpiresAt), healthMessageKey: PREVIOUS_CONNECTION.healthMessageKey, lastHealthCheck: new Date(PREVIOUS_CONNECTION.lastHealthCheck), createdByUserId: PREVIOUS_CONNECTION.createdByUserId, }]); await beginKwpOAuthConnection({ tenantId: "tenant-1", tenantSlug: "acme", userId: "user-1", customerCode: "geerkens2" }); expect(mocks.updateMany).toHaveBeenCalledWith({ where: expect.objectContaining({ connectionStatus: "active", updatedAt: PENDING_VERSION }), data: expect.objectContaining({ credentialsCiphertext: PENDING_CIPHERTEXT, connectionStatus: "pending", healthMessageKey: "integrations.kwp.health.oauthPending", updatedAt: expect.any(Date), }), }); expect(JSON.parse(mocks.encryptTenantField.mock.calls[0]?.[1] as string).previous).toEqual(PREVIOUS_CONNECTION); }); it("restores the working connection immediately when authorization cannot start", async () => { mocks.findMany.mockResolvedValueOnce([{ id: "connection-1", updatedAt: PENDING_VERSION, connectionStatus: "active", credentialsCiphertext: PREVIOUS_CIPHERTEXT, tokenExpiresAt: new Date(PREVIOUS_CONNECTION.tokenExpiresAt), healthMessageKey: PREVIOUS_CONNECTION.healthMessageKey, lastHealthCheck: new Date(PREVIOUS_CONNECTION.lastHealthCheck), createdByUserId: PREVIOUS_CONNECTION.createdByUserId, }]); mocks.buildAuthorizationUrl.mockRejectedValueOnce(new Error("provider unavailable")); await expect( beginKwpOAuthConnection({ tenantId: "tenant-1", tenantSlug: "acme", userId: "user-1", customerCode: "geerkens2" }) ).rejects.toEqual(expect.objectContaining>({ code: "authorization_failed" })); expect(mocks.updateMany).toHaveBeenCalledTimes(2); expect(mocks.updateMany.mock.calls[1]?.[0]).toEqual( expect.objectContaining({ data: expect.objectContaining({ credentialsCiphertext: PREVIOUS_CIPHERTEXT, connectionStatus: "active", createdByUserId: "original-owner", }), }) ); }); it("carries the original active snapshot into a replacement browser attempt", async () => { mocks.findMany.mockResolvedValueOnce([{ id: "connection-1", updatedAt: PENDING_VERSION, connectionStatus: "pending", credentialsCiphertext: PENDING_CIPHERTEXT, tokenExpiresAt: RESERVATION_EXPIRES_AT, healthMessageKey: "integrations.kwp.health.oauthPending", lastHealthCheck: null, createdByUserId: "user-1", }]); await beginKwpOAuthConnection({ tenantId: "tenant-1", tenantSlug: "acme", userId: "user-1", customerCode: "geerkens2" }); expect(JSON.parse(mocks.encryptTenantField.mock.calls[0]?.[1] as string).previous).toEqual(PREVIOUS_CONNECTION); }); it("rejects ambiguous rows and a lost attempt-generation CAS", async () => { mocks.findMany.mockResolvedValueOnce([ { id: "one", updatedAt: new Date(CHALLENGE.connectionVersion), connectionStatus: "pending" }, { id: "two", updatedAt: new Date(CHALLENGE.connectionVersion), connectionStatus: "pending" }, ]); await expect(beginKwpOAuthConnection({ tenantId: "tenant-1", tenantSlug: "acme", userId: "user-1", customerCode: "geerkens2" })).rejects.toEqual( expect.objectContaining>({ code: "slot_conflict" }) ); mocks.findMany.mockResolvedValueOnce([{ id: "one", updatedAt: PENDING_VERSION, connectionStatus: "pending" }]); mocks.updateMany.mockResolvedValueOnce({ count: 0 }); await expect(beginKwpOAuthConnection({ tenantId: "tenant-1", tenantSlug: "acme", userId: "user-1", customerCode: "geerkens2" })).rejects.toEqual( expect.objectContaining>({ code: "slot_conflict" }) ); }); }); describe("completeKwpOAuthConnection", () => { it("discovers immediately and CAS-stages an encrypted pending bundle without activating", async () => { await expect(completeKwpOAuthConnection({ callbackParameters: { code: "one-time-code", state: CHALLENGE.state }, challenge: CHALLENGE })).resolves.toEqual({ tenantId: "tenant-1", tenantSlug: "acme", connectionId: "connection-1", }); expect(mocks.discoverKwpMandants).toHaveBeenCalledWith(expect.objectContaining({ accessToken: ACCESS_TOKEN, customerCode: "geerkens2" })); const plaintext = mocks.encryptTenantField.mock.calls[0]?.[1] as string; expect(JSON.parse(plaintext)).toEqual( expect.objectContaining({ kind: "kwp_mandant_selection_v2", previous: PREVIOUS_CONNECTION, oauth: expect.objectContaining({ accessToken: ACCESS_TOKEN, refreshToken: REFRESH_TOKEN, clientSecret: CLIENT_SECRET }), discovery: AVAILABLE_BUNDLE.discovery, }) ); expect(JSON.parse(plaintext).oauth).not.toHaveProperty("mandantBinding"); expect(mocks.updateMany).toHaveBeenCalledWith({ where: expect.objectContaining({ id: "connection-1", tenantId: "tenant-1", appId: "kwp", archivedAt: null, updatedAt: new Date(CHALLENGE.connectionVersion), credentialsCiphertext: PENDING_CIPHERTEXT, tokenExpiresAt: RESERVATION_EXPIRES_AT, healthMessageKey: "integrations.kwp.health.oauthPending", }), data: expect.objectContaining({ credentialsCiphertext: PENDING_CIPHERTEXT, tokenExpiresAt: TOKEN_EXPIRES_AT, connectionStatus: "pending", createdByUserId: "user-1", }), }); expect(mocks.activateOAuthConnection).not.toHaveBeenCalled(); const persistedAndAudited = JSON.stringify([mocks.updateMany.mock.calls, mocks.createAuditEvent.mock.calls]); expect(persistedAndAudited).not.toContain(ACCESS_TOKEN); expect(persistedAndAudited).not.toContain(REFRESH_TOKEN); expect(persistedAndAudited).not.toContain(CLIENT_SECRET); }); it.each([ ["403", new mocks.KwpError("mandant_discovery_unavailable", 403)], ["404", new mocks.KwpError("mandant_discovery_unavailable", 404)], ["invalid response", new mocks.KwpError("invalid_response", 200)], ["provider outage", new mocks.KwpError("provider_outage", 503)], ["rate limit", new mocks.KwpError("rate_limit_exceeded", 429)], ])("stages manual selection on the documented %s discovery fallback", async (_label, error) => { mocks.discoverKwpMandants.mockRejectedValueOnce(error); await completeKwpOAuthConnection({ callbackParameters: { code: "code", state: CHALLENGE.state }, challenge: CHALLENGE }); const plaintext = JSON.parse(mocks.encryptTenantField.mock.calls[0]?.[1] as string); expect(plaintext.discovery).toEqual({ kind: "manual" }); expect(mocks.createAuditEvent).toHaveBeenCalledWith(expect.objectContaining({ metadata: { appId: "kwp", discovery: "manual", optionCount: 0 } })); }); it.each([ ["401", new mocks.KwpError("auth_token_invalid", 401), "discovery_auth_token_invalid"], ["invalid credentials", new mocks.KwpError("invalid_credentials", 400), "discovery_invalid_credentials"], ["invalid relay host", new mocks.KwpError("relay_host_invalid"), "discovery_relay_host_invalid"], ["unconfirmed binding", new mocks.KwpError("mandant_unconfirmed"), "discovery_mandant_unconfirmed"], ])("fails closed on %s discovery errors and restores the previous connection", async (_label, error, diagnosticCode) => { mocks.discoverKwpMandants.mockRejectedValueOnce(error); await expect(completeKwpOAuthConnection({ callbackParameters: { code: "code", state: CHALLENGE.state }, challenge: CHALLENGE })).rejects.toEqual( expect.objectContaining>({ code: "connection_failed", diagnosticCode }) ); expect(mocks.encryptTenantField).not.toHaveBeenCalled(); expect(mocks.updateMany).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ credentialsCiphertext: PREVIOUS_CIPHERTEXT, connectionStatus: "active", createdByUserId: "original-owner", }), }) ); }); it("normalizes a missing provider label and falls back manually on an invalid discovered option", async () => { mocks.discoverKwpMandants.mockResolvedValueOnce([{ id: 7, code: "007" }]); await completeKwpOAuthConnection({ callbackParameters: { code: "code", state: CHALLENGE.state }, challenge: CHALLENGE }); expect(JSON.parse(mocks.encryptTenantField.mock.calls[0]?.[1] as string).discovery).toEqual({ kind: "available", options: [{ id: 7, code: "007", label: "007" }], }); mocks.encryptTenantField.mockClear(); mocks.discoverKwpMandants.mockResolvedValueOnce([{ id: 7, code: "bad/code", label: "Invalid" }]); await completeKwpOAuthConnection({ callbackParameters: { code: "code", state: CHALLENGE.state }, challenge: CHALLENGE }); expect(JSON.parse(mocks.encryptTenantField.mock.calls[0]?.[1] as string).discovery).toEqual({ kind: "manual" }); }); it("rejects a callback CAS race and preserves stable token-exchange diagnostics", async () => { mocks.updateMany.mockResolvedValueOnce({ count: 0 }); await expect(completeKwpOAuthConnection({ callbackParameters: { code: "code", state: CHALLENGE.state }, challenge: CHALLENGE })).rejects.toEqual( expect.objectContaining>({ code: "invalid_state" }) ); mocks.exchangeAuthorizationCode.mockRejectedValueOnce(new mocks.KwpOidcClientError("invalid-token-response", { diagnosticCode: "missing_refresh_token" })); await expect(completeKwpOAuthConnection({ callbackParameters: { code: "code", state: CHALLENGE.state }, challenge: CHALLENGE })).rejects.toEqual( expect.objectContaining>({ code: "exchange_failed", diagnosticCode: "missing_refresh_token" }) ); expect(mocks.updateMany).toHaveBeenLastCalledWith( expect.objectContaining({ data: expect.objectContaining({ credentialsCiphertext: PREVIOUS_CIPHERTEXT, connectionStatus: "active" }) }) ); }); }); describe("cancelKwpOAuthConnectionAttempt", () => { it("restores the exact pre-reconnect credentials, health and owner", async () => { mockPendingBundle(RECONNECT_SELECTION_BUNDLE); await cancelKwpOAuthConnectionAttempt({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1", version: PENDING_VERSION.toISOString(), }); expect(mocks.updateMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ updatedAt: PENDING_VERSION, credentialsCiphertext: PENDING_CIPHERTEXT, healthMessageKey: "integrations.kwp.health.mandantSelectionRequired", }), data: expect.objectContaining({ credentialsCiphertext: PREVIOUS_CIPHERTEXT, connectionStatus: "active", tokenExpiresAt: new Date(PREVIOUS_CONNECTION.tokenExpiresAt), healthMessageKey: PREVIOUS_CONNECTION.healthMessageKey, lastHealthCheck: new Date(PREVIOUS_CONNECTION.lastHealthCheck), createdByUserId: PREVIOUS_CONNECTION.createdByUserId, updatedAt: expect.any(Date), }), }) ); expect(mocks.createAuditEvent).toHaveBeenCalledWith( expect.objectContaining({ action: "INTEGRATION_OAUTH_CANCEL", metadata: { appId: "kwp", restored: true } }) ); }); }); describe("loadKwpMandantSelection", () => { it("returns only the safe projection from the tenant/user-bound pending row", async () => { mockPendingBundle(); const view = await loadKwpMandantSelection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1" }); expect(view).toEqual({ connectionId: "connection-1", customerCode: "geerkens2", version: PENDING_VERSION.toISOString(), mode: "available", options: AVAILABLE_BUNDLE.discovery.options, }); expect(JSON.stringify(view)).not.toContain(ACCESS_TOKEN); expect(JSON.stringify(view)).not.toContain(REFRESH_TOKEN); expect(JSON.stringify(view)).not.toContain(CLIENT_SECRET); expect(mocks.findFirst).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ tenantId: "tenant-1", connectionStatus: "pending", createdByUserId: "user-1" }), }) ); }); it("clears an expired attempt with its exact ciphertext/version CAS", async () => { const expired = { ...AVAILABLE_BUNDLE, oauth: { ...OAUTH_CREDENTIALS, expiresAt: "2020-01-01T00:00:00.000Z" } }; mocks.findFirst.mockResolvedValue(pendingRow({ tokenExpiresAt: new Date(expired.oauth.expiresAt) })); mocks.decryptTenantField.mockResolvedValue(JSON.stringify(expired)); await expect(loadKwpMandantSelection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1" })).rejects.toEqual( expect.objectContaining>({ code: "selection_expired" }) ); expect(mocks.updateMany).toHaveBeenCalledWith({ where: expect.objectContaining({ connectionStatus: "pending", createdByUserId: "user-1", updatedAt: PENDING_VERSION, credentialsCiphertext: PENDING_CIPHERTEXT, }), data: expect.objectContaining({ credentialsCiphertext: null, tokenExpiresAt: null, connectionStatus: "pending" }), }); }); it("rejects an expired-attempt clear when its exact CAS generation was already replaced", async () => { const expired = { ...AVAILABLE_BUNDLE, oauth: { ...OAUTH_CREDENTIALS, expiresAt: "2020-01-01T00:00:00.000Z" } }; mocks.findFirst.mockResolvedValue(pendingRow({ tokenExpiresAt: new Date(expired.oauth.expiresAt) })); mocks.decryptTenantField.mockResolvedValue(JSON.stringify(expired)); mocks.updateMany.mockResolvedValueOnce({ count: 0 }); await expect(loadKwpMandantSelection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1" })).rejects.toEqual( expect.objectContaining>({ code: "invalid_state" }) ); }); it("rejects another identity and malformed encrypted state", async () => { mocks.context = { tenantId: "tenant-1", userId: "other-user" }; await expect(loadKwpMandantSelection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1" })).rejects.toEqual( expect.objectContaining>({ code: "invalid_state" }) ); expect(mocks.findFirst).not.toHaveBeenCalled(); mocks.context = { tenantId: "tenant-1", userId: "user-1" }; mockPendingBundle({ kind: "unexpected" }); await expect(loadKwpMandantSelection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1" })).rejects.toEqual( expect.objectContaining>({ code: "invalid_state" }) ); mockPendingBundle({ ...AVAILABLE_BUNDLE, discovery: { kind: "available", options: [{ id: 1, code: "bad/code", label: "Invalid" }] }, }); await expect(loadKwpMandantSelection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1" })).rejects.toEqual( expect.objectContaining>({ code: "invalid_state" }) ); }); }); describe("finalizeKwpOAuthConnection", () => { it("accepts only a discovered option, canonicalizes its label, and activates with the exact version fence", async () => { mockPendingBundle(); await expect( finalizeKwpOAuthConnection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1", version: PENDING_VERSION.toISOString(), mandantBinding: { kind: "selected", code: "002", label: "tampered browser label" }, }) ).resolves.toEqual({ connectionId: "connection-1" }); expect(mocks.activateOAuthConnection).toHaveBeenCalledWith({ tenantId: "tenant-1", connectionId: "connection-1", appId: "kwp", credentials: expect.objectContaining({ accessToken: ACCESS_TOKEN, mandantBinding: { kind: "selected", code: "002", label: "Geerkens Service GmbH" }, }), tokenExpiresAt: TOKEN_EXPIRES_AT, actorUserId: "user-1", expectedUpdatedAt: PENDING_VERSION, }); expect(mocks.createAuditEvent).toHaveBeenCalledWith( expect.objectContaining({ action: "INTEGRATION_KWP_MANDANT_BIND", metadata: { appId: "kwp", binding: "selected", source: "available" }, }) ); expect(JSON.stringify(mocks.createAuditEvent.mock.calls)).not.toContain("002"); }); it.each([{ kind: "none" } as const, { kind: "selected", code: "999" } as const])( "rejects a non-listed binding when discovery returned options", async (mandantBinding) => { mockPendingBundle(); await expect( finalizeKwpOAuthConnection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1", version: PENDING_VERSION.toISOString(), mandantBinding, }) ).rejects.toEqual(expect.objectContaining>({ code: "invalid_selection" })); expect(mocks.activateOAuthConnection).not.toHaveBeenCalled(); } ); it("allows an explicit none or validated manual code only in manual/empty mode", async () => { mockPendingBundle(MANUAL_BUNDLE); await finalizeKwpOAuthConnection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1", version: PENDING_VERSION.toISOString(), mandantBinding: { kind: "none" }, }); expect(mocks.activateOAuthConnection).toHaveBeenLastCalledWith( expect.objectContaining({ credentials: expect.objectContaining({ mandantBinding: { kind: "none" } }), }) ); mocks.activateOAuthConnection.mockClear(); mockPendingBundle(MANUAL_BUNDLE); await finalizeKwpOAuthConnection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1", version: PENDING_VERSION.toISOString(), mandantBinding: { kind: "selected", code: "007", label: "ignored" }, }); expect(mocks.activateOAuthConnection).toHaveBeenLastCalledWith( expect.objectContaining({ credentials: expect.objectContaining({ mandantBinding: { kind: "selected", code: "007" } }), }) ); mocks.activateOAuthConnection.mockClear(); mockPendingBundle(EMPTY_AVAILABLE_BUNDLE); await finalizeKwpOAuthConnection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1", version: PENDING_VERSION.toISOString(), mandantBinding: { kind: "none" }, }); expect(mocks.activateOAuthConnection).toHaveBeenLastCalledWith( expect.objectContaining({ credentials: expect.objectContaining({ mandantBinding: { kind: "none" } }) }) ); mocks.activateOAuthConnection.mockClear(); mockPendingBundle(EMPTY_AVAILABLE_BUNDLE); await finalizeKwpOAuthConnection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1", version: PENDING_VERSION.toISOString(), mandantBinding: { kind: "selected", code: "008" }, }); expect(mocks.activateOAuthConnection).toHaveBeenLastCalledWith( expect.objectContaining({ credentials: expect.objectContaining({ mandantBinding: { kind: "selected", code: "008" } }) }) ); }); it("rejects malformed manual codes and stale versions before provider I/O", async () => { mockPendingBundle(MANUAL_BUNDLE); await expect( finalizeKwpOAuthConnection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1", version: PENDING_VERSION.toISOString(), mandantBinding: { kind: "selected", code: "bad/code" }, }) ).rejects.toEqual(expect.objectContaining>({ code: "invalid_selection" })); mocks.findFirst.mockResolvedValueOnce(null); await expect( finalizeKwpOAuthConnection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1", version: PENDING_VERSION.toISOString(), mandantBinding: { kind: "none" }, }) ).rejects.toEqual(expect.objectContaining>({ code: "invalid_state" })); expect(mocks.activateOAuthConnection).not.toHaveBeenCalled(); }); it("clears the exact pending attempt and reports selection_expired after a Health 401", async () => { mockPendingBundle(MANUAL_BUNDLE); mocks.activateOAuthConnection.mockRejectedValueOnce(new mocks.AppsEngineServiceError("integrations.kwp.health.tokenInvalid")); await expect( finalizeKwpOAuthConnection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1", version: PENDING_VERSION.toISOString(), mandantBinding: { kind: "none" }, }) ).rejects.toEqual(expect.objectContaining>({ code: "selection_expired" })); expect(mocks.updateMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ updatedAt: PENDING_VERSION, credentialsCiphertext: PENDING_CIPHERTEXT }), data: expect.objectContaining({ credentialsCiphertext: null, tokenExpiresAt: null, connectionStatus: "pending" }), }) ); }); it("rejects a Health-401 clear when another attempt already won the CAS", async () => { mockPendingBundle(MANUAL_BUNDLE); mocks.activateOAuthConnection.mockRejectedValueOnce(new mocks.AppsEngineServiceError("integrations.kwp.health.tokenInvalid")); mocks.updateMany.mockResolvedValueOnce({ count: 0 }); await expect( finalizeKwpOAuthConnection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1", version: PENDING_VERSION.toISOString(), mandantBinding: { kind: "none" }, }) ).rejects.toEqual(expect.objectContaining>({ code: "invalid_state" })); }); it("maps a lost activation CAS after the Health probe to invalid_state", async () => { mockPendingBundle(MANUAL_BUNDLE); mocks.activateOAuthConnection.mockRejectedValueOnce(new CatalogError(foundationErrors.notFound, { connectionId: "connection-1" })); await expect( finalizeKwpOAuthConnection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1", version: PENDING_VERSION.toISOString(), mandantBinding: { kind: "none" }, }) ).rejects.toEqual(expect.objectContaining>({ code: "invalid_state" })); expect(mocks.updateMany).not.toHaveBeenCalled(); }); it("keeps the pending attempt on non-401 provider rejection", async () => { mockPendingBundle(MANUAL_BUNDLE); mocks.activateOAuthConnection.mockRejectedValueOnce(new mocks.AppsEngineServiceError("integrations.kwp.health.scopeMissing")); await expect( finalizeKwpOAuthConnection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1", version: PENDING_VERSION.toISOString(), mandantBinding: { kind: "none" }, }) ).rejects.toEqual(expect.objectContaining>({ code: "connection_failed", diagnosticCode: "health_scope_missing" })); expect(mocks.updateMany).not.toHaveBeenCalled(); }); it("keeps an unexpectedly unconfirmed Health result pending with a stable diagnostic", async () => { mockPendingBundle(MANUAL_BUNDLE); mocks.activateOAuthConnection.mockRejectedValueOnce(new mocks.AppsEngineServiceError("integrations.kwp.health.mandantUnconfirmed")); await expect( finalizeKwpOAuthConnection({ tenantId: "tenant-1", userId: "user-1", connectionId: "connection-1", version: PENDING_VERSION.toISOString(), mandantBinding: { kind: "none" }, }) ).rejects.toEqual(expect.objectContaining>({ code: "connection_failed", diagnosticCode: "health_mandant_unconfirmed" })); expect(mocks.updateMany).not.toHaveBeenCalled(); }); });