Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/modules/kwp/services/KwpConnectionService.server.test.ts
T

833 lines
35 KiB
TypeScript

// @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<typeof import("@hivegrid/integrations")>()),
KwpError: mocks.KwpError,
discoverKwpMandants: mocks.discoverKwpMandants,
}));
vi.mock("~/utils/db/.server/tenantRls.server", () => ({
withTenantRLS: (_tenantId: string, callback: (tx: unknown) => Promise<unknown>) =>
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<string, unknown> = {}) {
return {
id: "connection-1",
updatedAt: PENDING_VERSION,
credentialsCiphertext: PENDING_CIPHERTEXT,
tokenExpiresAt: TOKEN_EXPIRES_AT,
healthMessageKey: "integrations.kwp.health.mandantSelectionRequired",
...overrides,
};
}
function reservationRow(overrides: Record<string, unknown> = {}) {
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<Partial<KwpConnectError>>({ code: "not_configured" })
);
await expect(beginKwpOAuthConnection({ tenantId: "tenant-1", tenantSlug: "acme", userId: "user-1", customerCode: "bad/code" })).rejects.toEqual(
expect.objectContaining<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ 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<Partial<KwpConnectError>>({ code: "connection_failed", diagnosticCode: "health_mandant_unconfirmed" }));
expect(mocks.updateMany).not.toHaveBeenCalled();
});
});