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