#!/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