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

Fast test/migrate.test.ts: chop and rewrite under the budget (1 s a test, 10 s a file) #1283

Merged opened this

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

Merged

scout/forge:scout/1250-fast-test-migrate-test-ts-chop-and-rewri → main 2 commits fork

  • Merged. scout/1250-fast-test-migrate-test-ts-chop-and-rewri was merged into main as 967aa0d.

Review

Sign in to review this change.
2 files changed +177 −186
scripts/test-times.json +1 −1
@@ -24,7 +24,7 @@
2424 "test/helpers.test.ts": 0.7,
2525 "test/landing.test.ts": 9.4,
2626 "test/ledger.test.ts": 12.8,
27 "test/migrate.test.ts": 41.9,
27 "test/migrate.test.ts": 5.9,
2828 "test/offline.test.ts": 3.5,
2929 "test/pulls.test.ts": 14.6,
3030 "test/run-secrets.test.ts": 2.8,
test/migrate.test.ts +176 −185
@@ -17,7 +17,11 @@ import { mkdtemp, rm } from "node:fs/promises";
1717 import { tmpdir } from "node:os";
1818 import { join } from "node:path";
1919 import { createApp, type App } from "../src/app.ts";
20 import { migrateRepo, migrateUrl } from "../src/actions/migrate.ts";
20 import {
21 isPrivateHost,
22 migrateRepo,
23 migrateUrl,
24 } from "../src/actions/migrate.ts";
2125 import { createAgent, listCrew } from "../src/actions/crew.ts";
2226 import {
2327 commitToBranch,
@@ -28,25 +32,19 @@ import { getAppByRepo } from "../src/core/apps.ts";
2832 import { listIssues } from "../src/core/issues.ts";
2933 import { listOrigins } from "../src/core/origins.ts";
3034 import { getRepo, type Repo } from "../src/core/repos.ts";
31 import { createUser, getUserByName, type User } from "../src/core/users.ts";
35 import { createUser, type User } from "../src/core/users.ts";
3236 import { repoDir } from "../src/git/repos.ts";
3337 import { serve, type Servers } from "../src/http/server.ts";
38 import { createToken } from "../src/core/tokens.ts";
39 import { createSession } from "../src/core/sessions.ts";
3440 import { settled } from "../src/origins/jobs.ts";
3541 import { forgetProviders } from "../src/origins/providers/index.ts";
3642 import { newFakeState, serveFakeForge, type FakeForge } from "./fake-origin.ts";
3743 import { gitClientEnv } from "./helpers/git-client.ts";
3844 import { quietLogs } from "./helpers/globals.ts";
39 import { proveAll } from "./helpers/verified.ts";
4045
4146 quietLogs();
4247
43 const PASSWORD = "a-long-migrate-passphrase";
44 const UPSTREAM_LOGIN = "octo";
45 const UPSTREAM_SECRET = "octo-upstream-secret-7c1d";
46
47 const basic = (user: string, secret: string): string =>
48 `Basic ${Buffer.from(`${user}:${secret}`).toString("base64")}`;
49
5048 async function git(args: string[], cwd: string) {
5149 const proc = Bun.spawn(["git", ...args], {
5250 cwd,
@@ -86,34 +84,39 @@ async function allRefs(dir: string): Promise<string> {
8684
8785 describe("migrate", () => {
8886 let dataDir: string;
89 let upstreamDir: string;
9087 let work: string;
9188 let app: App;
9289 let servers: Servers;
93 let upstream: App;
94 let upstreamServers: Servers;
9590 let forgejo: FakeForge;
9691 let base: string;
9792 let alice: User;
9893 let bob: User;
9994 let aliceCookie = "";
10095 let bobCookie = "";
101 let fixture: Repo;
96 let bobToken = "";
10297 let fixtureUrl = "";
98 let originId = "";
99 // octo's credential at the "upstream": a token, since a password would
100 // cost an argon2 verify on every git request.
101 let upstreamSecret = "";
103102
104 const signUp = async (username: string): Promise<string> => {
105 const response = await fetch(`${base}/signup`, {
106 method: "POST",
107 body: new URLSearchParams({
108 username,
109 email: `${username}@forge.test`,
110 password: PASSWORD,
111 }),
112 redirect: "manual",
103 const person = async (
104 username: string,
105 adminIfFirst = false,
106 ): Promise<User> => {
107 const made = await createUser(app.db, {
108 trusted: true,
109 username,
110 email: `${username}@forge.test`,
111 adminIfFirst,
113112 });
114 return (response.headers.get("set-cookie") ?? "").split(";")[0]!;
113 if (!made.ok) throw new Error(String(made.error));
114 return made.value;
115115 };
116116
117 const cookieOf = (user: User): string =>
118 `forge_session=${createSession(app.db, user.id).token}`;
119
117120 const browser = (
118121 cookie: string,
119122 path: string,
@@ -132,34 +135,57 @@ describe("migrate", () => {
132135 ...(form === undefined ? {} : { body: new URLSearchParams(form) }),
133136 });
134137
135 const api = (
136 user: string,
137 path: string,
138 payload: unknown,
139 ): Promise<Response> =>
140 fetch(`${base}${path}`, {
138 /** The same call with a bearer token: no cookie, no origin header. */
139 const apiToken = (token: string, payload: unknown): Promise<Response> =>
140 fetch(`${base}/api/v1/migrate`, {
141141 method: "POST",
142142 headers: {
143143 "content-type": "application/json",
144 authorization: basic(user, PASSWORD),
144 authorization: `Bearer ${token}`,
145145 },
146146 body: JSON.stringify(payload),
147147 });
148148
149 const api = (cookie: string, payload: unknown): Promise<Response> =>
150 fetch(`${base}/api/v1/migrate`, {
151 method: "POST",
152 headers: {
153 "content-type": "application/json",
154 cookie,
155 origin: new URL(base).origin,
156 },
157 body: JSON.stringify(payload),
158 });
159
160 /** Start a migration and wait for it; the job is returned as it ended. */
161 const migrate = async (
162 user: User,
163 input: Parameters<typeof migrateRepo>[2],
164 ) => {
165 const started = await migrateRepo(app, user, input);
166 if (!started.ok) throw new Error(started.error);
167 return settled(started.value.job.id);
168 };
169
149170 beforeAll(async () => {
150171 dataDir = await mkdtemp(join(tmpdir(), "forge-migrate-"));
151 upstreamDir = await mkdtemp(join(tmpdir(), "forge-migrate-up-"));
152172 work = await mkdtemp(join(tmpdir(), "forge-migrate-work-"));
153173 // The fixture's source is the forge itself, on loopback: a private host,
154 // which a personal forge may say is fine.
174 // which a personal forge may say is fine. Its hook socket is what a
175 // real push needs; no argon2 per request (sessions and tokens, not
176 // passwords).
155177 app = createApp({ dataDir, port: 0, migrateFromPrivateHosts: true });
156178 servers = await serve(app);
157 base = servers.url;
158 aliceCookie = await signUp("alice");
159 bobCookie = await signUp("bob");
160 proveAll(app);
161 alice = getUserByName(app.db, "alice")!;
162 bob = getUserByName(app.db, "bob")!;
179 base = `http://127.0.0.1:${servers.port}`;
180 // No passwords: argon2 is the slow part of an account, and every
181 // request here rides a session or a token.
182 alice = await person("alice", true);
183 bob = await person("bob");
184 const octo = await person("octo");
185 upstreamSecret = createToken(app.db, octo.id, "upstream", "rw").token;
186 aliceCookie = cookieOf(alice);
187 bobCookie = cookieOf(bob);
188 bobToken = createToken(app.db, bob.id, "cli", "rw").token;
163189
164190 // The fixture: alice's public repository with three branches, two tags
165191 // (one annotated), and `trunk` as its default — not the forge's `main`.
@@ -168,26 +194,30 @@ describe("migrate", () => {
168194 visibility: "public",
169195 });
170196 if (!made.ok) throw new Error(made.error);
171 fixture = made.value;
172 const commit = async (branch: string, path: string, message: string) => {
173 const done = await commitToBranch(app, alice, fixture, {
197 const fixture = made.value;
198 const commit = async (
199 user: User,
200 repo: Repo,
201 branch: string,
202 path: string,
203 message: string,
204 ) => {
205 const done = await commitToBranch(app, user, repo, {
174206 branch,
175207 message,
176208 files: [{ path, content: `${message}\n` }],
177209 });
178210 if (!done.ok) throw new Error(String(done.error));
179 return done.value;
180211 };
181 await commit("main", "README.md", "first");
182 await commit("main", "a.txt", "second");
183 await commit("trunk", "trunk.txt", "on trunk");
184 await commit("feature", "feature.txt", "a feature");
185 await commit("feature", "feature2.txt", "more feature");
212 await commit(alice, fixture, "main", "README.md", "first");
213 await commit(alice, fixture, "main", "a.txt", "second");
214 await commit(alice, fixture, "trunk", "trunk.txt", "on trunk");
215 await commit(alice, fixture, "feature", "feature.txt", "a feature");
216 await commit(alice, fixture, "feature", "feature2.txt", "more feature");
186217 const settings = await updateRepoSettings(app, alice, fixture, {
187218 defaultBranch: "trunk",
188219 });
189220 if (!settings.ok) throw new Error(settings.error);
190 fixture = settings.value;
191221 const dir = repoDir(app.reposRoot, "alice", "fixture");
192222 expect((await git(["tag", "v1", "main~1"], dir)).code).toBe(0);
193223 expect(
@@ -195,59 +225,48 @@ describe("migrate", () => {
195225 ).toBe(0);
196226 fixtureUrl = `${base}/alice/fixture.git`;
197227
198 // A second forge holding a *private* repository, behind a fake origin
199 // whose clone URL points at it: only the origin's credential opens it.
200 upstream = createApp({ dataDir: upstreamDir, port: 0 });
201 upstreamServers = await serve(upstream);
202 await fetch(`${upstreamServers.url}/api/v1/users`, {
203 method: "POST",
204 headers: { "content-type": "application/json" },
205 body: JSON.stringify({
206 username: UPSTREAM_LOGIN,
207 email: "[email protected]",
208 password: UPSTREAM_SECRET,
209 }),
228 // A repository alice keeps private: no anonymous clone reaches it.
229 const hidden = await createRepo(app, alice, {
230 name: "hidden",
231 visibility: "private",
210232 });
211 const auth = { authorization: basic(UPSTREAM_LOGIN, UPSTREAM_SECRET) };
212 await fetch(`${upstreamServers.url}/api/v1/repos`, {
213 method: "POST",
214 headers: { "content-type": "application/json", ...auth },
215 body: JSON.stringify({ name: "secret-widgets", visibility: "private" }),
233 if (!hidden.ok) throw new Error(hidden.error);
234 await commit(alice, hidden.value, "main", "README.md", "secret");
235
236 // A private repository behind a fake origin whose clone URL points at
237 // it: only the origin's credential (octo's) opens it.
238 const widgets = await createRepo(app, octo, {
239 name: "secret-widgets",
240 visibility: "private",
216241 });
217 await fetch(
218 `${upstreamServers.url}/api/v1/repos/octo/secret-widgets/contents`,
219 {
220 method: "POST",
221 headers: { "content-type": "application/json", ...auth },
222 body: JSON.stringify({
223 branch: "main",
224 message: "first",
225 files: [{ path: "README.md", content: "# widgets\n" }],
226 }),
227 },
228 );
229 const state = newFakeState("forgejo", UPSTREAM_LOGIN, UPSTREAM_SECRET);
242 if (!widgets.ok) throw new Error(widgets.error);
243 await commit(octo, widgets.value, "main", "README.md", "first");
244 const state = newFakeState("forgejo", "octo", upstreamSecret);
230245 state.repos.push({
231246 id: 77,
232247 fullName: "octo/secret-widgets",
233248 description: "gears, privately",
234 cloneUrl: `${upstreamServers.url}/octo/secret-widgets.git`,
235 htmlUrl: `${upstreamServers.url}/octo/secret-widgets`,
249 cloneUrl: `${base}/octo/secret-widgets.git`,
250 htmlUrl: `${base}/octo/secret-widgets`,
236251 defaultBranch: "main",
237252 private: true,
238253 });
239254 forgejo = serveFakeForge(state);
240255 forgetProviders(dataDir);
241 });
256 const connected = await browser(aliceCookie, "/settings/origins/token", {
257 host: forgejo.url,
258 token: upstreamSecret,
259 });
260 expect(connected.status).toBe(303);
261 originId = listOrigins(app.db, alice.id)[0]!.id;
262 }, 30_000);
242263
243264 afterAll(async () => {
244265 forgejo.stop();
245266 servers.stop();
246 upstreamServers.stop();
247267 app.close();
248 upstream.close();
249268 forgetProviders(dataDir);
250 for (const dir of [dataDir, upstreamDir, work])
269 for (const dir of [dataDir, work])
251270 await rm(dir, { recursive: true, force: true });
252271 });
253272
@@ -255,13 +274,12 @@ describe("migrate", () => {
255274 const sourceDir = repoDir(app.reposRoot, "alice", "fixture");
256275 const sourceRefs = await allRefs(sourceDir);
257276
258 const response = await api("bob", "/api/v1/migrate", {
259 source: { url: fixtureUrl },
260 });
277 // By token, so the non-cookie door to the endpoint stays covered.
278 const response = await apiToken(bobToken, { source: { url: fixtureUrl } });
261279 expect(response.status).toBe(202);
262280 const started = (await response.json()) as {
263 repo: { full_name?: string; name: string };
264 job: { id: string; stage: string };
281 repo: { name: string };
282 job: { id: string };
265283 };
266284 expect(started.repo.name).toBe("fixture");
267285 const job = await settled(started.job.id);
@@ -282,7 +300,8 @@ describe("migrate", () => {
282300 expect((await git(["symbolic-ref", "HEAD"], dir)).out).toBe(
283301 "refs/heads/trunk",
284302 );
285 // Native: no origin row, no remote in its config — nothing to write through to.
303 // Native: no origin row, no remote in its config — nothing a push could
304 // write through to, so pushes land here and the source never hears.
286305 expect(migrated.origin).toBeNull();
287306 expect((await git(["config", "--get-regexp", "^remote\\."], dir)).out).toBe(
288307 "",
@@ -292,7 +311,7 @@ describe("migrate", () => {
292311 const clone = join(work, "bob-fixture");
293312 const url = new URL(`${base}/bob/fixture.git`);
294313 url.username = "bob";
295 url.password = PASSWORD;
314 url.password = bobToken;
296315 expect(
297316 (await git(["clone", "--quiet", url.toString(), clone], work)).code,
298317 ).toBe(0);
@@ -309,9 +328,9 @@ describe("migrate", () => {
309328 (await git(["rev-parse", "--verify", "refs/heads/bobs"], dir)).code,
310329 ).toBe(0);
311330 expect(await allRefs(sourceDir)).toBe(sourceRefs);
312 }, 60_000);
331 });
313332
314 test("refusals are words, and nothing is made", async () => {
333 test("a URL git must not run is refused in words before anything exists", async () => {
315334 const count = () =>
316335 (app.db.query("SELECT COUNT(*) AS n FROM repo").get() as { n: number }).n;
317336 const before = count();
@@ -326,52 +345,56 @@ describe("migrate", () => {
326345 `--upload-pack=touch ${canary}`,
327346 "https://user:[email protected]/a/b.git",
328347 "",
329 ]) {
330 const response = await api("bob", "/api/v1/migrate", { source: { url } });
331 expect(response.status).toBe(422);
332 const body = (await response.json()) as {
333 error: string;
334 message: string;
335 };
336 expect(body.error).toBe("invalid_url");
337 expect(body.message).toContain("not a URL this forge will clone from");
338 }
348 ])
349 expect(await migrateRepo(app, bob, { source: { url } })).toEqual({
350 ok: false,
351 error: "invalid_url",
352 });
339353 expect(existsSync(canary)).toBe(false);
354 // On the wire it is a 422 with words.
355 const response = await api(bobCookie, { source: { url: "file:///etc" } });
356 expect(response.status).toBe(422);
357 const body = (await response.json()) as { error: string; message: string };
358 expect(body.error).toBe("invalid_url");
359 expect(body.message).toContain("not a URL this forge will clone from");
340360 // What a person meant is what they get.
341361 expect(migrateUrl("github.com/a/b")).toBe("https://github.com/a/b");
342362 expect(migrateUrl("git://example.com/a/b.git")).toBe(
343363 "git://example.com/a/b.git",
344364 );
365 expect(count()).toBe(before);
366 });
345367
346 // bob already has a `fixture`.
347 const taken = await api("bob", "/api/v1/migrate", {
348 source: { url: fixtureUrl },
349 });
350 expect(taken.status).toBe(422);
351 expect(((await taken.json()) as { error: string }).error).toBe(
368 test("a name, an agent or a form that cannot work is refused before anything exists", async () => {
369 const made = await createRepo(app, bob, { name: "taken" });
370 expect(made.ok).toBe(true);
371 const count = () =>
372 (app.db.query("SELECT COUNT(*) AS n FROM repo").get() as { n: number }).n;
373 const before = count();
374 const refused = (input: Parameters<typeof migrateRepo>[2]) =>
375 migrateRepo(app, bob, input).then((r) => (r.ok ? "ok" : r.error));
376 expect(await refused({ source: { url: fixtureUrl }, name: "taken" })).toBe(
352377 "name_taken",
353378 );
354 const invalid = await api("bob", "/api/v1/migrate", {
355 source: { url: fixtureUrl },
356 name: "no spaces allowed",
357 });
358 expect(((await invalid.json()) as { error: string }).error).toBe(
359 "invalid_name",
360 );
361 const agent = await api("bob", "/api/v1/migrate", {
362 source: { url: fixtureUrl },
363 name: "other",
364 crew: { agent: "nobody-here", autonomous: false },
365 });
366 expect(((await agent.json()) as { error: string }).error).toBe(
367 "no_such_agent",
368 );
379 expect(
380 await refused({
381 source: { url: fixtureUrl },
382 name: "no spaces allowed",
383 }),
384 ).toBe("invalid_name");
385 expect(
386 await refused({
387 source: { url: fixtureUrl },
388 name: "other",
389 crew: { agent: "nobody-here", autonomous: false },
390 }),
391 ).toBe("no_such_agent");
369392
370393 // The same refusal on the web: the form again, with the reason in it.
371394 const page = await browser(bobCookie, "/migrate", {
372395 url: fixtureUrl,
373396 owner: "bob",
374 name: "",
397 name: "taken",
375398 });
376399 expect(page.status).toBe(422);
377400 expect(await page.text()).toContain(
@@ -386,25 +409,16 @@ describe("migrate", () => {
386409 });
387410
388411 test("a clone that fails leaves nothing behind", async () => {
389 // A private repository is not there for an anonymous clone.
390 const made = await createRepo(app, alice, {
391 name: "hidden",
392 visibility: "private",
393 });
394 expect(made.ok).toBe(true);
395 const started = await migrateRepo(app, bob, {
412 const job = await migrate(bob, {
396413 source: { url: `${base}/alice/hidden.git` },
397414 });
398 expect(started.ok).toBe(true);
399 if (!started.ok) return;
400 const job = await settled(started.value.job.id);
401415 expect(job?.stage).toBe("failed");
402416 expect(job?.error).toContain("could not be read");
403417 expect(getRepo(app.db, "bob", "hidden")).toBeUndefined();
404418 expect(existsSync(repoDir(app.reposRoot, "bob", "hidden"))).toBe(false);
405419 });
406420
407 test("with a crew: on the crew, an app, the importer's brief, autonomy when asked", async () => {
421 test("with a crew: on the crew, an app, the importer's brief, autonomy only when asked", async () => {
408422 const agent = await createAgent(app, alice, {
409423 username: "porter",
410424 model: "",
@@ -416,14 +430,11 @@ describe("migrate", () => {
416430 ["crewed", true],
417431 ["assisted", false],
418432 ] as const) {
419 const started = await migrateRepo(app, bob, {
433 const job = await migrate(bob, {
420434 source: { url: fixtureUrl },
421435 name,
422436 crew: { agent: "porter", autonomous },
423437 });
424 expect(started.ok).toBe(true);
425 if (!started.ok) return;
426 const job = await settled(started.value.job.id);
427438 expect(job?.error).toBeUndefined();
428439 expect(job?.stage).toBe("done");
429440 expect(job?.detail).toContain("porter is on it");
@@ -444,6 +455,8 @@ describe("migrate", () => {
444455 const issue = issues[0]!;
445456 expect(issue.title).toBe(`Make ${name} run here`);
446457 expect(issue.assigneeName).toBe("porter");
458 // The importer's brief is the same whether or not the crew is
459 // autonomous; autonomy is the repository's setting, asserted above.
447460 for (const words of [
448461 "Adapt, don't rewrite",
449462 "forge.json",
@@ -457,29 +470,19 @@ describe("migrate", () => {
457470 ])
458471 expect(issue.body).toContain(words);
459472 }
460 }, 60_000);
473 });
461474
462475 test("from an origin: the origin's credential opens a private repository", async () => {
463 const connected = await browser(aliceCookie, "/settings/origins/token", {
464 host: forgejo.url,
465 token: UPSTREAM_SECRET,
466 });
467 expect(connected.status).toBe(303);
468 const origin = listOrigins(app.db, alice.id)[0]!;
469
470476 // Anonymous, by URL: refused by the upstream, and nothing left here.
471 const anon = await migrateRepo(app, alice, {
472 source: { url: `${upstreamServers.url}/octo/secret-widgets.git` },
477 const anon = await migrate(alice, {
478 source: { url: `${base}/octo/secret-widgets.git` },
473479 });
474 expect(anon.ok).toBe(true);
475 if (!anon.ok) return;
476 expect((await settled(anon.value.job.id))?.stage).toBe("failed");
480 expect(anon?.stage).toBe("failed");
477481
478482 // Picked from the origin: the header opens it.
479 const response = await api("alice", "/api/v1/migrate", {
480 source: { originId: origin.id, fullName: "octo/secret-widgets" },
483 const response = await api(aliceCookie, {
484 source: { originId, fullName: "octo/secret-widgets" },
481485 });
482 // A password is the account itself; it clears the admin floor.
483486 expect(response.status).toBe(202);
484487 const started = (await response.json()) as { job: { id: string } };
485488 const job = await settled(started.job.id);
@@ -492,17 +495,17 @@ describe("migrate", () => {
492495 expect((await git(["log", "--format=%s", "main"], dir)).out).toBe("first");
493496 // The credential rode in the environment and stayed there.
494497 const config = await Bun.file(join(dir, "config")).text();
495 expect(config).not.toContain(UPSTREAM_SECRET);
498 expect(config).not.toContain(upstreamSecret);
496499 expect(config).not.toContain("extraHeader");
497500
498501 // Someone else's origin is not a source.
499 const bobs = await api("bob", "/api/v1/migrate", {
500 source: { originId: origin.id, fullName: "octo/secret-widgets" },
502 const bobs = await api(bobCookie, {
503 source: { originId, fullName: "octo/secret-widgets" },
501504 });
502505 expect(((await bobs.json()) as { error: string }).error).toBe(
503506 "no_such_origin",
504507 );
505 }, 60_000);
508 });
506509
507510 test("the page: the form, the plus menu, the job, and who may see it", async () => {
508511 const anonymous = await browser("", "/migrate");
@@ -559,10 +562,9 @@ describe("migrate", () => {
559562 ).text();
560563 expect(repoPage).toContain("Migrated as alice/copy");
561564 expect(getRepo(app.db, "alice", "copy")?.autonomy).toBe("autonomous");
562 }, 60_000);
565 });
563566
564567 test("a pasted address inside the network is an admin's alone, unless the forge says otherwise", async () => {
565 const { isPrivateHost } = await import("../src/actions/migrate.ts");
566568 for (const h of [
567569 "127.0.0.1",
568570 "localhost",
@@ -593,33 +595,25 @@ describe("migrate", () => {
593595 "[2001:db8::1]",
594596 ])
595597 expect(isPrivateHost(h)).toBe(false);
596 const strictDir = await mkdtemp(join(tmpdir(), "forge-migrate-strict-"));
597 const strict = createApp({ dataDir: strictDir, port: 0 });
598 // The forge without the personal-forge switch: bob is refused, alice
599 // (the admin) is not. The flag is flipped in place: this only proves
600 // anything because migrateRepo reads it on every call, which the
601 // refusal below (the app was built with it true) fails without.
602 const config = app.config as { migrateFromPrivateHosts?: boolean };
603 config.migrateFromPrivateHosts = false;
598604 try {
599 const a = await createUser(strict.db, { trusted: true,
600 username: "ann",
601 email: "[email protected]",
602 password: "a-good-passphrase",
603 adminIfFirst: true,
604 });
605 const b = await createUser(strict.db, { trusted: true,
606 username: "ben",
607 email: "[email protected]",
608 password: "a-good-passphrase",
609 });
610 if (!a.ok || !b.ok) throw new Error("users");
611 const refused = await migrateRepo(strict, b.value, {
605 const refused = await migrateRepo(app, bob, {
612606 source: { url: `${base}/alice/hidden.git` },
613607 });
614608 expect(refused).toEqual({ ok: false, error: "private_host" });
615 const allowed = await migrateRepo(strict, a.value, {
609 const allowed = await migrateRepo(app, alice, {
616610 source: { url: `${base}/alice/hidden.git` },
617611 name: "peek",
618612 });
619613 expect(allowed.ok).toBe(true);
614 if (allowed.ok) await settled(allowed.value.job.id);
620615 } finally {
621 strict.close();
622 await rm(strictDir, { recursive: true, force: true });
616 config.migrateFromPrivateHosts = true;
623617 }
624618 });
625619
@@ -646,13 +640,10 @@ describe("migrate", () => {
646640 },
647641 });
648642 try {
649 const started = await migrateRepo(app, alice, {
643 const job = await migrate(alice, {
650644 source: { url: `http://127.0.0.1:${hop.port}/hop.git` },
651645 name: "hopped",
652646 });
653 expect(started.ok).toBe(true);
654 if (!started.ok) return;
655 const job = await settled(started.value.job.id);
656647 expect(job?.stage).toBe("failed");
657648 expect(hits).toBe(0);
658649 } finally {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs