#!/usr/bin/env bash # FILE: test/test-api.sh # Chess API Robustness Test Suite # Tests the refactored chess API with security hardening # Requires: curl, jq BASE_URL="http://localhost:8080" API_URL="${BASE_URL}/api/v1" # Configurable delay between API calls (in milliseconds) API_DELAY=${API_DELAY:-50} # 50ms for dev mode testing # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' NC='\033[0m' # Test counters PASS=0 FAIL=0 SKIP=0 # Helper functions print_header() { echo -e "\n${CYAN}═══════════════════════════════════════════════════════════${NC}" echo -e "${CYAN}$1${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" } test_case() { echo -e "\n${YELLOW}▶ TEST: $1${NC}" sleep 0.0$API_DELAY } assert_status() { local expected=$1 local actual=$2 local test_name=$3 if [ "$actual" = "$expected" ]; then echo -e "${GREEN} ✓ $test_name: HTTP $actual${NC}" ((PASS++)) return 0 else echo -e "${RED} ✗ $test_name: Expected HTTP $expected, got $actual${NC}" ((FAIL++)) return 1 fi } assert_json_field() { local json=$1 local field=$2 local expected=$3 local test_name=$4 local actual=$(echo "$json" | jq -r "$field" 2>/dev/null) if [ "$actual" = "$expected" ]; then echo -e "${GREEN} ✓ $test_name: $field = '$actual'${NC}" ((PASS++)) return 0 else echo -e "${RED} ✗ $test_name: Expected $field = '$expected', got '$actual'${NC}" ((FAIL++)) return 1 fi } # Enhanced error display show_error() { local response=$1 local error=$(echo "$response" | jq -r '.error // "No error message"' 2>/dev/null) local code=$(echo "$response" | jq -r '.code // ""' 2>/dev/null) local details=$(echo "$response" | jq -r '.details // ""' 2>/dev/null) if [ "$error" != "No error message" ]; then echo -e "${RED} Error: $error${NC}" [ -n "$code" ] && [ "$code" != "null" ] && echo -e "${RED} Code: $code${NC}" [ -n "$details" ] && [ "$details" != "null" ] && echo -e "${RED} Details: $details${NC}" fi } api_request() { local method=$1 local url=$2 shift 2 curl -s "$@" -X "$method" "$url" local status=$? sleep 0.0$API_DELAY return $status } wait_for_state() { local game_id=$1 local target_state=$2 local max_attempts=${3:-20} local attempt=0 echo -e "${BLUE} ⏳ Waiting for state: $target_state${NC}" while [ $attempt -lt $max_attempts ]; do local response=$(api_request GET "$API_URL/games/$game_id") local current_state=$(echo "$response" | jq -r '.state' 2>/dev/null) if [ "$current_state" = "$target_state" ] || [[ "$target_state" = "!pending" && "$current_state" != "pending" ]]; then echo -e "${GREEN} ✓ State reached: $current_state${NC}" return 0 fi ((attempt++)) sleep 0.1 done echo -e "${RED} ✗ Timeout waiting for state: $target_state${NC}" return 1 } # Check if jq is installed if ! command -v jq &> /dev/null; then echo -e "${RED}Error: jq is required but not installed${NC}" echo "Install with: sudo pacman -S jq (Arch) or appropriate package manager" exit 1 fi # Start tests print_header "Chess API Robustness Test Suite" echo "Server: $BASE_URL" echo "API Version: v1" echo -e "${MAGENTA} IMPORTANT: Server must be started with -dev flag for tests to pass!${NC}" echo -e "${MAGENTA} Start the server first: test/run-test-server.sh${NC}" echo -e "${MAGENTA} Or directly after build: ./chessd -dev${NC}" echo "" echo "Starting comprehensive tests..." # ============================================================================== print_header "SECTION 1: Basic Functionality (Regression Tests)" # ============================================================================== test_case "1.1: Health Check" RESPONSE=$(api_request GET "$BASE_URL/health") STATUS=$(api_request GET "$BASE_URL/health" -o /dev/null -w "%{http_code}") assert_status 200 "$STATUS" "Health endpoint" assert_json_field "$RESPONSE" '.status' "healthy" "Health status" test_case "1.2: Create Human vs Human Game" RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 1}, "black": {"type": 1}}') HVH_ID=$(echo "$RESPONSE" | jq -r '.gameId' 2>/dev/null) if [ "$HVH_ID" = "null" ] || [ -z "$HVH_ID" ]; then echo -e "${RED} ✗ Failed to create game${NC}" show_error "$RESPONSE" ((FAIL++)) else echo -e "${GREEN} ✓ Create HvH game: HTTP 201${NC}" echo " Game ID: $HVH_ID" ((PASS++)) fi if [ "$HVH_ID" != "null" ] && [ -n "$HVH_ID" ]; then test_case "1.3: Make Valid Human Move" STATUS=$(api_request POST "$API_URL/games/$HVH_ID/moves" \ -o /dev/null -w "%{http_code}" \ -H "Content-Type: application/json" \ -d '{"move": "e2e4"}') assert_status 200 "$STATUS" "Valid move e2e4" test_case "1.4: Make Invalid Human Move" STATUS=$(api_request POST "$API_URL/games/$HVH_ID/moves" \ -o /dev/null -w "%{http_code}" \ -H "Content-Type: application/json" \ -d '{"move": "e2e5"}') assert_status 400 "$STATUS" "Invalid move e2e5 rejected" test_case "1.5: Get ASCII Board" RESPONSE=$(api_request GET "$API_URL/games/$HVH_ID/board") STATUS=$(api_request GET "$API_URL/games/$HVH_ID/board" -o /dev/null -w "%{http_code}") assert_status 200 "$STATUS" "Get board" BOARD=$(echo "$RESPONSE" | jq -r '.board' 2>/dev/null | head -3) if [ -n "$BOARD" ] && [ "$BOARD" != "null" ]; then echo -e "${GREEN} ✓ Board visualization returned${NC}" ((PASS++)) else echo -e "${RED} ✗ Board visualization empty${NC}" ((FAIL++)) fi test_case "1.6: Delete Game" STATUS=$(api_request DELETE "$API_URL/games/$HVH_ID" -o /dev/null -w "%{http_code}") assert_status 204 "$STATUS" "Delete game" else echo -e "${YELLOW} ⊘ Skipping tests 1.3-1.6 due to game creation failure${NC}" SKIP=$((SKIP + 4)) fi # ============================================================================== print_header "SECTION 2: New Computer Move Triggering Logic" # ============================================================================== test_case "2.1: Create Human vs Computer Game" RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 1}, "black": {"type": 2, "searchTime": 100}}') HVC_ID=$(echo "$RESPONSE" | jq -r '.gameId' 2>/dev/null) if [ "$HVC_ID" = "null" ] || [ -z "$HVC_ID" ]; then echo -e "${RED} ✗ Failed to create HvC game${NC}" show_error "$RESPONSE" ((FAIL++)) else assert_json_field "$RESPONSE" '.turn' "w" "White (human) starts" echo " Game ID: $HVC_ID" fi if [ "$HVC_ID" != "null" ] && [ -n "$HVC_ID" ]; then test_case "2.2: Human Makes First Move" RESPONSE=$(api_request POST "$API_URL/games/$HVC_ID/moves" \ -H "Content-Type: application/json" \ -d '{"move": "d2d4"}') assert_json_field "$RESPONSE" '.turn' "b" "Turn switches to black" assert_json_field "$RESPONSE" '.lastMove.move' "d2d4" "Move recorded" test_case "2.3: Trigger Computer Move with Empty Request" RESPONSE=$(api_request POST "$API_URL/games/$HVC_ID/moves" \ -H "Content-Type: application/json" \ -d '{"move": "cccc"}') STATUS=$(echo "$RESPONSE" | jq -r '.gameId' &>/dev/null && echo 200 || echo 400) assert_status 200 "200" "Empty move triggers computer" PENDING_STATE=$(echo "$RESPONSE" | jq -r '.state' 2>/dev/null) if [ "$PENDING_STATE" = "pending" ]; then echo -e "${GREEN} ✓ Game entered pending state${NC}" ((PASS++)) else echo -e "${RED} ✗ Game should be in pending state, got: $PENDING_STATE${NC}" ((FAIL++)) fi test_case "2.4: Wait for Computer Move Completion" wait_for_state "$HVC_ID" "!pending" RESPONSE=$(api_request GET "$API_URL/games/$HVC_ID") COMPUTER_MOVE=$(echo "$RESPONSE" | jq -r '.lastMove.move' 2>/dev/null) if [ -n "$COMPUTER_MOVE" ] && [ "$COMPUTER_MOVE" != "null" ] && [ "$COMPUTER_MOVE" != "d2d4" ]; then echo -e "${GREEN} ✓ Computer made move: $COMPUTER_MOVE${NC}" ((PASS++)) else echo -e "${RED} ✗ Computer move not detected${NC}" ((FAIL++)) fi test_case "2.5: Verify Empty Move During Human Turn Fails" RESPONSE=$(api_request POST "$API_URL/games/$HVC_ID/moves" \ -H "Content-Type: application/json" \ -d '{"move": "cccc"}') STATUS=$(echo "$RESPONSE" | jq -r '.error' &>/dev/null && echo 400 || echo 200) assert_status 400 "400" "Empty move rejected during human turn" # Clean up api_request DELETE "$API_URL/games/$HVC_ID" > /dev/null else echo -e "${YELLOW} ⊘ Skipping tests 2.2-2.5 due to game creation failure${NC}" SKIP=$((SKIP + 4)) fi # ============================================================================== print_header "SECTION 3: Pending State Race Condition Protection" # ============================================================================== test_case "3.1: Setup Game for Pending State Test" RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 1}, "black": {"type": 2, "searchTime": 500}}') PENDING_ID=$(echo "$RESPONSE" | jq -r '.gameId' 2>/dev/null) if [ "$PENDING_ID" != "null" ] && [ -n "$PENDING_ID" ]; then echo " Game ID: $PENDING_ID" ((PASS++)) # Make human move api_request POST "$API_URL/games/$PENDING_ID/moves" \ -H "Content-Type: application/json" \ -d '{"move": "e2e3"}' > /dev/null test_case "3.2: Trigger Computer Move and Immediately Try Undo" # Trigger computer move RESPONSE=$(api_request POST "$API_URL/games/$PENDING_ID/moves" \ -H "Content-Type: application/json" \ -d '{"move": "cccc"}') assert_json_field "$RESPONSE" '.state' "pending" "Computer move triggered" # Immediately try undo (should fail) STATUS=$(api_request POST "$API_URL/games/$PENDING_ID/undo" \ -o /dev/null -w "%{http_code}" \ -H "Content-Type: application/json" \ -d '{"count": 1}') assert_status 400 "$STATUS" "Undo blocked during pending state" test_case "3.3: Verify Move Attempts Blocked During Pending" STATUS=$(api_request POST "$API_URL/games/$PENDING_ID/moves" \ -o /dev/null -w "%{http_code}" \ -H "Content-Type: application/json" \ -d '{"move": "a2a3"}') assert_status 400 "$STATUS" "Move blocked during pending state" test_case "3.4: Verify GET Still Works During Pending" STATUS=$(api_request GET "$API_URL/games/$PENDING_ID" -o /dev/null -w "%{http_code}") assert_status 200 "$STATUS" "GET allowed during pending state" # Wait and clean up wait_for_state "$PENDING_ID" "!pending" api_request DELETE "$API_URL/games/$PENDING_ID" > /dev/null else echo -e "${RED} ✗ Failed to create game for pending state test${NC}" show_error "$RESPONSE" ((FAIL++)) SKIP=$((SKIP + 3)) fi # ============================================================================== print_header "SECTION 4: Computer vs Computer Flow" # ============================================================================== test_case "4.1: Create Computer vs Computer Game" RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 2, "searchTime": 100}, "black": {"type": 2, "searchTime": 100}}') CVC_ID=$(echo "$RESPONSE" | jq -r '.gameId' 2>/dev/null) if [ "$CVC_ID" != "null" ] && [ -n "$CVC_ID" ]; then assert_json_field "$RESPONSE" '.players.white.type' "2" "White is computer" assert_json_field "$RESPONSE" '.players.black.type' "2" "Black is computer" echo " Game ID: $CVC_ID" test_case "4.2: Play First 4 Moves in CvC Game" MOVE_COUNT=0 for i in {1..4}; do PLAYER=$([[ $((i % 2)) -eq 1 ]] && echo "White" || echo "Black") echo -e "${BLUE} Move $i: Triggering $PLAYER computer move${NC}" # Trigger computer move RESPONSE=$(api_request POST "$API_URL/games/$CVC_ID/moves" \ -H "Content-Type: application/json" \ -d '{"move": "cccc"}') if echo "$RESPONSE" | jq -r '.state' 2>/dev/null | grep -q "pending"; then echo -e "${GREEN} ✓ Move triggered, entering pending state${NC}" # Wait for completion wait_for_state "$CVC_ID" "!pending" 30 # Verify move was made RESPONSE=$(api_request GET "$API_URL/games/$CVC_ID") MOVES=$(echo "$RESPONSE" | jq -r '.moves | length' 2>/dev/null) if [ "$MOVES" -eq "$i" ]; then echo -e "${GREEN} ✓ Move $i completed successfully${NC}" ((PASS++)) ((MOVE_COUNT++)) else echo -e "${RED} ✗ Expected $i moves, found $MOVES${NC}" ((FAIL++)) fi else echo -e "${RED} ✗ Failed to trigger computer move${NC}" ((FAIL++)) break fi done if [ $MOVE_COUNT -eq 4 ]; then echo -e "${GREEN} ✓ Successfully played 4 CvC moves${NC}" ((PASS++)) fi # Clean up api_request DELETE "$API_URL/games/$CVC_ID" > /dev/null else echo -e "${RED} ✗ Failed to create CvC game${NC}" show_error "$RESPONSE" ((FAIL++)) SKIP=$((SKIP + 5)) fi # ============================================================================== print_header "SECTION 5: Error Handling" # ============================================================================== test_case "5.1: Non-existent Game ID" STATUS=$(api_request GET "$API_URL/games/11111111-1111-1111-1111-111111111111" -o /dev/null -w "%{http_code}") assert_status 404 "$STATUS" "Non-existent game returns 404" test_case "5.2: Invalid UUID Format" STATUS=$(api_request GET "$API_URL/games/not-a-uuid" -o /dev/null -w "%{http_code}") assert_status 400 "$STATUS" "Invalid UUID rejected" test_case "5.3: Invalid JSON Body" STATUS=$(api_request POST "$API_URL/games" \ -o /dev/null -w "%{http_code}" \ -H "Content-Type: application/json" \ -d 'not valid json{') assert_status 400 "$STATUS" "Invalid JSON rejected" test_case "5.4: Wrong Content-Type Header" STATUS=$(api_request POST "$API_URL/games" \ -o /dev/null -w "%{http_code}" \ -H "Content-Type: text/plain" \ -d '{"white": {"type": 1}, "black": {"type": 2, "searchTime": 100}}') assert_status 415 "$STATUS" "Wrong Content-Type rejected" test_case "5.5: Move When Not Player's Turn (HvH)" # Create HvH game RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 1}, "black": {"type": 2}}') TURN_ID=$(echo "$RESPONSE" | jq -r '.gameId' 2>/dev/null) if [ "$TURN_ID" != "null" ] && [ -n "$TURN_ID" ]; then # White moves api_request POST "$API_URL/games/$TURN_ID/moves" \ -H "Content-Type: application/json" \ -d '{"move": "e2e4"}' > /dev/null # Try another white move (should fail) STATUS=$(api_request POST "$API_URL/games/$TURN_ID/moves" \ -o /dev/null -w "%{http_code}" \ -H "Content-Type: application/json" \ -d '{"move": "d2d4"}') assert_status 400 "$STATUS" "Move rejected when not turn" # Clean up api_request DELETE "$API_URL/games/$TURN_ID" > /dev/null else echo -e "${YELLOW} ⊘ Skipping turn validation test${NC}" ((SKIP++)) fi test_case "5.6: Move After Game Over" # Create endgame position RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 1}, "black": {"type": 1}, "fen": "7k/5Q2/5K2/8/8/8/8/8 w - - 0 1"}') ENDGAME_ID=$(echo "$RESPONSE" | jq -r '.gameId' 2>/dev/null) if [ "$ENDGAME_ID" != "null" ] && [ -n "$ENDGAME_ID" ]; then # Make checkmate move api_request POST "$API_URL/games/$ENDGAME_ID/moves" \ -H "Content-Type: application/json" \ -d '{"move": "f7g7"}' > /dev/null # Try another move RESPONSE=$(api_request POST "$API_URL/games/$ENDGAME_ID/moves" \ -H "Content-Type: application/json" \ -d '{"move": "h8h7"}') ERROR_CODE=$(echo "$RESPONSE" | jq -r '.code' 2>/dev/null) if [ "$ERROR_CODE" = "GAME_OVER" ]; then echo -e "${GREEN} ✓ Move after game over properly rejected${NC}" ((PASS++)) else echo -e "${RED} ✗ Expected GAME_OVER error code${NC}" ((FAIL++)) fi # Clean up api_request DELETE "$API_URL/games/$ENDGAME_ID" > /dev/null else echo -e "${YELLOW} ⊘ Skipping endgame test${NC}" ((SKIP++)) fi # ============================================================================== print_header "SECTION 6: Rate Limiting (Dev Mode)" # ============================================================================== test_case "6.1: Rapid Requests in Dev Mode (25 requests)" # Clear rate limit window echo -e "${BLUE} Clearing rate limit window...${NC}" sleep 1.1 echo -e "${BLUE} Sending 25 rapid GET requests...${NC}" RATE_LIMITED_AT=0 SUCCESS_COUNT=0 for i in {1..25}; do # Use valid UUID format for better test STATUS=$(api_request GET "$API_URL/games/00000000-0000-0000-0000-00000000000$i" -o /dev/null -w "%{http_code}") if [ "$STATUS" = "429" ]; then if [ $RATE_LIMITED_AT -eq 0 ]; then RATE_LIMITED_AT=$i echo -e "${CYAN} Rate limited at request #$i${NC}" fi elif [ "$STATUS" = "404" ]; then ((SUCCESS_COUNT++)) fi done # In dev mode, we expect at least 20 requests to succeed before rate limiting if [ $SUCCESS_COUNT -ge 20 ]; then echo -e "${GREEN} ✓ At least 20 requests passed before rate limiting (dev mode)${NC}" echo -e "${GREEN} Successful requests: $SUCCESS_COUNT${NC}" if [ $RATE_LIMITED_AT -gt 0 ]; then echo -e "${GREEN} Rate limiting started at request #$RATE_LIMITED_AT${NC}" fi ((PASS++)) elif [ $SUCCESS_COUNT -lt 20 ] && [ $RATE_LIMITED_AT -gt 0 ]; then echo -e "${RED} ✗ Rate limiting too aggressive: only $SUCCESS_COUNT requests passed${NC}" echo -e "${RED} Expected at least 20 in dev mode (20/sec limit)${NC}" ((FAIL++)) else echo -e "${YELLOW} ⚠ No rate limiting detected in 25 requests. Disable dev mode and try again.${NC}" echo -e "${GREEN} All requests passed (acceptable in dev mode)${NC}" ((PASS++)) fi test_case "6.2: Different IPs Bypass Rate Limit" UUID1="11111111-1111-1111-1111-111111111111" UUID2="22222222-2222-2222-2222-222222222222" STATUS1=$(api_request GET "$API_URL/games/$UUID1" -o /dev/null -w "%{http_code}" -H "X-Forwarded-For: 10.0.0.1") STATUS2=$(api_request GET "$API_URL/games/$UUID2" -o /dev/null -w "%{http_code}" -H "X-Forwarded-For: 10.0.0.2") assert_status 404 "$STATUS1" "First IP request" assert_status 404 "$STATUS2" "Different IP not limited" # ============================================================================== print_header "SECTION 7: Advanced Scenarios" # ============================================================================== test_case "7.1: Undo Multiple Moves" # Create game and make moves RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 1}, "black": {"type": 2}}') UNDO_ID=$(echo "$RESPONSE" | jq -r '.gameId' 2>/dev/null) if [ "$UNDO_ID" != "null" ] && [ -n "$UNDO_ID" ]; then # Make 3 moves api_request POST "$API_URL/games/$UNDO_ID/moves" -H "Content-Type: application/json" -d '{"move": "e2e4"}' > /dev/null api_request POST "$API_URL/games/$UNDO_ID/moves" -H "Content-Type: application/json" -d '{"move": "cccc"}' > /dev/null wait_for_state "$UNDO_ID" "!pending" api_request POST "$API_URL/games/$UNDO_ID/moves" -H "Content-Type: application/json" -d '{"move": "g1f3"}' > /dev/null # Undo 2 moves RESPONSE=$(api_request POST "$API_URL/games/$UNDO_ID/undo" \ -H "Content-Type: application/json" \ -d '{"count": 2}') MOVES_COUNT=$(echo "$RESPONSE" | jq -r '.moves | length' 2>/dev/null) if [ "$MOVES_COUNT" = "1" ]; then echo -e "${GREEN} ✓ Successfully undid 2 moves${NC}" ((PASS++)) else echo -e "${RED} ✗ Expected 1 move remaining, got $MOVES_COUNT${NC}" ((FAIL++)) fi # Clean up api_request DELETE "$API_URL/games/$UNDO_ID" > /dev/null else echo -e "${YELLOW} ⊘ Skipping undo test${NC}" ((SKIP++)) fi test_case "7.2: Custom FEN Position" CUSTOM_FEN="r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 4 4" RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d "{\"white\": {\"type\": 1}, \"black\": {\"type\": 1}, \"fen\": \"$CUSTOM_FEN\"}") RETURNED_FEN=$(echo "$RESPONSE" | jq -r '.fen' 2>/dev/null) if echo "$RETURNED_FEN" | grep -q "r1bqkbnr"; then echo -e "${GREEN} ✓ Custom FEN position accepted${NC}" ((PASS++)) else echo -e "${RED} ✗ Custom FEN not properly set${NC}" show_error "$RESPONSE" ((FAIL++)) fi GAME_ID=$(echo "$RESPONSE" | jq -r '.gameId' 2>/dev/null) [ "$GAME_ID" != "null" ] && [ -n "$GAME_ID" ] && api_request DELETE "$API_URL/games/$GAME_ID" > /dev/null # ============================================================================== print_header "SECTION 8: Player Configuration" # ============================================================================== test_case "8.1: Create Game with Engine Configuration" RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 1}, "black": {"type": 2, "level": 10, "searchTime": 500}}') CONFIG_ID=$(echo "$RESPONSE" | jq -r '.gameId' 2>/dev/null) if [ "$CONFIG_ID" != "null" ] && [ -n "$CONFIG_ID" ]; then assert_json_field "$RESPONSE" '.players.black.type' "2" "Black is computer" echo " Game ID: $CONFIG_ID" test_case "8.2: Change Players Mid-Game" # Make a move first api_request POST "$API_URL/games/$CONFIG_ID/moves" \ -H "Content-Type: application/json" \ -d '{"move": "e2e4"}' > /dev/null # Configure players RESPONSE=$(api_request PUT "$API_URL/games/$CONFIG_ID/players" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 2, "level": 5, "searchTime": 100}, "black": {"type": 1}}') RESPONSE=$(api_request GET "$API_URL/games/$CONFIG_ID") assert_json_field "$RESPONSE" '.players.white.type' "2" "White changed to computer" assert_json_field "$RESPONSE" '.players.black.type' "1" "Black changed to human" # Clean up api_request DELETE "$API_URL/games/$CONFIG_ID" > /dev/null else echo -e "${RED} ✗ Failed to create configured game${NC}" show_error "$RESPONSE" ((FAIL++)) SKIP=$((SKIP + 3)) fi # ============================================================================== print_header "SECTION 9: Security Hardening Tests" # ============================================================================== test_case "9.1: UCI Command Injection Prevention" # Attempt to inject quit command via FEN RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 1}, "black": {"type": 2, "searchTime": 100}, "fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\nquit"}') ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error' 2>/dev/null) if [[ "$ERROR_MSG" == *"invalid FEN"* ]] || [[ "$ERROR_MSG" == *"invalid characters"* ]]; then echo -e "${GREEN} ✓ FEN injection blocked${NC}" ((PASS++)) else echo -e "${RED} ✗ FEN injection not properly blocked${NC}" show_error "$RESPONSE" ((FAIL++)) fi # Attempt to inject via move string RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 1}, "black": {"type": 1}}') INJECT_ID=$(echo "$RESPONSE" | jq -r '.gameId' 2>/dev/null) if [ -n "$INJECT_ID" ] && [ "$INJECT_ID" != "null" ]; then RESPONSE=$(api_request POST "$API_URL/games/$INJECT_ID/moves" \ -H "Content-Type: application/json" \ -d '{"move": "e2e4\nquit"}') ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error' 2>/dev/null) if [[ "$ERROR_MSG" == *"validation failed"* ]]; then echo -e "${GREEN} ✓ Move injection blocked${NC}" ((PASS++)) else echo -e "${RED} ✗ Move injection not properly blocked${NC}" ((FAIL++)) fi api_request DELETE "$API_URL/games/$INJECT_ID" > /dev/null fi test_case "9.2: Input Validation - Player Configuration" # Invalid player type (out of range) RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 99}, "black": {"type": 1}}') ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error' 2>/dev/null) if [[ "$ERROR_MSG" == *"validation failed"* ]] || [[ "$ERROR_MSG" == *"type must be one of"* ]]; then echo -e "${GREEN} ✓ Invalid player type rejected${NC}" ((PASS++)) else echo -e "${RED} ✗ Invalid player type not rejected${NC}" show_error "$RESPONSE" ((FAIL++)) fi # AI level out of bounds RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 1}, "black": {"type": 2, "level": 100, "searchTime": 100}}') ERROR_MSG=$(echo "$RESPONSE" | jq -r '.details' 2>/dev/null) if [[ "$ERROR_MSG" == *"Level must be at most 20"* ]]; then echo -e "${GREEN} ✓ Invalid AI level rejected${NC}" ((PASS++)) else echo -e "${RED} ✗ Invalid AI level not rejected${NC}" show_error "$RESPONSE" ((FAIL++)) fi # Negative search time RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 2, "searchTime": -1000}, "black": {"type": 1}}') ERROR_MSG=$(echo "$RESPONSE" | jq -r '.details' 2>/dev/null) if [[ "$ERROR_MSG" == *"SearchTime must be at least 100"* ]]; then echo -e "${GREEN} ✓ Invalid search time rejected${NC}" ((PASS++)) else echo -e "${RED} ✗ Invalid search time not rejected${NC}" show_error "$RESPONSE" ((FAIL++)) fi # SearchTime too small RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 2, "searchTime": 50}, "black": {"type": 1}}') ERROR_MSG=$(echo "$RESPONSE" | jq -r '.details' 2>/dev/null) if [[ "$ERROR_MSG" == *"SearchTime must be at least 100"* ]]; then echo -e "${GREEN} ✓ Search time below minimum rejected${NC}" ((PASS++)) else echo -e "${RED} ✗ Search time below minimum not rejected${NC}" show_error "$RESPONSE" ((FAIL++)) fi test_case "9.3: Move Format Validation" # Create test game RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 1}, "black": {"type": 1}}') MOVE_TEST_ID=$(echo "$RESPONSE" | jq -r '.gameId' 2>/dev/null) if [ -n "$MOVE_TEST_ID" ] && [ "$MOVE_TEST_ID" != "null" ]; then # Too short move (3 chars) STATUS=$(api_request POST "$API_URL/games/$MOVE_TEST_ID/moves" \ -o /dev/null -w "%{http_code}" \ -H "Content-Type: application/json" \ -d '{"move": "e2e"}') assert_status 400 "$STATUS" "3-character move rejected" # Too long move (6 chars) STATUS=$(api_request POST "$API_URL/games/$MOVE_TEST_ID/moves" \ -o /dev/null -w "%{http_code}" \ -H "Content-Type: application/json" \ -d '{"move": "e2e4qq"}') assert_status 400 "$STATUS" "6-character move rejected" # Invalid UCI format (algebraic notation) STATUS=$(api_request POST "$API_URL/games/$MOVE_TEST_ID/moves" \ -o /dev/null -w "%{http_code}" \ -H "Content-Type: application/json" \ -d '{"move": "O-O"}') assert_status 400 "$STATUS" "Algebraic notation rejected" # Valid UCI move STATUS=$(api_request POST "$API_URL/games/$MOVE_TEST_ID/moves" \ -o /dev/null -w "%{http_code}" \ -H "Content-Type: application/json" \ -d '{"move": "e2e4"}') assert_status 200 "$STATUS" "Valid UCI move accepted" api_request DELETE "$API_URL/games/$MOVE_TEST_ID" > /dev/null else echo -e "${YELLOW} ⊘ Skipping move format tests${NC}" SKIP=$((SKIP + 4)) fi test_case "9.4: FEN Character Set Validation" # FEN with invalid characters (SQL injection attempt) RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d "{\"white\": {\"type\": 1}, \"black\": {\"type\": 2, \"searchTime\": 100}, \"fen\": \"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; DROP TABLE--\"}") ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error' 2>/dev/null) if [[ "$ERROR_MSG" == *"invalid FEN"* ]]; then echo -e "${GREEN} ✓ SQL injection in FEN blocked${NC}" ((PASS++)) else echo -e "${RED} ✗ SQL injection in FEN not blocked${NC}" ((FAIL++)) fi # FEN with control characters RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d "$(printf '{"white": {"type": 1}, "black": {"type": 2, "searchTime": 100}, "fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\\x13"}')") ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error' 2>/dev/null) # Accept either the specific validation error OR the body parser error if [[ "$ERROR_MSG" == *"invalid FEN"* ]] || [[ "$ERROR_MSG" == *"invalid characters"* ]] || [[ "$ERROR_MSG" == *"invalid request body"* ]]; then echo -e "${GREEN} ✓ Control characters in FEN blocked${NC}" ((PASS++)) else echo -e "${RED} ✗ Control characters in FEN not blocked${NC}" ((FAIL++)) fi test_case "9.5: Undo Count Validation" # Create game for undo test RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {"type": 1}, "black": {"type": 1}}') UNDO_TEST_ID=$(echo "$RESPONSE" | jq -r '.gameId' 2>/dev/null) if [ -n "$UNDO_TEST_ID" ] && [ "$UNDO_TEST_ID" != "null" ]; then # Make a move first api_request POST "$API_URL/games/$UNDO_TEST_ID/moves" \ -H "Content-Type: application/json" \ -d '{"move": "e2e4"}' > /dev/null # Zero undo count (should default to 1) RESPONSE=$(api_request POST "$API_URL/games/$UNDO_TEST_ID/undo" \ -H "Content-Type: application/json" \ -d '{"count": 0}') MOVES_COUNT=$(echo "$RESPONSE" | jq -r '.moves | length' 2>/dev/null) if [ "$MOVES_COUNT" = "0" ]; then echo -e "${GREEN} ✓ Zero undo count defaults to 1${NC}" ((PASS++)) else echo -e "${RED} ✗ Zero undo count handling incorrect${NC}" ((FAIL++)) fi # Re-make move api_request POST "$API_URL/games/$UNDO_TEST_ID/moves" \ -H "Content-Type: application/json" \ -d '{"move": "e2e4"}' > /dev/null # Excessive undo count (over 300) RESPONSE=$(api_request POST "$API_URL/games/$UNDO_TEST_ID/undo" \ -H "Content-Type: application/json" \ -d '{"count": 301}') ERROR_MSG=$(echo "$RESPONSE" | jq -r '.details' 2>/dev/null) if [[ "$ERROR_MSG" == *"Count must be at most 300"* ]]; then echo -e "${GREEN} ✓ Excessive undo count rejected${NC}" ((PASS++)) else echo -e "${RED} ✗ Excessive undo count not rejected${NC}" show_error "$RESPONSE" ((FAIL++)) fi api_request DELETE "$API_URL/games/$UNDO_TEST_ID" > /dev/null else echo -e "${YELLOW} ⊘ Skipping undo validation tests${NC}" SKIP=$((SKIP + 2)) fi test_case "9.6: Validation Bypass Prevention" # Attempt to send malformed JSON to trigger fallback STATUS=$(api_request POST "$API_URL/games" \ -o /dev/null -w "%{http_code}" \ -H "Content-Type: application/json" \ -d '{"white": {"type": "INJECT"}, "black": }') assert_status 400 "$STATUS" "Malformed JSON rejected" # Missing required fields RESPONSE=$(api_request POST "$API_URL/games" \ -H "Content-Type: application/json" \ -d '{"white": {}}') ERROR_MSG=$(echo "$RESPONSE" | jq -r '.details' 2>/dev/null) if [[ "$ERROR_MSG" == *"Type is required"* ]]; then echo -e "${GREEN} ✓ Missing required fields caught${NC}" ((PASS++)) else echo -e "${RED} ✗ Missing required fields not caught${NC}" show_error "$RESPONSE" ((FAIL++)) fi # ============================================================================== print_header "Test Summary" # ============================================================================== TOTAL=$((PASS + FAIL + SKIP)) SUCCESS_RATE=0 if [ $TOTAL -gt 0 ]; then SUCCESS_RATE=$(( (PASS * 100) / TOTAL )) fi echo -e "\n${CYAN}══════════════════════════════════════${NC}" echo -e "${GREEN}✓ Passed: $PASS${NC}" echo -e "${RED}✗ Failed: $FAIL${NC}" if [ $SKIP -gt 0 ]; then echo -e "${YELLOW}⊘ Skipped: $SKIP${NC}" fi echo -e "${CYAN}──────────────────────────────────────${NC}" echo -e "Total Tests: $TOTAL" echo -e "Success Rate: ${SUCCESS_RATE}%" echo -e "${CYAN}══════════════════════════════════════${NC}" if [ $FAIL -eq 0 ]; then echo -e "\n${GREEN}🎉 All tests passed successfully!${NC}" exit 0 else echo -e "\n${RED}⚠️ Some tests failed. Review the output above.${NC}" exit 1 fi