import express from "express"; import request from "supertest"; import { beforeEach, describe, expect, it, vi } from "vitest"; const mockStorage = vi.hoisted(() => ({ headObject: vi.fn(), })); function registerModuleMocks() { vi.doMock("../storage/index.js", () => ({ getStorageService: () => mockStorage, })); } function createSelectChain(rows: unknown[]) { const query = { then(resolve: (value: unknown[]) => unknown) { return Promise.resolve(rows).then(resolve); }, leftJoin() { return query; }, orderBy() { return query; }, where() { return query; }, }; return { from() { return query; }, }; } function createDbStub(...selectResponses: unknown[][]) { let selectCall = 0; return { select() { const rows = selectResponses[selectCall] ?? []; selectCall += 1; return createSelectChain(rows); }, }; } async function createApp( db: Record, actor: Record = { type: "anon" }, ) { const [{ accessRoutes }, { errorHandler }] = await Promise.all([ vi.importActual("../routes/access.js"), vi.importActual("../middleware/index.js"), ]); const app = express(); app.use((req, _res, next) => { (req as any).actor = actor; next(); }); app.use( "/api", accessRoutes(db as any, { deploymentMode: "local_trusted", deploymentExposure: "private", bindHost: "127.0.0.1", allowedHostnames: [], }), ); app.use(errorHandler); return app; } describe("GET /invites/:token", () => { beforeEach(() => { vi.resetModules(); vi.doUnmock("../storage/index.js"); vi.doUnmock("../routes/access.js"); vi.doUnmock("../middleware/index.js"); registerModuleMocks(); mockStorage.headObject.mockReset(); mockStorage.headObject.mockResolvedValue({ exists: true, contentLength: 3, contentType: "image/png" }); }); it("returns company branding in the invite summary response", async () => { const invite = { id: "invite-1", companyId: "company-1", inviteType: "company_join", allowedJoinTypes: "human", tokenHash: "hash", defaultsPayload: null, expiresAt: new Date("2027-03-07T00:10:00.000Z"), invitedByUserId: null, revokedAt: null, acceptedAt: null, createdAt: new Date("2026-03-07T00:00:00.000Z"), updatedAt: new Date("2026-03-07T00:00:00.000Z"), }; const app = await createApp( createDbStub( [invite], [ { name: "Acme Robotics", brandColor: "#114488", logoAssetId: "logo-1", }, ], [ { companyId: "company-1", objectKey: "company-1/assets/companies/logo-1", contentType: "image/png", byteSize: 3, originalFilename: "logo.png", }, ], ), ); const res = await request(app).get("/api/invites/pcp_invite_test"); expect(res.status).toBe(200); expect(res.body.companyId).toBe("company-1"); expect(res.body.companyName).toBe("Acme Robotics"); expect(res.body.companyBrandColor).toBe("#114488"); expect(res.body.companyLogoUrl).toBe("/api/invites/pcp_invite_test/logo"); expect(res.body.inviteType).toBe("company_join"); }, 10_000); it("omits companyLogoUrl when the stored logo object is missing", async () => { mockStorage.headObject.mockResolvedValue({ exists: false }); const invite = { id: "invite-1", companyId: "company-1", inviteType: "company_join", allowedJoinTypes: "human", tokenHash: "hash", defaultsPayload: null, expiresAt: new Date("2027-03-07T00:10:00.000Z"), invitedByUserId: null, revokedAt: null, acceptedAt: null, createdAt: new Date("2026-03-07T00:00:00.000Z"), updatedAt: new Date("2026-03-07T00:00:00.000Z"), }; const app = await createApp( createDbStub( [invite], [ { name: "Acme Robotics", brandColor: "#114488", logoAssetId: "logo-1", }, ], [ { companyId: "company-1", objectKey: "company-1/assets/companies/logo-1", contentType: "image/png", byteSize: 3, originalFilename: "logo.png", }, ], ), ); const res = await request(app).get("/api/invites/pcp_invite_test"); expect(res.status).toBe(200); expect(res.body.companyLogoUrl).toBeNull(); }, 10_000); it("returns pending join-request status for an already-accepted invite", async () => { const invite = { id: "invite-1", companyId: "company-1", inviteType: "company_join", allowedJoinTypes: "human", tokenHash: "hash", defaultsPayload: null, expiresAt: new Date("2027-03-07T00:10:00.000Z"), invitedByUserId: null, revokedAt: null, acceptedAt: new Date("2026-03-07T00:05:00.000Z"), createdAt: new Date("2026-03-07T00:00:00.000Z"), updatedAt: new Date("2026-03-07T00:05:00.000Z"), }; const app = await createApp( createDbStub( [invite], [{ requestType: "human", status: "pending_approval" }], [ { name: "Acme Robotics", brandColor: "#114488", logoAssetId: "logo-1", }, ], [ { companyId: "company-1", objectKey: "company-1/assets/companies/logo-1", contentType: "image/png", byteSize: 3, originalFilename: "logo.png", }, ], ), ); const res = await request(app).get("/api/invites/pcp_invite_test"); expect(res.status).toBe(200); expect(res.body.joinRequestStatus).toBe("pending_approval"); expect(res.body.joinRequestType).toBe("human"); expect(res.body.companyName).toBe("Acme Robotics"); }, 10_000); it("falls back to a reusable human join request when the accepted invite reused an existing queue entry", async () => { const invite = { id: "invite-2", companyId: "company-1", inviteType: "company_join", allowedJoinTypes: "human", tokenHash: "hash", defaultsPayload: null, expiresAt: new Date("2027-03-07T00:10:00.000Z"), invitedByUserId: null, revokedAt: null, acceptedAt: new Date("2026-03-07T00:05:00.000Z"), createdAt: new Date("2026-03-07T00:00:00.000Z"), updatedAt: new Date("2026-03-07T00:05:00.000Z"), }; const reusableJoinRequest = { id: "join-1", requestType: "human", status: "pending_approval", requestingUserId: "user-1", requestEmailSnapshot: "jane@example.com", }; const companyBranding = { name: "Acme Robotics", brandColor: "#114488", logoAssetId: "logo-1", }; const logoAsset = { companyId: "company-1", objectKey: "company-1/assets/companies/logo-1", contentType: "image/png", byteSize: 3, originalFilename: "logo.png", }; const app = await createApp( createDbStub( [invite], [], [{ email: "jane@example.com" }], [reusableJoinRequest], [reusableJoinRequest], [companyBranding], [companyBranding], [logoAsset], [logoAsset], ), { type: "board", userId: "user-1", source: "session" }, ); const res = await request(app).get("/api/invites/pcp_invite_test"); expect(res.status).toBe(200); expect(res.body.joinRequestStatus).toBe("pending_approval"); expect(res.body.joinRequestType).toBe("human"); }, 10_000); });