diff --git a/crates/starknet_transaction_prover/src/server/config_test.rs b/crates/starknet_transaction_prover/src/server/config_test.rs index 288d1dbfdb8..fcc6feca035 100644 --- a/crates/starknet_transaction_prover/src/server/config_test.rs +++ b/crates/starknet_transaction_prover/src/server/config_test.rs @@ -1,7 +1,9 @@ use std::io::Write; +use std::net::IpAddr; use std::path::PathBuf; use std::sync::{Mutex, MutexGuard}; +use assert_matches::assert_matches; use clap::Parser; use rstest::rstest; use tempfile::NamedTempFile; @@ -77,24 +79,14 @@ fn rejects_request_limits_exceeding_semaphore_capacity() { assert!(matches!(error, ConfigError::InvalidArgument(_))); } -/// Happy-path cases: input origins -> expected normalized output. +/// `cors_test.rs` owns the normalization matrix; these cases only pin that `from_args` routes +/// origins through it. #[rstest] #[case::disabled(vec![], vec![])] -#[case::wildcard(vec!["*"], vec!["*"])] -#[case::multiple( - vec!["https://example.com", "http://localhost:5173"], - vec!["https://example.com", "http://localhost:5173"], +#[case::normalizes_and_deduplicates( + vec!["https://example.com", "https://example.com:443/"], + vec!["https://example.com"], )] -#[case::normalizes_default_port(vec!["https://example.com:443/"], vec!["https://example.com"])] -#[case::deduplicates( - vec!["https://example.com", "https://example.com:443/", "http://localhost:5173", "http://localhost:5173"], - vec!["https://example.com", "http://localhost:5173"], -)] -#[case::wildcard_takes_precedence( - vec!["https://example.com", "*", "http://localhost:5173"], - vec!["*"], -)] -#[case::wildcard_ignores_invalid(vec!["*", "://invalid-origin"], vec!["*"])] fn cors_allow_origin_valid_cases(#[case] input: Vec<&str>, #[case] expected: Vec<&str>) { let mut args = base_args(); args.cors_allow_origin = input.into_iter().map(String::from).collect(); @@ -105,13 +97,13 @@ fn cors_allow_origin_valid_cases(#[case] input: Vec<&str>, #[case] expected: Vec } #[test] -fn cors_allow_origin_rejects_path() { +fn cors_allow_origin_rejection_propagates() { let mut args = base_args(); args.cors_allow_origin = vec!["http://localhost:5173/path".to_string()]; let error = ServiceConfig::from_args(args).unwrap_err(); - assert!(matches!(error, ConfigError::InvalidArgument(_))); + assert_matches!(error, ConfigError::InvalidArgument(_)); } #[test] @@ -193,47 +185,46 @@ fn allows_zero_queue_wait_timeout_without_a_queue() { ServiceConfig::from_args(args).unwrap(); } -/// TLS configuration validation: partial TLS config is rejected, complete config is accepted. #[rstest] -#[case::cert_without_key( - Some("cert.pem".into()), - None, - None, - true -)] -#[case::key_without_cert( - None, - Some("key.pem".into()), - None, - true -)] -#[case::both_provided( - Some("cert.pem".into()), - Some("key.pem".into()), - None, - false -)] -#[case::neither_provided(None, None, None, false)] +#[case::cert_without_key(Some("cert.pem".into()), None, true)] +#[case::key_without_cert(None, Some("key.pem".into()), true)] +#[case::both_provided(Some("cert.pem".into()), Some("key.pem".into()), false)] +#[case::neither_provided(None, None, false)] fn tls_config_validation( #[case] tls_cert_file: Option, #[case] tls_key_file: Option, - #[case] config_file: Option, - #[case] expect_error: bool, + #[case] expect_incomplete_tls_config: bool, ) { let mut args = base_args(); args.tls_cert_file = tls_cert_file; args.tls_key_file = tls_key_file; - args.config_file = config_file; let result = ServiceConfig::from_args(args); - if expect_error { - assert!(matches!(result.unwrap_err(), ConfigError::IncompleteTlsConfig(_))); + if expect_incomplete_tls_config { + assert_matches!(result.unwrap_err(), ConfigError::IncompleteTlsConfig(_)); } else { result.unwrap(); } } +/// A config file supplying only `tls_cert_file`, with no CLI key to complete it, is the one +/// partial-TLS shape the CLI-only cases above cannot express. +#[test] +fn config_file_tls_cert_without_key_is_rejected() { + let mut config_file = NamedTempFile::new().unwrap(); + write!(config_file, r#"{{"rpc_node_url":"http://localhost:9545","tls_cert_file":"cert.pem"}}"#) + .unwrap(); + + let mut args = base_args(); + args.config_file = Some(config_file.path().to_path_buf()); + args.rpc_url = None; + + let error = ServiceConfig::from_args(args).unwrap_err(); + + assert_matches!(error, ConfigError::IncompleteTlsConfig(_)); +} + #[test] fn tls_transport_mode_is_https_when_both_files_provided() { let mut args = base_args(); @@ -342,3 +333,145 @@ fn env_var_sets_tls_key_file() { assert_eq!(args.tls_key_file, Some(PathBuf::from("/etc/ssl/key.pem"))); } + +#[test] +fn missing_rpc_url_rejected() { + let mut args = base_args(); + args.rpc_url = None; + + assert_matches!( + ServiceConfig::from_args(args).unwrap_err(), + ConfigError::MissingRequiredField(message) if message.contains("rpc_node_url") + ); +} + +#[rstest] +#[case::max_concurrent_requests_zero( + |args: &mut CliArgs| args.max_concurrent_requests = Some(0), + "max_concurrent_requests" +)] +#[case::max_connections_zero( + |args: &mut CliArgs| args.max_connections = Some(0), + "max_connections" +)] +#[case::max_request_body_size_zero( + |args: &mut CliArgs| args.max_request_body_size = Some(0), + "max_request_body_size" +)] +fn zero_limits_rejected(#[case] set_zero_limit: fn(&mut CliArgs), #[case] expected_message: &str) { + let mut args = base_args(); + set_zero_limit(&mut args); + + assert_matches!( + ServiceConfig::from_args(args).unwrap_err(), + ConfigError::InvalidArgument(message) if message.contains(expected_message) + ); +} + +#[test] +fn max_concurrent_requests_of_one_accepted() { + let mut args = base_args(); + args.max_concurrent_requests = Some(1); + + let config = ServiceConfig::from_args(args).unwrap(); + + assert_eq!(config.max_concurrent_requests, 1); +} + +#[rstest] +#[case::invalid_ip(|args: &mut CliArgs| args.ip = Some("not-an-ip".to_string()), "IP address")] +#[case::invalid_strk_fee_token_address( + |args: &mut CliArgs| args.strk_fee_token_address = Some("not-an-address".to_string()), + "strk_fee_token_address" +)] +#[case::invalid_blocking_check_url( + |args: &mut CliArgs| args.blocking_check_url = Some("not-a-url".to_string()), + "blocking_check_url" +)] +fn invalid_argument_parse_rejected( + #[case] set_invalid_value: fn(&mut CliArgs), + #[case] expected_message: &str, +) { + let mut args = base_args(); + set_invalid_value(&mut args); + + assert_matches!( + ServiceConfig::from_args(args).unwrap_err(), + ConfigError::InvalidArgument(message) if message.contains(expected_message) + ); +} + +#[test] +fn no_cors_with_cors_allow_origin_rejected() { + let mut args = base_args(); + args.no_cors = true; + args.cors_allow_origin = vec!["http://localhost:5173".to_string()]; + + assert_matches!( + ServiceConfig::from_args(args).unwrap_err(), + ConfigError::InvalidArgument(message) if message.contains("mutually exclusive") + ); +} + +#[test] +fn skip_fee_field_validation_disables_validation() { + let mut args = base_args(); + args.skip_fee_field_validation = true; + + let config = ServiceConfig::from_args(args).unwrap(); + + assert!(!config.prover_config.validate_zero_fee_fields); +} + +#[test] +fn no_cors_clears_config_file_origins() { + let mut config_file = NamedTempFile::new().unwrap(); + write!( + config_file, + r#"{{"rpc_node_url":"http://localhost:9545","cors_allow_origin":["http://localhost:5173"]}}"#, + ) + .unwrap(); + + let mut args = base_args(); + args.config_file = Some(config_file.path().to_path_buf()); + args.rpc_url = None; + args.no_cors = true; + + let config = ServiceConfig::from_args(args).unwrap(); + + assert!(config.cors_allow_origin.is_empty()); +} + +#[test] +fn config_file_values_used_when_no_cli_overrides() { + let mut config_file = NamedTempFile::new().unwrap(); + write!( + config_file, + r#"{{"rpc_node_url":"http://localhost:9545","port":8080,"ip":"127.0.0.1"}}"#, + ) + .unwrap(); + + let mut args = base_args(); + args.config_file = Some(config_file.path().to_path_buf()); + args.rpc_url = None; + + let config = ServiceConfig::from_args(args).unwrap(); + + assert_eq!(config.port, 8080); + assert_eq!(config.ip, "127.0.0.1".parse::().unwrap()); +} + +#[test] +fn cli_overrides_config_file_values() { + let mut config_file = NamedTempFile::new().unwrap(); + write!(config_file, r#"{{"rpc_node_url":"http://localhost:9545","port":8080}}"#).unwrap(); + + let mut args = base_args(); + args.config_file = Some(config_file.path().to_path_buf()); + args.rpc_url = None; + args.port = Some(9090); + + let config = ServiceConfig::from_args(args).unwrap(); + + assert_eq!(config.port, 9090); +}