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

pages.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.

pages.test.tsBlame231 lines · 1 contributor
25a91a6Claude1/**
2 * Block C3 — Pages unit + route tests.
3 *
4 * These tests run without a live database. Route tests therefore accept the
5 * whole class of graceful-degradation responses (404 / 302 / 303 / 503):
6 * 503 is emitted when the DB proxy throws, 404 when the repo row isn't found,
7 * and 302/303 when auth middleware redirects to /login.
8 */
9
10import { describe, it, expect } from "bun:test";
11import app from "../app";
12import {
13 contentTypeFor,
14 resolvePagesPath,
15 onPagesPush,
16} from "../lib/pages";
17
18describe("lib/pages — contentTypeFor", () => {
19 it("returns text/html for .html", () => {
20 expect(contentTypeFor("index.html")).toContain("text/html");
21 });
22
23 it("returns text/css for .css", () => {
24 expect(contentTypeFor("site.css")).toContain("text/css");
25 });
26
27 it("returns application/javascript for .js", () => {
28 expect(contentTypeFor("app.js")).toContain("javascript");
29 });
30
31 it("returns image/svg+xml for .svg", () => {
32 expect(contentTypeFor("logo.svg")).toBe("image/svg+xml");
33 });
34
35 it("returns image/png for .png", () => {
36 expect(contentTypeFor("pic.PNG")).toBe("image/png");
37 });
38
39 it("returns image/jpeg for .jpg and .jpeg", () => {
40 expect(contentTypeFor("a.jpg")).toBe("image/jpeg");
41 expect(contentTypeFor("b.jpeg")).toBe("image/jpeg");
42 });
43
44 it("returns application/json for .json", () => {
45 expect(contentTypeFor("data.json")).toContain("application/json");
46 });
47
48 it("returns application/octet-stream for unknown extensions", () => {
49 expect(contentTypeFor("mystery.xyz")).toBe("application/octet-stream");
50 });
51
52 it("returns application/octet-stream for files with no extension", () => {
53 expect(contentTypeFor("Makefile")).toBe("application/octet-stream");
54 });
55});
56
57describe("lib/pages — resolvePagesPath", () => {
58 it("returns index.html for empty url rest at root", () => {
59 expect(resolvePagesPath("", "/", "index.html")).toEqual(["index.html"]);
60 });
61
62 it("returns index.html for a trailing slash", () => {
63 expect(resolvePagesPath("/", "/", "index.html")).toEqual(["index.html"]);
64 });
65
66 it("probes both foo.html and foo/index.html for extensionless urls", () => {
67 const paths = resolvePagesPath("about", "/", "index.html");
68 expect(paths).toContain("about.html");
69 expect(paths).toContain("about/index.html");
70 expect(paths[0]).toBe("about.html");
71 });
72
73 it("serves a file directly when it has an extension", () => {
74 expect(resolvePagesPath("assets/app.css", "/", "index.html")).toEqual([
75 "assets/app.css",
76 ]);
77 });
78
79 it("applies sourceDir as a prefix", () => {
80 const paths = resolvePagesPath("about", "/docs", "index.html");
81 expect(paths[0]).toBe("docs/about.html");
82 expect(paths[1]).toBe("docs/about/index.html");
83 });
84
85 it("serves the index inside a nested directory when url ends with slash", () => {
86 expect(resolvePagesPath("blog/", "/", "index.html")).toEqual([
87 "blog/index.html",
88 ]);
89 });
90
91 it("strips path-traversal segments", () => {
92 const paths = resolvePagesPath("../../etc/passwd", "/", "index.html");
93 // .. entries are dropped; the remaining "etc/passwd" is treated as a
94 // pretty URL because it has no file extension.
95 expect(paths).not.toContain("../etc/passwd");
96 expect(paths).not.toContain("../../etc/passwd");
97 for (const p of paths) {
98 expect(p.startsWith("..")).toBe(false);
99 expect(p).not.toContain("../");
100 }
101 expect(paths[0]).toBe("etc/passwd.html");
102 });
103
104 it("strips leading slashes on the url rest", () => {
105 expect(resolvePagesPath("/about.html", "/", "index.html")).toEqual([
106 "about.html",
107 ]);
108 });
109
110 it("normalises source dir with or without leading / trailing slash", () => {
111 expect(resolvePagesPath("", "docs/", "index.html")).toEqual([
112 "docs/index.html",
113 ]);
114 expect(resolvePagesPath("", "/docs/", "index.html")).toEqual([
115 "docs/index.html",
116 ]);
117 });
118});
119
120describe("lib/pages — onPagesPush", () => {
121 it("never throws, even with a bogus repositoryId and no DB", async () => {
122 // No DATABASE_URL in the test env => db proxy throws. The helper must
123 // swallow that and return normally.
124 await expect(
125 onPagesPush({
126 ownerLogin: "alice",
127 repoName: "project",
128 repositoryId: "00000000-0000-0000-0000-000000000000",
129 ref: "refs/heads/gh-pages",
130 newSha: "0".repeat(40),
131 triggeredByUserId: null,
132 })
133 ).resolves.toBeUndefined();
134 });
135});
136
137describe("routes/pages — guards", () => {
138 it("GET /:owner/:repo/pages/ 404s when repo does not exist", async () => {
139 const res = await app.request("/alice/project/pages/");
140 // 404 when repo row not found, 503 when DB is unreachable.
141 expect([404, 503]).toContain(res.status);
142 });
143
144 it("GET /:owner/:repo/pages/foo 404s when repo does not exist", async () => {
145 const res = await app.request("/nobody/nothing/pages/foo.html");
146 expect([404, 503]).toContain(res.status);
147 });
148
149 it("GET /:owner/:repo/settings/pages requires auth (or is not yet mounted)", async () => {
150 const res = await app.request(
151 "/alice/project/settings/pages",
152 { redirect: "manual" }
153 );
154 // When mounted: anonymous -> redirected to /login.
155 // When not yet wired into app.tsx (integration step handled by owner):
156 // the global 404 handler answers instead.
157 expect([302, 303, 404, 503]).toContain(res.status);
158 if (res.status === 302 || res.status === 303) {
159 const loc = res.headers.get("location") || "";
160 expect(loc).toContain("/login");
161 }
162 });
163
164 it("POST /:owner/:repo/settings/pages without auth redirects to /login or 404s", async () => {
165 const res = await app.request("/alice/project/settings/pages", {
166 method: "POST",
167 headers: { "Content-Type": "application/x-www-form-urlencoded" },
168 body: "enabled=1&source_branch=gh-pages&source_dir=/",
169 redirect: "manual",
170 });
171 expect([302, 303, 404, 503]).toContain(res.status);
172 if (res.status === 302 || res.status === 303) {
173 const loc = res.headers.get("location") || "";
174 expect(loc).toContain("/login");
175 }
176 });
177
178 it("POST /:owner/:repo/settings/pages/redeploy without auth redirects to /login or 404s", async () => {
179 const res = await app.request(
180 "/alice/project/settings/pages/redeploy",
181 {
182 method: "POST",
183 redirect: "manual",
184 }
185 );
186 expect([302, 303, 404, 503]).toContain(res.status);
187 if (res.status === 302 || res.status === 303) {
188 const loc = res.headers.get("location") || "";
189 expect(loc).toContain("/login");
190 }
191 });
192});
193
194describe("routes/pages — direct route tests (no app mount)", () => {
195 // These test the exported Hono app directly so we don't depend on the
196 // owner wiring up app.tsx. This mirrors what the integration step will
197 // yield once the parent agent mounts pagesRoute.
198 it("direct GET /:owner/:repo/settings/pages without auth redirects to /login", async () => {
199 const { default: pagesRoute } = await import("../routes/pages");
200 const res = await pagesRoute.request(
201 "/alice/project/settings/pages",
202 { redirect: "manual" }
203 );
204 expect([302, 303, 503]).toContain(res.status);
205 if (res.status === 302 || res.status === 303) {
206 const loc = res.headers.get("location") || "";
207 expect(loc).toContain("/login");
208 }
209 });
210
211 it("direct POST /:owner/:repo/settings/pages without auth redirects to /login", async () => {
212 const { default: pagesRoute } = await import("../routes/pages");
213 const res = await pagesRoute.request("/alice/project/settings/pages", {
214 method: "POST",
215 headers: { "Content-Type": "application/x-www-form-urlencoded" },
216 body: "enabled=1&source_branch=gh-pages&source_dir=/",
217 redirect: "manual",
218 });
219 expect([302, 303, 503]).toContain(res.status);
220 if (res.status === 302 || res.status === 303) {
221 const loc = res.headers.get("location") || "";
222 expect(loc).toContain("/login");
223 }
224 });
225
226 it("direct GET /:owner/:repo/pages/ 404s when repo does not exist", async () => {
227 const { default: pagesRoute } = await import("../routes/pages");
228 const res = await pagesRoute.request("/alice/project/pages/");
229 expect([404, 503]).toContain(res.status);
230 });
231});