forked from EduCraft/curriculum-project-hub
133 lines
5.1 KiB
TypeScript
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}`,
|
|
});
|
|
}
|