|
|
@@ -0,0 +1,176 @@ |
| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test"; |
| 9 |
import { mkdtemp, rm } from "node:fs/promises"; |
| 10 |
import { tmpdir } from "node:os"; |
| 11 |
import { join } from "node:path"; |
| 12 |
import { createApp, type App } from "../src/app.ts"; |
| 13 |
import { createSession } from "../src/core/sessions.ts"; |
| 14 |
import { createAgentRow } from "../src/core/agents.ts"; |
| 15 |
import { createUser } from "../src/core/users.ts"; |
| 16 |
import { getRepo } from "../src/core/repos.ts"; |
| 17 |
import { serve, type Servers } from "../src/http/server.ts"; |
| 18 |
import { syncsSettled } from "../src/origins/sync.ts"; |
| 19 |
import { mirrorAndWait, newFakeState, serveFakeForge, type FakeForge } from "./fake-origin.ts"; |
| 20 |
import { seedAdmin } from "./helpers/admin.ts"; |
| 21 |
import { proveAll } from "./helpers/verified.ts"; |
| 22 |
|
| 23 |
setDefaultTimeout(60_000); |
| 24 |
|
| 25 |
const LOGIN = "octo"; |
| 26 |
const TOKEN = "octo-private-secret-7e2a"; |
| 27 |
const PASSWORD = "alice-login-secret"; |
| 28 |
const NAME = "quillmoth"; |
| 29 |
const COMMIT = "zephyrmarker first commit"; |
| 30 |
const basic = (u: string, p: string) => `Basic ${Buffer.from(`${u}:${p}`).toString("base64")}`; |
| 31 |
|
| 32 |
describe("a mirror of a read-only origin is private to its owner", () => { |
| 33 |
const dirs: string[] = []; |
| 34 |
let app: App; |
| 35 |
let servers: Servers; |
| 36 |
let originApp: App; |
| 37 |
let originServers: Servers; |
| 38 |
let fake: FakeForge; |
| 39 |
let originId = ""; |
| 40 |
const cookies: Record<string, string> = {}; |
| 41 |
|
| 42 |
const api = (path: string, init?: RequestInit) => { |
| 43 |
const headers = new Headers(init?.headers); |
| 44 |
headers.set("content-type", "application/json"); |
| 45 |
headers.set("authorization", basic("alice", PASSWORD)); |
| 46 |
return fetch(`${servers.url}${path}`, { ...init, headers }); |
| 47 |
}; |
| 48 |
const as = (who: string, path: string) => |
| 49 |
fetch(`${servers.url}${path}`, { |
| 50 |
headers: cookies[who] === undefined ? {} : { cookie: cookies[who]! }, |
| 51 |
redirect: "manual", |
| 52 |
}); |
| 53 |
|
| 54 |
beforeAll(async () => { |
| 55 |
for (const p of ["forge-rop-", "forge-rop-up-"]) dirs.push(await mkdtemp(join(tmpdir(), p))); |
| 56 |
originApp = createApp({ dataDir: dirs[1]!, port: 0 }); |
| 57 |
originServers = await serve(originApp); |
| 58 |
await seedAdmin(originApp, { username: LOGIN, email: "[email protected]", password: TOKEN }); |
| 59 |
const oh = { "content-type": "application/json", authorization: basic(LOGIN, TOKEN) }; |
| 60 |
await fetch(`${originServers.url}/api/v1/repos`, { |
| 61 |
method: "POST", |
| 62 |
headers: oh, |
| 63 |
body: JSON.stringify({ name: NAME, visibility: "public" }), |
| 64 |
}); |
| 65 |
await fetch(`${originServers.url}/api/v1/repos/octo/${NAME}/contents`, { |
| 66 |
method: "POST", |
| 67 |
headers: oh, |
| 68 |
body: JSON.stringify({ branch: "main", message: COMMIT, files: [{ path: "a.txt", content: "one\n" }] }), |
| 69 |
}); |
| 70 |
const state = newFakeState("github", LOGIN, TOKEN); |
| 71 |
state.repos.push({ |
| 72 |
id: 9, |
| 73 |
fullName: `octo/${NAME}`, |
| 74 |
cloneUrl: `${originServers.url}/octo/${NAME}.git`, |
| 75 |
defaultBranch: "main", |
| 76 |
htmlUrl: `${originServers.url}/octo/${NAME}`, |
| 77 |
push: true, |
| 78 |
}); |
| 79 |
fake = serveFakeForge(state); |
| 80 |
|
| 81 |
app = createApp({ dataDir: dirs[0]!, port: 0 }); |
| 82 |
servers = await serve(app); |
| 83 |
const made = await seedAdmin(app, { username: "alice", email: "[email protected]", password: PASSWORD }); |
| 84 |
if (!made.ok) throw new Error(made.error); |
| 85 |
const bob = await createUser(app.db, { username: "bob", email: "[email protected]", password: "a-good-passphrase", origin: "seed", trusted: true }); |
| 86 |
const crew = await createUser(app.db, { username: "crewmate", email: "[email protected]", agent: true, origin: "agent" }); |
| 87 |
if (crew.ok) createAgentRow(app.db, { userId: crew.value.id, brief: "builds." }); |
| 88 |
if (!bob.ok || !crew.ok) throw new Error("users"); |
| 89 |
cookies["owner"] = `forge_session=${createSession(app.db, made.value.id).token}`; |
| 90 |
cookies["stranger"] = `forge_session=${createSession(app.db, bob.value.id).token}`; |
| 91 |
cookies["agent"] = `forge_session=${createSession(app.db, crew.value.id).token}`; |
| 92 |
|
| 93 |
const connected = (await ( |
| 94 |
await api("/api/v1/user/origins", { |
| 95 |
method: "POST", |
| 96 |
body: JSON.stringify({ host: fake.url, kind: "github", token: TOKEN, readOnly: true }), |
| 97 |
}) |
| 98 |
).json()) as { id: string }; |
| 99 |
originId = connected.id; |
| 100 |
await mirrorAndWait( |
| 101 |
await api(`/api/v1/user/origins/${originId}/mirror`, { |
| 102 |
method: "POST", |
| 103 |
body: JSON.stringify({ fullName: `octo/${NAME}` }), |
| 104 |
}), |
| 105 |
); |
| 106 |
await syncsSettled(); |
| 107 |
proveAll(app); |
| 108 |
}); |
| 109 |
|
| 110 |
afterAll(async () => { |
| 111 |
await syncsSettled(); |
| 112 |
fake.stop(); |
| 113 |
servers.stop(); |
| 114 |
originServers.stop(); |
| 115 |
app.close(); |
| 116 |
originApp.close(); |
| 117 |
for (const d of dirs) await rm(d, { recursive: true, force: true }); |
| 118 |
}); |
| 119 |
|
| 120 |
test("it is created private although its origin repo is public", () => { |
| 121 |
const repo = getRepo(app.db, "alice", NAME); |
| 122 |
expect(repo).toBeDefined(); |
| 123 |
expect(repo!.visibility).toBe("private"); |
| 124 |
}); |
| 125 |
|
| 126 |
test("it cannot be made public while the origin is read-only", async () => { |
| 127 |
const res = await api(`/api/v1/repos/alice/${NAME}`, { |
| 128 |
method: "PATCH", |
| 129 |
body: JSON.stringify({ visibility: "public" }), |
| 130 |
}); |
| 131 |
expect(res.status).toBe(409); |
| 132 |
expect(((await res.json()) as { error: string }).error).toBe("origin_read_only"); |
| 133 |
expect(getRepo(app.db, "alice", NAME)!.visibility).toBe("private"); |
| 134 |
}); |
| 135 |
|
| 136 |
const listings = [ |
| 137 |
"/", |
| 138 |
"/explore", |
| 139 |
"/api/v1/explore", |
| 140 |
"/search?q=quillmo", |
| 141 |
"/api/v1/search?q=quillmo&kind=all", |
| 142 |
"/api/v1/search?q=zephyrm&kind=all", |
| 143 |
"/changelog", |
| 144 |
"/changelog.atom", |
| 145 |
"/api/v1/changelog", |
| 146 |
"/talk", |
| 147 |
"/alice", |
| 148 |
"/api/v1/users/alice/repos", |
| 149 |
]; |
| 150 |
|
| 151 |
for (const who of ["anonymous", "stranger", "agent"]) { |
| 152 |
test(`${who} finds no trace of its name or commits`, async () => { |
| 153 |
for (const path of listings) { |
| 154 |
const res = await as(who, path); |
| 155 |
const body = await res.text(); |
| 156 |
expect(body, `${who} ${path}`).not.toContain(NAME); |
| 157 |
expect(body, `${who} ${path}`).not.toContain("zephyrmarker"); |
| 158 |
} |
| 159 |
}); |
| 160 |
} |
| 161 |
|
| 162 |
for (const who of ["anonymous", "stranger", "agent"]) { |
| 163 |
test(`${who} is told the repository does not exist`, async () => { |
| 164 |
expect((await as(who, `/alice/${NAME}`)).status).toBe(404); |
| 165 |
expect((await as(who, `/api/v1/repos/alice/${NAME}`)).status).toBe(404); |
| 166 |
}); |
| 167 |
} |
| 168 |
|
| 169 |
test("the owner finds it", async () => { |
| 170 |
for (const path of ["/search?q=quillmo", "/api/v1/search?q=quillmo&kind=all", `/search?q=${NAME}`, `/api/v1/search?q=${NAME}&kind=all`, "/alice", `/alice/${NAME}`, `/api/v1/repos/alice/${NAME}`]) { |
| 171 |
const res = await as("owner", path); |
| 172 |
expect(res.status, path).toBeLessThan(300); |
| 173 |
expect(await res.text(), path).toContain(NAME); |
| 174 |
} |
| 175 |
}); |
| 176 |
}); |