forked from EduCraft/curriculum-project-hub
215 lines
8.1 KiB
TypeScript
215 lines
8.1 KiB
TypeScript
import Fastify from "fastify";
|
|
import { afterAll, beforeEach, describe, expect, it } from "vitest";
|
|
import { registerAdminPlugin } from "../../src/admin/plugin.js";
|
|
import { ProviderConnectionService } from "../../src/connections/providerConnections.js";
|
|
import { mintSessionToken, sessionCookieHeader } from "../../src/admin/routes/authRoutes.js";
|
|
import { DEFAULT_ORG_ID, prisma, resetDb, testSecretEnvelope } from "./helpers.js";
|
|
|
|
const SESSION_SECRET = "integration-test-session-secret";
|
|
const secrets = testSecretEnvelope;
|
|
|
|
async function buildApp() {
|
|
const app = Fastify({ logger: false });
|
|
await registerAdminPlugin(app, {
|
|
prisma,
|
|
sessionSecret: SESSION_SECRET,
|
|
publicBaseUrl: "http://127.0.0.1:8788",
|
|
feishuAppId: "cli_test",
|
|
feishuAppSecret: "secret_test",
|
|
projectWorkspaceRoot: "/tmp/cph-test-workspaces",
|
|
secretEnvelope: secrets,
|
|
providerReadinessProbe: async () => {},
|
|
cookieSecure: false,
|
|
});
|
|
await app.ready();
|
|
return app;
|
|
}
|
|
|
|
async function seedUser(role: "OWNER" | "ADMIN" | "MEMBER"): Promise<string> {
|
|
const id = `u-${role.toLowerCase()}`;
|
|
const openId = `ou_${role.toLowerCase()}`;
|
|
await prisma.user.create({ data: { id, feishuOpenId: openId, displayName: id } });
|
|
await prisma.organizationMembership.create({
|
|
data: { organizationId: DEFAULT_ORG_ID, userId: id, role },
|
|
});
|
|
return mintSessionToken({ userId: id, feishuOpenId: openId }, SESSION_SECRET);
|
|
}
|
|
|
|
describe("Organization BYOK provider connections", () => {
|
|
beforeEach(async () => {
|
|
await resetDb();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await prisma.$disconnect();
|
|
});
|
|
|
|
it("stores write-only credentials in an encrypted immutable version", async () => {
|
|
const token = await seedUser("OWNER");
|
|
const app = await buildApp();
|
|
const credential = {
|
|
baseUrl: "https://provider.example/api",
|
|
authToken: "org-a-auth-token-never-return",
|
|
anthropicApiKey: "org-a-api-key-never-return",
|
|
};
|
|
try {
|
|
const response = await app.inject({
|
|
method: "PUT",
|
|
url: "/api/org/test-default/provider-connections/openrouter",
|
|
headers: {
|
|
cookie: sessionCookieHeader(token),
|
|
"content-type": "application/json",
|
|
},
|
|
payload: credential,
|
|
});
|
|
|
|
expect(response.statusCode).toBe(201);
|
|
expect(response.json()).toMatchObject({
|
|
providerId: "openrouter",
|
|
mode: "BYOK",
|
|
status: "ACTIVE",
|
|
activeVersion: 1,
|
|
keyId: "test-active",
|
|
});
|
|
expect(response.body).not.toContain(credential.authToken);
|
|
expect(response.body).not.toContain(credential.anthropicApiKey);
|
|
expect(response.body).not.toContain(credential.baseUrl);
|
|
|
|
const persisted = await prisma.$queryRaw<Array<{
|
|
envelope: unknown;
|
|
activeSecretVersionId: string;
|
|
}>>`
|
|
SELECT v."envelope", c."activeSecretVersionId"
|
|
FROM "ProviderCredentialVersion" v
|
|
JOIN "OrganizationProviderConnection" c ON c."id" = v."connectionId"
|
|
WHERE c."organizationId" = ${DEFAULT_ORG_ID} AND c."providerId" = 'openrouter'
|
|
`;
|
|
expect(persisted).toHaveLength(1);
|
|
const serialized = JSON.stringify(persisted);
|
|
expect(serialized).not.toContain(credential.authToken);
|
|
expect(serialized).not.toContain(credential.anthropicApiKey);
|
|
expect(serialized).not.toContain(credential.baseUrl);
|
|
expect(persisted[0]?.activeSecretVersionId).toBeTruthy();
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|
|
|
|
it("rotates by appending a version and never exposes old or new plaintext", async () => {
|
|
const token = await seedUser("ADMIN");
|
|
const app = await buildApp();
|
|
const cookie = sessionCookieHeader(token);
|
|
try {
|
|
const first = await app.inject({
|
|
method: "PUT",
|
|
url: "/api/org/test-default/provider-connections/openrouter",
|
|
headers: { cookie, "content-type": "application/json" },
|
|
payload: { baseUrl: "https://one.example", authToken: "first-secret" },
|
|
});
|
|
const second = await app.inject({
|
|
method: "PUT",
|
|
url: "/api/org/test-default/provider-connections/openrouter",
|
|
headers: { cookie, "content-type": "application/json" },
|
|
payload: { baseUrl: "https://two.example", authToken: "second-secret" },
|
|
});
|
|
|
|
expect(first.statusCode).toBe(201);
|
|
expect(second.statusCode).toBe(200);
|
|
expect(second.json()).toMatchObject({ activeVersion: 2, status: "ACTIVE" });
|
|
const versions = await prisma.$queryRaw<Array<{ version: number; retiredAt: Date | null }>>`
|
|
SELECT v."version", v."retiredAt"
|
|
FROM "ProviderCredentialVersion" v
|
|
JOIN "OrganizationProviderConnection" c ON c."id" = v."connectionId"
|
|
WHERE c."organizationId" = ${DEFAULT_ORG_ID}
|
|
ORDER BY v."version"
|
|
`;
|
|
expect(versions).toEqual([
|
|
{ version: 1, retiredAt: expect.any(Date) },
|
|
{ version: 2, retiredAt: null },
|
|
]);
|
|
|
|
const read = await app.inject({
|
|
method: "GET",
|
|
url: "/api/org/test-default/provider-connections",
|
|
headers: { cookie },
|
|
});
|
|
expect(read.statusCode).toBe(200);
|
|
expect(read.json()).toEqual({ connections: [expect.objectContaining({ activeVersion: 2 })] });
|
|
expect(read.body).not.toContain("first-secret");
|
|
expect(read.body).not.toContain("second-secret");
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|
|
|
|
it("rejects members and refuses to overwrite a platform-managed connection", async () => {
|
|
const memberToken = await seedUser("MEMBER");
|
|
const app = await buildApp();
|
|
try {
|
|
const denied = await app.inject({
|
|
method: "PUT",
|
|
url: "/api/org/test-default/provider-connections/openrouter",
|
|
headers: {
|
|
cookie: sessionCookieHeader(memberToken),
|
|
"content-type": "application/json",
|
|
},
|
|
payload: { baseUrl: "https://provider.example", authToken: "denied-secret" },
|
|
});
|
|
expect(denied.statusCode).toBe(403);
|
|
const service = new ProviderConnectionService(prisma, secrets, async () => {});
|
|
await expect(service.rotateByok({
|
|
organizationId: DEFAULT_ORG_ID,
|
|
providerId: "another-provider",
|
|
actorUserId: "u-member",
|
|
baseUrl: "https://provider.example",
|
|
authToken: "service-bypass-secret",
|
|
})).rejects.toThrow("requires an active Organization OWNER or ADMIN");
|
|
await expect(prisma.organizationProviderConnection.count({
|
|
where: { organizationId: DEFAULT_ORG_ID, providerId: "another-provider" },
|
|
})).resolves.toBe(0);
|
|
|
|
await prisma.$executeRaw`
|
|
INSERT INTO "OrganizationProviderConnection"
|
|
("id", "organizationId", "providerId", "mode", "status", "createdAt", "updatedAt")
|
|
VALUES
|
|
('platform-connection', ${DEFAULT_ORG_ID}, 'openrouter', 'PLATFORM_MANAGED', 'DRAFT', NOW(), NOW())
|
|
`;
|
|
const ownerToken = await seedUser("OWNER");
|
|
const refused = await app.inject({
|
|
method: "PUT",
|
|
url: "/api/org/test-default/provider-connections/openrouter",
|
|
headers: {
|
|
cookie: sessionCookieHeader(ownerToken),
|
|
"content-type": "application/json",
|
|
},
|
|
payload: { baseUrl: "https://provider.example", authToken: "takeover-secret" },
|
|
});
|
|
expect(refused.statusCode).toBe(403);
|
|
expect(refused.json()).toMatchObject({
|
|
error: { code: "forbidden" },
|
|
});
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|
|
|
|
it("does not activate or persist a credential that fails readiness", async () => {
|
|
await seedUser("ADMIN");
|
|
const service = new ProviderConnectionService(prisma, secrets, async () => {
|
|
throw new Error("provider credential readiness check failed: status 401");
|
|
});
|
|
|
|
await expect(service.rotateByok({
|
|
organizationId: DEFAULT_ORG_ID,
|
|
providerId: "openrouter",
|
|
actorUserId: "u-admin",
|
|
baseUrl: "https://provider.example/api",
|
|
authToken: "rejected-secret",
|
|
})).rejects.toThrow("readiness check failed");
|
|
await expect(prisma.organizationProviderConnection.count({
|
|
where: { organizationId: DEFAULT_ORG_ID, providerId: "openrouter" },
|
|
})).resolves.toBe(0);
|
|
await expect(prisma.providerCredentialVersion.count()).resolves.toBe(0);
|
|
});
|
|
});
|