skillhub/scripts/namespace-smoke-test.sh
2026-03-16 16:03:46 +08:00

257 lines
8.1 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${1:-http://localhost:8080}"
PASS=0
FAIL=0
USER_COOKIE="$(mktemp)"
ADMIN_COOKIE="$(mktemp)"
SLUG="nsmoke$(date +%s)"
cleanup() {
rm -f "$USER_COOKIE" "$ADMIN_COOKIE"
}
trap cleanup EXIT
pass() {
echo "PASS: $1"
PASS=$((PASS + 1))
}
fail() {
echo "FAIL: $1"
FAIL=$((FAIL + 1))
}
csrf_token() {
local cookie_file="$1"
awk '$6 == "XSRF-TOKEN" { print $7 }' "$cookie_file" | tail -n 1
}
bootstrap_csrf() {
local cookie_file="$1"
local user_id="$2"
curl -s -c "$cookie_file" -H "X-Mock-User-Id: $user_id" "$BASE_URL/api/v1/auth/providers" >/dev/null
}
json_field() {
local json="$1"
local expr="$2"
JSON_INPUT="$json" python3 - "$expr" <<'PY'
import json
import os
import sys
expr = sys.argv[1]
data = json.loads(os.environ["JSON_INPUT"])
value = data
for part in expr.split('.'):
if part.isdigit():
value = value[int(part)]
else:
value = value[part]
if isinstance(value, (dict, list)):
print(json.dumps(value, ensure_ascii=False))
else:
print(value)
PY
}
assert_code() {
local description="$1"
local json="$2"
local expected="$3"
local actual
actual="$(json_field "$json" "code")"
if [[ "$actual" == "$expected" ]]; then
pass "$description"
else
fail "$description (expected code $expected, got $actual)"
fi
}
USER_HEADERS=(-H "X-Mock-User-Id: local-user" -b "$USER_COOKIE" -c "$USER_COOKIE")
ADMIN_HEADERS=(-H "X-Mock-User-Id: local-admin" -b "$ADMIN_COOKIE" -c "$ADMIN_COOKIE")
echo "=== Namespace Workflow Smoke Test ==="
echo "Target: $BASE_URL"
echo "Slug: $SLUG"
echo
bootstrap_csrf "$USER_COOKIE" "local-user"
bootstrap_csrf "$ADMIN_COOKIE" "local-admin"
USER_CSRF="$(csrf_token "$USER_COOKIE")"
ADMIN_CSRF="$(csrf_token "$ADMIN_COOKIE")"
if [[ -z "$USER_CSRF" || -z "$ADMIN_CSRF" ]]; then
echo "Could not bootstrap CSRF tokens"
exit 1
fi
CREATE_RESPONSE="$(curl -sS "${USER_HEADERS[@]}" \
-H "X-XSRF-TOKEN: $USER_CSRF" \
-H "Content-Type: application/json" \
-X POST "$BASE_URL/api/web/namespaces" \
-d "{\"slug\":\"$SLUG\",\"displayName\":\"Namespace Smoke $SLUG\",\"description\":\"namespace workflow smoke test\"}")"
assert_code "Owner can create namespace" "$CREATE_RESPONSE" "0"
NAMESPACE_ID="$(json_field "$CREATE_RESPONSE" "data.id")"
MINE_RESPONSE="$(curl -sS "${USER_HEADERS[@]}" "$BASE_URL/api/web/me/namespaces")"
assert_code "Owner can list my namespaces" "$MINE_RESPONSE" "0"
if JSON_INPUT="$MINE_RESPONSE" python3 - "$SLUG" <<'PY'
import json
import os
import sys
slug = sys.argv[1]
data = json.loads(os.environ["JSON_INPUT"])
items = data["data"]
match = next((item for item in items if item["slug"] == slug), None)
if not match:
raise SystemExit(1)
if match["currentUserRole"] != "OWNER":
raise SystemExit(2)
if match["status"] != "ACTIVE":
raise SystemExit(3)
PY
then
pass "Created namespace shows up as ACTIVE owner namespace"
else
fail "Created namespace should appear in owner namespace list with OWNER role"
fi
ADMIN_MINE_RESPONSE="$(curl -sS "${ADMIN_HEADERS[@]}" "$BASE_URL/api/web/me/namespaces")"
assert_code "Other user can list my namespaces" "$ADMIN_MINE_RESPONSE" "0"
if JSON_INPUT="$ADMIN_MINE_RESPONSE" python3 - "$SLUG" <<'PY'
import json
import os
import sys
slug = sys.argv[1]
data = json.loads(os.environ["JSON_INPUT"])
items = data["data"]
raise SystemExit(0 if all(item["slug"] != slug for item in items) else 1)
PY
then
pass "Namespace is not visible to unrelated users in my namespaces"
else
fail "Unrelated user should not see team namespace in my namespaces"
fi
FREEZE_FORBIDDEN_RESPONSE="$(curl -sS "${ADMIN_HEADERS[@]}" \
-H "X-XSRF-TOKEN: $ADMIN_CSRF" \
-X POST "$BASE_URL/api/web/namespaces/$SLUG/freeze")"
assert_code "Unrelated user cannot freeze namespace" "$FREEZE_FORBIDDEN_RESPONSE" "403"
CANDIDATES_RESPONSE="$(curl -sS "${USER_HEADERS[@]}" "$BASE_URL/api/web/namespaces/$SLUG/member-candidates?search=local")"
assert_code "Owner can search namespace member candidates" "$CANDIDATES_RESPONSE" "0"
if JSON_INPUT="$CANDIDATES_RESPONSE" python3 - <<'PY'
import json
import os
import sys
data = json.loads(os.environ["JSON_INPUT"])
ids = {item["userId"] for item in data["data"]}
raise SystemExit(0 if "local-admin" in ids else 1)
PY
then
pass "Candidate search returns local-admin"
else
fail "Candidate search should include local-admin"
fi
ADD_MEMBER_RESPONSE="$(curl -sS "${USER_HEADERS[@]}" \
-H "X-XSRF-TOKEN: $USER_CSRF" \
-H "Content-Type: application/json" \
-X POST "$BASE_URL/api/web/namespaces/$SLUG/members" \
-d '{"userId":"local-admin","role":"MEMBER"}')"
assert_code "Owner can add namespace members" "$ADD_MEMBER_RESPONSE" "0"
MEMBERS_RESPONSE="$(curl -sS "${USER_HEADERS[@]}" "$BASE_URL/api/web/namespaces/$SLUG/members")"
assert_code "Owner can list namespace members" "$MEMBERS_RESPONSE" "0"
if JSON_INPUT="$MEMBERS_RESPONSE" python3 - <<'PY'
import json
import os
import sys
data = json.loads(os.environ["JSON_INPUT"])
items = data["data"]["items"]
ids = {item["userId"] for item in items}
raise SystemExit(0 if {"local-user", "local-admin"}.issubset(ids) else 1)
PY
then
pass "Member list shows owner and invited admin user"
else
fail "Member list should contain owner and invited user"
fi
REVIEWS_RESPONSE="$(curl -sS "${USER_HEADERS[@]}" "$BASE_URL/api/web/reviews?status=PENDING&namespaceId=$NAMESPACE_ID")"
assert_code "Owner can open namespace review list" "$REVIEWS_RESPONSE" "0"
PROMOTE_RESPONSE="$(curl -sS "${USER_HEADERS[@]}" \
-H "X-XSRF-TOKEN: $USER_CSRF" \
-H "Content-Type: application/json" \
-X PUT "$BASE_URL/api/web/namespaces/$SLUG/members/local-admin/role" \
-d '{"role":"ADMIN"}')"
assert_code "Owner can promote member to admin" "$PROMOTE_RESPONSE" "0"
ADMIN_FREEZE_RESPONSE="$(curl -sS "${ADMIN_HEADERS[@]}" \
-H "X-XSRF-TOKEN: $ADMIN_CSRF" \
-X POST "$BASE_URL/api/web/namespaces/$SLUG/freeze")"
assert_code "Namespace admin can freeze namespace" "$ADMIN_FREEZE_RESPONSE" "0"
if [[ "$(json_field "$ADMIN_FREEZE_RESPONSE" "data.status")" == "FROZEN" ]]; then
pass "Freeze changes namespace status to FROZEN"
else
fail "Freeze should set namespace status to FROZEN"
fi
ADD_WHILE_FROZEN_RESPONSE="$(curl -sS "${USER_HEADERS[@]}" \
-H "X-XSRF-TOKEN: $USER_CSRF" \
-H "Content-Type: application/json" \
-X POST "$BASE_URL/api/web/namespaces/$SLUG/members" \
-d '{"userId":"local-user","role":"MEMBER"}')"
assert_code "Frozen namespace rejects member mutation" "$ADD_WHILE_FROZEN_RESPONSE" "400"
ADMIN_UNFREEZE_RESPONSE="$(curl -sS "${ADMIN_HEADERS[@]}" \
-H "X-XSRF-TOKEN: $ADMIN_CSRF" \
-X POST "$BASE_URL/api/web/namespaces/$SLUG/unfreeze")"
assert_code "Namespace admin can unfreeze namespace" "$ADMIN_UNFREEZE_RESPONSE" "0"
ADMIN_ARCHIVE_RESPONSE="$(curl -sS "${ADMIN_HEADERS[@]}" \
-H "X-XSRF-TOKEN: $ADMIN_CSRF" \
-H "Content-Type: application/json" \
-X POST "$BASE_URL/api/web/namespaces/$SLUG/archive" \
-d '{"reason":"smoke"}')"
assert_code "Namespace admin cannot archive namespace" "$ADMIN_ARCHIVE_RESPONSE" "403"
OWNER_ARCHIVE_RESPONSE="$(curl -sS "${USER_HEADERS[@]}" \
-H "X-XSRF-TOKEN: $USER_CSRF" \
-H "Content-Type: application/json" \
-X POST "$BASE_URL/api/web/namespaces/$SLUG/archive" \
-d '{"reason":"smoke"}')"
assert_code "Owner can archive namespace" "$OWNER_ARCHIVE_RESPONSE" "0"
if [[ "$(json_field "$OWNER_ARCHIVE_RESPONSE" "data.status")" == "ARCHIVED" ]]; then
pass "Archive changes namespace status to ARCHIVED"
else
fail "Archive should set namespace status to ARCHIVED"
fi
OWNER_RESTORE_RESPONSE="$(curl -sS "${USER_HEADERS[@]}" \
-H "X-XSRF-TOKEN: $USER_CSRF" \
-X POST "$BASE_URL/api/web/namespaces/$SLUG/restore")"
assert_code "Owner can restore archived namespace" "$OWNER_RESTORE_RESPONSE" "0"
if [[ "$(json_field "$OWNER_RESTORE_RESPONSE" "data.status")" == "ACTIVE" ]]; then
pass "Restore changes namespace status back to ACTIVE"
else
fail "Restore should set namespace status back to ACTIVE"
fi
REMOVE_MEMBER_RESPONSE="$(curl -sS "${USER_HEADERS[@]}" \
-H "X-XSRF-TOKEN: $USER_CSRF" \
-X DELETE "$BASE_URL/api/web/namespaces/$SLUG/members/local-admin")"
assert_code "Owner can remove namespace admin" "$REMOVE_MEMBER_RESPONSE" "0"
echo
echo "Results: $PASS passed, $FAIL failed"
if [[ "$FAIL" -ne 0 ]]; then
exit 1
fi