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

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

mirrors.test.tsBlame119 lines · 1 contributor
4a0dea1Claude1/**
2 * Block I9 — Repository mirroring tests.
3 *
4 * Pure validation tests for URL safety + auth smoke on mirror routes.
5 * Actual git fetch is exercised by the live server — `runMirrorSync` is
6 * not tested here because it needs a real upstream.
7 */
8
9import { describe, it, expect } from "bun:test";
10import app from "../app";
11import { validateUpstreamUrl, safeUrlForLog } from "../lib/mirrors";
12
13describe("mirrors — validateUpstreamUrl", () => {
14 it("accepts https URLs", () => {
15 expect(validateUpstreamUrl("https://github.com/foo/bar.git").ok).toBe(true);
16 });
17
18 it("accepts http URLs", () => {
19 expect(validateUpstreamUrl("http://git.example.com/x.git").ok).toBe(true);
20 });
21
22 it("accepts git:// URLs", () => {
23 expect(validateUpstreamUrl("git://kernel.org/linux.git").ok).toBe(true);
24 });
25
26 it("rejects ssh URLs", () => {
27 const r = validateUpstreamUrl("ssh://git@github.com/foo/bar.git");
28 expect(r.ok).toBe(false);
29 expect(r.error).toMatch(/https|http|git/);
30 });
31
32 it("rejects file:// URLs", () => {
33 const r = validateUpstreamUrl("file:///tmp/evil.git");
34 expect(r.ok).toBe(false);
35 });
36
37 it("rejects local paths", () => {
38 expect(validateUpstreamUrl("/etc/passwd").ok).toBe(false);
39 expect(validateUpstreamUrl("./foo.git").ok).toBe(false);
40 });
41
42 it("rejects URLs with shell metacharacters", () => {
43 expect(validateUpstreamUrl("https://evil;rm -rf /").ok).toBe(false);
44 expect(validateUpstreamUrl("https://evil`id`").ok).toBe(false);
45 expect(validateUpstreamUrl("https://evil$(whoami)").ok).toBe(false);
46 expect(validateUpstreamUrl("https://evil|nc 1.2.3.4 9").ok).toBe(false);
47 expect(validateUpstreamUrl("https://evil<payload").ok).toBe(false);
48 });
49
50 it("rejects empty or whitespace-only URLs", () => {
51 expect(validateUpstreamUrl("").ok).toBe(false);
52 expect(validateUpstreamUrl(" ").ok).toBe(false);
53 });
54
55 it("rejects URLs over 2048 chars", () => {
56 const long = "https://example.com/" + "a".repeat(2100);
57 expect(validateUpstreamUrl(long).ok).toBe(false);
58 });
59});
60
61describe("mirrors — safeUrlForLog", () => {
62 it("passes plain URLs through", () => {
63 expect(safeUrlForLog("https://github.com/foo/bar.git")).toBe(
64 "https://github.com/foo/bar.git"
65 );
66 });
67
68 it("redacts embedded credentials", () => {
69 const redacted = safeUrlForLog("https://user:pw@github.com/foo/bar.git");
70 expect(redacted).not.toContain("user:pw");
71 expect(redacted).not.toContain("pw");
72 expect(redacted).toContain("***");
73 expect(redacted).toContain("github.com");
74 });
75
76 it("returns original on unparseable input", () => {
77 expect(safeUrlForLog("not-a-url")).toBe("not-a-url");
78 });
79});
80
81describe("mirrors — route auth", () => {
82 it("GET /:owner/:repo/settings/mirror without auth → 302 /login", async () => {
83 const res = await app.request("/alice/repo/settings/mirror");
84 expect(res.status).toBe(302);
85 expect(res.headers.get("location") || "").toContain("/login");
86 });
87
88 it("POST /:owner/:repo/settings/mirror without auth → 302 /login", async () => {
89 const res = await app.request("/alice/repo/settings/mirror", {
90 method: "POST",
91 body: new URLSearchParams({ upstream_url: "https://example.com/x.git" }),
92 headers: { "content-type": "application/x-www-form-urlencoded" },
93 });
94 expect(res.status).toBe(302);
95 expect(res.headers.get("location") || "").toContain("/login");
96 });
97
98 it("POST /:owner/:repo/settings/mirror/sync without auth → 302 /login", async () => {
99 const res = await app.request("/alice/repo/settings/mirror/sync", {
100 method: "POST",
101 });
102 expect(res.status).toBe(302);
103 expect(res.headers.get("location") || "").toContain("/login");
104 });
105
106 it("POST /:owner/:repo/settings/mirror/delete without auth → 302 /login", async () => {
107 const res = await app.request("/alice/repo/settings/mirror/delete", {
108 method: "POST",
109 });
110 expect(res.status).toBe(302);
111 expect(res.headers.get("location") || "").toContain("/login");
112 });
113
114 it("POST /admin/mirrors/sync-all without auth → 302 /login", async () => {
115 const res = await app.request("/admin/mirrors/sync-all", { method: "POST" });
116 expect(res.status).toBe(302);
117 expect(res.headers.get("location") || "").toContain("/login");
118 });
119});