test: cover subscription credential client contract

Add a UI API client contract test for company-scoped subscription credentials and document BYO subscription caveats in the Docker guide.
This commit is contained in:
Codex authored and Devin Foley committed 2026-06-24 14:49:52 -07:00
1 parent c2e3b9bf0f
commit c83fe29ff3
2 files changed
+69

No files matched your search

+11
View File
@@ -156,6 +156,17 @@ Notes:
- Without API keys, the app still runs normally.
- Adapter environment checks in Paperclip will surface missing auth/CLI prerequisites.
## Bring Your Own Subscription
Paperclip's subscription-credential flow is per user, not a company-shared secret store. Each employee links their own official Claude or Codex subscription material from the profile/settings UI, and Paperclip stores the material encrypted at rest while only showing redacted metadata back to the board.
Keep these caveats in mind when you run a public deployment:
- Do not ask one employee to paste another employee's subscription token or `auth.json` / `.credentials.json` file.
- Do not treat the linked credential as a shared service account.
- Runtime refreshes are written back to the same encrypted per-user record, so the stored copy stays authoritative after a CLI session rotates material.
- Live-provider checks still depend on the employee's local CLI login state or subscription account access; automated tests cover the storage and redaction paths, but they do not replace a real host-side login.
## Podman Quadlet (systemd)
The `docker/quadlet/` directory contains unit files to run Paperclip + PostgreSQL as systemd services via Podman Quadlet.
@@ -0,0 +1,58 @@
import { describe, expect, it, vi } from "vitest";
const mockApi = vi.hoisted(() => ({
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
}));
vi.mock("./client", () => ({
api: mockApi,
}));
import { subscriptionCredentialsApi } from "./subscriptionCredentials";
describe("subscriptionCredentialsApi", () => {
it("uses company-scoped endpoints and preserves the redacted client contract", async () => {
mockApi.get.mockResolvedValue([]);
mockApi.put.mockResolvedValue({
id: "cred-1",
provider: "claude",
});
mockApi.post.mockResolvedValue({
id: "cred-1",
provider: "claude",
});
mockApi.delete.mockResolvedValue(undefined);
await subscriptionCredentialsApi.list("company-1");
await subscriptionCredentialsApi.get("company-1", "cred-1");
await subscriptionCredentialsApi.upsert("company-1", {
provider: "claude",
credentialKind: "claude_oauth_token",
material: "secret-token",
status: "active",
});
await subscriptionCredentialsApi.recordTestResult("company-1", "cred-1", {
testStatus: "ready",
});
await subscriptionCredentialsApi.remove("company-1", "cred-1");
expect(mockApi.get).toHaveBeenNthCalledWith(1, "/companies/company-1/subscription-credentials");
expect(mockApi.get).toHaveBeenNthCalledWith(2, "/companies/company-1/subscription-credentials/cred-1");
expect(mockApi.put).toHaveBeenCalledWith("/companies/company-1/subscription-credentials", {
provider: "claude",
credentialKind: "claude_oauth_token",
material: "secret-token",
status: "active",
});
expect(mockApi.post).toHaveBeenCalledWith(
"/companies/company-1/subscription-credentials/cred-1/test-result",
{
testStatus: "ready",
},
);
expect(mockApi.delete).toHaveBeenCalledWith("/companies/company-1/subscription-credentials/cred-1");
});
});