#!/usr/bin/env bash set -euo pipefail BASE_URL="${1:-http://localhost:8080}" PASS=0 FAIL=0 COOKIE_JAR="$(mktemp)" REGISTER_RESPONSE_FILE="$(mktemp)" AUTH_METHODS_RESPONSE_FILE="$(mktemp)" AUTH_PROVIDERS_RESPONSE_FILE="$(mktemp)" USERNAME="smoketest_$(date +%s)" EMAIL="${USERNAME}@example.com" PASSWORD="Smoke@2026" NEW_PASSWORD="Smoke@2027" cleanup() { rm -f \ "$COOKIE_JAR" \ "$REGISTER_RESPONSE_FILE" \ "$AUTH_METHODS_RESPONSE_FILE" \ "$AUTH_PROVIDERS_RESPONSE_FILE" } trap cleanup EXIT check() { local desc="$1" local url="$2" local expected="$3" local status status="$(curl --retry 3 --retry-delay 1 --max-time 10 -s -o /dev/null -w "%{http_code}" "$url" || true)" if [[ "$status" == "$expected" ]]; then echo "PASS: $desc (HTTP $status)" PASS=$((PASS + 1)) else echo "FAIL: $desc (expected $expected, got $status)" FAIL=$((FAIL + 1)) fi } echo "=== SkillHub Smoke Test ===" echo "Target: $BASE_URL" echo check "Health endpoint" "$BASE_URL/actuator/health" "200" check "Prometheus metrics requires auth" "$BASE_URL/actuator/prometheus" "401" check "Namespaces API requires auth" "$BASE_URL/api/v1/namespaces" "401" check "Auth required" "$BASE_URL/api/v1/auth/me" "401" check "Unknown OAuth provider fails before upstream redirect" \ "$BASE_URL/oauth2/authorization/__missing_provider__" "403" AUTH_METHODS_STATUS="$(curl --max-time 10 -s \ -o "$AUTH_METHODS_RESPONSE_FILE" \ -w "%{http_code}" \ "$BASE_URL/api/v1/auth/methods" || true)" AUTH_PROVIDERS_STATUS="$(curl --max-time 10 -s \ -o "$AUTH_PROVIDERS_RESPONSE_FILE" \ -w "%{http_code}" \ "$BASE_URL/api/v1/auth/providers" || true)" if [[ "$AUTH_METHODS_STATUS" == "200" \ && "$AUTH_PROVIDERS_STATUS" == "200" ]] \ && python3 - \ "$AUTH_METHODS_RESPONSE_FILE" \ "$AUTH_PROVIDERS_RESPONSE_FILE" <<'PY' import json import sys with open(sys.argv[1], encoding="utf-8") as response: methods = json.load(response)["data"] with open(sys.argv[2], encoding="utf-8") as response: providers = json.load(response)["data"] method_ids = {method["id"] for method in methods} oauth_methods = { method["provider"]: method for method in methods if method["methodType"] == "OAUTH_REDIRECT" } oauth_providers = {provider["id"]: provider for provider in providers} valid = ( "local-password" in method_ids and oauth_methods.keys() == oauth_providers.keys() and all( method["actionUrl"].startswith( f"/oauth2/authorization/{provider_code}" ) for provider_code, method in oauth_methods.items() ) ) raise SystemExit(0 if valid else 1) PY then echo "PASS: Authentication catalog exposes only reconciled OAuth providers" PASS=$((PASS + 1)) else echo "FAIL: Authentication catalog is inconsistent (methods HTTP $AUTH_METHODS_STATUS, providers HTTP $AUTH_PROVIDERS_STATUS)" FAIL=$((FAIL + 1)) fi curl -s -c "$COOKIE_JAR" "$BASE_URL/api/v1/auth/me" >/dev/null CSRF_TOKEN="$(awk '$6 == "XSRF-TOKEN" { print $7 }' "$COOKIE_JAR" | tail -n 1)" REGISTER_STATUS="$(curl --max-time 10 -s -o "$REGISTER_RESPONSE_FILE" -w "%{http_code}" \ -X POST "$BASE_URL/api/v1/auth/local/register" \ -b "$COOKIE_JAR" \ -c "$COOKIE_JAR" \ -H "X-XSRF-TOKEN: $CSRF_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD\",\"email\":\"$EMAIL\"}" || true)" if [[ "$REGISTER_STATUS" == "200" ]]; then echo "PASS: Register (HTTP $REGISTER_STATUS)" PASS=$((PASS + 1)) else echo "FAIL: Register (got $REGISTER_STATUS)" FAIL=$((FAIL + 1)) fi REGISTERED_USER_ID="$(python3 - "$REGISTER_RESPONSE_FILE" <<'PY' import json import sys try: with open(sys.argv[1], encoding="utf-8") as response: print(json.load(response)["data"]["userId"]) except (KeyError, TypeError, json.JSONDecodeError): pass PY )" AUTH_ME_STATUS="$(curl --max-time 10 -s -o /dev/null -w "%{http_code}" -b "$COOKIE_JAR" "$BASE_URL/api/v1/auth/me" || true)" if [[ "$AUTH_ME_STATUS" == "200" ]]; then echo "PASS: Auth me with session (HTTP $AUTH_ME_STATUS)" PASS=$((PASS + 1)) else echo "FAIL: Auth me with session (got $AUTH_ME_STATUS)" FAIL=$((FAIL + 1)) fi NAMESPACES_AUTH_STATUS="$(curl --max-time 10 -s -o /dev/null -w "%{http_code}" -b "$COOKIE_JAR" "$BASE_URL/api/v1/namespaces" || true)" if [[ "$NAMESPACES_AUTH_STATUS" == "200" ]]; then echo "PASS: Namespaces API with session (HTTP $NAMESPACES_AUTH_STATUS)" PASS=$((PASS + 1)) else echo "FAIL: Namespaces API with session (got $NAMESPACES_AUTH_STATUS)" FAIL=$((FAIL + 1)) fi CHANGE_PASSWORD_STATUS="$(curl --max-time 10 -s -o /dev/null -w "%{http_code}" \ -X POST "$BASE_URL/api/v1/auth/local/change-password" \ -b "$COOKIE_JAR" \ -H "X-XSRF-TOKEN: $CSRF_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"currentPassword\":\"$PASSWORD\",\"newPassword\":\"$NEW_PASSWORD\"}" || true)" if [[ "$CHANGE_PASSWORD_STATUS" == "200" ]]; then echo "PASS: Change password (HTTP $CHANGE_PASSWORD_STATUS)" PASS=$((PASS + 1)) else echo "FAIL: Change password (got $CHANGE_PASSWORD_STATUS)" FAIL=$((FAIL + 1)) fi LOGOUT_STATUS="$(curl --max-time 10 -s -o /dev/null -w "%{http_code}" \ -X POST "$BASE_URL/api/v1/auth/logout" \ -b "$COOKIE_JAR" \ -c "$COOKIE_JAR" \ -H "X-XSRF-TOKEN: $CSRF_TOKEN" || true)" if [[ "$LOGOUT_STATUS" == "302" || "$LOGOUT_STATUS" == "200" || "$LOGOUT_STATUS" == "204" ]]; then echo "PASS: Logout (HTTP $LOGOUT_STATUS)" PASS=$((PASS + 1)) else echo "FAIL: Logout (got $LOGOUT_STATUS)" FAIL=$((FAIL + 1)) fi POST_LOGOUT_STATUS="$(curl --max-time 10 -s -o /dev/null -w "%{http_code}" -b "$COOKIE_JAR" "$BASE_URL/api/v1/auth/me" || true)" if [[ "$POST_LOGOUT_STATUS" == "401" ]]; then echo "PASS: Auth me after logout (HTTP $POST_LOGOUT_STATUS)" PASS=$((PASS + 1)) else echo "FAIL: Auth me after logout (got $POST_LOGOUT_STATUS)" FAIL=$((FAIL + 1)) fi # ---- Label Management (requires admin) ---- ADMIN_USERNAME="${BOOTSTRAP_ADMIN_USERNAME:-admin}" ADMIN_PASSWORD="${BOOTSTRAP_ADMIN_PASSWORD:-ChangeMe!2026}" ADMIN_COOKIE_JAR="$(mktemp)" LABEL_SLUG="smoke-label-$(date +%s)" cleanup_admin() { rm -f "$ADMIN_COOKIE_JAR" } trap 'cleanup; cleanup_admin' EXIT # Get CSRF token for admin session curl -s -c "$ADMIN_COOKIE_JAR" "$BASE_URL/api/v1/auth/me" >/dev/null ADMIN_CSRF="$(awk '$6 == "XSRF-TOKEN" { print $7 }' "$ADMIN_COOKIE_JAR" | tail -n 1)" # Login as admin ADMIN_LOGIN_STATUS="$(curl --max-time 10 -s -o /dev/null -w "%{http_code}" \ -X POST "$BASE_URL/api/v1/auth/local/login" \ -b "$ADMIN_COOKIE_JAR" \ -c "$ADMIN_COOKIE_JAR" \ -H "X-XSRF-TOKEN: $ADMIN_CSRF" \ -H "Content-Type: application/json" \ -d "{\"username\":\"$ADMIN_USERNAME\",\"password\":\"$ADMIN_PASSWORD\"}" || true)" if [[ "$ADMIN_LOGIN_STATUS" == "200" ]]; then echo "PASS: Admin login (HTTP $ADMIN_LOGIN_STATUS)" PASS=$((PASS + 1)) else echo "FAIL: Admin login (got $ADMIN_LOGIN_STATUS)" FAIL=$((FAIL + 1)) fi # Refresh CSRF after login ADMIN_CSRF="$(awk '$6 == "XSRF-TOKEN" { print $7 }' "$ADMIN_COOKIE_JAR" | tail -n 1)" UNKNOWN_RECOVERY_STATUS="$(curl --max-time 10 -s -o /dev/null -w "%{http_code}" \ -X POST "$BASE_URL/api/v1/admin/identity-providers/__missing_provider__/authority/recover" \ -b "$ADMIN_COOKIE_JAR" \ -H "X-XSRF-TOKEN: $ADMIN_CSRF" || true)" if [[ "$UNKNOWN_RECOVERY_STATUS" == "404" ]]; then echo "PASS: Provider authority recovery rejects unknown provider without mutation (HTTP $UNKNOWN_RECOVERY_STATUS)" PASS=$((PASS + 1)) else echo "FAIL: Provider authority recovery unknown-provider guard (expected 404, got $UNKNOWN_RECOVERY_STATUS)" FAIL=$((FAIL + 1)) fi # Exercise the administrator activation workflow over HTTP. The transactional # integration test covers the missing-membership precondition; this smoke path # verifies the deployed controller, security, persistence, and read model. if [[ -n "$REGISTERED_USER_ID" && "$ADMIN_LOGIN_STATUS" == "200" ]]; then DISABLE_USER_STATUS="$(curl --max-time 10 -s -o /dev/null -w "%{http_code}" \ -X POST "$BASE_URL/api/v1/admin/users/$REGISTERED_USER_ID/disable" \ -b "$ADMIN_COOKIE_JAR" \ -H "X-XSRF-TOKEN: $ADMIN_CSRF" || true)" if [[ "$DISABLE_USER_STATUS" == "200" ]]; then echo "PASS: Admin disables smoke user (HTTP $DISABLE_USER_STATUS)" PASS=$((PASS + 1)) else echo "FAIL: Admin disables smoke user (got $DISABLE_USER_STATUS)" FAIL=$((FAIL + 1)) fi APPROVE_USER_STATUS="$(curl --max-time 10 -s -o /dev/null -w "%{http_code}" \ -X POST "$BASE_URL/api/v1/admin/users/$REGISTERED_USER_ID/approve" \ -b "$ADMIN_COOKIE_JAR" \ -H "X-XSRF-TOKEN: $ADMIN_CSRF" || true)" if [[ "$APPROVE_USER_STATUS" == "200" ]]; then echo "PASS: Admin activates smoke user (HTTP $APPROVE_USER_STATUS)" PASS=$((PASS + 1)) else echo "FAIL: Admin activates smoke user (got $APPROVE_USER_STATUS)" FAIL=$((FAIL + 1)) fi GLOBAL_MEMBERS_RESPONSE="$(curl --max-time 10 -s \ -b "$ADMIN_COOKIE_JAR" \ "$BASE_URL/api/web/namespaces/global/members?size=1000" || true)" if JSON_INPUT="$GLOBAL_MEMBERS_RESPONSE" python3 - "$REGISTERED_USER_ID" <<'PY' import json import os import sys user_id = sys.argv[1] try: items = json.loads(os.environ["JSON_INPUT"])["data"]["items"] except (KeyError, TypeError, json.JSONDecodeError): raise SystemExit(1) raise SystemExit(0 if any( item.get("userId") == user_id and item.get("role") == "MEMBER" for item in items ) else 1) PY then echo "PASS: Activated user has @global MEMBER membership" PASS=$((PASS + 1)) else echo "FAIL: Activated user is missing @global MEMBER membership" FAIL=$((FAIL + 1)) fi else echo "FAIL: Cannot exercise admin activation workflow without registered user and admin session" FAIL=$((FAIL + 1)) fi # Create label definition CREATE_LABEL_STATUS="$(curl --max-time 10 -s -o /dev/null -w "%{http_code}" \ -X POST "$BASE_URL/api/v1/admin/labels" \ -b "$ADMIN_COOKIE_JAR" \ -H "X-XSRF-TOKEN: $ADMIN_CSRF" \ -H "Content-Type: application/json" \ -d "{\"slug\":\"$LABEL_SLUG\",\"type\":\"RECOMMENDED\",\"visibleInFilter\":true,\"sortOrder\":99,\"translations\":[{\"locale\":\"en\",\"displayName\":\"Smoke Label\"}]}" || true)" if [[ "$CREATE_LABEL_STATUS" == "200" ]]; then echo "PASS: Create label definition (HTTP $CREATE_LABEL_STATUS)" PASS=$((PASS + 1)) else echo "FAIL: Create label definition (got $CREATE_LABEL_STATUS)" FAIL=$((FAIL + 1)) fi # List admin label definitions LIST_LABELS_STATUS="$(curl --max-time 10 -s -o /dev/null -w "%{http_code}" \ -b "$ADMIN_COOKIE_JAR" "$BASE_URL/api/v1/admin/labels" || true)" if [[ "$LIST_LABELS_STATUS" == "200" ]]; then echo "PASS: List admin label definitions (HTTP $LIST_LABELS_STATUS)" PASS=$((PASS + 1)) else echo "FAIL: List admin label definitions (got $LIST_LABELS_STATUS)" FAIL=$((FAIL + 1)) fi # List visible labels (public) VISIBLE_LABELS_STATUS="$(curl --max-time 10 -s -o /dev/null -w "%{http_code}" \ "$BASE_URL/api/v1/labels" || true)" if [[ "$VISIBLE_LABELS_STATUS" == "200" ]]; then echo "PASS: List visible labels (HTTP $VISIBLE_LABELS_STATUS)" PASS=$((PASS + 1)) else echo "FAIL: List visible labels (got $VISIBLE_LABELS_STATUS)" FAIL=$((FAIL + 1)) fi # Delete label definition (cleanup) DELETE_LABEL_STATUS="$(curl --max-time 10 -s -o /dev/null -w "%{http_code}" \ -X DELETE "$BASE_URL/api/v1/admin/labels/$LABEL_SLUG" \ -b "$ADMIN_COOKIE_JAR" \ -H "X-XSRF-TOKEN: $ADMIN_CSRF" || true)" if [[ "$DELETE_LABEL_STATUS" == "200" || "$DELETE_LABEL_STATUS" == "204" ]]; then echo "PASS: Delete label definition (HTTP $DELETE_LABEL_STATUS)" PASS=$((PASS + 1)) else echo "FAIL: Delete label definition (got $DELETE_LABEL_STATUS)" FAIL=$((FAIL + 1)) fi echo echo "Results: $PASS passed, $FAIL failed" if [[ "$FAIL" -ne 0 ]]; then exit 1 fi