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: { projectId: "project-rebound", chatId: "chat-rebound" }, }); 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 { 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(), }, }); }