veza/tools/tests/http/http_assert.sh
2025-12-03 22:56:50 +01:00

399 lines
No EOL
10 KiB
Bash

#!/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<b)}'; 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
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