Public: anyone can read this; the crew can change it.
A test pins the fork button and the upstream link: no fork of your own repository, and the link stays inside the forge #6929
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6908-a-test-pins-the-fork-button-and-the-upst → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +77
−2
src/web/scope.ts +2 −2
| @@ -114,9 +114,9 @@ export interface ProposeTarget { | ||
| 114 | 114 | const forkNameFor = (login: string, fullName: string): string => |
| 115 | 115 | `${login}/${fullName.split("/")[1] ?? fullName}`; |
| 116 | 116 | |
| 117 | /** `codeberg.org` out of `https://codeberg.org/octo/widgets`. */ | |
| 117 | /** `codeberg.org` out of `https://codeberg.org/octo/widgets`; "" for anything that is not a URL with a host. */ | |
| 118 | 118 | export function hostOf(url: string): string { |
| 119 | return URL.parse(url)?.host ?? url; | |
| 119 | return URL.parse(url)?.host ?? ""; | |
| 120 | 120 | } |
| 121 | 121 | |
| 122 | 122 | // ---- building one ---- |
test/web-scope.test.ts added +75
| @@ -0,0 +1,75 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { | |
| 3 | forkButton, | |
| 4 | hostOf, | |
| 5 | upstreamHref, | |
| 6 | type RepoScope, | |
| 7 | } from "../src/web/scope.ts"; | |
| 8 | ||
| 9 | const origin = (login: string) => ({ | |
| 10 | id: "o1", | |
| 11 | login, | |
| 12 | webUrl: "https://codeberg.org", | |
| 13 | }); | |
| 14 | ||
| 15 | const scope = (over: Record<string, unknown>): RepoScope => | |
| 16 | ({ | |
| 17 | kind: "origin", | |
| 18 | base: "/origins/o1/r/octo/widgets", | |
| 19 | fullName: "octo/widgets", | |
| 20 | origin: origin("me"), | |
| 21 | repo: undefined, | |
| 22 | upstream: undefined, | |
| 23 | ...over, | |
| 24 | }) as unknown as RepoScope; | |
| 25 | ||
| 26 | test("the fork button is offered on someone else's repository", () => { | |
| 27 | const out = forkButton(scope({})).toString(); | |
| 28 | expect(out).toContain('action="/origins/o1/r/octo/widgets/fork"'); | |
| 29 | expect(out).toContain("codeberg.org"); | |
| 30 | }); | |
| 31 | ||
| 32 | test("no fork button on a repository the account owns, in any case", () => { | |
| 33 | expect(forkButton(scope({ fullName: "me/widgets" })).toString()).toBe(""); | |
| 34 | expect(forkButton(scope({ fullName: "ME/widgets" })).toString()).toBe(""); | |
| 35 | }); | |
| 36 | ||
| 37 | test("no fork button on a local scope or without an origin", () => { | |
| 38 | expect(forkButton(scope({ kind: "local" })).toString()).toBe(""); | |
| 39 | expect(forkButton(scope({ origin: undefined })).toString()).toBe(""); | |
| 40 | }); | |
| 41 | ||
| 42 | test("a repository name with markup is escaped in the button", () => { | |
| 43 | const out = forkButton( | |
| 44 | scope({ fullName: 'octo/<img src=x onerror=1>"' }), | |
| 45 | ).toString(); | |
| 46 | expect(out).not.toContain("<img"); | |
| 47 | expect(out).toContain("<img"); | |
| 48 | expect(out).toContain("""); | |
| 49 | }); | |
| 50 | ||
| 51 | test("upstreamHref is a path on this forge, never an absolute URL", () => { | |
| 52 | const href = upstreamHref(scope({ upstream: "up/widgets" })); | |
| 53 | expect(href).toBe("/origins/o1/r/up/widgets"); | |
| 54 | expect(href).not.toContain("codeberg.org"); | |
| 55 | expect(href?.startsWith("/")).toBe(true); | |
| 56 | }); | |
| 57 | ||
| 58 | test("upstreamHref reads the mirror's origin id and is undefined without upstream", () => { | |
| 59 | const mirror = scope({ | |
| 60 | origin: undefined, | |
| 61 | repo: { origin: { originId: "o9" } }, | |
| 62 | upstream: "up/widgets", | |
| 63 | }); | |
| 64 | expect(upstreamHref(mirror)).toBe("/origins/o9/r/up/widgets"); | |
| 65 | expect(upstreamHref(scope({}))).toBeUndefined(); | |
| 66 | expect( | |
| 67 | upstreamHref(scope({ origin: undefined, upstream: "a/b" })), | |
| 68 | ).toBeUndefined(); | |
| 69 | }); | |
| 70 | ||
| 71 | test("hostOf gives the host, and nothing for garbage or javascript:", () => { | |
| 72 | expect(hostOf("https://codeberg.org/octo/widgets")).toBe("codeberg.org"); | |
| 73 | expect(hostOf("not a url")).toBe(""); | |
| 74 | expect(hostOf("javascript:alert(1)")).toBe(""); | |
| 75 | }); |
new above
offline
Good: "" for a non-URL is what
site.ts's|| o.fullNamewas already expecting. Note for later: theat.htmlUrl === "" ? at.fullName : hostOf(at.htmlUrl)sites at ~352 and ~514 would draw an empty label for a non-URLhtmlUrl;hostOf(at.htmlUrl) || at.fullNamecovers both cases.