mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 20:21:50 +01:00
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 4.1.11 to 5.0.3. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/vitest-dev/vitest/releases">vitest's releases</a>.</em></p> <blockquote> <h2>v5.0.3</h2> <h3> 🐞 Bug Fixes</h3> <ul> <li>Isolate <code>result.status</code> between <code>repeats</code> runs - by <a href="https://github.com/hi-ogawa"><code>@hi-ogawa</code></a>, <strong>Hiroshi Ogawa</strong> and <strong>Codex (GPT-6)</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11218">vitest-dev/vitest#11218</a> <a href="https://github.com/vitest-dev/vitest/commit/5dbebe9e3"><!-- raw HTML omitted -->(5dbeb)<!-- raw HTML omitted --></a></li> <li>Don't print an interceptor warning in browser mode - by <a href="https://github.com/sheremet-va"><code>@sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11377">vitest-dev/vitest#11377</a> <a href="https://github.com/vitest-dev/vitest/commit/15cc006aa"><!-- raw HTML omitted -->(15cc0)<!-- raw HTML omitted --></a></li> <li>Don't retry when <code>test.fails</code> expectedly failed - by <a href="https://github.com/hi-ogawa"><code>@hi-ogawa</code></a>, <strong>Hiroshi Ogawa</strong> and <strong>Codex (GPT-6)</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11219">vitest-dev/vitest#11219</a> <a href="https://github.com/vitest-dev/vitest/commit/b24585f08"><!-- raw HTML omitted -->(b2458)<!-- raw HTML omitted --></a></li> <li>Scope cache key generators to projects - by <a href="https://github.com/ecoyoung"><code>@ecoyoung</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11281">vitest-dev/vitest#11281</a> and <a href="https://redirect.github.com/vitest-dev/vitest/issues/11301">vitest-dev/vitest#11301</a> <a href="https://github.com/vitest-dev/vitest/commit/92ba7fc1d"><!-- raw HTML omitted -->(92ba7)<!-- raw HTML omitted --></a></li> <li><strong>browser</strong>: <ul> <li>Delay server <code>listen</code> until tests start running - by <a href="https://github.com/sheremet-va"><code>@sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11366">vitest-dev/vitest#11366</a> <a href="https://github.com/vitest-dev/vitest/commit/7d8ed3e9b"><!-- raw HTML omitted -->(7d8ed)<!-- raw HTML omitted --></a></li> <li>Check mock path boundaries - by <a href="https://github.com/saryn17"><code>@saryn17</code></a>, <strong>Ryosei Sato</strong> and <a href="https://github.com/sheremet-va"><code>@sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11361">vitest-dev/vitest#11361</a> and <a href="https://redirect.github.com/vitest-dev/vitest/issues/11362">vitest-dev/vitest#11362</a> <a href="https://github.com/vitest-dev/vitest/commit/1c3888bce"><!-- raw HTML omitted -->(1c388)<!-- raw HTML omitted --></a></li> <li>Keep config of browser-consumed environments - by <a href="https://github.com/kasperpeulen"><code>@kasperpeulen</code></a> and <strong>Claude</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11378">vitest-dev/vitest#11378</a> <a href="https://github.com/vitest-dev/vitest/commit/aafc0996f"><!-- raw HTML omitted -->(aafc0)<!-- raw HTML omitted --></a></li> <li>Ignore page crash while cancelling - by <a href="https://github.com/sheremet-va"><code>@sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11386">vitest-dev/vitest#11386</a> <a href="https://github.com/vitest-dev/vitest/commit/7c36748fa"><!-- raw HTML omitted -->(7c367)<!-- raw HTML omitted --></a></li> <li><code>toMatchScreenshot</code> uses wrong reference on retried tests - by <a href="https://github.com/macarie"><code>@macarie</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11393">vitest-dev/vitest#11393</a> <a href="https://github.com/vitest-dev/vitest/commit/c22aba992"><!-- raw HTML omitted -->(c22ab)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>cache</strong>: <ul> <li>Revalidate imports of cached modules - by <a href="https://github.com/sheremet-va"><code>@sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11381">vitest-dev/vitest#11381</a> <a href="https://github.com/vitest-dev/vitest/commit/38f98855f"><!-- raw HTML omitted -->(38f98)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>deps</strong>: <ul> <li>Pin <code>why-is-node-running</code> to <code>3.2.1</code> to avoid users running into <code>ERR_PNPM_TRUST_DOWNGRADE</code> - by <a href="https://github.com/AriPerkkio"><code>@AriPerkkio</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11403">vitest-dev/vitest#11403</a> <a href="https://github.com/vitest-dev/vitest/commit/f6c9a4977"><!-- raw HTML omitted -->(f6c9a)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>expect</strong>: <ul> <li>Pass current equality testers to <code>expect.extend</code> asymmetric matchers - by <a href="https://github.com/hi-ogawa"><code>@hi-ogawa</code></a>, <strong>Hiroshi Ogawa</strong> and <strong>Claude</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11401">vitest-dev/vitest#11401</a> <a href="https://github.com/vitest-dev/vitest/commit/3e794a96b"><!-- raw HTML omitted -->(3e794)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>jsdom</strong>: <ul> <li>Support Blob on jsdom 30.1 - by <a href="https://github.com/sheremet-va"><code>@sheremet-va</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11379">vitest-dev/vitest#11379</a> <a href="https://github.com/vitest-dev/vitest/commit/6c49b7197"><!-- raw HTML omitted -->(6c49b)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>pool</strong>: <ul> <li>Preserve unique pool ids when <code>groupOrder</code> is set - by <a href="https://github.com/mtorp"><code>@mtorp</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11392">vitest-dev/vitest#11392</a> <a href="https://github.com/vitest-dev/vitest/commit/50312ebb4"><!-- raw HTML omitted -->(50312)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>ui</strong>: <ul> <li>Split-pane handle overlapping iframe - by <a href="https://github.com/macarie"><code>@macarie</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11221">vitest-dev/vitest#11221</a> <a href="https://github.com/vitest-dev/vitest/commit/f91db0dfd"><!-- raw HTML omitted -->(f91db)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>vitest</strong>: <ul> <li>Remove root temp dir on close - by <a href="https://github.com/abhinav-phi"><code>@abhinav-phi</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11248">vitest-dev/vitest#11248</a> <a href="https://github.com/vitest-dev/vitest/commit/7c7119cf7"><!-- raw HTML omitted -->(7c711)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>vm</strong>: <ul> <li>Do not optimize deps from index.html - by <a href="https://github.com/ezefernandezyf"><code>@ezefernandezyf</code></a>, <strong>Hiroshi Ogawa</strong> and <strong>Codex (GPT-6)</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11329">vitest-dev/vitest#11329</a> and <a href="https://redirect.github.com/vitest-dev/vitest/issues/11360">vitest-dev/vitest#11360</a> <a href="https://github.com/vitest-dev/vitest/commit/caf2887de"><!-- raw HTML omitted -->(caf28)<!-- raw HTML omitted --></a></li> <li>Don't reuse scripts across vite environments - by <a href="https://github.com/MO2k4"><code>@MO2k4</code></a>, <strong>Martin Oehlert</strong> and <strong>Claude</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11395">vitest-dev/vitest#11395</a> <a href="https://github.com/vitest-dev/vitest/commit/346d3896b"><!-- raw HTML omitted -->(346d3)<!-- raw HTML omitted --></a></li> </ul> </li> </ul> <h5> <a href="https://github.com/vitest-dev/vitest/compare/v5.0.2...v5.0.3">View changes on GitHub</a></h5> <h2>v5.0.2</h2> <h3> 🐞 Bug Fixes</h3> <ul> <li>Bind <code>process</code> in case global is overwritten - by <a href="https://github.com/AriPerkkio"><code>@AriPerkkio</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11343">vitest-dev/vitest#11343</a> <a href="https://github.com/vitest-dev/vitest/commit/0b79231ad"><!-- raw HTML omitted -->(0b792)<!-- raw HTML omitted --></a></li> <li><strong>detect-async-leaks</strong>: <ul> <li>Ignore <code>process.stdio</code> handles - by <a href="https://github.com/AriPerkkio"><code>@AriPerkkio</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11333">vitest-dev/vitest#11333</a> <a href="https://github.com/vitest-dev/vitest/commit/0fd6b9790"><!-- raw HTML omitted -->(0fd6b)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>expect</strong>: <ul> <li>Fix <code>toMatchObject</code> with asymmetric matchers - by <a href="https://github.com/ShreeBohara"><code>@ShreeBohara</code></a>, <strong>Claude Opus 5</strong>, <a href="https://github.com/hi-ogawa"><code>@hi-ogawa</code></a>, <strong>Hiroshi Ogawa</strong> and <strong>Codex (GPT-5)</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11100">vitest-dev/vitest#11100</a> <a href="https://github.com/vitest-dev/vitest/commit/42523289e"><!-- raw HTML omitted -->(42523)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>jsdom</strong>: <ul> <li>Fix <code>Request</code> with <code>Blob</code> body on jsdom 28+ - by <a href="https://github.com/harshit-d3v"><code>@harshit-d3v</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11295">vitest-dev/vitest#11295</a> <a href="https://github.com/vitest-dev/vitest/commit/d1c3ecc93"><!-- raw HTML omitted -->(d1c3e)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>reporter</strong>: <ul> <li><code>agent</code> to respect <code>--silent</code> - by <a href="https://github.com/Raj4478"><code>@Raj4478</code></a> and <a href="https://github.com/AriPerkkio"><code>@AriPerkkio</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11271">vitest-dev/vitest#11271</a> <a href="https://github.com/vitest-dev/vitest/commit/5b95efb6d"><!-- raw HTML omitted -->(5b95e)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>reporters</strong>: <ul> <li>Handle concurrent <code>createReport</code> calls - by <a href="https://github.com/7rulnik"><code>@7rulnik</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11278">vitest-dev/vitest#11278</a> <a href="https://github.com/vitest-dev/vitest/commit/e8e556ff7"><!-- raw HTML omitted -->(e8e55)<!-- raw HTML omitted --></a></li> <li><code>hanging-process</code> to use ESM entrypoint - by <a href="https://github.com/AriPerkkio"><code>@AriPerkkio</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11316">vitest-dev/vitest#11316</a> <a href="https://github.com/vitest-dev/vitest/commit/4e91e5668"><!-- raw HTML omitted -->(4e91e)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>spy</strong>: <ul> <li>Fix stack overflow when spying <code>Set.prototype.add</code> - by <a href="https://github.com/fengmk2"><code>@fengmk2</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/11299">vitest-dev/vitest#11299</a> <a href="https://github.com/vitest-dev/vitest/commit/a0a939653"><!-- raw HTML omitted -->(a0a93)<!-- raw HTML omitted --></a></li> </ul> </li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/vitest-dev/vitest/commit/33cadea62e8763c455c7fca38d9ab1dda87c5f75"><code>33cadea</code></a> chore: release v5.0.3 (<a href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/11409">#11409</a>)</li> <li><a href="https://github.com/vitest-dev/vitest/commit/346d3896b65c3c907174447a035807342799f346"><code>346d389</code></a> fix(vm): don't reuse scripts across vite environments (<a href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/11395">#11395</a>)</li> <li><a href="https://github.com/vitest-dev/vitest/commit/f6c9a4977ad3363f796a737834572e54c6ad5c18"><code>f6c9a49</code></a> fix(deps): pin <code>why-is-node-running</code> to <code>3.2.1</code> to avoid users running into `...</li> <li><a href="https://github.com/vitest-dev/vitest/commit/062c75d8b63519211d951d8293ea81b5a9e3c124"><code>062c75d</code></a> chore: fix standalone docs build, update exports maps (<a href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/11394">#11394</a>)</li> <li><a href="https://github.com/vitest-dev/vitest/commit/caf2887dee8987a60118d53933f6e9cabd6b3e2a"><code>caf2887</code></a> fix(vm): do not optimize deps from index.html (fix <a href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/11329">#11329</a>) (<a href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/11360">#11360</a>)</li> <li><a href="https://github.com/vitest-dev/vitest/commit/50312ebb4eca6a98f6d0b2b61d5d9d38cbbabcef"><code>50312eb</code></a> fix(pool): preserve unique pool ids when <code>groupOrder</code> is set (<a href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/11392">#11392</a>)</li> <li><a href="https://github.com/vitest-dev/vitest/commit/7c36748fad1eae9687312f2f7ceadce6ec88b5df"><code>7c36748</code></a> fix(browser): ignore page crash while cancelling (<a href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/11386">#11386</a>)</li> <li><a href="https://github.com/vitest-dev/vitest/commit/92ba7fc1df16a4fa5bbee3f198c582fbd56689d8"><code>92ba7fc</code></a> fix: scope cache key generators to projects (fix <a href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/11281">#11281</a>) (<a href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/11301">#11301</a>)</li> <li><a href="https://github.com/vitest-dev/vitest/commit/38f98855fa9cd7fd376afb84094eba0fda256a74"><code>38f9885</code></a> fix(cache): revalidate imports of cached modules (<a href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/11381">#11381</a>)</li> <li><a href="https://github.com/vitest-dev/vitest/commit/b24585f08f2ea267746a2d6ca0e43edcbb29726f"><code>b24585f</code></a> fix: don't retry when <code>test.fails</code> expectedly failed (<a href="https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest/issues/11219">#11219</a>)</li> <li>Additional commits viewable in <a href="https://github.com/vitest-dev/vitest/commits/v5.0.3/packages/vitest">compare view</a></li> </ul> </details> <br /> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Priya Raman <priya.raman@paperclip.ing> Co-authored-by: Paperclip <noreply@paperclip.ing> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
490 lines
18 KiB
TypeScript
490 lines
18 KiB
TypeScript
import express from "express";
|
|
import request from "supertest";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mockAccessService = vi.hoisted(() => ({
|
|
isInstanceAdmin: vi.fn(),
|
|
hasPermission: vi.fn(),
|
|
canUser: vi.fn(),
|
|
}));
|
|
|
|
const mockAgentService = vi.hoisted(() => ({
|
|
getById: vi.fn(),
|
|
}));
|
|
|
|
const mockBoardAuthService = vi.hoisted(() => ({
|
|
createCliAuthChallenge: vi.fn(),
|
|
describeCliAuthChallenge: vi.fn(),
|
|
approveCliAuthChallenge: vi.fn(),
|
|
cancelCliAuthChallenge: vi.fn(),
|
|
resolveBoardAccess: vi.fn(),
|
|
resolveBoardActivityCompanyIds: vi.fn(),
|
|
assertCurrentBoardKey: vi.fn(),
|
|
revokeBoardApiKey: vi.fn(),
|
|
listBoardApiKeys: vi.fn(),
|
|
createNamedBoardApiKey: vi.fn(),
|
|
getBoardApiKeyForUser: vi.fn(),
|
|
}));
|
|
|
|
const mockLogActivity = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("../services/index.js", () => ({
|
|
accessService: () => mockAccessService,
|
|
agentService: () => mockAgentService,
|
|
boardAuthService: () => mockBoardAuthService,
|
|
logActivity: mockLogActivity,
|
|
notifyHireApproved: vi.fn(),
|
|
deduplicateAgentName: vi.fn((name: string) => name),
|
|
}));
|
|
|
|
function registerModuleMocks() {
|
|
vi.doMock("../routes/authz.js", async () => vi.importActual("../routes/authz.js"));
|
|
|
|
vi.doMock("../services/index.js", () => ({
|
|
accessService: () => mockAccessService,
|
|
agentService: () => mockAgentService,
|
|
boardAuthService: () => mockBoardAuthService,
|
|
logActivity: mockLogActivity,
|
|
notifyHireApproved: vi.fn(),
|
|
deduplicateAgentName: vi.fn((name: string) => name),
|
|
}));
|
|
}
|
|
|
|
let appImportCounter = 0;
|
|
|
|
async function createApp(actor: any, db: any = {} as any) {
|
|
appImportCounter += 1;
|
|
const routeModulePath = `../routes/access.js?cli-auth-routes-${appImportCounter}`;
|
|
const middlewareModulePath = `../middleware/index.js?cli-auth-routes-${appImportCounter}`;
|
|
const [{ accessRoutes }, { errorHandler }] = await Promise.all([
|
|
import(routeModulePath) as Promise<typeof import("../routes/access.js")>,
|
|
import(middlewareModulePath) as Promise<typeof import("../middleware/index.js")>,
|
|
]);
|
|
|
|
const app = express();
|
|
app.use(express.json());
|
|
app.use((req, _res, next) => {
|
|
req.actor = {
|
|
...actor,
|
|
companyIds: Array.isArray(actor.companyIds) ? [...actor.companyIds] : actor.companyIds,
|
|
memberships: Array.isArray(actor.memberships)
|
|
? actor.memberships.map((membership: unknown) =>
|
|
typeof membership === "object" && membership !== null
|
|
? { ...membership }
|
|
: membership,
|
|
)
|
|
: actor.memberships,
|
|
};
|
|
next();
|
|
});
|
|
app.use(
|
|
"/api",
|
|
accessRoutes(db, {
|
|
deploymentMode: "authenticated",
|
|
deploymentExposure: "private",
|
|
bindHost: "127.0.0.1",
|
|
allowedHostnames: [],
|
|
}),
|
|
);
|
|
app.use(errorHandler);
|
|
return app;
|
|
}
|
|
|
|
describe("cli auth routes", () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
vi.doUnmock("../services/index.js");
|
|
vi.doUnmock("../routes/authz.js");
|
|
vi.doUnmock("../routes/access.js");
|
|
vi.doUnmock("../middleware/index.js");
|
|
registerModuleMocks();
|
|
vi.resetAllMocks();
|
|
});
|
|
|
|
it("creates a CLI auth challenge with approval metadata", async () => {
|
|
mockBoardAuthService.createCliAuthChallenge.mockResolvedValue({
|
|
challenge: {
|
|
id: "12345678-1234-4123-8123-123456789abc",
|
|
expiresAt: new Date("2026-03-23T13:00:00.000Z"),
|
|
},
|
|
challengeSecret: "pcp_cli_auth_secret",
|
|
pendingBoardToken: "pcp_board_token",
|
|
});
|
|
|
|
const app = await createApp({ type: "none", source: "none" });
|
|
const res = await request(app)
|
|
.post("/api/cli-auth/challenges")
|
|
.send({
|
|
command: "paperclipai company import",
|
|
clientName: "paperclipai cli",
|
|
requestedAccess: "board",
|
|
});
|
|
|
|
expect(res.status, res.text || JSON.stringify(res.body)).toBe(201);
|
|
expect(res.body).toMatchObject({
|
|
id: "12345678-1234-4123-8123-123456789abc",
|
|
token: "pcp_cli_auth_secret",
|
|
approvalPath: "/cli-auth/12345678-1234-4123-8123-123456789abc?token=pcp_cli_auth_secret",
|
|
pollPath: "/cli-auth/challenges/12345678-1234-4123-8123-123456789abc",
|
|
expiresAt: "2026-03-23T13:00:00.000Z",
|
|
});
|
|
expect(res.body.boardApiToken).toBe("pcp_board_token");
|
|
expect(res.body.approvalUrl).toContain("/cli-auth/12345678-1234-4123-8123-123456789abc?token=pcp_cli_auth_secret");
|
|
});
|
|
|
|
it("rejects anonymous access to generic skill documents", async () => {
|
|
const indexApp = await createApp({ type: "none", source: "none" });
|
|
const skillApp = await createApp({ type: "none", source: "none" });
|
|
|
|
const indexRes = await request(indexApp).get("/api/skills/index");
|
|
const skillRes = await request(skillApp).get("/api/skills/paperclip");
|
|
|
|
expect(indexRes.status, JSON.stringify(indexRes.body)).toBe(401);
|
|
expect(skillRes.status, skillRes.text || JSON.stringify(skillRes.body)).toBe(401);
|
|
});
|
|
|
|
it("serves the invite-scoped paperclip skill anonymously for active invites", async () => {
|
|
const invite = {
|
|
id: "invite-1",
|
|
companyId: "company-1",
|
|
inviteType: "company_join",
|
|
allowedJoinTypes: "agent",
|
|
tokenHash: "hash",
|
|
defaultsPayload: null,
|
|
expiresAt: new Date(Date.now() + 60_000),
|
|
invitedByUserId: null,
|
|
revokedAt: null,
|
|
acceptedAt: null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
const db = {
|
|
select: vi.fn(() => ({
|
|
from: vi.fn(() => ({
|
|
where: vi.fn().mockResolvedValue([invite]),
|
|
})),
|
|
})),
|
|
};
|
|
|
|
const app = await createApp({ type: "none", source: "none" }, db);
|
|
const res = await request(app).get("/api/invites/token-123/skills/paperclip");
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.headers["content-type"]).toContain("text/markdown");
|
|
expect(res.text).toContain("# Paperclip Skill");
|
|
});
|
|
|
|
it("marks challenge status as requiring sign-in for anonymous viewers", async () => {
|
|
mockBoardAuthService.describeCliAuthChallenge.mockResolvedValue({
|
|
id: "12345678-1234-4123-8123-123456789abc",
|
|
status: "pending",
|
|
command: "paperclipai company import",
|
|
clientName: "paperclipai cli",
|
|
requestedAccess: "board",
|
|
requestedCompanyId: null,
|
|
requestedCompanyName: null,
|
|
approvedAt: null,
|
|
cancelledAt: null,
|
|
expiresAt: "2026-03-23T13:00:00.000Z",
|
|
approvedByUser: null,
|
|
});
|
|
|
|
const app = await createApp({ type: "none", source: "none" });
|
|
const res = await request(app).get("/api/cli-auth/challenges/12345678-1234-4123-8123-123456789abc?token=pcp_cli_auth_secret");
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.requiresSignIn).toBe(true);
|
|
expect(res.body.canApprove).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
["cloud_tenant", "board", false, false, true],
|
|
["cloud_tenant", "instance_admin_required", false, false, false],
|
|
["session", "board", false, false, true],
|
|
["board_api_key", "board", false, true, false],
|
|
])("reports CLI approval for %s requesting %s", async (source, requestedAccess, isInstanceAdmin, requiresSignIn, canApprove) => {
|
|
mockBoardAuthService.describeCliAuthChallenge.mockResolvedValue({
|
|
id: "12345678-1234-4123-8123-123456789abc", status: "pending", requestedAccess,
|
|
});
|
|
const app = await createApp({ type: "board", source, userId: "user-1", isInstanceAdmin });
|
|
const res = await request(app).get("/api/cli-auth/challenges/12345678-1234-4123-8123-123456789abc?token=pcp_cli_auth_secret");
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toMatchObject({ requiresSignIn, canApprove });
|
|
});
|
|
|
|
it.each(["PASTE_ID_HERE", "not-a-uuid"])("rejects malformed challenge ID %s before calling the service", async (id) => {
|
|
const app = await createApp({ type: "board", userId: "user-1", source: "session" });
|
|
const responses = [
|
|
await request(app).get(`/api/cli-auth/challenges/${id}?token=pcp_cli_auth_secret`),
|
|
await request(app).post(`/api/cli-auth/challenges/${id}/approve`).send({ token: "pcp_cli_auth_secret" }),
|
|
await request(app).post(`/api/cli-auth/challenges/${id}/cancel`).send({ token: "pcp_cli_auth_secret" }),
|
|
];
|
|
for (const response of responses) {
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toBe("Invalid CLI auth challenge ID");
|
|
}
|
|
expect(mockBoardAuthService.describeCliAuthChallenge).not.toHaveBeenCalled();
|
|
expect(mockBoardAuthService.approveCliAuthChallenge).not.toHaveBeenCalled();
|
|
expect(mockBoardAuthService.cancelCliAuthChallenge).not.toHaveBeenCalled();
|
|
expect(mockLogActivity).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("requires authentication before approving even a malformed challenge ID", async () => {
|
|
const app = await createApp({ type: "none", source: "none" });
|
|
const res = await request(app)
|
|
.post("/api/cli-auth/challenges/PASTE_ID_HERE/approve")
|
|
.send({ token: "pcp_cli_auth_secret" });
|
|
expect(res.status).toBe(401);
|
|
expect(mockBoardAuthService.approveCliAuthChallenge).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("preserves missing-secret and unknown-challenge responses", async () => {
|
|
const app = await createApp({ type: "none", source: "none" });
|
|
const path = "/api/cli-auth/challenges/12345678-1234-4123-8123-123456789abc";
|
|
const missing = await request(app).get(path);
|
|
expect(missing.status).toBe(404);
|
|
expect(mockBoardAuthService.describeCliAuthChallenge).not.toHaveBeenCalled();
|
|
mockBoardAuthService.describeCliAuthChallenge.mockResolvedValue(null);
|
|
const unknown = await request(app).get(`${path}?token=wrong-secret`);
|
|
expect(unknown.status).toBe(404);
|
|
});
|
|
|
|
it("cancels a valid challenge and preserves path whitespace normalization", async () => {
|
|
mockBoardAuthService.cancelCliAuthChallenge.mockResolvedValue({ status: "cancelled" });
|
|
const app = await createApp({ type: "none", source: "none" });
|
|
const id = "12345678-1234-4123-8123-123456789ABC";
|
|
const res = await request(app)
|
|
.post(`/api/cli-auth/challenges/${encodeURIComponent(` ${id} `)}/cancel`)
|
|
.send({ token: "pcp_cli_auth_secret" });
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toEqual({ status: "cancelled", cancelled: true });
|
|
expect(mockBoardAuthService.cancelCliAuthChallenge).toHaveBeenCalledWith(id, "pcp_cli_auth_secret");
|
|
});
|
|
|
|
it("approves a CLI auth challenge for a signed-in board user", async () => {
|
|
mockBoardAuthService.approveCliAuthChallenge.mockResolvedValue({
|
|
status: "approved",
|
|
challenge: {
|
|
id: "12345678-1234-4123-8123-123456789abc",
|
|
boardApiKeyId: "board-key-1",
|
|
requestedAccess: "board",
|
|
requestedCompanyId: "company-1",
|
|
expiresAt: new Date("2026-03-23T13:00:00.000Z"),
|
|
},
|
|
});
|
|
mockBoardAuthService.resolveBoardAccess.mockResolvedValue({
|
|
user: { id: "user-1", name: "User One", email: "user@example.com" },
|
|
companyIds: ["company-1"],
|
|
isInstanceAdmin: false,
|
|
});
|
|
mockBoardAuthService.resolveBoardActivityCompanyIds.mockResolvedValue(["company-1"]);
|
|
|
|
const app = await createApp({
|
|
type: "board",
|
|
userId: "user-1",
|
|
source: "session",
|
|
isInstanceAdmin: false,
|
|
companyIds: ["company-1"],
|
|
});
|
|
const res = await request(app)
|
|
.post("/api/cli-auth/challenges/12345678-1234-4123-8123-123456789abc/approve")
|
|
.send({ token: "pcp_cli_auth_secret" });
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(mockBoardAuthService.approveCliAuthChallenge).toHaveBeenCalledWith(
|
|
"12345678-1234-4123-8123-123456789abc",
|
|
"pcp_cli_auth_secret",
|
|
"user-1",
|
|
);
|
|
expect(mockLogActivity).toHaveBeenCalledTimes(1);
|
|
expect(mockLogActivity).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
companyId: "company-1",
|
|
action: "board_api_key.created",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("logs approve activity for instance admins without company memberships", async () => {
|
|
mockBoardAuthService.approveCliAuthChallenge.mockResolvedValue({
|
|
status: "approved",
|
|
challenge: {
|
|
id: "12345678-1234-4123-8123-123456789def",
|
|
boardApiKeyId: "board-key-2",
|
|
requestedAccess: "instance_admin_required",
|
|
requestedCompanyId: null,
|
|
expiresAt: new Date("2026-03-23T13:00:00.000Z"),
|
|
},
|
|
});
|
|
mockBoardAuthService.resolveBoardActivityCompanyIds.mockResolvedValue(["company-a", "company-b"]);
|
|
|
|
const app = await createApp({
|
|
type: "board",
|
|
userId: "admin-1",
|
|
source: "session",
|
|
isInstanceAdmin: true,
|
|
companyIds: [],
|
|
});
|
|
const res = await request(app)
|
|
.post("/api/cli-auth/challenges/12345678-1234-4123-8123-123456789def/approve")
|
|
.send({ token: "pcp_cli_auth_secret" });
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(mockBoardAuthService.resolveBoardActivityCompanyIds).toHaveBeenCalledWith({
|
|
userId: "admin-1",
|
|
requestedCompanyId: null,
|
|
boardApiKeyId: "board-key-2",
|
|
});
|
|
expect(mockLogActivity).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("logs revoke activity with resolved audit company ids", async () => {
|
|
mockBoardAuthService.assertCurrentBoardKey.mockResolvedValue({
|
|
id: "board-key-3",
|
|
userId: "admin-2",
|
|
});
|
|
mockBoardAuthService.resolveBoardActivityCompanyIds.mockResolvedValue(["company-z"]);
|
|
|
|
const app = await createApp({
|
|
type: "board",
|
|
userId: "admin-2",
|
|
keyId: "board-key-3",
|
|
source: "board_key",
|
|
isInstanceAdmin: true,
|
|
companyIds: [],
|
|
});
|
|
const res = await request(app).post("/api/cli-auth/revoke-current").send({});
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(mockBoardAuthService.resolveBoardActivityCompanyIds).toHaveBeenCalledWith({
|
|
userId: "admin-2",
|
|
boardApiKeyId: "board-key-3",
|
|
});
|
|
expect(mockLogActivity).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
companyId: "company-z",
|
|
action: "board_api_key.revoked",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("creates a named board API key and logs audit activity", async () => {
|
|
mockBoardAuthService.createNamedBoardApiKey.mockResolvedValue({
|
|
id: "board-key-4",
|
|
name: "external-admin",
|
|
token: "pcp_board_plaintext",
|
|
createdAt: new Date("2026-05-23T12:00:00.000Z"),
|
|
lastUsedAt: null,
|
|
revokedAt: null,
|
|
expiresAt: new Date("2026-06-23T12:00:00.000Z"),
|
|
});
|
|
mockBoardAuthService.resolveBoardActivityCompanyIds.mockResolvedValue(["11111111-1111-4111-8111-111111111111"]);
|
|
|
|
const app = await createApp({
|
|
type: "board",
|
|
userId: "user-1",
|
|
source: "board_key",
|
|
isInstanceAdmin: false,
|
|
companyIds: ["11111111-1111-4111-8111-111111111111"],
|
|
});
|
|
const res = await request(app)
|
|
.post("/api/board-api-keys")
|
|
.send({
|
|
name: "external-admin",
|
|
requestedCompanyId: "11111111-1111-4111-8111-111111111111",
|
|
expiresAt: "2026-06-23T12:00:00.000Z",
|
|
});
|
|
|
|
expect(res.status, res.text || JSON.stringify(res.body)).toBe(201);
|
|
expect(res.body).toMatchObject({
|
|
id: "board-key-4",
|
|
name: "external-admin",
|
|
token: "pcp_board_plaintext",
|
|
expiresAt: "2026-06-23T12:00:00.000Z",
|
|
});
|
|
expect(mockBoardAuthService.createNamedBoardApiKey).toHaveBeenCalledWith({
|
|
userId: "user-1",
|
|
name: "external-admin",
|
|
expiresAt: new Date("2026-06-23T12:00:00.000Z"),
|
|
});
|
|
expect(mockLogActivity).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
companyId: "11111111-1111-4111-8111-111111111111",
|
|
action: "board_api_key.created",
|
|
details: expect.objectContaining({ name: "external-admin" }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("lists and revokes named board API keys for the current board user", async () => {
|
|
const keyId = "55555555-5555-4555-8555-555555555555";
|
|
mockBoardAuthService.listBoardApiKeys.mockResolvedValue([
|
|
{
|
|
id: keyId,
|
|
name: "external-admin",
|
|
createdAt: new Date("2026-05-23T12:00:00.000Z"),
|
|
lastUsedAt: null,
|
|
revokedAt: null,
|
|
expiresAt: null,
|
|
},
|
|
]);
|
|
mockBoardAuthService.getBoardApiKeyForUser.mockResolvedValue({
|
|
id: keyId,
|
|
userId: "user-1",
|
|
name: "external-admin",
|
|
});
|
|
mockBoardAuthService.revokeBoardApiKey.mockResolvedValue({
|
|
id: keyId,
|
|
userId: "user-1",
|
|
name: "external-admin",
|
|
});
|
|
mockBoardAuthService.resolveBoardActivityCompanyIds.mockResolvedValue(["company-1"]);
|
|
|
|
const app = await createApp({
|
|
type: "board",
|
|
userId: "user-1",
|
|
source: "board_key",
|
|
isInstanceAdmin: false,
|
|
companyIds: ["company-1"],
|
|
});
|
|
|
|
const listRes = await request(app).get("/api/board-api-keys");
|
|
expect(listRes.status).toBe(200);
|
|
expect(listRes.body[0]).toMatchObject({ id: keyId, name: "external-admin" });
|
|
expect(mockBoardAuthService.listBoardApiKeys).toHaveBeenCalledWith(
|
|
"user-1",
|
|
{ includeInactive: false },
|
|
);
|
|
|
|
const revokeRes = await request(app).delete(`/api/board-api-keys/${keyId}`);
|
|
expect(revokeRes.status).toBe(200);
|
|
expect(revokeRes.body).toEqual({ ok: true, keyId });
|
|
expect(mockLogActivity).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
companyId: "company-1",
|
|
action: "board_api_key.revoked",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("rejects malformed board API key IDs before database lookup", async () => {
|
|
const app = await createApp({
|
|
type: "board",
|
|
userId: "user-1",
|
|
source: "board_key",
|
|
isInstanceAdmin: false,
|
|
companyIds: ["company-1"],
|
|
});
|
|
|
|
const res = await request(app).delete("/api/board-api-keys/not-a-uuid");
|
|
|
|
expect(res.status).toBe(400);
|
|
expect(mockBoardAuthService.getBoardApiKeyForUser).not.toHaveBeenCalled();
|
|
expect(mockBoardAuthService.revokeBoardApiKey).not.toHaveBeenCalled();
|
|
});
|
|
});
|