use std::error::Error;
use std::fs;
use runewarp::{
CLIENT_CERT_FILENAME, CLIENT_IDENTITY_FILENAME, CLIENT_KEY_FILENAME, ClientPublicCertConfig,
ClientRuntimeArgs, ClientSettingsResolutionDefaults, ClientSettingsResolutionError, LogLevel,
SelectedClientConfig, resolve_selected_client_settings,
};
use tempfile::tempdir;
#[test]
fn cli_only_resolution_uses_the_runtime_owned_client_retry_defaults() -> Result<(), Box<dyn Error>>
{
let tempdir = tempdir()?;
let identity_directory = tempdir.path().join("client-identity");
write_identity_material(&identity_directory)?;
let settings = resolve_selected_client_settings(
SelectedClientConfig::None,
&ClientRuntimeArgs {
server_address: Some("Tunnel.Example.Test.".to_owned()),
backend_address: Some("localhost:8443".to_owned()),
},
&ClientSettingsResolutionDefaults {
identity_directory: identity_directory.clone(),
public_cert_directory: tempdir.path().join("unused-public-cert"),
},
)?;
assert_eq!(settings.server_hostname, "tunnel.example.test");
assert_eq!(settings.server_port, 443);
assert_eq!(settings.log_level, LogLevel::Info);
assert_eq!(settings.server_ca_file, None);
assert_eq!(settings.identity_directory, identity_directory);
assert_eq!(settings.services.len(), 1);
assert_eq!(settings.services[0].public_hostnames, None);
assert_eq!(settings.services[0].backend_address, "localhost:8443");
Ok(())
}
#[test]
fn cli_only_resolution_requires_backend_address_without_a_selected_config()
-> Result<(), Box<dyn Error>> {
let tempdir = tempdir()?;
let identity_directory = tempdir.path().join("client-identity");
write_identity_material(&identity_directory)?;
let error = resolve_selected_client_settings(
SelectedClientConfig::None,
&ClientRuntimeArgs {
server_address: Some("tunnel.example.test".to_owned()),
backend_address: None,
},
&ClientSettingsResolutionDefaults {
identity_directory,
public_cert_directory: tempdir.path().join("unused-public-cert"),
},
)
.expect_err("expected backend-address validation error");
match error {
ClientSettingsResolutionError::Validation {
path: None,
messages,
} => {
assert_eq!(messages.len(), 1);
assert_eq!(
messages[0],
"--backend-address is required when no selected client config is available"
);
}
other => panic!("expected a CLI-only validation error, got {other}"),
}
Ok(())
}
#[test]
fn selected_config_without_a_client_section_can_still_resolve_from_runtime_flags()
-> Result<(), Box<dyn Error>> {
let tempdir = tempdir()?;
let identity_directory = tempdir.path().join("client-identity");
write_identity_material(&identity_directory)?;
fs::write(
tempdir.path().join("config.toml"),
r#"
[server]
hostname = "tunnel.example.test"
"#,
)?;
let settings = resolve_selected_client_settings(
SelectedClientConfig::Explicit(tempdir.path().join("config.toml")),
&ClientRuntimeArgs {
server_address: Some("tunnel.example.test:9443".to_owned()),
backend_address: Some("backend.internal:443".to_owned()),
},
&ClientSettingsResolutionDefaults {
identity_directory,
public_cert_directory: tempdir.path().join("unused-public-cert"),
},
)?;
assert_eq!(settings.server_hostname, "tunnel.example.test");
assert_eq!(settings.server_port, 9443);
assert_eq!(settings.log_level, LogLevel::Info);
assert_eq!(settings.services.len(), 1);
assert_eq!(settings.services[0].public_hostnames, None);
assert_eq!(settings.services[0].backend_address, "backend.internal:443");
Ok(())
}
#[test]
fn server_address_runtime_flag_overrides_a_selected_config_before_validation()
-> Result<(), Box<dyn Error>> {
let tempdir = tempdir()?;
let identity_directory = tempdir.path().join("client-identity");
write_identity_material(&identity_directory)?;
fs::write(
tempdir.path().join("config.toml"),
r#"
log-level = "off"
[client]
"#,
)?;
let settings = resolve_selected_client_settings(
SelectedClientConfig::Explicit(tempdir.path().join("config.toml")),
&ClientRuntimeArgs {
server_address: Some("Tunnel.Example.Test.".to_owned()),
backend_address: Some("backend.internal:443".to_owned()),
},
&ClientSettingsResolutionDefaults {
identity_directory,
public_cert_directory: tempdir.path().join("unused-public-cert"),
},
)?;
assert_eq!(settings.server_hostname, "tunnel.example.test");
assert_eq!(settings.server_port, 443);
assert_eq!(settings.log_level, LogLevel::Off);
assert_eq!(settings.services.len(), 1);
assert_eq!(settings.services[0].public_hostnames, None);
assert_eq!(settings.services[0].backend_address, "backend.internal:443");
Ok(())
}
#[test]
fn server_address_runtime_flag_rescues_an_invalid_selected_config_value()
-> Result<(), Box<dyn Error>> {
let tempdir = tempdir()?;
let identity_directory = tempdir.path().join("client-identity");
write_identity_material(&identity_directory)?;
fs::write(
tempdir.path().join("config.toml"),
r#"
log-level = "off"
[client]
server-address = "127.0.0.1:443"
"#,
)?;
let settings = resolve_selected_client_settings(
SelectedClientConfig::Explicit(tempdir.path().join("config.toml")),
&ClientRuntimeArgs {
server_address: Some("Tunnel.Example.Test.".to_owned()),
backend_address: Some("backend.internal:443".to_owned()),
},
&ClientSettingsResolutionDefaults {
identity_directory,
public_cert_directory: tempdir.path().join("unused-public-cert"),
},
)?;
assert_eq!(settings.server_hostname, "tunnel.example.test");
assert_eq!(settings.server_port, 443);
assert_eq!(settings.log_level, LogLevel::Off);
assert_eq!(settings.services.len(), 1);
assert_eq!(settings.services[0].public_hostnames, None);
assert_eq!(settings.services[0].backend_address, "backend.internal:443");
Ok(())
}
#[test]
fn backend_address_runtime_flag_is_rejected_when_selected_config_already_has_services()
-> Result<(), Box<dyn Error>> {
let tempdir = tempdir()?;
let identity_directory = tempdir.path().join("client-identity");
write_identity_material(&identity_directory)?;
fs::write(
tempdir.path().join("config.toml"),
r#"
[client]
server-address = "tunnel.example.test"
[[client.services]]
backend-address = "backend.internal:443"
"#,
)?;
let error = resolve_selected_client_settings(
SelectedClientConfig::Explicit(tempdir.path().join("config.toml")),
&ClientRuntimeArgs {
server_address: None,
backend_address: Some("override.internal:8443".to_owned()),
},
&ClientSettingsResolutionDefaults {
identity_directory,
public_cert_directory: tempdir.path().join("unused-public-cert"),
},
)
.expect_err("expected backend-address conflict");
let messages = error
.validation_messages()
.expect("selected config validation messages");
assert_eq!(messages.len(), 1);
assert_eq!(
messages[0],
"--backend-address may be used only when the selected config contributes no [[client.services]] blocks"
);
Ok(())
}
#[test]
fn malformed_selected_services_are_not_masked_by_backend_address_runtime_flags()
-> Result<(), Box<dyn Error>> {
let tempdir = tempdir()?;
let identity_directory = tempdir.path().join("client-identity");
write_identity_material(&identity_directory)?;
fs::write(
tempdir.path().join("config.toml"),
r#"
[client]
server-address = "tunnel.example.test"
[[client.services]]
public-hostnames = ["app.example.test"]
"#,
)?;
let error = resolve_selected_client_settings(
SelectedClientConfig::Explicit(tempdir.path().join("config.toml")),
&ClientRuntimeArgs {
server_address: None,
backend_address: Some("override.internal:8443".to_owned()),
},
&ClientSettingsResolutionDefaults {
identity_directory,
public_cert_directory: tempdir.path().join("unused-public-cert"),
},
)
.expect_err("expected malformed service validation");
let messages = error
.validation_messages()
.expect("selected config validation messages");
assert!(messages.contains(
&"--backend-address may be used only when the selected config contributes no [[client.services]] blocks"
.to_owned()
));
assert!(messages.contains(&"client.services[].backend-address is required".to_owned()));
Ok(())
}
#[test]
fn cli_only_resolution_rejects_ip_literal_server_addresses() -> Result<(), Box<dyn Error>> {
let tempdir = tempdir()?;
let identity_directory = tempdir.path().join("client-identity");
write_identity_material(&identity_directory)?;
let error = resolve_selected_client_settings(
SelectedClientConfig::None,
&ClientRuntimeArgs {
server_address: Some("127.0.0.1:443".to_owned()),
backend_address: Some("backend.internal:443".to_owned()),
},
&ClientSettingsResolutionDefaults {
identity_directory,
public_cert_directory: tempdir.path().join("unused-public-cert"),
},
)
.expect_err("expected IP literal validation");
let messages = error
.validation_messages()
.expect("CLI-only validation messages");
assert!(
messages.contains(
&"client.server-address is invalid: IP literals are not supported".to_owned()
)
);
Ok(())
}
#[test]
fn selected_config_uses_the_injected_default_public_cert_dir_for_terminate_mode()
-> Result<(), Box<dyn Error>> {
let tempdir = tempdir()?;
let identity_directory = tempdir.path().join("client-identity");
let public_cert_directory = tempdir.path().join("xdg-data/client/public-cert");
write_identity_material(&identity_directory)?;
fs::create_dir_all(&public_cert_directory)?;
fs::write(
tempdir.path().join("config.toml"),
r#"
[client]
server-address = "tunnel.example.test"
[[client.services]]
public-hostnames = ["app.example.test"]
backend-address = "backend.internal:443"
tls-mode = "terminate"
"#,
)?;
let settings = resolve_selected_client_settings(
SelectedClientConfig::Explicit(tempdir.path().join("config.toml")),
&ClientRuntimeArgs::default(),
&ClientSettingsResolutionDefaults {
identity_directory,
public_cert_directory: public_cert_directory.clone(),
},
)?;
assert!(matches!(
settings.public_cert_config,
Some(ClientPublicCertConfig::Manual { directory }) if directory == public_cert_directory
));
Ok(())
}
fn write_identity_material(path: &std::path::Path) -> Result<(), Box<dyn Error>> {
fs::create_dir_all(path)?;
fs::write(path.join(CLIENT_CERT_FILENAME), "placeholder certificate")?;
fs::write(path.join(CLIENT_KEY_FILENAME), "placeholder key")?;
fs::write(
path.join(CLIENT_IDENTITY_FILENAME),
"00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
)?;
Ok(())
}