API Testing Guide
Complete guide for testing TVL Platform APIs using curl, automated scripts, and integration tools.
Table of Contents
- Quick Start
- Authentication Testing
- CRUD Operations Examples
- Advanced Testing Scenarios
- Testing Scripts
- Integration Testing
- Performance Testing
- Webhook Testing
- Common Testing Patterns
- CI/CD Integration
1. Quick Start
Prerequisites
Install required tools:
# curl (usually pre-installed)
curl --version
# jq for JSON parsing
# Ubuntu/Debian
sudo apt-get install jq
# macOS
brew install jq
# httpie (optional, user-friendly alternative)
pip install httpie
# newman (Postman CLI)
npm install -g newman
Setting Up Environment Variables
Create a .env.test file:
# API Configuration
export API_BASE_URL="https://api.tvl.com"
export API_VERSION="v1"
export API_TIMEOUT=30
# Authentication
export GOOGLE_CLIENT_ID="your-google-client-id"
export GOOGLE_CLIENT_SECRET="your-google-client-secret"
export TEST_USER_EMAIL="test@example.com"
export TEST_USER_PASSWORD="test-password"
# Testing Configuration
export TEST_PROPERTY_ID=""
export TEST_UNIT_ID=""
export TEST_BOOKING_ID=""
export IDEMPOTENCY_KEY=""
# Debug Mode
export DEBUG=false
export VERBOSE=false
Load environment variables:
source .env.test
Quick Test Script
#!/bin/bash
# quick-test.sh - Quick API health check
# Colors for output
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo "Testing TVL API..."
# Health check
response=$(curl -s -o /dev/null -w "%{http_code}" ${API_BASE_URL}/health)
if [ $response -eq 200 ]; then
    echo -e "${GREEN}✓ Health check passed${NC}"
else
    echo -e "${RED}✗ Health check failed (${response})${NC}"
    exit 1
fi
# Version check
version=$(curl -s ${API_BASE_URL}/version | jq -r '.version')
echo -e "${GREEN}✓ API Version: ${version}${NC}"
echo "API is ready for testing!"
2. Authentication Testing
Getting JWT Token (Google OIDC Simulation)
#!/bin/bash
# get-token.sh - Obtain JWT token for testing
# Step 1: Initiate Google OAuth flow
AUTH_URL="${API_BASE_URL}/v1/auth/google/authorize"
echo "Initiating Google OAuth flow..."
auth_response=$(curl -s -X POST ${AUTH_URL} \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "'${GOOGLE_CLIENT_ID}'",
    "redirect_uri": "http://localhost:3000/callback",
    "scope": "openid profile email",
    "state": "random-state-string"
  }')
echo "Auth Response: ${auth_response}"
# Step 2: Simulate OAuth callback with authorization code
# In real scenario, user would authenticate with Google
AUTH_CODE=$(echo $auth_response | jq -r '.authorization_code')
# Step 3: Exchange authorization code for tokens
TOKEN_URL="${API_BASE_URL}/v1/auth/google/token"
token_response=$(curl -s -X POST ${TOKEN_URL} \
  -H "Content-Type: application/json" \
  -d '{
    "code": "'${AUTH_CODE}'",
    "client_id": "'${GOOGLE_CLIENT_ID}'",
    "client_secret": "'${GOOGLE_CLIENT_SECRET}'",
    "redirect_uri": "http://localhost:3000/callback"
  }')
# Extract tokens
export ACCESS_TOKEN=$(echo $token_response | jq -r '.access_token')
export REFRESH_TOKEN=$(echo $token_response | jq -r '.refresh_token')
export TOKEN_EXPIRES_IN=$(echo $token_response | jq -r '.expires_in')
echo "Access Token: ${ACCESS_TOKEN}"
echo "Refresh Token: ${REFRESH_TOKEN}"
echo "Expires In: ${TOKEN_EXPIRES_IN} seconds"
# Save tokens to file
cat > .tokens <<EOF
ACCESS_TOKEN=${ACCESS_TOKEN}
REFRESH_TOKEN=${REFRESH_TOKEN}
TOKEN_EXPIRES_IN=${TOKEN_EXPIRES_IN}
TOKEN_OBTAINED_AT=$(date +%s)
EOF
echo "Tokens saved to .tokens file"
Test Token Validity
# Test if token is valid
curl -X GET "${API_BASE_URL}/v1/auth/me" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  | jq '.'
# Expected response:
# {
#   "id": "usr_123456",
#   "email": "test@example.com",
#   "name": "Test User",
#   "role": "property_manager",
#   "permissions": ["read:properties", "write:bookings"]
# }
Refresh Token Flow
#!/bin/bash
# refresh-token.sh - Refresh expired access token
source .tokens
REFRESH_URL="${API_BASE_URL}/v1/auth/refresh"
refresh_response=$(curl -s -X POST ${REFRESH_URL} \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "'${REFRESH_TOKEN}'"
  }')
# Update tokens
export ACCESS_TOKEN=$(echo $refresh_response | jq -r '.access_token')
NEW_REFRESH_TOKEN=$(echo $refresh_response | jq -r '.refresh_token')
# Update if new refresh token provided
if [ "$NEW_REFRESH_TOKEN" != "null" ]; then
  export REFRESH_TOKEN=$NEW_REFRESH_TOKEN
fi
# Save updated tokens
cat > .tokens <<EOF
ACCESS_TOKEN=${ACCESS_TOKEN}
REFRESH_TOKEN=${REFRESH_TOKEN}
TOKEN_EXPIRES_IN=$(echo $refresh_response | jq -r '.expires_in')
TOKEN_OBTAINED_AT=$(date +%s)
EOF
echo "Token refreshed successfully"
3. CRUD Operations Examples
Properties API
CREATE Property
# Create a new property
curl -X POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: prop-create-$(uuidgen)" \
  -d '{
    "name": "Sunset Villa",
    "description": "Luxurious beachfront villa with stunning sunset views",
    "property_type": "villa",
    "address": {
      "street": "123 Ocean Drive",
      "city": "Malibu",
      "state": "CA",
      "postal_code": "90265",
      "country": "US"
    },
    "coordinates": {
      "latitude": 34.0259,
      "longitude": -118.7798
    },
    "amenities": [
      "wifi",
      "pool",
      "ocean_view",
      "parking",
      "air_conditioning"
    ],
    "images": [
      "https://storage.tvl.com/properties/sunset-villa-1.jpg",
      "https://storage.tvl.com/properties/sunset-villa-2.jpg"
    ],
    "metadata": {
      "hostaway_property_id": "12345",
      "check_in_time": "15:00",
      "check_out_time": "11:00"
    }
  }' | jq '.'
# Save property ID for later use
export TEST_PROPERTY_ID=$(curl -s -X POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: prop-create-$(uuidgen)" \
  -d '{"name":"Test Property"}' | jq -r '.id')
READ Property
# Get single property by ID
curl -X GET "${API_BASE_URL}/v1/properties/${TEST_PROPERTY_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  | jq '.'
# List all properties with pagination
curl -X GET "${API_BASE_URL}/v1/properties?limit=20&cursor=eyJpZCI6MTIzfQ==" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  | jq '.'
# Filter properties by criteria
curl -X GET "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -G \
  --data-urlencode "property_type=villa" \
  --data-urlencode "city=Malibu" \
  --data-urlencode "amenities=pool,wifi" \
  --data-urlencode "min_price=100" \
  --data-urlencode "max_price=500" \
  | jq '.'
UPDATE Property
# Full update (PUT)
curl -X PUT "${API_BASE_URL}/v1/properties/${TEST_PROPERTY_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Sunset Villa - Updated",
    "description": "Updated description with new amenities",
    "property_type": "villa",
    "address": {
      "street": "123 Ocean Drive",
      "city": "Malibu",
      "state": "CA",
      "postal_code": "90265",
      "country": "US"
    }
  }' | jq '.'
# Partial update (PATCH)
curl -X PATCH "${API_BASE_URL}/v1/properties/${TEST_PROPERTY_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Sunset Villa - Premium",
    "amenities": ["wifi", "pool", "ocean_view", "parking", "air_conditioning", "hot_tub"]
  }' | jq '.'
DELETE Property
# Soft delete (default)
curl -X DELETE "${API_BASE_URL}/v1/properties/${TEST_PROPERTY_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  | jq '.'
# Hard delete (permanent)
curl -X DELETE "${API_BASE_URL}/v1/properties/${TEST_PROPERTY_ID}?permanent=true" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Confirm-Delete: true" \
  | jq '.'
Units API
CREATE Unit
# Create a new unit
curl -X POST "${API_BASE_URL}/v1/units" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: unit-create-$(uuidgen)" \
  -d '{
    "property_id": "'${TEST_PROPERTY_ID}'",
    "name": "Master Suite",
    "unit_type": "bedroom",
    "floor": 2,
    "max_occupancy": 2,
    "bedrooms": 1,
    "bathrooms": 1,
    "size_sqft": 450,
    "base_price": 250.00,
    "amenities": ["king_bed", "private_bathroom", "balcony"],
    "images": [
      "https://storage.tvl.com/units/master-suite-1.jpg"
    ]
  }' | jq '.'
# Save unit ID
export TEST_UNIT_ID=$(curl -s -X POST "${API_BASE_URL}/v1/units" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: unit-create-$(uuidgen)" \
  -d '{"property_id":"'${TEST_PROPERTY_ID}'","name":"Test Unit"}' | jq -r '.id')
READ Unit
# Get single unit
curl -X GET "${API_BASE_URL}/v1/units/${TEST_UNIT_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  | jq '.'
# List units for a property
curl -X GET "${API_BASE_URL}/v1/properties/${TEST_PROPERTY_ID}/units" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  | jq '.'
UPDATE Unit
# Partial update
curl -X PATCH "${API_BASE_URL}/v1/units/${TEST_UNIT_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "base_price": 275.00,
    "amenities": ["king_bed", "private_bathroom", "balcony", "ocean_view"]
  }' | jq '.'
DELETE Unit
# Delete unit
curl -X DELETE "${API_BASE_URL}/v1/units/${TEST_UNIT_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  | jq '.'
Bookings API
CREATE Booking
# Create a new booking
curl -X POST "${API_BASE_URL}/v1/bookings" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: booking-create-$(uuidgen)" \
  -d '{
    "property_id": "'${TEST_PROPERTY_ID}'",
    "unit_id": "'${TEST_UNIT_ID}'",
    "check_in_date": "2025-11-01",
    "check_out_date": "2025-11-05",
    "guest": {
      "first_name": "John",
      "last_name": "Doe",
      "email": "john.doe@example.com",
      "phone": "+1-555-0123",
      "adults": 2,
      "children": 0
    },
    "pricing": {
      "base_price": 250.00,
      "nights": 4,
      "subtotal": 1000.00,
      "cleaning_fee": 75.00,
      "service_fee": 50.00,
      "taxes": 87.50,
      "total": 1212.50
    },
    "payment_method": "credit_card",
    "special_requests": "Late check-in around 8 PM",
    "source": "direct",
    "metadata": {
      "hostaway_booking_id": "67890"
    }
  }' | jq '.'
# Save booking ID
export TEST_BOOKING_ID=$(curl -s -X POST "${API_BASE_URL}/v1/bookings" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: booking-create-$(uuidgen)" \
  -d '{
    "property_id": "'${TEST_PROPERTY_ID}'",
    "unit_id": "'${TEST_UNIT_ID}'",
    "check_in_date": "2025-11-01",
    "check_out_date": "2025-11-05"
  }' | jq -r '.id')
READ Booking
# Get single booking
curl -X GET "${API_BASE_URL}/v1/bookings/${TEST_BOOKING_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  | jq '.'
# List bookings with filters
curl -X GET "${API_BASE_URL}/v1/bookings" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -G \
  --data-urlencode "property_id=${TEST_PROPERTY_ID}" \
  --data-urlencode "status=confirmed" \
  --data-urlencode "check_in_from=2025-11-01" \
  --data-urlencode "check_in_to=2025-11-30" \
  | jq '.'
UPDATE Booking
# Update booking status
curl -X PATCH "${API_BASE_URL}/v1/bookings/${TEST_BOOKING_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "confirmed",
    "confirmed_at": "2025-10-24T12:00:00Z"
  }' | jq '.'
# Modify booking dates
curl -X PATCH "${API_BASE_URL}/v1/bookings/${TEST_BOOKING_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "check_in_date": "2025-11-02",
    "check_out_date": "2025-11-06"
  }' | jq '.'
DELETE Booking (Cancel)
# Cancel booking
curl -X DELETE "${API_BASE_URL}/v1/bookings/${TEST_BOOKING_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "cancellation_reason": "Guest requested cancellation",
    "refund_amount": 1000.00
  }' | jq '.'
Availability API
Check Availability
# Check unit availability
curl -X GET "${API_BASE_URL}/v1/availability" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -G \
  --data-urlencode "property_id=${TEST_PROPERTY_ID}" \
  --data-urlencode "check_in_date=2025-11-01" \
  --data-urlencode "check_out_date=2025-11-05" \
  --data-urlencode "adults=2" \
  | jq '.'
Update Availability
# Block dates
curl -X POST "${API_BASE_URL}/v1/availability/block" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "unit_id": "'${TEST_UNIT_ID}'",
    "start_date": "2025-12-24",
    "end_date": "2025-12-26",
    "reason": "maintenance"
  }' | jq '.'
# Unblock dates
curl -X DELETE "${API_BASE_URL}/v1/availability/block/${BLOCK_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  | jq '.'
4. Advanced Testing Scenarios
Pagination Testing
#!/bin/bash
# test-pagination.sh - Test cursor-based pagination
echo "Testing pagination..."
# Get first page
page1=$(curl -s -X GET "${API_BASE_URL}/v1/properties?limit=10" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}")
echo "Page 1 items: $(echo $page1 | jq '.data | length')"
# Extract next cursor
next_cursor=$(echo $page1 | jq -r '.pagination.next_cursor')
if [ "$next_cursor" != "null" ]; then
  # Get second page
  page2=$(curl -s -X GET "${API_BASE_URL}/v1/properties?limit=10&cursor=${next_cursor}" \
    -H "Authorization: Bearer ${ACCESS_TOKEN}")
  echo "Page 2 items: $(echo $page2 | jq '.data | length')"
fi
# Test edge cases
echo "Testing limit=0 (should use default)..."
curl -s -X GET "${API_BASE_URL}/v1/properties?limit=0" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.pagination'
echo "Testing limit=1000 (should cap at max)..."
curl -s -X GET "${API_BASE_URL}/v1/properties?limit=1000" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.pagination'
echo "Testing invalid cursor..."
curl -s -X GET "${API_BASE_URL}/v1/properties?cursor=invalid" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.error'
Filtering and Search
#!/bin/bash
# test-filtering.sh - Test filtering capabilities
echo "Testing filters..."
# Multiple filters
curl -s -X GET "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -G \
  --data-urlencode "city=Malibu" \
  --data-urlencode "property_type=villa" \
  --data-urlencode "min_price=100" \
  --data-urlencode "max_price=500" \
  --data-urlencode "amenities=pool,wifi" \
  | jq '.data | length'
# Full-text search
curl -s -X GET "${API_BASE_URL}/v1/properties/search" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -G \
  --data-urlencode "q=beachfront villa sunset view" \
  | jq '.'
# Date range filters
curl -s -X GET "${API_BASE_URL}/v1/bookings" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -G \
  --data-urlencode "check_in_from=2025-11-01" \
  --data-urlencode "check_in_to=2025-11-30" \
  --data-urlencode "status=confirmed,pending" \
  | jq '.'
Rate Limiting Testing
#!/bin/bash
# test-rate-limiting.sh - Test rate limits
echo "Testing rate limits..."
# Make rapid requests
for i in {1..100}; do
  response=$(curl -s -w "\n%{http_code}" -X GET "${API_BASE_URL}/v1/properties" \
    -H "Authorization: Bearer ${ACCESS_TOKEN}")
  http_code=$(echo "$response" | tail -n1)
  if [ "$http_code" == "429" ]; then
    echo "Rate limit hit at request $i"
    # Extract retry-after header
    retry_after=$(curl -s -I -X GET "${API_BASE_URL}/v1/properties" \
      -H "Authorization: Bearer ${ACCESS_TOKEN}" | grep -i "retry-after" | cut -d' ' -f2)
    echo "Retry after: ${retry_after} seconds"
    break
  fi
  echo "Request $i: $http_code"
  sleep 0.1
done
# Test rate limit headers
curl -I -X GET "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  | grep -E "(X-RateLimit-|Retry-After)"
Idempotency Testing
#!/bin/bash
# test-idempotency.sh - Test idempotency keys
echo "Testing idempotency..."
# Generate unique idempotency key
IDEMPOTENCY_KEY="test-$(uuidgen)"
# First request
response1=$(curl -s -X POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: ${IDEMPOTENCY_KEY}" \
  -d '{
    "name": "Idempotency Test Property",
    "property_type": "apartment"
  }')
property_id1=$(echo $response1 | jq -r '.id')
echo "First request created property: ${property_id1}"
# Second request with same idempotency key (should return cached response)
response2=$(curl -s -X POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: ${IDEMPOTENCY_KEY}" \
  -d '{
    "name": "Different Name",
    "property_type": "villa"
  }')
property_id2=$(echo $response2 | jq -r '.id')
echo "Second request returned property: ${property_id2}"
if [ "$property_id1" == "$property_id2" ]; then
  echo "✓ Idempotency working correctly"
else
  echo "✗ Idempotency failed - different IDs returned"
fi
# Clean up
curl -s -X DELETE "${API_BASE_URL}/v1/properties/${property_id1}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" > /dev/null
Concurrent Requests
#!/bin/bash
# test-concurrent.sh - Test concurrent request handling
echo "Testing concurrent requests..."
# Function to make a booking request
make_booking() {
  local id=$1
  curl -s -X POST "${API_BASE_URL}/v1/bookings" \
    -H "Authorization: Bearer ${ACCESS_TOKEN}" \
    -H "Content-Type: application/json" \
    -H "X-Idempotency-Key: concurrent-${id}" \
    -d '{
      "property_id": "'${TEST_PROPERTY_ID}'",
      "unit_id": "'${TEST_UNIT_ID}'",
      "check_in_date": "2025-11-01",
      "check_out_date": "2025-11-05"
    }' -w "\nHTTP_CODE:%{http_code}" -o /tmp/booking_${id}.json
}
# Launch 10 concurrent requests for the same dates
for i in {1..10}; do
  make_booking $i &
done
# Wait for all background jobs
wait
# Check results
successful=0
conflicts=0
for i in {1..10}; do
  http_code=$(tail -n1 /tmp/booking_${i}.json | cut -d':' -f2)
  if [ "$http_code" == "201" ]; then
    ((successful++))
  elif [ "$http_code" == "409" ]; then
    ((conflicts++))
  fi
done
echo "Successful bookings: ${successful}"
echo "Conflicts detected: ${conflicts}"
# Only one should succeed
if [ $successful -eq 1 ] && [ $conflicts -eq 9 ]; then
  echo "✓ Concurrent booking handling working correctly"
else
  echo "✗ Unexpected concurrent booking behavior"
fi
# Clean up
rm -f /tmp/booking_*.json
Error Handling
#!/bin/bash
# test-error-handling.sh - Test error responses
echo "Testing error handling..."
# 400 Bad Request - Invalid data
echo "Testing 400 Bad Request..."
curl -s -X POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "",
    "property_type": "invalid_type"
  }' | jq '.'
# 401 Unauthorized - Missing token
echo "Testing 401 Unauthorized..."
curl -s -X GET "${API_BASE_URL}/v1/properties" \
  | jq '.'
# 403 Forbidden - Insufficient permissions
echo "Testing 403 Forbidden..."
curl -s -X DELETE "${API_BASE_URL}/v1/users/admin_user_id" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  | jq '.'
# 404 Not Found - Resource doesn't exist
echo "Testing 404 Not Found..."
curl -s -X GET "${API_BASE_URL}/v1/properties/nonexistent_id" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  | jq '.'
# 409 Conflict - Duplicate booking
echo "Testing 409 Conflict..."
curl -s -X POST "${API_BASE_URL}/v1/bookings" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "property_id": "'${TEST_PROPERTY_ID}'",
    "unit_id": "'${TEST_UNIT_ID}'",
    "check_in_date": "2025-11-01",
    "check_out_date": "2025-11-05"
  }' | jq '.'
# 422 Unprocessable Entity - Validation error
echo "Testing 422 Unprocessable Entity..."
curl -s -X POST "${API_BASE_URL}/v1/bookings" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "property_id": "'${TEST_PROPERTY_ID}'",
    "check_in_date": "2025-11-05",
    "check_out_date": "2025-11-01"
  }' | jq '.'
# 500 Internal Server Error (simulate)
echo "Testing 500 Internal Server Error..."
curl -s -X POST "${API_BASE_URL}/v1/internal/trigger-error" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  | jq '.'
5. Testing Scripts
test-auth.sh
#!/bin/bash
# test-auth.sh - Complete authentication flow testing
set -e
echo "=== Authentication Flow Test ==="
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Load configuration
source .env.test
# Test 1: Health check
echo -e "\n${YELLOW}Test 1: API Health Check${NC}"
health_response=$(curl -s -w "\n%{http_code}" ${API_BASE_URL}/health)
health_code=$(echo "$health_response" | tail -n1)
if [ "$health_code" == "200" ]; then
  echo -e "${GREEN}✓ Health check passed${NC}"
else
  echo -e "${RED}✗ Health check failed${NC}"
  exit 1
fi
# Test 2: Get authorization URL
echo -e "\n${YELLOW}Test 2: Get Authorization URL${NC}"
auth_response=$(curl -s -X POST ${API_BASE_URL}/v1/auth/google/authorize \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "'${GOOGLE_CLIENT_ID}'",
    "redirect_uri": "http://localhost:3000/callback",
    "scope": "openid profile email"
  }')
auth_url=$(echo $auth_response | jq -r '.authorization_url')
echo "Authorization URL: ${auth_url}"
if [ "$auth_url" != "null" ]; then
  echo -e "${GREEN}✓ Authorization URL obtained${NC}"
else
  echo -e "${RED}✗ Failed to get authorization URL${NC}"
  exit 1
fi
# Test 3: Simulate OAuth callback (in test environment)
echo -e "\n${YELLOW}Test 3: OAuth Callback Simulation${NC}"
# In production, this would come from Google
TEST_AUTH_CODE="test_auth_code_$(date +%s)"
token_response=$(curl -s -X POST ${API_BASE_URL}/v1/auth/google/token \
  -H "Content-Type: application/json" \
  -d '{
    "code": "'${TEST_AUTH_CODE}'",
    "client_id": "'${GOOGLE_CLIENT_ID}'",
    "client_secret": "'${GOOGLE_CLIENT_SECRET}'",
    "redirect_uri": "http://localhost:3000/callback"
  }')
ACCESS_TOKEN=$(echo $token_response | jq -r '.access_token')
REFRESH_TOKEN=$(echo $token_response | jq -r '.refresh_token')
if [ "$ACCESS_TOKEN" != "null" ]; then
  echo -e "${GREEN}✓ Access token obtained${NC}"
  echo "Token preview: ${ACCESS_TOKEN:0:20}..."
else
  echo -e "${RED}✗ Failed to obtain access token${NC}"
  exit 1
fi
# Test 4: Validate token
echo -e "\n${YELLOW}Test 4: Token Validation${NC}"
me_response=$(curl -s -X GET ${API_BASE_URL}/v1/auth/me \
  -H "Authorization: Bearer ${ACCESS_TOKEN}")
user_id=$(echo $me_response | jq -r '.id')
if [ "$user_id" != "null" ]; then
  echo -e "${GREEN}✓ Token is valid${NC}"
  echo "User ID: ${user_id}"
  echo "Email: $(echo $me_response | jq -r '.email')"
else
  echo -e "${RED}✗ Token validation failed${NC}"
  exit 1
fi
# Test 5: Token refresh
echo -e "\n${YELLOW}Test 5: Token Refresh${NC}"
sleep 2
refresh_response=$(curl -s -X POST ${API_BASE_URL}/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "'${REFRESH_TOKEN}'"
  }')
NEW_ACCESS_TOKEN=$(echo $refresh_response | jq -r '.access_token')
if [ "$NEW_ACCESS_TOKEN" != "null" ]; then
  echo -e "${GREEN}✓ Token refreshed successfully${NC}"
else
  echo -e "${RED}✗ Token refresh failed${NC}"
  exit 1
fi
# Test 6: Revoke token
echo -e "\n${YELLOW}Test 6: Token Revocation${NC}"
revoke_response=$(curl -s -X POST ${API_BASE_URL}/v1/auth/revoke \
  -H "Authorization: Bearer ${NEW_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "token": "'${NEW_ACCESS_TOKEN}'"
  }')
# Try to use revoked token
revoked_test=$(curl -s -w "\n%{http_code}" -X GET ${API_BASE_URL}/v1/auth/me \
  -H "Authorization: Bearer ${NEW_ACCESS_TOKEN}")
revoked_code=$(echo "$revoked_test" | tail -n1)
if [ "$revoked_code" == "401" ]; then
  echo -e "${GREEN}✓ Token successfully revoked${NC}"
else
  echo -e "${RED}✗ Revoked token still works${NC}"
  exit 1
fi
echo -e "\n${GREEN}=== All Authentication Tests Passed ===${NC}"
test-booking-flow.sh
#!/bin/bash
# test-booking-flow.sh - Complete booking workflow test
set -e
echo "=== Booking Flow Test ==="
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
source .env.test
source .tokens
# Step 1: Check availability
echo -e "\n${YELLOW}Step 1: Check Availability${NC}"
availability_response=$(curl -s -X GET "${API_BASE_URL}/v1/availability" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -G \
  --data-urlencode "property_id=${TEST_PROPERTY_ID}" \
  --data-urlencode "check_in_date=2025-11-01" \
  --data-urlencode "check_out_date=2025-11-05" \
  --data-urlencode "adults=2")
available_units=$(echo $availability_response | jq -r '.available_units | length')
echo "Available units: ${available_units}"
if [ "$available_units" -gt 0 ]; then
  echo -e "${GREEN}✓ Units available${NC}"
  AVAILABLE_UNIT_ID=$(echo $availability_response | jq -r '.available_units[0].id')
else
  echo -e "${RED}✗ No units available${NC}"
  exit 1
fi
# Step 2: Get pricing quote
echo -e "\n${YELLOW}Step 2: Get Pricing Quote${NC}"
quote_response=$(curl -s -X POST "${API_BASE_URL}/v1/bookings/quote" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "property_id": "'${TEST_PROPERTY_ID}'",
    "unit_id": "'${AVAILABLE_UNIT_ID}'",
    "check_in_date": "2025-11-01",
    "check_out_date": "2025-11-05",
    "adults": 2,
    "children": 0
  }')
total_price=$(echo $quote_response | jq -r '.total')
echo "Total price: \$${total_price}"
if [ "$total_price" != "null" ]; then
  echo -e "${GREEN}✓ Quote generated${NC}"
else
  echo -e "${RED}✗ Failed to get quote${NC}"
  exit 1
fi
# Step 3: Create booking (with idempotency)
echo -e "\n${YELLOW}Step 3: Create Booking${NC}"
IDEMPOTENCY_KEY="booking-test-$(uuidgen)"
booking_response=$(curl -s -X POST "${API_BASE_URL}/v1/bookings" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: ${IDEMPOTENCY_KEY}" \
  -d '{
    "property_id": "'${TEST_PROPERTY_ID}'",
    "unit_id": "'${AVAILABLE_UNIT_ID}'",
    "check_in_date": "2025-11-01",
    "check_out_date": "2025-11-05",
    "guest": {
      "first_name": "Test",
      "last_name": "User",
      "email": "test@example.com",
      "phone": "+1-555-0123",
      "adults": 2,
      "children": 0
    },
    "pricing": '$quote_response',
    "payment_method": "credit_card"
  }')
BOOKING_ID=$(echo $booking_response | jq -r '.id')
booking_status=$(echo $booking_response | jq -r '.status')
echo "Booking ID: ${BOOKING_ID}"
echo "Status: ${booking_status}"
if [ "$BOOKING_ID" != "null" ]; then
  echo -e "${GREEN}✓ Booking created${NC}"
else
  echo -e "${RED}✗ Failed to create booking${NC}"
  exit 1
fi
# Step 4: Verify idempotency (retry same request)
echo -e "\n${YELLOW}Step 4: Test Idempotency${NC}"
retry_response=$(curl -s -X POST "${API_BASE_URL}/v1/bookings" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: ${IDEMPOTENCY_KEY}" \
  -d '{
    "property_id": "'${TEST_PROPERTY_ID}'",
    "unit_id": "'${AVAILABLE_UNIT_ID}'",
    "check_in_date": "2025-11-01",
    "check_out_date": "2025-11-05"
  }')
RETRY_BOOKING_ID=$(echo $retry_response | jq -r '.id')
if [ "$BOOKING_ID" == "$RETRY_BOOKING_ID" ]; then
  echo -e "${GREEN}✓ Idempotency working${NC}"
else
  echo -e "${RED}✗ Idempotency failed${NC}"
fi
# Step 5: Confirm booking
echo -e "\n${YELLOW}Step 5: Confirm Booking${NC}"
confirm_response=$(curl -s -X PATCH "${API_BASE_URL}/v1/bookings/${BOOKING_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "confirmed",
    "payment_status": "paid"
  }')
confirmed_status=$(echo $confirm_response | jq -r '.status')
if [ "$confirmed_status" == "confirmed" ]; then
  echo -e "${GREEN}✓ Booking confirmed${NC}"
else
  echo -e "${RED}✗ Failed to confirm booking${NC}"
fi
# Step 6: Retrieve booking details
echo -e "\n${YELLOW}Step 6: Retrieve Booking${NC}"
get_response=$(curl -s -X GET "${API_BASE_URL}/v1/bookings/${BOOKING_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}")
echo $get_response | jq '.'
# Step 7: Modify booking (change dates)
echo -e "\n${YELLOW}Step 7: Modify Booking Dates${NC}"
modify_response=$(curl -s -X PATCH "${API_BASE_URL}/v1/bookings/${BOOKING_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "check_out_date": "2025-11-06"
  }')
new_checkout=$(echo $modify_response | jq -r '.check_out_date')
echo "New checkout date: ${new_checkout}"
if [ "$new_checkout" == "2025-11-06" ]; then
  echo -e "${GREEN}✓ Booking modified${NC}"
else
  echo -e "${RED}✗ Failed to modify booking${NC}"
fi
# Step 8: Cancel booking
echo -e "\n${YELLOW}Step 8: Cancel Booking${NC}"
cancel_response=$(curl -s -X DELETE "${API_BASE_URL}/v1/bookings/${BOOKING_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "cancellation_reason": "Test completed",
    "refund_amount": '$total_price'
  }')
cancelled_status=$(echo $cancel_response | jq -r '.status')
if [ "$cancelled_status" == "cancelled" ]; then
  echo -e "${GREEN}✓ Booking cancelled${NC}"
else
  echo -e "${RED}✗ Failed to cancel booking${NC}"
fi
echo -e "\n${GREEN}=== Booking Flow Test Complete ===${NC}"
test-property-crud.sh
#!/bin/bash
# test-property-crud.sh - Property CRUD operations test
set -e
echo "=== Property CRUD Test ==="
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
source .env.test
source .tokens
# CREATE
echo -e "\n${YELLOW}CREATE: Creating new property${NC}"
create_response=$(curl -s -X POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: prop-crud-$(uuidgen)" \
  -d '{
    "name": "CRUD Test Property",
    "description": "Property for testing CRUD operations",
    "property_type": "apartment",
    "address": {
      "street": "456 Test Street",
      "city": "Test City",
      "state": "CA",
      "postal_code": "90001",
      "country": "US"
    },
    "amenities": ["wifi", "parking"]
  }')
PROPERTY_ID=$(echo $create_response | jq -r '.id')
echo "Created property ID: ${PROPERTY_ID}"
if [ "$PROPERTY_ID" != "null" ]; then
  echo -e "${GREEN}✓ CREATE successful${NC}"
else
  echo -e "${RED}✗ CREATE failed${NC}"
  exit 1
fi
# READ
echo -e "\n${YELLOW}READ: Fetching property${NC}"
read_response=$(curl -s -X GET "${API_BASE_URL}/v1/properties/${PROPERTY_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}")
property_name=$(echo $read_response | jq -r '.name')
echo "Property name: ${property_name}"
if [ "$property_name" == "CRUD Test Property" ]; then
  echo -e "${GREEN}✓ READ successful${NC}"
else
  echo -e "${RED}✗ READ failed${NC}"
fi
# UPDATE (PATCH)
echo -e "\n${YELLOW}UPDATE: Patching property${NC}"
update_response=$(curl -s -X PATCH "${API_BASE_URL}/v1/properties/${PROPERTY_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "CRUD Test Property - Updated",
    "amenities": ["wifi", "parking", "pool"]
  }')
updated_name=$(echo $update_response | jq -r '.name')
updated_amenities=$(echo $update_response | jq -r '.amenities | length')
echo "Updated name: ${updated_name}"
echo "Amenities count: ${updated_amenities}"
if [ "$updated_name" == "CRUD Test Property - Updated" ] && [ "$updated_amenities" == "3" ]; then
  echo -e "${GREEN}✓ UPDATE successful${NC}"
else
  echo -e "${RED}✗ UPDATE failed${NC}"
fi
# LIST
echo -e "\n${YELLOW}LIST: Fetching properties list${NC}"
list_response=$(curl -s -X GET "${API_BASE_URL}/v1/properties?limit=5" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}")
properties_count=$(echo $list_response | jq -r '.data | length')
echo "Properties returned: ${properties_count}"
if [ "$properties_count" -gt 0 ]; then
  echo -e "${GREEN}✓ LIST successful${NC}"
else
  echo -e "${RED}✗ LIST failed${NC}"
fi
# DELETE (Soft)
echo -e "\n${YELLOW}DELETE: Soft deleting property${NC}"
delete_response=$(curl -s -X DELETE "${API_BASE_URL}/v1/properties/${PROPERTY_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}")
deleted_status=$(echo $delete_response | jq -r '.deleted')
if [ "$deleted_status" == "true" ]; then
  echo -e "${GREEN}✓ DELETE successful${NC}"
else
  echo -e "${RED}✗ DELETE failed${NC}"
fi
# Verify soft delete
echo -e "\n${YELLOW}Verifying soft delete${NC}"
verify_response=$(curl -s -w "\n%{http_code}" -X GET "${API_BASE_URL}/v1/properties/${PROPERTY_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}")
verify_code=$(echo "$verify_response" | tail -n1)
if [ "$verify_code" == "404" ]; then
  echo -e "${GREEN}✓ Property not accessible after deletion${NC}"
else
  echo -e "${RED}✗ Property still accessible${NC}"
fi
echo -e "\n${GREEN}=== CRUD Test Complete ===${NC}"
load-test-availability.sh
#!/bin/bash
# load-test-availability.sh - Load test availability endpoint
echo "=== Availability Endpoint Load Test ==="
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
source .env.test
source .tokens
# Configuration
CONCURRENT_USERS=10
REQUESTS_PER_USER=50
TOTAL_REQUESTS=$((CONCURRENT_USERS * REQUESTS_PER_USER))
echo "Configuration:"
echo "  Concurrent users: ${CONCURRENT_USERS}"
echo "  Requests per user: ${REQUESTS_PER_USER}"
echo "  Total requests: ${TOTAL_REQUESTS}"
# Prepare test data
PROPERTY_IDS=("prop_123" "prop_456" "prop_789")
CHECK_IN_DATES=("2025-11-01" "2025-11-15" "2025-12-01")
# Results file
RESULTS_FILE="/tmp/load_test_results.txt"
> $RESULTS_FILE
# Function to make availability request
test_availability() {
  local user_id=$1
  local request_num=$2
  # Rotate through test data
  local prop_idx=$((RANDOM % 3))
  local date_idx=$((RANDOM % 3))
  local property_id=${PROPERTY_IDS[$prop_idx]}
  local check_in=${CHECK_IN_DATES[$date_idx]}
  local start_time=$(date +%s%N)
  response=$(curl -s -w "\n%{http_code}\n%{time_total}" \
    -X GET "${API_BASE_URL}/v1/availability" \
    -H "Authorization: Bearer ${ACCESS_TOKEN}" \
    -G \
    --data-urlencode "property_id=${property_id}" \
    --data-urlencode "check_in_date=${check_in}" \
    --data-urlencode "check_out_date=2025-11-05")
  local http_code=$(echo "$response" | tail -n2 | head -n1)
  local time_total=$(echo "$response" | tail -n1)
  echo "${user_id},${request_num},${http_code},${time_total}" >> $RESULTS_FILE
}
# Run load test
echo -e "\n${YELLOW}Starting load test...${NC}"
start_time=$(date +%s)
for user in $(seq 1 $CONCURRENT_USERS); do
  (
    for req in $(seq 1 $REQUESTS_PER_USER); do
      test_availability $user $req
    done
  ) &
done
# Wait for all background jobs
wait
end_time=$(date +%s)
duration=$((end_time - start_time))
# Analyze results
echo -e "\n${YELLOW}Analyzing results...${NC}"
total_requests=$(wc -l < $RESULTS_FILE)
successful=$(awk -F',' '$3 == 200' $RESULTS_FILE | wc -l)
failed=$(awk -F',' '$3 != 200' $RESULTS_FILE | wc -l)
avg_response_time=$(awk -F',' '{sum+=$4; count++} END {print sum/count}' $RESULTS_FILE)
min_response_time=$(awk -F',' '{print $4}' $RESULTS_FILE | sort -n | head -n1)
max_response_time=$(awk -F',' '{print $4}' $RESULTS_FILE | sort -n | tail -n1)
# Calculate percentiles
p50=$(awk -F',' '{print $4}' $RESULTS_FILE | sort -n | awk '{a[NR]=$1} END {print a[int(NR*0.5)]}')
p95=$(awk -F',' '{print $4}' $RESULTS_FILE | sort -n | awk '{a[NR]=$1} END {print a[int(NR*0.95)]}')
p99=$(awk -F',' '{print $4}' $RESULTS_FILE | sort -n | awk '{a[NR]=$1} END {print a[int(NR*0.99)]}')
requests_per_second=$(echo "scale=2; $total_requests / $duration" | bc)
# Print results
echo -e "\n${GREEN}=== Load Test Results ===${NC}"
echo "Duration: ${duration} seconds"
echo "Total requests: ${total_requests}"
echo "Successful: ${successful}"
echo "Failed: ${failed}"
echo "Success rate: $(echo "scale=2; $successful * 100 / $total_requests" | bc)%"
echo ""
echo "Response Times (seconds):"
echo "  Average: ${avg_response_time}"
echo "  Min: ${min_response_time}"
echo "  Max: ${max_response_time}"
echo "  P50: ${p50}"
echo "  P95: ${p95}"
echo "  P99: ${p99}"
echo ""
echo "Throughput: ${requests_per_second} req/s"
# Check error codes
echo -e "\n${YELLOW}Error Distribution:${NC}"
awk -F',' '$3 != 200 {print $3}' $RESULTS_FILE | sort | uniq -c
# Clean up
rm $RESULTS_FILE
echo -e "\n${GREEN}=== Load Test Complete ===${NC}"
test-idempotency.sh
#!/bin/bash
# test-idempotency.sh - Comprehensive idempotency testing
set -e
echo "=== Idempotency Test Suite ==="
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
source .env.test
source .tokens
# Test 1: Same idempotency key returns same result
echo -e "\n${YELLOW}Test 1: Duplicate Request Detection${NC}"
IDEM_KEY="test-duplicate-$(uuidgen)"
response1=$(curl -s -X POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: ${IDEM_KEY}" \
  -d '{
    "name": "Idempotency Test 1",
    "property_type": "apartment"
  }')
property_id1=$(echo $response1 | jq -r '.id')
# Wait a moment
sleep 1
response2=$(curl -s -X POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: ${IDEM_KEY}" \
  -d '{
    "name": "Different Name",
    "property_type": "villa"
  }')
property_id2=$(echo $response2 | jq -r '.id')
if [ "$property_id1" == "$property_id2" ]; then
  echo -e "${GREEN}✓ Duplicate request returned cached response${NC}"
else
  echo -e "${RED}✗ Different IDs returned: ${property_id1} vs ${property_id2}${NC}"
fi
# Test 2: Different keys create different resources
echo -e "\n${YELLOW}Test 2: Different Keys Different Resources${NC}"
KEY1="test-key1-$(uuidgen)"
KEY2="test-key2-$(uuidgen)"
resp1=$(curl -s -X POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: ${KEY1}" \
  -d '{"name": "Property A", "property_type": "apartment"}')
resp2=$(curl -s -X POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: ${KEY2}" \
  -d '{"name": "Property B", "property_type": "apartment"}')
id1=$(echo $resp1 | jq -r '.id')
id2=$(echo $resp2 | jq -r '.id')
if [ "$id1" != "$id2" ]; then
  echo -e "${GREEN}✓ Different keys created different resources${NC}"
else
  echo -e "${RED}✗ Same ID returned for different keys${NC}"
fi
# Test 3: Idempotency key expiration
echo -e "\n${YELLOW}Test 3: Idempotency Key Expiration${NC}"
EXPIRE_KEY="test-expire-$(uuidgen)"
# First request
first=$(curl -s -X POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: ${EXPIRE_KEY}" \
  -d '{"name": "Expire Test", "property_type": "apartment"}')
first_id=$(echo $first | jq -r '.id')
# Wait for expiration (typically 24 hours, but may be shorter in test)
echo "Waiting for key expiration (5 seconds in test environment)..."
sleep 5
# Request with expired key should create new resource
expired=$(curl -s -X POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: ${EXPIRE_KEY}" \
  -d '{"name": "Expire Test 2", "property_type": "apartment"}')
expired_id=$(echo $expired | jq -r '.id')
if [ "$first_id" != "$expired_id" ]; then
  echo -e "${GREEN}✓ Expired key allowed new resource creation${NC}"
else
  echo -e "${YELLOW}⚠ Key may not have expired (or cache still valid)${NC}"
fi
# Test 4: Idempotency with different HTTP methods
echo -e "\n${YELLOW}Test 4: Idempotency with PATCH${NC}"
PATCH_KEY="test-patch-$(uuidgen)"
# Create a property first
create=$(curl -s -X POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"name": "Patch Test", "property_type": "apartment"}')
patch_prop_id=$(echo $create | jq -r '.id')
# First PATCH
patch1=$(curl -s -X PATCH "${API_BASE_URL}/v1/properties/${patch_prop_id}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: ${PATCH_KEY}" \
  -d '{"name": "Patched Name"}')
# Duplicate PATCH
patch2=$(curl -s -X PATCH "${API_BASE_URL}/v1/properties/${patch_prop_id}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: ${PATCH_KEY}" \
  -d '{"name": "Different Patched Name"}')
name1=$(echo $patch1 | jq -r '.name')
name2=$(echo $patch2 | jq -r '.name')
if [ "$name1" == "$name2" ]; then
  echo -e "${GREEN}✓ PATCH idempotency working${NC}"
else
  echo -e "${RED}✗ PATCH not idempotent${NC}"
fi
# Test 5: No idempotency key behavior
echo -e "\n${YELLOW}Test 5: Missing Idempotency Key${NC}"
no_key1=$(curl -s -X POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"name": "No Key Test", "property_type": "apartment"}')
no_key2=$(curl -s -X POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"name": "No Key Test", "property_type": "apartment"}')
id_no1=$(echo $no_key1 | jq -r '.id')
id_no2=$(echo $no_key2 | jq -r '.id')
if [ "$id_no1" != "$id_no2" ]; then
  echo -e "${GREEN}✓ Without key, duplicate resources created${NC}"
else
  echo -e "${RED}✗ Same resource returned without key${NC}"
fi
echo -e "\n${GREEN}=== Idempotency Tests Complete ===${NC}"
# Cleanup
echo -e "\n${YELLOW}Cleaning up test resources...${NC}"
curl -s -X DELETE "${API_BASE_URL}/v1/properties/${property_id1}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" > /dev/null
curl -s -X DELETE "${API_BASE_URL}/v1/properties/${id1}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" > /dev/null
curl -s -X DELETE "${API_BASE_URL}/v1/properties/${id2}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" > /dev/null
echo -e "${GREEN}Cleanup complete${NC}"
6. Integration Testing
Newman (Postman CLI)
Export your Postman collection and run tests:
# Install Newman
npm install -g newman
# Run collection
newman run tvl-api-collection.json \
  --environment tvl-test-env.json \
  --reporters cli,json,html \
  --reporter-html-export /tmp/newman-report.html
# Run with specific folder
newman run tvl-api-collection.json \
  --folder "Booking Tests" \
  --environment tvl-test-env.json
# Run with data file (data-driven testing)
newman run tvl-api-collection.json \
  --iteration-data test-data.csv \
  --environment tvl-test-env.json
# Example test data CSV
# property_id,check_in,check_out,adults
# prop_123,2025-11-01,2025-11-05,2
# prop_456,2025-11-15,2025-11-20,4
Sample Postman test script:
// Test script in Postman
pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});
pm.test("Response time is less than 500ms", function () {
    pm.expect(pm.response.responseTime).to.be.below(500);
});
pm.test("Response has required fields", function () {
    const jsonData = pm.response.json();
    pm.expect(jsonData).to.have.property('id');
    pm.expect(jsonData).to.have.property('name');
});
pm.test("Property ID is valid UUID", function () {
    const jsonData = pm.response.json();
    const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
    pm.expect(jsonData.id).to.match(uuidRegex);
});
// Save to environment
pm.environment.set("property_id", pm.response.json().id);
HTTPie Examples
HTTPie provides a more user-friendly alternative to curl:
# Install HTTPie
pip install httpie
# Basic GET request
http GET ${API_BASE_URL}/v1/properties \
  "Authorization: Bearer ${ACCESS_TOKEN}"
# POST with JSON
http POST ${API_BASE_URL}/v1/properties \
  "Authorization: Bearer ${ACCESS_TOKEN}" \
  name="Test Property" \
  property_type="apartment"
# POST with JSON file
http POST ${API_BASE_URL}/v1/properties \
  "Authorization: Bearer ${ACCESS_TOKEN}" \
  < property-data.json
# Query parameters
http GET ${API_BASE_URL}/v1/properties \
  "Authorization: Bearer ${ACCESS_TOKEN}" \
  city=="Malibu" \
  property_type=="villa"
# Download response
http --download ${API_BASE_URL}/v1/properties/${PROPERTY_ID}/export \
  "Authorization: Bearer ${ACCESS_TOKEN}"
# Session support (saves headers)
http --session=tvl POST ${API_BASE_URL}/v1/auth/login \
  email=test@example.com password=test123
http --session=tvl GET ${API_BASE_URL}/v1/properties
# Pretty print JSON
http ${API_BASE_URL}/v1/properties | jq '.'
Insomnia Examples
Insomnia CLI for automation:
# Install Insomnia CLI
npm install -g insomnia-inso
# Run tests
inso run test "TVL API Tests" \
  --env "Test Environment"
# Export collection
inso export spec "TVL API" \
  --output tvl-api-spec.yaml
# Lint API spec
inso lint spec tvl-api-spec.yaml
# Run specific test suite
inso run test "Booking Flow" \
  --env "Test Environment" \
  --reporter json \
  --reporter-file results.json
7. Performance Testing
Apache Bench (ab)
Simple load testing tool:
# Install Apache Bench
# Ubuntu/Debian
sudo apt-get install apache2-utils
# macOS (included with Apache)
# which ab
# Basic load test
ab -n 1000 -c 10 \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  "${API_BASE_URL}/v1/properties"
# With POST data
ab -n 100 -c 5 \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -p property-data.json \
  "${API_BASE_URL}/v1/properties"
# Save results
ab -n 1000 -c 10 \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -g results.tsv \
  "${API_BASE_URL}/v1/properties"
# Analyze with gnuplot
gnuplot <<EOF
set terminal png
set output "performance.png"
set title "API Performance"
set xlabel "Request"
set ylabel "Response Time (ms)"
plot "results.tsv" using 5 with lines title "Response Time"
EOF
k6 Load Testing
Modern load testing tool with JavaScript DSL:
// load-test.js - k6 load testing script
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
// Custom metrics
const errorRate = new Rate('errors');
// Test configuration
export const options = {
  stages: [
    { duration: '30s', target: 10 },  // Ramp up to 10 users
    { duration: '1m', target: 50 },   // Stay at 50 users
    { duration: '30s', target: 100 }, // Spike to 100 users
    { duration: '1m', target: 50 },   // Back to 50 users
    { duration: '30s', target: 0 },   // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
    http_req_failed: ['rate<0.01'],   // Error rate under 1%
    errors: ['rate<0.1'],             // Custom error rate under 10%
  },
};
// Test setup
export function setup() {
  // Authenticate and get token
  const loginRes = http.post(`${__ENV.API_BASE_URL}/v1/auth/login`, {
    email: __ENV.TEST_USER_EMAIL,
    password: __ENV.TEST_USER_PASSWORD,
  });
  return { token: loginRes.json('access_token') };
}
// Main test function
export default function(data) {
  const headers = {
    'Authorization': `Bearer ${data.token}`,
    'Content-Type': 'application/json',
  };
  // Test 1: List properties
  const listRes = http.get(
    `${__ENV.API_BASE_URL}/v1/properties?limit=20`,
    { headers }
  );
  check(listRes, {
    'list status is 200': (r) => r.status === 200,
    'list has data': (r) => r.json('data').length > 0,
  }) || errorRate.add(1);
  sleep(1);
  // Test 2: Get property details
  const propertyId = listRes.json('data.0.id');
  const detailRes = http.get(
    `${__ENV.API_BASE_URL}/v1/properties/${propertyId}`,
    { headers }
  );
  check(detailRes, {
    'detail status is 200': (r) => r.status === 200,
    'detail has id': (r) => r.json('id') !== undefined,
  }) || errorRate.add(1);
  sleep(1);
  // Test 3: Check availability
  const availRes = http.get(
    `${__ENV.API_BASE_URL}/v1/availability?property_id=${propertyId}&check_in_date=2025-11-01&check_out_date=2025-11-05`,
    { headers }
  );
  check(availRes, {
    'availability status is 200': (r) => r.status === 200,
  }) || errorRate.add(1);
  sleep(2);
}
// Test teardown
export function teardown(data) {
  // Cleanup if needed
}
Run k6 tests:
# Install k6
# Ubuntu/Debian
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
# macOS
brew install k6
# Run test
k6 run load-test.js
# With environment variables
k6 run -e API_BASE_URL=https://api.tvl.com \
       -e TEST_USER_EMAIL=test@example.com \
       load-test.js
# Output to InfluxDB
k6 run --out influxdb=http://localhost:8086/k6 load-test.js
# Cloud execution
k6 cloud load-test.js
Artillery Load Testing
Artillery configuration:
# artillery-config.yml
config:
  target: "https://api.tvl.com"
  phases:
    - duration: 60
      arrivalRate: 10
      name: "Warm up"
    - duration: 120
      arrivalRate: 50
      name: "Sustained load"
    - duration: 60
      arrivalRate: 100
      name: "Spike test"
  variables:
    api_base_url: "https://api.tvl.com"
  processor: "./auth-processor.js"
scenarios:
  - name: "Property browsing flow"
    weight: 70
    flow:
      - get:
          url: "/v1/properties"
          headers:
            Authorization: "Bearer {{ $processEnvironment.ACCESS_TOKEN }}"
          capture:
            - json: "$.data[0].id"
              as: "propertyId"
      - get:
          url: "/v1/properties/{{ propertyId }}"
          headers:
            Authorization: "Bearer {{ $processEnvironment.ACCESS_TOKEN }}"
      - think: 2
      - get:
          url: "/v1/availability?property_id={{ propertyId }}&check_in_date=2025-11-01&check_out_date=2025-11-05"
          headers:
            Authorization: "Bearer {{ $processEnvironment.ACCESS_TOKEN }}"
  - name: "Booking flow"
    weight: 30
    flow:
      - post:
          url: "/v1/bookings"
          headers:
            Authorization: "Bearer {{ $processEnvironment.ACCESS_TOKEN }}"
            Content-Type: "application/json"
            X-Idempotency-Key: "test-{{ $randomString() }}"
          json:
            property_id: "{{ propertyId }}"
            check_in_date: "2025-11-01"
            check_out_date: "2025-11-05"
          capture:
            - json: "$.id"
              as: "bookingId"
      - get:
          url: "/v1/bookings/{{ bookingId }}"
          headers:
            Authorization: "Bearer {{ $processEnvironment.ACCESS_TOKEN }}"
Run Artillery:
# Install Artillery
npm install -g artillery
# Run test
artillery run artillery-config.yml
# Quick test
artillery quick --count 10 --num 50 \
  https://api.tvl.com/v1/properties
# With report
artillery run artillery-config.yml \
  --output report.json
artillery report report.json \
  --output report.html
8. Webhook Testing
ngrok Setup for Local Testing
Test webhooks locally with ngrok:
# Install ngrok
# Download from https://ngrok.com/download
# Start local webhook server
python3 -m http.server 8080 &
# Create ngrok tunnel
ngrok http 8080
# Copy the forwarding URL (e.g., https://abc123.ngrok.io)
# Configure webhook in TVL
curl -X POST "${API_BASE_URL}/v1/webhooks" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://abc123.ngrok.io/webhook",
    "events": ["booking.created", "booking.updated", "booking.cancelled"],
    "secret": "webhook_secret_key"
  }'
# Simple webhook receiver (save as webhook-receiver.py)
# python3 webhook-receiver.py
Webhook receiver script:
#!/usr/bin/env python3
# webhook-receiver.py - Simple webhook receiver
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import hmac
import hashlib
WEBHOOK_SECRET = "webhook_secret_key"
class WebhookHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        content_length = int(self.headers['Content-Length'])
        body = self.rfile.read(content_length)
        # Verify signature
        signature = self.headers.get('X-TVL-Signature')
        expected = hmac.new(
            WEBHOOK_SECRET.encode(),
            body,
            hashlib.sha256
        ).hexdigest()
        if signature != expected:
            self.send_response(401)
            self.end_headers()
            return
        # Process webhook
        data = json.loads(body)
        print(f"Received webhook: {data['event']}")
        print(json.dumps(data, indent=2))
        # Respond
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(json.dumps({"status": "received"}).encode())
if __name__ == '__main__':
    server = HTTPServer(('localhost', 8080), WebhookHandler)
    print("Webhook receiver running on http://localhost:8080")
    server.serve_forever()
Webhook.site Testing
Use webhook.site for quick testing:
# Get a unique webhook URL from https://webhook.site
WEBHOOK_URL="https://webhook.site/unique-id"
# Register webhook
curl -X POST "${API_BASE_URL}/v1/webhooks" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "'${WEBHOOK_URL}'",
    "events": ["booking.created"],
    "secret": "test_secret"
  }'
# Trigger event
curl -X POST "${API_BASE_URL}/v1/bookings" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "property_id": "'${TEST_PROPERTY_ID}'",
    "check_in_date": "2025-11-01",
    "check_out_date": "2025-11-05"
  }'
# Check webhook.site for received payload
Hostaway Webhook Simulation
Simulate incoming webhooks from Hostaway:
#!/bin/bash
# simulate-hostaway-webhook.sh
# Simulate booking created webhook
curl -X POST "http://localhost:3000/webhooks/hostaway" \
  -H "Content-Type: application/json" \
  -H "X-Hostaway-Signature: $(echo -n '{"event":"booking.created"}' | openssl dgst -sha256 -hmac 'hostaway_secret' | cut -d' ' -f2)" \
  -d '{
    "event": "booking.created",
    "timestamp": "2025-10-24T12:00:00Z",
    "data": {
      "id": "12345",
      "listingId": "67890",
      "guestName": "John Doe",
      "checkIn": "2025-11-01",
      "checkOut": "2025-11-05",
      "status": "confirmed",
      "totalPrice": 1200.00
    }
  }'
# Simulate booking cancelled webhook
curl -X POST "http://localhost:3000/webhooks/hostaway" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "booking.cancelled",
    "timestamp": "2025-10-24T13:00:00Z",
    "data": {
      "id": "12345",
      "reason": "guest_request",
      "cancelledAt": "2025-10-24T13:00:00Z"
    }
  }'
9. Common Testing Patterns
Testing with Fixtures
#!/bin/bash
# Create test fixtures
# fixtures/property.json
cat > /tmp/fixture-property.json <<'EOF'
{
  "name": "Test Property",
  "property_type": "villa",
  "address": {
    "street": "123 Test St",
    "city": "Test City",
    "state": "CA",
    "postal_code": "90001",
    "country": "US"
  },
  "amenities": ["wifi", "pool"]
}
EOF
# Use fixture
curl -X POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d @/tmp/fixture-property.json
Cleaning Up Test Data
#!/bin/bash
# cleanup-test-data.sh - Clean up all test resources
echo "Cleaning up test data..."
source .env.test
source .tokens
# Delete test properties
test_properties=$(curl -s -X GET "${API_BASE_URL}/v1/properties?name_contains=Test" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" | jq -r '.data[].id')
for prop_id in $test_properties; do
  echo "Deleting property: ${prop_id}"
  curl -s -X DELETE "${API_BASE_URL}/v1/properties/${prop_id}" \
    -H "Authorization: Bearer ${ACCESS_TOKEN}" > /dev/null
done
# Delete test bookings
test_bookings=$(curl -s -X GET "${API_BASE_URL}/v1/bookings?guest_email=test@example.com" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" | jq -r '.data[].id')
for booking_id in $test_bookings; do
  echo "Cancelling booking: ${booking_id}"
  curl -s -X DELETE "${API_BASE_URL}/v1/bookings/${booking_id}" \
    -H "Authorization: Bearer ${ACCESS_TOKEN}" > /dev/null
done
echo "Cleanup complete"
Assertion Patterns
#!/bin/bash
# assert.sh - Assertion helper functions
assert_equals() {
  if [ "$1" == "$2" ]; then
    echo "✓ Assertion passed: $1 == $2"
    return 0
  else
    echo "✗ Assertion failed: expected $2, got $1"
    return 1
  fi
}
assert_not_null() {
  if [ "$1" != "null" ] && [ -n "$1" ]; then
    echo "✓ Assertion passed: value is not null"
    return 0
  else
    echo "✗ Assertion failed: value is null"
    return 1
  fi
}
assert_contains() {
  if [[ "$1" == *"$2"* ]]; then
    echo "✓ Assertion passed: '$1' contains '$2'"
    return 0
  else
    echo "✗ Assertion failed: '$1' does not contain '$2'"
    return 1
  fi
}
assert_status() {
  if [ "$1" == "$2" ]; then
    echo "✓ HTTP status assertion passed: $1"
    return 0
  else
    echo "✗ HTTP status assertion failed: expected $2, got $1"
    return 1
  fi
}
# Usage example
response=$(curl -s -w "\n%{http_code}" -X GET "${API_BASE_URL}/v1/properties/${PROPERTY_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | head -n -1)
property_name=$(echo $body | jq -r '.name')
assert_status $http_code 200
assert_not_null "$property_name"
assert_contains "$property_name" "Villa"
Debugging Failed Requests
#!/bin/bash
# debug-request.sh - Detailed request debugging
DEBUG=true
VERBOSE=true
debug_request() {
  local method=$1
  local url=$2
  shift 2
  echo "=== REQUEST DEBUG ==="
  echo "Method: ${method}"
  echo "URL: ${url}"
  echo "Headers:"
  # Make request with verbose output
  response=$(curl -v -X ${method} "${url}" "$@" 2>&1)
  echo "$response"
  echo "=== END DEBUG ==="
}
# Usage
debug_request POST "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"name":"Debug Test"}'
# Or save to file
curl -v -X GET "${API_BASE_URL}/v1/properties" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  2>&1 | tee debug.log
10. CI/CD Integration
GitHub Actions Example
# .github/workflows/api-tests.yml
name: API Tests
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 */6 * * *'  # Every 6 hours
jobs:
  api-tests:
    runs-on: ubuntu-latest
    env:
      API_BASE_URL: ${{ secrets.API_BASE_URL }}
      ACCESS_TOKEN: ${{ secrets.API_ACCESS_TOKEN }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      - name: Install dependencies
        run: |
          npm install -g newman
          sudo apt-get install -y jq curl
      - name: Run authentication tests
        run: |
          chmod +x ./scripts/test-auth.sh
          ./scripts/test-auth.sh
      - name: Run CRUD tests
        run: |
          chmod +x ./scripts/test-property-crud.sh
          ./scripts/test-property-crud.sh
      - name: Run booking flow tests
        run: |
          chmod +x ./scripts/test-booking-flow.sh
          ./scripts/test-booking-flow.sh
      - name: Run Newman tests
        run: |
          newman run postman-collection.json \
            --environment postman-environment.json \
            --reporters cli,json \
            --reporter-json-export results.json
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: results.json
      - name: Cleanup test data
        if: always()
        run: |
          chmod +x ./scripts/cleanup-test-data.sh
          ./scripts/cleanup-test-data.sh
      - name: Notify on failure
        if: failure()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: 'API tests failed!'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}
GitLab CI Example
# .gitlab-ci.yml
stages:
  - test
  - report
variables:
  API_BASE_URL: $API_BASE_URL
  ACCESS_TOKEN: $API_ACCESS_TOKEN
before_script:
  - apt-get update -qq
  - apt-get install -y curl jq
api_tests:
  stage: test
  image: node:18
  script:
    - npm install -g newman
    - chmod +x ./scripts/*.sh
    - ./scripts/test-auth.sh
    - ./scripts/test-property-crud.sh
    - ./scripts/test-booking-flow.sh
    - newman run postman-collection.json --environment postman-environment.json
  after_script:
    - ./scripts/cleanup-test-data.sh
  artifacts:
    when: always
    paths:
      - results.json
      - logs/
    expire_in: 1 week
  only:
    - main
    - develop
    - merge_requests
load_tests:
  stage: test
  image: loadimpact/k6:latest
  script:
    - k6 run load-test.js
  only:
    - schedules
  artifacts:
    paths:
      - load-test-results.json
generate_report:
  stage: report
  image: python:3.9
  script:
    - pip install pytest-html
    - python generate-report.py
  artifacts:
    paths:
      - report.html
  only:
    - main
Running Tests in Docker
# Dockerfile.test
FROM node:18-alpine
# Install dependencies
RUN apk add --no-cache \
    curl \
    jq \
    bash \
    openssl
# Install testing tools
RUN npm install -g \
    newman \
    artillery
# Copy test scripts
WORKDIR /tests
COPY ./scripts ./scripts
COPY ./postman ./postman
COPY .env.test .env.test
# Make scripts executable
RUN chmod +x ./scripts/*.sh
# Default command
CMD ["./scripts/test-all.sh"]
Build and run:
# Build test image
docker build -f Dockerfile.test -t tvl-api-tests .
# Run tests
docker run --rm \
  -e API_BASE_URL=${API_BASE_URL} \
  -e ACCESS_TOKEN=${ACCESS_TOKEN} \
  tvl-api-tests
# Run specific test
docker run --rm \
  -e API_BASE_URL=${API_BASE_URL} \
  -e ACCESS_TOKEN=${ACCESS_TOKEN} \
  tvl-api-tests \
  ./scripts/test-booking-flow.sh
# Run with volume for results
docker run --rm \
  -v $(pwd)/results:/tests/results \
  -e API_BASE_URL=${API_BASE_URL} \
  -e ACCESS_TOKEN=${ACCESS_TOKEN} \
  tvl-api-tests
Docker Compose for test environment:
# docker-compose.test.yml
version: '3.8'
services:
  api-tests:
    build:
      context: .
      dockerfile: Dockerfile.test
    environment:
      - API_BASE_URL=${API_BASE_URL}
      - ACCESS_TOKEN=${ACCESS_TOKEN}
    volumes:
      - ./results:/tests/results
      - ./logs:/tests/logs
    command: ./scripts/test-all.sh
  load-tests:
    image: loadimpact/k6:latest
    volumes:
      - ./load-test.js:/load-test.js
    environment:
      - API_BASE_URL=${API_BASE_URL}
      - ACCESS_TOKEN=${ACCESS_TOKEN}
    command: run /load-test.js
Run with Docker Compose:
# Run all tests
docker-compose -f docker-compose.test.yml up --abort-on-container-exit
# Run specific service
docker-compose -f docker-compose.test.yml up api-tests
# Clean up
docker-compose -f docker-compose.test.yml down -v
Validation Checklist
Before deploying to production, verify:
- All authentication flows tested
- CRUD operations work for all entities
- Pagination tested with various limits
- Rate limiting properly enforced
- Idempotency keys working correctly
- Error responses match specification
- Webhook signatures validated
- Performance metrics within acceptable ranges
- Concurrent requests handled properly
- CI/CD pipeline runs tests successfully
Related Documentation
- API Reference - Complete API endpoint documentation
- Authentication Guide - Authentication and authorization details
- Rate Limiting - Rate limiting policies and headers
- Error Handling - Error codes and responses
- Webhooks - Webhook integration guide
Last Updated: 2025-10-24 Maintained By: API Testing Team Version: 1.0.0