mirror of
https://github.com/block/buzz.git
synced 2026-10-31 06:21:34 +00:00
Remove unused NIP-28 proxy
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
parent
982fa1f483
commit
9edfd33fac
@@ -1,740 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# e2e-relay-membership.sh — End-to-end test for NIP-43 relay membership
|
||||
# =============================================================================
|
||||
# Tests relay-level membership enforcement and admin commands.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Docker services running (postgres, redis, minio)
|
||||
# - Relay built: cargo build --release --bin buzz-relay
|
||||
# - nak available on PATH (for event signing)
|
||||
#
|
||||
# What it tests:
|
||||
# 1. Non-member REST calls are rejected (401/403)
|
||||
# 2. Owner adds admin via kind:9030
|
||||
# 3. Admin adds member via kind:9030
|
||||
# 4. Owner changes member role via kind:9032
|
||||
# 5. Admin removes member via kind:9031
|
||||
# 6. Members cannot add others (permission denied)
|
||||
# 7. Admins cannot change roles (permission denied)
|
||||
# 8. Owner cannot be removed
|
||||
# 9. GET /api/relay/members returns correct list
|
||||
# 10. GET /api/relay/members/me returns correct role
|
||||
# 11. NIP-11 self field is advertised
|
||||
# 12. kind:13534 membership list published after add
|
||||
# 13. kind:8000 member-added announcement published
|
||||
# 14. kind:28936 leave request (happy path)
|
||||
# 15. kind:8001 member-removed announcement after leave
|
||||
# 16. Leave request without NIP-70 - tag is rejected
|
||||
# 17. Stale admin command rejected (replay protection)
|
||||
# 18. Owner cannot leave (lockout prevention)
|
||||
# 19. Admin cannot add another admin
|
||||
# 20. Admin cannot remove the owner
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${BLUE}[e2e-membership]${NC} $*"; }
|
||||
success() { echo -e "${GREEN}[e2e-membership]${NC} ✓ $*"; }
|
||||
warn() { echo -e "${YELLOW}[e2e-membership]${NC} ⚠ $*"; }
|
||||
|
||||
# Failure counter — tests record failures and continue; fatal() exits immediately.
|
||||
FAILURES=0
|
||||
|
||||
fail() {
|
||||
echo -e "${RED}[e2e-membership]${NC} ✗ $*" >&2
|
||||
FAILURES=$((FAILURES + 1))
|
||||
}
|
||||
|
||||
fatal() {
|
||||
echo -e "${RED}[e2e-membership]${NC} ✗ FATAL: $*" >&2
|
||||
cleanup
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── Cleanup ───────────────────────────────────────────────────────────────────
|
||||
|
||||
RELAY_PID=""
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "$RELAY_PID" ]]; then
|
||||
kill "$RELAY_PID" 2>/dev/null || true
|
||||
wait "$RELAY_PID" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# ── Helpers: keypair generation ───────────────────────────────────────────────
|
||||
|
||||
generate_keypair() {
|
||||
openssl rand -hex 32
|
||||
}
|
||||
|
||||
derive_pubkey() {
|
||||
local privkey="$1"
|
||||
python3 -c "
|
||||
P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
|
||||
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
|
||||
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
|
||||
|
||||
def point_add(p1, p2):
|
||||
if p1 is None: return p2
|
||||
if p2 is None: return p1
|
||||
x1, y1 = p1; x2, y2 = p2
|
||||
if x1 == x2 and y1 != y2: return None
|
||||
if x1 == x2: lam = (3*x1*x1) * pow(2*y1, P-2, P) % P
|
||||
else: lam = (y2-y1) * pow(x2-x1, P-2, P) % P
|
||||
x3 = (lam*lam - x1 - x2) % P
|
||||
y3 = (lam*(x1-x3) - y1) % P
|
||||
return (x3, y3)
|
||||
|
||||
def scalar_mult(k, point):
|
||||
result = None; addend = point
|
||||
while k:
|
||||
if k & 1: result = point_add(result, addend)
|
||||
addend = point_add(addend, addend)
|
||||
k >>= 1
|
||||
return result
|
||||
|
||||
pub = scalar_mult(int('$privkey', 16), (Gx, Gy))
|
||||
print(format(pub[0], '064x'))
|
||||
"
|
||||
}
|
||||
|
||||
# ── Helper: sign and send a Nostr event via nak ───────────────────────────────
|
||||
#
|
||||
# Usage: send_event <privkey> <kind> <content> [tag ...]
|
||||
# Each tag is passed as a separate argument in nak --tag format: "key=value"
|
||||
#
|
||||
# Returns the relay response on stdout.
|
||||
|
||||
send_event() {
|
||||
local privkey="$1"
|
||||
local kind="$2"
|
||||
local content="$3"
|
||||
shift 3
|
||||
local tag_args=()
|
||||
for t in "$@"; do
|
||||
tag_args+=(--tag "$t")
|
||||
done
|
||||
|
||||
nak event \
|
||||
--sec "$privkey" \
|
||||
--kind "$kind" \
|
||||
--content "$content" \
|
||||
"${tag_args[@]}" \
|
||||
ws://localhost:3000 2>&1
|
||||
}
|
||||
|
||||
# ── Helper: REST call with X-Pubkey header (dev mode) ────────────────────────
|
||||
#
|
||||
# When BUZZ_REQUIRE_AUTH_TOKEN=false the relay accepts an X-Pubkey header
|
||||
# containing the caller's hex pubkey — no token minting required.
|
||||
# This is the correct pattern for dev-mode E2E tests.
|
||||
#
|
||||
# Returns the HTTP status code on stdout.
|
||||
|
||||
rest_call() {
|
||||
local privkey="$1"
|
||||
local method="$2"
|
||||
local path="$3"
|
||||
local body="${4:-}"
|
||||
|
||||
local pubkey
|
||||
pubkey=$(derive_pubkey "$privkey")
|
||||
|
||||
local curl_args=(-s -o /dev/null -w "%{http_code}" -X "$method"
|
||||
-H "X-Pubkey: $pubkey")
|
||||
if [[ -n "$body" ]]; then
|
||||
curl_args+=(-H "Content-Type: application/json" -d "$body")
|
||||
fi
|
||||
|
||||
curl "${curl_args[@]}" "http://localhost:3000${path}"
|
||||
}
|
||||
|
||||
# ── Helper: REST GET, return body ─────────────────────────────────────────────
|
||||
|
||||
rest_get_body() {
|
||||
local privkey="$1"
|
||||
local path="$2"
|
||||
|
||||
local pubkey
|
||||
pubkey=$(derive_pubkey "$privkey")
|
||||
|
||||
curl -s -X GET \
|
||||
-H "X-Pubkey: $pubkey" \
|
||||
"http://localhost:3000${path}"
|
||||
}
|
||||
|
||||
# ── Helper: check relay OK response ──────────────────────────────────────────
|
||||
#
|
||||
# Returns 0 if the response is ["OK", <id>, true, ...], 1 otherwise.
|
||||
|
||||
is_ok() {
|
||||
echo "$1" | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
if isinstance(data, list) and len(data) >= 3 and data[0] == 'OK' and data[2] is True:
|
||||
sys.exit(0)
|
||||
except: pass
|
||||
sys.exit(1)
|
||||
" 2>/dev/null
|
||||
}
|
||||
|
||||
# ── Helper: check relay rejection (OK false) ─────────────────────────────────
|
||||
#
|
||||
# Returns 0 if the response is ["OK", <id>, false, ...], 1 otherwise.
|
||||
|
||||
is_rejected() {
|
||||
echo "$1" | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
if isinstance(data, list) and len(data) >= 3 and data[0] == 'OK' and data[2] is False:
|
||||
sys.exit(0)
|
||||
except: pass
|
||||
sys.exit(1)
|
||||
" 2>/dev/null
|
||||
}
|
||||
|
||||
# ── Start relay ───────────────────────────────────────────────────────────────
|
||||
|
||||
log "Starting relay with relay membership enforcement enabled..."
|
||||
|
||||
if [[ -f .env ]]; then
|
||||
set -o allexport
|
||||
source .env
|
||||
set +o allexport
|
||||
fi
|
||||
|
||||
export BUZZ_BIND_ADDR="0.0.0.0:3000"
|
||||
export RELAY_URL="ws://localhost:3000"
|
||||
export RUST_LOG="buzz_relay=warn"
|
||||
export BUZZ_REQUIRE_AUTH_TOKEN=false
|
||||
export BUZZ_REQUIRE_RELAY_MEMBERSHIP=true
|
||||
|
||||
# Generate owner keypair BEFORE relay start — main.rs requires RELAY_OWNER_PUBKEY
|
||||
log "Generating owner keypair..."
|
||||
OWNER_PRIVKEY=$(generate_keypair)
|
||||
OWNER_PUBKEY=$(derive_pubkey "$OWNER_PRIVKEY")
|
||||
export RELAY_OWNER_PUBKEY="$OWNER_PUBKEY"
|
||||
log "Owner pubkey: $OWNER_PUBKEY"
|
||||
|
||||
# Generate a stable relay signing key for NIP-43 self-signed events (kind:13534, etc.)
|
||||
RELAY_SK=$(generate_keypair)
|
||||
export BUZZ_RELAY_PRIVATE_KEY="$RELAY_SK"
|
||||
log "Relay signing key set (NIP-43 self-signed events enabled)"
|
||||
|
||||
# Kill any existing relay
|
||||
pkill -f "buzz-relay" 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
./target/release/buzz-relay > /tmp/buzz-relay-membership-e2e.log 2>&1 &
|
||||
RELAY_PID=$!
|
||||
|
||||
for i in $(seq 1 15); do
|
||||
if curl -s http://localhost:3000/ -H "Accept: application/nostr+json" | grep -q "Buzz"; then
|
||||
break
|
||||
fi
|
||||
if [[ $i -eq 15 ]]; then
|
||||
fatal "Relay did not start. Check /tmp/buzz-relay-membership-e2e.log"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
success "Relay started (PID $RELAY_PID)"
|
||||
|
||||
# ── Generate identities ───────────────────────────────────────────────────────
|
||||
|
||||
# Generate remaining keypairs (after relay is running)
|
||||
log "Generating remaining keypairs..."
|
||||
ADMIN_PRIVKEY=$(generate_keypair)
|
||||
ADMIN_PUBKEY=$(derive_pubkey "$ADMIN_PRIVKEY")
|
||||
MEMBER_PRIVKEY=$(generate_keypair)
|
||||
MEMBER_PUBKEY=$(derive_pubkey "$MEMBER_PRIVKEY")
|
||||
NONMEMBER_PRIVKEY=$(generate_keypair)
|
||||
NONMEMBER_PUBKEY=$(derive_pubkey "$NONMEMBER_PRIVKEY")
|
||||
ATTACKER_PRIVKEY=$(generate_keypair)
|
||||
ATTACKER_PUBKEY=$(derive_pubkey "$ATTACKER_PRIVKEY")
|
||||
|
||||
log " Owner: ${OWNER_PUBKEY:0:16}..."
|
||||
log " Admin: ${ADMIN_PUBKEY:0:16}..."
|
||||
log " Member: ${MEMBER_PUBKEY:0:16}..."
|
||||
log " Non-member:${NONMEMBER_PUBKEY:0:16}..."
|
||||
log " Attacker: ${ATTACKER_PUBKEY:0:16}..."
|
||||
|
||||
# ── Test 1: Non-member REST calls are rejected ────────────────────────────────
|
||||
|
||||
log "Test 1: Non-member REST calls should be rejected..."
|
||||
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/channels)
|
||||
if [[ "$STATUS" == "401" || "$STATUS" == "403" ]]; then
|
||||
success "Unauthenticated request rejected ($STATUS)"
|
||||
else
|
||||
fail "Expected 401/403 for unauthenticated request, got $STATUS"
|
||||
fi
|
||||
|
||||
# Non-member with valid auth should also be rejected
|
||||
STATUS=$(rest_call "$NONMEMBER_PRIVKEY" GET /api/channels)
|
||||
if [[ "$STATUS" == "401" || "$STATUS" == "403" ]]; then
|
||||
success "Non-member request rejected ($STATUS)"
|
||||
else
|
||||
fail "Non-member got $STATUS — expected 401 or 403 (membership enforcement bypass!)"
|
||||
fi
|
||||
|
||||
# ── Test 1b: Non-member WebSocket event should be rejected ───────────────────
|
||||
|
||||
log "Test 1b: Non-member WebSocket event should be rejected..."
|
||||
|
||||
# Non-member tries to send a stream message over WebSocket.
|
||||
# The relay enforces NIP-43 during NIP-42 AUTH, so the event will be rejected
|
||||
# before it is processed. Channel ID is irrelevant — auth fires first.
|
||||
RESULT=$(send_event "$NONMEMBER_PRIVKEY" 9 "h=00000000-0000-0000-0000-000000000000" 2>&1 || true)
|
||||
if echo "$RESULT" | grep -qi 'not a relay member\|restricted\|auth\|false'; then
|
||||
success "Non-member WebSocket event rejected"
|
||||
else
|
||||
warn "Could not verify WebSocket non-member rejection: $RESULT"
|
||||
fi
|
||||
|
||||
# ── Test 2: Owner adds admin (kind:9030) ──────────────────────────────────────
|
||||
|
||||
log "Test 2: Owner adds admin via kind:9030..."
|
||||
|
||||
RESULT=$(send_event "$OWNER_PRIVKEY" 9030 "" \
|
||||
"p=$ADMIN_PUBKEY" \
|
||||
"role=admin")
|
||||
if is_ok "$RESULT"; then
|
||||
success "Owner added admin (kind:9030 accepted)"
|
||||
else
|
||||
fail "Owner failed to add admin: $RESULT"
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
|
||||
# Verify admin appears in member list
|
||||
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
|
||||
if echo "$MEMBERS" | grep -q "$ADMIN_PUBKEY"; then
|
||||
success "Admin pubkey appears in /api/relay/members"
|
||||
else
|
||||
fail "Admin pubkey not found in /api/relay/members: $MEMBERS"
|
||||
fi
|
||||
|
||||
# ── Test 3: Admin adds member (kind:9030) ─────────────────────────────────────
|
||||
|
||||
log "Test 3: Admin adds member via kind:9030..."
|
||||
|
||||
RESULT=$(send_event "$ADMIN_PRIVKEY" 9030 "" \
|
||||
"p=$MEMBER_PUBKEY" \
|
||||
"role=member")
|
||||
if is_ok "$RESULT"; then
|
||||
success "Admin added member (kind:9030 accepted)"
|
||||
else
|
||||
fail "Admin failed to add member: $RESULT"
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
|
||||
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
|
||||
if echo "$MEMBERS" | grep -q "$MEMBER_PUBKEY"; then
|
||||
success "Member pubkey appears in /api/relay/members"
|
||||
else
|
||||
fail "Member pubkey not found in /api/relay/members: $MEMBERS"
|
||||
fi
|
||||
|
||||
# ── Test 4: Owner changes role (kind:9032) ────────────────────────────────────
|
||||
|
||||
log "Test 4: Owner changes member role to admin via kind:9032..."
|
||||
|
||||
RESULT=$(send_event "$OWNER_PRIVKEY" 9032 "" \
|
||||
"p=$MEMBER_PUBKEY" \
|
||||
"role=admin")
|
||||
if is_ok "$RESULT"; then
|
||||
success "Owner changed member role (kind:9032 accepted)"
|
||||
else
|
||||
fail "Owner failed to change role: $RESULT"
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
|
||||
# Verify role changed
|
||||
MEMBER_INFO=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
|
||||
if echo "$MEMBER_INFO" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
members = data if isinstance(data, list) else data.get('members', [])
|
||||
for m in members:
|
||||
if m.get('pubkey', '').startswith('${MEMBER_PUBKEY:0:8}') or m.get('pubkey') == '${MEMBER_PUBKEY}':
|
||||
if m.get('role') == 'admin':
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
" 2>/dev/null; then
|
||||
success "Member role updated to admin in /api/relay/members"
|
||||
else
|
||||
fail "Role change not reflected in /api/relay/members response"
|
||||
fi
|
||||
|
||||
# Reset member back to 'member' role for subsequent tests
|
||||
send_event "$OWNER_PRIVKEY" 9032 "" \
|
||||
"p=$MEMBER_PUBKEY" \
|
||||
"role=member" > /dev/null 2>&1 || true
|
||||
sleep 1
|
||||
|
||||
# ── Test 5: Admin removes member (kind:9031) ──────────────────────────────────
|
||||
|
||||
log "Test 5: Admin removes member via kind:9031..."
|
||||
|
||||
RESULT=$(send_event "$ADMIN_PRIVKEY" 9031 "" \
|
||||
"p=$MEMBER_PUBKEY")
|
||||
if is_ok "$RESULT"; then
|
||||
success "Admin removed member (kind:9031 accepted)"
|
||||
else
|
||||
fail "Admin failed to remove member: $RESULT"
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
|
||||
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
|
||||
if echo "$MEMBERS" | grep -q "$MEMBER_PUBKEY"; then
|
||||
fail "Member pubkey still present in /api/relay/members after removal"
|
||||
else
|
||||
success "Member pubkey absent from /api/relay/members after removal"
|
||||
fi
|
||||
|
||||
# Re-add member for permission tests
|
||||
send_event "$ADMIN_PRIVKEY" 9030 "" \
|
||||
"p=$MEMBER_PUBKEY" \
|
||||
"role=member" > /dev/null 2>&1 || true
|
||||
sleep 1
|
||||
|
||||
# ── Test 6: Members cannot add others ────────────────────────────────────────
|
||||
|
||||
log "Test 6: Member attempts to add non-member (should be denied)..."
|
||||
|
||||
RESULT=$(send_event "$MEMBER_PRIVKEY" 9030 "" \
|
||||
"p=$NONMEMBER_PUBKEY" \
|
||||
"role=member")
|
||||
if is_ok "$RESULT"; then
|
||||
fail "Member was able to add another user (should be denied)"
|
||||
elif is_rejected "$RESULT" || echo "$RESULT" | grep -qi 'restricted\|denied\|forbidden\|unauthorized\|permission'; then
|
||||
success "Member add attempt rejected"
|
||||
else
|
||||
fail "Ambiguous response for member add attempt: $RESULT"
|
||||
fi
|
||||
|
||||
# ── Test 7: Admins cannot change roles ───────────────────────────────────────
|
||||
|
||||
log "Test 7: Admin attempts to change owner role (should be denied)..."
|
||||
|
||||
RESULT=$(send_event "$ADMIN_PRIVKEY" 9032 "" \
|
||||
"p=$OWNER_PUBKEY" \
|
||||
"role=member")
|
||||
if is_ok "$RESULT"; then
|
||||
fail "Admin was able to change owner role (should be denied)"
|
||||
elif is_rejected "$RESULT" || echo "$RESULT" | grep -qi 'restricted\|denied\|forbidden\|unauthorized\|permission'; then
|
||||
success "Admin role-change on owner rejected"
|
||||
else
|
||||
fail "Ambiguous response for admin role-change attempt: $RESULT"
|
||||
fi
|
||||
|
||||
# ── Test 8: Owner cannot be removed ──────────────────────────────────────────
|
||||
|
||||
log "Test 8: Admin attempts to remove owner (should be denied)..."
|
||||
|
||||
RESULT=$(send_event "$ADMIN_PRIVKEY" 9031 "" \
|
||||
"p=$OWNER_PUBKEY")
|
||||
if is_ok "$RESULT"; then
|
||||
fail "Admin was able to remove the owner (should be denied)"
|
||||
elif is_rejected "$RESULT" || echo "$RESULT" | grep -qi 'restricted\|denied\|forbidden\|unauthorized\|permission'; then
|
||||
success "Owner removal attempt rejected"
|
||||
else
|
||||
fail "Ambiguous response for owner removal attempt: $RESULT"
|
||||
fi
|
||||
|
||||
# Verify owner still present
|
||||
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
|
||||
if echo "$MEMBERS" | grep -q "$OWNER_PUBKEY"; then
|
||||
success "Owner still present in /api/relay/members"
|
||||
else
|
||||
fail "Owner missing from /api/relay/members after removal attempt"
|
||||
fi
|
||||
|
||||
# ── Test 9: GET /api/relay/members returns correct list ───────────────────────
|
||||
|
||||
log "Test 9: GET /api/relay/members returns expected members..."
|
||||
|
||||
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
|
||||
STATUS=$(rest_call "$OWNER_PRIVKEY" GET /api/relay/members)
|
||||
|
||||
if [[ "$STATUS" == "200" ]]; then
|
||||
success "GET /api/relay/members returned 200"
|
||||
else
|
||||
fail "GET /api/relay/members returned $STATUS (expected 200)"
|
||||
fi
|
||||
|
||||
for pubkey in "$OWNER_PUBKEY" "$ADMIN_PUBKEY" "$MEMBER_PUBKEY"; do
|
||||
if echo "$MEMBERS" | grep -q "$pubkey"; then
|
||||
success " ${pubkey:0:16}... present in member list"
|
||||
else
|
||||
fail " ${pubkey:0:16}... missing from member list"
|
||||
fi
|
||||
done
|
||||
|
||||
if echo "$MEMBERS" | grep -q "$NONMEMBER_PUBKEY"; then
|
||||
fail "Non-member ${NONMEMBER_PUBKEY:0:16}... should not appear in member list"
|
||||
else
|
||||
success "Non-member correctly absent from member list"
|
||||
fi
|
||||
|
||||
# ── Test 10: GET /api/relay/members/me returns correct role ──────────────────
|
||||
|
||||
log "Test 10: GET /api/relay/members/me returns correct role..."
|
||||
|
||||
for pair in "${OWNER_PRIVKEY}:owner" "${ADMIN_PRIVKEY}:admin" "${MEMBER_PRIVKEY}:member"; do
|
||||
privkey="${pair%%:*}"
|
||||
expected_role="${pair##*:}"
|
||||
STATUS=$(rest_call "$privkey" GET /api/relay/members/me)
|
||||
BODY=$(rest_get_body "$privkey" /api/relay/members/me)
|
||||
|
||||
if [[ "$STATUS" == "200" ]]; then
|
||||
if echo "$BODY" | grep -q "\"$expected_role\""; then
|
||||
success " /api/relay/members/me: $expected_role role confirmed (status $STATUS)"
|
||||
else
|
||||
warn " /api/relay/members/me: expected role '$expected_role', got: $BODY"
|
||||
fi
|
||||
else
|
||||
warn " /api/relay/members/me returned $STATUS for $expected_role"
|
||||
fi
|
||||
done
|
||||
|
||||
# Non-member should get 403/404
|
||||
STATUS=$(rest_call "$NONMEMBER_PRIVKEY" GET /api/relay/members/me)
|
||||
if [[ "$STATUS" == "403" || "$STATUS" == "404" ]]; then
|
||||
success "Non-member /api/relay/members/me correctly rejected ($STATUS)"
|
||||
else
|
||||
warn "Non-member /api/relay/members/me returned $STATUS (expected 403/404)"
|
||||
fi
|
||||
|
||||
# ── Test 11: NIP-11 self field is advertised ──────────────────────────────────
|
||||
|
||||
log "Test 11: NIP-11 self field is advertised..."
|
||||
|
||||
RELAY_HTTP="http://localhost:3000"
|
||||
SELF_PK=$(curl -s -H "Accept: application/nostr+json" "$RELAY_HTTP" | jq -r '.self // empty')
|
||||
if [[ -n "$SELF_PK" && "$SELF_PK" != "null" ]]; then
|
||||
success "NIP-11 self field present: ${SELF_PK:0:16}..."
|
||||
else
|
||||
fail "NIP-11 self field missing from relay info document"
|
||||
fi
|
||||
|
||||
# ── Test 12: kind:13534 membership list published after add ───────────────────
|
||||
#
|
||||
# After Test 3 added a member, the relay should have published a signed
|
||||
# kind:13534 membership list event to the DB.
|
||||
|
||||
log "Test 12: kind:13534 membership list published after member add..."
|
||||
|
||||
COUNT=$(psql "$DATABASE_URL" -t -c "SELECT count(*) FROM events WHERE kind=13534;" 2>/dev/null | tr -d ' \n')
|
||||
if [[ -n "$COUNT" && "$COUNT" -gt 0 ]]; then
|
||||
success "kind:13534 membership list found in DB ($COUNT row(s))"
|
||||
else
|
||||
fail "kind:13534 not found in DB — relay may not be publishing membership lists"
|
||||
fi
|
||||
|
||||
# ── Test 13: kind:8000 member-added announcement published ────────────────────
|
||||
|
||||
log "Test 13: kind:8000 member-added announcement published..."
|
||||
|
||||
COUNT=$(psql "$DATABASE_URL" -t -c "SELECT count(*) FROM events WHERE kind=8000;" 2>/dev/null | tr -d ' \n')
|
||||
if [[ -n "$COUNT" && "$COUNT" -gt 0 ]]; then
|
||||
success "kind:8000 member-added announcement found in DB ($COUNT row(s))"
|
||||
else
|
||||
fail "kind:8000 not found in DB — relay may not be publishing member-added announcements"
|
||||
fi
|
||||
|
||||
# ── Test 14: kind:28936 leave request (happy path) ────────────────────────────
|
||||
#
|
||||
# Member sends a valid leave request with the NIP-70 protected-event `-` tag.
|
||||
# The relay should accept it and remove the member.
|
||||
|
||||
log "Test 14: kind:28936 leave request (happy path)..."
|
||||
|
||||
RESULT=$(send_event "$MEMBER_PRIVKEY" 28936 "" "-=")
|
||||
if is_ok "$RESULT"; then
|
||||
success "Leave request accepted (kind:28936 with NIP-70 - tag)"
|
||||
else
|
||||
fail "Leave request rejected: $RESULT"
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
|
||||
# Verify member was removed
|
||||
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
|
||||
if echo "$MEMBERS" | grep -q "$MEMBER_PUBKEY"; then
|
||||
fail "Member still present in /api/relay/members after leave request"
|
||||
else
|
||||
success "Member correctly absent from /api/relay/members after leave"
|
||||
fi
|
||||
|
||||
# ── Test 15: kind:8001 member-removed announcement after leave ────────────────
|
||||
|
||||
log "Test 15: kind:8001 member-removed announcement published after leave..."
|
||||
|
||||
COUNT=$(psql "$DATABASE_URL" -t -c "SELECT count(*) FROM events WHERE kind=8001;" 2>/dev/null | tr -d ' \n')
|
||||
if [[ -n "$COUNT" && "$COUNT" -gt 0 ]]; then
|
||||
success "kind:8001 member-removed announcement found in DB ($COUNT row(s))"
|
||||
else
|
||||
fail "kind:8001 not found in DB — relay may not be publishing member-removed announcements"
|
||||
fi
|
||||
|
||||
# ── Test 16: Leave request without NIP-70 - tag is rejected ──────────────────
|
||||
#
|
||||
# Re-add the member first, then send a leave WITHOUT the required `-` tag.
|
||||
# The relay must reject it (NIP-70 enforcement).
|
||||
|
||||
log "Test 16: Leave request without NIP-70 - tag is rejected..."
|
||||
|
||||
# Re-add member
|
||||
send_event "$ADMIN_PRIVKEY" 9030 "" \
|
||||
"p=$MEMBER_PUBKEY" \
|
||||
"role=member" > /dev/null 2>&1 || true
|
||||
sleep 1
|
||||
|
||||
# Send leave without the - tag
|
||||
RESULT=$(send_event "$MEMBER_PRIVKEY" 28936 "")
|
||||
if is_rejected "$RESULT" || echo "$RESULT" | grep -qi 'restricted\|denied\|forbidden\|nip-70\|protected\|false'; then
|
||||
success "Leave without NIP-70 - tag correctly rejected"
|
||||
else
|
||||
fail "Leave without NIP-70 - tag was accepted (should be rejected): $RESULT"
|
||||
fi
|
||||
|
||||
# ── Test 17: Stale admin command rejected (replay protection) ─────────────────
|
||||
#
|
||||
# An admin command with a created_at more than 5 minutes in the past must be
|
||||
# rejected to prevent replay attacks.
|
||||
|
||||
log "Test 17: Stale admin command rejected (replay protection)..."
|
||||
|
||||
OLD_TS=$(($(date +%s) - 300))
|
||||
# Use nak directly to control the timestamp; pipe output to the relay
|
||||
RESULT=$(nak event \
|
||||
--sec "$ADMIN_PRIVKEY" \
|
||||
--kind 9030 \
|
||||
--created-at "$OLD_TS" \
|
||||
--tag "p=$ATTACKER_PUBKEY" \
|
||||
--tag "role=member" \
|
||||
--content "" \
|
||||
ws://localhost:3000 2>&1 || true)
|
||||
if is_rejected "$RESULT" || echo "$RESULT" | grep -qi 'stale\|old\|expired\|restricted\|false'; then
|
||||
success "Stale admin command (created_at -5m) rejected"
|
||||
else
|
||||
fail "Stale admin command was accepted (replay protection missing): $RESULT"
|
||||
fi
|
||||
|
||||
# Verify attacker was NOT added
|
||||
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
|
||||
if echo "$MEMBERS" | grep -q "$ATTACKER_PUBKEY"; then
|
||||
fail "Attacker was added via stale command (replay protection bypass!)"
|
||||
else
|
||||
success "Attacker not present in member list after stale command"
|
||||
fi
|
||||
|
||||
# ── Test 18: Owner cannot leave (lockout prevention) ─────────────────────────
|
||||
#
|
||||
# The relay must refuse a leave request from the owner to prevent lockout.
|
||||
|
||||
log "Test 18: Owner cannot leave (lockout prevention)..."
|
||||
|
||||
RESULT=$(send_event "$OWNER_PRIVKEY" 28936 "" "-=")
|
||||
if is_rejected "$RESULT" || echo "$RESULT" | grep -qi 'owner\|restricted\|denied\|forbidden\|false'; then
|
||||
success "Owner leave request correctly rejected"
|
||||
else
|
||||
fail "Owner leave request was accepted (lockout prevention missing): $RESULT"
|
||||
fi
|
||||
|
||||
# Verify owner still present
|
||||
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
|
||||
if echo "$MEMBERS" | grep -q "$OWNER_PUBKEY"; then
|
||||
success "Owner still present in /api/relay/members after leave attempt"
|
||||
else
|
||||
fail "Owner missing from /api/relay/members after leave attempt"
|
||||
fi
|
||||
|
||||
# ── Test 19: Admin cannot add another admin ───────────────────────────────────
|
||||
#
|
||||
# Admins can only add members. Elevating to admin requires owner privilege.
|
||||
|
||||
log "Test 19: Admin cannot add another admin..."
|
||||
|
||||
RESULT=$(send_event "$ADMIN_PRIVKEY" 9030 "" \
|
||||
"p=$ATTACKER_PUBKEY" \
|
||||
"role=admin")
|
||||
if is_rejected "$RESULT" || echo "$RESULT" | grep -qi 'restricted\|denied\|forbidden\|unauthorized\|permission\|false'; then
|
||||
success "Admin cannot add another admin (correctly rejected)"
|
||||
else
|
||||
fail "Admin was able to add an admin (privilege escalation!): $RESULT"
|
||||
fi
|
||||
|
||||
# Verify attacker was NOT added as admin
|
||||
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
|
||||
if echo "$MEMBERS" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
members = data if isinstance(data, list) else data.get('members', [])
|
||||
for m in members:
|
||||
if m.get('pubkey') == '${ATTACKER_PUBKEY}' and m.get('role') == 'admin':
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
" 2>/dev/null; then
|
||||
fail "Attacker has admin role in member list (privilege escalation confirmed!)"
|
||||
else
|
||||
success "Attacker not present as admin in member list"
|
||||
fi
|
||||
|
||||
# ── Test 20: Admin cannot remove the owner ────────────────────────────────────
|
||||
|
||||
log "Test 20: Admin cannot remove the owner..."
|
||||
|
||||
RESULT=$(send_event "$ADMIN_PRIVKEY" 9031 "" \
|
||||
"p=$OWNER_PUBKEY")
|
||||
if is_rejected "$RESULT" || echo "$RESULT" | grep -qi 'restricted\|denied\|forbidden\|unauthorized\|permission\|false'; then
|
||||
success "Admin cannot remove the owner (correctly rejected)"
|
||||
else
|
||||
fail "Admin was able to remove the owner (privilege escalation!): $RESULT"
|
||||
fi
|
||||
|
||||
# Verify owner still present
|
||||
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
|
||||
if echo "$MEMBERS" | grep -q "$OWNER_PUBKEY"; then
|
||||
success "Owner still present in /api/relay/members after admin removal attempt"
|
||||
else
|
||||
fail "Owner missing from /api/relay/members — admin removed owner!"
|
||||
fi
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "Final member list:"
|
||||
echo "─────────────────────"
|
||||
rest_get_body "$OWNER_PRIVKEY" /api/relay/members | python3 -m json.tool 2>/dev/null || true
|
||||
echo ""
|
||||
|
||||
if [[ "$FAILURES" -eq 0 ]]; then
|
||||
echo -e "${GREEN}════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} ✅ All NIP-43 relay membership E2E tests passed!${NC}"
|
||||
echo -e "${GREEN}════════════════════════════════════════════════════════${NC}"
|
||||
else
|
||||
echo -e "${RED}════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${RED} ❌ ${FAILURES} test(s) failed!${NC}"
|
||||
echo -e "${RED}════════════════════════════════════════════════════════${NC}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,140 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Live e2e test for buzz-proxy — requires relay on :3000 and proxy on :4869
|
||||
set -euo pipefail
|
||||
|
||||
PROXY_URL="ws://0.0.0.0:4869"
|
||||
PROXY_HTTP="http://localhost:4869"
|
||||
ADMIN_SECRET="test-admin-secret"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
ok() { PASS=$((PASS+1)); echo " ✅ $1"; }
|
||||
fail() { FAIL=$((FAIL+1)); echo " ❌ $1"; }
|
||||
|
||||
echo "═══ buzz-proxy e2e tests ═══"
|
||||
echo ""
|
||||
|
||||
# ── Test 1: NIP-11 ──────────────────────────────────────────────────────
|
||||
echo "1. NIP-11 relay info"
|
||||
NIP11=$(curl -sf "$PROXY_HTTP" -H "Accept: application/nostr+json")
|
||||
if echo "$NIP11" | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['name']=='buzz-proxy'" 2>/dev/null; then
|
||||
ok "NIP-11 returns valid relay info"
|
||||
else
|
||||
fail "NIP-11 check failed"
|
||||
fi
|
||||
|
||||
# ── Test 2: Register guest ──────────────────────────────────────────────
|
||||
echo "2. Guest registration (pubkey-based)"
|
||||
GUEST_SK=$(nak key generate)
|
||||
GUEST_PK=$(nak key public "$GUEST_SK")
|
||||
CHANNEL_UUID="6f953bb4-f761-4bc2-98de-392470c2b897"
|
||||
|
||||
REG=$(curl -sf -X POST "$PROXY_HTTP/admin/guests" \
|
||||
-H "Authorization: Bearer $ADMIN_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"pubkey\": \"$GUEST_PK\", \"channels\": \"$CHANNEL_UUID\"}")
|
||||
if echo "$REG" | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['pubkey']=='$GUEST_PK'" 2>/dev/null; then
|
||||
ok "Guest registered: $GUEST_PK"
|
||||
else
|
||||
fail "Guest registration failed: $REG"
|
||||
fi
|
||||
|
||||
# ── Test 3: List guests ─────────────────────────────────────────────────
|
||||
echo "3. List guests"
|
||||
LIST=$(curl -sf "$PROXY_HTTP/admin/guests" -H "Authorization: Bearer $ADMIN_SECRET")
|
||||
if echo "$LIST" | python3 -c "import sys,json; d=json.load(sys.stdin); assert len(d['guests'])>=1" 2>/dev/null; then
|
||||
ok "Guest list returns registered guests"
|
||||
else
|
||||
fail "Guest list failed: $LIST"
|
||||
fi
|
||||
|
||||
# ── Test 4: Admin auth required ─────────────────────────────────────────
|
||||
echo "4. Admin auth enforcement"
|
||||
NOAUTH=$(curl -s -o /dev/null -w "%{http_code}" "$PROXY_HTTP/admin/guests")
|
||||
if [ "$NOAUTH" = "401" ]; then
|
||||
ok "Admin endpoint rejects unauthenticated requests"
|
||||
else
|
||||
fail "Expected 401, got $NOAUTH"
|
||||
fi
|
||||
|
||||
# ── Test 5: WebSocket AUTH challenge ────────────────────────────────────
|
||||
echo "5. WebSocket NIP-42 challenge"
|
||||
CHALLENGE=$(echo "" | timeout 3 websocat -t -1 "$PROXY_URL" 2>/dev/null | head -1)
|
||||
if echo "$CHALLENGE" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); assert d[0]=='AUTH'" 2>/dev/null; then
|
||||
ok "Proxy sends NIP-42 AUTH challenge on connect"
|
||||
else
|
||||
fail "No AUTH challenge received: $CHALLENGE"
|
||||
fi
|
||||
|
||||
# ── Test 6: Full NIP-42 handshake (pubkey-based, no token) ─────────────
|
||||
echo "6. Full NIP-42 auth handshake (pubkey-based)"
|
||||
|
||||
# Use a Python script to do the full handshake on a single WebSocket connection.
|
||||
RESPONSE=$(uv run python3 -c "
|
||||
import asyncio, json, subprocess, sys
|
||||
|
||||
async def test():
|
||||
import websockets
|
||||
async with websockets.connect('$PROXY_URL') as ws:
|
||||
# 1. Receive AUTH challenge
|
||||
msg = json.loads(await ws.recv())
|
||||
assert msg[0] == 'AUTH', f'Expected AUTH, got {msg[0]}'
|
||||
challenge = msg[1]
|
||||
|
||||
# 2. Create signed auth event with nak
|
||||
auth_json = subprocess.check_output([
|
||||
'nak', 'event', '--sec', '$GUEST_SK', '-k', '22242',
|
||||
'--tag', 'relay=$PROXY_URL',
|
||||
'--tag', f'challenge={challenge}',
|
||||
'-c', ''
|
||||
], text=True).strip()
|
||||
|
||||
# 3. Send AUTH response
|
||||
await ws.send(json.dumps(['AUTH', json.loads(auth_json)]))
|
||||
|
||||
# 4. Read OK
|
||||
ok_msg = json.loads(await ws.recv())
|
||||
print(json.dumps(ok_msg))
|
||||
|
||||
asyncio.run(test())
|
||||
" 2>&1) || true
|
||||
|
||||
echo " Response: $RESPONSE"
|
||||
|
||||
if echo "$RESPONSE" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); assert d[0]=='OK' and d[2]==True" 2>/dev/null; then
|
||||
ok "NIP-42 auth succeeded (pubkey-based, no token)"
|
||||
else
|
||||
# Fall back: try without websockets library (use websocat with named pipe)
|
||||
fail "NIP-42 auth handshake: $RESPONSE"
|
||||
fi
|
||||
|
||||
# ── Test 7: Create invite token ─────────────────────────────────────────
|
||||
echo "7. Invite token creation"
|
||||
INVITE=$(curl -sf -X POST "$PROXY_HTTP/admin/invite" \
|
||||
-H "Authorization: Bearer $ADMIN_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"channels\": \"$CHANNEL_UUID\", \"hours\": 1, \"max_uses\": 5}")
|
||||
TOKEN=$(echo "$INVITE" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])" 2>/dev/null)
|
||||
if [ -n "$TOKEN" ]; then
|
||||
ok "Invite token created: ${TOKEN:0:30}..."
|
||||
else
|
||||
fail "Invite token creation failed: $INVITE"
|
||||
fi
|
||||
|
||||
# ── Test 8: Revoke guest ────────────────────────────────────────────────
|
||||
echo "8. Guest revocation"
|
||||
REVOKE=$(curl -sf -X DELETE "$PROXY_HTTP/admin/guests" \
|
||||
-H "Authorization: Bearer $ADMIN_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"pubkey\": \"$GUEST_PK\"}")
|
||||
if echo "$REVOKE" | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['revoked']==True" 2>/dev/null; then
|
||||
ok "Guest revoked"
|
||||
else
|
||||
fail "Guest revocation failed: $REVOKE"
|
||||
fi
|
||||
|
||||
# ── Summary ─────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "═══ Results: $PASS passed, $FAIL failed ═══"
|
||||
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
||||
@@ -1,89 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# End-to-end test for buzz-proxy
|
||||
# Prerequisites:
|
||||
# - Buzz relay running on :3000 (just relay)
|
||||
# - Buzz proxy running on :4869 (just proxy)
|
||||
# - websocat installed (cargo install websocat)
|
||||
# - curl installed
|
||||
# - jq installed
|
||||
set -euo pipefail
|
||||
|
||||
PROXY_URL="${PROXY_URL:-ws://localhost:4869}"
|
||||
PROXY_HTTP="${PROXY_HTTP:-http://localhost:4869}"
|
||||
RELAY_HTTP="${RELAY_HTTP:-http://localhost:3000}"
|
||||
|
||||
echo "=== Buzz Proxy E2E Test ==="
|
||||
echo "Proxy: $PROXY_URL"
|
||||
echo "Relay: $RELAY_HTTP"
|
||||
|
||||
# Step 1: Check NIP-11
|
||||
echo ""
|
||||
echo "--- Step 1: NIP-11 Info Document ---"
|
||||
NIP11=$(curl -sf -H "Accept: application/nostr+json" "$PROXY_HTTP/" || true)
|
||||
if [ -z "$NIP11" ]; then
|
||||
echo "FAIL: NIP-11 endpoint not responding"
|
||||
exit 1
|
||||
fi
|
||||
echo "$NIP11" | jq .
|
||||
echo "PASS: NIP-11 document served"
|
||||
|
||||
# Step 2: Create invite token
|
||||
echo ""
|
||||
echo "--- Step 2: Create Invite Token ---"
|
||||
# Get a channel UUID from the relay
|
||||
CHANNEL_ID=$(curl -sf "$RELAY_HTTP/api/channels" -H "X-Pubkey: 0101010101010101010101010101010101010101010101010101010101010101" | jq -r '.[0].id // empty')
|
||||
if [ -z "$CHANNEL_ID" ]; then
|
||||
echo "WARN: No channels found in relay. Creating a test channel..."
|
||||
CHANNEL_ID=$(curl -sf -X POST "$RELAY_HTTP/api/channels" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Pubkey: 0101010101010101010101010101010101010101010101010101010101010101" \
|
||||
-d '{"name":"proxy-test","channel_type":"stream","visibility":"open"}' | jq -r '.id')
|
||||
fi
|
||||
echo "Using channel: $CHANNEL_ID"
|
||||
|
||||
INVITE=$(curl -sf -X POST "$PROXY_HTTP/admin/invite" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"channels\": \"$CHANNEL_ID\", \"hours\": 1, \"max_uses\": 10}" || true)
|
||||
if [ -z "$INVITE" ]; then
|
||||
echo "FAIL: Could not create invite token"
|
||||
exit 1
|
||||
fi
|
||||
TOKEN=$(echo "$INVITE" | jq -r '.token')
|
||||
echo "Token: ${TOKEN:0:30}..."
|
||||
echo "PASS: Invite token created"
|
||||
|
||||
# Step 3: Connect and test NIP-42 + kind:40 query
|
||||
echo ""
|
||||
echo "--- Step 3: WebSocket Connection Test ---"
|
||||
echo "Connecting to $PROXY_URL?token=$TOKEN ..."
|
||||
|
||||
# Use a timeout approach: send a REQ for kind:40, expect AUTH challenge + EOSE
|
||||
# websocat with -t (text) and timeout
|
||||
RESULT=$(echo '["REQ","test-sub",{"kinds":[40],"limit":10}]' | \
|
||||
timeout 5 websocat -t -1 "$PROXY_URL?token=$TOKEN" 2>/dev/null || true)
|
||||
|
||||
if [ -z "$RESULT" ]; then
|
||||
echo "FAIL: No response from proxy (timeout or connection refused)"
|
||||
echo "Make sure the proxy is running: just proxy"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Response: $RESULT"
|
||||
|
||||
# Check if we got an AUTH challenge (expected before we authenticate)
|
||||
if echo "$RESULT" | grep -q '"AUTH"'; then
|
||||
echo "PASS: Received NIP-42 AUTH challenge"
|
||||
else
|
||||
echo "INFO: Did not receive AUTH challenge in first message"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Test Summary ==="
|
||||
echo "NIP-11: PASS"
|
||||
echo "Invite creation: PASS"
|
||||
echo "WebSocket connection: PASS"
|
||||
echo ""
|
||||
echo "For full interactive testing, use:"
|
||||
echo " websocat $PROXY_URL?token=$TOKEN"
|
||||
echo " Then send: [\"AUTH\", <signed-kind-22242-event>]"
|
||||
echo " Then send: [\"REQ\",\"sub1\",{\"kinds\":[40],\"limit\":10}]"
|
||||
@@ -1,152 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
buzz-proxy E2E test using nostr-sdk v0.44 (Python bindings for rust-nostr)
|
||||
|
||||
nostr-sdk is the official Rust Nostr SDK with Python/Swift/Flutter bindings.
|
||||
Second most popular Nostr SDK after nostr-tools, powers native clients.
|
||||
|
||||
Tests:
|
||||
1. NIP-42 authentication (automatic_authentication)
|
||||
2. Channel discovery (kind:40)
|
||||
3. Channel metadata (kind:41)
|
||||
4. Send channel message (kind:42 via EventBuilder.channel_msg)
|
||||
5. Read messages (kind:42 fetch + round-trip verification)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
import nostr_sdk
|
||||
|
||||
RELAY_URL = "ws://localhost:4869"
|
||||
PRIVKEY_HEX = open("/tmp/guest3_privkey.txt").read().strip()
|
||||
CHANNEL_EVENT_ID_HEX = "8155f2a8685beb2b4c5ac1796d555ac0b8e8ae1d3a76a7a3f63cce57905cddeb"
|
||||
|
||||
results = []
|
||||
|
||||
def ok(name, detail=""):
|
||||
results.append((name, True))
|
||||
print(f" ✅ {name}" + (f" — {detail}" if detail else ""))
|
||||
|
||||
def fail(name, detail=""):
|
||||
results.append((name, False))
|
||||
print(f" ❌ {name}" + (f" — {detail}" if detail else ""))
|
||||
|
||||
|
||||
async def main():
|
||||
print("=" * 64)
|
||||
print(" nostr-sdk v0.44 (Python/Rust) E2E TEST against buzz-proxy")
|
||||
print("=" * 64)
|
||||
|
||||
keys = nostr_sdk.Keys.parse(PRIVKEY_HEX)
|
||||
pubkey = keys.public_key().to_hex()
|
||||
print(f" Relay: {RELAY_URL}")
|
||||
print(f" Pubkey: {pubkey[:24]}...")
|
||||
print()
|
||||
|
||||
relay_url = nostr_sdk.RelayUrl.parse(RELAY_URL)
|
||||
channel_eid = nostr_sdk.EventId.parse(CHANNEL_EVENT_ID_HEX)
|
||||
timeout = timedelta(seconds=10)
|
||||
|
||||
signer = nostr_sdk.NostrSigner.keys(keys)
|
||||
client = nostr_sdk.ClientBuilder().signer(signer).build()
|
||||
client.automatic_authentication(True)
|
||||
|
||||
# ── 1. Connect + NIP-42 Auth ─────────────────────────────────────────
|
||||
print("[1/5] Connect + NIP-42 Authentication")
|
||||
try:
|
||||
await client.add_relay(relay_url)
|
||||
await client.connect()
|
||||
await asyncio.sleep(2)
|
||||
ok("NIP-42 Auth", f"automatic_authentication as {pubkey[:16]}...")
|
||||
except Exception as e:
|
||||
fail("NIP-42 Auth", str(e))
|
||||
return
|
||||
|
||||
# ── 2. Channel Discovery (kind:40) ───────────────────────────────────
|
||||
print("\n[2/5] Channel Discovery (kind:40)")
|
||||
try:
|
||||
f40 = nostr_sdk.Filter().kind(nostr_sdk.Kind(40)).limit(100)
|
||||
events = await client.fetch_events(f40, timeout)
|
||||
evts = events.to_vec()
|
||||
if evts:
|
||||
for e in evts:
|
||||
content = json.loads(e.content()) if e.content() else {}
|
||||
name = content.get("name", "?")
|
||||
print(f" • {name} (id: {e.id().to_hex()[:24]}...)")
|
||||
ok("Channel Discovery", f"{len(evts)} channel(s)")
|
||||
else:
|
||||
fail("Channel Discovery", "no kind:40 events")
|
||||
except Exception as e:
|
||||
fail("Channel Discovery", str(e))
|
||||
|
||||
# ── 3. Channel Metadata (kind:41) ────────────────────────────────────
|
||||
print("\n[3/5] Channel Metadata (kind:41)")
|
||||
try:
|
||||
f41 = nostr_sdk.Filter().kind(nostr_sdk.Kind(41)).event(channel_eid).limit(5)
|
||||
meta = await client.fetch_events(f41, timeout)
|
||||
meta_vec = meta.to_vec()
|
||||
if meta_vec:
|
||||
c = json.loads(meta_vec[0].content())
|
||||
ok("Channel Metadata", f'name="{c.get("name")}", about="{c.get("about","")[:50]}"')
|
||||
else:
|
||||
fail("Channel Metadata", "no kind:41 events")
|
||||
except Exception as e:
|
||||
fail("Channel Metadata", str(e))
|
||||
|
||||
# ── 4. Send Channel Message (kind:42) ────────────────────────────────
|
||||
print("\n[4/5] Send Channel Message (EventBuilder.channel_msg)")
|
||||
ts = int(time.time())
|
||||
msg_text = f"Hello from nostr-sdk Python! 🐍 [{ts}]"
|
||||
try:
|
||||
builder = nostr_sdk.EventBuilder.channel_msg(channel_eid, relay_url, msg_text)
|
||||
output = await client.send_event_builder(builder)
|
||||
sent_id = output.id.to_hex() # .id is a property, not a method
|
||||
ok("Send Message", f'"{msg_text}" (id: {sent_id[:16]}...)')
|
||||
except Exception as e:
|
||||
fail("Send Message", str(e))
|
||||
|
||||
# ── 5. Read Messages + Round-Trip ────────────────────────────────────
|
||||
print("\n[5/5] Read Messages + Round-Trip Verification")
|
||||
try:
|
||||
f42 = nostr_sdk.Filter().kind(nostr_sdk.Kind(42)).event(channel_eid).limit(10)
|
||||
msgs = await client.fetch_events(f42, timeout)
|
||||
msg_vec = msgs.to_vec()
|
||||
found_ours = False
|
||||
print(f" {len(msg_vec)} message(s):")
|
||||
for e in msg_vec:
|
||||
marker = "→ " if "nostr-sdk Python" in e.content() else " "
|
||||
print(f" {marker}[{e.author().to_hex()[:12]}...] {e.content()[:65]}")
|
||||
if "nostr-sdk Python" in e.content():
|
||||
found_ours = True
|
||||
if found_ours:
|
||||
ok("Read + Round-Trip", f"our message verified among {len(msg_vec)}")
|
||||
elif msg_vec:
|
||||
ok("Read + Round-Trip", f"{len(msg_vec)} messages (ours may be propagating)")
|
||||
else:
|
||||
fail("Read + Round-Trip", "no messages")
|
||||
except Exception as e:
|
||||
fail("Read + Round-Trip", str(e))
|
||||
|
||||
await client.disconnect()
|
||||
|
||||
# ── Summary ──────────────────────────────────────────────────────────
|
||||
print("\n" + "=" * 64)
|
||||
print(" RESULTS")
|
||||
print("=" * 64)
|
||||
all_pass = True
|
||||
for name, passed in results:
|
||||
print(f" {'✅ PASS' if passed else '❌ FAIL'} {name}")
|
||||
if not passed:
|
||||
all_pass = False
|
||||
print("=" * 64)
|
||||
if all_pass:
|
||||
print(" 🎉 ALL TESTS PASSED — nostr-sdk Python works with buzz-proxy!")
|
||||
else:
|
||||
print(" ⚠️ Some tests failed")
|
||||
print("=" * 64)
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -1,260 +0,0 @@
|
||||
/**
|
||||
* buzz-proxy E2E test using nostr-tools v2.23.3
|
||||
*
|
||||
* nostr-tools is the most widely-used Nostr library in the ecosystem,
|
||||
* powering Coracle, Snort, Damus Web, and hundreds of other clients.
|
||||
*
|
||||
* Tests the full NIP-28 channel flow through buzz-proxy:
|
||||
* 1. NIP-42 authentication (automatic via onauth callback)
|
||||
* 2. Channel discovery (kind:40)
|
||||
* 3. Channel metadata (kind:41)
|
||||
* 4. Send a channel message (kind:42 via NIP-28 channelMessageEvent)
|
||||
* 5. Receive messages (kind:42 subscription)
|
||||
* 6. Live streaming (real-time event delivery)
|
||||
*/
|
||||
|
||||
import { Relay } from 'nostr-tools/relay'
|
||||
import { finalizeEvent, getPublicKey } from 'nostr-tools/pure'
|
||||
import { channelMessageEvent } from 'nostr-tools/nip28'
|
||||
import { hexToBytes } from 'nostr-tools/utils'
|
||||
import WebSocket from 'ws'
|
||||
|
||||
// ── Config ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const RELAY_URL = process.env.RELAY_URL || 'ws://localhost:4869'
|
||||
const GUEST_PRIVKEY_HEX = process.env.GUEST_PRIVKEY
|
||||
const CHANNEL_EVENT_ID = process.env.CHANNEL_EVENT_ID
|
||||
|
||||
if (!GUEST_PRIVKEY_HEX || !CHANNEL_EVENT_ID) {
|
||||
console.error('Usage: GUEST_PRIVKEY=<hex> CHANNEL_EVENT_ID=<hex> node test-buzz-proxy.mjs')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const secretKey = hexToBytes(GUEST_PRIVKEY_HEX)
|
||||
const pubkey = getPublicKey(secretKey)
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const results = []
|
||||
function pass(name, detail) {
|
||||
results.push({ name, ok: true })
|
||||
console.log(` ✅ ${name}${detail ? ` — ${detail}` : ''}`)
|
||||
}
|
||||
function fail(name, detail) {
|
||||
results.push({ name, ok: false })
|
||||
console.log(` ❌ ${name}${detail ? ` — ${detail}` : ''}`)
|
||||
}
|
||||
|
||||
/** Collect events from a subscription until EOSE, with timeout. */
|
||||
function list(relay, filters, timeoutMs = 8000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const events = []
|
||||
const timer = setTimeout(() => {
|
||||
sub.close()
|
||||
resolve(events)
|
||||
}, timeoutMs)
|
||||
|
||||
const sub = relay.subscribe(filters, {
|
||||
onevent(evt) { events.push(evt) },
|
||||
oneose() {
|
||||
clearTimeout(timer)
|
||||
sub.close()
|
||||
resolve(events)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
console.log('═'.repeat(64))
|
||||
console.log(' nostr-tools v2.23 E2E TEST against buzz-proxy')
|
||||
console.log('═'.repeat(64))
|
||||
console.log(` Relay: ${RELAY_URL}`)
|
||||
console.log(` Pubkey: ${pubkey.slice(0, 24)}...`)
|
||||
console.log()
|
||||
|
||||
// ── 1. Connect + NIP-42 Auth ──────────────────────────────────────────
|
||||
|
||||
console.log('[1/6] Connect + NIP-42 Authentication')
|
||||
|
||||
// nostr-tools' Relay class calls onauth when it receives an AUTH challenge.
|
||||
// We set it before connecting so the handshake is automatic.
|
||||
const relay = new Relay(RELAY_URL, { websocketImplementation: WebSocket })
|
||||
|
||||
// Wire up the NIP-42 auth callback BEFORE connecting.
|
||||
// nostr-tools calls: relay.auth(onauth) → onauth(makeAuthEvent(url, challenge))
|
||||
// So onauth receives an unsigned event template and must return a signed event.
|
||||
let authResolved = false
|
||||
relay.onauth = async (authEventTemplate) => {
|
||||
const signed = finalizeEvent(authEventTemplate, secretKey)
|
||||
authResolved = true
|
||||
return signed
|
||||
}
|
||||
|
||||
try {
|
||||
await relay.connect()
|
||||
} catch (e) {
|
||||
fail('Connect', e.message)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Wait a moment for the AUTH challenge to arrive and be handled
|
||||
// nostr-tools' onauth is called synchronously when the AUTH message arrives
|
||||
await new Promise(r => setTimeout(r, 1500))
|
||||
|
||||
if (authResolved) {
|
||||
pass('NIP-42 Auth', `challenge received, signed as ${pubkey.slice(0, 16)}...`)
|
||||
} else {
|
||||
// Auth might still work reactively — the proxy sends CLOSED/OK auth-required
|
||||
// which triggers nostr-tools to call relay.auth()
|
||||
console.log(' ⏳ No proactive AUTH challenge yet — testing reactive auth...')
|
||||
}
|
||||
|
||||
// ── 2. Channel Discovery (kind:40) ────────────────────────────────────
|
||||
|
||||
console.log('\n[2/6] Channel Discovery (kind:40)')
|
||||
|
||||
try {
|
||||
const channels = await list(relay, [{ kinds: [40], limit: 100 }])
|
||||
if (channels.length > 0) {
|
||||
for (const evt of channels) {
|
||||
try {
|
||||
const c = JSON.parse(evt.content)
|
||||
console.log(` • ${c.name || '(unnamed)'} (id: ${evt.id.slice(0, 24)}...)`)
|
||||
} catch { console.log(` • (id: ${evt.id.slice(0, 24)}...)`) }
|
||||
}
|
||||
pass('Channel Discovery', `${channels.length} channel(s)`)
|
||||
} else {
|
||||
fail('Channel Discovery', 'no kind:40 events returned')
|
||||
}
|
||||
} catch (e) {
|
||||
fail('Channel Discovery', e.message)
|
||||
}
|
||||
|
||||
// ── 3. Channel Metadata (kind:41) ─────────────────────────────────────
|
||||
|
||||
console.log('\n[3/6] Channel Metadata (kind:41)')
|
||||
|
||||
try {
|
||||
const meta = await list(relay, [{ kinds: [41], '#e': [CHANNEL_EVENT_ID], limit: 5 }])
|
||||
if (meta.length > 0) {
|
||||
const c = JSON.parse(meta[0].content)
|
||||
pass('Channel Metadata', `name="${c.name}", about="${(c.about || '').slice(0, 50)}"`)
|
||||
} else {
|
||||
fail('Channel Metadata', 'no kind:41 events')
|
||||
}
|
||||
} catch (e) {
|
||||
fail('Channel Metadata', e.message)
|
||||
}
|
||||
|
||||
// ── 4. Send Channel Message (kind:42 via NIP-28) ──────────────────────
|
||||
|
||||
console.log('\n[4/6] Send Channel Message (NIP-28 channelMessageEvent)')
|
||||
|
||||
const ts = Math.floor(Date.now() / 1000)
|
||||
const msgText = `Hello from nostr-tools! 📦 [${ts}]`
|
||||
|
||||
let sentEvent
|
||||
try {
|
||||
sentEvent = channelMessageEvent({
|
||||
channel_create_event_id: CHANNEL_EVENT_ID,
|
||||
relay_url: RELAY_URL,
|
||||
content: msgText,
|
||||
created_at: ts,
|
||||
}, secretKey)
|
||||
|
||||
await relay.publish(sentEvent)
|
||||
pass('Send Message', `"${msgText}" (id: ${sentEvent.id.slice(0, 16)}...)`)
|
||||
} catch (e) {
|
||||
fail('Send Message', e.message)
|
||||
}
|
||||
|
||||
// ── 5. Read Messages (kind:42) ────────────────────────────────────────
|
||||
|
||||
console.log('\n[5/6] Read Messages (kind:42 query)')
|
||||
|
||||
try {
|
||||
const msgs = await list(relay, [{ kinds: [42], '#e': [CHANNEL_EVENT_ID], limit: 10 }])
|
||||
let foundOurs = false
|
||||
console.log(` ${msgs.length} message(s):`)
|
||||
for (const evt of msgs) {
|
||||
const marker = evt.content.includes('nostr-tools') ? '→ ' : ' '
|
||||
console.log(` ${marker}[${evt.pubkey.slice(0, 12)}...] ${evt.content.slice(0, 65)}`)
|
||||
if (evt.content.includes('nostr-tools')) foundOurs = true
|
||||
}
|
||||
if (foundOurs) {
|
||||
pass('Read Messages', `found our message among ${msgs.length}`)
|
||||
} else if (msgs.length > 0) {
|
||||
pass('Read Messages', `${msgs.length} messages (ours may be propagating)`)
|
||||
} else {
|
||||
fail('Read Messages', 'no messages')
|
||||
}
|
||||
} catch (e) {
|
||||
fail('Read Messages', e.message)
|
||||
}
|
||||
|
||||
// ── 6. Live Streaming ─────────────────────────────────────────────────
|
||||
|
||||
console.log('\n[6/6] Live Streaming (real-time event delivery)')
|
||||
|
||||
try {
|
||||
const streamResult = await new Promise((resolve) => {
|
||||
const timer = setTimeout(() => { sub.close(); resolve(false) }, 8000)
|
||||
|
||||
const sub = relay.subscribe([{
|
||||
kinds: [42],
|
||||
'#e': [CHANNEL_EVENT_ID],
|
||||
since: ts,
|
||||
}], {
|
||||
onevent(evt) {
|
||||
if (evt.content.includes('stream-ping')) {
|
||||
clearTimeout(timer)
|
||||
sub.close()
|
||||
resolve(true)
|
||||
}
|
||||
},
|
||||
oneose() {
|
||||
// After EOSE, publish a new message that should arrive live
|
||||
const ping = channelMessageEvent({
|
||||
channel_create_event_id: CHANNEL_EVENT_ID,
|
||||
relay_url: RELAY_URL,
|
||||
content: `nostr-tools stream-ping ${Date.now()}`,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
}, secretKey)
|
||||
relay.publish(ping).catch(() => {})
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
if (streamResult) {
|
||||
pass('Live Streaming', 'received real-time event')
|
||||
} else {
|
||||
fail('Live Streaming', 'timed out')
|
||||
}
|
||||
} catch (e) {
|
||||
fail('Live Streaming', e.message)
|
||||
}
|
||||
|
||||
// ── Summary ───────────────────────────────────────────────────────────
|
||||
|
||||
relay.close()
|
||||
|
||||
console.log('\n' + '═'.repeat(64))
|
||||
console.log(' RESULTS')
|
||||
console.log('═'.repeat(64))
|
||||
let allPass = true
|
||||
for (const r of results) {
|
||||
console.log(` ${r.ok ? '✅ PASS' : '❌ FAIL'} ${r.name}`)
|
||||
if (!r.ok) allPass = false
|
||||
}
|
||||
console.log('═'.repeat(64))
|
||||
console.log(allPass
|
||||
? ' 🎉 ALL TESTS PASSED — nostr-tools works with buzz-proxy!'
|
||||
: ' ⚠️ Some tests failed')
|
||||
console.log('═'.repeat(64))
|
||||
process.exit(allPass ? 0 : 1)
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('Fatal:', e); process.exit(1) })
|
||||
Reference in New Issue
Block a user