|
| 1 | +# CORS Testing Guide |
| 2 | + |
| 3 | +## Key Terms |
| 4 | + |
| 5 | +**CORS (Cross-Origin Resource Sharing)**: Browser security mechanism that |
| 6 | +controls whether web pages from one domain can access resources from another |
| 7 | +domain. |
| 8 | + |
| 9 | +**Preflight Request**: An OPTIONS request browsers send before the actual |
| 10 | +request to check if the cross-origin request is permitted. |
| 11 | + |
| 12 | +**Origin Header**: Identifies the domain making the request (e.g., |
| 13 | +`https://app.example.com`). Automatically set by browsers, cannot be overridden |
| 14 | +in JavaScript. |
| 15 | + |
| 16 | +**Access-Control-Allow-\* Headers**: Server response headers that tell browsers |
| 17 | +which cross-origin requests are permitted. |
| 18 | + |
| 19 | +--- |
| 20 | + |
| 21 | +## What We're Testing |
| 22 | + |
| 23 | +### CORS Configuration for Public API Access |
| 24 | + |
| 25 | +Testing that the `/api/claim_token` endpoint properly supports cross-origin |
| 26 | +requests from any domain: |
| 27 | + |
| 28 | +- **CalibnetFIL, CalibnetUSDFC, MainnetFIL** faucets accessible from external |
| 29 | + websites |
| 30 | +- **Browser-based DApps** can integrate with the faucet API |
| 31 | +- **Third-party integrations** work without CORS errors |
| 32 | + |
| 33 | +### Current Configuration |
| 34 | + |
| 35 | +```rust |
| 36 | +// src/lib.rs |
| 37 | +let cors = CorsLayer::new() |
| 38 | + .allow_origin(Any) // Accept requests from ANY origin |
| 39 | + .allow_methods(Method::GET); // Only GET requests allowed |
| 40 | +``` |
| 41 | + |
| 42 | +**Result**: `Access-Control-Allow-Origin: *` header on all responses |
| 43 | + |
| 44 | +**Note**: We don't set `allow_headers()` because GET requests only use "simple |
| 45 | +headers" (like `Accept`, `User-Agent`) which don't require CORS preflight |
| 46 | +permission. |
| 47 | + |
| 48 | +--- |
| 49 | + |
| 50 | +## How We Test in CI |
| 51 | + |
| 52 | +### Matrix Strategy with Isolated State |
| 53 | + |
| 54 | +```yaml |
| 55 | +- name: "CORS Tests" |
| 56 | + script: "e2e/test_cors.js" |
| 57 | + port: 8788 # Separate port from Browser/API tests |
| 58 | + k6_browser_enabled: false # No browser support needed |
| 59 | + state_dir: "cors" # Isolated Durable Object state |
| 60 | +``` |
| 61 | +
|
| 62 | +Each test suite runs independently: |
| 63 | +
|
| 64 | +```bash |
| 65 | +yarn wrangler dev --port 8788 --persist-to .wrangler-state-cors-${github.run_id} |
| 66 | +``` |
| 67 | + |
| 68 | +This prevents CORS test state from interfering with Browser or API tests. |
| 69 | + |
| 70 | +--- |
| 71 | + |
| 72 | +## Test Flow |
| 73 | + |
| 74 | +### 1. Preflight OPTIONS Request (5 checks) |
| 75 | + |
| 76 | +Validates that browsers can check permissions before sending actual requests: |
| 77 | + |
| 78 | +```javascript |
| 79 | +// Request |
| 80 | +OPTIONS /api/claim_token |
| 81 | +Origin: https://external-example.com |
| 82 | +Access-Control-Request-Method: GET |
| 83 | + |
| 84 | +// Validates |
| 85 | +✓ Status is 200 or 204 |
| 86 | +✓ Has Access-Control-Allow-Origin header |
| 87 | +✓ Allows all origins (*) |
| 88 | +✓ Has Access-Control-Allow-Methods header |
| 89 | +✓ Allows GET method |
| 90 | +``` |
| 91 | + |
| 92 | +### 2. Actual Cross-Origin GET Request (3 checks) |
| 93 | + |
| 94 | +Confirms the API properly responds to cross-origin requests: |
| 95 | + |
| 96 | +```javascript |
| 97 | +// Request |
| 98 | +GET /api/claim_token?faucet_info=CalibnetFIL&address=0x... |
| 99 | +Origin: https://external-example.com |
| 100 | + |
| 101 | +// Validates |
| 102 | +✓ Has Access-Control-Allow-Origin in response |
| 103 | +✓ CORS header allows all origins (*) |
| 104 | +✓ Response received (not blocked by CORS) |
| 105 | +``` |
| 106 | + |
| 107 | +### 3. Same-Origin Request (2 checks) |
| 108 | + |
| 109 | +Verifies CORS headers are present even without explicit origin: |
| 110 | + |
| 111 | +```javascript |
| 112 | +// Request (no Origin header) |
| 113 | +GET /api/claim_token?faucet_info=CalibnetFIL&address=0x... |
| 114 | + |
| 115 | +// Validates |
| 116 | +✓ Request succeeds (200 or 429) |
| 117 | +✓ Has Access-Control-Allow-Origin header |
| 118 | +``` |
| 119 | +
|
| 120 | +### 4. Multiple Origins (9 checks) |
| 121 | +
|
| 122 | +Tests that different domains can all access the API: |
| 123 | +
|
| 124 | +```javascript |
| 125 | +// Tests three different origins: |
| 126 | +- https://app.example.com |
| 127 | +- http://localhost:3000 |
| 128 | +- https://wallet.filecoin.io |
| 129 | + |
| 130 | +// For each origin: |
| 131 | +✓ Request succeeds |
| 132 | +✓ CORS allows origin |
| 133 | +✓ Response received |
| 134 | +``` |
| 135 | + |
| 136 | +### 5. Security & Edge Cases (3 checks) |
| 137 | + |
| 138 | +Validates security headers and error handling: |
| 139 | + |
| 140 | +```javascript |
| 141 | +// Security |
| 142 | +✓ Access-Control-Allow-Credentials is NOT set (safer for public APIs) |
| 143 | + |
| 144 | +// Error responses |
| 145 | +✓ CORS headers present even on 500/400 errors (critical for browser error handling) |
| 146 | +``` |
| 147 | + |
| 148 | +**Total: 22 checks covering full CORS compliance** |
| 149 | + |
| 150 | +--- |
| 151 | + |
| 152 | +## Running Tests Locally |
| 153 | + |
| 154 | +```bash |
| 155 | +# Start server |
| 156 | +yarn wrangler dev --port 8787 |
| 157 | + |
| 158 | +# Run CORS tests |
| 159 | +API_URL="http://127.0.0.1:8787" k6 run e2e/test_cors.js |
| 160 | +``` |
| 161 | + |
| 162 | +### Expected Output (All Passing) |
| 163 | + |
| 164 | +``` |
| 165 | +checks_succeeded...: 100.00% 22 out of 22 |
| 166 | +
|
| 167 | +✓ Preflight: Status is 200 or 204 |
| 168 | +✓ Preflight: Has Access-Control-Allow-Origin header |
| 169 | +✓ Preflight: Allows all origins (*) |
| 170 | +✓ Preflight: Has Access-Control-Allow-Methods |
| 171 | +✓ Preflight: Allows GET method |
| 172 | +✓ Actual Request: Has Access-Control-Allow-Origin in response |
| 173 | +✓ Actual Request: CORS header allows all origins |
| 174 | +✓ Actual Request: Response received (not blocked by CORS) |
| 175 | +✓ Same-Origin: Request succeeds |
| 176 | +✓ Same-Origin: Has Access-Control-Allow-Origin (even for same-origin) |
| 177 | +✓ Multiple Origins: https://app.example.com - Request succeeds |
| 178 | +✓ Multiple Origins: https://app.example.com - CORS allows origin |
| 179 | +✓ Multiple Origins: https://app.example.com - Response received |
| 180 | +✓ Multiple Origins: http://localhost:3000 - Request succeeds |
| 181 | +✓ Multiple Origins: http://localhost:3000 - CORS allows origin |
| 182 | +✓ Multiple Origins: http://localhost:3000 - Response received |
| 183 | +✓ Multiple Origins: https://wallet.filecoin.io - Request succeeds |
| 184 | +✓ Multiple Origins: https://wallet.filecoin.io - CORS allows origin |
| 185 | +✓ Multiple Origins: https://wallet.filecoin.io - Response received |
| 186 | +✓ Security: Has Access-Control-Allow-Origin |
| 187 | +✓ Security: Access-Control-Allow-Credentials is not set |
| 188 | +✓ Error Response: CORS headers present even on errors |
| 189 | +``` |
| 190 | + |
| 191 | +**Note**: Total of 22 checks (5 preflight + 3 actual + 2 same-origin + 9 |
| 192 | +multiple-origins + 3 security/edge cases) |
| 193 | + |
| 194 | +### Common Test Results |
| 195 | + |
| 196 | +**All CORS checks pass, but some "Request succeeds" fail:** |
| 197 | + |
| 198 | +- ✅ CORS is working correctly |
| 199 | +- ⚠️ Server returning 500 errors (unrelated to CORS) |
| 200 | +- Check server logs for the actual API error |
| 201 | + |
| 202 | +**CORS header checks failing:** |
| 203 | + |
| 204 | +- ❌ Configuration issue in `src/lib.rs` |
| 205 | +- Verify `allow_origin(Any)` and `allow_headers(Any)` are set |
| 206 | +- Confirm `CorsLayer` is applied to the router |
| 207 | + |
| 208 | +--- |
| 209 | + |
| 210 | +## Quick Manual Test |
| 211 | + |
| 212 | +### Browser DevTools (Simplest) |
| 213 | + |
| 214 | +1. Open browser console (F12) on any page |
| 215 | +2. Run: |
| 216 | + |
| 217 | +```javascript |
| 218 | +fetch( |
| 219 | + "http://127.0.0.1:8787/api/claim_token?faucet_info=CalibnetFIL&address=f1pxxbe7he3c6vcw5as3gfvq33kprpmlufgtjgfdq", |
| 220 | +) |
| 221 | + .then((r) => console.log("✅ CORS works! Status:", r.status)) |
| 222 | + .catch((err) => console.error("❌ CORS blocked:", err)); |
| 223 | +``` |
| 224 | + |
| 225 | +### curl (For CI/Scripts) |
| 226 | + |
| 227 | +```bash |
| 228 | +# Test preflight |
| 229 | +curl -i -X OPTIONS \ |
| 230 | + -H "Origin: https://example.com" \ |
| 231 | + -H "Access-Control-Request-Method: GET" \ |
| 232 | + http://127.0.0.1:8787/api/claim_token |
| 233 | + |
| 234 | +# Look for: Access-Control-Allow-Origin: * |
| 235 | +``` |
| 236 | + |
| 237 | +--- |
| 238 | + |
| 239 | +## Why CORS Matters |
| 240 | + |
| 241 | +### Before CORS (Blocked) |
| 242 | + |
| 243 | +```javascript |
| 244 | +// From https://my-dapp.com |
| 245 | +fetch("https://faucet.chainsafe.io/api/claim_token?..."); |
| 246 | +// ❌ Error: CORS policy: No 'Access-Control-Allow-Origin' header |
| 247 | +``` |
| 248 | + |
| 249 | +### After CORS (Allowed) |
| 250 | + |
| 251 | +```javascript |
| 252 | +// From https://my-dapp.com |
| 253 | +fetch("https://faucet.chainsafe.io/api/claim_token?..."); |
| 254 | +// ✅ Success: Response includes "Access-Control-Allow-Origin: *" |
| 255 | +``` |
| 256 | + |
| 257 | +**Security**: Protected by rate limiting (60s cooldown, 2-drip wallet cap), not |
| 258 | +CORS restrictions. |
0 commit comments