import { env } from "cloudflare:workers"; import { describe, expect, it } from "vitest"; import { createControlPlaneDb } from "@/worker/db/d1/client"; import { bootstrapSubject } from "@/worker/db/d1/repository"; import { MeResponseSchema, PatCreateResponseSchema, PatResponseSchema } from "@/worker/openapi/schemas"; import { completeOidcLogin } from "@tests/worker/helpers/auth"; import { basicPat } from "@tests/worker/helpers/dav"; import { controlRequest, fetchWorker, subjectRequest } from "@tests/worker/helpers/http"; import type { z } from "zod"; type PatCreateResponse = z.infer; async function createPat(sessionCookie: string, input: Record = {}): Promise { const response = await fetchWorker( controlRequest("/api/v1/pats", { method: "POST", headers: { cookie: sessionCookie, origin: "https://dav.example.com", "content-type": "application/json", }, body: JSON.stringify({ name: "test token", scopes: ["files.full"], ...input, }), }), ); expect(response.status).toBe(201); return PatCreateResponseSchema.parse(await response.json()); } describe("Personal Access Tokens", () => { it("returns PAT plaintext only on create, never on list or detail", async () => { const login = await completeOidcLogin(); const created = await createPat(login.sessionCookie); expect(created.token).toMatch(/^dab_pat_[0-9a-f]{24,}_[a-z2-7]{52,}$/); const listResponse = await fetchWorker( controlRequest("/api/v1/pats", { headers: { cookie: login.sessionCookie } }), ); expect(listResponse.status).toBe(200); const list = PatResponseSchema.array().parse(await listResponse.json()); const listed = list.find((row) => row.id === created.id); expect(listed).toBeDefined(); expect(listed).not.toHaveProperty("token"); const detailResponse = await fetchWorker( controlRequest(`/api/v1/pats/${created.id}`, { headers: { cookie: login.sessionCookie } }), ); expect(detailResponse.status).toBe(200); const detail = PatResponseSchema.parse(await detailResponse.json()); expect(detail.id).toBe(created.id); expect(detail).not.toHaveProperty("token"); }); it("authenticates subject-host DAV requests through AUTH and records last use", async () => { const login = await completeOidcLogin(); const meResponse = await fetchWorker(controlRequest("/api/v1/me", { headers: { cookie: login.sessionCookie } })); const me = MeResponseSchema.parse(await meResponse.json()); const pat = await createPat(login.sessionCookie); expect(pat.token).toBeDefined(); const davResponse = await fetchWorker( subjectRequest(me.host_label, "/files/", { method: "PROPFIND", headers: { authorization: basicPat(pat.token!), depth: "0" }, }), ); expect(davResponse.status).toBe(207); const detailResponse = await fetchWorker( controlRequest(`/api/v1/pats/${pat.id}`, { headers: { cookie: login.sessionCookie } }), ); const detail = PatResponseSchema.parse(await detailResponse.json()); expect(detail.last_used_at).toEqual(expect.any(String)); }); it("fails closed for malformed, revoked, expired, and wrong-subject PATs", async () => { const login = await completeOidcLogin(); const meResponse = await fetchWorker(controlRequest("/api/v1/me", { headers: { cookie: login.sessionCookie } })); const me = MeResponseSchema.parse(await meResponse.json()); const malformed = await fetchWorker( subjectRequest(me.host_label, "/files/", { headers: { authorization: basicPat("dab_pat_bad") }, }), ); expect(malformed.status).toBe(401); const revoked = await createPat(login.sessionCookie); const revokeResponse = await fetchWorker( controlRequest(`/api/v1/pats/${revoked.id}`, { method: "DELETE", headers: { cookie: login.sessionCookie, origin: "https://dav.example.com", }, }), ); expect(revokeResponse.status).toBe(204); const revokedAuth = await fetchWorker( subjectRequest(me.host_label, "/files/", { headers: { authorization: basicPat(revoked.token!) }, }), ); expect(revokedAuth.status).toBe(401); const expired = await createPat(login.sessionCookie, { expires_at: "2000-01-01T00:00:00.000Z" }); const expiredAuth = await fetchWorker( subjectRequest(me.host_label, "/files/", { headers: { authorization: basicPat(expired.token!) }, }), ); expect(expiredAuth.status).toBe(401); const other = await bootstrapSubject(createControlPlaneDb(env.DAV_CONTROL_PLANE), { sub: "other-subject-for-pat-test", email: "other@example.test", displayName: "Other Subject", }); const wrongSubject = await createPat(login.sessionCookie); const wrongSubjectAuth = await fetchWorker( subjectRequest(other.subject.hostLabel, "/files/", { headers: { authorization: basicPat(wrongSubject.token!) }, }), ); expect(wrongSubjectAuth.status).toBe(401); }); it("updates and revokes PAT projections through same-origin API routes", async () => { const login = await completeOidcLogin(); const pat = await createPat(login.sessionCookie); const patchResponse = await fetchWorker( controlRequest(`/api/v1/pats/${pat.id}`, { method: "PATCH", headers: { cookie: login.sessionCookie, origin: "https://dav.example.com", "content-type": "application/json", }, body: JSON.stringify({ name: "renamed token", expires_at: null }), }), ); expect(patchResponse.status).toBe(200); expect(PatResponseSchema.parse(await patchResponse.json())).toMatchObject({ id: pat.id, name: "renamed token", expires_at: null, }); const revokeResponse = await fetchWorker( controlRequest(`/api/v1/pats/${pat.id}`, { method: "DELETE", headers: { cookie: login.sessionCookie, origin: "https://dav.example.com", }, }), ); expect(revokeResponse.status).toBe(204); const detailResponse = await fetchWorker( controlRequest(`/api/v1/pats/${pat.id}`, { headers: { cookie: login.sessionCookie } }), ); const detail = PatResponseSchema.parse(await detailResponse.json()); expect(detail.revoked_at).toEqual(expect.any(String)); }); });