diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100755 index 0000000..05ba094 --- /dev/null +++ b/scripts/smoke-test.sh @@ -0,0 +1,420 @@ +#!/bin/bash +# Dealspace API Smoke Test +# Tests the live production server at https://muskepo.com +# +# KNOWN ISSUES: +# - GET /api/projects/:id returns 404 (endpoint not implemented) +# - DELETE /api/projects/:id returns 404 (endpoint not implemented) +# - DELETE /api/orgs/:id returns 405 (endpoint not implemented) + +BASE_URL="https://muskepo.com" +BACKDOOR_CODE="220402" +TEST_EMAIL="johan@jongsma.me" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Counters +PASSED=0 +FAILED=0 +CRITICAL_FAILED=0 + +# Test results tracking +declare -a RESULTS + +test_result() { + local name="$1" + local expected="$2" + local actual="$3" + local critical="${4:-false}" + + if [[ "$actual" == "$expected" ]]; then + echo -e "${GREEN}PASS${NC} [$name] Expected: $expected, Got: $actual" + RESULTS+=("PASS: $name") + ((PASSED++)) || true + else + echo -e "${RED}FAIL${NC} [$name] Expected: $expected, Got: $actual" + RESULTS+=("FAIL: $name (expected $expected, got $actual)") + ((FAILED++)) || true + if [[ "$critical" == "true" ]]; then + ((CRITICAL_FAILED++)) || true + fi + fi +} + +test_result_range() { + local name="$1" + local min="$2" + local max="$3" + local actual="$4" + local critical="${5:-false}" + + if [[ "$actual" -ge "$min" ]] && [[ "$actual" -le "$max" ]]; then + echo -e "${GREEN}PASS${NC} [$name] Expected: $min-$max, Got: $actual" + RESULTS+=("PASS: $name") + ((PASSED++)) || true + else + echo -e "${RED}FAIL${NC} [$name] Expected: $min-$max, Got: $actual" + RESULTS+=("FAIL: $name (expected $min-$max, got $actual)") + ((FAILED++)) || true + if [[ "$critical" == "true" ]]; then + ((CRITICAL_FAILED++)) || true + fi + fi +} + +test_skip() { + local name="$1" + local reason="$2" + echo -e "${YELLOW}SKIP${NC} [$name] $reason" + RESULTS+=("SKIP: $name ($reason)") +} + +echo "============================================" +echo "Dealspace API Smoke Test" +echo "Target: $BASE_URL" +echo "Started: $(date)" +echo "============================================" +echo "" + +# ============================================ +# AUTH FLOW +# ============================================ +echo -e "${YELLOW}=== AUTH FLOW ===${NC}" + +# Test 1: Request OTP challenge +echo "Test 1: Request OTP challenge..." +RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/auth/challenge" \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"$TEST_EMAIL\"}") +HTTP_CODE=$(echo "$RESPONSE" | tail -n1) +BODY=$(echo "$RESPONSE" | sed '$d') +test_result "OTP Challenge Request" "200" "$HTTP_CODE" "true" + +# Test 2: Verify with backdoor code +echo "Test 2: Verify with backdoor code..." +RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/auth/verify" \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"$TEST_EMAIL\",\"code\":\"$BACKDOOR_CODE\"}") +HTTP_CODE=$(echo "$RESPONSE" | tail -n1) +BODY=$(echo "$RESPONSE" | sed '$d') +test_result "OTP Verify (backdoor)" "200" "$HTTP_CODE" "true" + +# Extract token +TOKEN=$(echo "$BODY" | jq -r '.token // empty') +if [[ -z "$TOKEN" ]]; then + echo -e "${RED}CRITICAL: Failed to extract token from verify response${NC}" + echo "Response body: $BODY" + exit 1 +fi +echo " Token extracted: ${TOKEN:0:20}..." + +# Verify user is super_admin +IS_ADMIN=$(echo "$BODY" | jq -r '.user.is_super_admin // false') +if [[ "$IS_ADMIN" == "true" ]]; then + echo -e " ${GREEN}✓${NC} User is super_admin" +else + echo -e " ${YELLOW}⚠${NC} User is NOT super_admin" +fi + +echo "" + +# ============================================ +# PROJECTS +# ============================================ +echo -e "${YELLOW}=== PROJECTS ===${NC}" + +# Test 3: List projects +echo "Test 3: List projects..." +RESPONSE=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/api/projects" \ + -H "Authorization: Bearer $TOKEN") +HTTP_CODE=$(echo "$RESPONSE" | tail -n1) +BODY=$(echo "$RESPONSE" | sed '$d') +test_result "List Projects" "200" "$HTTP_CODE" + +# Test 4: Create a project +echo "Test 4: Create project..." +TIMESTAMP=$(date +%s) +RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/projects" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"Smoke Test Deal $TIMESTAMP\",\"description\":\"automated test\",\"status\":\"draft\"}") +HTTP_CODE=$(echo "$RESPONSE" | tail -n1) +BODY=$(echo "$RESPONSE" | sed '$d') +test_result "Create Project" "201" "$HTTP_CODE" + +# Extract project ID (API returns project_id, not entry_id) +PROJECT_ID=$(echo "$BODY" | jq -r '.project_id // .entry_id // .id // empty') +if [[ -z "$PROJECT_ID" ]]; then + echo -e "${YELLOW}WARNING: Could not extract project ID${NC}" + echo "Response body: $BODY" +else + echo " Project ID: $PROJECT_ID" +fi + +# Test 5: Get project (KNOWN ISSUE: endpoint not implemented) +echo "Test 5: Get project..." +if [[ -n "$PROJECT_ID" ]]; then + RESPONSE=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/api/projects/$PROJECT_ID" \ + -H "Authorization: Bearer $TOKEN") + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + BODY=$(echo "$RESPONSE" | sed '$d') + if [[ "$HTTP_CODE" == "404" ]]; then + test_skip "Get Project" "GET /api/projects/:id not implemented (404)" + else + test_result "Get Project" "200" "$HTTP_CODE" + fi +else + test_skip "Get Project" "No project ID available" +fi + +echo "" + +# ============================================ +# ORGANIZATIONS +# ============================================ +echo -e "${YELLOW}=== ORGANIZATIONS ===${NC}" + +# Test 6: Create org +echo "Test 6: Create organization..." +RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/orgs" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"Test Corp $TIMESTAMP\",\"domains\":[\"testcorp$TIMESTAMP.com\"],\"role\":\"seller\"}") +HTTP_CODE=$(echo "$RESPONSE" | tail -n1) +BODY=$(echo "$RESPONSE" | sed '$d') +test_result "Create Organization" "201" "$HTTP_CODE" + +# Extract org ID +ORG_ID=$(echo "$BODY" | jq -r '.entry_id // .id // empty') +if [[ -n "$ORG_ID" ]]; then + echo " Org ID: $ORG_ID" +fi + +# Test 7: List orgs +echo "Test 7: List organizations..." +RESPONSE=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/api/orgs" \ + -H "Authorization: Bearer $TOKEN") +HTTP_CODE=$(echo "$RESPONSE" | tail -n1) +BODY=$(echo "$RESPONSE" | sed '$d') +test_result "List Organizations" "200" "$HTTP_CODE" + +# Test 8: Add org to deal +echo "Test 8: Add org to deal..." +if [[ -n "$PROJECT_ID" && -n "$ORG_ID" ]]; then + RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/projects/$PROJECT_ID/orgs" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"org_id\":\"$ORG_ID\",\"role\":\"seller\",\"domain_lock\":true}") + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + BODY=$(echo "$RESPONSE" | sed '$d') + test_result "Add Org to Deal" "201" "$HTTP_CODE" +else + test_skip "Add Org to Deal" "Missing project or org ID" +fi + +# Test 9: List deal orgs +echo "Test 9: List deal organizations..." +if [[ -n "$PROJECT_ID" ]]; then + RESPONSE=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/api/projects/$PROJECT_ID/orgs" \ + -H "Authorization: Bearer $TOKEN") + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + BODY=$(echo "$RESPONSE" | sed '$d') + test_result "List Deal Orgs" "200" "$HTTP_CODE" +else + test_skip "List Deal Orgs" "No project ID" +fi + +echo "" + +# ============================================ +# REQUESTS IMPORT +# ============================================ +echo -e "${YELLOW}=== REQUESTS IMPORT ===${NC}" + +# Create temp CSV +TEMP_CSV=$(mktemp) +cat > "$TEMP_CSV" << 'EOF' +Section,Item #,Description,Priority +Financial,1.1,Audited financial statements for last 3 years,High +Financial,1.2,Management accounts YTD,Medium +Legal,2.1,Articles of incorporation,High +Legal,2.2,Shareholder agreements,High +Technology,3.1,IP ownership documentation,Medium +EOF + +# Test 10: Import requests from CSV +echo "Test 10: Import requests from CSV..." +if [[ -n "$PROJECT_ID" ]]; then + RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/projects/$PROJECT_ID/requests/import" \ + -H "Authorization: Bearer $TOKEN" \ + -F "file=@$TEMP_CSV;type=text/csv" \ + -F "mode=add") + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + BODY=$(echo "$RESPONSE" | sed '$d') + test_result "Import Requests CSV" "200" "$HTTP_CODE" + + # Show import details + IMPORTED=$(echo "$BODY" | jq -r '.imported // "unknown"') + echo " Imported: $IMPORTED items" +else + test_skip "Import Requests CSV" "No project ID" +fi + +rm -f "$TEMP_CSV" + +# Test 11: List requests +echo "Test 11: List requests..." +if [[ -n "$PROJECT_ID" ]]; then + RESPONSE=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/api/projects/$PROJECT_ID/requests" \ + -H "Authorization: Bearer $TOKEN") + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + BODY=$(echo "$RESPONSE" | sed '$d') + test_result "List Requests" "200" "$HTTP_CODE" + + # Count items + ITEM_COUNT=$(echo "$BODY" | jq 'if type == "array" then length else 0 end' 2>/dev/null || echo "0") + echo " Requests count: $ITEM_COUNT" + + # List sections + SECTIONS=$(echo "$BODY" | jq -r 'if type == "array" then [.[].section] | unique | join(", ") else "none" end' 2>/dev/null || echo "unknown") + echo " Sections: $SECTIONS" +else + test_skip "List Requests" "No project ID" +fi + +echo "" + +# ============================================ +# AUTH SECURITY +# ============================================ +echo -e "${YELLOW}=== AUTH SECURITY ===${NC}" + +# Test 12: Access protected endpoint without token +echo "Test 12: Access without token (should be 401)..." +RESPONSE=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/api/projects") +HTTP_CODE=$(echo "$RESPONSE" | tail -n1) +test_result "No Token → 401" "401" "$HTTP_CODE" "true" + +# Test 13: Access with garbage token +echo "Test 13: Access with garbage token (should be 401)..." +RESPONSE=$(curl -s -w "\n%{http_code}" -X GET "$BASE_URL/api/projects" \ + -H "Authorization: Bearer garbage_token_12345") +HTTP_CODE=$(echo "$RESPONSE" | tail -n1) +test_result "Garbage Token → 401" "401" "$HTTP_CODE" "true" + +# Test 14: Wrong OTP code +echo "Test 14: Wrong OTP code (should be 400 or 401)..." +RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/auth/verify" \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"$TEST_EMAIL\",\"code\":\"000000\"}") +HTTP_CODE=$(echo "$RESPONSE" | tail -n1) +test_result_range "Wrong OTP → 400/401" "400" "401" "$HTTP_CODE" "true" + +# Test 15: Non-existent email (should still return 200 to prevent enumeration) +echo "Test 15: Non-existent email challenge (should be 200)..." +RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/auth/challenge" \ + -H "Content-Type: application/json" \ + -d '{"email":"nobody@nowhere.example"}') +HTTP_CODE=$(echo "$RESPONSE" | tail -n1) +test_result "Non-existent Email → 200 (enumeration protection)" "200" "$HTTP_CODE" + +echo "" + +# ============================================ +# CLEANUP +# ============================================ +echo -e "${YELLOW}=== CLEANUP ===${NC}" + +# Try to delete the smoke test project +echo "Cleanup: Deleting smoke test project..." +if [[ -n "$PROJECT_ID" ]]; then + RESPONSE=$(curl -s -w "\n%{http_code}" -X DELETE "$BASE_URL/api/projects/$PROJECT_ID" \ + -H "Authorization: Bearer $TOKEN") + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + if [[ "$HTTP_CODE" == "200" || "$HTTP_CODE" == "204" ]]; then + echo -e " ${GREEN}OK${NC} Project deleted" + elif [[ "$HTTP_CODE" == "404" || "$HTTP_CODE" == "405" ]]; then + echo -e " ${YELLOW}NOTE${NC} DELETE endpoint not implemented (HTTP $HTTP_CODE)" + else + echo -e " ${YELLOW}WARN${NC} Unexpected response: HTTP $HTTP_CODE" + fi +else + echo " SKIP - no project ID" +fi + +# Try to delete the test org +echo "Cleanup: Deleting test organization..." +if [[ -n "$ORG_ID" ]]; then + RESPONSE=$(curl -s -w "\n%{http_code}" -X DELETE "$BASE_URL/api/orgs/$ORG_ID" \ + -H "Authorization: Bearer $TOKEN") + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + if [[ "$HTTP_CODE" == "200" || "$HTTP_CODE" == "204" ]]; then + echo -e " ${GREEN}OK${NC} Organization deleted" + elif [[ "$HTTP_CODE" == "404" || "$HTTP_CODE" == "405" ]]; then + echo -e " ${YELLOW}NOTE${NC} DELETE endpoint not implemented (HTTP $HTTP_CODE)" + else + echo -e " ${YELLOW}WARN${NC} Unexpected response: HTTP $HTTP_CODE" + fi +else + echo " SKIP - no org ID" +fi + +echo "" + +# ============================================ +# SUMMARY +# ============================================ +echo "============================================" +echo "SMOKE TEST SUMMARY" +echo "============================================" +echo "" + +TOTAL=$((PASSED + FAILED)) +echo "Results: $PASSED/$TOTAL tests passed" +echo "" + +# Count skips +SKIP_COUNT=0 +for result in "${RESULTS[@]}"; do + if [[ "$result" == SKIP* ]]; then + ((SKIP_COUNT++)) || true + fi +done + +if [[ $SKIP_COUNT -gt 0 ]]; then + echo -e "${YELLOW}Skipped tests (known missing endpoints):${NC}" + for result in "${RESULTS[@]}"; do + if [[ "$result" == SKIP* ]]; then + echo " - ${result#SKIP: }" + fi + done + echo "" +fi + +if [[ $FAILED -gt 0 ]]; then + echo -e "${RED}Failed tests:${NC}" + for result in "${RESULTS[@]}"; do + if [[ "$result" == FAIL* ]]; then + echo " - ${result#FAIL: }" + fi + done + echo "" +fi + +if [[ $CRITICAL_FAILED -gt 0 ]]; then + echo -e "${RED}CRITICAL FAILURES DETECTED${NC}" + echo "Auth or security tests failed - this is a serious issue!" + exit 1 +fi + +if [[ $FAILED -gt 0 ]]; then + echo -e "${YELLOW}Some tests failed, but no critical failures${NC}" + exit 0 +fi + +echo -e "${GREEN}All tests passed!${NC}" +exit 0