Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

google-oauth.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

google-oauth.test.tsBlame182 lines · 1 contributor
582cdacClaude1/**
2 * Smoke tests for the Google OAuth login flow + config page.
3 *
4 * Mirrors the structure of github-oauth.test.ts (if it exists). Covers:
5 * - /admin/google-oauth requires admin auth
6 * - /login/google redirects to /login?error= when not configured
7 * - The OAuth URL builder produces a valid Google authorize URL
8 * - The login page renders a "Sign in with Google" button when enabled
9 *
10 * These tests exercise the route registration + the pure helper functions
11 * in src/lib/google-oauth.ts. The full callback flow (token exchange +
12 * userinfo) requires a real Google response and is exercised manually
13 * during deploy validation.
14 */
15
16import { describe, it, expect } from "bun:test";
17import app from "../app";
18import {
19 buildGoogleAuthorizeUrl,
20 fetchGoogleUserinfo,
21 exchangeGoogleCode,
22} from "../lib/google-oauth";
23
24describe("google-oauth — route registration", () => {
25 it("/admin/google-oauth without auth redirects to /login", async () => {
26 const res = await app.request("/admin/google-oauth", {
27 redirect: "manual",
28 });
29 expect([302, 303]).toContain(res.status);
30 const loc = res.headers.get("location") || "";
31 expect(loc).toContain("/login");
32 });
33
34 it("/login/google when unconfigured redirects to /login with error", async () => {
35 const res = await app.request("/login/google", { redirect: "manual" });
36 expect([302, 303]).toContain(res.status);
37 const loc = res.headers.get("location") || "";
38 expect(loc).toContain("/login?error=");
39 });
40
41 it("/login/google/callback without code params redirects with error", async () => {
42 const res = await app.request("/login/google/callback", {
43 redirect: "manual",
44 });
45 expect([302, 303]).toContain(res.status);
46 const loc = res.headers.get("location") || "";
47 expect(loc).toContain("/login?error=");
48 });
49});
50
51describe("google-oauth — buildGoogleAuthorizeUrl", () => {
52 it("constructs a valid Google authorize URL with required params", () => {
53 const url = buildGoogleAuthorizeUrl(
54 {
55 authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
56 clientId: "client-id-123",
57 scopes: "openid email profile",
58 },
59 "state-abc",
60 "https://example.com/login/google/callback",
61 "nonce-xyz"
62 );
63 const u = new URL(url);
64 expect(u.origin + u.pathname).toBe(
65 "https://accounts.google.com/o/oauth2/v2/auth"
66 );
67 expect(u.searchParams.get("client_id")).toBe("client-id-123");
68 expect(u.searchParams.get("response_type")).toBe("code");
69 expect(u.searchParams.get("scope")).toBe("openid email profile");
70 expect(u.searchParams.get("state")).toBe("state-abc");
71 expect(u.searchParams.get("nonce")).toBe("nonce-xyz");
72 expect(u.searchParams.get("redirect_uri")).toBe(
73 "https://example.com/login/google/callback"
74 );
75 // We force account picker so users with multiple Google accounts can
76 // choose; without this Google silently uses the most-recent one.
77 expect(u.searchParams.get("prompt")).toBe("select_account");
78 });
79
80 it("throws when authorization_endpoint or client_id is missing", () => {
81 expect(() =>
82 buildGoogleAuthorizeUrl(
83 {
84 authorizationEndpoint: null,
85 clientId: "abc",
86 scopes: null,
87 } as any,
88 "s",
89 "r",
90 "n"
91 )
92 ).toThrow();
93 expect(() =>
94 buildGoogleAuthorizeUrl(
95 {
96 authorizationEndpoint: "https://x",
97 clientId: null,
98 scopes: null,
99 } as any,
100 "s",
101 "r",
102 "n"
103 )
104 ).toThrow();
105 });
106});
107
108describe("google-oauth — exchangeGoogleCode + fetchGoogleUserinfo", () => {
109 it("exchangeGoogleCode posts urlencoded body and returns access_token", async () => {
110 const captured: { url?: string; body?: string } = {};
111 const fakeFetch = (async (url: any, init: any) => {
112 captured.url = String(url);
113 captured.body = String(init.body);
114 return new Response(
115 JSON.stringify({ access_token: "tok-1", id_token: "id-1" }),
116 { status: 200, headers: { "content-type": "application/json" } }
117 );
a004c46Claude118 }) as unknown as typeof fetch;
582cdacClaude119
120 const result = await exchangeGoogleCode(
121 {
122 tokenEndpoint: "https://oauth2.googleapis.com/token",
123 clientId: "cid",
124 clientSecret: "secret",
125 },
126 "auth-code",
127 "https://example.com/cb",
128 fakeFetch
129 );
130 expect(result.accessToken).toBe("tok-1");
131 expect(result.idToken).toBe("id-1");
132 expect(captured.url).toBe("https://oauth2.googleapis.com/token");
133 expect(captured.body).toContain("grant_type=authorization_code");
134 expect(captured.body).toContain("code=auth-code");
135 expect(captured.body).toContain("client_id=cid");
136 });
137
138 it("fetchGoogleUserinfo parses sub + emailVerified correctly", async () => {
139 const fakeFetch = (async () =>
140 new Response(
141 JSON.stringify({
142 sub: "12345",
143 email: "user@example.com",
144 email_verified: true,
145 name: "Jane Doe",
146 picture: "https://lh.example/avatar.jpg",
147 }),
148 { status: 200, headers: { "content-type": "application/json" } }
a004c46Claude149 )) as unknown as typeof fetch;
582cdacClaude150
151 const info = await fetchGoogleUserinfo(
152 { userinfoEndpoint: "https://openidconnect.googleapis.com/v1/userinfo" },
153 "tok",
154 fakeFetch
155 );
156 expect(info.sub).toBe("12345");
157 expect(info.email).toBe("user@example.com");
158 expect(info.emailVerified).toBe(true);
159 expect(info.name).toBe("Jane Doe");
160 });
161
162 it("fetchGoogleUserinfo coerces email_verified=\"true\" string to bool", async () => {
163 const fakeFetch = (async () =>
164 new Response(
165 JSON.stringify({
166 sub: "999",
167 email: "x@y.z",
168 email_verified: "true",
169 name: null,
170 picture: null,
171 }),
172 { status: 200, headers: { "content-type": "application/json" } }
a004c46Claude173 )) as unknown as typeof fetch;
582cdacClaude174
175 const info = await fetchGoogleUserinfo(
176 { userinfoEndpoint: "https://openidconnect.googleapis.com/v1/userinfo" },
177 "tok",
178 fakeFetch
179 );
180 expect(info.emailVerified).toBe(true);
181 });
182});