Skip to content

Commit cf88612

Browse files
avi-starkwareclaude
andcommitted
starknet_transaction_prover: add CORS and config validation tests
Add 8 CORS tests (layer construction, mode labels, origin normalization edge cases) and 10 config validation tests (missing rpc_url, zero max_concurrent/max_connections, CORS conflicts, config file merging, CLI override precedence). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e9cdab3 commit cf88612

File tree

3 files changed

+205
-0
lines changed

3 files changed

+205
-0
lines changed

crates/starknet_transaction_prover/src/server/config_test.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::io::Write;
22
use std::path::PathBuf;
33
use std::sync::{Mutex, MutexGuard};
44

5+
use assert_matches::assert_matches;
56
use clap::Parser;
67
use rstest::rstest;
78
use tempfile::NamedTempFile;
@@ -296,3 +297,130 @@ fn env_var_sets_bouncer_config_override() {
296297

297298
assert_eq!(args.bouncer_config_override, Some(PathBuf::from("/tmp/bouncer.json")));
298299
}
300+
301+
#[test]
302+
fn test_missing_rpc_url_rejected() {
303+
let mut args = base_args();
304+
args.rpc_url = None; // No CLI arg, and no config file, so default empty rpc_node_url is used.
305+
306+
let error = ServiceConfig::from_args(args).unwrap_err();
307+
308+
assert_matches!(error, ConfigError::MissingRequiredField(_));
309+
}
310+
311+
#[test]
312+
fn test_max_concurrent_requests_zero_rejected() {
313+
let mut args = base_args();
314+
args.max_concurrent_requests = Some(0);
315+
316+
let error = ServiceConfig::from_args(args).unwrap_err();
317+
318+
assert_matches!(error, ConfigError::InvalidArgument(_));
319+
}
320+
321+
#[test]
322+
fn test_max_connections_zero_rejected() {
323+
let mut args = base_args();
324+
args.max_connections = Some(0);
325+
326+
let error = ServiceConfig::from_args(args).unwrap_err();
327+
328+
assert_matches!(error, ConfigError::InvalidArgument(_));
329+
}
330+
331+
#[test]
332+
fn test_no_cors_with_cors_allow_origin_rejected() {
333+
let mut args = base_args();
334+
args.no_cors = true;
335+
args.cors_allow_origin = vec!["http://localhost:5173".to_string()];
336+
337+
let error = ServiceConfig::from_args(args).unwrap_err();
338+
339+
let ConfigError::InvalidArgument(message) = error else {
340+
panic!("Expected ConfigError::InvalidArgument, got {error:?}");
341+
};
342+
assert!(message.contains("mutually exclusive"), "expected 'mutually exclusive' in: {message}");
343+
}
344+
345+
#[test]
346+
fn test_skip_fee_field_validation_disables_validation() {
347+
let mut args = base_args();
348+
args.skip_fee_field_validation = true;
349+
350+
let config = ServiceConfig::from_args(args).unwrap();
351+
352+
assert!(!config.prover_config.validate_zero_fee_fields);
353+
}
354+
355+
#[test]
356+
fn test_no_cors_clears_config_file_origins() {
357+
let mut config_file = NamedTempFile::new().unwrap();
358+
write!(
359+
config_file,
360+
r#"{{"rpc_node_url":"http://localhost:9545","cors_allow_origin":["http://localhost:5173"]}}"#,
361+
)
362+
.unwrap();
363+
364+
let mut args = base_args();
365+
args.config_file = Some(config_file.path().to_path_buf());
366+
args.rpc_url = None;
367+
args.no_cors = true;
368+
369+
let config = ServiceConfig::from_args(args).unwrap();
370+
371+
assert!(config.cors_allow_origin.is_empty());
372+
}
373+
374+
#[test]
375+
fn test_config_file_values_used_when_no_cli_overrides() {
376+
let mut config_file = NamedTempFile::new().unwrap();
377+
write!(
378+
config_file,
379+
r#"{{"rpc_node_url":"http://localhost:9545","port":8080,"ip":"127.0.0.1"}}"#,
380+
)
381+
.unwrap();
382+
383+
let mut args = base_args();
384+
args.config_file = Some(config_file.path().to_path_buf());
385+
args.rpc_url = None;
386+
387+
let config = ServiceConfig::from_args(args).unwrap();
388+
389+
assert_eq!(config.port, 8080);
390+
assert_eq!(config.ip, "127.0.0.1".parse::<std::net::IpAddr>().unwrap());
391+
}
392+
393+
#[test]
394+
fn test_cli_overrides_config_file_values() {
395+
let mut config_file = NamedTempFile::new().unwrap();
396+
write!(config_file, r#"{{"rpc_node_url":"http://localhost:9545","port":8080}}"#,).unwrap();
397+
398+
let mut args = base_args();
399+
args.config_file = Some(config_file.path().to_path_buf());
400+
args.rpc_url = None;
401+
args.port = Some(9090);
402+
403+
let config = ServiceConfig::from_args(args).unwrap();
404+
405+
assert_eq!(config.port, 9090);
406+
}
407+
408+
#[test]
409+
fn test_cors_rejects_non_http_scheme() {
410+
let mut args = base_args();
411+
args.cors_allow_origin = vec!["ftp://example.com".to_string()];
412+
413+
let error = ServiceConfig::from_args(args).unwrap_err();
414+
415+
assert_matches!(error, ConfigError::InvalidArgument(_));
416+
}
417+
418+
#[test]
419+
fn test_cors_rejects_origin_with_userinfo() {
420+
let mut args = base_args();
421+
args.cors_allow_origin = vec!["http://user@example.com".to_string()];
422+
423+
let error = ServiceConfig::from_args(args).unwrap_err();
424+
425+
assert_matches!(error, ConfigError::InvalidArgument(_));
426+
}

crates/starknet_transaction_prover/src/server/cors.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
//! CORS configuration utilities for the JSON-RPC server.
22
3+
#[cfg(test)]
4+
#[path = "cors_test.rs"]
5+
mod cors_test;
6+
37
use anyhow::Context;
48
use http::{header, HeaderValue, Method};
59
use tower_http::cors::{AllowOrigin, CorsLayer};
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use assert_matches::assert_matches;
2+
use rstest::rstest;
3+
4+
use crate::errors::ConfigError;
5+
use crate::server::cors::{build_cors_layer, cors_mode, normalize_cors_allow_origins};
6+
7+
#[test]
8+
fn test_build_cors_layer_returns_none_for_empty_origins() {
9+
let result = build_cors_layer(&[]).expect("build_cors_layer should not fail");
10+
assert!(result.is_none(), "Expected None for empty origins list");
11+
}
12+
13+
#[test]
14+
fn test_build_cors_layer_returns_some_for_wildcard() {
15+
let origins = vec!["*".to_string()];
16+
let result = build_cors_layer(&origins).expect("build_cors_layer should not fail");
17+
assert!(result.is_some(), "Expected Some for wildcard origin");
18+
}
19+
20+
#[test]
21+
fn test_build_cors_layer_returns_some_for_allowlist() {
22+
let origins = vec!["http://example.com".to_string()];
23+
let result = build_cors_layer(&origins).expect("build_cors_layer should not fail");
24+
assert!(result.is_some(), "Expected Some for non-empty allowlist");
25+
}
26+
27+
#[rstest]
28+
#[case::disabled(vec![], "disabled")]
29+
#[case::wildcard(vec!["*".to_string()], "wildcard")]
30+
#[case::allowlist(vec!["http://example.com".to_string()], "allowlist")]
31+
#[case::multiple_origins(vec!["http://a.com".to_string(), "http://b.com".to_string()], "allowlist")]
32+
fn test_cors_mode_labels(#[case] origins: Vec<String>, #[case] expected_label: &str) {
33+
assert_eq!(cors_mode(&origins), expected_label);
34+
}
35+
36+
#[test]
37+
fn test_normalize_rejects_ftp_scheme() {
38+
let result = normalize_cors_allow_origins(vec!["ftp://example.com".to_string()]);
39+
assert_matches!(result, Err(ConfigError::InvalidArgument(_)));
40+
}
41+
42+
#[test]
43+
fn test_normalize_rejects_missing_host() {
44+
let result = normalize_cors_allow_origins(vec!["http://".to_string()]);
45+
assert_matches!(result, Err(ConfigError::InvalidArgument(_)));
46+
}
47+
48+
#[test]
49+
fn test_normalize_rejects_userinfo() {
50+
let result = normalize_cors_allow_origins(vec!["http://user:pass@example.com".to_string()]);
51+
assert_matches!(result, Err(ConfigError::InvalidArgument(_)));
52+
}
53+
54+
#[test]
55+
fn test_normalize_strips_default_http_port() {
56+
let result = normalize_cors_allow_origins(vec!["http://example.com:80".to_string()])
57+
.expect("normalize should succeed");
58+
assert_eq!(result, vec!["http://example.com".to_string()]);
59+
}
60+
61+
#[test]
62+
fn test_normalize_strips_default_https_port() {
63+
let result = normalize_cors_allow_origins(vec!["https://example.com:443".to_string()])
64+
.expect("normalize should succeed");
65+
assert_eq!(result, vec!["https://example.com".to_string()]);
66+
}
67+
68+
#[test]
69+
fn test_normalize_preserves_non_default_port() {
70+
let result = normalize_cors_allow_origins(vec!["http://example.com:8080".to_string()])
71+
.expect("normalize should succeed");
72+
assert_eq!(result, vec!["http://example.com:8080".to_string()]);
73+
}

0 commit comments

Comments
 (0)