Skip to content
File

Blob: test/pat-verify.worker.test.ts

typescript183 lines
1import { applyD1Migrations } from "cloudflare:test";
2import { env } from "cloudflare:workers";
3import { beforeAll, beforeEach, describe, expect, it } from "vitest";
4 
5import { createDb } from "@/worker/db/d1/client";
6import { generatePatPlaintext, hashPatPlaintext, verifyPat } from "@/worker/auth/pat";
7import {
8 insertMembershipIfMissing,
9 insertPatWithGrants,
10 insertRepositoryIfNew,
11 insertUserIfNew,
12} from "@/worker/db/d1/dal";
13import { claimNamespace } from "@/worker/db/d1/dal/namespaces";
14 
15import { readAppD1Migrations } from "./util/d1Migrations";
16 
17beforeAll(async () => {
18 await applyD1Migrations(env.DB, readAppD1Migrations());
19});
20 
21const USER_ID = "user-pv";
22const NAMESPACE_ID = "ns-pv";
23const NAMESPACE_SLUG = "verify-rachel";
24const REPO_ID = "repo-pv";
25const REPO_SLUG = "site";
26const DO_NAME = "verify-rachel/site";
27 
28beforeEach(async () => {
29 const db = createDb(env.DB);
30 const now = Date.now();
31 await insertUserIfNew(db, { id: USER_ID, tesseraSub: "sub-pv", createdAt: now });
32 await claimNamespace(db, {
33 id: NAMESPACE_ID,
34 slug: NAMESPACE_SLUG,
35 createdBy: USER_ID,
36 createdAt: now,
37 });
38 await insertMembershipIfMissing(db, {
39 namespaceId: NAMESPACE_ID,
40 userId: USER_ID,
41 createdAt: now,
42 });
43 await insertRepositoryIfNew(db, {
44 id: REPO_ID,
45 namespaceId: NAMESPACE_ID,
46 createdBy: USER_ID,
47 slug: REPO_SLUG,
48 doName: DO_NAME,
49 visibility: "public",
50 createdAt: now,
51 updatedAt: now,
52 });
53});
54 
55async function seedPat(args: {
56 patId: string;
57 namespaceGrants?: Array<{ namespaceId: string; level: "pull" | "push" }>;
58 repoGrants?: Array<{ repoId: string; level: "pull" | "push" }>;
59 revokedAt?: number;
60 expiresAt?: number;
61}): Promise<string> {
62 const db = createDb(env.DB);
63 const generated = generatePatPlaintext();
64 const hash = await hashPatPlaintext(generated.plaintext);
65 await insertPatWithGrants(db, {
66 pat: {
67 id: args.patId,
68 userId: USER_ID,
69 name: "ci",
70 prefix: generated.publicPrefix,
71 hash,
72 createdAt: Date.now(),
73 expiresAt: args.expiresAt ?? null,
74 revokedAt: args.revokedAt ?? null,
75 lastUsedAt: null,
76 },
77 namespaceGrants: args.namespaceGrants?.map((g) => ({ patId: args.patId, ...g })) ?? [],
78 repoGrants: args.repoGrants?.map((g) => ({ patId: args.patId, ...g })) ?? [],
79 });
80 return generated.plaintext;
81}
82 
83describe("verifyPat", () => {
84 it("ok via repo grant returns level=push when granted", async () => {
85 const plaintext = await seedPat({
86 patId: "pat-repo-ok",
87 repoGrants: [{ repoId: REPO_ID, level: "push" }],
88 });
89 const result = await verifyPat(env, {
90 username: NAMESPACE_SLUG,
91 plaintext,
92 namespaceId: NAMESPACE_ID,
93 repositoryId: REPO_ID,
94 });
95 expect(result.ok).toBe(true);
96 if (!result.ok) return;
97 expect(result.repositoryId).toBe(REPO_ID);
98 expect(result.level).toBe("push");
99 });
100 
101 it("ok via namespace grant returns level=pull for pull-only", async () => {
102 const plaintext = await seedPat({
103 patId: "pat-ns-ok",
104 namespaceGrants: [{ namespaceId: NAMESPACE_ID, level: "pull" }],
105 });
106 const result = await verifyPat(env, {
107 username: NAMESPACE_SLUG,
108 plaintext,
109 namespaceId: NAMESPACE_ID,
110 repositoryId: REPO_ID,
111 });
112 expect(result.ok).toBe(true);
113 if (!result.ok) return;
114 expect(result.level).toBe("pull");
115 expect(result.repositoryId).toBe(REPO_ID);
116 });
117 
118 it("returns malformed for an obviously bad token", async () => {
119 expect(await verifyPat(env, { username: NAMESPACE_SLUG, plaintext: "" })).toEqual({
120 ok: false,
121 reason: "malformed",
122 });
123 });
124 
125 it("returns token-not-found for a well-formed token with no DB row", async () => {
126 const orphan = generatePatPlaintext();
127 expect(await verifyPat(env, { username: NAMESPACE_SLUG, plaintext: orphan.plaintext })).toEqual(
128 { ok: false, reason: "token-not-found" }
129 );
130 });
131 
132 it("returns token-revoked when the PAT is marked revoked", async () => {
133 const plaintext = await seedPat({
134 patId: "pat-revoked",
135 namespaceGrants: [{ namespaceId: NAMESPACE_ID, level: "pull" }],
136 revokedAt: Date.now() - 10,
137 });
138 expect(await verifyPat(env, { username: NAMESPACE_SLUG, plaintext })).toEqual({
139 ok: false,
140 reason: "token-revoked",
141 });
142 });
143 
144 it("returns token-expired when expires_at is in the past", async () => {
145 const plaintext = await seedPat({
146 patId: "pat-expired",
147 namespaceGrants: [{ namespaceId: NAMESPACE_ID, level: "pull" }],
148 expiresAt: Date.now() - 10,
149 });
150 expect(await verifyPat(env, { username: NAMESPACE_SLUG, plaintext })).toEqual({
151 ok: false,
152 reason: "token-expired",
153 });
154 });
155 
156 it("returns username-mismatch when the username segment does not match the namespace", async () => {
157 const plaintext = await seedPat({
158 patId: "pat-username-mismatch",
159 namespaceGrants: [{ namespaceId: NAMESPACE_ID, level: "pull" }],
160 });
161 expect(
162 await verifyPat(env, {
163 username: "someone-else",
164 plaintext,
165 namespaceId: NAMESPACE_ID,
166 repositoryId: REPO_ID,
167 })
168 ).toEqual({ ok: false, reason: "username-mismatch" });
169 });
170 
171 it("returns grant-missing when no grant covers the requested resource", async () => {
172 const plaintext = await seedPat({ patId: "pat-no-grant" });
173 expect(
174 await verifyPat(env, {
175 username: NAMESPACE_SLUG,
176 plaintext,
177 namespaceId: NAMESPACE_ID,
178 repositoryId: REPO_ID,
179 })
180 ).toEqual({ ok: false, reason: "grant-missing" });
181 });
182});