Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

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

week3.test.tsBlame188 lines · 2 contributors
79136bbClaude1import { describe, it, expect, beforeAll, afterAll } from "bun:test";
2import { join } from "path";
3import { rm, mkdir } from "fs/promises";
4import app from "../app";
5import { initBareRepo, getRepoPath } from "../git/repository";
6import { renderMarkdown } from "../lib/markdown";
7
8const TEST_REPOS = join(import.meta.dir, "../../.test-repos-w3-" + Date.now());
9
10beforeAll(async () => {
11 await rm(TEST_REPOS, { recursive: true, force: true });
12 await mkdir(TEST_REPOS, { recursive: true });
13 process.env.GIT_REPOS_PATH = TEST_REPOS;
14
15 await initBareRepo("alice", "project");
16 const cloneDir = join(TEST_REPOS, "_clone");
17 await mkdir(cloneDir, { recursive: true });
18 const repoPath = getRepoPath("alice", "project");
19 const workDir = join(cloneDir, "work");
20
779c681ccantynz-alt21 // Must throw on failure. These fixtures live under the project directory,
22 // so if `git clone` fails, `cwd` is still inside the developer's REAL
23 // repository — and every following git command (config, add, commit) then
24 // silently targets that repo instead. That is not hypothetical: it
25 // rewrote this repo's local user.email/user.name to the values below and
26 // committed the working tree as "Add feature".
79136bbClaude27 const run = async (cmd: string[], cwd: string) => {
28 const proc = Bun.spawn(cmd, { cwd, stdout: "pipe", stderr: "pipe" });
779c681ccantynz-alt29 const [exitCode, stderr] = await Promise.all([
30 proc.exited,
31 new Response(proc.stderr).text(),
32 ]);
33 if (exitCode !== 0) {
34 throw new Error(
35 `[week3 fixture] ${cmd.join(" ")} failed in ${cwd} (exit ${exitCode}): ${stderr}`
36 );
37 }
79136bbClaude38 };
39
40 await run(["git", "clone", repoPath, workDir], TEST_REPOS);
41 await run(["git", "config", "user.email", "alice@test.com"], workDir);
42 await run(["git", "config", "user.name", "Alice"], workDir);
43
44 await mkdir(join(workDir, "src"), { recursive: true });
45 await Bun.write(
46 join(workDir, "README.md"),
47 "# My Project\n\nThis is a **bold** statement.\n\n- Item 1\n- Item 2\n\n```ts\nconst x = 1;\n```"
48 );
49 await Bun.write(
50 join(workDir, "src/main.ts"),
51 'function hello(name: string) {\n console.log(`Hello, ${name}!`);\n}\nhello("world");'
52 );
53 await Bun.write(join(workDir, "data.json"), '{"key": "value"}');
54
55 await run(["git", "add", "-A"], workDir);
56 await run(["git", "commit", "-m", "Initial commit"], workDir);
57 await run(["git", "branch", "-M", "main"], workDir);
58 await run(["git", "push", "-u", "origin", "main"], workDir);
59
60 // Second branch
61 await run(["git", "checkout", "-b", "dev"], workDir);
62 await Bun.write(join(workDir, "src/new.ts"), "export const y = 2;");
63 await run(["git", "add", "-A"], workDir);
64 await run(["git", "commit", "-m", "Add new module"], workDir);
65 await run(["git", "push", "-u", "origin", "dev"], workDir);
66
67 await rm(cloneDir, { recursive: true, force: true });
68});
69
70afterAll(async () => {
71 await rm(TEST_REPOS, { recursive: true, force: true });
72});
73
74describe("markdown rendering", () => {
75 it("should render bold text", () => {
76 const html = renderMarkdown("This is **bold**.");
77 expect(html).toContain("<strong>bold</strong>");
78 });
79
80 it("should render code blocks with highlighting", () => {
81 const html = renderMarkdown("```ts\nconst x = 1;\n```");
82 expect(html).toContain("<pre>");
83 expect(html).toContain("hljs-");
84 });
85
86 it("should render lists", () => {
87 const html = renderMarkdown("- one\n- two");
88 expect(html).toContain("<li>");
89 });
90
91 it("should sanitize dangerous links", () => {
92 const html = renderMarkdown("[click](javascript:alert(1))");
93 expect(html).not.toContain("javascript:");
94 });
95
96 it("README renders as markdown in repo view", async () => {
97 const res = await app.request("/alice/project");
98 expect(res.status).toBe(200);
99 const html = await res.text();
100 expect(html).toContain("<strong>bold</strong>");
101 expect(html).toContain("markdown-body");
102 });
103});
104
105describe("raw file download", () => {
106 it("GET /:owner/:repo/raw/:ref/:path returns file content", async () => {
107 const res = await app.request("/alice/project/raw/main/data.json");
108 expect(res.status).toBe(200);
109 expect(res.headers.get("content-type")).toContain("application/octet-stream");
110 const text = await res.text();
111 expect(text).toContain('"key"');
112 });
113
114 it("returns 404 for missing file", async () => {
115 const res = await app.request("/alice/project/raw/main/nope.txt");
116 expect(res.status).toBe(404);
117 });
118});
119
120describe("blame view", () => {
121 it("GET /:owner/:repo/blame/:ref/:path shows blame", async () => {
122 const res = await app.request("/alice/project/blame/main/src/main.ts");
123 expect(res.status).toBe(200);
124 const html = await res.text();
125 expect(html).toContain("blame");
126 expect(html).toContain("Alice");
127 expect(html).toContain("hello");
128 });
129
130 it("returns 404 for missing file", async () => {
131 const res = await app.request("/alice/project/blame/main/nope.ts");
132 expect(res.status).toBe(404);
133 });
134});
135
136describe("code search", () => {
137 it("GET /:owner/:repo/search?q=... returns results", async () => {
138 const res = await app.request("/alice/project/search?q=hello");
139 expect(res.status).toBe(200);
140 const html = await res.text();
141 expect(html).toContain("result");
142 expect(html).toContain("main.ts");
143 });
144
145 it("empty query shows no results", async () => {
146 const res = await app.request("/alice/project/search");
147 expect(res.status).toBe(200);
148 const html = await res.text();
149 expect(html).toContain("Search");
150 });
151});
152
153describe("compare view", () => {
154 it("GET /:owner/:repo/compare shows picker", async () => {
155 const res = await app.request("/alice/project/compare");
156 expect(res.status).toBe(200);
157 const html = await res.text();
158 expect(html).toContain("Compare");
159 expect(html).toContain("main");
160 expect(html).toContain("dev");
161 });
162
163 it("GET /:owner/:repo/compare/:base...:head shows diff", async () => {
164 const res = await app.request("/alice/project/compare/main...dev");
165 expect(res.status).toBe(200);
166 const html = await res.text();
167 expect(html).toContain("new.ts");
168 expect(html).toContain("Add new module");
169 });
170});
171
172describe("blob view links", () => {
173 it("blob view contains Raw and Blame links", async () => {
174 const res = await app.request("/alice/project/blob/main/src/main.ts");
175 expect(res.status).toBe(200);
176 const html = await res.text();
177 expect(html).toContain("/alice/project/raw/main/src/main.ts");
178 expect(html).toContain("/alice/project/blame/main/src/main.ts");
179 });
180});
181
182describe("error handling", () => {
183 it("404 for unknown routes returns proper page", async () => {
184 const res = await app.request("/alice/project/unknown-route-xyz");
185 // This might hit the repo page or 404 depending on routing
186 expect([200, 404]).toContain(res.status);
187 });
188});