A zero-dependency AT Protocol Personal Data Server written in JavaScript
atproto pds
1#!/bin/bash 2# E2E tests for PDS - runs against local wrangler dev 3set -e 4 5BASE="http://localhost:8787" 6DID="did:plc:c6vxslynzebnlk5kw2orx37o" 7 8# Helper for colored output 9pass() { echo "$1"; } 10fail() { echo "$1" >&2; cleanup; exit 1; } 11 12# Cleanup function 13cleanup() { 14 if [ -n "$WRANGLER_PID" ]; then 15 echo "Shutting down wrangler..." 16 kill $WRANGLER_PID 2>/dev/null || true 17 wait $WRANGLER_PID 2>/dev/null || true 18 fi 19} 20trap cleanup EXIT 21 22# Start wrangler dev 23echo "Starting wrangler dev..." 24npx wrangler dev --port 8787 > /dev/null 2>&1 & 25WRANGLER_PID=$! 26 27# Wait for server to be ready 28for i in {1..30}; do 29 if curl -sf "$BASE/" > /dev/null 2>&1; then 30 break 31 fi 32 sleep 0.5 33done 34 35# Verify server is up 36curl -sf "$BASE/" > /dev/null || fail "Server failed to start" 37pass "Server started" 38 39# Initialize PDS 40PRIVKEY=$(openssl rand -hex 32) 41curl -sf -X POST "$BASE/init?did=$DID" \ 42 -H "Content-Type: application/json" \ 43 -d "{\"did\":\"$DID\",\"privateKey\":\"$PRIVKEY\",\"handle\":\"test.local\"}" > /dev/null \ 44 && pass "PDS initialized" || fail "PDS init" 45 46echo 47echo "Running tests..." 48echo 49 50# 1. Root returns ASCII art 51curl -sf "$BASE/" | grep -q "PDS" && pass "Root returns ASCII art" || fail "Root" 52 53# 2. describeServer works 54curl -sf "$BASE/xrpc/com.atproto.server.describeServer" | jq -e '.did' > /dev/null \ 55 && pass "describeServer" || fail "describeServer" 56 57# 3. resolveHandle works 58curl -sf "$BASE/xrpc/com.atproto.identity.resolveHandle?handle=test.local" \ 59 | jq -e '.did' > /dev/null && pass "resolveHandle" || fail "resolveHandle" 60 61# 4. createSession returns tokens 62SESSION=$(curl -sf -X POST "$BASE/xrpc/com.atproto.server.createSession" \ 63 -H "Content-Type: application/json" \ 64 -d "{\"identifier\":\"$DID\",\"password\":\"test-password\"}") 65TOKEN=$(echo "$SESSION" | jq -r '.accessJwt') 66[ "$TOKEN" != "null" ] && [ -n "$TOKEN" ] && pass "createSession returns token" || fail "createSession" 67 68# 5. getSession works with token 69curl -sf "$BASE/xrpc/com.atproto.server.getSession" \ 70 -H "Authorization: Bearer $TOKEN" | jq -e '.did' > /dev/null \ 71 && pass "getSession with valid token" || fail "getSession" 72 73# 6. Protected endpoint rejects without auth 74STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE/xrpc/com.atproto.repo.createRecord" \ 75 -H "Content-Type: application/json" \ 76 -d '{"repo":"x","collection":"x","record":{}}') 77[ "$STATUS" = "401" ] && pass "createRecord rejects without auth" || fail "createRecord should reject" 78 79# 7. getPreferences works (returns empty array initially) 80curl -sf "$BASE/xrpc/app.bsky.actor.getPreferences" \ 81 -H "Authorization: Bearer $TOKEN" | jq -e '.preferences' > /dev/null \ 82 && pass "getPreferences" || fail "getPreferences" 83 84# 8. putPreferences works 85curl -sf -X POST "$BASE/xrpc/app.bsky.actor.putPreferences" \ 86 -H "Authorization: Bearer $TOKEN" \ 87 -H "Content-Type: application/json" \ 88 -d '{"preferences":[{"$type":"app.bsky.actor.defs#savedFeedsPrefV2"}]}' > /dev/null \ 89 && pass "putPreferences" || fail "putPreferences" 90 91# 9. createRecord works with auth 92RECORD=$(curl -sf -X POST "$BASE/xrpc/com.atproto.repo.createRecord" \ 93 -H "Authorization: Bearer $TOKEN" \ 94 -H "Content-Type: application/json" \ 95 -d "{\"repo\":\"$DID\",\"collection\":\"app.bsky.feed.post\",\"record\":{\"text\":\"test\",\"createdAt\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}}") 96URI=$(echo "$RECORD" | jq -r '.uri') 97[ "$URI" != "null" ] && [ -n "$URI" ] && pass "createRecord with auth" || fail "createRecord" 98 99# 10. getRecord retrieves it 100RKEY=$(echo "$URI" | sed 's|.*/||') 101curl -sf "$BASE/xrpc/com.atproto.repo.getRecord?repo=$DID&collection=app.bsky.feed.post&rkey=$RKEY" \ 102 | jq -e '.value.text' > /dev/null && pass "getRecord" || fail "getRecord" 103 104# 11. putRecord updates the record 105curl -sf -X POST "$BASE/xrpc/com.atproto.repo.putRecord" \ 106 -H "Authorization: Bearer $TOKEN" \ 107 -H "Content-Type: application/json" \ 108 -d "{\"repo\":\"$DID\",\"collection\":\"app.bsky.feed.post\",\"rkey\":\"$RKEY\",\"record\":{\"text\":\"updated\",\"createdAt\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}}" \ 109 | jq -e '.uri' > /dev/null && pass "putRecord" || fail "putRecord" 110 111# 12. listRecords shows the record 112curl -sf "$BASE/xrpc/com.atproto.repo.listRecords?repo=$DID&collection=app.bsky.feed.post" \ 113 | jq -e '.records | length > 0' > /dev/null && pass "listRecords" || fail "listRecords" 114 115# 13. describeRepo returns repo info 116curl -sf "$BASE/xrpc/com.atproto.repo.describeRepo?repo=$DID" \ 117 | jq -e '.did' > /dev/null && pass "describeRepo" || fail "describeRepo" 118 119# 14. applyWrites batch operation (create then delete a record) 120APPLY_RESULT=$(curl -sf -X POST "$BASE/xrpc/com.atproto.repo.applyWrites" \ 121 -H "Authorization: Bearer $TOKEN" \ 122 -H "Content-Type: application/json" \ 123 -d "{\"repo\":\"$DID\",\"writes\":[{\"\$type\":\"com.atproto.repo.applyWrites#create\",\"collection\":\"app.bsky.feed.post\",\"rkey\":\"applytest\",\"value\":{\"text\":\"batch\",\"createdAt\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}}]}") 124echo "$APPLY_RESULT" | jq -e '.results' > /dev/null && pass "applyWrites create" || fail "applyWrites create" 125 126# 15. applyWrites delete 127curl -sf -X POST "$BASE/xrpc/com.atproto.repo.applyWrites" \ 128 -H "Authorization: Bearer $TOKEN" \ 129 -H "Content-Type: application/json" \ 130 -d "{\"repo\":\"$DID\",\"writes\":[{\"\$type\":\"com.atproto.repo.applyWrites#delete\",\"collection\":\"app.bsky.feed.post\",\"rkey\":\"applytest\"}]}" \ 131 | jq -e '.results' > /dev/null && pass "applyWrites delete" || fail "applyWrites delete" 132 133# 16. sync.getLatestCommit returns head 134curl -sf "$BASE/xrpc/com.atproto.sync.getLatestCommit?did=$DID" \ 135 | jq -e '.cid' > /dev/null && pass "sync.getLatestCommit" || fail "sync.getLatestCommit" 136 137# 17. sync.getRepoStatus returns status 138curl -sf "$BASE/xrpc/com.atproto.sync.getRepoStatus?did=$DID" \ 139 | jq -e '.did' > /dev/null && pass "sync.getRepoStatus" || fail "sync.getRepoStatus" 140 141# 18. sync.getRepo returns CAR file 142REPO_SIZE=$(curl -sf "$BASE/xrpc/com.atproto.sync.getRepo?did=$DID" | wc -c) 143[ "$REPO_SIZE" -gt 100 ] && pass "sync.getRepo returns CAR" || fail "sync.getRepo" 144 145# 19. sync.getRecord returns record with proof (binary CAR data) 146RECORD_SIZE=$(curl -sf "$BASE/xrpc/com.atproto.sync.getRecord?did=$DID&collection=app.bsky.feed.post&rkey=$RKEY" | wc -c) 147[ "$RECORD_SIZE" -gt 50 ] && pass "sync.getRecord" || fail "sync.getRecord" 148 149# 20. sync.listRepos lists repos 150curl -sf "$BASE/xrpc/com.atproto.sync.listRepos" \ 151 | jq -e '.repos | length > 0' > /dev/null && pass "sync.listRepos" || fail "sync.listRepos" 152 153# Error handling tests 154echo 155echo "Testing error handling..." 156 157# 21. Invalid password rejected 158STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE/xrpc/com.atproto.server.createSession" \ 159 -H "Content-Type: application/json" \ 160 -d "{\"identifier\":\"$DID\",\"password\":\"wrong-password\"}") 161[ "$STATUS" = "401" ] && pass "Invalid password rejected (401)" || fail "Invalid password should return 401" 162 163# 22. Wrong repo rejected (can't modify another user's repo) 164STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE/xrpc/com.atproto.repo.createRecord" \ 165 -H "Authorization: Bearer $TOKEN" \ 166 -H "Content-Type: application/json" \ 167 -d '{"repo":"did:plc:z72i7hdynmk6r22z27h6tvur","collection":"app.bsky.feed.post","record":{"text":"x","createdAt":"2024-01-01T00:00:00Z"}}') 168[ "$STATUS" = "403" ] && pass "Wrong repo rejected (403)" || fail "Wrong repo should return 403" 169 170# 23. Non-existent record returns 404 171STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/xrpc/com.atproto.repo.getRecord?repo=$DID&collection=app.bsky.feed.post&rkey=nonexistent") 172[ "$STATUS" = "400" ] || [ "$STATUS" = "404" ] && pass "Non-existent record error" || fail "Non-existent record should error" 173 174# Cleanup: delete the test record 175curl -sf -X POST "$BASE/xrpc/com.atproto.repo.deleteRecord" \ 176 -H "Authorization: Bearer $TOKEN" \ 177 -H "Content-Type: application/json" \ 178 -d "{\"repo\":\"$DID\",\"collection\":\"app.bsky.feed.post\",\"rkey\":\"$RKEY\"}" > /dev/null \ 179 && pass "deleteRecord (cleanup)" || fail "deleteRecord" 180 181echo 182echo "All tests passed!"