upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-26 03:53:31 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-26 03:53:31 +0000
commit963b2971ec2f43b1c2f669a969c294fc1d291d3b (patch)
tree9495d5bd350ebf5123a9c72b9da3367b17e5534f
parent75f3da90edb66b81dbb6ed9806155f6bd7925fe1 (diff)
add cors support
-rw-r--r--grasp-audit/src/specs/grasp01/cors.rs507
-rw-r--r--grasp-audit/src/specs/grasp01/mod.rs2
-rw-r--r--src/http/mod.rs46
-rw-r--r--tests/cors.rs95
4 files changed, 645 insertions, 5 deletions
diff --git a/grasp-audit/src/specs/grasp01/cors.rs b/grasp-audit/src/specs/grasp01/cors.rs
new file mode 100644
index 0000000..4e0513e
--- /dev/null
+++ b/grasp-audit/src/specs/grasp01/cors.rs
@@ -0,0 +1,507 @@
1//! GRASP-01 CORS Tests
2//!
3//! Tests for GRASP-01 CORS requirements (lines 40-47 of ../grasp/01.md)
4//!
5//! These tests validate that a GRASP-01 compliant relay implements CORS correctly:
6//! - Sets `Access-Control-Allow-Origin: *` on ALL responses
7//! - Sets `Access-Control-Allow-Methods: GET, POST` on ALL responses
8//! - Sets `Access-Control-Allow-Headers: Content-Type` on ALL responses
9//! - Responds to OPTIONS requests with 204 No Content
10//!
11//! ## Running Tests
12//!
13//! ```bash
14//! cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test
15//! ```
16
17use crate::{AuditClient, AuditResult, FixtureKind, TestContext, TestResult};
18use nostr_sdk::prelude::*;
19use std::path::Path;
20
21pub struct CorsTests;
22
23impl CorsTests {
24 /// Run all CORS tests
25 pub async fn run_all(client: &AuditClient, relay_domain: &str) -> AuditResult {
26 let mut results = AuditResult::new("GRASP-01 CORS Tests");
27
28 // CORS tests against Git HTTP endpoints
29 results.add(Self::test_cors_allow_origin(client, relay_domain).await);
30 results.add(Self::test_cors_allow_methods(client, relay_domain).await);
31 results.add(Self::test_cors_allow_headers(client, relay_domain).await);
32 results.add(Self::test_cors_options_preflight(client, relay_domain).await);
33
34 results
35 }
36
37 // =========================================================================
38 // CORS Tests
39 // =========================================================================
40
41 /// Test: Access-Control-Allow-Origin header on all responses
42 ///
43 /// Spec: Line 44 of ../grasp/01.md
44 /// Requirement: Set `Access-Control-Allow-Origin: *` on ALL responses
45 pub async fn test_cors_allow_origin(
46 _client: &AuditClient,
47 relay_domain: &str,
48 ) -> TestResult {
49 TestResult::new(
50 "cors_allow_origin",
51 "GRASP-01:git-http:cors:44",
52 "Access-Control-Allow-Origin: * on all responses",
53 )
54 .run(|| {
55 let relay_domain = relay_domain.to_string();
56 async move {
57 // Test multiple endpoints to verify "ALL responses" requirement
58 let http_client = reqwest::Client::new();
59
60 // 1. Test root endpoint
61 let root_url = format!("http://{}/", relay_domain);
62 let response = http_client
63 .get(&root_url)
64 .send()
65 .await
66 .map_err(|e| format!("Failed to GET root: {}", e))?;
67
68 check_cors_allow_origin(&response, "root endpoint")?;
69
70 // 2. Test a non-existent repo path (still should have CORS headers)
71 let repo_url = format!(
72 "http://{}/npub1test/nonexistent.git/info/refs?service=git-upload-pack",
73 relay_domain
74 );
75 let response = http_client
76 .get(&repo_url)
77 .send()
78 .await
79 .map_err(|e| format!("Failed to GET repo endpoint: {}", e))?;
80
81 // Even 404 responses must have CORS headers
82 check_cors_allow_origin(&response, "git-upload-pack endpoint (even if 404)")?;
83
84 Ok(())
85 }
86 })
87 .await
88 }
89
90 /// Test: Access-Control-Allow-Methods header on all responses
91 ///
92 /// Spec: Line 45 of ../grasp/01.md
93 /// Requirement: Set `Access-Control-Allow-Methods: GET, POST` on ALL responses
94 pub async fn test_cors_allow_methods(
95 _client: &AuditClient,
96 relay_domain: &str,
97 ) -> TestResult {
98 TestResult::new(
99 "cors_allow_methods",
100 "GRASP-01:git-http:cors:45",
101 "Access-Control-Allow-Methods: GET, POST on all responses",
102 )
103 .run(|| {
104 let relay_domain = relay_domain.to_string();
105 async move {
106 let http_client = reqwest::Client::new();
107
108 // Test root endpoint
109 let root_url = format!("http://{}/", relay_domain);
110 let response = http_client
111 .get(&root_url)
112 .send()
113 .await
114 .map_err(|e| format!("Failed to GET root: {}", e))?;
115
116 check_cors_allow_methods(&response, "root endpoint")?;
117
118 // Test a repo path
119 let repo_url = format!(
120 "http://{}/npub1test/nonexistent.git/info/refs?service=git-upload-pack",
121 relay_domain
122 );
123 let response = http_client
124 .get(&repo_url)
125 .send()
126 .await
127 .map_err(|e| format!("Failed to GET repo endpoint: {}", e))?;
128
129 check_cors_allow_methods(&response, "git-upload-pack endpoint")?;
130
131 Ok(())
132 }
133 })
134 .await
135 }
136
137 /// Test: Access-Control-Allow-Headers header on all responses
138 ///
139 /// Spec: Line 46 of ../grasp/01.md
140 /// Requirement: Set `Access-Control-Allow-Headers: Content-Type` on ALL responses
141 pub async fn test_cors_allow_headers(
142 _client: &AuditClient,
143 relay_domain: &str,
144 ) -> TestResult {
145 TestResult::new(
146 "cors_allow_headers",
147 "GRASP-01:git-http:cors:46",
148 "Access-Control-Allow-Headers: Content-Type on all responses",
149 )
150 .run(|| {
151 let relay_domain = relay_domain.to_string();
152 async move {
153 let http_client = reqwest::Client::new();
154
155 // Test root endpoint
156 let root_url = format!("http://{}/", relay_domain);
157 let response = http_client
158 .get(&root_url)
159 .send()
160 .await
161 .map_err(|e| format!("Failed to GET root: {}", e))?;
162
163 check_cors_allow_headers(&response, "root endpoint")?;
164
165 // Test a repo path
166 let repo_url = format!(
167 "http://{}/npub1test/nonexistent.git/info/refs?service=git-upload-pack",
168 relay_domain
169 );
170 let response = http_client
171 .get(&repo_url)
172 .send()
173 .await
174 .map_err(|e| format!("Failed to GET repo endpoint: {}", e))?;
175
176 check_cors_allow_headers(&response, "git-upload-pack endpoint")?;
177
178 Ok(())
179 }
180 })
181 .await
182 }
183
184 /// Test: OPTIONS preflight requests return 204 No Content
185 ///
186 /// Spec: Line 47 of ../grasp/01.md
187 /// Requirement: Respond to OPTIONS requests with 204 No Content
188 pub async fn test_cors_options_preflight(
189 _client: &AuditClient,
190 relay_domain: &str,
191 ) -> TestResult {
192 TestResult::new(
193 "cors_options_preflight",
194 "GRASP-01:git-http:cors:47",
195 "OPTIONS requests return 204 No Content",
196 )
197 .run(|| {
198 let relay_domain = relay_domain.to_string();
199 async move {
200 let http_client = reqwest::Client::new();
201
202 // 1. Test OPTIONS on root endpoint
203 let root_url = format!("http://{}/", relay_domain);
204 let response = http_client
205 .request(reqwest::Method::OPTIONS, &root_url)
206 .header("Origin", "https://example.com")
207 .header("Access-Control-Request-Method", "POST")
208 .send()
209 .await
210 .map_err(|e| format!("Failed to OPTIONS root: {}", e))?;
211
212 check_options_response(&response, "root endpoint")?;
213
214 // 2. Test OPTIONS on git-upload-pack endpoint
215 let repo_url = format!(
216 "http://{}/npub1test/test.git/git-upload-pack",
217 relay_domain
218 );
219 let response = http_client
220 .request(reqwest::Method::OPTIONS, &repo_url)
221 .header("Origin", "https://example.com")
222 .header("Access-Control-Request-Method", "POST")
223 .send()
224 .await
225 .map_err(|e| format!("Failed to OPTIONS git-upload-pack: {}", e))?;
226
227 check_options_response(&response, "git-upload-pack endpoint")?;
228
229 // 3. Test OPTIONS on info/refs endpoint
230 let refs_url = format!(
231 "http://{}/npub1test/test.git/info/refs",
232 relay_domain
233 );
234 let response = http_client
235 .request(reqwest::Method::OPTIONS, &refs_url)
236 .header("Origin", "https://example.com")
237 .header("Access-Control-Request-Method", "GET")
238 .send()
239 .await
240 .map_err(|e| format!("Failed to OPTIONS info/refs: {}", e))?;
241
242 check_options_response(&response, "info/refs endpoint")?;
243
244 Ok(())
245 }
246 })
247 .await
248 }
249
250 // =========================================================================
251 // Integration test methods for use from external test files
252 // These match the pattern used by GitCloneTests
253 // =========================================================================
254
255 /// Integration test: CORS Allow-Origin header with repository creation
256 ///
257 /// For integration tests that want to test against real repositories
258 pub async fn test_cors_on_real_repo(
259 client: &AuditClient,
260 _git_data_dir: &Path,
261 relay_domain: &str,
262 ) -> TestResult {
263 let test_name = "test_cors_on_real_repo";
264 let ctx = TestContext::new(client);
265
266 // Create repository announcement to get a real repo path
267 let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await {
268 Ok(r) => r,
269 Err(e) => {
270 return TestResult::new(
271 test_name,
272 "GRASP-01",
273 "CORS headers on real repository endpoint",
274 )
275 .fail(&format!("Failed to create repo fixture: {}", e))
276 }
277 };
278
279 // Wait for repository creation
280 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
281
282 // Extract repo identifier and npub
283 let repo_id = match repo
284 .tags
285 .iter()
286 .find(|t| t.kind() == TagKind::d())
287 .and_then(|t| t.content())
288 {
289 Some(id) => id.to_string(),
290 None => {
291 return TestResult::new(
292 test_name,
293 "GRASP-01",
294 "CORS headers on real repository endpoint",
295 )
296 .fail("Repository announcement missing d tag")
297 }
298 };
299
300 let npub = match repo.pubkey.to_bech32() {
301 Ok(n) => n,
302 Err(e) => {
303 return TestResult::new(
304 test_name,
305 "GRASP-01",
306 "CORS headers on real repository endpoint",
307 )
308 .fail(&format!("Failed to convert pubkey to npub: {}", e))
309 }
310 };
311
312 // Test CORS on real repo endpoint
313 let http_client = reqwest::Client::new();
314 let info_refs_url = format!(
315 "http://{}/{}/{}.git/info/refs?service=git-upload-pack",
316 relay_domain, npub, repo_id
317 );
318
319 let response = match http_client.get(&info_refs_url).send().await {
320 Ok(r) => r,
321 Err(e) => {
322 return TestResult::new(
323 test_name,
324 "GRASP-01",
325 "CORS headers on real repository endpoint",
326 )
327 .fail(&format!("Failed to GET info/refs: {}", e))
328 }
329 };
330
331 // Check all CORS headers
332 if let Err(e) = check_cors_allow_origin(&response, "info/refs") {
333 return TestResult::new(
334 test_name,
335 "GRASP-01",
336 "CORS headers on real repository endpoint",
337 )
338 .fail(&e);
339 }
340
341 if let Err(e) = check_cors_allow_methods(&response, "info/refs") {
342 return TestResult::new(
343 test_name,
344 "GRASP-01",
345 "CORS headers on real repository endpoint",
346 )
347 .fail(&e);
348 }
349
350 if let Err(e) = check_cors_allow_headers(&response, "info/refs") {
351 return TestResult::new(
352 test_name,
353 "GRASP-01",
354 "CORS headers on real repository endpoint",
355 )
356 .fail(&e);
357 }
358
359 TestResult::new(
360 test_name,
361 "GRASP-01",
362 "CORS headers on real repository endpoint",
363 )
364 .pass()
365 }
366}
367
368// =========================================================================
369// Helper functions
370// =========================================================================
371
372/// Check Access-Control-Allow-Origin header
373fn check_cors_allow_origin(response: &reqwest::Response, context: &str) -> Result<(), String> {
374 let header = response
375 .headers()
376 .get("Access-Control-Allow-Origin")
377 .ok_or_else(|| format!("Missing Access-Control-Allow-Origin header on {}", context))?;
378
379 let value = header
380 .to_str()
381 .map_err(|e| format!("Invalid Access-Control-Allow-Origin header value: {}", e))?;
382
383 if value != "*" {
384 return Err(format!(
385 "Expected Access-Control-Allow-Origin: *, got: '{}' on {}",
386 value, context
387 ));
388 }
389
390 Ok(())
391}
392
393/// Check Access-Control-Allow-Methods header
394fn check_cors_allow_methods(response: &reqwest::Response, context: &str) -> Result<(), String> {
395 let header = response
396 .headers()
397 .get("Access-Control-Allow-Methods")
398 .ok_or_else(|| format!("Missing Access-Control-Allow-Methods header on {}", context))?;
399
400 let value = header
401 .to_str()
402 .map_err(|e| format!("Invalid Access-Control-Allow-Methods header value: {}", e))?;
403
404 // The header should contain at least GET and POST
405 // Value could be "GET, POST" or "GET,POST" or include other methods
406 let methods: Vec<&str> = value.split(',').map(|s| s.trim()).collect();
407
408 if !methods.contains(&"GET") || !methods.contains(&"POST") {
409 return Err(format!(
410 "Expected Access-Control-Allow-Methods to include GET and POST, got: '{}' on {}",
411 value, context
412 ));
413 }
414
415 Ok(())
416}
417
418/// Check Access-Control-Allow-Headers header
419fn check_cors_allow_headers(response: &reqwest::Response, context: &str) -> Result<(), String> {
420 let header = response
421 .headers()
422 .get("Access-Control-Allow-Headers")
423 .ok_or_else(|| format!("Missing Access-Control-Allow-Headers header on {}", context))?;
424
425 let value = header
426 .to_str()
427 .map_err(|e| format!("Invalid Access-Control-Allow-Headers header value: {}", e))?;
428
429 // The header should contain at least Content-Type (case-insensitive)
430 let headers_lower = value.to_lowercase();
431 if !headers_lower.contains("content-type") {
432 return Err(format!(
433 "Expected Access-Control-Allow-Headers to include Content-Type, got: '{}' on {}",
434 value, context
435 ));
436 }
437
438 Ok(())
439}
440
441/// Check OPTIONS preflight response
442fn check_options_response(response: &reqwest::Response, context: &str) -> Result<(), String> {
443 // 1. Verify 204 No Content status
444 if response.status().as_u16() != 204 {
445 return Err(format!(
446 "Expected 204 No Content for OPTIONS on {}, got: {} {}",
447 context,
448 response.status().as_u16(),
449 response.status().canonical_reason().unwrap_or("Unknown")
450 ));
451 }
452
453 // 2. Also verify CORS headers are present on OPTIONS response
454 check_cors_allow_origin(response, &format!("OPTIONS {}", context))?;
455 check_cors_allow_methods(response, &format!("OPTIONS {}", context))?;
456 check_cors_allow_headers(response, &format!("OPTIONS {}", context))?;
457
458 Ok(())
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use crate::AuditConfig;
465
466 #[tokio::test]
467 #[ignore] // Requires running relay
468 async fn test_grasp01_cors_against_relay() {
469 // Read relay URL from environment variable - must be supplied
470 let relay_url = std::env::var("RELAY_URL").expect(
471 "RELAY_URL environment variable must be set. Example: RELAY_URL=ws://localhost:18081",
472 );
473
474 // Extract domain from relay URL for HTTP requests
475 let relay_domain = relay_url
476 .replace("ws://", "")
477 .replace("wss://", "")
478 .trim_end_matches('/')
479 .to_string();
480
481 let config = AuditConfig::ci();
482 let client = AuditClient::new(&relay_url, config)
483 .await
484 .unwrap_or_else(|_| {
485 panic!(
486 "Failed to connect to relay at {}. Ensure relay is running and accessible. \
487 Try: docker run --rm -p 18081:8081 ghcr.io/danconwaydev/ngit-relay:latest",
488 relay_url
489 )
490 });
491
492 let results = CorsTests::run_all(&client, &relay_domain).await;
493 results.print_report();
494
495 // Assert all tests passed
496 assert!(
497 results.all_passed(),
498 "Some GRASP-01 CORS tests failed"
499 );
500 }
501
502 #[test]
503 fn test_module_exists() {
504 // Simple compilation test
505 assert!(true);
506 }
507} \ No newline at end of file
diff --git a/grasp-audit/src/specs/grasp01/mod.rs b/grasp-audit/src/specs/grasp01/mod.rs
index 495672a..8b29f8c 100644
--- a/grasp-audit/src/specs/grasp01/mod.rs
+++ b/grasp-audit/src/specs/grasp01/mod.rs
@@ -1,11 +1,13 @@
1//! GRASP-01 specification tests 1//! GRASP-01 specification tests
2 2
3pub mod cors;
3pub mod event_acceptance_policy; 4pub mod event_acceptance_policy;
4pub mod git_clone; 5pub mod git_clone;
5pub mod nip01_smoke; 6pub mod nip01_smoke;
6pub mod nip11_document; 7pub mod nip11_document;
7pub mod repository_creation; 8pub mod repository_creation;
8 9
10pub use cors::CorsTests;
9pub use event_acceptance_policy::EventAcceptancePolicyTests; 11pub use event_acceptance_policy::EventAcceptancePolicyTests;
10pub use git_clone::GitCloneTests; 12pub use git_clone::GitCloneTests;
11pub use nip01_smoke::Nip01SmokeTests; 13pub use nip01_smoke::Nip01SmokeTests;
diff --git a/src/http/mod.rs b/src/http/mod.rs
index c676bda..85b72f4 100644
--- a/src/http/mod.rs
+++ b/src/http/mod.rs
@@ -24,6 +24,19 @@ use base64::Engine;
24use crate::config::Config; 24use crate::config::Config;
25use crate::git; 25use crate::git;
26 26
27/// CORS headers required by GRASP-01 specification (lines 40-47)
28const CORS_ALLOW_ORIGIN: &str = "*";
29const CORS_ALLOW_METHODS: &str = "GET, POST";
30const CORS_ALLOW_HEADERS: &str = "Content-Type";
31
32/// Add CORS headers to a response builder
33fn add_cors_headers(builder: hyper::http::response::Builder) -> hyper::http::response::Builder {
34 builder
35 .header("Access-Control-Allow-Origin", CORS_ALLOW_ORIGIN)
36 .header("Access-Control-Allow-Methods", CORS_ALLOW_METHODS)
37 .header("Access-Control-Allow-Headers", CORS_ALLOW_HEADERS)
38}
39
27/// HTTP Service that serves both WebSocket (relay) and HTML landing page 40/// HTTP Service that serves both WebSocket (relay) and HTML landing page
28struct HttpService { 41struct HttpService {
29 relay: LocalRelay, 42 relay: LocalRelay,
@@ -47,12 +60,23 @@ impl Service<Request<Incoming>> for HttpService {
47 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>; 60 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
48 61
49 fn call(&self, req: Request<Incoming>) -> Self::Future { 62 fn call(&self, req: Request<Incoming>) -> Self::Future {
50 let base = Response::builder().header("server", "ngit-grasp"); 63 let base = add_cors_headers(Response::builder().header("server", "ngit-grasp"));
51 let path = req.uri().path().to_string(); 64 let path = req.uri().path().to_string();
52 let query = req.uri().query().map(|s| s.to_string()); 65 let query = req.uri().query().map(|s| s.to_string());
53 let method = req.method().clone(); 66 let method = req.method().clone();
54 let git_data_path = self.config.git_data_path.clone(); 67 let git_data_path = self.config.git_data_path.clone();
55 68
69 // Handle OPTIONS preflight requests (CORS)
70 // GRASP-01 spec line 47: Respond to OPTIONS with 204 No Content
71 if method == Method::OPTIONS {
72 return Box::pin(async move {
73 Ok(add_cors_headers(Response::builder().header("server", "ngit-grasp"))
74 .status(204)
75 .body(Full::new(Bytes::new()))
76 .unwrap())
77 });
78 }
79
56 // Check for Git HTTP requests first 80 // Check for Git HTTP requests first
57 if let Some((npub, identifier, subpath)) = git::parse_git_url(&path) { 81 if let Some((npub, identifier, subpath)) = git::parse_git_url(&path) {
58 let npub = npub.to_string(); 82 let npub = npub.to_string();
@@ -104,11 +128,24 @@ impl Service<Request<Incoming>> for HttpService {
104 }; 128 };
105 129
106 match result { 130 match result {
107 Ok(response) => Ok(response), 131 Ok(response) => {
132 // Add CORS headers to successful Git responses
133 let (parts, body) = response.into_parts();
134 Ok(add_cors_headers(Response::builder()
135 .status(parts.status))
136 .header("content-type", parts.headers.get("content-type")
137 .and_then(|v| v.to_str().ok())
138 .unwrap_or("application/octet-stream"))
139 .header("cache-control", parts.headers.get("cache-control")
140 .and_then(|v| v.to_str().ok())
141 .unwrap_or("no-cache"))
142 .body(body)
143 .unwrap())
144 }
108 Err(e) => { 145 Err(e) => {
109 tracing::error!("Git handler error: {}", e); 146 tracing::error!("Git handler error: {}", e);
110 let error_msg = format!("Git error: {}", e); 147 let error_msg = format!("Git error: {}", e);
111 Ok(Response::builder() 148 Ok(add_cors_headers(Response::builder())
112 .status(e.status_code()) 149 .status(e.status_code())
113 .body(Full::new(Bytes::from(error_msg))) 150 .body(Full::new(Bytes::from(error_msg)))
114 .unwrap()) 151 .unwrap())
@@ -133,10 +170,9 @@ impl Service<Request<Incoming>> for HttpService {
133 tracing::debug!("Serving NIP-11 relay information document to {}", self.remote); 170 tracing::debug!("Serving NIP-11 relay information document to {}", self.remote);
134 171
135 return Box::pin(async move { 172 return Box::pin(async move {
136 Ok(base 173 Ok(add_cors_headers(Response::builder().header("server", "ngit-grasp"))
137 .status(200) 174 .status(200)
138 .header("content-type", "application/nostr+json") 175 .header("content-type", "application/nostr+json")
139 .header("access-control-allow-origin", "*")
140 .body(Full::new(Bytes::from(json))) 176 .body(Full::new(Bytes::from(json)))
141 .unwrap()) 177 .unwrap())
142 }); 178 });
diff --git a/tests/cors.rs b/tests/cors.rs
new file mode 100644
index 0000000..9cce817
--- /dev/null
+++ b/tests/cors.rs
@@ -0,0 +1,95 @@
1//! CORS Integration Tests
2//!
3//! Tests that verify CORS headers are correctly set on Git HTTP backend responses.
4//!
5//! # Test Strategy
6//!
7//! - Each test runs in complete isolation with its own fresh relay instance
8//! - Uses macro to eliminate boilerplate while maintaining test isolation
9//! - Calls individual test methods from grasp-audit for minimal duplication
10//! - Automatic cleanup via TestRelay fixture (removes container and temp dirs)
11//!
12//! # Running Tests
13//!
14//! ```bash
15//! # Run all CORS tests
16//! cargo test --test cors
17//!
18//! # Run specific test
19//! cargo test --test cors test_cors_allow_origin
20//!
21//! # With output
22//! cargo test --test cors -- --nocapture
23//! ```
24
25mod common;
26
27use common::TestRelay;
28use grasp_audit::specs::grasp01::CorsTests;
29use grasp_audit::*;
30
31/// Macro to generate isolated CORS integration tests with relay domain
32///
33/// Each test runs with its own fresh relay instance to ensure complete isolation.
34macro_rules! isolated_cors_test {
35 ($test_name:ident) => {
36 #[tokio::test]
37 async fn $test_name() {
38 let relay = TestRelay::start().await;
39 let config = AuditConfig::ci();
40 let client = AuditClient::new(relay.url(), config)
41 .await
42 .expect("Failed to create audit client");
43
44 let result = CorsTests::$test_name(&client, &relay.domain()).await;
45
46 relay.stop().await;
47
48 assert!(
49 result.passed,
50 "{} failed: {}",
51 stringify!($test_name),
52 result.error.as_deref().unwrap_or("unknown error")
53 );
54 }
55 };
56}
57
58/// Macro for CORS tests that need git_data_dir (the full integration test)
59macro_rules! isolated_cors_test_with_repo {
60 ($test_name:ident) => {
61 #[tokio::test]
62 async fn $test_name() {
63 let relay = TestRelay::start().await;
64 let config = AuditConfig::ci();
65 let client = AuditClient::new(relay.url(), config)
66 .await
67 .expect("Failed to create audit client");
68
69 let result = CorsTests::$test_name(
70 &client,
71 relay.git_data_dir(),
72 &relay.domain(),
73 )
74 .await;
75
76 relay.stop().await;
77
78 assert!(
79 result.passed,
80 "{} failed: {}",
81 stringify!($test_name),
82 result.error.as_deref().unwrap_or("unknown error")
83 );
84 }
85 };
86}
87
88// Generate isolated tests for all CORS tests
89isolated_cors_test!(test_cors_allow_origin);
90isolated_cors_test!(test_cors_allow_methods);
91isolated_cors_test!(test_cors_allow_headers);
92isolated_cors_test!(test_cors_options_preflight);
93
94// Integration test that creates a real repository and tests CORS on it
95isolated_cors_test_with_repo!(test_cors_on_real_repo); \ No newline at end of file