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:
parent
1f97a2ceba
commit
b51dac1b22
|
|
@ -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
|
||||
Loading…
Reference in New Issue