Public: anyone can read this; the crew can change it.
test/parity.test.ts and test/public-url.test.ts sign up their own accounts and run in the gate #11094
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/11082-test-parity-test-ts-and-test-public-url → main 2 commits fork
3 files
changed +105
−60
scripts/gate.ts −10
| @@ -54,11 +54,6 @@ export const EXCLUDED: readonly Exclusion[] = [ | ||
| 54 | 54 | reason: "not yet admitted", |
| 55 | 55 | issue: 261, |
| 56 | 56 | }, |
| 57 | { | |
| 58 | file: "test/parity.test.ts", | |
| 59 | reason: "red alone under --randomize (2026-11-10): a pull reopened, team listings and profile edits depend on earlier tests' state", | |
| 60 | issue: 11049, | |
| 61 | }, | |
| 62 | 57 | { |
| 63 | 58 | file: "test/probe.test.ts", |
| 64 | 59 | reason: "red alone under --randomize (2026-11-10): a scaffolded app obeys every cell: verdict mismatch, not ok", |
| @@ -70,11 +65,6 @@ export const EXCLUDED: readonly Exclusion[] = [ | ||
| 70 | 65 | issue: 11049, |
| 71 | 66 | }, |
| 72 | 67 | { file: "test/prompts.test.ts", reason: "not yet admitted", issue: 261 }, |
| 73 | { | |
| 74 | file: "test/public-url.test.ts", | |
| 75 | reason: "red alone under --randomize (2026-11-10): clone URLs name the public address: create answers 401 when run before signup", | |
| 76 | issue: 11049, | |
| 77 | }, | |
| 78 | 68 | { |
| 79 | 69 | file: "test/rehearsal-real.test.ts", |
| 80 | 70 | reason: "red on main@accaa65 under the gate: the second real roll's old forge does not come up", |
test/parity.test.ts +91 −42
| @@ -184,6 +184,7 @@ describe("parity: full control through actions and the API", () => { | ||
| 184 | 184 | expect(profile.json.email).toBeUndefined(); |
| 185 | 185 | expect(typeof profile.json.createdAt).toBe("string"); |
| 186 | 186 | expect((await call("GET", "/api/v1/users/nobody")).status).toBe(404); |
| 187 | const updatedBefore = kinds().filter((k) => k === "user.updated").length; | |
| 187 | 188 | |
| 188 | 189 | // An rw token is not the account; an admin token or the session is. |
| 189 | 190 | expect( |
| @@ -219,7 +220,7 @@ describe("parity: full control through actions and the API", () => { | ||
| 219 | 220 | }) |
| 220 | 221 | ).json.error, |
| 221 | 222 | ).toBe("invalid_email"); |
| 222 | expect(kinds().filter((k) => k === "user.updated")).toHaveLength(2); | |
| 223 | expect(kinds().filter((k) => k === "user.updated")).toHaveLength(updatedBefore + 2); | |
| 223 | 224 | }); |
| 224 | 225 | |
| 225 | 226 | test("changing the password needs the current one and never an rw token", async () => { |
| @@ -368,10 +369,21 @@ describe("parity: full control through actions and the API", () => { | ||
| 368 | 369 | // ── labels ─────────────────────────────────────────────────────────── |
| 369 | 370 | |
| 370 | 371 | test("labels: admin-managed, writer-assigned, visible to readers, filterable", async () => { |
| 372 | // A repository of its own, so no other test's labels or issues show here. | |
| 373 | // Alice may have changed her email in another test: taken as proven. | |
| 374 | proveAll(app); | |
| 375 | expect( | |
| 376 | ( | |
| 377 | await call("POST", "/api/v1/repos", { | |
| 378 | token: aliceRw, | |
| 379 | body: { name: "labeled", visibility: "public" }, | |
| 380 | }) | |
| 381 | ).status, | |
| 382 | ).toBe(201); | |
| 371 | 383 | // Creating a label is repo administration. |
| 372 | 384 | expect( |
| 373 | 385 | ( |
| 374 | await call("POST", "/api/v1/repos/alice/pub/labels", { | |
| 386 | await call("POST", "/api/v1/repos/alice/labeled/labels", { | |
| 375 | 387 | token: aliceRw, |
| 376 | 388 | body: { name: "bug", color: "ff0000" }, |
| 377 | 389 | }) |
| @@ -379,33 +391,33 @@ describe("parity: full control through actions and the API", () => { | ||
| 379 | 391 | ).toBe(403); |
| 380 | 392 | expect( |
| 381 | 393 | ( |
| 382 | await call("POST", "/api/v1/repos/alice/pub/labels", { | |
| 394 | await call("POST", "/api/v1/repos/alice/labeled/labels", { | |
| 383 | 395 | token: bobRw, |
| 384 | 396 | body: { name: "bug", color: "ff0000" }, |
| 385 | 397 | }) |
| 386 | 398 | ).status, |
| 387 | 399 | ).toBe(403); |
| 388 | const bug = await call("POST", "/api/v1/repos/alice/pub/labels", { | |
| 400 | const bug = await call("POST", "/api/v1/repos/alice/labeled/labels", { | |
| 389 | 401 | token: aliceAdmin, |
| 390 | 402 | body: { name: "bug", color: "ff0000" }, |
| 391 | 403 | }); |
| 392 | 404 | expect(bug.status).toBe(201); |
| 393 | const queue = await call("POST", "/api/v1/repos/alice/pub/labels", { | |
| 405 | const queue = await call("POST", "/api/v1/repos/alice/labeled/labels", { | |
| 394 | 406 | token: aliceAdmin, |
| 395 | 407 | body: { name: "queue", color: "00ff00" }, |
| 396 | 408 | }); |
| 397 | 409 | expect(queue.status).toBe(201); |
| 398 | 410 | expect( |
| 399 | ((await call("GET", "/api/v1/repos/alice/pub/labels")).json.items as Json[]).map( | |
| 411 | ((await call("GET", "/api/v1/repos/alice/labeled/labels")).json.items as Json[]).map( | |
| 400 | 412 | (l) => l.name, |
| 401 | 413 | ), |
| 402 | 414 | ).toEqual(["bug", "queue"]); |
| 403 | 415 | |
| 404 | const one = await call("POST", "/api/v1/repos/alice/pub/issues", { | |
| 416 | const one = await call("POST", "/api/v1/repos/alice/labeled/issues", { | |
| 405 | 417 | token: bobRw, |
| 406 | 418 | body: { title: "first issue" }, |
| 407 | 419 | }); |
| 408 | const two = await call("POST", "/api/v1/repos/alice/pub/issues", { | |
| 420 | const two = await call("POST", "/api/v1/repos/alice/labeled/issues", { | |
| 409 | 421 | token: aliceRw, |
| 410 | 422 | body: { title: "second issue" }, |
| 411 | 423 | }); |
| @@ -416,7 +428,7 @@ describe("parity: full control through actions and the API", () => { | ||
| 416 | 428 | // Bob can read (and even authored #1) but is not a writer: forbidden. |
| 417 | 429 | expect( |
| 418 | 430 | ( |
| 419 | await call("PUT", `/api/v1/repos/alice/pub/issues/${n1}/labels`, { | |
| 431 | await call("PUT", `/api/v1/repos/alice/labeled/issues/${n1}/labels`, { | |
| 420 | 432 | token: bobRw, |
| 421 | 433 | body: { labels: ["bug"] }, |
| 422 | 434 | }) |
| @@ -425,7 +437,7 @@ describe("parity: full control through actions and the API", () => { | ||
| 425 | 437 | // A read-only token fails the scope floor before anything else. |
| 426 | 438 | expect( |
| 427 | 439 | ( |
| 428 | await call("PUT", `/api/v1/repos/alice/pub/issues/${n1}/labels`, { | |
| 440 | await call("PUT", `/api/v1/repos/alice/labeled/issues/${n1}/labels`, { | |
| 429 | 441 | token: aliceRo, |
| 430 | 442 | body: { labels: ["bug"] }, |
| 431 | 443 | }) |
| @@ -434,7 +446,7 @@ describe("parity: full control through actions and the API", () => { | ||
| 434 | 446 | // Unknown labels reject the whole set. |
| 435 | 447 | const unknown = await call( |
| 436 | 448 | "PUT", |
| 437 | `/api/v1/repos/alice/pub/issues/${n1}/labels`, | |
| 449 | `/api/v1/repos/alice/labeled/issues/${n1}/labels`, | |
| 438 | 450 | { |
| 439 | 451 | token: aliceRw, |
| 440 | 452 | body: { labels: ["bug", "ghost"] }, |
| @@ -443,13 +455,13 @@ describe("parity: full control through actions and the API", () => { | ||
| 443 | 455 | expect(unknown.status).toBe(422); |
| 444 | 456 | expect(unknown.json.error).toBe("unknown_label"); |
| 445 | 457 | expect( |
| 446 | (await call("GET", `/api/v1/repos/alice/pub/issues/${n1}`)).json.labels, | |
| 458 | (await call("GET", `/api/v1/repos/alice/labeled/issues/${n1}`)).json.labels, | |
| 447 | 459 | ).toEqual([]); |
| 448 | 460 | |
| 449 | 461 | // By name and by id, case-insensitive, de-duplicated. |
| 450 | 462 | const set = await call( |
| 451 | 463 | "PUT", |
| 452 | `/api/v1/repos/alice/pub/issues/${n1}/labels`, | |
| 464 | `/api/v1/repos/alice/labeled/issues/${n1}/labels`, | |
| 453 | 465 | { |
| 454 | 466 | token: aliceRw, |
| 455 | 467 | body: { labels: ["BUG", bug.json.id, "queue"] }, |
| @@ -457,16 +469,16 @@ describe("parity: full control through actions and the API", () => { | ||
| 457 | 469 | ); |
| 458 | 470 | expect(set.status).toBe(200); |
| 459 | 471 | expect(set.list.map((l) => l.name)).toEqual(["bug", "queue"]); |
| 460 | await call("PUT", `/api/v1/repos/alice/pub/issues/${n2}/labels`, { | |
| 472 | await call("PUT", `/api/v1/repos/alice/labeled/issues/${n2}/labels`, { | |
| 461 | 473 | token: aliceRw, |
| 462 | 474 | body: { labels: ["queue"] }, |
| 463 | 475 | }); |
| 464 | 476 | |
| 465 | const bugs = await call("GET", "/api/v1/repos/alice/pub/issues?label=bug"); | |
| 477 | const bugs = await call("GET", "/api/v1/repos/alice/labeled/issues?label=bug"); | |
| 466 | 478 | expect((bugs.json.items as Json[]).map((i) => i.number)).toEqual([n1]); |
| 467 | 479 | const queued = await call( |
| 468 | 480 | "GET", |
| 469 | "/api/v1/repos/alice/pub/issues?label=queue", | |
| 481 | "/api/v1/repos/alice/labeled/issues?label=queue", | |
| 470 | 482 | ); |
| 471 | 483 | expect((queued.json.items as Json[]).map((i) => i.number)).toEqual([ |
| 472 | 484 | n2, |
| @@ -477,7 +489,7 @@ describe("parity: full control through actions and the API", () => { | ||
| 477 | 489 | ).toEqual(["queue"]); |
| 478 | 490 | const byBob = await call( |
| 479 | 491 | "GET", |
| 480 | "/api/v1/repos/alice/pub/issues?author=bob", | |
| 492 | "/api/v1/repos/alice/labeled/issues?author=bob", | |
| 481 | 493 | ); |
| 482 | 494 | expect((byBob.json.items as Json[]).map((i) => i.number)).toEqual([n1]); |
| 483 | 495 | |
| @@ -486,7 +498,7 @@ describe("parity: full control through actions and the API", () => { | ||
| 486 | 498 | ( |
| 487 | 499 | await call( |
| 488 | 500 | "DELETE", |
| 489 | `/api/v1/repos/alice/pub/labels/${queue.json.id}`, | |
| 501 | `/api/v1/repos/alice/labeled/labels/${queue.json.id}`, | |
| 490 | 502 | { |
| 491 | 503 | token: aliceAdmin, |
| 492 | 504 | }, |
| @@ -495,7 +507,7 @@ describe("parity: full control through actions and the API", () => { | ||
| 495 | 507 | ).toBe(204); |
| 496 | 508 | expect( |
| 497 | 509 | ( |
| 498 | (await call("GET", `/api/v1/repos/alice/pub/issues/${n2}`)).json | |
| 510 | (await call("GET", `/api/v1/repos/alice/labeled/issues/${n2}`)).json | |
| 499 | 511 | .labels as Json[] |
| 500 | 512 | ).length, |
| 501 | 513 | ).toBe(0); |
| @@ -513,6 +525,14 @@ describe("parity: full control through actions and the API", () => { | ||
| 513 | 525 | }); |
| 514 | 526 | expect(pr.status).toBe(201); |
| 515 | 527 | const n = pr.json.number as number; |
| 528 | expect( | |
| 529 | ( | |
| 530 | await call("POST", "/api/v1/repos/alice/pub/labels", { | |
| 531 | token: aliceAdmin, | |
| 532 | body: { name: "bug", color: "ff0000" }, | |
| 533 | }) | |
| 534 | ).status, | |
| 535 | ).toBe(201); | |
| 516 | 536 | |
| 517 | 537 | // The pull number works on the labels endpoint (one table). |
| 518 | 538 | const labeled = await call( |
| @@ -855,11 +875,13 @@ describe("parity: full control through actions and the API", () => { | ||
| 855 | 875 | |
| 856 | 876 | const myOrgs = await call("GET", "/api/v1/user/orgs", { token: aliceRo }); |
| 857 | 877 | const orgItems = (r: { json: Json }) => (r.json.items ?? []) as Json[]; |
| 858 | expect(orgItems(myOrgs).map((o) => o.username)).toEqual(["acme"]); | |
| 878 | expect(orgItems(myOrgs).map((o) => o.username)).toContain("acme"); | |
| 859 | 879 | expect(myOrgs.json.next_cursor).toBeNull(); |
| 860 | 880 | expect( |
| 861 | orgItems(await call("GET", "/api/v1/user/orgs", { token: bobRw })), | |
| 862 | ).toEqual([]); | |
| 881 | orgItems(await call("GET", "/api/v1/user/orgs", { token: bobRw })).map( | |
| 882 | (o) => o.username, | |
| 883 | ), | |
| 884 | ).not.toContain("acme"); | |
| 863 | 885 | for (const kind of [ |
| 864 | 886 | "team.created", |
| 865 | 887 | "team.deleted", |
| @@ -870,49 +892,63 @@ describe("parity: full control through actions and the API", () => { | ||
| 870 | 892 | }); |
| 871 | 893 | |
| 872 | 894 | test("teams and members page as { items, next_cursor }", async () => { |
| 873 | await call("POST", "/api/v1/orgs/acme/teams", { | |
| 895 | // An org of its own: Owners, devs and zeta, nothing another test made. | |
| 896 | proveAll(app); | |
| 897 | expect( | |
| 898 | (await call("POST", "/api/v1/orgs", { token: aliceRw, body: { name: "paging" } })) | |
| 899 | .status, | |
| 900 | ).toBe(201); | |
| 901 | expect( | |
| 902 | ( | |
| 903 | await call("POST", "/api/v1/orgs/paging/teams", { | |
| 904 | token: aliceAdmin, | |
| 905 | body: { name: "devs", permission: "write" }, | |
| 906 | }) | |
| 907 | ).status, | |
| 908 | ).toBe(201); | |
| 909 | await call("POST", "/api/v1/orgs/paging/teams", { | |
| 874 | 910 | token: aliceAdmin, |
| 875 | 911 | body: { name: "zeta", permission: "read" }, |
| 876 | 912 | }); |
| 877 | const all = await call("GET", "/api/v1/orgs/acme/teams", { token: aliceAdmin }); | |
| 913 | const all = await call("GET", "/api/v1/orgs/paging/teams", { token: aliceAdmin }); | |
| 878 | 914 | const names = (all.json.items as Json[]).map((t) => t.name as string); |
| 879 | 915 | expect(names.length).toBeGreaterThanOrEqual(3); |
| 880 | 916 | expect(all.json.next_cursor).toBeNull(); |
| 881 | const first = await call("GET", "/api/v1/orgs/acme/teams?limit=2", { token: aliceAdmin }); | |
| 917 | const first = await call("GET", "/api/v1/orgs/paging/teams?limit=2", { token: aliceAdmin }); | |
| 882 | 918 | expect((first.json.items as Json[]).map((t) => t.name)).toEqual(names.slice(0, 2)); |
| 883 | 919 | expect(first.json.next_cursor).toBe(names[1]!); |
| 884 | 920 | const second = await call( |
| 885 | 921 | "GET", |
| 886 | `/api/v1/orgs/acme/teams?limit=200&cursor=${names[1]}`, | |
| 922 | `/api/v1/orgs/paging/teams?limit=200&cursor=${names[1]}`, | |
| 887 | 923 | { token: aliceAdmin }, |
| 888 | 924 | ); |
| 889 | 925 | expect((second.json.items as Json[]).map((t) => t.name)).toEqual(names.slice(2)); |
| 890 | 926 | expect(second.json.next_cursor).toBeNull(); |
| 891 | 927 | |
| 892 | 928 | for (const user of ["bob", "carol"]) |
| 893 | await call("POST", "/api/v1/orgs/acme/teams/devs/members", { | |
| 929 | await call("POST", "/api/v1/orgs/paging/teams/devs/members", { | |
| 894 | 930 | token: aliceAdmin, |
| 895 | 931 | body: { user }, |
| 896 | 932 | }); |
| 897 | const m1 = await call("GET", "/api/v1/orgs/acme/teams/devs/members?limit=1", { | |
| 933 | const m1 = await call("GET", "/api/v1/orgs/paging/teams/devs/members?limit=1", { | |
| 898 | 934 | token: aliceAdmin, |
| 899 | 935 | }); |
| 900 | 936 | expect((m1.json.items as Json[]).map((m) => m.username)).toEqual(["bob"]); |
| 901 | 937 | expect(m1.json.next_cursor).toBe("bob"); |
| 902 | const m2 = await call("GET", "/api/v1/orgs/acme/teams/devs/members?limit=1&cursor=bob", { | |
| 938 | const m2 = await call("GET", "/api/v1/orgs/paging/teams/devs/members?limit=1&cursor=bob", { | |
| 903 | 939 | token: aliceAdmin, |
| 904 | 940 | }); |
| 905 | 941 | expect((m2.json.items as Json[]).map((m) => m.username)).toEqual(["carol"]); |
| 906 | 942 | expect(m2.json.next_cursor).toBeNull(); |
| 907 | 943 | |
| 908 | 944 | // A member of a private team is not shown to an outsider. |
| 909 | const outsider = await call("GET", "/api/v1/orgs/acme/teams/zeta/members", { token: bobRw }); | |
| 945 | const outsider = await call("GET", "/api/v1/orgs/paging/teams/zeta/members", { token: bobRw }); | |
| 910 | 946 | expect(outsider.status).toBe(404); |
| 911 | 947 | expect(outsider.json.items).toBeUndefined(); |
| 912 | 948 | |
| 913 | 949 | for (const path of ["teams", "teams/devs/members"]) |
| 914 | 950 | for (const bad of ["0", "201", "-1", "abc", "1.5"]) { |
| 915 | const res = await call("GET", `/api/v1/orgs/acme/${path}?limit=${bad}`, { | |
| 951 | const res = await call("GET", `/api/v1/orgs/paging/${path}?limit=${bad}`, { | |
| 916 | 952 | token: aliceAdmin, |
| 917 | 953 | }); |
| 918 | 954 | expect(res.status).toBe(422); |
| @@ -922,7 +958,7 @@ describe("parity: full control through actions and the API", () => { | ||
| 922 | 958 | // The cursor is only compared: any 1..160 characters, nothing else. |
| 923 | 959 | for (const path of ["teams", "teams/devs/members"]) |
| 924 | 960 | for (const bad of ["", "x".repeat(161)]) { |
| 925 | const res = await call("GET", `/api/v1/orgs/acme/${path}?cursor=${bad}`, { | |
| 961 | const res = await call("GET", `/api/v1/orgs/paging/${path}?cursor=${bad}`, { | |
| 926 | 962 | token: aliceAdmin, |
| 927 | 963 | }); |
| 928 | 964 | expect(res.status).toBe(422); |
| @@ -930,21 +966,21 @@ describe("parity: full control through actions and the API", () => { | ||
| 930 | 966 | } |
| 931 | 967 | |
| 932 | 968 | // A name with a control character pages on: its own cursor is accepted. |
| 933 | const odd = await call("POST", "/api/v1/orgs/acme/teams", { | |
| 969 | const odd = await call("POST", "/api/v1/orgs/paging/teams", { | |
| 934 | 970 | token: aliceAdmin, |
| 935 | 971 | body: { name: "a\u0001b", permission: "read" }, |
| 936 | 972 | }); |
| 937 | 973 | expect(odd.status).toBe(201); |
| 938 | 974 | { |
| 939 | const list = await call("GET", "/api/v1/orgs/acme/teams", { token: aliceAdmin }); | |
| 975 | const list = await call("GET", "/api/v1/orgs/paging/teams", { token: aliceAdmin }); | |
| 940 | 976 | const at = (list.json.items as Json[]).findIndex((t) => t.name === "a\u0001b"); |
| 941 | const page = await call("GET", `/api/v1/orgs/acme/teams?limit=${at + 1}`, { | |
| 977 | const page = await call("GET", `/api/v1/orgs/paging/teams?limit=${at + 1}`, { | |
| 942 | 978 | token: aliceAdmin, |
| 943 | 979 | }); |
| 944 | 980 | expect(page.json.next_cursor).toBe("a\u0001b"); |
| 945 | 981 | const next = await call( |
| 946 | 982 | "GET", |
| 947 | `/api/v1/orgs/acme/teams?cursor=${encodeURIComponent("a\u0001b")}`, | |
| 983 | `/api/v1/orgs/paging/teams?cursor=${encodeURIComponent("a\u0001b")}`, | |
| 948 | 984 | { token: aliceAdmin }, |
| 949 | 985 | ); |
| 950 | 986 | expect(next.status).toBe(200); |
| @@ -952,21 +988,34 @@ describe("parity: full control through actions and the API", () => { | ||
| 952 | 988 | |
| 953 | 989 | // The default page is 50. |
| 954 | 990 | for (let i = 0; i < 50; i++) |
| 955 | await call("POST", "/api/v1/orgs/acme/teams", { | |
| 991 | await call("POST", "/api/v1/orgs/paging/teams", { | |
| 956 | 992 | token: aliceAdmin, |
| 957 | 993 | body: { name: `p${String(i).padStart(2, "0")}`, permission: "read" }, |
| 958 | 994 | }); |
| 959 | const dflt = await call("GET", "/api/v1/orgs/acme/teams", { token: aliceAdmin }); | |
| 995 | const dflt = await call("GET", "/api/v1/orgs/paging/teams", { token: aliceAdmin }); | |
| 960 | 996 | expect((dflt.json.items as Json[]).length).toBe(50); |
| 961 | 997 | expect(dflt.json.next_cursor).not.toBeNull(); |
| 962 | 998 | }); |
| 963 | 999 | |
| 964 | 1000 | test("a team member sees the org's private repos in listings", async () => { |
| 1001 | proveAll(app); | |
| 1002 | expect( | |
| 1003 | (await call("POST", "/api/v1/orgs", { token: aliceRw, body: { name: "corp" } })) | |
| 1004 | .status, | |
| 1005 | ).toBe(201); | |
| 1006 | expect( | |
| 1007 | ( | |
| 1008 | await call("POST", "/api/v1/orgs/corp/teams", { | |
| 1009 | token: aliceAdmin, | |
| 1010 | body: { name: "devs", permission: "write" }, | |
| 1011 | }) | |
| 1012 | ).status, | |
| 1013 | ).toBe(201); | |
| 965 | 1014 | await call("POST", "/api/v1/repos", { |
| 966 | 1015 | token: aliceRw, |
| 967 | body: { owner: "acme", name: "internal", visibility: "private" }, | |
| 1016 | body: { owner: "corp", name: "internal", visibility: "private" }, | |
| 968 | 1017 | }); |
| 969 | await call("POST", "/api/v1/orgs/acme/teams/devs/members", { | |
| 1018 | await call("POST", "/api/v1/orgs/corp/teams/devs/members", { | |
| 970 | 1019 | token: aliceAdmin, |
| 971 | 1020 | body: { user: "bob" }, |
| 972 | 1021 | }); |
| @@ -974,9 +1023,9 @@ describe("parity: full control through actions and the API", () => { | ||
| 974 | 1023 | const names = (bobSees.json.items as Json[]).map( |
| 975 | 1024 | (r) => `${r.ownerName}/${r.name}`, |
| 976 | 1025 | ); |
| 977 | expect(names).toContain("acme/internal"); | |
| 1026 | expect(names).toContain("corp/internal"); | |
| 978 | 1027 | expect(names).not.toContain("alice/secret"); |
| 979 | const carolSees = await call("GET", "/api/v1/users/acme/repos"); | |
| 1028 | const carolSees = await call("GET", "/api/v1/users/corp/repos"); | |
| 980 | 1029 | expect((carolSees.json.items as Json[]).length).toBe(0); |
| 981 | 1030 | }); |
| 982 | 1031 |
test/public-url.test.ts +14 −8
| @@ -6,7 +6,7 @@ import { createApp, type App } from "../src/app.ts"; | ||
| 6 | 6 | import { crossOrigin, publicUrl } from "../src/http/context.ts"; |
| 7 | 7 | import { serve, type Servers } from "../src/http/server.ts"; |
| 8 | 8 | import { promote } from "./helpers/members.ts"; |
| 9 | import { proveAll } from "./helpers/verified.ts"; | |
| 9 | import { signUpProven } from "./helpers/verified.ts"; | |
| 10 | 10 | |
| 11 | 11 | describe("publicUrl", () => { |
| 12 | 12 | test("without a configured origin the request's URL is the truth", () => { |
| @@ -89,14 +89,20 @@ describe("a forge behind a TLS-terminating proxy", () => { | ||
| 89 | 89 | }); |
| 90 | 90 | |
| 91 | 91 | test("clone URLs name the public address", async () => { |
| 92 | // Ada signed up above: an owner proves and promotes her before she makes things. | |
| 93 | proveAll(app); | |
| 92 | // Grace signs up here, so this test does not lean on the one above: | |
| 93 | // an owner proves and promotes her before she makes things. | |
| 94 | const grace = await signUpProven(app, { | |
| 95 | username: "grace", | |
| 96 | email: "[email protected]", | |
| 97 | password: "compiler-pioneer", | |
| 98 | }); | |
| 99 | if (!grace.ok) throw new Error("grace could not sign up"); | |
| 94 | 100 | promote(app.db); |
| 95 | 101 | const token = await fetch(`${base}/api/v1/tokens`, { |
| 96 | 102 | method: "POST", |
| 97 | 103 | body: JSON.stringify({ adminConfirm: true, |
| 98 | username: "ada", | |
| 99 | password: "analytical-engine", | |
| 104 | username: "grace", | |
| 105 | password: "compiler-pioneer", | |
| 100 | 106 | name: "cli", |
| 101 | 107 | scope: "rw", |
| 102 | 108 | }), |
| @@ -109,8 +115,8 @@ describe("a forge behind a TLS-terminating proxy", () => { | ||
| 109 | 115 | body: JSON.stringify({ name: "hello", visibility: "public" }), |
| 110 | 116 | }); |
| 111 | 117 | expect(created.status).toBe(201); |
| 112 | const page = await fetch(`${base}/ada/hello`).then((r) => r.text()); | |
| 113 | expect(page).toContain("https://forge.example/ada/hello.git"); | |
| 114 | expect(page).not.toContain(`${base}/ada/hello.git`); | |
| 118 | const page = await fetch(`${base}/grace/hello`).then((r) => r.text()); | |
| 119 | expect(page).toContain("https://forge.example/grace/hello.git"); | |
| 120 | expect(page).not.toContain(`${base}/grace/hello.git`); | |
| 115 | 121 | }); |
| 116 | 122 | }); |