Files
curriculum-project-hub/hub/test/integration/feishu-binding-lifecycle.test.ts
T

182 lines
6.3 KiB
TypeScript

import { afterAll, beforeEach, describe, expect, it } from "vitest";
import { archiveFeishuBindingForLifecycleEvent } from "../../src/feishu/bindingLifecycle.js";
import { scopedFeishuPrincipalId } from "../../src/feishu/identityNamespace.js";
import { DEFAULT_ORG_ID, prisma, resetDb, seedProject } from "./helpers.js";
const CONNECTION_ID = "feishu-connection-lifecycle";
describe("Feishu binding lifecycle events", () => {
beforeEach(async () => {
await resetDb();
});
afterAll(async () => prisma.$disconnect());
it("archives a bound project and revokes its chat grant when the chat is dissolved", async () => {
await seedProject("project-dissolved", "chat-dissolved");
await seedFeishuConnection();
await prisma.permissionGrant.create({
data: {
resourceType: "PROJECT",
resourceId: "project-dissolved",
principalType: "FEISHU_CHAT",
principalId: scopedFeishuPrincipalId("CHAT", CONNECTION_ID, "chat-dissolved"),
role: "EDIT",
},
});
await prisma.permissionGrant.create({
data: {
resourceType: "PROJECT",
resourceId: "project-dissolved",
principalType: "FEISHU_CHAT",
principalId: "chat-dissolved",
role: "EDIT",
},
});
await prisma.permissionGrant.create({
data: {
resourceType: "PROJECT",
resourceId: "project-dissolved",
principalType: "FEISHU_CHAT",
principalId: scopedFeishuPrincipalId("CHAT", CONNECTION_ID, "chat-unrelated"),
role: "EDIT",
},
});
await expect(archiveFeishuBindingForLifecycleEvent(prisma, {
chatId: "chat-dissolved",
eventId: "event-dissolved",
reason: "chat_dissolved",
})).resolves.toEqual({ archived: true, projectId: "project-dissolved" });
await expect(prisma.projectGroupBinding.findFirst({
where: { chatId: "chat-dissolved" },
select: { archivedAt: true },
})).resolves.toMatchObject({ archivedAt: expect.any(Date) });
await expect(prisma.permissionGrant.findFirst({
where: {
resourceId: "project-dissolved",
principalId: scopedFeishuPrincipalId("CHAT", CONNECTION_ID, "chat-dissolved"),
},
select: { revokedAt: true },
})).resolves.toMatchObject({ revokedAt: expect.any(Date) });
await expect(prisma.permissionGrant.findFirst({
where: { resourceId: "project-dissolved", principalId: "chat-dissolved" },
select: { revokedAt: true },
})).resolves.toMatchObject({ revokedAt: expect.any(Date) });
await expect(prisma.permissionGrant.findFirst({
where: {
resourceId: "project-dissolved",
principalId: scopedFeishuPrincipalId("CHAT", CONNECTION_ID, "chat-unrelated"),
},
select: { revokedAt: true },
})).resolves.toEqual({ revokedAt: null });
await expect(prisma.auditEntry.findFirst({
where: { projectId: "project-dissolved", action: "project.chat_binding_archived" },
select: { organizationId: true, metadata: true },
})).resolves.toEqual({
organizationId: DEFAULT_ORG_ID,
metadata: { chatId: "chat-dissolved", eventId: "event-dissolved", reason: "chat_dissolved" },
});
});
it("is idempotent when Feishu redelivers a lifecycle event", async () => {
await seedProject("project-redelivery", "chat-redelivery");
await seedFeishuConnection();
await archiveFeishuBindingForLifecycleEvent(prisma, {
chatId: "chat-redelivery",
eventId: "event-redelivery",
reason: "bot_removed",
});
await expect(archiveFeishuBindingForLifecycleEvent(prisma, {
chatId: "chat-redelivery",
eventId: "event-redelivery",
reason: "bot_removed",
})).resolves.toEqual({ archived: false });
await expect(prisma.auditEntry.count({
where: { projectId: "project-redelivery", action: "project.chat_binding_archived" },
})).resolves.toBe(1);
});
it("does not archive a later binding when an old lifecycle event is redelivered", async () => {
await seedProject("project-original", "chat-rebound");
await prisma.project.create({
data: {
id: "project-rebound",
organizationId: DEFAULT_ORG_ID,
name: "Rebound project",
workspaceDir: "/tmp/test-project-rebound",
},
});
await seedFeishuConnection();
await archiveFeishuBindingForLifecycleEvent(prisma, {
chatId: "chat-rebound",
eventId: "event-before-rebind",
reason: "bot_removed",
});
await prisma.projectGroupBinding.create({
data: {
organizationId: DEFAULT_ORG_ID,
projectId: "project-rebound",
chatId: "chat-rebound",
selectedAgentRoleId: `agent_role_draft_${DEFAULT_ORG_ID}`,
},
});
await expect(archiveFeishuBindingForLifecycleEvent(prisma, {
chatId: "chat-rebound",
eventId: "event-before-rebind",
reason: "bot_removed",
})).resolves.toEqual({ archived: false });
await expect(prisma.projectGroupBinding.findFirst({
where: { projectId: "project-rebound", archivedAt: null },
select: { id: true },
})).resolves.not.toBeNull();
});
it("leaves unrelated active bindings untouched", async () => {
await seedProject("project-kept", "chat-kept");
await expect(archiveFeishuBindingForLifecycleEvent(prisma, {
chatId: "chat-unknown",
eventId: "event-unknown",
reason: "chat_dissolved",
})).resolves.toEqual({ archived: false });
await expect(prisma.projectGroupBinding.findFirst({
where: { chatId: "chat-kept", archivedAt: null },
select: { id: true },
})).resolves.not.toBeNull();
});
});
async function seedFeishuConnection(): Promise<void> {
await prisma.organizationFeishuApplicationConnection.create({
data: {
id: CONNECTION_ID,
organizationId: DEFAULT_ORG_ID,
appIdentityFingerprint: "fingerprint-lifecycle",
},
});
await prisma.feishuApplicationCredentialVersion.create({
data: {
id: "feishu-secret-version-lifecycle",
connectionId: CONNECTION_ID,
version: 1,
keyId: "test-active",
envelope: { fixture: true },
},
});
await prisma.organizationFeishuApplicationConnection.update({
where: { id: CONNECTION_ID },
data: {
status: "ACTIVE",
activeSecretVersionId: "feishu-secret-version-lifecycle",
activatedAt: new Date(),
},
});
}