import express from "express"; import request from "supertest"; import { beforeEach, describe, expect, it, vi } from "vitest"; const mockRegistry = vi.hoisted(() => ({ getById: vi.fn(), getByKey: vi.fn(), })); const mockLifecycle = vi.hoisted(() => ({ load: vi.fn(), upgrade: vi.fn(), })); vi.mock("../services/plugin-registry.js", () => ({ pluginRegistryService: () => mockRegistry, })); vi.mock("../services/plugin-lifecycle.js", () => ({ pluginLifecycleManager: () => mockLifecycle, })); vi.mock("../services/activity-log.js", () => ({ logActivity: vi.fn(), })); vi.mock("../services/live-events.js", () => ({ publishGlobalLiveEvent: vi.fn(), })); async function createApp(actor: Record, loaderOverrides: Record = {}) { const [{ pluginRoutes }, { errorHandler }] = await Promise.all([ import("../routes/plugins.js"), import("../middleware/index.js"), ]); const loader = { installPlugin: vi.fn(), ...loaderOverrides, }; const app = express(); app.use(express.json()); app.use((req, _res, next) => { req.actor = actor as typeof req.actor; next(); }); app.use("/api", pluginRoutes({} as never, loader as never)); app.use(errorHandler); return { app, loader }; } describe("plugin install and upgrade authz", () => { beforeEach(() => { vi.resetAllMocks(); }); it("rejects plugin installation for non-admin board users", async () => { const { app, loader } = await createApp({ type: "board", userId: "user-1", source: "session", isInstanceAdmin: false, companyIds: ["company-1"], }); const res = await request(app) .post("/api/plugins/install") .send({ packageName: "paperclip-plugin-example" }); expect(res.status).toBe(403); expect(loader.installPlugin).not.toHaveBeenCalled(); }, 20_000); it("allows instance admins to install plugins", async () => { const pluginId = "11111111-1111-4111-8111-111111111111"; const pluginKey = "paperclip.example"; const discovered = { manifest: { id: pluginKey, }, }; mockRegistry.getByKey.mockResolvedValue({ id: pluginId, pluginKey, packageName: "paperclip-plugin-example", version: "1.0.0", }); mockRegistry.getById.mockResolvedValue({ id: pluginId, pluginKey, packageName: "paperclip-plugin-example", version: "1.0.0", }); mockLifecycle.load.mockResolvedValue(undefined); const { app, loader } = await createApp( { type: "board", userId: "admin-1", source: "session", isInstanceAdmin: true, companyIds: [], }, { installPlugin: vi.fn().mockResolvedValue(discovered) }, ); const res = await request(app) .post("/api/plugins/install") .send({ packageName: "paperclip-plugin-example" }); expect(res.status).toBe(200); expect(loader.installPlugin).toHaveBeenCalledWith({ packageName: "paperclip-plugin-example", version: undefined, }); expect(mockLifecycle.load).toHaveBeenCalledWith(pluginId); }, 20_000); it("rejects plugin upgrades for non-admin board users", async () => { const pluginId = "11111111-1111-4111-8111-111111111111"; const { app } = await createApp({ type: "board", userId: "user-1", source: "session", isInstanceAdmin: false, companyIds: ["company-1"], }); const res = await request(app) .post(`/api/plugins/${pluginId}/upgrade`) .send({}); expect(res.status).toBe(403); expect(mockRegistry.getById).not.toHaveBeenCalled(); expect(mockLifecycle.upgrade).not.toHaveBeenCalled(); }, 20_000); it("allows instance admins to upgrade plugins", async () => { const pluginId = "11111111-1111-4111-8111-111111111111"; mockRegistry.getById.mockResolvedValue({ id: pluginId, pluginKey: "paperclip.example", version: "1.0.0", }); mockLifecycle.upgrade.mockResolvedValue({ id: pluginId, version: "1.1.0", }); const { app } = await createApp({ type: "board", userId: "admin-1", source: "session", isInstanceAdmin: true, companyIds: [], }); const res = await request(app) .post(`/api/plugins/${pluginId}/upgrade`) .send({ version: "1.1.0" }); expect(res.status).toBe(200); expect(mockLifecycle.upgrade).toHaveBeenCalledWith(pluginId, "1.1.0"); }, 20_000); });