Skip to content

Commit f227aa4

Browse files
kennethkalmerclaude
andcommitted
Add content negotiation test suite and fix map priority
Adds comprehensive CI test suite to verify content negotiation works correctly for all Accept header scenarios. The test suite validates that nginx serves markdown or HTML based on the Accept header, with proper fallback behavior. Test coverage (16 test cases): - Basic content negotiation (6 tests): text/markdown, application/markdown, text/plain, text/html, */* - Browser behavior (2 tests): Complex Accept headers, HTML priority when listed first - Direct access (2 tests): .md and .html file access - Path variations (3 tests): Index paths, non-index paths, nested paths - Edge cases (3 tests): 404 handling, fallback behavior, non-docs paths Also fixes nginx map priority order to ensure anchored patterns (^text/html, ^text/plain) are evaluated before wildcard patterns. This ensures "text/html, text/markdown" correctly serves HTML instead of markdown. Changes: - Created bin/assert-content-negotiation.sh with run_test() helper function - Integrated test into CircleCI test-nginx job - Reordered nginx map patterns for correct priority matching - All 16 tests passing locally 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 0df9f7c commit f227aa4

File tree

3 files changed

+148
-3
lines changed

3 files changed

+148
-3
lines changed

.circleci/config.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ jobs:
128128
- run:
129129
name: Verify all files are compressed
130130
command: ./bin/assert-compressed.sh
131+
- run:
132+
name: Test content negotiation for markdown
133+
command: |
134+
export PATH="$PWD/bin:$PWD/buildpack/build/.heroku-buildpack-nginx/ruby/bin:$PATH"
135+
./bin/assert-content-negotiation.sh
131136
- run:
132137
name: Test content request auth tokens
133138
command: |

bin/assert-content-negotiation.sh

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/bin/bash
2+
3+
# Content Negotiation Test Suite
4+
# Verifies that nginx serves markdown or HTML based on Accept header
5+
6+
source "$(dirname "$0")/nginx-utils.sh"
7+
trap stop_nginx EXIT
8+
9+
set -euo pipefail
10+
11+
# Disable auth for content negotiation tests
12+
export ENABLE_BASIC_AUTH=false
13+
export CONTENT_REQUEST_AUTH_TOKENS=""
14+
15+
# Set default port if not already set
16+
export PORT=${PORT:-3001}
17+
18+
# Test helper function
19+
# Parameters:
20+
# $1: path - URL path to test
21+
# $2: accept_header - Accept header value (empty string for default)
22+
# $3: expected_status - Expected HTTP status code
23+
# $4: expected_format - "html", "markdown", or "any"
24+
# $5: test_name - Human-readable test description
25+
run_test() {
26+
local path="$1"
27+
local accept_header="$2"
28+
local expected_status="$3"
29+
local expected_format="$4"
30+
local test_name="$5"
31+
32+
echo "🧪 $test_name"
33+
34+
# Build curl command with optional Accept header
35+
local curl_cmd="curl --silent --header \"X-Forwarded-Proto: https\""
36+
37+
if [ -n "$accept_header" ]; then
38+
curl_cmd="$curl_cmd --header \"Accept: $accept_header\""
39+
fi
40+
41+
curl_cmd="$curl_cmd --write-out \"\\n%{http_code}\\n%{content_type}\""
42+
curl_cmd="$curl_cmd \"http://localhost:\${PORT}\${path}\""
43+
44+
# Execute request and capture response + metadata
45+
local response
46+
response=$(eval "$curl_cmd")
47+
48+
# Parse response components
49+
local body=$(echo "$response" | sed '$d' | sed '$d')
50+
local status=$(echo "$response" | tail -2 | head -1)
51+
local content_type=$(echo "$response" | tail -1)
52+
53+
# Assert status code
54+
if [ "$status" != "$expected_status" ]; then
55+
echo " ❌ Expected status $expected_status, got $status"
56+
exit 1
57+
fi
58+
59+
# Verify content format
60+
if [ "$expected_format" = "markdown" ]; then
61+
# Check for markdown heading (first line should start with #)
62+
local first_line=$(echo "$body" | head -1)
63+
if ! grep -q "^#" <<< "$first_line"; then
64+
echo " ❌ Expected markdown (starting with #), got: ${first_line:0:50}"
65+
exit 1
66+
fi
67+
68+
# Verify Content-Type header (warning only, not fatal)
69+
if ! grep -q "text/markdown" <<< "$content_type"; then
70+
echo " ⚠️ Warning: Content-Type is '$content_type', expected 'text/markdown'"
71+
fi
72+
elif [ "$expected_format" = "html" ]; then
73+
# Check for HTML doctype using here-string to avoid broken pipe
74+
if ! grep -q "<!DOCTYPE html>" <<< "$body"; then
75+
echo " ❌ Expected HTML (with DOCTYPE), but not found"
76+
exit 1
77+
fi
78+
fi
79+
# "any" format means we don't validate content
80+
81+
echo " ✅ Passed (status: $status, format: $expected_format)"
82+
}
83+
84+
# Main test suite
85+
echo "================================"
86+
echo "Content Negotiation Test Suite"
87+
echo "================================"
88+
echo
89+
90+
start_nginx
91+
92+
# Group 1: Basic Content Negotiation
93+
echo "Group 1: Basic Content Negotiation"
94+
echo "-----------------------------------"
95+
run_test "/docs/channels" "" "200" "html" "Default serves HTML"
96+
run_test "/docs/channels" "text/markdown" "200" "markdown" "Accept: text/markdown"
97+
run_test "/docs/channels" "application/markdown" "200" "markdown" "Accept: application/markdown"
98+
run_test "/docs/channels" "text/plain" "200" "markdown" "Accept: text/plain"
99+
run_test "/docs/channels" "text/html" "200" "html" "Accept: text/html"
100+
run_test "/docs/channels" "*/*" "200" "html" "Accept: */*"
101+
echo
102+
103+
# Group 2: Browser Behavior
104+
echo "Group 2: Browser Behavior"
105+
echo "-------------------------"
106+
run_test "/docs/channels" "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" "200" "html" "Browser Accept header"
107+
run_test "/docs/channels" "text/html, text/markdown" "200" "html" "HTML prioritized when first"
108+
echo
109+
110+
# Group 3: Direct Access
111+
echo "Group 3: Direct Access"
112+
echo "----------------------"
113+
run_test "/docs/channels.md" "" "200" "markdown" "Direct .md access"
114+
run_test "/docs/channels/index.html" "" "200" "html" "Direct index.html access"
115+
echo
116+
117+
# Group 4: Path Variations
118+
echo "Group 4: Path Variations"
119+
echo "------------------------"
120+
run_test "/docs/chat/connect" "text/markdown" "200" "markdown" "Non-index path"
121+
run_test "/docs/api/realtime-sdk" "text/markdown" "200" "markdown" "Nested index path"
122+
run_test "/docs/basics" "text/markdown" "200" "markdown" "Simple path"
123+
echo
124+
125+
# Group 5: Edge Cases
126+
echo "Group 5: Edge Cases"
127+
echo "-------------------"
128+
run_test "/docs/nonexistent" "" "404" "any" "404 when path missing"
129+
run_test "/docs/nonexistent" "text/markdown" "404" "any" "404 with markdown Accept"
130+
run_test "/llms.txt" "" "200" "any" "Non-docs paths unaffected"
131+
echo
132+
133+
echo "================================"
134+
echo "✅ All 16 tests passed!"
135+
echo "================================"
136+
137+
# Exit explicitly with success
138+
exit 0

config/nginx.conf.erb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,15 @@ http {
7474
"application/markdown" ".md";
7575
"text/plain" ".md";
7676

77+
# IMPORTANT: Check start-of-string patterns FIRST (before wildcard patterns)
78+
# Explicit HTML request gets HTML (handles browser defaults like "text/html, text/markdown")
79+
"~*^text/html" ".html";
80+
"~*^text/plain" ".md";
81+
7782
# Handle multiple Accept values - prefer markdown if explicitly requested
7883
"~*text/markdown" ".md";
7984
"~*application/markdown" ".md";
80-
"~*^text/plain" ".md";
8185

82-
# Explicit HTML request gets HTML (handles browser defaults)
83-
"~*^text/html" ".html";
8486
"*/*" ".html";
8587
}
8688

0 commit comments

Comments
 (0)