Public: anyone can read this; the crew can change it.

A frozen mirror says so in one line on its repository page, not in a boxed card #10033

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/9953-a-frozen-mirror-says-so-in-one-line-on-i → main 2 commits fork

  • Merged. scout/9953-a-frozen-mirror-says-so-in-one-line-on-i was merged into main as 39411c6.

Review

Sign in to review this change.
2 files changed +98 −24
src/web/pages/frozen.ts +11 −24
@@ -23,7 +23,7 @@ import { loadRepo, notFoundPage, repoBase } from "../chrome.ts";
2323 import { field, readForm } from "../form.ts";
2424 import { html, type Html } from "../html.ts";
2525 import { flashTo } from "../layout.ts";
26 import { btn, card, selectField } from "../ui.ts";
26 import { btn, selectField } from "../ui.ts";
2727
2828 /** `codeberg.org` from a URL — what a person calls a forge; "" if unknown. */
2929 const hostLabel = (url: string): string => URL.parse(url)?.host ?? "";
@@ -82,24 +82,14 @@ export function frozenBlock(ctx: Ctx, repo: Repo): Html | null {
8282 // is filled with something discovery takes as it is.
8383 const forge = URL.parse(d.htmlUrl)?.origin ?? "";
8484 const connect = `/settings/origins${forge === "" ? "" : `?host=${encodeURIComponent(forge)}`}#connect`;
85 const row = (body: Html): Html =>
86 html`<li class="row"><div class="row-main">${body}</div></li>`;
8785 const action = !admin
88 ? row(
89 html`<p class="muted small">
90 An admin of this repository can reconnect it.
91 </p>`,
92 )
86 ? html`<span class="muted">An admin of this repository can reconnect it.</span>`
9387 : origins.length === 0
94 ? row(
95 html`<p class="small">
96 Connect an
97 account${host === "" ? null : html` at ${hostLabel(d.htmlUrl)}`}
98 under <a href="${connect}">Settings → Origins</a>: connecting one
99 reconnects this mirror by itself.
100 </p>`,
101 )
102 : row(html`<form
88 ? html`Connect an
89 account${host === "" ? null : html` at ${hostLabel(d.htmlUrl)}`} under
90 <a href="${connect}">Settings → Origins</a>: connecting one
91 reconnects this mirror by itself.`
92 : html`<form
10393 method="post"
10494 action="${repoBase(repo)}/settings/origin/reconnect"
10595 class="field-row"
@@ -114,13 +104,10 @@ export function frozenBlock(ctx: Ctx, repo: Repo): Html | null {
114104 })),
115105 })}
116106 ${btn("Reconnect", { size: "sm", variant: "primary", icon: "sync" })}
117 </form>`);
118 return card(
119 html`<ul class="list">
120 ${row(html`<p>${frozenSentence(d)}</p>`)} ${action}
121 </ul>`,
122 { tone: "attn", id: "frozen", pad: false },
123 );
107 </form>`;
108 return html`<div class="small mb" id="frozen">
109 ${frozenSentence(d)} ${action}
110 </div>`;
124111 }
125112
126113 /** A refusal to re-attach, in a sentence a person can act on. */
test/frozen-line.test.ts added +87
@@ -0,0 +1,87 @@
1 /** A frozen mirror says so in one line, not a boxed card (#9953). */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { createUser, getUserByName } from "../src/core/users.ts";
8 import { createSession } from "../src/core/sessions.ts";
9 import { createOrigin } from "../src/core/origins.ts";
10 import { createRepo } from "../src/core/repos.ts";
11 import { repoDir } from "../src/git/repos.ts";
12 import { commitFiles } from "../src/git/write.ts";
13 import { serve, type Servers } from "../src/http/server.ts";
14 import { seedAdmin } from "./helpers/admin.ts";
15
16 let dataDir = "";
17 let app: App, servers: Servers;
18 let admin = "", reader = "";
19
20 beforeAll(async () => {
21 dataDir = await mkdtemp(join(tmpdir(), "forge-frozen-line-"));
22 app = createApp({ dataDir, port: 0 });
23 servers = await serve(app);
24 const a = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" });
25 if (!a.ok) throw new Error(a.error);
26 const b = await createUser(app.db, { origin: "seed", trusted: true, username: "bobby", email: "[email protected]", password: "correct-horse-battery" });
27 if (!b.ok) throw new Error(b.error);
28 const alice = getUserByName(app.db, "alice")!;
29 admin = `forge_session=${createSession(app.db, alice.id).token}`;
30 reader = `forge_session=${createSession(app.db, getUserByName(app.db, "bobby")!.id).token}`;
31 const made = await createRepo(app.db, join(dataDir, "repos"), { ownerId: alice.id, name: "frozen", visibility: "public" });
32 if (!made.ok) throw new Error(made.error);
33 const c = await commitFiles(repoDir(join(dataDir, "repos"), "alice", "frozen"), {
34 branch: "main", message: "first", files: [{ path: "a.txt", content: "a" }],
35 author: { name: "T", email: "[email protected]" },
36 });
37 if (!c.ok) throw new Error(JSON.stringify(c.error));
38 app.db.query(
39 "UPDATE repo SET origin_full_name = 'octo/frozen', origin_html_url = 'https://codeberg.org/octo/frozen', origin_detached_at = '2026-10-11T00:00:00.000Z' WHERE id = ?1",
40 ).run(made.value.id);
41 });
42 afterAll(async () => {
43 servers.stop();
44 app.close();
45 await rm(dataDir, { recursive: true, force: true });
46 });
47
48 const main = (page: string) => page.slice(page.indexOf("<main"), page.indexOf("</main>"));
49 const get = async (path: string, cookie: string) =>
50 main(await (await fetch(`${servers.url}${path}`, { headers: { cookie } })).text());
51
52 for (const path of ["/alice/frozen", "/alice/frozen/branches"]) {
53 test(`${path}: one line, no card; a reader sees the sentence only`, async () => {
54 const m = await get(path, reader);
55 const at = m.indexOf('id="frozen"');
56 expect(at).toBeGreaterThan(-1);
57 expect(m.slice(Math.max(0, at - 60), at)).not.toContain('class="card');
58 expect(m).not.toContain('class="card attn');
59 expect(m).toContain("This repository mirrors");
60 expect(m).toContain("An admin of this repository can reconnect it.");
61 expect(m).not.toContain("/settings/origin/reconnect");
62 });
63 }
64
65 test("an admin without an origin is pointed at Settings → Origins", async () => {
66 const m = await get("/alice/frozen", admin);
67 expect(m).toContain("Settings → Origins");
68 expect(m).not.toContain("/settings/origin/reconnect");
69 });
70
71 test("an admin with an origin on that site gets the same-origin reconnect form", async () => {
72 const alice = getUserByName(app.db, "alice")!;
73 createOrigin(app.db, dataDir, {
74 userId: alice.id, kind: "forgejo", apiUrl: "https://codeberg.org/api/v1",
75 webUrl: "https://codeberg.org", login: "alice-cb", token: "tok",
76 });
77 for (const path of ["/alice/frozen", "/alice/frozen/branches"]) {
78 const m = await get(path, admin);
79 expect(m).toContain('action="/alice/frozen/settings/origin/reconnect"');
80 expect(m).toContain("alice-cb at codeberg.org");
81 expect(m).not.toContain('class="card attn');
82 }
83 const cross = await fetch(`${servers.url}/alice/frozen/settings/origin/reconnect`, {
84 method: "POST", headers: { cookie: admin, origin: "https://evil.example", "content-type": "application/x-www-form-urlencoded" }, body: "origin=x",
85 });
86 expect(cross.status).toBeGreaterThanOrEqual(400);
87 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs