#!/usr/bin/env sh
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
work=$(mktemp -d "${TMPDIR:-/tmp}/zds-smoke.XXXXXX")
port="${ZDS_SMOKE_PORT:-2585}"
db="${work}/zds-smoke.sqlite3"
blob_root="${work}/zds-smoke-blobs"
log="${work}/zds-smoke.log"
base="http://127.0.0.1:${port}"
jwt_secret="smoke-jwt-secret"
cleanup() {
status=$?
if [ -n "${server_pid:-}" ]; then
kill "$server_pid" 2>/dev/null || true
wait "$server_pid" 2>/dev/null || true
fi
if [ -n "${plc_stub_pid:-}" ]; then
kill "$plc_stub_pid" 2>/dev/null || true
wait "$plc_stub_pid" 2>/dev/null || true
fi
if [ "$status" -eq 0 ]; then
rm -rf "$work"
else
echo "Smoke failed; diagnostic files: $work" >&2
fi
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
cd "$root"
rm -f "$db" "$db-wal" "$db-shm" "$log"
rm -rf "$blob_root"
mkdir -p "$blob_root"
if [ "${ZDS_SMOKE_PREBUILT:-0}" != 1 ]; then
zig build smoke-build
fi
# stand-in PLC directory so createAccount genesis never reaches plc.directory;
# it also hosts a public OAuth client's metadata so PAR can be exercised.
plc_port="${ZDS_SMOKE_PLC_PORT:-2586}"
client_id="http://127.0.0.1:${plc_port}/oauth-client-metadata.json"
python3 -c "
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
metadata = json.dumps({
'client_id': '$client_id',
'client_name': 'smoke',
'redirect_uris': ['http://127.0.0.1/callback'],
'scope': 'atproto repo:*',
'grant_types': ['authorization_code', 'refresh_token'],
'response_types': ['code'],
'token_endpoint_auth_method': 'none',
'application_type': 'native',
'dpop_bound_access_tokens': True,
}).encode()
class Stub(BaseHTTPRequestHandler):
def do_GET(self):
if self.path != '/oauth-client-metadata.json':
self.send_response(404)
self.end_headers()
return
self.send_response(200)
self.send_header('content-type', 'application/json')
self.send_header('content-length', str(len(metadata)))
self.end_headers()
self.wfile.write(metadata)
def do_POST(self):
self.rfile.read(int(self.headers.get('content-length', 0)))
self.send_response(200)
self.end_headers()
def log_message(self, *args):
pass
HTTPServer(('127.0.0.1', $plc_port), Stub).serve_forever()
" &
plc_stub_pid=$!
# run the installed binary directly so $! is the server itself; killing the
# `zig build run` wrapper orphans zds and leaves the port taken for the next run
ZDS_PLC_ROTATION_KEY=1111111111111111111111111111111111111111111111111111111111111111 \
./zig-out/bin/zds \
--host 127.0.0.1 \
--jwt-secret "$jwt_secret" \
--port "$port" \
--db "$db" \
--blobstore-path "$blob_root" \
--public-url "$base" \
--server-did did:web:localhost \
--handle-domains .test \
--invite-required \
--admin-token smoke-admin-token \
--operator-handle smoke-operator.test \
--plc-directory "http://127.0.0.1:${plc_port}" \
>"$log" 2>&1 &
server_pid=$!
i=0
while [ "$i" -lt 80 ]; do
if curl -fsS "$base/xrpc/_health" >/dev/null 2>&1; then
break
fi
i=$((i + 1))
sleep 0.1
done
curl -fsS "$base/xrpc/_health" >/dev/null
# A normal build must never pick up cached operator drawings implicitly.
account_page=$(curl -fsS "$base/account/manage")
if printf '%s' "$account_page" | grep -q '/ui/icon?cid='; then
echo 'unconfigured build unexpectedly includes custom drawings' >&2
exit 1
fi
test "$(curl -sS -o /dev/null -w '%{http_code}' "$base/ui/icon?cid=bafkreidhkabyea5vkonzvfdjvgbinqt76kovuli7hrwqqffx5cx2nf4chi")" = 404
stats_page=$(curl -fsS "$base/stats")
printf '%s' "$stats_page" | grep -q '
pds health
'
printf '%s' "$stats_page" | grep -q 'writes
'
curl -fsS "$base/xrpc/com.atproto.server.describeServer" >/dev/null
sqlite3 "$db" "insert into accounts (did, handle, email, password_hash, activated_at, email_confirmed_at) values ('did:plc:smoketest', 'smoke.test', 'smoke@test.com', 'password', unixepoch(), unixepoch())"
sqlite3 "$db" "insert into oauth_requests (request_id, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, login_hint, expires_at) values ('smoke-oauth', 'https://client.example/oauth-client.json', 'https://client.example/callback', 'repo:*?action=create blob:*/*', 'state', 'challenge', 'S256', 'smoke.test', unixepoch() + 600)"
resolved=$(curl -fsS "$base/xrpc/com.atproto.identity.resolveHandle?handle=smoke.test")
printf '%s' "$resolved" | grep -q '"did":"did:plc:smoketest"'
well_known=$(curl -fsS -H 'host: smoke.test' "$base/.well-known/atproto-did")
test "$well_known" = "did:plc:smoketest"
unsupported_well_known_status=$(curl -sS -o "$work/zds-unsupported-handle.txt" -w '%{http_code}' \
-H 'host: smoke.example.com' "$base/.well-known/atproto-did")
test "$unsupported_well_known_status" = "404"
session=$(curl -fsS -X POST "$base/xrpc/com.atproto.server.createSession" \
-H 'content-type: application/json' \
--data '{"identifier":"smoke.test","password":"password"}')
token=$(printf '%s' "$session" | sed -n 's/.*"accessJwt":"\([^"]*\)".*/\1/p')
test -n "$token"
printf '%s' "$session" | grep -q '"didDoc":'
! printf '%s' "$session" | grep -q '"status":'
printf '%s' "$session" | grep -q '"serviceEndpoint":"http://127.0.0.1:'
session_did=$(printf '%s' "$session" | jq -r .did)
# Grouped account access uses the same password-session boundary and validates paging.
access_page=$(curl -fsS -H "authorization: Bearer $token" "$base/xrpc/dev.zat.account.listAccess?kind=password&limit=1")
printf '%s' "$access_page" | jq -e '.authorized >= 1 and (.groups | length) == 1 and (.connections | length) == 0' >/dev/null
access_group=$(printf '%s' "$access_page" | jq -r '.groups[0].key')
curl -fsS -G -H "authorization: Bearer $token" --data-urlencode "kind=password" --data-urlencode "group=$access_group" "$base/xrpc/dev.zat.account.listAccess" | jq -e --arg did "$session_did" '.connections | length > 0 and all(.[]; .did == $did)' >/dev/null
for query in 'kind=unknown' 'state=unknown' 'cursor=-1' 'cursor=nope' 'limit=0' 'limit=101'; do
test "$(curl -sS -o /dev/null -w '%{http_code}' -H "authorization: Bearer $token" "$base/xrpc/dev.zat.account.listAccess?$query")" = 400
done
test "$(curl -sS -o /dev/null -w '%{http_code}' "$base/xrpc/dev.zat.account.listAccess")" = 401
# an access token this server signed whose exp has passed must answer 400
# ExpiredToken (the reference PDS's answer, which clients key on to refresh),
# while a token that never verified stays 401 InvalidToken.
expired_token=$(python3 -c "
import base64, hashlib, hmac, json, time
def b64(b): return base64.urlsafe_b64encode(b).rstrip(b'=').decode()
header = b64(json.dumps({'alg': 'HS256', 'typ': 'JWT'}).encode())
now = int(time.time())
payload = b64(json.dumps({'sub': '$session_did', 'scope': 'com.atproto.access', 'iat': now - 7200, 'exp': now - 3600, 'jti': 'zds-smoke-expired'}).encode())
sig = hmac.new(b'$jwt_secret', f'{header}.{payload}'.encode(), hashlib.sha256).digest()
print(f'{header}.{payload}.{b64(sig)}')
")
expired_body="${work}/zds-smoke-expired.json"
expired_status=$(curl -sS -o "$expired_body" -w '%{http_code}' \
-H "authorization: Bearer $expired_token" "$base/xrpc/com.atproto.server.getSession")
test "$expired_status" = "400"
grep -q '"error":"ExpiredToken"' "$expired_body"
garbage_status=$(curl -sS -o "$expired_body" -w '%{http_code}' \
-H "authorization: Bearer ${expired_token}x" "$base/xrpc/com.atproto.server.getSession")
test "$garbage_status" = "401"
grep -q '"error":"InvalidToken"' "$expired_body"
invalid_auth_headers="${work}/zds-smoke-invalid-auth.headers"
invalid_auth_body="${work}/zds-smoke-invalid-auth.json"
invalid_auth_status=$(curl -sS -D "$invalid_auth_headers" -o "$invalid_auth_body" -w '%{http_code}' \
-H 'authorization: Bearer invalid-token' \
"$base/xrpc/com.atproto.server.getSession")
test "$invalid_auth_status" = "401"
grep -qi '^www-authenticate: Bearer error="invalid_token", error_description="Token is invalid"' "$invalid_auth_headers"
invalid_dpop_status=$(curl -sS -D "$invalid_auth_headers" -o "$invalid_auth_body" -w '%{http_code}' \
-H 'authorization: DPoP invalid-token' \
"$base/xrpc/com.atproto.server.getSession")
test "$invalid_dpop_status" = "401"
grep -qi '^www-authenticate: DPoP error="invalid_token", error_description="Token is invalid"' "$invalid_auth_headers"
grep -qi '^dpop-nonce:' "$invalid_auth_headers"
# PAR scope validation must terminate the request: an unregistered scope is a
# 400 invalid_scope, never a 201 (a helper once wrote the error and returned
# success, and the handler overwrote it with a request_uri).
par_body="${work}/zds-smoke-par.json"
# the client is native and registers a port-less loopback redirect, so PAR
# sending the port it actually bound is the RFC 8252 exception at work; the
# path still has to match exactly.
par() {
par_redirect="${par_redirect:-http://127.0.0.1:${plc_port}/callback}"
curl -sS -o "$par_body" -w '%{http_code}' -X POST "$base/oauth/par" \
--data-urlencode "client_id=${par_client:-$client_id}" \
--data-urlencode "redirect_uri=$par_redirect" \
--data-urlencode 'response_type=code' \
--data-urlencode 'code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM' \
--data-urlencode 'code_challenge_method=S256' \
--data-urlencode 'state=x' \
--data-urlencode "scope=$1"
}
test "$(par_redirect="http://127.0.0.1:${plc_port}/elsewhere" par 'atproto repo:*')" = "400"
grep -q 'redirect_uri not registered' "$par_body"
test "$(par 'atproto garbage:thing')" = "400"
grep -q '"error":"invalid_scope"' "$par_body"
test "$(par 'atproto include:com.example.nope')" = "400"
grep -q '"error":"invalid_scope"' "$par_body"
test "$(par 'atproto transition:generic repo:*')" = "400"
grep -q '"error":"invalid_scope"' "$par_body"
# a valid scope gets past validation to the DPoP check (no proof was sent)
test "$(par 'atproto repo:*')" = "400"
grep -q '"error":"use_dpop_nonce"' "$par_body"
# the spec's localhost client development exception: an `http://localhost`
# client_id (no port) is virtual, so nothing is fetched and its metadata comes
# from the query string. Reaching the DPoP check proves metadata, redirect, and
# scope all resolved.
test "$(par_client='http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%3A4444%2Fcb&scope=atproto%20repo%3A*' par_redirect='http://127.0.0.1:4444/cb' par 'atproto repo:*')" = "400"
grep -q '"error":"use_dpop_nonce"' "$par_body"
test "$(par_client='http://localhost' par_redirect='http://127.0.0.1:5199/' par 'atproto')" = "400"
grep -q '"error":"use_dpop_nonce"' "$par_body"
test "$(par_client='http://localhost?scope=atproto' par_redirect='http://[::1]:5199/' par 'atproto repo:*')" = "400"
grep -q '"error":"invalid_scope"' "$par_body"
test "$(par_client='http://localhost?redirect_uri=https%3A%2F%2Fevil.example%2F' par_redirect='https://evil.example/' par 'atproto')" = "400"
grep -q 'Invalid loopback client_id' "$par_body"
test "$(par_client='http://localhost?redirect_uri=http%3A%2F%2Flocalhost%3A4444%2Fcb' par_redirect='http://localhost:4444/cb' par 'atproto')" = "400"
grep -q 'Invalid loopback client_id' "$par_body"
# a port makes it an ordinary URL client, which has to be fetched
test "$(par_client='http://localhost:1' par_redirect='http://127.0.0.1:5199/' par 'atproto')" = "400"
grep -q 'Could not fetch client metadata' "$par_body"
describe=$(curl -fsS "$base/xrpc/com.atproto.server.describeServer")
printf '%s' "$describe" | grep -q '"inviteCodeRequired":true'
missing_invite_status=$(curl -sS -o "$work/zds-missing-invite.json" -w '%{http_code}' -X POST "$base/xrpc/com.atproto.server.createAccount" \
-H 'content-type: application/json' \
--data '{"handle":"missing-invite.test","email":"missing-invite@test.com","password":"password"}')
test "$missing_invite_status" = "400"
grep -q '"error":"InvalidInviteCode"' "$work/zds-missing-invite.json"
invite=$(curl -fsS -X POST "$base/xrpc/com.atproto.server.createInviteCode" \
-H "authorization: Bearer smoke-admin-token" \
-H 'content-type: application/json' \
--data '{"useCount":1,"forAccount":"did:plc:smoketest"}')
printf '%s' "$invite" | grep -q '"code":"127-0-0-1-'
invites=$(curl -fsS -H "authorization: Bearer $token" "$base/xrpc/com.atproto.server.getAccountInviteCodes")
printf '%s' "$invites" | grep -q '"available":1'
signup_page=$(curl -fsS "$base/signup")
printf '%s' "$signup_page" | grep -q 'join zds'
printf '%s' "$signup_page" | grep -q 'com.atproto.server.describeServer'
printf '%s' "$signup_page" | grep -q 'com.atproto.server.createAccount'
printf '%s' "$signup_page" | grep -q 'DM /dev/null
test "$(refresh_session "$app_refresh" "$refresh_body")" = "400"
grep -q '"error":"ExpiredToken"' "$refresh_body"
# a new account must be announced on the firehose immediately: identity,
# account, genesis commit, and sync events plus a servable empty repo
genesis=$(curl -fsS "$base/xrpc/com.atproto.sync.getLatestCommit?did=$signup_did")
printf '%s' "$genesis" | grep -q '"cid":'
printf '%s' "$genesis" | grep -q '"rev":'
curl -fsS "$base/xrpc/com.atproto.sync.getRepo?did=$signup_did" -o "$work/zds-signup-genesis.car"
test -s "$work/zds-signup-genesis.car"
signup_events=$(sqlite3 "$db" "select count(*) from seq_events where did = '$signup_did'")
test "$signup_events" = "4"
signup_taken_invite=$(curl -fsS -X POST "$base/xrpc/com.atproto.server.createInviteCode" \
-H "authorization: Bearer smoke-admin-token" \
-H 'content-type: application/json' \
--data '{"useCount":1}' | jq -r .code)
signup_taken_status=$(curl -sS -o "$work/zds-signup-taken.json" -w '%{http_code}' -X POST "$base/xrpc/com.atproto.server.createAccount" \
-H 'content-type: application/json' \
--data "$(printf '{"handle":"signup-smoke.test","email":"signup-smoke-2@test.com","password":"signup-password","inviteCode":"%s"}' "$signup_taken_invite")")
test "$signup_taken_status" = "400"
! grep -q '"error":"InvalidInviteCode"' "$work/zds-signup-taken.json"
grep -q '"error":' "$work/zds-signup-taken.json"
curl -fsS -X POST "$base/xrpc/app.bsky.actor.putPreferences" \
-H "authorization: Bearer $token" \
-H 'content-type: application/json' \
--data '{"preferences":[{"$type":"app.bsky.actor.defs#contentLabelPref","label":"dogs","visibility":"show"},{"$type":"app.bsky.actor.defs#contentLabelPref","label":"cats","visibility":"warn"},{"$type":"app.bsky.actor.defs#personalDetailsPref","birthDate":"1970-01-01"},{"$type":"app.bsky.actor.defs#declaredAgePref","isOverAge13":false,"isOverAge16":false,"isOverAge18":false}]}' >/dev/null
prefs=$(curl -fsS -H "authorization: Bearer $token" "$base/xrpc/app.bsky.actor.getPreferences")
printf '%s' "$prefs" | grep -q '"label":"dogs"'
printf '%s' "$prefs" | grep -q '"label":"cats"'
printf '%s' "$prefs" | grep -q '"birthDate":"1970-01-01"'
printf '%s' "$prefs" | grep -q '"isOverAge13":true'
printf '%s' "$prefs" | grep -q '"isOverAge16":true'
printf '%s' "$prefs" | grep -q '"isOverAge18":true'
test "$(printf '%s' "$prefs" | grep -o 'declaredAgePref' | wc -l | tr -d ' ')" = "1"
create=$(curl -fsS -X POST "$base/xrpc/com.atproto.repo.createRecord" \
-H "authorization: Bearer $token" \
-H 'content-type: application/json' \
--data '{"repo":"did:plc:smoketest","collection":"app.bsky.feed.post","record":{"$type":"app.bsky.feed.post","text":"smoke","createdAt":"2026-05-22T00:00:00.000Z"}}')
printf '%s' "$create" | grep -q '"uri":"at://did:plc:smoketest/app.bsky.feed.post/'
printf '%s' "$create" | grep -q '"validationStatus":"valid"'
post_uri=$(printf '%s' "$create" | jq -r .uri)
post_cid=$(printf '%s' "$create" | sed -n 's/.*"cid":"\([^"]*\)".*/\1/p')
test -n "$post_cid"
# sync.getRecord serves a proof CAR (commit + mst path + record); strict OAuth
# lexicon resolvers depend on it and previously got 404 UnknownMethod
post_rkey=$(printf '%s' "$create" | jq -r '.uri | split("/")[-1]')
proof_car="$work/zds-record-proof.car"
proof_status=$(curl -sS -o "$proof_car" -w '%{http_code}' "$base/xrpc/com.atproto.sync.getRecord?did=did:plc:smoketest&collection=app.bsky.feed.post&rkey=$post_rkey")
test "$proof_status" = "200"
test -s "$proof_car"
proof_type=$(curl -sS -o /dev/null -w '%{content_type}' "$base/xrpc/com.atproto.sync.getRecord?did=did:plc:smoketest&collection=app.bsky.feed.post&rkey=$post_rkey")
test "$proof_type" = "application/vnd.ipld.car"
proof_head=$(curl -sSI -o /dev/null -w '%{http_code}' "$base/xrpc/com.atproto.sync.getRecord?did=did:plc:smoketest&collection=app.bsky.feed.post&rkey=$post_rkey")
test "$proof_head" = "200"
# exclusion proof for an absent rkey is still a 200 CAR
exclusion_status=$(curl -sS -o "$proof_car" -w '%{http_code}' "$base/xrpc/com.atproto.sync.getRecord?did=did:plc:smoketest&collection=app.bsky.feed.post&rkey=3zdsnosuchrkey")
test "$exclusion_status" = "200"
test -s "$proof_car"
proof_missing_repo=$(curl -sS -o /dev/null -w '%{http_code}' "$base/xrpc/com.atproto.sync.getRecord?did=did:plc:nobody&collection=app.bsky.feed.post&rkey=$post_rkey")
test "$proof_missing_repo" = "404"
like=$(curl -fsS -X POST "$base/xrpc/com.atproto.repo.createRecord" \
-H "authorization: Bearer $token" \
-H 'content-type: application/json' \
--data "$(printf '{"repo":"did:plc:smoketest","collection":"app.bsky.feed.like","record":{"$type":"app.bsky.feed.like","subject":{"uri":"%s","cid":"%s"},"createdAt":"2026-05-22T00:00:01.000Z"}}' "$post_uri" "$post_cid")")
printf '%s' "$like" | grep -q '"uri":"at://did:plc:smoketest/app.bsky.feed.like/'
printf '%s' "$like" | grep -q '"validationStatus":"valid"'
like_rkey=$(printf '%s' "$like" | jq -r '.uri | split("/")[-1]')
test "$(printf '%s' "$like_rkey" | wc -c | tr -d ' ')" = "13"
case "$like_rkey" in
3zds*)
echo "generated app.bsky.feed.like rkey is not a TID: $like_rkey" >&2
exit 1
;;
esac
invalid_like_status=$(curl -sS -o "$work/zds-invalid-like.json" -w '%{http_code}' -X POST "$base/xrpc/com.atproto.repo.createRecord" \
-H "authorization: Bearer $token" \
-H 'content-type: application/json' \
--data "$(printf '{"repo":"did:plc:smoketest","collection":"app.bsky.feed.like","rkey":"3zdsbad","record":{"$type":"app.bsky.feed.like","subject":{"uri":"%s","cid":"%s"},"createdAt":"2026-05-22T00:00:02.000Z"}}' "$post_uri" "$post_cid")")
test "$invalid_like_status" = "400"
grep -q '"error":"InvalidRequest"' "$work/zds-invalid-like.json"
# a guard that writes an xrpc error must end the request. requireRepoMatches
# once wrote 400 and returned success, and createRecord went on to write the
# record and answer 200 over it.
wrong_repo_body="${work}/zds-smoke-wrong-repo.json"
wrong_repo_status=$(curl -sS -o "$wrong_repo_body" -w '%{http_code}' -X POST "$base/xrpc/com.atproto.repo.createRecord" \
-H "authorization: Bearer $token" \
-H 'content-type: application/json' \
--data '{"repo":"did:plc:someoneelse","collection":"app.bsky.feed.post","record":{"$type":"app.bsky.feed.post","text":"must not land","createdAt":"2026-05-22T00:00:03.000Z"}}')
test "$wrong_repo_status" = "400"
grep -q '"error":"InvalidRepo"' "$wrong_repo_body"
! curl -fsS "$base/xrpc/com.atproto.repo.listRecords?repo=did:plc:smoketest&collection=app.bsky.feed.post&limit=10" | grep -q 'must not land'
# and a 404 from requirePublicRepoAvailable stays a 404 rather than a 500
missing_repo_status=$(curl -sS -o "$wrong_repo_body" -w '%{http_code}' "$base/xrpc/com.atproto.repo.getRecord?repo=did:plc:nobody&collection=app.bsky.feed.post&rkey=3zds")
test "$missing_repo_status" = "404"
grep -q '"error":"RepoNotFound"' "$wrong_repo_body"
# the xrpc proxy path bypasses dispatch, so its HandledResponse must be caught
# there too: a bad token on a proxied method is a 401 InvalidToken json body,
# never a plain-text 500 (the top-level handler once overwrote it).
proxy_body="${work}/zds-smoke-proxy.json"
proxy_status=$(curl -sS -o "$proxy_body" -w '%{http_code}' -X POST "$base/xrpc/app.bsky.notification.updateSeen" \
-H "authorization: Bearer not-a-real-token" \
-H 'atproto-proxy: did:web:api.bsky.app#bsky_appview' \
-H 'content-type: application/json' \
--data '{"seenAt":"2026-05-22T00:00:03.000Z"}')
test "$proxy_status" = "401"
grep -q '"error":"InvalidToken"' "$proxy_body"
records=$(curl -fsS "$base/xrpc/com.atproto.repo.listRecords?repo=did:plc:smoketest&collection=app.bsky.feed.post&limit=10")
printf '%s' "$records" | grep -q '"text":"smoke"'
latest=$(curl -fsS "$base/xrpc/com.atproto.sync.getLatestCommit?did=did:plc:smoketest")
printf '%s' "$latest" | grep -q '"cid":"'
printf '%s' "$latest" | grep -q '"rev":"'
repo_car="${work}/zds-smoke.car"
code=$(curl -sS -o "$repo_car" -w '%{http_code}' "$base/xrpc/com.atproto.sync.getRepo?did=did:plc:smoketest")
test "$code" = "200"
test "$(wc -c < "$repo_car")" -gt 100
repo_head=$(curl -sSI -o /dev/null -w '%{http_code}' "$base/xrpc/com.atproto.sync.getRepo?did=did:plc:smoketest")
test "$repo_head" = "200"
blob_payload="${work}/zds-smoke-blob.jpg"
printf '\377\330\377\340zds-smoke' > "$blob_payload"
blob=$(curl -fsS -X POST "$base/xrpc/com.atproto.repo.uploadBlob" \
-H "authorization: Bearer $token" \
-H 'content-type: image/jpeg' \
--data-binary "@$blob_payload")
printf '%s' "$blob" | grep -q '"blob":'
printf '%s' "$blob" | grep -q '"mimeType":"image/jpeg"'
blob_cid=$(printf '%s' "$blob" | sed -n 's/.*"\$link":"\([^"]*\)".*/\1/p')
test -n "$blob_cid"
blob_head=$(curl -sSI -o /dev/null -w '%{http_code}' "$base/xrpc/com.atproto.sync.getBlob?did=did:plc:smoketest&cid=$blob_cid")
test "$blob_head" = "404"
blob_ref=$(curl -fsS -X POST "$base/xrpc/com.atproto.repo.createRecord" \
-H "authorization: Bearer $token" \
-H 'content-type: application/json' \
--data "$(jq -nc --arg cid "$blob_cid" '{repo:"did:plc:smoketest",collection:"dev.zds.smoke.blob",record:{"$type":"dev.zds.smoke.blob",blob:{"$type":"blob",ref:{"$link":$cid},mimeType:"image/jpeg",size:13}}}')")
printf '%s' "$blob_ref" | grep -q '"uri":"at://did:plc:smoketest/dev.zds.smoke.blob/'
blob_head=$(curl -sSI -o /dev/null -w '%{http_code}' "$base/xrpc/com.atproto.sync.getBlob?did=did:plc:smoketest&cid=$blob_cid")
test "$blob_head" = "200"
large_blob_payload="${work}/zds-smoke-large-blob.jpg"
dd if=/dev/zero bs=1048576 count=17 of="$large_blob_payload" 2>/dev/null
large_blob=$(curl -fsS -X POST "$base/xrpc/com.atproto.repo.uploadBlob" \
-H "authorization: Bearer $token" \
-H 'content-type: image/jpeg' \
--data-binary "@$large_blob_payload")
printf '%s' "$large_blob" | grep -q '"blob":'
printf '%s' "$large_blob" | grep -q '"size":17825792'
oauth_info=$(curl -fsS -H 'accept: application/json' "$base/oauth/authorize?request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3Asmoke-oauth")
printf '%s' "$oauth_info" | grep -q '"login_hint":"smoke.test"'
oauth_page="${work}/zds-smoke-oauth.html"
curl -fsS "$base/oauth/authorize?request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3Asmoke-oauth" > "$oauth_page"
grep -q 'value="smoke.test"' "$oauth_page"
grep -Fq 'repo:*?action=create' "$oauth_page"
grep -Fq 'blob:*/*' "$oauth_page"
sqlite3 "$db" "insert into oauth_requests (request_id, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, login_hint, expires_at) values ('smoke-loopback', 'http://localhost?scope=atproto', 'http://127.0.0.1:5199/', 'atproto', 'state', 'challenge', 'S256', 'smoke.test', unixepoch() + 600)"
curl -fsS "$base/oauth/authorize?request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3Asmoke-loopback" | grep -q 'development client'
python3 tools/smoke-browser-login.py "$base" "$db"
echo "zds smoke ok"