Skip to content
File

Blob: test/auth-account-merged.worker.test.ts

typescript120 lines
1import { applyD1Migrations } from "cloudflare:test";
2import { env, exports as workerExports } from "cloudflare:workers";
3import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
4 
5import { __test as oidcTest } from "@/worker/auth/oidc";
6 
7import { fakeProvider } from "./util/oidcFake";
8import { readAppD1Migrations } from "./util/d1Migrations";
9import {
10 extractSessionToken,
11 oidcTransactionCookieHeader,
12 sessionCookieHeader,
13} from "./util/authCookies";
14 
15beforeAll(async () => {
16 await applyD1Migrations(env.DB, readAppD1Migrations());
17});
18 
19beforeEach(() => {
20 oidcTest.setProviderForTesting(
21 {
22 issuer: env.TESSERA_OIDC_ISSUER,
23 clientId: env.TESSERA_OIDC_CLIENT_ID,
24 clientSecret: env.TESSERA_OIDC_CLIENT_SECRET,
25 },
26 fakeProvider({
27 authorizationEndpoint: "https://auth.example.com/authorize",
28 tokenEndpoint: "https://auth.example.com/token",
29 jwksUri: "https://auth.example.com/.well-known/jwks.json",
30 })
31 );
32});
33 
34afterEach(() => {
35 oidcTest.clearProviderCache();
36 oidcTest.setAuthorizationCodeGrantImpl(null);
37});
38 
39async function signIn(sub: string, preferredUsername?: string): Promise<string> {
40 const state = `state-${sub}`;
41 const cookie = await oidcTransactionCookieHeader(env.TESSERA_OIDC_CLIENT_SECRET, {
42 state,
43 nonce: "n",
44 codeVerifier: "v",
45 redirectUri: "https://example.com/auth/callback",
46 createdAt: Date.now(),
47 });
48 oidcTest.setAuthorizationCodeGrantImpl(async () => {
49 const claims = preferredUsername ? { sub, preferred_username: preferredUsername } : { sub };
50 return {
51 access_token: "fake",
52 token_type: "Bearer",
53 claims: () => claims,
54 } as unknown as Awaited<ReturnType<typeof import("openid-client").authorizationCodeGrant>>;
55 });
56 const url = new URL("https://example.com/auth/callback");
57 url.searchParams.set("code", "x");
58 url.searchParams.set("state", state);
59 const res = await workerExports.default.fetch(url.toString(), {
60 redirect: "manual",
61 headers: { Cookie: cookie },
62 });
63 expect(res.status).toBe(302);
64 const token = extractSessionToken(res.headers.get("set-cookie"));
65 expect(token).toBeTruthy();
66 return token!;
67}
68 
69describe("merged /auth/account hub", () => {
70 it("renders identity, namespaces, repositories, and the tokens island on one page", async () => {
71 const token = await signIn("sub-merged-1", "merged-rachel");
72 const res = await workerExports.default.fetch("https://example.com/auth/account", {
73 headers: { Cookie: sessionCookieHeader(token) },
74 });
75 expect(res.status).toBe(200);
76 const html = await res.text();
77 
78 // Identity moment renders the namespace handle, not "Your account".
79 expect(html).toContain("@merged-rachel");
80 expect(html).toContain("Identity");
81 expect(html).not.toContain("Your account");
82 
83 // The four sections all live on this single page.
84 expect(html).toContain("Namespaces");
85 expect(html).toContain("Repositories");
86 expect(html).toContain("Tokens");
87 
88 // The tokens section is anchored and hosts the island for client hydration.
89 expect(html).toContain('id="tokens"');
90 expect(html).toContain('data-island="tokens"');
91 });
92 
93 it("renders the merged page even when the user has no preferred_username claim", async () => {
94 const token = await signIn("sub-merged-no-slug", undefined);
95 const res = await workerExports.default.fetch("https://example.com/auth/account", {
96 headers: { Cookie: sessionCookieHeader(token) },
97 });
98 expect(res.status).toBe(200);
99 const html = await res.text();
100 expect(html).toContain("Identity not yet claimed");
101 expect(html).toContain('data-island="tokens"');
102 });
103 
104 it("/auth/tokens no longer routes to the management page", async () => {
105 const token = await signIn("sub-tokens-route-gone", "route-gone");
106 const res = await workerExports.default.fetch("https://example.com/auth/tokens", {
107 headers: { Cookie: sessionCookieHeader(token) },
108 redirect: "manual",
109 });
110 // The management page is exclusively at /auth/account now. Whatever the
111 // /:owner/:repo fallthrough returns for an unknown namespace, it must not
112 // be the tokens management island.
113 if (res.status === 200) {
114 const html = await res.text();
115 expect(html).not.toContain('data-island="tokens"');
116 expect(html).not.toContain('id="tokens"');
117 }
118 });
119});