Date: 2025-07-06
Test Suite: tests/api_endpoints_test.ts
Status: 7 Failed Tests / 22 Total Tests
Analysis Phase: In-Depth Root Cause Investigation
Environment: Using .env.example values
| Issue | Endpoint | Expected | Actual | Severity | Status |
|---|---|---|---|---|---|
| 1 | GET /api/v1/tour/ | 400 | 404 | HIGH | Open |
| 2 | POST /api/v1/create-nonce (missing fields) | 400 | 403 | HIGH | Open |
| 3 | POST /api/v1/create-nonce (happy path) | 200 | 403 | CRITICAL | Open |
| 4 | POST /api/v1/payment/exchange-nonce (happy) | 200 | 401 | CRITICAL | Open |
| 5 | POST /api/v1/admin/generate-pay-token (missing body) | 400 | 401 | MEDIUM | Open |
| 6 | POST /api/v1/admin/generate-pay-token (happy) | 200 | 401 | HIGH | Open |
| 7 | GET /api/v1/booking/{token} (happy) | 200 | TypeError | HIGH | Open |
✅ Current Environment: Server running with .env.example values
✅ TURNSTILE_SECRET_KEY: Configured (0x4AAAAAABjPDChnTzNbiXh-p50UU_uolno)
❌ ADMIN_API_KEY: Missing from environment (root cause of Issues 5-7)
✅ Payment Credentials: Configured (PAYMENT_USERNAME/PASSWORD)
✅ VALTOWN_TOKEN: Configured
Actual Set-Cookie Headers (from curl test):
set-cookie: SESSION=f3e256d...8c5e; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800
set-cookie: XSRF-TOKEN=47a3e85b...730f; Path=/; Secure; SameSite=Strict; Max-Age=1800
Test Parsing Logic Issue:
// ❌ INCORRECT parsing in test
const cookies = seed.headers.get('set-cookie')!;
const xsrf = cookies.split(/; */)[1]; // Gets "Path=/" instead of XSRF-TOKEN
const session = cookies.split(/; */)[0]; // Gets full SESSION cookie string
✅ Service Running: TurnstileService properly configured
✅ Secret Key Present: Environment variable correctly set
❌ Test Token Issue: Using "test_token" instead of valid Turnstile response
📋 Expected: Tests need mock Turnstile tokens or service bypass for testing
Current Flow (CORRECT):
- CSRF validation (403 if X-XSRF-TOKEN invalid)
- Body parsing (400 if JSON malformed)
- Field validation (400 if required fields missing)
- Turnstile validation (403 if bot detection fails)
- Business logic processing
Test Expectations (INCORRECT):
- Tests expect field validation errors (400) before security validation (403)
- This violates security-first principle where authentication/authorization comes before business logic
Test: GET /api/v1/tour/ (missing id) => 400
Problem:
- Expected: 400 Bad Request for missing tour ID
- Actual: 404 Not Found
Detailed Root Cause Analysis:
- OpenAPI Route Matching: The Hono framework with OpenAPI integration treats
/api/v1/tour/and/api/v1/tour/{id}as completely different routes - Path Resolution: When
/api/v1/tour/is requested, it doesn't match the parameterized route/api/v1/tour/{id} - Framework Behavior: Hono returns 404 for unmatched routes before any handler code executes
Code Analysis - tourHandler.ts:36-49:
// Validation exists but never reached for /api/v1/tour/
if (!tourId || tourId === '' || tourId === 'tour') {
logger.warn("Validation failed in getTourDetails", {
path: url.pathname,
received: { tourId },
errors: ["id: Required"]
});
return addCorsHeaders(
new Response(JSON.stringify({ error: "Validation failed", details: ["id: Required"] }), {
status: 400,
headers: { "Content-Type": "application/json" }
}),
origin
);
}
Technical Details:
- Handler has correct validation logic
- Validation never executes because route doesn't match
- Framework-level 404 occurs before application-level validation
Test Verification:
curl -s -w "Status: %{http_code}\n" http://localhost:5000/api/v1/tour/ # Returns: 404 Not Found curl -s -w "Status: %{http_code}\n" http://localhost:5000/api/v1/tour/tour-paris-explorer # Returns: 200 OK (works correctly)
Fix Strategy Options:
- Route Addition: Add specific route for
/api/v1/tour/that returns 400 - Test Update: Change test expectation from 400 to 404 (framework behavior)
- Middleware Solution: Add catch-all middleware to handle missing parameters
Recommendation: Option 2 (Test Update) - Framework 404 is correct behavior for unmatched routes
Test: POST /api/v1/create-nonce missing fields => 400
Problem:
- Expected: 400 Bad Request for missing required fields
- Actual: 403 Forbidden due to CSRF validation
Detailed Root Cause Analysis:
Code Flow Analysis - publicPayFlowHandler.ts:27-31:
// 1. CSRF Protection (validates X-XSRF-TOKEN header against SESSION cookie)
const csrfError = csrfProtection(request);
if (csrfError) {
return addCorsHeaders(csrfError, origin); // ← Returns 403 immediately
}
Security-First Validation Order (CORRECT BEHAVIOR):
- CSRF validation (Line 28) - Prevents cross-site attacks
- Body parsing (Line 35) - Only after authentication passes
- Field validation (Line 91) - Business logic validation last
- Turnstile validation (Line 105) - Bot protection
- Business processing - Only after all security checks pass
Test Logic Issue:
// Test sends empty body {} but expects 400
body: JSON.stringify({}) // Missing tourId, cfToken, email_verify
// Test expects field validation error (400)
assertEquals(res.status, 400); // ❌ WRONG - security comes first
// Actual behavior: CSRF fails before field validation
// Returns 403: "CSRF validation failed", "details": "Missing X-XSRF-TOKEN header"
CSRF Validation Details - csrf.ts:72-80:
export function validateCSRFToken(request: Request): { valid: boolean; error?: string; sessionId?: string } {
const xsrfHeader = request.headers.get('X-XSRF-TOKEN');
const cookieHeader = request.headers.get('Cookie');
if (!xsrfHeader) {
return { valid: false, error: 'Missing X-XSRF-TOKEN header' }; // ← Test fails here
}
Why This Is Correct Security Design:
- Defense in Depth: Security validation before business logic prevents attacks
- Early Rejection: Malicious requests blocked before processing sensitive data
- Performance: Avoid expensive validation on unauthenticated requests
- Audit Trail: Security failures logged separately from business validation failures
Test Verification:
# Without CSRF token - gets 403 (correct) curl -X POST http://localhost:5000/api/v1/create-nonce -d '{}' # Response: {"error":"CSRF validation failed","details":"Missing X-XSRF-TOKEN header"} # With CSRF but missing fields - would get 400 (if CSRF passes) curl -X POST http://localhost:5000/api/v1/create-nonce \ -H "X-XSRF-TOKEN: valid_token" -H "Cookie: SESSION=valid_session" -d '{}'
Fix Strategy Options:
- Test Update (RECOMMENDED): Change expectation from 400 to 403
- Add Pre-CSRF Field Check: Validate fields before CSRF (NOT RECOMMENDED - security risk)
- Separate Test: Create test that properly sets up CSRF first
Recommendation: Option 1 - Tests should reflect correct security-first behavior
Test: POST /api/v1/create-nonce happy => 200 + RateLimit headers
Problem:
- Expected: 200 OK with nonce and rate limit headers
- Actual: 403 Forbidden
Detailed Root Cause Analysis:
Cookie Parsing Bug in Test - api_endpoints_test.ts:96-98:
const seed = await fetch(`${BASE_URL}/api/v1/tour/${TOURS[0].tourId}`);
const cookies = seed.headers.get('set-cookie')!;
const xsrf = cookies.split(/; */)[1]; // ❌ CRITICAL BUG
const session = cookies.split(/; */)[0];
Actual Set-Cookie Header Format (from server):
set-cookie: SESSION=f3e256d209f24e2481900c006bb4ceef71b88f282eb6421941fa0278fac28c5e; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800 set-cookie: XSRF-TOKEN=47a3e85baa275256c84f83195704dba73f538a403612b5f982d7f9c36962730f; Path=/; Secure; SameSite=Strict; Max-Age=1800
What The Bug Does:
cookies.split(/; */)[0]="SESSION=f3e256d...8c5e"cookies.split(/; */)[1]="Path=/"← WRONG! Should be XSRF-TOKEN
Actual Headers.get() Behavior (CORRECTION):
// headers.get('set-cookie') returns BOTH cookies comma-separated:
"SESSION=be4b6f46...ab870; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800, XSRF-TOKEN=53a80779...208838; Path=/; Secure; SameSite=Strict; Max-Age=1800"
Test Parsing Bug Analysis:
const cookies = seed.headers.get('set-cookie')!;
// cookies = "SESSION=xxx; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800, XSRF-TOKEN=yyy; Path=/; Secure; SameSite=Strict; Max-Age=1800"
const xsrf = cookies.split(/; */)[1];
// xsrf = "Path=/" ← WRONG! Should split on comma first
const session = cookies.split(/; */)[0];
// session = "SESSION=xxx" ← This part is correct
CSRF Validation Failure:
// Test extracts wrong value
'X-XSRF-TOKEN': xsrf.split('=')[1], // "Path=/" → Results in: "/"
// Server expects actual XSRF token
// Expected: "53a80779eff69e251c75167d2e9c81040dc4cc6e6d6b1a96e67b75c522208838"
Correct Cookie Parsing Logic (Option 1 - getSetCookie):
// ✅ CORRECT approach - use getSetCookie() method
const response = await fetch(url);
const allCookies = response.headers.getSetCookie(); // Returns array of individual cookies
let sessionValue = '';
let xsrfValue = '';
allCookies.forEach(cookie => {
if (cookie.startsWith('SESSION=')) {
sessionValue = cookie.split(';')[0]; // "SESSION=actual_value"
} else if (cookie.startsWith('XSRF-TOKEN=')) {
xsrfValue = cookie.split(';')[0].split('=')[1]; // Extract token value only
}
});
Correct Cookie Parsing Logic (Option 2 - Manual parsing):
// ✅ CORRECT approach - parse comma-separated cookies properly
const cookieHeader = seed.headers.get('set-cookie')!;
const individualCookies = cookieHeader.split(', XSRF-TOKEN='); // Split on known separator
const sessionCookie = individualCookies[0]; // "SESSION=xxx; Path=/; ..."
const xsrfCookie = 'XSRF-TOKEN=' + individualCookies[1]; // "XSRF-TOKEN=yyy; Path=/; ..."
const sessionValue = sessionCookie.split(';')[0]; // "SESSION=xxx"
const xsrfValue = xsrfCookie.split(';')[0].split('=')[1]; // "yyy"
Corrected Test Code:
// Fix for api_endpoints_test.ts
const seed = await fetch(`${BASE_URL}/api/v1/tour/${TOURS[0].tourId}`);
const allCookies = seed.headers.getSetCookie();
let sessionCookie = '';
let xsrfToken = '';
allCookies.forEach(cookie => {
if (cookie.startsWith('SESSION=')) {
sessionCookie = cookie.split(';')[0];
} else if (cookie.startsWith('XSRF-TOKEN=')) {
xsrfToken = cookie.split(';')[0].split('=')[1];
}
});
// Use in request
const res = await fetch(`${BASE_URL}/api/v1/create-nonce`, {
method: 'POST',
headers: {
'Cookie': sessionCookie + '; XSRF-TOKEN=' + xsrfToken,
'X-XSRF-TOKEN': xsrfToken, // ✅ Now contains actual token
'content-type': 'application/json'
},
body: JSON.stringify({ tourId: TOURS[0].tourId, cfToken: 'test_cf', email_verify: '🍯' })
});
Test Verification:
# Check what test is actually sending echo "X-XSRF-TOKEN: /" # vs what server expects echo "X-XSRF-TOKEN: 47a3e85baa275256c84f83195704dba73f538a403612b5f982d7f9c36962730f"
Secondary Issue: Turnstile Validation
Even if CSRF passes, test uses cfToken: 'test_cf' which fails Turnstile validation:
// From test
body: JSON.stringify({ tourId: TOURS[0].tourId, cfToken: 'test_cf', email_verify: '🍯' })
// Turnstile validation failure
{"error":"Bot detection failed","details":"Turnstile challenge failed"}
Complete Fix Requirements:
- Fix cookie parsing to handle multiple Set-Cookie headers correctly
- Mock Turnstile service or use valid test tokens
- Verify CSRF token matching between cookie and header
Recommendation: Implement proper multi-cookie parsing and Turnstile test mocking
Test: POST /api/v1/payment/exchange-nonce happy => 200 + fingerprint
Problem:
- Expected: 200 OK with payment fingerprint
- Actual: 401 Unauthorized
Root Cause: Test depends on nonce from Issue 3. Since create-nonce is failing (Issue 3), no valid nonce is available for exchange-nonce test.
Dependency Chain:
- create-nonce test fails → no valid nonce stored
- exchange-nonce test uses invalid/undefined nonce → 401 Unauthorized
Fix Required: Resolve Issue 3 first, then retest this endpoint.
Test: POST /api/v1/admin/generate-pay-token missing body => 400
Problem:
- Expected: 400 Bad Request for missing request body
- Actual: 401 Unauthorized
Detailed Root Cause Analysis:
Environment Configuration Issue:
# Current .env.example (ADMIN_API_KEY missing) PAYMENT_USERNAME=your_payment_username PAYMENT_PASSWORD=your_payment_password VALTOWN_TOKEN=your_val_town_api_token TURNSTILE_SECRET_KEY=your_cloudflare_turnstile_secret # ADMIN_API_KEY=??? ← NOT PRESENT
Code Analysis - adminHandler.ts:38-51:
const providedKey = authHeader.substring(7); // Remove "Bearer "
const expectedKey = Deno.env.get("ADMIN_API_KEY"); // ← Returns undefined
if (!expectedKey || providedKey !== expectedKey) { // ← !undefined = true
logger.warn("Invalid admin API key", {
ip: request.headers.get("cf-connecting-ip") || "unknown"
});
return addCorsHeaders(
new Response(JSON.stringify({ error: "Invalid API key" }), {
status: 401, // ← Always returns 401 when ADMIN_API_KEY undefined
headers: { "Content-Type": "application/json" }
}),
origin
);
}
Test Logic vs Implementation:
// Test sends valid Authorization header format
headers: { 'Authorization': `Bearer ${ADMIN_API_KEY}` }
// But ADMIN_API_KEY from test environment is empty string ""
// Code comparison fails:
providedKey = "" // From test environment
expectedKey = undefined // From missing env var
// Result: 401 Unauthorized (correct behavior)
Security-First Validation Order (CORRECT):
- Authentication check (API key validation) - Line 38-51
- Body parsing (JSON validation) - Line 54-65
- Field validation (bookingId required) - Line 69-77
- Business logic (booking exists check) - Line 80-94
Environment Verification:
# Test what environment variables are available echo $ADMIN_API_KEY # Returns empty deno eval "console.log(Deno.env.get('ADMIN_API_KEY'))" # Returns undefined
Fix Requirements:
- Add to .env.example:
ADMIN_API_KEY=admin_test_key_123 - Update test environment: Ensure ADMIN_API_KEY is loaded
- Test configuration: Use consistent key value across test and server
Recommendation: Add ADMIN_API_KEY to environment configuration - this is proper security behavior
Test: POST /api/v1/admin/generate-pay-token happy => 200 + shareableUrl
Problem:
- Expected: 200 OK with shareable URL
- Actual: 401 Unauthorized
Root Cause: Same as Issue 5 - missing ADMIN_API_KEY environment variable.
Fix Required:
Add ADMIN_API_KEY to environment configuration.
Test: GET /api/v1/booking/{token} happy => 200 + cookies
Problem:
- Expected: 200 OK with SESSION and XSRF-TOKEN cookies
- Actual:
TypeError: Invalid URL: 'undefined'
Root Cause: Test depends on Issue 6 success. The tokenUrl is undefined because generate-pay-token test failed:
// Line 217 in test
const url = new URL((t as any).tokenUrl); // tokenUrl is undefined
Dependency Chain:
- generate-pay-token test fails → no tokenUrl stored
- booking token test tries to parse undefined URL → TypeError
Fix Required: Resolve Issue 6 first, then retest this endpoint.
- Issue 3: Fix create-nonce CSRF token parsing
- Issue 4: Fix exchange-nonce (dependency on Issue 3)
- Issue 5 & 6: Add ADMIN_API_KEY to environment
- Issue 7: Fix booking token test (dependency on Issue 6)
- Issue 1: Add tour path validation
- Issue 2: Update test expectations for security-first validation
Required Addition to .env.dev:
ADMIN_API_KEY=your_admin_key_here
Example:
PAYMENT_USERNAME=X PAYMENT_PASSWORD=X VALTOWN_TOKEN=vtwn_3MCBkFj8vFx5xgLymxZS3mnQFhfU TURNSTILE_SECRET_KEY=0x4AAAAAABjPDChnTzNbiXh-p50UU_uolno ADMIN_API_KEY=admin_test_key_123 DEBUG_MODE=true
Create a helper function for proper Set-Cookie header parsing:
function parseCookies(setCookieHeader: string): Record<string, string> {
const cookies: Record<string, string> = {};
setCookieHeader.split(', ').forEach(cookie => {
const [nameValue] = cookie.split(';');
const [name, value] = nameValue.split('=');
cookies[name.trim()] = value.trim();
});
return cookies;
}
Break test dependencies by making each test self-contained or using proper test setup/teardown.
Add environment validation at test startup to ensure all required variables are present.
The current 403 responses for Issues 2 and 3 are actually correct security behavior:
- CSRF validation first - Prevents CSRF attacks before processing business logic
- Authentication before authorization - API key validation before body parsing
- Security-first approach - Block malicious requests early in the pipeline
Recommendation: Update test expectations to match security-first validation order rather than changing the security behavior.
- Add ADMIN_API_KEY to .env.example
ADMIN_API_KEY=admin_test_key_123
- Update test environment to load all required variables
- Verify environment loading in test setup
- Fix cookie parsing logic using
response.headers.getSetCookie() - Add Turnstile test mocking or bypass for testing
- Update test expectations to match security-first validation:
- Issue 1: 404 → 400 (change test expectation)
- Issue 2: 400 → 403 (change test expectation)
- Create cookie parsing utility for reusable test code
- Add test environment validation to ensure all vars present
- Implement proper test dependencies to avoid cascade failures
- FIRST: Environment configuration (Issues 5, 6, 7)
- SECOND: Cookie parsing fix (Issues 3, 4)
- THIRD: Test expectation updates (Issues 1, 2)
- FOURTH: Turnstile mocking implementation
- Environment test: Verify all required variables present
- Cookie test: Verify CSRF tokens extracted correctly
- Integration test: Full payment flow with proper setup
- Admin test: All admin endpoints with valid API key
ANALYSIS COMPLETE - READY FOR SYSTEMATIC RESOLUTION