From 5d60f54f9c4dc009a994b42b47f96cfc4bad0cf0 Mon Sep 17 00:00:00 2001 From: colin Date: Wed, 27 Aug 2025 19:26:59 -0400 Subject: [PATCH] tests: harden shell tests; add header/CORS assertions; nonzero exits; bump to v0.4.1 --- VERSION | 2 +- docker-compose.yml | 2 +- test_api_curl.sh | 144 +++++++++++++------------------- test_api_headers.sh | 172 ++++++++++++-------------------------- test_headers.sh | 197 ++++++++++++++++++++++++-------------------- version_history.log | 1 + 6 files changed, 220 insertions(+), 298 deletions(-) diff --git a/VERSION b/VERSION index 1d0ba9e..267577d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.0 +0.4.1 diff --git a/docker-compose.yml b/docker-compose.yml index 24bfd4d..004daaa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,7 @@ services: - POSTGRES_DB=ploughshares - POSTGRES_USER=ploughshares - POSTGRES_PASSWORD=ploughshares_password - - APP_VERSION=0.4.0 + - APP_VERSION=0.4.1 - APP_ENV=development depends_on: db: diff --git a/test_api_curl.sh b/test_api_curl.sh index ccbce28..65d8947 100755 --- a/test_api_curl.sh +++ b/test_api_curl.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -Eeuo pipefail + # Colors for better readability GREEN='\033[0;32m' RED='\033[0;31m' @@ -7,6 +9,10 @@ YELLOW='\033[0;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color +FAILURES=0 +fail() { echo -e "${RED}✗ $1${NC}"; FAILURES=$((FAILURES+1)); } +pass() { echo -e "${GREEN}✓ $1${NC}"; } + # Base URLs to test DEV_URL="http://localhost:5005" PROD_URL="https://ploughshares.nixc.us" @@ -17,94 +23,58 @@ URL=${1:-$DEV_URL} echo -e "${BLUE}Testing API endpoints with curl for ${URL}${NC}" echo "==================================================" -# Function to test API endpoint with different curl options -test_api() { - local endpoint=$1 - local method=$2 - local description=$3 - local headers=$4 - local data=$5 - local full_url="${URL}${endpoint}" - - echo -e "\n${YELLOW}Test: ${description}${NC}" - echo "------------------------------------------------" - - # Build curl command based on parameters - cmd="curl -s" - - # Add method if specified - if [ -n "$method" ]; then - cmd="$cmd -X $method" - fi - - # Add headers if specified - if [ -n "$headers" ]; then - cmd="$cmd $headers" - fi - - # Add data if specified - if [ -n "$data" ]; then - cmd="$cmd -d '$data'" - fi - - # Add URL - cmd="$cmd \"$full_url\"" - - # Show the command being executed - echo -e "${BLUE}Command:${NC} $cmd" - - # Execute the command - echo -e "${BLUE}Response:${NC}" - eval $cmd | jq 2>/dev/null || echo "Response is not valid JSON" - - echo "------------------------------------------------" +assert_status_and_json_field() { + local method=$1; shift + local url=$1; shift + local data=${1:-}; shift || true + local headers=${1:-}; shift || true + local expected_status=${1:-200}; shift || true + local json_field=${1:-}; shift || true + + local tmp + tmp=$(mktemp) + local cmd="curl -s -o $tmp -w \"%{http_code}\" -X $method" + if [ -n "$headers" ]; then + cmd="$cmd $headers" + fi + if [ -n "$data" ]; then + cmd="$cmd -d '$data'" + fi + cmd="$cmd \"$url\"" + + echo -e "${BLUE}Command:${NC} $cmd" + # shellcheck disable=SC2086 + status=$(eval $cmd) + echo -e "${BLUE}Status:${NC} $status" + echo -e "${BLUE}Body:${NC}"; cat "$tmp" | jq 2>/dev/null || cat "$tmp" + + if [ "$status" != "$expected_status" ]; then + fail "$method $url expected $expected_status, got $status" + rm -f "$tmp" + return + fi + if [ -n "$json_field" ] && ! jq -e ".${json_field} // empty" >/dev/null 2>&1 <"$tmp"; then + fail "$method $url response missing field: $json_field" + rm -f "$tmp" + return + fi + pass "$method $url returned $expected_status${json_field:+ and contains $json_field}" + rm -f "$tmp" } -# Test 1: Basic GET request to API test endpoint -test_api "/api/test" "GET" "Basic GET request to API test endpoint" "" "" +# Tests that do not mutate state +assert_status_and_json_field GET "${URL}/api/test" "" "-H \"Accept: application/json\"" 200 status +assert_status_and_json_field OPTIONS "${URL}/api/test" "" "-H \"Origin: http://example.com\"" 200 -# Test 2: GET request with Accept header -test_api "/api/test" "GET" "GET request with Accept: application/json header" "-H \"Accept: application/json\"" "" +# Mutating tests guarded by env var +if [[ "${ALLOW_MUTATING_TESTS:-0}" == "1" ]]; then + assert_status_and_json_field POST "${URL}/api/transaction" '{"transaction_type":"test","company_division":"div","recipient":"Test Corp"}' "-H \"Content-Type: application/json\" -H \"Accept: application/json\"" 201 transaction_id +else + echo -e "${YELLOW}Skipping POST/PUT/DELETE tests (set ALLOW_MUTATING_TESTS=1 to enable).${NC}" +fi -# Test 3: OPTIONS request (CORS preflight) -test_api "/api/test" "OPTIONS" "OPTIONS request (CORS preflight)" "-H \"Origin: http://example.com\"" "" - -# Test 4: POST request with JSON data -test_api "/api/transactions" "POST" "POST request with JSON data" "-H \"Content-Type: application/json\"" "{\"test\": \"data\"}" - -# Test 5: GET request with custom User-Agent -test_api "/api/test" "GET" "GET request with custom User-Agent" "-H \"User-Agent: CustomClient/1.0\"" "" - -# Test 6: GET request with Authorization header -test_api "/api/test" "GET" "GET request with Authorization header" "-H \"Authorization: Bearer test-token\"" "" - -# Test 7: POST request with form data -test_api "/api/transactions" "POST" "POST request with form data" "-H \"Content-Type: application/x-www-form-urlencoded\"" "test=data" - -# Test 8: GET request with multiple custom headers -test_api "/api/test" "GET" "GET request with multiple custom headers" "-H \"Accept: application/json\" -H \"X-Custom-Header: test\"" "" - -echo -e "\n${BLUE}API curl tests completed for ${URL}${NC}" - -# Now test the API with a more complex curl command that simulates a real client -echo -e "\n${YELLOW}Simulating a real API client with complex curl command${NC}" -echo "------------------------------------------------" - -echo -e "${BLUE}Command:${NC}" -echo "curl -v -X POST \"${URL}/api/transactions\" \\" -echo " -H \"Content-Type: application/json\" \\" -echo " -H \"Accept: application/json\" \\" -echo " -H \"User-Agent: APIClient/1.0\" \\" -echo " -H \"Authorization: Bearer test-token\" \\" -echo " -d '{\"transaction_type\":\"test\",\"amount\":100,\"recipient\":\"Test Corp\"}'" - -echo -e "\n${BLUE}Response:${NC}" -curl -v -X POST "${URL}/api/transactions" \ - -H "Content-Type: application/json" \ - -H "Accept: application/json" \ - -H "User-Agent: APIClient/1.0" \ - -H "Authorization: Bearer test-token" \ - -d '{"transaction_type":"test","amount":100,"recipient":"Test Corp"}' 2>&1 - -echo -e "\n------------------------------------------------" -echo -e "${GREEN}All tests completed.${NC}" \ No newline at end of file +if [ "$FAILURES" -gt 0 ]; then + echo -e "${RED}API curl tests failed: $FAILURES issue(s).${NC}"; exit 1 +else + echo -e "${GREEN}All API curl tests passed.${NC}" +fi \ No newline at end of file diff --git a/test_api_headers.sh b/test_api_headers.sh index de06fc2..7b8eaf1 100755 --- a/test_api_headers.sh +++ b/test_api_headers.sh @@ -1,138 +1,72 @@ #!/bin/bash -# Colors for better readability +set -Eeuo pipefail + GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[0;33m' BLUE='\033[0;34m' -NC='\033[0m' # No Color +NC='\033[0m' + +FAILURES=0 + +fail() { echo -e "${RED}✗ $1${NC}"; FAILURES=$((FAILURES+1)); } +pass() { echo -e "${GREEN}✓ $1${NC}"; } + +require_header() { + local headers=$1; local name=$2; local pattern=${3:-} + if ! echo "$headers" | grep -qi "^$name:"; then fail "$name header not found"; return; fi + if [ -n "$pattern" ] && ! echo "$headers" | grep -i "^$name:" | grep -qiE "$pattern"; then fail "$name does not match expected pattern: $pattern"; return; fi + pass "$name present" +} + +require_header_absent() { + local headers=$1; local name=$2 + if echo "$headers" | grep -qi "^$name:"; then fail "$name should be absent"; else pass "$name absent as expected"; fi +} + +assert_http_status() { + local url=$1; local expected=$2 + local code; code=$(curl -s -o /dev/null -w "%{http_code}" -I "$url") + if [ "$code" != "$expected" ]; then fail "HTTP status $url expected $expected, got $code"; else pass "HTTP $expected for $url"; fi +} -# Base URLs to test DEV_URL="http://localhost:5005" PROD_URL="https://ploughshares.nixc.us" - -# Default to development URL unless specified URL=${1:-$DEV_URL} echo -e "${BLUE}Testing API Security Headers for ${URL}${NC}" echo "==================================================" -# Function to test an endpoint's headers -test_headers() { - local endpoint=$1 - local description=$2 - local full_url="${URL}${endpoint}" - - echo -e "\n${YELLOW}Testing headers for ${description} (${endpoint})${NC}" - echo "------------------------------------------------" - - # Get headers only with curl - headers=$(curl -s -I "${full_url}") - - # Display all headers for reference - echo -e "${BLUE}All Headers:${NC}" - echo "$headers" | grep -v "Date:" | grep -v "Server:" - - echo -e "\n${BLUE}Security Header Analysis:${NC}" - - # Check for Content-Security-Policy - if echo "$headers" | grep -q "Content-Security-Policy"; then - csp=$(echo "$headers" | grep "Content-Security-Policy" | sed "s/Content-Security-Policy: //") - echo -e "${GREEN}✓ Content-Security-Policy:${NC}" - - # Check for unsafe-inline and unsafe-eval in script-src - if echo "$csp" | grep -q "script-src.*'unsafe-inline'"; then - echo -e " ${YELLOW}⚠ script-src contains 'unsafe-inline' - needed for API clients${NC}" - else - echo -e " ${RED}✗ script-src missing 'unsafe-inline' - may cause issues with some clients${NC}" - fi - - if echo "$csp" | grep -q "script-src.*'unsafe-eval'"; then - echo -e " ${YELLOW}⚠ script-src contains 'unsafe-eval' - needed for API clients${NC}" - else - echo -e " ${RED}✗ script-src missing 'unsafe-eval' - may cause issues with some clients${NC}" - fi - - # Check for connect-src - if echo "$csp" | grep -q "connect-src.*\\*"; then - echo -e " ${GREEN}✓ connect-src includes * - good for API access${NC}" - else - echo -e " ${RED}✗ connect-src restrictive - may block API access${NC}" - fi - else - echo -e "${RED}✗ Content-Security-Policy header not found${NC}" - fi - - # Check for CORS headers for API endpoints - if [[ "$endpoint" == "/api/"* ]]; then - echo -e "\n${BLUE}CORS Headers (critical for API):${NC}" - - if echo "$headers" | grep -q "Access-Control-Allow-Origin"; then - origin=$(echo "$headers" | grep "Access-Control-Allow-Origin" | sed "s/Access-Control-Allow-Origin: //") - echo -e "${GREEN}✓ Access-Control-Allow-Origin: $origin${NC}" - else - echo -e "${RED}✗ Access-Control-Allow-Origin header not found - CORS will fail${NC}" - fi - - if echo "$headers" | grep -q "Access-Control-Allow-Methods"; then - methods=$(echo "$headers" | grep "Access-Control-Allow-Methods" | sed "s/Access-Control-Allow-Methods: //") - echo -e "${GREEN}✓ Access-Control-Allow-Methods: $methods${NC}" - else - echo -e "${RED}✗ Access-Control-Allow-Methods header not found - CORS will fail${NC}" - fi - - if echo "$headers" | grep -q "Access-Control-Allow-Headers"; then - allowed_headers=$(echo "$headers" | grep "Access-Control-Allow-Headers" | sed "s/Access-Control-Allow-Headers: //") - echo -e "${GREEN}✓ Access-Control-Allow-Headers: $allowed_headers${NC}" - else - echo -e "${RED}✗ Access-Control-Allow-Headers header not found - CORS will fail${NC}" - fi - fi - - # Check for Cross-Origin-Resource-Policy - if echo "$headers" | grep -q "Cross-Origin-Resource-Policy"; then - corp=$(echo "$headers" | grep "Cross-Origin-Resource-Policy" | sed "s/Cross-Origin-Resource-Policy: //") - echo -e "\n${BLUE}Resource Policy:${NC}" - - if [[ "$endpoint" == "/api/"* ]]; then - if [[ "$corp" == "cross-origin" ]]; then - echo -e "${GREEN}✓ Cross-Origin-Resource-Policy: $corp - correct for API${NC}" - else - echo -e "${RED}✗ Cross-Origin-Resource-Policy: $corp - should be 'cross-origin' for API endpoints${NC}" - fi - else - if [[ "$corp" == "same-origin" ]]; then - echo -e "${GREEN}✓ Cross-Origin-Resource-Policy: $corp - correct for UI${NC}" - else - echo -e "${YELLOW}⚠ Cross-Origin-Resource-Policy: $corp - should be 'same-origin' for UI endpoints${NC}" - fi - fi - else - echo -e "${YELLOW}⚠ Cross-Origin-Resource-Policy header not found${NC}" - fi - +test_api_headers() { + local endpoint=$1; local description=$2; local full_url="${URL}${endpoint}" + echo -e "\n${YELLOW}${description} (${endpoint})${NC}" echo "------------------------------------------------" + assert_http_status "$full_url" 200 + headers=$(curl -s -I "$full_url") + echo -e "${BLUE}Headers:${NC}"; echo "$headers" | grep -v "Date:" | grep -v "Server:" + + require_header "$headers" "Content-Security-Policy" + require_header "$headers" "Access-Control-Allow-Origin" "\*" + require_header "$headers" "Access-Control-Allow-Methods" + require_header "$headers" "Access-Control-Allow-Headers" + require_header "$headers" "Cross-Origin-Resource-Policy" "cross-origin" + require_header_absent "$headers" "Cross-Origin-Embedder-Policy" } -# Test UI endpoint -test_headers "/" "Main UI page" +test_api_headers "/api/test" "API test endpoint headers" -# Test API endpoints -test_headers "/api/test" "API test endpoint" +echo -e "\n${YELLOW}OPTIONS preflight for /api/test${NC}" +echo "------------------------------------------------" +preflight_code=$(curl -s -o /dev/null -w "%{http_code}" -X OPTIONS -I "${URL}/api/test") +if [ "$preflight_code" != "200" ]; then fail "OPTIONS /api/test expected 200, got $preflight_code"; else pass "OPTIONS /api/test returns 200"; fi +preflight_headers=$(curl -s -X OPTIONS -I "${URL}/api/test") +require_header "$preflight_headers" "Access-Control-Allow-Origin" "\*" +require_header "$preflight_headers" "Access-Control-Allow-Methods" +require_header "$preflight_headers" "Access-Control-Allow-Headers" -# Test OPTIONS request for CORS preflight -echo -e "\n${YELLOW}Testing OPTIONS request for CORS preflight${NC}" -echo "------------------------------------------------" -curl -s -X OPTIONS -H "Origin: http://example.com" -I "${URL}/api/test" | grep -v "Date:" | grep -v "Server:" -echo "------------------------------------------------" - -# Test with a real-world API client simulation -echo -e "\n${YELLOW}Testing with a simulated API client${NC}" -echo "------------------------------------------------" -echo -e "${BLUE}Command:${NC} curl -s -X GET -H \"Origin: http://example.com\" -H \"Accept: application/json\" \"${URL}/api/test\"" -response=$(curl -s -X GET -H "Origin: http://example.com" -H "Accept: application/json" "${URL}/api/test") -echo -e "${BLUE}Response:${NC}" -echo "$response" | jq 2>/dev/null || echo "$response" -echo "------------------------------------------------" - -echo -e "\n${GREEN}All header tests completed.${NC}" \ No newline at end of file +if [ "$FAILURES" -gt 0 ]; then + echo -e "${RED}API header tests failed: $FAILURES issue(s).${NC}"; exit 1 +else + echo -e "${GREEN}All API header tests passed.${NC}" +fi \ No newline at end of file diff --git a/test_headers.sh b/test_headers.sh index c0c0ea4..36cfba9 100755 --- a/test_headers.sh +++ b/test_headers.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -Eeuo pipefail + # Colors for better readability GREEN='\033[0;32m' RED='\033[0;31m' @@ -7,6 +9,56 @@ YELLOW='\033[0;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color +FAILURES=0 + +fail() { + echo -e "${RED}✗ $1${NC}" + FAILURES=$((FAILURES+1)) +} + +pass() { + echo -e "${GREEN}✓ $1${NC}" +} + +require_header() { + local headers=$1 + local name=$2 + local pattern=${3:-} + if ! echo "$headers" | grep -qi "^$name:"; then + fail "$name header not found" + return + fi + if [ -n "$pattern" ]; then + if ! echo "$headers" | grep -i "^$name:" | grep -qiE "$pattern"; then + fail "$name does not match expected pattern: $pattern" + return + fi + fi + pass "$name present" +} + +require_header_absent() { + local headers=$1 + local name=$2 + if echo "$headers" | grep -qi "^$name:"; then + fail "$name should be absent" + else + pass "$name absent as expected" + fi +} + +assert_http_status() { + local url=$1 + local expected=$2 + local code + code=$(curl -s -o /dev/null -w "%{http_code}" -I "$url") + if [ "$code" != "$expected" ]; then + fail "HTTP status for $url expected $expected, got $code" + else + pass "HTTP $expected for $url" + fi +} + # Base URLs to test DEV_URL="http://localhost:5005" PROD_URL="https://ploughshares.nixc.us" @@ -17,107 +69,72 @@ URL=${1:-$DEV_URL} echo -e "${BLUE}Testing headers for ${URL}${NC}" echo "==================================================" -# Function to test an endpoint -test_endpoint() { +test_headers() { local endpoint=$1 - local method=$2 - local description=$3 + local description=$2 local full_url="${URL}${endpoint}" - - echo -e "\n${YELLOW}Testing ${method} ${endpoint}: ${description}${NC}" - echo "------------------------------------------------" - - if [ "$method" == "HEAD" ]; then - # Use -I for HEAD requests - curl -s -I "${full_url}" | grep -v "Date:" | grep -v "Server:" - elif [ "$method" == "OPTIONS" ]; then - # Use -X OPTIONS for OPTIONS requests - curl -s -X OPTIONS "${full_url}" -I | grep -v "Date:" | grep -v "Server:" - elif [ "$method" == "GET" ]; then - # First check headers - echo -e "${BLUE}Headers:${NC}" - curl -s -I "${full_url}" | grep -v "Date:" | grep -v "Server:" - - # Then check response body - echo -e "\n${BLUE}Response:${NC}" - curl -s "${full_url}" - echo -e "\n" - else - echo "Unsupported method: ${method}" - fi - - echo "------------------------------------------------" -} -# Function to check for specific security headers -check_security_headers() { - local endpoint=$1 - local full_url="${URL}${endpoint}" - - echo -e "\n${YELLOW}Checking security headers for ${endpoint}${NC}" + echo -e "\n${YELLOW}Testing headers for ${description} (${endpoint})${NC}" echo "------------------------------------------------" - - # Store headers in a variable - headers=$(curl -s -I "${full_url}") - - # Check for important security headers - check_header "Content-Security-Policy" "$headers" - check_header "Cross-Origin-Resource-Policy" "$headers" - check_header "Cross-Origin-Embedder-Policy" "$headers" - check_header "Cross-Origin-Opener-Policy" "$headers" - check_header "X-Frame-Options" "$headers" - check_header "X-Content-Type-Options" "$headers" - check_header "X-XSS-Protection" "$headers" - check_header "Referrer-Policy" "$headers" - - # For API endpoints, check CORS headers + + assert_http_status "$full_url" 200 + + headers=$(curl -s -I "$full_url") + + echo -e "${BLUE}All Headers:${NC}" + echo "$headers" | grep -v "Date:" | grep -v "Server:" + + # Common expectations + require_header "$headers" "Content-Security-Policy" + require_header "$headers" "Referrer-Policy" + require_header "$headers" "X-Frame-Options" + require_header "$headers" "X-Content-Type-Options" + if [[ "$endpoint" == "/api/"* ]]; then - check_header "Access-Control-Allow-Origin" "$headers" - check_header "Access-Control-Allow-Methods" "$headers" - check_header "Access-Control-Allow-Headers" "$headers" + # API expectations (CORS enabled, CORP cross-origin, COEP absent) + require_header "$headers" "Access-Control-Allow-Origin" "\*" + require_header "$headers" "Access-Control-Allow-Methods" + require_header "$headers" "Access-Control-Allow-Headers" + require_header "$headers" "Cross-Origin-Resource-Policy" "cross-origin" + require_header_absent "$headers" "Cross-Origin-Embedder-Policy" + else + # UI expectations (no CORS headers, CORP same-origin, COEP unsafe-none) + require_header "$headers" "Cross-Origin-Resource-Policy" "same-origin" + require_header "$headers" "Cross-Origin-Embedder-Policy" "unsafe-none" + if echo "$headers" | grep -qi "^Access-Control-Allow-Origin:"; then + fail "Access-Control-Allow-Origin should not be present on UI routes" + else + pass "No CORS headers on UI routes" + fi fi - + echo "------------------------------------------------" } -# Function to check for a specific header -check_header() { - local header=$1 - local headers=$2 - - if echo "$headers" | grep -q "$header"; then - header_value=$(echo "$headers" | grep "$header" | sed "s/$header: //") - echo -e "${GREEN}✓ $header: $header_value${NC}" - else - echo -e "${RED}✗ $header not found${NC}" - fi -} +# Test UI endpoint +test_headers "/" "Main UI page" -# Test UI endpoints -test_endpoint "/" "HEAD" "Main page headers" -check_security_headers "/" +# Test API endpoint +test_headers "/api/test" "API test endpoint" -# Test API endpoints -test_endpoint "/api/test" "HEAD" "API test endpoint headers" -check_security_headers "/api/test" - -test_endpoint "/api/test" "GET" "API test endpoint response" - -# Test OPTIONS request for CORS preflight -test_endpoint "/api/test" "OPTIONS" "API CORS preflight request" - -# Test a POST request to API -echo -e "\n${YELLOW}Testing POST to /api/transactions${NC}" +# OPTIONS preflight check +echo -e "\n${YELLOW}Testing OPTIONS request for CORS preflight${NC}" echo "------------------------------------------------" -curl -s -X POST "${URL}/api/transactions" \ - -H "Content-Type: application/json" \ - -d '{"test": "data"}' | jq 2>/dev/null || echo "Response is not valid JSON" -echo -e "\n------------------------------------------------" - -# Test with different Accept headers -echo -e "\n${YELLOW}Testing with Accept: application/json header${NC}" +preflight_code=$(curl -s -o /dev/null -w "%{http_code}" -X OPTIONS -I "${URL}/api/test") +if [ "$preflight_code" != "200" ]; then + fail "OPTIONS /api/test expected 200, got $preflight_code" +else + pass "OPTIONS /api/test returns 200" +fi +preflight_headers=$(curl -s -X OPTIONS -I "${URL}/api/test") +require_header "$preflight_headers" "Access-Control-Allow-Origin" "\*" +require_header "$preflight_headers" "Access-Control-Allow-Methods" +require_header "$preflight_headers" "Access-Control-Allow-Headers" echo "------------------------------------------------" -curl -s -H "Accept: application/json" "${URL}/api/test" | jq 2>/dev/null || echo "Response is not valid JSON" -echo -e "\n------------------------------------------------" -echo -e "\n${BLUE}Tests completed for ${URL}${NC}" \ No newline at end of file +if [ "$FAILURES" -gt 0 ]; then + echo -e "${RED}Header tests failed: $FAILURES issue(s).${NC}" + exit 1 +else + echo -e "${GREEN}All header tests passed.${NC}" +fi \ No newline at end of file diff --git a/version_history.log b/version_history.log index babcd87..f4acc44 100644 --- a/version_history.log +++ b/version_history.log @@ -5,3 +5,4 @@ Fri Jul 4 20:03:15 EDT 2025: Version changed from 0.2.0 to 0.2.1 Fri Jul 4 22:25:55 EDT 2025: Version changed from 0.2.1 to 0.3.0 Fri Jul 4 22:33:48 EDT 2025: Version changed from 0.3.0 to 0.3.1 Fri Jul 4 23:19:37 EDT 2025: Version changed from 0.3.1 to 0.4.0 +Wed Aug 27 19:26:44 EDT 2025: Version changed from 0.4.0 to 0.4.1