Files
curriculum-project-hub/hub/test/integration/feishu-identity-namespace.test.ts
T

133 lines
5.1 KiB
TypeScript

import { afterAll, beforeEach, describe, expect, it } from "vitest";
import { FeishuApplicationConnectionService } from "../../src/connections/feishuApplicationConnections.js";
import {
scopedFeishuPrincipalId,
upsertScopedFeishuIdentity,
resolveScopedFeishuIdentity,
} from "../../src/feishu/identityNamespace.js";
import { DEFAULT_ORG_ID, prisma, resetDb, seedTestOrganization, testSecretEnvelope } from "./helpers.js";
describe("Feishu connection identity namespace", () => {
beforeEach(async () => {
await resetDb();
});
afterAll(async () => {
await prisma.$disconnect();
});
it("keeps colliding provider-local user and chat ids isolated by connection", async () => {
const first = await seedConnection(DEFAULT_ORG_ID, "identity-admin-a", "cli_identity_a");
await seedTestOrganization("org_identity_other", "identity-other");
const second = await seedConnection("org_identity_other", "identity-admin-b", "cli_identity_b");
const [identityA, identityB] = await Promise.all([
upsertScopedFeishuIdentity(prisma, {
connectionId: first.id,
openId: "ou_collision",
displayName: "Teacher A",
}),
upsertScopedFeishuIdentity(prisma, {
connectionId: second.id,
openId: "ou_collision",
displayName: "Teacher B",
}),
]);
expect(identityA.userId).not.toBe(identityB.userId);
expect(identityA.principalId).not.toBe(identityB.principalId);
expect(scopedFeishuPrincipalId("CHAT", first.id, "oc_collision"))
.not.toBe(scopedFeishuPrincipalId("CHAT", second.id, "oc_collision"));
await expect(resolveScopedFeishuIdentity(prisma, {
connectionId: first.id,
openId: "ou_collision",
})).resolves.toMatchObject({ organizationId: DEFAULT_ORG_ID, userId: identityA.userId });
await expect(resolveScopedFeishuIdentity(prisma, {
connectionId: second.id,
openId: "ou_collision",
})).resolves.toMatchObject({ organizationId: "org_identity_other", userId: identityB.userId });
});
it("fails closed for an Organization mismatch or disabled connection", async () => {
const connection = await seedConnection(DEFAULT_ORG_ID, "identity-owner", "cli_identity");
await upsertScopedFeishuIdentity(prisma, {
connectionId: connection.id,
openId: "ou_teacher",
displayName: "Teacher",
});
await expect(resolveScopedFeishuIdentity(prisma, {
connectionId: connection.id,
expectedOrganizationId: "org_wrong",
openId: "ou_teacher",
})).rejects.toThrow("scope mismatch");
await new FeishuApplicationConnectionService(prisma, testSecretEnvelope, async () => {}).disable({
organizationId: DEFAULT_ORG_ID,
actorUserId: "identity-owner",
});
await expect(resolveScopedFeishuIdentity(prisma, {
connectionId: connection.id,
openId: "ou_teacher",
})).rejects.toThrow("active Feishu Application Connection not found");
});
it("serializes identity creation behind a concurrent connection disable", async () => {
const connection = await seedConnection(DEFAULT_ORG_ID, "identity-owner", "cli_race");
let release!: () => void;
const releasePromise = new Promise<void>((resolve) => { release = resolve; });
let locked!: () => void;
const lockedPromise = new Promise<void>((resolve) => { locked = resolve; });
const disabler = prisma.$transaction(async (tx) => {
await tx.$queryRaw`
SELECT "id" FROM "OrganizationFeishuApplicationConnection"
WHERE "id" = ${connection.id} FOR UPDATE
`;
locked();
await releasePromise;
await tx.organizationFeishuApplicationConnection.update({
where: { id: connection.id },
data: { status: "DISABLED", disabledAt: new Date() },
});
}, { timeout: 10_000 });
await lockedPromise;
const identityAttempt = upsertScopedFeishuIdentity(prisma, {
connectionId: connection.id,
openId: "ou_racing_user",
displayName: "Racing User",
});
const stateBeforeRelease = await Promise.race([
identityAttempt.then(() => "settled", () => "settled"),
new Promise<"blocked">((resolve) => setTimeout(() => resolve("blocked"), 50)),
]);
expect(stateBeforeRelease).toBe("blocked");
release();
await disabler;
await expect(identityAttempt).rejects.toThrow("active Feishu Application Connection not found");
await expect(prisma.feishuUserIdentity.count({
where: { connectionId: connection.id, openId: "ou_racing_user" },
})).resolves.toBe(0);
});
});
async function seedConnection(
organizationId: string,
actorUserId: string,
appId: string,
): Promise<{ readonly id: string }> {
await prisma.user.create({
data: { id: actorUserId, feishuOpenId: `legacy_${actorUserId}`, displayName: actorUserId },
});
await prisma.organizationMembership.create({
data: { organizationId, userId: actorUserId, role: "OWNER" },
});
return new FeishuApplicationConnectionService(prisma, testSecretEnvelope, async () => {})
.rotateCustomerApplication({
organizationId,
actorUserId,
appId,
appSecret: `secret_${appId}`,
botOpenId: `ou_bot_${appId}`,
});
}