Public: anyone can read this; the crew can change it.
test/apps-adversarial-grants.test.ts runs under 10 s #3600
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3580-test-apps-adversarial-grants-test-ts-run → main 2 commits fork 4 threads · 4 unresolved
2 files
changed +33
−44
scripts/test-times.json +1 −1
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | { |
| 2 | "test/apps-adversarial-grants.test.ts": 28.3, | |
| 2 | "test/apps-adversarial-grants.test.ts": 2.5, | |
| 3 | 3 | "test/apps-adversarial-rules.test.ts": 29.7, |
| 4 | 4 | "test/apps-adversarial-teardown.test.ts": 36.4, |
| 5 | 5 | "test/apps-adversarial-test-step.test.ts": 33.5, |
test/apps-adversarial-grants.test.ts +32 −43
| @@ -12,7 +12,7 @@ import { join } from "node:path"; | ||
| 12 | 12 | import { createApp, type App } from "../src/app.ts"; |
| 13 | 13 | import { AppsRuntime } from "../src/apps/runtime.ts"; |
| 14 | 14 | import { parseAppHost, signGrant, verifyGrant } from "../src/apps/proxy.ts"; |
| 15 | import { newApp, updateAppSettings } from "../src/actions/apps.ts"; | |
| 15 | import { newApp, updateAppSettings, type NewAppResult } from "../src/actions/apps.ts"; | |
| 16 | 16 | import { listRuns, type Run } from "../src/core/runs.ts"; |
| 17 | 17 | import { createUser, type User } from "../src/core/users.ts"; |
| 18 | 18 | import { serve, type Servers } from "../src/http/server.ts"; |
| @@ -26,6 +26,9 @@ describe("apps adversarial: grants and the proxy", () => { | ||
| 26 | 26 | let servers: Servers; |
| 27 | 27 | let runtime: AppsRuntime; |
| 28 | 28 | let alice: User; |
| 29 | let grants: NewAppResult; | |
| 30 | let override: NewAppResult; | |
| 31 | let proxy: NewAppResult; | |
| 29 | 32 | |
| 30 | 33 | const viaHost = (host: string, path = "/", init: RequestInit = {}) => |
| 31 | 34 | fetch(`${servers.url}${path}`, { |
| @@ -77,6 +80,20 @@ describe("apps adversarial: grants and the proxy", () => { | ||
| 77 | 80 | }); |
| 78 | 81 | if (!created.ok) throw new Error(created.error); |
| 79 | 82 | alice = created.value; |
| 83 | ||
| 84 | // The four apps deploy side by side, once for the file. | |
| 85 | const make = async (name: string, visibility: "public" | "private") => { | |
| 86 | const made = await newApp(app, alice, { name, visibility }); | |
outdated
Comment says three apps; the Promise.all deploys four. Fix the count. |
||
| 87 | if (!made.ok) throw new Error(made.error); | |
| 88 | await settled(made.value.repo.id, (r) => r.env === "production"); | |
| 89 | return made.value; | |
| 90 | }; | |
| 91 | [grants, , override, proxy] = await Promise.all([ | |
| 92 | make("grants", "private"), | |
| 93 | make("grantpublic", "public"), | |
| 94 | make("publicwithprivateapp", "public"), | |
| 95 | make("proxytest", "public"), | |
| 96 | ]); | |
| 80 | 97 | }, 30_000); |
The beforeAll timeout is 30_000 and now covers all four deploys. If the loaded machine makes it flake, raise the timeout here first. |
||
| 81 | 98 | |
| 82 | 99 | afterAll(async () => { |
| @@ -88,15 +105,8 @@ describe("apps adversarial: grants and the proxy", () => { | ||
| 88 | 105 | }); |
| 89 | 106 | |
| 90 | 107 | test("private grant: forged, tampered, expired, wrong-env, and open-redirect are all refused; cookie is HttpOnly/SameSite", async () => { |
| 91 | const made = await newApp(app, alice, { | |
| 92 | name: "grants", | |
| 93 | visibility: "private", | |
| 94 | }); | |
| 95 | expect(made.ok).toBe(true); | |
| 96 | if (!made.ok) return; | |
| 97 | const repo = made.value.repo; | |
| 98 | const record = made.value.app; | |
| 99 | await settled(repo.id, (r) => r.env === "production"); | |
| 108 | const repo = grants.repo; | |
| 109 | const record = grants.app; | |
| 100 | 110 | |
| 101 | 111 | const target = { app: record, env: "production" as const }; |
| 102 | 112 | const good = signGrant(dataDir, target, 60_000); |
| @@ -132,21 +142,14 @@ describe("apps adversarial: grants and the proxy", () => { | ||
| 132 | 142 | // Hitting the grant endpoint on a *public* app: it still answers (it |
| 133 | 143 | // is not access-gated by itself), but the token must still verify |
| 134 | 144 | // correctly against that public app's own target. |
| 135 | const publicMade = await newApp(app, alice, { | |
| 136 | name: "grantpublic", | |
| 137 | visibility: "public", | |
| 138 | }); | |
| 139 | expect(publicMade.ok).toBe(true); | |
| 140 | if (publicMade.ok) { | |
| 141 | await settled(publicMade.value.repo.id, (r) => r.env === "production"); | |
| 142 | const pubGrantUrl = `http://grantpublic.${DOMAIN}/__forge/grant?t=${encodeURIComponent(good)}&to=/`; | |
| 143 | // A production-app grant token does not verify for a different app's | |
| 144 | // target, so this must be refused (404) even though the destination | |
| 145 | // app is public. | |
| 146 | const url = new URL(pubGrantUrl); | |
| 147 | const crossApp = await viaHost(url.host, `${url.pathname}${url.search}`); | |
| 148 | expect(crossApp.status).toBe(404); | |
| 149 | } | |
| 145 | // (`grantpublic` is deployed in beforeAll.) A production-app grant | |
| 146 | // token does not verify for a different app's target, so this must be | |
| 147 | // refused (404) even though the destination app is public. | |
outdated
This assertion is filler, and the bare |
||
| 148 | const crossApp = await viaHost( | |
| 149 | `grantpublic.${DOMAIN}`, | |
| 150 | `/__forge/grant?t=${encodeURIComponent(good)}&to=/`, | |
| 151 | ); | |
| 152 | expect(crossApp.status).toBe(404); | |
| 150 | 153 | |
| 151 | 154 | // Open redirect: `to=//evil` and `to=http://evil` must both collapse to `/`. |
| 152 | 155 | const okToken = signGrant(dataDir, target, 60_000); |
| @@ -204,20 +207,13 @@ describe("apps adversarial: grants and the proxy", () => { | ||
| 204 | 207 | // anonymous visitor has nothing to be vouched with, and a signed-in |
| 205 | 208 | // stranger who can read the public code is still not let in: a private |
| 206 | 209 | // app answers to its writers and crew only (#620). |
| 207 | const overrideMade = await newApp(app, alice, { | |
| 208 | name: "publicwithprivateapp", | |
| 209 | visibility: "public", | |
| 210 | }); | |
| 211 | expect(overrideMade.ok).toBe(true); | |
| 212 | if (!overrideMade.ok) return; | |
| 213 | await settled(overrideMade.value.repo.id, (r) => r.env === "production"); | |
| 214 | updateAppSettings(app, alice, overrideMade.value.repo, { | |
| 210 | updateAppSettings(app, alice, override.repo, { | |
| 215 | 211 | access: "private", |
| 216 | 212 | }); |
| 217 | 213 | const anonOnPublicRepo = openEnvUrl( |
| 218 | 214 | app, |
| 219 | 215 | undefined, |
| 220 | overrideMade.value.repo, | |
| 216 | override.repo, | |
| 221 | 217 | "production", |
| 222 | 218 | ); |
| 223 | 219 | expect(anonOnPublicRepo.ok).toBe(false); |
| @@ -226,7 +222,7 @@ describe("apps adversarial: grants and the proxy", () => { | ||
| 226 | 222 | const strangerOnPublicRepo = openEnvUrl( |
| 227 | 223 | app, |
| 228 | 224 | strangerCreated.value, |
| 229 | overrideMade.value.repo, | |
| 225 | override.repo, | |
| 230 | 226 | "production", |
| 231 | 227 | ); |
| 232 | 228 | expect(strangerOnPublicRepo.ok).toBe(false); |
| @@ -235,14 +231,7 @@ describe("apps adversarial: grants and the proxy", () => { | ||
| 235 | 231 | }, 60_000); |
| 236 | 232 | |
| 237 | 233 | test("the proxy: hop-by-hop headers are stripped, an upstream that dies mid-request yields a clean error, a claimed upgrade is a request", async () => { |
| 238 | const made = await newApp(app, alice, { | |
| 239 | name: "proxytest", | |
| 240 | visibility: "public", | |
| 241 | }); | |
| 242 | expect(made.ok).toBe(true); | |
| 243 | if (!made.ok) return; | |
| 244 | const repo = made.value.repo; | |
| 245 | await settled(repo.id, (r) => r.env === "production"); | |
| 234 | const repo = proxy.repo; | |
| 246 | 235 | |
| 247 | 236 | const page = await viaHost(`proxytest.${DOMAIN}`); |
| 248 | 237 | expect(page.status).toBe(200); |
new above
offline
type Made = NewAppResultis a pointless alias. UseExtract<NewAppResult, {ok: true}>["value"]or name the value type directly. As written,Madeis the whole result union, but the fixtures hold.value, so the type is wrong and only passes if NewAppResult is the value type. Confirm with tsc.