#!/bin/bash # HTTP Test Assertions Library # Provides reusable assertion functions for HTTP testing set -euo pipefail # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Test counters TESTS_RUN=0 TESTS_PASSED=0 TESTS_FAILED=0 # Log functions log_info() { echo -e "${GREEN}[INFO]${NC} $*" >&2 } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2 } log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2 } # Core assertion failure handler fail() { local message="$1" local context="${2:-}" TESTS_FAILED=$((TESTS_FAILED + 1)) log_error "ASSERTION FAILED: $message" if [ -n "$context" ]; then log_error "Context: $context" fi # Don't exit immediately, let the test continue return 1 } pass() { local message="$1" TESTS_PASSED=$((TESTS_PASSED + 1)) if [ "${VERBOSE:-false}" = "true" ]; then log_info "✓ $message" fi } # HTTP Status assertions assert_status() { local actual="$1" local expected="$2" local context="${3:-HTTP request}" TESTS_RUN=$((TESTS_RUN + 1)) if [ "$actual" -eq "$expected" ]; then pass "Status code $expected for $context" return 0 else fail "Expected status $expected but got $actual" "$context" return 1 fi } assert_status_range() { local actual="$1" local min="$2" local max="$3" local context="${4:-HTTP request}" TESTS_RUN=$((TESTS_RUN + 1)) if [ "$actual" -ge "$min" ] && [ "$actual" -le "$max" ]; then pass "Status code $actual in range [$min-$max] for $context" return 0 else fail "Expected status in range [$min-$max] but got $actual" "$context" return 1 fi } # JSON assertions assert_json_has() { local json="$1" local path="$2" local context="${3:-JSON response}" TESTS_RUN=$((TESTS_RUN + 1)) if echo "$json" | jq -e "$path" >/dev/null 2>&1; then pass "JSON has path '$path' in $context" return 0 else fail "JSON missing path: $path" "$context" return 1 fi } assert_json_equals() { local json="$1" local path="$2" local expected="$3" local context="${4:-JSON response}" TESTS_RUN=$((TESTS_RUN + 1)) local actual actual=$(echo "$json" | jq -r "$path" 2>/dev/null || echo "") if [ "$actual" = "$expected" ]; then pass "JSON path '$path' equals '$expected' in $context" return 0 else fail "JSON path '$path' expected '$expected' but got '$actual'" "$context" return 1 fi } assert_json_type() { local json="$1" local path="$2" local expected_type="$3" local context="${4:-JSON response}" TESTS_RUN=$((TESTS_RUN + 1)) local actual_type actual_type=$(echo "$json" | jq -r "$path | type" 2>/dev/null || echo "null") if [ "$actual_type" = "$expected_type" ]; then pass "JSON path '$path' has type '$expected_type' in $context" return 0 else fail "JSON path '$path' expected type '$expected_type' but got '$actual_type'" "$context" return 1 fi } assert_json_not_empty() { local json="$1" local path="$2" local context="${3:-JSON response}" TESTS_RUN=$((TESTS_RUN + 1)) local value value=$(echo "$json" | jq -r "$path" 2>/dev/null || echo "null") if [ "$value" != "null" ] && [ "$value" != "" ] && [ "$value" != "[]" ] && [ "$value" != "{}" ]; then pass "JSON path '$path' is not empty in $context" return 0 else fail "JSON path '$path' is empty or null" "$context" return 1 fi } # Header assertions assert_header_exists() { local headers="$1" local header_name="$2" local context="${3:-HTTP response}" TESTS_RUN=$((TESTS_RUN + 1)) if echo "$headers" | grep -qi "^$header_name:"; then pass "Header '$header_name' exists in $context" return 0 else fail "Header '$header_name' missing" "$context" return 1 fi } assert_header_contains() { local headers="$1" local header_name="$2" local expected_value="$3" local context="${4:-HTTP response}" TESTS_RUN=$((TESTS_RUN + 1)) local header_line header_line=$(echo "$headers" | grep -i "^$header_name:" || echo "") if echo "$header_line" | grep -qi "$expected_value"; then pass "Header '$header_name' contains '$expected_value' in $context" return 0 else fail "Header '$header_name' does not contain '$expected_value'" "$context" return 1 fi } assert_header_equals() { local headers="$1" local header_name="$2" local expected_value="$3" local context="${4:-HTTP response}" TESTS_RUN=$((TESTS_RUN + 1)) local actual_value actual_value=$(echo "$headers" | grep -i "^$header_name:" | cut -d' ' -f2- | tr -d '\r\n' || echo "") if [ "$actual_value" = "$expected_value" ]; then pass "Header '$header_name' equals '$expected_value' in $context" return 0 else fail "Header '$header_name' expected '$expected_value' but got '$actual_value'" "$context" return 1 fi } # Performance assertions assert_latency_lt() { local actual_ms="$1" local threshold_ms="$2" local context="${3:-HTTP request}" TESTS_RUN=$((TESTS_RUN + 1)) # Use bc for floating point comparison if available, otherwise awk if command -v bc >/dev/null 2>&1; then if [ "$(echo "$actual_ms < $threshold_ms" | bc)" -eq 1 ]; then pass "Latency ${actual_ms}ms < ${threshold_ms}ms for $context" return 0 else fail "Latency ${actual_ms}ms >= ${threshold_ms}ms" "$context" return 1 fi else if awk -v a="$actual_ms" -v b="$threshold_ms" 'BEGIN{exit !(a= ${threshold_ms}ms" "$context" return 1 fi fi } # Content assertions assert_body_contains() { local body="$1" local expected="$2" local context="${3:-response body}" TESTS_RUN=$((TESTS_RUN + 1)) if echo "$body" | grep -q "$expected"; then pass "Body contains '$expected' in $context" return 0 else fail "Body does not contain '$expected'" "$context" return 1 fi } assert_body_not_contains() { local body="$1" local unexpected="$2" local context="${3:-response body}" TESTS_RUN=$((TESTS_RUN + 1)) if echo "$body" | grep -q "$unexpected"; then fail "Body contains unexpected '$unexpected'" "$context" return 1 else pass "Body does not contain '$unexpected' in $context" return 0 fi } # Security assertions assert_security_headers() { local headers="$1" local context="${2:-HTTP response}" local all_passed=true # Check for essential security headers local security_headers=( "X-Content-Type-Options" "X-Frame-Options" "Referrer-Policy" "Content-Security-Policy" ) for header in "${security_headers[@]}"; do if ! assert_header_exists "$headers" "$header" "$context"; then all_passed=false fi done if [ "$all_passed" = true ]; then return 0 else return 1 fi } # Cookie assertions assert_cookie_exists() { local headers="$1" local cookie_name="$2" local context="${3:-HTTP response}" TESTS_RUN=$((TESTS_RUN + 1)) if echo "$headers" | grep -i "^set-cookie:" | grep -q "$cookie_name="; then pass "Cookie '$cookie_name' exists in $context" return 0 else fail "Cookie '$cookie_name' not found" "$context" return 1 fi } assert_cookie_has_flag() { local headers="$1" local cookie_name="$2" local flag="$3" local context="${4:-HTTP response}" TESTS_RUN=$((TESTS_RUN + 1)) local cookie_line cookie_line=$(echo "$headers" | grep -i "^set-cookie:" | grep "$cookie_name=" || echo "") if echo "$cookie_line" | grep -qi "$flag"; then pass "Cookie '$cookie_name' has flag '$flag' in $context" return 0 else fail "Cookie '$cookie_name' missing flag '$flag'" "$context" return 1 fi } # Test summary print_test_summary() { echo echo "==========================================" echo "Test Summary" echo "==========================================" echo "Total tests run: $TESTS_RUN" echo -e "Passed: ${GREEN}$TESTS_PASSED${NC}" echo -e "Failed: ${RED}$TESTS_FAILED${NC}" echo "==========================================" if [ "$TESTS_FAILED" -gt 0 ]; then return 1 else return 0 fi } # Utility functions url_encode() { local string="$1" echo -n "$string" | jq -sRr @uri } generate_test_email() { local prefix="${TEST_EMAIL_PREFIX:-user+test}" local domain="${TEST_EMAIL_DOMAIN:-lab.veza}" local rand="${1:-$(date +%s)}" echo "${prefix}${rand}@${domain}" } generate_test_password() { local prefix="${TEST_PASSWORD_PREFIX:-V3za!lab-}" local rand="${1:-$(date +%s)}" echo "${prefix}${rand}" } # JSON Schema validation (if ajv available) assert_json_schema() { local json="$1" local schema_file="$2" local context="${3:-JSON response}" TESTS_RUN=$((TESTS_RUN + 1)) if ! command -v ajv >/dev/null 2>&1; then log_warn "ajv not installed, skipping schema validation" return 0 fi if echo "$json" | ajv validate -s "$schema_file" >/dev/null 2>&1; then pass "JSON validates against schema in $context" return 0 else fail "JSON schema validation failed" "$context" return 1 fi } # Export all functions export -f log_info log_warn log_error fail pass export -f assert_status assert_status_range export -f assert_json_has assert_json_equals assert_json_type assert_json_not_empty export -f assert_header_exists assert_header_contains assert_header_equals export -f assert_latency_lt export -f assert_body_contains assert_body_not_contains export -f assert_security_headers export -f assert_cookie_exists assert_cookie_has_flag export -f print_test_summary export -f url_encode generate_test_email generate_test_password export -f assert_json_schema