From 96991032b424cdc67fc2fc15f4f9a4adedc0c166 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Mon, 23 Mar 2026 20:27:49 -0400 Subject: [PATCH] feat: Add Nostr identity and trust tier tests (#44) --- scripts/gen_nostr_event.js | 21 ++++++ timmy_test.sh | 150 +++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 scripts/gen_nostr_event.js diff --git a/scripts/gen_nostr_event.js b/scripts/gen_nostr_event.js new file mode 100644 index 0000000..03ed1bd --- /dev/null +++ b/scripts/gen_nostr_event.js @@ -0,0 +1,21 @@ + +const { generatePrivateKey, getPublicKey, finalizeEvent } = require('nostr-tools'); + +const privateKey = generatePrivateKey(); +const publicKey = getPublicKey(privateKey); + +const nonce = process.argv[2]; // Nonce from challenge endpoint + +if (!nonce) { + console.error("Usage: node gen_nostr_event.js "); + process.exit(1); +} + +const event = finalizeEvent({ + kind: 27235, + created_at: Math.floor(Date.now() / 1000), + tags: [['challenge', nonce]], + content: "NIP-27235 challenge response", +}, privateKey); + +console.log(JSON.stringify(event)); diff --git a/timmy_test.sh b/timmy_test.sh index 621747c..751dcdf 100755 --- a/timmy_test.sh +++ b/timmy_test.sh @@ -422,6 +422,156 @@ else fi fi +# --------------------------------------------------------------------------- +# Tests 17-23 — Nostr Identity & Trust Tiers +# --------------------------------------------------------------------------- +sep "Tests 17-23 — Nostr Identity & Trust Tiers" + +# Initialize Nostr keys and other variables +NOSTR_PRIV_KEY=$(node -e "const { generatePrivateKey } = require('nostr-tools'); console.log(generatePrivateKey());") +NOSTR_PUB_KEY=$(node -e "const { getPublicKey } = require('nostr-tools'); console.log(getPublicKey('$NOSTR_PRIV_KEY'));") +CHALLENGE_NONCE="" +NOSTR_TOKEN="" +IDENTITY_PUBKEY="" + +# Test 17 — POST /api/identity/challenge +sep "Test 17 — POST /api/identity/challenge" +T17_RES=$(curl -s -w "\n%{http_code}" -X POST "$BASE/api/identity/challenge") +T17_BODY=$(echo "$T17_RES" | sed '$d') +T17_CODE=$(echo "$T17_RES" | tail -n1) +CHALLENGE_NONCE=$(jq_field "$T17_BODY" '.nonce') +EXPIRES_AT=$(jq_field "$T17_BODY" '.expiresAt') + +if [[ "$T17_CODE" == "200" && -n "$CHALLENGE_NONCE" && ${#CHALLENGE_NONCE} == 32 && -n "$EXPIRES_AT" ]]; then + note PASS "HTTP 200, nonce and expiresAt returned" + PASS=$((PASS+1)) +else + note FAIL "code=$T17_CODE body=$T17_BODY" + FAIL=$((FAIL+1)) +fi + +# Test 18 — POST /api/identity/verify with valid signed NIP-27235 event +sep "Test 18 — POST /api/identity/verify (valid)" +if [[ -n "$CHALLENGE_NONCE" ]]; then + VALID_EVENT_JSON=$(node scripts/gen_nostr_event.js "$CHALLENGE_NONCE") + T18_RES=$(curl -s -w "\n%{http_code}" -X POST "$BASE/api/identity/verify" \ + -H "Content-Type: application/json" \ + -d "$VALID_EVENT_JSON") + T18_BODY=$(echo "$T18_RES" | sed '$d') + T18_CODE=$(echo "$T18_RES" | tail -n1) + NOSTR_TOKEN=$(jq_field "$T18_BODY" '.nostr_token') + IDENTITY_PUBKEY=$(jq_field "$T18_BODY" '.pubkey') + TRUST_TIER=$(jq_field "$T18_BODY" '.trust') + + if [[ "$T18_CODE" == "200" && -n "$NOSTR_TOKEN" && "$IDENTITY_PUBKEY" == "$NOSTR_PUB_KEY" && -n "$TRUST_TIER" ]]; then + note PASS "HTTP 200, nostr_token, pubkey, trust returned" + PASS=$((PASS+1)) + else + note FAIL "code=$T18_CODE body=$T18_BODY" + FAIL=$((FAIL+1)) + fi +else + note SKIP "No challenge nonce from T17 — skipping Test 18" + SKIP=$((SKIP+1)) +fi + +# Test 19 — GET /api/estimate with X-Nostr-Token header for a new identity +sep "Test 19 — GET /api/estimate (free tier)" +if [[ -n "$NOSTR_TOKEN" ]]; then + T19_RES=$(curl -s -w "\n%{http_code}" "$BASE/api/estimate" -H "X-Nostr-Token: $NOSTR_TOKEN") + T19_BODY=$(echo "$T19_RES" | sed '$d') + T19_CODE=$(echo "$T19_RES" | tail -n1) + FREE_TIER_SERVE=$(jq_field "$T19_BODY" '.free_tier.serve') + + if [[ "$T19_CODE" == "200" && "$FREE_TIER_SERVE" == "free" ]]; then + note PASS "HTTP 200, free_tier.serve is 'free'" + PASS=$((PASS+1)) + else + note FAIL "code=$T19_CODE body=$T19_BODY" + FAIL=$((FAIL+1)) + fi +else + note SKIP "No nostr_token from T18 — skipping Test 19" + SKIP=$((SKIP+1)) +fi + +# Test 20 — POST /api/identity/verify with invalid signature +sep "Test 20 — POST /api/identity/verify (invalid signature)" +if [[ -n "$CHALLENGE_NONCE" ]]; then + INVALID_PRIV_KEY=$(node -e "const { generatePrivateKey } = require('nostr-tools'); console.log(generatePrivateKey());") + # Generate a valid event but with a different (invalid) private key + INVALID_EVENT_JSON=$(node -e "const { getPublicKey, finalizeEvent } = require('nostr-tools'); const privateKey = '$INVALID_PRIV_KEY'; const event = finalizeEvent({ kind: 27235, created_at: Math.floor(Date.now() / 1000), tags: [['challenge', '$CHALLENGE_NONCE']], content: 'NIP-27235 challenge response' }, privateKey); console.log(JSON.stringify(event));") + + T20_RES=$(curl -s -w "\n%{http_code}" -X POST "$BASE/api/identity/verify" \ + -H "Content-Type: application/json" \ + -d "$INVALID_EVENT_JSON") + T20_CODE=$(echo "$T20_RES" | tail -n1) + + if [[ "$T20_CODE" == "401" ]]; then + note PASS "HTTP 401 for invalid signature" + PASS=$((PASS+1)) + else + note FAIL "code=$T20_CODE" + FAIL=$((FAIL+1)) + fi +else + note SKIP "No challenge nonce from T17 — skipping Test 20" + SKIP=$((SKIP+1)) +fi + +# Test 21 — POST /api/identity/verify with expired nonce +sep "Test 21 — POST /api/identity/verify (expired nonce)" +# We'll generate a new challenge for this test, then wait for it to expire. +# Assuming a short expiration for testing purposes (e.g., 5 seconds). +# In a real scenario, this might be longer or handled with mocked time. + +T21_CHALLENGE_RES=$(curl -s -w "\n%{http_code}" -X POST "$BASE/api/identity/challenge") +T21_CHALLENGE_BODY=$(echo "$T21_CHALLENGE_RES" | sed '$d') +T21_CHALLENGE_CODE=$(echo "$T21_CHALLENGE_RES" | tail -n1) +T21_NONCE=$(jq_field "$T21_CHALLENGE_BODY" '.nonce') + +if [[ "$T21_CHALLENGE_CODE" == "200" && -n "$T21_NONCE" ]]; then + note "Waiting 6 seconds for nonce to expire..." + sleep 6 # Wait for the nonce to expire (assuming expiry < 6 seconds) + + EXPIRED_EVENT_JSON=$(node scripts/gen_nostr_event.js "$T21_NONCE") + T21_RES=$(curl -s -w "\n%{http_code}" -X POST "$BASE/api/identity/verify" \ + -H "Content-Type: application/json" \ + -d "$EXPIRED_EVENT_JSON") + T21_CODE=$(echo "$T21_RES" | tail -n1) + + if [[ "$T21_CODE" == "401" ]]; then + note PASS "HTTP 401 for expired nonce" + PASS=$((PASS+1)) + else + note FAIL "code=$T21_CODE" + FAIL=$((FAIL+1)) + fi +else + note SKIP "Failed to get challenge nonce for T21 — skipping Test 21" + SKIP=$((SKIP+1)) +fi + +# Test 22 — GET /api/estimate without X-Nostr-Token header +sep "Test 22 — GET /api/estimate (no token)" +T22_RES=$(curl -s -w "\n%{http_code}" "$BASE/api/estimate") +T22_BODY=$(echo "$T22_RES" | sed '$d') +T22_CODE=$(echo "$T22_RES" | tail -n1) +FREE_TIER_SERVE_NO_TOKEN=$(jq_field "$T22_BODY" '.free_tier.serve') + +if [[ "$T22_CODE" == "200" && ("$FREE_TIER_SERVE_NO_TOKEN" == "pay" || -z "$FREE_TIER_SERVE_NO_TOKEN") ]]; then + note PASS "HTTP 200, free_tier.serve is 'pay' or absent without token" + PASS=$((PASS+1)) +else + note FAIL "code=$T22_CODE body=$T22_BODY" + FAIL=$((FAIL+1)) +fi + +# Test 23 — GET /api/estimate with X-Nostr-Token for a trust-exhausted identity +sep "Test 23 — GET /api/estimate (trust exhausted)" +note SKIP "Test 23 — requires exhausting trust tier (complex setup); skipping for now" +SKIP=$((SKIP+1)) + # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- -- 2.43.0