clone

a personal forge on plain git: static pages, stacked-diff reviews, signed local ci, repo sites and releases

Make scenarios' HTTP requests with Node, not curl

cat committed 18f6a75
The forge's container has no curl, so every scenario that talks to the
private, events or Caddy servers failed there. test/http.ts is a small
client with the curl options the scenarios use (-o -D -i -I -w -H -X -d
-u --max-time, -s and -N), curl's output (the status line and headers as
received, CRLF and all) and its exit codes (7 when nothing answers, 28 on
--max-time, which private-events relies on). lib.sh calls it http, and
wait_http and response use it, so the requests are the same on every host
and the suite needs no curl at all. Curl stays out of the image, which
needs it for nothing else.
commit
18f6a757c8e0e2c96166361830122eb3ea9973a7
parent
396feb70568544c0208d1c83dd0e78e28ba535c9
author
cat <admin@dev.site>
ci
✓ passed in 81s, signed by cat
kind
test risk score 1.8 of 3, Jev's estimate
review
#5 Pass the scenarios in the container, as root
ci attestation
forge-attestation 1
type ci
commit 18f6a757c8e0e2c96166361830122eb3ea9973a7
tree b68155c82fb3f103ce302e54d894cdc25b534071
script .forge/ci e6f038ae810b87f90a4be115a762a9c91dd76a01
result pass
exit 0
started 2026-10-18T22:58:45Z
duration 81s
runner (not shown here; refs/notes/ci has it)
system Darwin 25.5.0 arm64
env host
log sha256:c2ea633add157b1cdaa5cbbe48b709218e1c1fa384b2ab53ec3fac0397e54057 240 lines
|remote: review #4: /dami/greet/reviews/4/
|pushed wip/i18n
|8c3fdab running .forge/ci: greet.sh: say what it is
|  | sh: /bin/sh
|  | ok   greet
|8c3fdab pass in 0s, signed
|pushed main
|remote: review #4: /dami/greet/reviews/4/
|pushed wip/i18n
|8d4bdfd running .forge/ci: wave at the world
|  | sh: /bin/sh
|  | FAIL greet : got 'hello, world o/', want 'hello, world'
|8d4bdfd fail in 0s, signed
|forge: 1 commit(s) did not pass
|remote: review #6: /dami/greet/reviews/6/
|pushed ana/wave
|3febabf running .forge/ci: update the test for the wave
|  | sh: /bin/sh
|  | ok   greet
|3febabf pass in 0s, signed
|remote: review #6: /dami/greet/reviews/6/
|pushed ana/wave
|warning: You appear to have cloned an empty repository.
|site home: deployed 171c730 -> /sites/ana/dotfiles/home/
|created ana/notes
|remote: review #1: /ana/notes/reviews/1/
|pushed ana/todo
|landing #1 A todo list: ana/todo -> main
|c401d37 running .forge/ci: web, server and Python versions of greet
|  | sh: /bin/sh
|  | ok   greet
|c401d37 pass in 0s, signed
|pushed main
|3ae2aba running .forge/ci: messages: notes for the later regions
|  | sh: /bin/sh
|  | ok   greet
|3ae2aba pass in 0s, signed
|remote: review #7: /dami/greet/reviews/7/
|pushed dami/regions
|created dami/forge
|warning: You appear to have cloned an empty repository.
|site guide: deployed 982f0cb -> /sites/dami/forge/guide/
|(node:58775) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built ana/dotfiles in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www
|(node:58899) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built ana/notes in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www
|(node:59038) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built dami/forge in 0s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www
|(node:59168) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built dami/greet in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www
|(node:59833) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time
|(Use `node --trace-warnings ...` to show where the warning was created)
|built - in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www
|226 of 226 pages render the same without 11ty
|guide: 4 files, links ok
|ok

Signature: good, cat

13 files changed, +162 -48
Atest/http.ts109++++++++++++++++++++++++++++++
Mtest/lib.sh13++-
Mtest/scenarios/identity-backup.sh2+-
Mtest/scenarios/identity-enroll.sh6+-
Mtest/scenarios/identity-passkey-pqc.sh6+-
Mtest/scenarios/identity-passkey.sh2+-
Mtest/scenarios/identity-sessions.sh12++--
Mtest/scenarios/insights-private-pages.sh6+-
Mtest/scenarios/private-events.sh16++--
Mtest/scenarios/private-web-clone.sh4+-
Mtest/scenarios/private-web-session.sh10+-
Mtest/scenarios/private-web-token.sh14++--
Mtest/scenarios/release-private-web.sh10+-
A test/http.ts +109 -0
@@ -0,0 +1,109 @@
1+// An HTTP client for scenarios, so they don't need curl (the forge's2+// container has none): the few curl options they use, with curl's output3+// and exit codes, over Node's own http client. test/lib.sh calls it `http`.4+//5+// node test/http.ts [options] <url>6+// -o <file> the body to file instead of stdout (- is stdout)7+// -D <file> the status line and headers to file (- is stdout)8+// -i the status line and headers before the body9+// -I a HEAD request, its status line and headers10+// -w <format> then format, with %{http_code} and %{redirect_url}11+// -H 'Name: value' a request header (more than one: repeat it)12+// -X <method> the request's method13+// -d <data> a POST body, form-encoded unless -H says otherwise14+// -u user:password basic auth15+// --max-time <s> give up after that long (exit 28, as curl)16+// -s, -N accepted, and what it does anyway: no progress, no buffering17+// Flags without a value may share a dash (-sN).18+// Exit 0 for any response, 7 when nothing answers, 28 on --max-time.19+import fs from 'node:fs';20+import http from 'node:http';21+22+const args = process.argv.slice(2).flatMap((a) => (/^-[sNiI]{2,}$/.test(a) ? [...a.slice(1)].map((c) => `-${c}`) : [a]));23+let url = '';24+let out = '-';25+let dump = '';26+let include = false;27+let head = false;28+let format = '';29+let method = '';30+let data: string | undefined;31+let maxTime = 0;32+const headers: [string, string][] = [];33+for (let i = 0; i < args.length; i++) {34+ const a = args[i];35+ const next = (): string => {36+ if (i + 1 >= args.length) usage(`${a} wants a value`);37+ return args[++i];38+ };39+ if (a === '-s' || a === '-N') continue;40+ else if (a === '-o') out = next();41+ else if (a === '-D') dump = next();42+ else if (a === '-i') include = true;43+ else if (a === '-I') head = true;44+ else if (a === '-w') format = next();45+ else if (a === '-X') method = next();46+ else if (a === '-d') data = next();47+ else if (a === '--max-time') maxTime = Number(next());48+ else if (a === '-u') headers.push(['Authorization', `Basic ${Buffer.from(next()).toString('base64')}`]);49+ else if (a === '-H') {50+ const h = next();51+ const c = h.indexOf(':');52+ if (c <= 0) usage(`not a header: ${h}`);53+ headers.push([h.slice(0, c).trim(), h.slice(c + 1).trim()]);54+ } else if (a.startsWith('-')) usage(`unknown option ${a}`);55+ else if (url) usage('one URL at a time');56+ else url = a;57+}58+if (!url) usage('no URL');59+60+function usage(why: string): never {61+ process.stderr.write(`test/http.ts: ${why}\n`);62+ process.exit(2);63+}64+65+const has = (name: string): boolean => headers.some(([k]) => k.toLowerCase() === name);66+if (data !== undefined) {67+ if (!has('content-type')) headers.push(['Content-Type', 'application/x-www-form-urlencoded']);68+ headers.push(['Content-Length', String(Buffer.byteLength(data))]);69+}70+if (!has('host')) headers.unshift(['Host', new URL(url).host]);71+if (!has('accept')) headers.push(['Accept', '*/*']);72+method ||= head ? 'HEAD' : data !== undefined ? 'POST' : 'GET';73+74+const fd = (f: string): number => (f === '-' ? 1 : fs.openSync(f, 'w'));75+const body = fd(out);76+const headFd = dump ? fd(dump) : -1;77+let code = 0;78+let location = '';79+80+function finish(exit: number): never {81+ if (format) {82+ const w = format83+ .replaceAll('%{http_code}', String(code).padStart(3, '0'))84+ .replaceAll('%{redirect_url}', location ? new URL(location, url).href : '')85+ .replaceAll('\\n', '\n');86+ fs.writeSync(1, w);87+ }88+ process.exit(exit);89+}90+91+if (maxTime > 0) setTimeout(() => finish(28), maxTime * 1000).unref();92+93+const req = http.request(url, { method, headers: headers.flat() }, (res) => {94+ code = res.statusCode ?? 0;95+ location = typeof res.headers.location === 'string' ? res.headers.location : '';96+ let lines = `HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}\r\n`;97+ for (let i = 0; i < res.rawHeaders.length; i += 2) lines += `${res.rawHeaders[i]}: ${res.rawHeaders[i + 1]}\r\n`;98+ lines += '\r\n';99+ if (headFd >= 0) fs.writeSync(headFd, lines);100+ if (include || head) fs.writeSync(body, lines);101+ res.on('data', (d: Buffer) => {102+ if (!head) fs.writeSync(body, d);103+ });104+ res.on('end', () => finish(0));105+ res.on('error', () => finish(56));106+});107+req.on('error', () => finish(code ? 56 : 7));108+if (data !== undefined) req.write(data);109+req.end();
M test/lib.sh +9 -4
@@ -20,7 +20,8 @@
2020 # sync_builds hooks and ssh commands build pages before returning2121 # lq "<sql>" forge lake query: the rows as CSV, no header2222 # private_server lib/private-server.ts for this forge, at $PRIV23-# response url [curl args] a whole HTTP response less its Date, to compare23+# http [curl options] url an HTTP request: curl's options (test/http.ts)24+# response url [http options] a whole HTTP response less its Date, to compare2425 #2526 # Assertions print what failed and stop the scenario:2627 # ok cmd... cmd succeeds fails cmd... cmd fails
@@ -237,10 +238,14 @@ lq() {
237238 # free_port: a TCP port nothing listens on just now.238239 free_port() { node -e 'const s = require("net").createServer().listen(0, "127.0.0.1", () => { console.log(s.address().port); s.close(); })'; }239240 241+# http [options] url: the curl options scenarios use, from test/http.ts, so242+# they run where curl isn't installed (the forge's container).243+http() { node "$T_HOME/test/http.ts" "$@"; }244+240245 # wait_http url: until something answers there (5s at most).241246 wait_http() {242247 n=0243- until curl -s -o /dev/null "$1" || [ "$n" -ge 50 ]; do sleep 0.1; n=$((n + 1)); done248+ until http -s -o /dev/null "$1" || [ "$n" -ge 50 ]; do sleep 0.1; n=$((n + 1)); done244249 }245250 246251 # private_server: lib/private-server.ts on a free port, in the background,
@@ -275,10 +280,10 @@ stop_servers() {
275280 T_PIDS=276281 }277282 278-# response url [curl args...]: the whole HTTP response (status line,283+# response url [http options...]: the whole HTTP response (status line,279284 # headers, body) less its Date, to compare byte for byte.280285 response() {281286 u=$1282287 shift283- curl -s -i "$@" "$u" | grep -av '^Date: '288+ http -s -i "$@" "$u" | grep -av '^Date: '284289 }
M test/scenarios/identity-backup.sh +1 -1
M test/scenarios/identity-enroll.sh +3 -3
M test/scenarios/identity-passkey-pqc.sh +3 -3
M test/scenarios/identity-passkey.sh +1 -1
M test/scenarios/identity-sessions.sh +6 -6
@@ -15,14 +15,14 @@ build
1515 export FORGE_BASE_URL=https://forge.test1616 private_server1717 auth() { node "$T_HOME/test/authenticator.ts" "$@"; }18-page() { curl -s -o /dev/null -w '%{http_code}' -H "Cookie: __Host-forge=$1" "$PRIV/dami/secret/"; }18+page() { http -s -o /dev/null -w '%{http_code}' -H "Cookie: __Host-forge=$1" "$PRIV/dami/secret/"; }1919 for k in one two; do2020 code=$(ssh_as dami passkey "$k" | sed -n 's/^ .*#enroll=//p')2121 ok auth enroll "$PRIV" "$code" "$T/$k.key"2222 done2323 2424 c=$(auth login "$PRIV" "$T/one.key")25-curl -s -D "$T/h" -o "$T/login" -H "Cookie: __Host-forge=$c" "$PRIV/login/"25+http -s -D "$T/h" -o "$T/login" -H "Cookie: __Host-forge=$c" "$PRIV/login/"2626 lacks "$T/h" '^Set-Cookie'2727 has "$T/h" "^Content-Security-Policy: default-src 'self'"2828 has "$T/h" '^Cache-Control: private, no-store'
@@ -38,7 +38,7 @@ grep -q 'name="csrf" value="[A-Za-z0-9_-]\{32\}"' "$T/login" || fail "no csrf to
3838 stop_servers3939 export FORGE_NOW=$((T_NOW + 3660))4040 private_server41-curl -s -D "$T/h" -o /dev/null -H "Cookie: __Host-forge=$c" "$PRIV/dami/secret/"41+http -s -D "$T/h" -o /dev/null -H "Cookie: __Host-forge=$c" "$PRIV/dami/secret/"4242 has "$T/h" '^HTTP/1.1 200'4343 has "$T/h" "^Set-Cookie: __Host-forge=$c; Path=/; Max-Age=2592000; Secure; HttpOnly; SameSite=Lax"4444 ok ssh_as dami sessions
@@ -58,13 +58,13 @@ private_server
5858 5959 # Sign-out: only from the forge's page, with its token.6060 c=$(auth login "$PRIV" "$T/one.key")61-out() { curl -s -o /dev/null -w '%{http_code}' -X POST -H "Cookie: __Host-forge=$c" "$@" "$PRIV/login/logout"; }62-csrf=$(curl -s -H "Cookie: __Host-forge=$c" "$PRIV/login/" | sed -n 's/.*name="csrf" value="\([^"]*\)".*/\1/p')61+out() { http -s -o /dev/null -w '%{http_code}' -X POST -H "Cookie: __Host-forge=$c" "$@" "$PRIV/login/logout"; }62+csrf=$(http -s -H "Cookie: __Host-forge=$c" "$PRIV/login/" | sed -n 's/.*name="csrf" value="\([^"]*\)".*/\1/p')6363 eq "$(out -H 'Origin: https://evil.test' -d "csrf=$csrf")" 4036464 eq "$(out -d "csrf=$csrf")" 4036565 eq "$(out -H 'Origin: https://forge.test' -d 'csrf=nope')" 4036666 eq "$(page "$c")" 20067-curl -s -o /dev/null -D "$T/h" -X POST -H "Cookie: __Host-forge=$c" -H 'Origin: https://forge.test' -d "csrf=$csrf" "$PRIV/login/logout"67+http -s -o /dev/null -D "$T/h" -X POST -H "Cookie: __Host-forge=$c" -H 'Origin: https://forge.test' -d "csrf=$csrf" "$PRIV/login/logout"6868 has "$T/h" '^HTTP/1.1 303'6969 has "$T/h" '^Location: /login/'7070 has "$T/h" '^Set-Cookie: __Host-forge=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Lax'
M test/scenarios/insights-private-pages.sh +3 -3
M test/scenarios/private-events.sh +8 -8
@@ -17,17 +17,17 @@ FORGE_STATE=$FORGE_ROOT/.forge FORGE_EVENTS_PORT=$port node "$T_HOME/lib/events-
1717 T_PIDS="$T_PIDS $!"1818 es=http://127.0.0.1:$port/events1919 n=020-until curl -s -o /dev/null "$es/x/y/" || [ "$n" -ge 50 ]; do sleep 0.1; n=$((n + 1)); done20+until http -s -o /dev/null "$es/x/y/" || [ "$n" -ge 50 ]; do sleep 0.1; n=$((n + 1)); done2121 22-code() { curl -s -o /dev/null -w '%{http_code}' --max-time 1 "$@" || true; }22+code() { http -s -o /dev/null -w '%{http_code}' --max-time 1 "$@" || true; }2323 eq "$(code "$es/dami/secret/")" 4042424 eq "$(code "$es/dami/nope/")" 4042525 eq "$(code "$es/dami/open/")" 2002626 eq "$(code "$es/")" 2002727 28-curl -sN --max-time 4 "$es/" >"$T/all" &28+http -sN --max-time 4 "$es/" >"$T/all" &2929 all=$!30-curl -sN --max-time 4 "$es/dami/open/" >"$T/open" &30+http -sN --max-time 4 "$es/dami/open/" >"$T/open" &3131 one=$!3232 T_PIDS="$T_PIDS $all $one"3333 sleep 1
@@ -39,7 +39,7 @@ sleep 1
3939 rm "$FORGE_ROOT/dami/open.git/git-daemon-export-ok"4040 push dami/open after4141 # The watcher of dami/open is let go before its 4 seconds are up.42-if wait "$one"; then :; else fail "the stream of dami/open wasn't closed (curl exited $?)"; fi42+if wait "$one"; then :; else fail "the stream of dami/open wasn't closed (http exited $?)"; fi4343 wait "$all" || true4444 has "$T/all" '"refs":"refs/heads/before"'4545 lacks "$T/all" 'dami/secret'
@@ -62,9 +62,9 @@ eq "$(code -H "Cookie: __Host-forge=$ana" "$es/dami/secret/")" 200
6262 eq "$(code -H "Cookie: __Host-forge=$cat" "$es/dami/secret/")" 4046363 eq "$(code -H 'Cookie: __Host-forge=nonsense' "$es/dami/secret/")" 4046464 eq "$(code -H "Cookie: __Host-forge=$ana" "$es/dami/nope/")" 40465-curl -sN --max-time 4 -H "Cookie: __Host-forge=$ana" "$es/dami/secret/" >"$T/ana" &65+http -sN --max-time 4 -H "Cookie: __Host-forge=$ana" "$es/dami/secret/" >"$T/ana" &6666 one=$!67-curl -sN --max-time 4 -H "Cookie: __Host-forge=$cat" "$es/" >"$T/cat" &67+http -sN --max-time 4 -H "Cookie: __Host-forge=$cat" "$es/" >"$T/cat" &6868 all=$!6969 T_PIDS="$T_PIDS $one $all"7070 sleep 1
@@ -72,7 +72,7 @@ push dami/secret for-readers ',"private":true'
7272 sleep 17373 ok ssh_as dami readers dami/secret rm ana7474 push dami/secret after-rm ',"private":true'75-if wait "$one"; then :; else fail "ana's stream of dami/secret wasn't closed (curl exited $?)"; fi75+if wait "$one"; then :; else fail "ana's stream of dami/secret wasn't closed (http exited $?)"; fi7676 wait "$all" || true7777 has "$T/ana" '"refs":"refs/heads/for-readers"'7878 lacks "$T/ana" 'after-rm'
M test/scenarios/private-web-clone.sh +2 -2
M test/scenarios/private-web-session.sh +5 -5
@@ -29,7 +29,7 @@ signin() {
2929 }3030 ana=$(signin ana) || fail "ana"3131 cat=$(signin cat) || fail "cat"32-as() { u=$1; shift; curl -s -H "Cookie: __Host-forge=$u" "$@"; }32+as() { u=$1; shift; http -s -H "Cookie: __Host-forge=$u" "$@"; }3333 3434 eq "$(as "$ana" -o /dev/null -w '%{http_code}' "$PRIV/dami/secret/")" 2003535 as "$ana" "$PRIV/dami/secret/" | grep -q 'hidden thing' || fail "ana doesn't see dami/secret"
@@ -53,15 +53,15 @@ lacks "$T/list" '<b>(own|open)</b>'
5353 as "$cat" "$PRIV/private/" >"$T/list"5454 has "$T/list" '<b>own</b>'5555 lacks "$T/list" '<b>(secret|unbuilt|open)</b>'56-eq "$(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' "$PRIV/private/")" "303 $PRIV/login/?next=/private/"57-curl -s "$PRIV/login/?next=/dami/secret/" >"$T/page"56+eq "$(http -s -o /dev/null -w '%{http_code} %{redirect_url}' "$PRIV/private/")" "303 $PRIV/login/?next=/private/"57+http -s "$PRIV/login/?next=/dami/secret/" >"$T/page"5858 has "$T/page" 'data-next="/dami/secret/"'59-curl -s "$PRIV/login/?next=//evil.test/" >"$T/page"59+http -s "$PRIV/login/?next=//evil.test/" >"$T/page"6060 has "$T/page" 'data-next=""'6161 6262 # Its scripts and styles, from the private server itself when www lacks them.6363 for a in $(grep -oE '(href|src)="/assets/[^"]+"' "$T/page" | sed -E 's/^[a-z]+="//; s/"$//' | sort -u); do64- eq "$(curl -s -o /dev/null -w '%{http_code}' "$PRIV$a")" 20064+ eq "$(http -s -o /dev/null -w '%{http_code}' "$PRIV$a")" 2006565 done6666 grep -q '/assets/webauthn\.[A-Za-z0-9_-]*\.js' "$T/page" || fail "no webauthn script on the sign-in page"6767 grep -q '/assets/login\.[A-Za-z0-9_-]*\.js' "$T/page" || fail "no login script on the sign-in page"
M test/scenarios/private-web-token.sh +7 -7
@@ -32,10 +32,10 @@ u=$PRIV/dami/secret/
3232 3333 # Readers.3434 for t in "$dami" "$ana"; do35- eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $t" "$u")" 20035+ eq "$(http -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $t" "$u")" 2003636 done37-eq "$(curl -s -o /dev/null -w '%{http_code}' -u "ana:$ana" "$u")" 20038-eq "$(curl -s -o /dev/null -w '%{http_code}' -u "$ana:" "$u")" 20037+eq "$(http -s -o /dev/null -w '%{http_code}' -u "ana:$ana" "$u")" 20038+eq "$(http -s -o /dev/null -w '%{http_code}' -u "$ana:" "$u")" 2003939 response "$u" -H "Authorization: Bearer $dami" >"$T/page"4040 has "$T/page" '^HTTP/1.1 200 OK'4141 has "$T/page" '^Cache-Control: private, no-cache'
@@ -44,9 +44,9 @@ has "$T/page" '^ETag: "'
4444 has "$T/page" '^Last-Modified: '4545 has "$T/page" 'hidden thing'4646 etag=$(sed -n 's/^ETag: //p' "$T/page" | tr -d '\r')47-eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $dami" -H "If-None-Match: $etag" "$u")" 30448-eq "$(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' -H "Authorization: Bearer $dami" "$PRIV/dami/secret")" "308 $PRIV/dami/secret/"49-eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $dami" "$PRIV/dami/secret/search.json")" 20047+eq "$(http -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $dami" -H "If-None-Match: $etag" "$u")" 30448+eq "$(http -s -o /dev/null -w '%{http_code} %{redirect_url}' -H "Authorization: Bearer $dami" "$PRIV/dami/secret")" "308 $PRIV/dami/secret/"49+eq "$(http -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $dami" "$PRIV/dami/secret/search.json")" 2005050 5151 # Everyone and everything else: the same 404.5252 response "$PRIV/dami/nope/" >"$T/missing"
@@ -84,7 +84,7 @@ stop_servers
8484 export FORGE_NOW=$((T_NOW + 3700))8585 private_server8686 same "$PRIV/dami/secret/" -H "Authorization: Bearer $short" -I87-eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $dami" "$PRIV/dami/secret/")" 20087+eq "$(http -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $dami" "$PRIV/dami/secret/")" 2008888 ok ssh_as dami tokens8989 eq "$(grep -c ' token$' "$T_LOG")" 19090 has "$T_LOG" ' 2026-09-01 01:01 +token$'
M test/scenarios/release-private-web.sh +5 -5
@@ -30,26 +30,26 @@ export FORGE_BASE_URL=https://forge.test
3030 private_server3131 u=$PRIV/dami/secret/releases/download/v1/tool.html3232 tok=$(ssh_as ana token --repo dami/secret 2>/dev/null)33-curl -s -D "$T/h" -o "$T/got" -u "ana:$tok" "$u"33+http -s -D "$T/h" -o "$T/got" -u "ana:$tok" "$u"3434 has "$T/h" '^HTTP/1.1 200'3535 has "$T/h" '^Content-Disposition: attachment'3636 has "$T/h" '^X-Content-Type-Options: nosniff'3737 has "$T/h" '^Cache-Control: private, no-cache'3838 has "$T/h" '^Accept-Ranges: bytes'3939 ok cmp "$T/f/tool.html" "$T/got"40-curl -s -D "$T/h" -o "$T/part" -H "Authorization: Bearer $tok" -H 'Range: bytes=7-10' "$u"40+http -s -D "$T/h" -o "$T/part" -H "Authorization: Bearer $tok" -H 'Range: bytes=7-10' "$u"4141 has "$T/h" '^HTTP/1.1 206'4242 has "$T/h" "^Content-Range: bytes 7-10/$(wc -c <"$T/f/tool.html" | tr -d ' ')"4343 has "$T/h" '^Content-Disposition: attachment'4444 eq "$(cat "$T/part")" "tool"4545 etag=$(sed -n 's/^ETag: //p' "$T/h" | tr -d '\r')46-eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $tok" -H "If-None-Match: $etag" "$u")" 30447-eq "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $tok" "$PRIV/dami/secret/releases/download/v1/SHA256SUMS.sig")" 20046+eq "$(http -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $tok" -H "If-None-Match: $etag" "$u")" 30447+eq "$(http -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $tok" "$PRIV/dami/secret/releases/download/v1/SHA256SUMS.sig")" 2004848 4949 # Signed in with a passkey.5050 code=$(ssh_as ana passkey | sed -n 's/^ .*#enroll=//p')5151 ana=$(node "$T_HOME/test/authenticator.ts" enroll "$PRIV" "$code" "$T/ana.key") || fail "ana can't sign in"52-curl -s -D "$T/h" -o "$T/got" -H "Cookie: __Host-forge=$ana" -H 'Range: bytes=0-5' "$u"52+http -s -D "$T/h" -o "$T/got" -H "Cookie: __Host-forge=$ana" -H 'Range: bytes=0-5' "$u"5353 has "$T/h" '^HTTP/1.1 206'5454 has "$T/h" '^Content-Disposition: attachment'5555 has "$T/h" '^X-Content-Type-Options: nosniff'