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 <AUTH_TOKEN>

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-16 21:15:52 -07:00
parent 1f97a2ceba
commit b51dac1b22
1 changed files with 368 additions and 0 deletions

368
e2e/tests/space-members-api.sh Executable file
View File

@ -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 <AUTH_TOKEN>
#
# 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 <AUTH_TOKEN>${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