Skip to content

Commit ce8a83d

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 084d115 commit ce8a83d

File tree

3 files changed

+204
-0
lines changed

3 files changed

+204
-0
lines changed

crates/starknet_transaction_prover/src/server/config_test.rs

Lines changed: 127 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;
@@ -286,3 +287,129 @@ fn env_var_sets_tls_key_file() {
286287
assert_eq!(args.tls_key_file, Some(PathBuf::from("/etc/ssl/key.pem")));
287288
}
288289

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

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)