From b51dac1b22213bdb20b7bb1b1796859ceece425e Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 16 Mar 2026 21:15:52 -0700 Subject: [PATCH] test(spaces): add API test script for space creation & member management Covers 19 test cases: space CRUD, member add by username, role changes (viewer/member/moderator/admin), email invites, removal, auth guards. Run with: ./e2e/tests/space-members-api.sh Co-Authored-By: Claude Opus 4.6 --- e2e/tests/space-members-api.sh | 368 +++++++++++++++++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100755 e2e/tests/space-members-api.sh diff --git a/e2e/tests/space-members-api.sh b/e2e/tests/space-members-api.sh new file mode 100755 index 0000000..15d22fb --- /dev/null +++ b/e2e/tests/space-members-api.sh @@ -0,0 +1,368 @@ +#!/usr/bin/env bash +# +# Space Creation & Member Management API Test +# +# Tests the full lifecycle: +# 1. Create a test space +# 2. Verify space exists +# 3. Add member by EncryptID username (each role) +# 4. List members & verify roles +# 5. Change member role +# 6. Invite by email +# 7. Remove member +# 8. Delete the test space +# +# Usage: +# ./e2e/tests/space-members-api.sh +# +# Get your token from browser: localStorage.getItem("encryptid_session") → .token +# +# Optionally set: +# BASE_URL (default: https://rspace.online) +# TEST_USER (default: jeff) — an existing EncryptID username to add as member + +set -euo pipefail + +# ── Config ── +TOKEN="${1:-}" +BASE="${BASE_URL:-https://rspace.online}" +TEST_USER="${TEST_USER:-jeff}" +TEST_SLUG="api-test-$(date +%s)" +PASS=0 +FAIL=0 +WARN=0 + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +if [[ -z "$TOKEN" ]]; then + echo -e "${RED}Usage: $0 ${NC}" + echo "" + echo "Get your token from the browser console:" + echo " JSON.parse(localStorage.getItem('encryptid_session')).token" + exit 1 +fi + +AUTH="Authorization: Bearer $TOKEN" +CT="Content-Type: application/json" + +# ── Helpers ── + +pass() { + PASS=$((PASS + 1)) + echo -e " ${GREEN}PASS${NC} $1" +} + +fail() { + FAIL=$((FAIL + 1)) + echo -e " ${RED}FAIL${NC} $1" + if [[ -n "${2:-}" ]]; then + echo -e " ${RED}$2${NC}" + fi +} + +warn() { + WARN=$((WARN + 1)) + echo -e " ${YELLOW}WARN${NC} $1" +} + +assert_status() { + local label="$1" expected="$2" actual="$3" body="${4:-}" + if [[ "$actual" == "$expected" ]]; then + pass "$label (HTTP $actual)" + else + fail "$label — expected HTTP $expected, got $actual" "$body" + fi +} + +assert_json_field() { + local label="$1" json="$2" field="$3" expected="$4" + local actual + actual=$(echo "$json" | jq -r "$field" 2>/dev/null || echo "PARSE_ERROR") + if [[ "$actual" == "$expected" ]]; then + pass "$label ($field = $actual)" + else + fail "$label — $field: expected '$expected', got '$actual'" + fi +} + +api() { + local method="$1" path="$2" + shift 2 + curl -s -w "\n%{http_code}" -X "$method" "$BASE$path" -H "$AUTH" "$@" +} + +api_with_body() { + local method="$1" path="$2" body="$3" + curl -s -w "\n%{http_code}" -X "$method" "$BASE$path" -H "$AUTH" -H "$CT" -d "$body" +} + +extract_body() { echo "$1" | sed '$d'; } +extract_status() { echo "$1" | tail -1; } + +# ── Preamble ── + +echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${BOLD} rSpace — Space Creation & Member Management API Test${NC}" +echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo "" +echo -e " Base URL: ${CYAN}$BASE${NC}" +echo -e " Test slug: ${CYAN}$TEST_SLUG${NC}" +echo -e " Test user: ${CYAN}$TEST_USER${NC}" +echo "" + +# ── 0. Verify auth token works ── + +echo -e "${BOLD}[0] Verify authentication${NC}" +RES=$(api GET "/api/spaces") +STATUS=$(extract_status "$RES") +BODY=$(extract_body "$RES") +assert_status "GET /api/spaces — token valid" "200" "$STATUS" "$BODY" +echo "" + +# ── 1. Create a test space ── + +echo -e "${BOLD}[1] Create test space${NC}" +RES=$(api_with_body POST "/api/spaces" "{\"name\":\"API Test Space\",\"slug\":\"$TEST_SLUG\",\"visibility\":\"private\"}") +STATUS=$(extract_status "$RES") +BODY=$(extract_body "$RES") +assert_status "POST /api/spaces — create space" "201" "$STATUS" "$BODY" +assert_json_field "Space slug" "$BODY" ".slug" "$TEST_SLUG" +assert_json_field "Space visibility" "$BODY" ".visibility" "private" +echo "" + +# ── 2. Verify space exists ── + +echo -e "${BOLD}[2] Verify space exists${NC}" +RES=$(api GET "/api/spaces/$TEST_SLUG") +STATUS=$(extract_status "$RES") +BODY=$(extract_body "$RES") +assert_status "GET /api/spaces/$TEST_SLUG" "200" "$STATUS" "$BODY" +assert_json_field "Space name" "$BODY" ".name" "API Test Space" +echo "" + +# ── 3. Cannot create duplicate ── + +echo -e "${BOLD}[3] Duplicate slug rejected${NC}" +RES=$(api_with_body POST "/api/spaces" "{\"name\":\"Dupe\",\"slug\":\"$TEST_SLUG\"}") +STATUS=$(extract_status "$RES") +BODY=$(extract_body "$RES") +assert_status "POST /api/spaces — duplicate slug" "409" "$STATUS" "$BODY" +echo "" + +# ── 4. Add member by username (as 'viewer') ── + +echo -e "${BOLD}[4] Add member by username (viewer)${NC}" +RES=$(api_with_body POST "/api/spaces/$TEST_SLUG/members/add" "{\"username\":\"$TEST_USER\",\"role\":\"viewer\"}") +STATUS=$(extract_status "$RES") +BODY=$(extract_body "$RES") +assert_status "POST /members/add — viewer" "200" "$STATUS" "$BODY" +assert_json_field "Role assigned" "$BODY" ".role" "viewer" + +# Capture the DID for later use +MEMBER_DID=$(echo "$BODY" | jq -r '.did // empty' 2>/dev/null) +if [[ -n "$MEMBER_DID" ]]; then + pass "Got member DID: ${MEMBER_DID:0:24}..." +else + warn "Could not extract member DID from response" +fi +echo "" + +# ── 5. List members — verify viewer is present ── + +echo -e "${BOLD}[5] List members${NC}" +RES=$(api GET "/api/spaces/$TEST_SLUG/members") +STATUS=$(extract_status "$RES") +BODY=$(extract_body "$RES") +assert_status "GET /members" "200" "$STATUS" "$BODY" + +MEMBER_COUNT=$(echo "$BODY" | jq '.members | length' 2>/dev/null || echo "0") +if [[ "$MEMBER_COUNT" -ge 1 ]]; then + pass "Members list has $MEMBER_COUNT entries" +else + fail "Expected at least 1 member, got $MEMBER_COUNT" +fi +echo "" + +# ── 6. Change role: viewer → member ── + +echo -e "${BOLD}[6] Change role: viewer → member${NC}" +if [[ -n "$MEMBER_DID" ]]; then + ENCODED_DID=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$MEMBER_DID', safe=''))") + RES=$(api_with_body PATCH "/api/spaces/$TEST_SLUG/members/$ENCODED_DID" "{\"role\":\"member\"}") + STATUS=$(extract_status "$RES") + BODY=$(extract_body "$RES") + assert_status "PATCH /members/:did — viewer→member" "200" "$STATUS" "$BODY" + assert_json_field "New role" "$BODY" ".role" "member" +else + warn "Skipped — no DID captured" +fi +echo "" + +# ── 7. Change role: member → admin ── + +echo -e "${BOLD}[7] Change role: member → admin${NC}" +if [[ -n "$MEMBER_DID" ]]; then + RES=$(api_with_body PATCH "/api/spaces/$TEST_SLUG/members/$ENCODED_DID" "{\"role\":\"admin\"}") + STATUS=$(extract_status "$RES") + BODY=$(extract_body "$RES") + assert_status "PATCH /members/:did — member→admin" "200" "$STATUS" "$BODY" + assert_json_field "New role" "$BODY" ".role" "admin" +else + warn "Skipped — no DID captured" +fi +echo "" + +# ── 8. Change role: admin → viewer (demote) ── + +echo -e "${BOLD}[8] Demote role: admin → viewer${NC}" +if [[ -n "$MEMBER_DID" ]]; then + RES=$(api_with_body PATCH "/api/spaces/$TEST_SLUG/members/$ENCODED_DID" "{\"role\":\"viewer\"}") + STATUS=$(extract_status "$RES") + BODY=$(extract_body "$RES") + assert_status "PATCH /members/:did — admin→viewer" "200" "$STATUS" "$BODY" + assert_json_field "New role" "$BODY" ".role" "viewer" +else + warn "Skipped — no DID captured" +fi +echo "" + +# ── 9. Invalid role rejected ── + +echo -e "${BOLD}[9] Invalid role rejected${NC}" +if [[ -n "$MEMBER_DID" ]]; then + RES=$(api_with_body PATCH "/api/spaces/$TEST_SLUG/members/$ENCODED_DID" "{\"role\":\"superadmin\"}") + STATUS=$(extract_status "$RES") + BODY=$(extract_body "$RES") + assert_status "PATCH /members/:did — invalid role" "400" "$STATUS" "$BODY" +else + warn "Skipped — no DID captured" +fi +echo "" + +# ── 10. Invite by email ── + +echo -e "${BOLD}[10] Invite by email${NC}" +RES=$(api_with_body POST "/api/spaces/$TEST_SLUG/invite" "{\"email\":\"test@example.com\",\"role\":\"member\"}") +STATUS=$(extract_status "$RES") +BODY=$(extract_body "$RES") +# May be 200 (ok) or 500 (SMTP not configured) — both indicate the route works +if [[ "$STATUS" == "200" ]]; then + pass "POST /invite — email invite created (HTTP $STATUS)" + INVITE_URL=$(echo "$BODY" | jq -r '.inviteUrl // empty' 2>/dev/null) + if [[ -n "$INVITE_URL" ]]; then + pass "Invite URL generated: ${INVITE_URL:0:50}..." + fi +elif [[ "$STATUS" == "500" ]]; then + warn "POST /invite — SMTP not configured (HTTP 500, expected in dev)" +else + fail "POST /invite — unexpected HTTP $STATUS" "$BODY" +fi +echo "" + +# ── 11. Invite with invalid role rejected ── + +echo -e "${BOLD}[11] Invite with invalid role rejected${NC}" +RES=$(api_with_body POST "/api/spaces/$TEST_SLUG/invite" "{\"email\":\"test@example.com\",\"role\":\"overlord\"}") +STATUS=$(extract_status "$RES") +BODY=$(extract_body "$RES") +assert_status "POST /invite — invalid role" "400" "$STATUS" "$BODY" +echo "" + +# ── 12. Add member by nonexistent username ── + +echo -e "${BOLD}[12] Add nonexistent username${NC}" +RES=$(api_with_body POST "/api/spaces/$TEST_SLUG/members/add" "{\"username\":\"nonexistent-user-xyz-99999\"}") +STATUS=$(extract_status "$RES") +BODY=$(extract_body "$RES") +assert_status "POST /members/add — nonexistent user" "404" "$STATUS" "$BODY" +echo "" + +# ── 13. Remove member ── + +echo -e "${BOLD}[13] Remove member${NC}" +if [[ -n "$MEMBER_DID" ]]; then + RES=$(api DELETE "/api/spaces/$TEST_SLUG/members/$ENCODED_DID") + STATUS=$(extract_status "$RES") + BODY=$(extract_body "$RES") + assert_status "DELETE /members/:did" "200" "$STATUS" "$BODY" +else + warn "Skipped — no DID captured" +fi +echo "" + +# ── 14. Verify member removed ── + +echo -e "${BOLD}[14] Verify member removed${NC}" +RES=$(api GET "/api/spaces/$TEST_SLUG/members") +STATUS=$(extract_status "$RES") +BODY=$(extract_body "$RES") +assert_status "GET /members after removal" "200" "$STATUS" "$BODY" +# Should have 0 non-owner members (owner isn't in members by default) +MEMBER_COUNT=$(echo "$BODY" | jq '.members | length' 2>/dev/null || echo "?") +pass "Members after removal: $MEMBER_COUNT" +echo "" + +# ── 15. Re-add as admin for multi-role verification ── + +echo -e "${BOLD}[15] Add member as admin${NC}" +RES=$(api_with_body POST "/api/spaces/$TEST_SLUG/members/add" "{\"username\":\"$TEST_USER\",\"role\":\"admin\"}") +STATUS=$(extract_status "$RES") +BODY=$(extract_body "$RES") +assert_status "POST /members/add — admin role" "200" "$STATUS" "$BODY" +assert_json_field "Role assigned" "$BODY" ".role" "admin" +echo "" + +# ── 16. Re-add as moderator (overwrite) ── + +echo -e "${BOLD}[16] Overwrite role via add (admin → moderator)${NC}" +RES=$(api_with_body POST "/api/spaces/$TEST_SLUG/members/add" "{\"username\":\"$TEST_USER\",\"role\":\"moderator\"}") +STATUS=$(extract_status "$RES") +BODY=$(extract_body "$RES") +assert_status "POST /members/add — moderator overwrite" "200" "$STATUS" "$BODY" +assert_json_field "Role assigned" "$BODY" ".role" "moderator" +echo "" + +# ── 17. Unauthenticated access denied ── + +echo -e "${BOLD}[17] Unauthenticated access denied${NC}" +RES=$(curl -s -w "\n%{http_code}" -X POST "$BASE/api/spaces" -H "$CT" -d '{"name":"Nope","slug":"nope"}') +STATUS=$(extract_status "$RES") +assert_status "POST /api/spaces — no auth" "401" "$STATUS" +echo "" + +# ── 18. Cleanup: remove member, then delete space ── + +echo -e "${BOLD}[18] Cleanup — remove member & delete space${NC}" +if [[ -n "$MEMBER_DID" ]]; then + api DELETE "/api/spaces/$TEST_SLUG/members/$ENCODED_DID" > /dev/null 2>&1 || true +fi +RES=$(api DELETE "/api/spaces/$TEST_SLUG") +STATUS=$(extract_status "$RES") +BODY=$(extract_body "$RES") +assert_status "DELETE /api/spaces/$TEST_SLUG" "200" "$STATUS" "$BODY" +echo "" + +# ── 19. Verify space gone ── + +echo -e "${BOLD}[19] Verify space deleted${NC}" +RES=$(api GET "/api/spaces/$TEST_SLUG") +STATUS=$(extract_status "$RES") +assert_status "GET deleted space" "404" "$STATUS" +echo "" + +# ── Summary ── + +TOTAL=$((PASS + FAIL)) +echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${BOLD} Results: ${GREEN}$PASS passed${NC} / ${RED}$FAIL failed${NC} / ${YELLOW}$WARN warnings${NC} (${TOTAL} total)" +echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + +if [[ $FAIL -gt 0 ]]; then + exit 1 +fi