use super::*;
use serial_test::serial;
#[test]
fn parse_docker_config_auth() {
let config_json = r#"{
"auths": {
"ghcr.io": {
"auth": "dXNlcjpwYXNz"
}
}
}"#;
let config: DockerConfig = serde_json::from_str(config_json).unwrap();
let auth = resolve_from_docker_auths(&config.auths, "ghcr.io").unwrap();
assert_eq!(auth.username, "user");
assert_eq!(auth.password, "pass");
}
#[test]
fn parse_docker_config_with_url_key() {
let config_json = r#"{
"auths": {
"https://index.docker.io/v1/": {
"auth": "ZG9ja2VyOnNlY3JldA=="
}
}
}"#;
let config: DockerConfig = serde_json::from_str(config_json).unwrap();
let auth = resolve_from_docker_auths(&config.auths, "docker.io").unwrap();
assert_eq!(auth.username, "docker");
assert_eq!(auth.password, "secret");
}
#[test]
fn parse_docker_config_no_auth() {
let config_json = r#"{ "auths": {} }"#;
let config: DockerConfig = serde_json::from_str(config_json).unwrap();
let auth = resolve_from_docker_auths(&config.auths, "ghcr.io");
assert!(auth.is_none());
}
#[test]
fn parse_docker_config_cred_helpers() {
let config_json = r#"{
"auths": {},
"credHelpers": {
"gcr.io": "gcloud",
"ghcr.io": "gh"
}
}"#;
let config: DockerConfig = serde_json::from_str(config_json).unwrap();
assert_eq!(config.cred_helpers.get("gcr.io").unwrap(), "gcloud");
assert_eq!(config.cred_helpers.get("ghcr.io").unwrap(), "gh");
}
#[test]
fn base64_encode_decode() {
let original = "user:password";
let encoded = base64_encode(original.as_bytes());
let decoded = base64_decode(&encoded).unwrap();
assert_eq!(String::from_utf8(decoded).unwrap(), original);
}
#[test]
fn base64_decode_known() {
let decoded = base64_decode("dXNlcjpwYXNz").unwrap();
assert_eq!(String::from_utf8(decoded).unwrap(), "user:pass");
}
#[test]
fn base64_empty() {
assert_eq!(base64_encode(b""), "");
assert_eq!(base64_decode("").unwrap(), Vec::<u8>::new());
}
#[test]
#[serial]
fn registry_auth_from_env() {
let old_user = std::env::var("REGISTRY_USERNAME").ok();
let old_pass = std::env::var("REGISTRY_PASSWORD").ok();
unsafe {
std::env::set_var("REGISTRY_USERNAME", "testuser");
std::env::set_var("REGISTRY_PASSWORD", "testpass");
}
let auth = RegistryAuth::resolve("any-registry.io").unwrap();
assert_eq!(auth.username, "testuser");
assert_eq!(auth.password, "testpass");
unsafe {
match old_user {
Some(v) => std::env::set_var("REGISTRY_USERNAME", v),
None => std::env::remove_var("REGISTRY_USERNAME"),
}
match old_pass {
Some(v) => std::env::set_var("REGISTRY_PASSWORD", v),
None => std::env::remove_var("REGISTRY_PASSWORD"),
}
}
}
#[test]
fn extract_auth_params() {
let header = r#"Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:myorg/mymod:pull""#;
assert_eq!(
extract_auth_param(header, "realm"),
Some("https://ghcr.io/token")
);
assert_eq!(extract_auth_param(header, "service"), Some("ghcr.io"));
assert_eq!(
extract_auth_param(header, "scope"),
Some("repository:myorg/mymod:pull")
);
assert_eq!(extract_auth_param(header, "nonexistent"), None);
}
#[test]
fn decode_docker_auth_valid() {
let result = decode_docker_auth("dXNlcjpwYXNz");
assert!(result.is_some());
let auth = result.unwrap();
assert_eq!(auth.username, "user");
assert_eq!(auth.password, "pass");
}
#[test]
fn decode_docker_auth_no_colon() {
let result = decode_docker_auth("bm9jb2xvbg=="); assert!(result.is_none());
}
#[test]
fn decode_docker_auth_empty_password() {
let result = decode_docker_auth("dXNlcjo=");
assert!(result.is_some());
let auth = result.unwrap();
assert_eq!(auth.username, "user");
assert_eq!(auth.password, "");
}
#[test]
fn decode_docker_auth_invalid_base64() {
let result = decode_docker_auth("!!!invalid!!!");
assert!(result.is_none());
}
#[test]
fn decode_docker_auth_password_with_colons() {
let result = decode_docker_auth("dXNlcjpwYTpzczp3b3Jk");
assert!(result.is_some());
let auth = result.unwrap();
assert_eq!(auth.username, "user");
assert_eq!(auth.password, "pa:ss:word");
}
#[test]
fn decode_docker_auth_empty_username_rejected() {
let result = decode_docker_auth("OnBhc3N3b3Jk");
assert!(result.is_none());
}
#[test]
fn resolve_from_docker_auths_tries_https_prefix() {
let mut auths = HashMap::new();
auths.insert(
"https://registry.example.com".to_string(),
DockerAuthEntry {
auth: Some("dXNlcjpwYXNz".to_string()), },
);
let result = resolve_from_docker_auths(&auths, "registry.example.com");
assert!(result.is_some());
let auth = result.unwrap();
assert_eq!(auth.username, "user");
assert_eq!(auth.password, "pass");
}
#[test]
fn resolve_from_docker_auths_tries_v2_suffix() {
let mut auths = HashMap::new();
auths.insert(
"https://myregistry.io/v2/".to_string(),
DockerAuthEntry {
auth: Some("dXNlcjpwYXNz".to_string()),
},
);
let result = resolve_from_docker_auths(&auths, "myregistry.io");
assert!(result.is_some());
assert_eq!(result.unwrap().username, "user");
}
#[test]
fn resolve_from_docker_auths_no_match() {
let mut auths = HashMap::new();
auths.insert(
"https://other.io".to_string(),
DockerAuthEntry {
auth: Some("dXNlcjpwYXNz".to_string()),
},
);
let result = resolve_from_docker_auths(&auths, "registry.example.com");
assert!(result.is_none());
}
#[test]
fn resolve_from_docker_auths_v1_suffix() {
let mut auths = HashMap::new();
auths.insert(
"https://myregistry.io/v1/".to_string(),
DockerAuthEntry {
auth: Some("dXNlcjpwYXNz".to_string()), },
);
let result = resolve_from_docker_auths(&auths, "myregistry.io");
assert!(result.is_some(), "should match v1 URL suffix");
let auth = result.unwrap();
assert_eq!(auth.username, "user");
assert_eq!(auth.password, "pass");
}
#[test]
fn resolve_from_docker_auths_exact_hostname_match() {
let mut auths = HashMap::new();
auths.insert(
"ghcr.io".to_string(),
DockerAuthEntry {
auth: Some("dXNlcjpwYXNz".to_string()),
},
);
let result = resolve_from_docker_auths(&auths, "ghcr.io");
assert!(result.is_some(), "should match exact hostname");
assert_eq!(result.unwrap().username, "user");
}
#[test]
fn resolve_from_docker_auths_docker_io_fallback_to_index() {
let mut auths = HashMap::new();
auths.insert(
"index.docker.io".to_string(),
DockerAuthEntry {
auth: Some("ZG9ja2VyOnNlY3JldA==".to_string()), },
);
let result = resolve_from_docker_auths(&auths, "docker.io");
assert!(
result.is_some(),
"docker.io should fall back to index.docker.io"
);
let auth = result.unwrap();
assert_eq!(auth.username, "docker");
assert_eq!(auth.password, "secret");
}
#[test]
fn resolve_from_docker_auths_index_docker_io_fallback() {
let mut auths = HashMap::new();
auths.insert(
"https://index.docker.io/v1/".to_string(),
DockerAuthEntry {
auth: Some("ZG9ja2VyOnNlY3JldA==".to_string()), },
);
let result = resolve_from_docker_auths(&auths, "index.docker.io");
assert!(
result.is_some(),
"index.docker.io should match https://index.docker.io/v1/ entry"
);
assert_eq!(result.unwrap().username, "docker");
}
#[test]
fn resolve_from_docker_auths_entry_with_null_auth_field() {
let mut auths = HashMap::new();
auths.insert("ghcr.io".to_string(), DockerAuthEntry { auth: None });
let result = resolve_from_docker_auths(&auths, "ghcr.io");
assert!(
result.is_none(),
"entry with null auth field should not resolve"
);
}
#[test]
fn resolve_from_docker_auths_entry_with_invalid_base64() {
let mut auths = HashMap::new();
auths.insert(
"ghcr.io".to_string(),
DockerAuthEntry {
auth: Some("not-valid-base64!!!".to_string()),
},
);
let result = resolve_from_docker_auths(&auths, "ghcr.io");
assert!(
result.is_none(),
"entry with invalid base64 should not resolve"
);
}
#[test]
fn resolve_from_docker_auths_prefers_exact_match_over_url() {
let mut auths = HashMap::new();
auths.insert(
"ghcr.io".to_string(),
DockerAuthEntry {
auth: Some("ZXhhY3Q6bWF0Y2g=".to_string()), },
);
auths.insert(
"https://ghcr.io".to_string(),
DockerAuthEntry {
auth: Some("dXJsOm1hdGNo".to_string()), },
);
let result = resolve_from_docker_auths(&auths, "ghcr.io");
assert!(result.is_some());
assert_eq!(result.unwrap().username, "exact");
}
#[test]
fn get_bearer_token_parses_www_authenticate() {
let mut server = mockito::Server::new();
let www_auth = format!(
r#"Bearer realm="{}/auth/token",service="registry.example.com",scope="repository:lib/test:pull,push""#,
server.url()
);
server
.mock(
"GET",
mockito::Matcher::Regex(r"/auth/token\?service=.*&scope=.*".to_string()),
)
.with_status(200)
.with_body(r#"{"token":"tok-abc-123"}"#)
.create();
let agent = ureq::AgentBuilder::new()
.timeout(std::time::Duration::from_secs(10))
.build();
let result = get_bearer_token(&agent, &www_auth, None);
assert!(
result.is_ok(),
"get_bearer_token failed: {:?}",
result.err()
);
assert_eq!(result.unwrap(), "tok-abc-123");
}
#[test]
fn get_bearer_token_uses_access_token_field() {
let mut server = mockito::Server::new();
let www_auth = format!(r#"Bearer realm="{}/token",service="svc""#, server.url());
server
.mock(
"GET",
mockito::Matcher::Regex(r"/token\?service=.*".to_string()),
)
.with_status(200)
.with_body(r#"{"access_token":"alt-token-456"}"#)
.create();
let agent = ureq::AgentBuilder::new()
.timeout(std::time::Duration::from_secs(10))
.build();
let result = get_bearer_token(&agent, &www_auth, None);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "alt-token-456");
}
#[test]
fn get_bearer_token_fails_without_realm() {
let agent = ureq::AgentBuilder::new().build();
let result = get_bearer_token(&agent, "Bearer service=\"svc\"", None);
assert!(result.is_err());
assert!(
matches!(result, Err(OciError::AuthFailed { .. })),
"expected AuthFailed variant"
);
}
#[test]
fn get_bearer_token_fails_on_non_json_token_response() {
let mut server = mockito::Server::new();
let www_auth = format!(r#"Bearer realm="{}/token",service="svc""#, server.url());
server
.mock(
"GET",
mockito::Matcher::Regex(r"/token\?service=.*".to_string()),
)
.with_status(200)
.with_body("not json at all")
.create();
let agent = ureq::AgentBuilder::new()
.timeout(std::time::Duration::from_secs(10))
.build();
let result = get_bearer_token(&agent, &www_auth, None);
assert!(matches!(result, Err(OciError::AuthFailed { .. })));
}
#[test]
fn get_bearer_token_sends_basic_auth_when_provided() {
let mut server = mockito::Server::new();
let www_auth = format!(r#"Bearer realm="{}/token",service="svc""#, server.url());
let auth = RegistryAuth {
username: "user".to_string(),
password: "pass".to_string(),
};
server
.mock(
"GET",
mockito::Matcher::Regex(r"/token\?service=.*".to_string()),
)
.match_header(
"Authorization",
mockito::Matcher::Regex("Basic .*".to_string()),
)
.with_status(200)
.with_body(r#"{"token":"authed-token"}"#)
.create();
let agent = ureq::AgentBuilder::new()
.timeout(std::time::Duration::from_secs(10))
.build();
let result = get_bearer_token(&agent, &www_auth, Some(&auth));
assert!(result.is_ok());
assert_eq!(result.unwrap(), "authed-token");
}
#[test]
#[serial]
fn docker_config_path_uses_env() {
let prev = std::env::var("DOCKER_CONFIG").ok();
let tmp = tempfile::tempdir().unwrap();
unsafe {
std::env::set_var("DOCKER_CONFIG", tmp.path().to_str().unwrap());
}
let path = docker_config_path();
assert_eq!(path, tmp.path().join("config.json"));
unsafe {
match prev {
Some(v) => std::env::set_var("DOCKER_CONFIG", v),
None => std::env::remove_var("DOCKER_CONFIG"),
}
}
}
#[test]
fn docker_config_empty_json() {
let config: DockerConfig = serde_json::from_str("{}").unwrap();
assert!(config.auths.is_empty());
assert!(config.cred_helpers.is_empty());
}
#[test]
fn docker_config_with_only_cred_helpers() {
let json = r#"{"credHelpers":{"gcr.io":"gcloud"}}"#;
let config: DockerConfig = serde_json::from_str(json).unwrap();
assert!(config.auths.is_empty());
assert_eq!(config.cred_helpers.len(), 1);
assert_eq!(config.cred_helpers.get("gcr.io").unwrap(), "gcloud");
}
#[test]
fn extract_auth_param_empty_value() {
let header = r#"Bearer realm="",service="svc""#;
assert_eq!(extract_auth_param(header, "realm"), Some(""));
}
#[test]
fn extract_auth_param_no_quotes() {
let header = "Bearer realm_value=noquotes";
assert_eq!(extract_auth_param(header, "realm"), None);
}
#[test]
fn extract_auth_param_url_with_special_chars() {
let header = r#"Bearer realm="https://auth.example.com/token?foo=bar",service="svc""#;
let realm = extract_auth_param(header, "realm").unwrap();
assert_eq!(realm, "https://auth.example.com/token?foo=bar");
}
#[test]
fn docker_config_with_empty_auth_field() {
let config_json = r#"{
"auths": {
"ghcr.io": {
"auth": ""
}
}
}"#;
let config: DockerConfig = serde_json::from_str(config_json).unwrap();
let auth = resolve_from_docker_auths(&config.auths, "ghcr.io");
assert!(auth.is_none(), "empty auth field should not resolve");
}
#[test]
fn docker_config_index_docker_io_fallback() {
let config_json = r#"{
"auths": {
"index.docker.io": {
"auth": "dXNlcjpwYXNz"
}
}
}"#;
let config: DockerConfig = serde_json::from_str(config_json).unwrap();
let auth = resolve_from_docker_auths(&config.auths, "docker.io");
assert!(
auth.is_some(),
"docker.io should fall back to index.docker.io"
);
assert_eq!(auth.unwrap().username, "user");
}
#[test]
fn docker_config_v1_suffix_match() {
let config_json = r#"{
"auths": {
"https://ghcr.io/v1/": {
"auth": "dXNlcjpwYXNz"
}
}
}"#;
let config: DockerConfig = serde_json::from_str(config_json).unwrap();
let auth = resolve_from_docker_auths(&config.auths, "ghcr.io");
assert!(auth.is_some(), "should match https://ghcr.io/v1/");
}
#[test]
fn base64_encode_single_byte() {
let encoded = base64_encode(b"a");
let decoded = base64_decode(&encoded).unwrap();
assert_eq!(decoded, b"a");
}
#[test]
fn base64_encode_two_bytes() {
let encoded = base64_encode(b"ab");
let decoded = base64_decode(&encoded).unwrap();
assert_eq!(decoded, b"ab");
}
#[test]
fn base64_encode_three_bytes() {
let encoded = base64_encode(b"abc");
assert!(!encoded.contains('='));
let decoded = base64_decode(&encoded).unwrap();
assert_eq!(decoded, b"abc");
}
#[test]
fn base64_encode_binary_data() {
let data: Vec<u8> = (0..=255).collect();
let encoded = base64_encode(&data);
let decoded = base64_decode(&encoded).unwrap();
assert_eq!(decoded, data);
}
#[test]
fn base64_decode_with_whitespace() {
let decoded = base64_decode(" dXNlcjpwYXNz ").unwrap();
assert_eq!(String::from_utf8(decoded).unwrap(), "user:pass");
}
#[test]
fn base64_decode_invalid_length() {
assert!(base64_decode("abc").is_none());
assert!(base64_decode("abcde").is_none());
}
#[test]
fn base64_decode_invalid_chars() {
assert!(base64_decode("ab~d").is_none());
}
#[test]
fn registry_auth_basic_auth_header_format() {
let auth = RegistryAuth {
username: "testuser".to_string(),
password: "testpass".to_string(),
};
let header = auth.basic_auth_header();
assert!(header.starts_with("Basic "));
let encoded_part = header.strip_prefix("Basic ").unwrap();
let decoded = base64_decode(encoded_part).unwrap();
let decoded_str = String::from_utf8(decoded).unwrap();
assert_eq!(decoded_str, "testuser:testpass");
}
#[test]
fn registry_auth_basic_auth_header_special_chars() {
let auth = RegistryAuth {
username: "user".to_string(),
password: "p@ss:w0rd!".to_string(),
};
let header = auth.basic_auth_header();
let encoded_part = header.strip_prefix("Basic ").unwrap();
let decoded = base64_decode(encoded_part).unwrap();
let decoded_str = String::from_utf8(decoded).unwrap();
assert_eq!(decoded_str, "user:p@ss:w0rd!");
}
#[test]
fn base64_encode_padding_one_byte() {
let encoded = base64_encode(b"a");
assert_eq!(encoded, "YQ==");
let decoded = base64_decode(&encoded).unwrap();
assert_eq!(decoded, b"a");
}
#[test]
fn base64_encode_padding_two_bytes() {
let encoded = base64_encode(b"ab");
assert_eq!(encoded, "YWI=");
let decoded = base64_decode(&encoded).unwrap();
assert_eq!(decoded, b"ab");
}
#[test]
fn base64_encode_no_padding_three_bytes() {
let encoded = base64_encode(b"abc");
assert_eq!(encoded, "YWJj");
let decoded = base64_decode(&encoded).unwrap();
assert_eq!(decoded, b"abc");
}
#[test]
fn base64_decode_invalid_length_short() {
let result = base64_decode("abc");
assert!(result.is_none(), "invalid length should fail");
}
#[test]
fn base64_decode_invalid_chars_special() {
let result = base64_decode("!!!!");
assert!(result.is_none(), "invalid base64 characters should fail");
}
#[test]
fn base64_roundtrip_binary_data() {
let data: Vec<u8> = (0..=255).collect();
let encoded = base64_encode(&data);
let decoded = base64_decode(&encoded).unwrap();
assert_eq!(decoded, data);
}
#[test]
fn registry_auth_basic_header_format() {
let auth = RegistryAuth {
username: "myuser".to_string(),
password: "mypass".to_string(),
};
let header = auth.basic_auth_header();
assert!(header.starts_with("Basic "), "should start with 'Basic '");
let encoded = header.strip_prefix("Basic ").unwrap();
let decoded = base64_decode(encoded).unwrap();
let cred = String::from_utf8(decoded).unwrap();
assert_eq!(cred, "myuser:mypass");
}
#[test]
fn extract_auth_param_missing_param() {
let header = r#"Bearer realm="https://auth.example.com/token""#;
assert!(extract_auth_param(header, "service").is_none());
assert!(extract_auth_param(header, "scope").is_none());
assert!(extract_auth_param(header, "realm").is_some());
}
#[test]
fn extract_auth_param_empty_value_parsed() {
let header = r#"Bearer realm="",service="svc""#;
assert_eq!(extract_auth_param(header, "realm"), Some(""));
assert_eq!(extract_auth_param(header, "service"), Some("svc"));
}
#[test]
fn decode_docker_auth_with_token_password() {
let input = "user:ghp_abc123xyz";
let encoded = base64_encode(input.as_bytes());
let result = decode_docker_auth(&encoded);
assert!(result.is_some());
let auth = result.unwrap();
assert_eq!(auth.username, "user");
assert_eq!(auth.password, "ghp_abc123xyz");
}
#[test]
#[serial]
fn registry_auth_resolve_reads_docker_config_file() {
use crate::test_helpers::EnvVarGuard;
let _user = EnvVarGuard::unset("REGISTRY_USERNAME");
let _pass = EnvVarGuard::unset("REGISTRY_PASSWORD");
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("config.json"),
r#"{"auths":{"my.reg.example":{"auth":"dXNlcjpwYXNz"}}}"#,
)
.unwrap();
let _dc = EnvVarGuard::set("DOCKER_CONFIG", tmp.path().to_str().unwrap());
let auth = RegistryAuth::resolve("my.reg.example").expect("file-based resolve must succeed");
assert_eq!(auth.username, "user");
assert_eq!(auth.password, "pass");
}
#[test]
#[serial]
fn registry_auth_resolve_returns_none_when_no_match_in_config() {
use crate::test_helpers::EnvVarGuard;
let _user = EnvVarGuard::unset("REGISTRY_USERNAME");
let _pass = EnvVarGuard::unset("REGISTRY_PASSWORD");
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("config.json"),
r#"{"auths":{"other.example":{"auth":"dXNlcjpwYXNz"}}}"#,
)
.unwrap();
let _dc = EnvVarGuard::set("DOCKER_CONFIG", tmp.path().to_str().unwrap());
let result = RegistryAuth::resolve("missing.example");
assert!(
result.is_none(),
"no matching entry must return None, got: {:?}",
result.map(|a| a.username)
);
}
#[test]
#[serial]
fn registry_auth_resolve_returns_none_when_env_vars_empty() {
use crate::test_helpers::EnvVarGuard;
let _user = EnvVarGuard::set("REGISTRY_USERNAME", "");
let _pass = EnvVarGuard::set("REGISTRY_PASSWORD", "");
let tmp = tempfile::tempdir().unwrap();
let _dc = EnvVarGuard::set("DOCKER_CONFIG", tmp.path().to_str().unwrap());
let result = RegistryAuth::resolve("any.example");
assert!(
result.is_none(),
"empty env vars + no config must return None"
);
}
#[test]
#[serial]
fn registry_auth_resolve_credential_helper_branch_invokes_helper() {
use crate::test_helpers::EnvVarGuard;
let _user = EnvVarGuard::unset("REGISTRY_USERNAME");
let _pass = EnvVarGuard::unset("REGISTRY_PASSWORD");
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("config.json"),
r#"{"auths":{},"credHelpers":{"helper.example":"cfgd-test-no-such-helper-binary"}}"#,
)
.unwrap();
let _dc = EnvVarGuard::set("DOCKER_CONFIG", tmp.path().to_str().unwrap());
let result = RegistryAuth::resolve("helper.example");
assert!(result.is_none());
}
#[test]
fn get_bearer_token_response_with_no_token_returns_auth_failed() {
let mut server = mockito::Server::new();
let www_auth = format!(r#"Bearer realm="{}/token",service="svc""#, server.url());
server
.mock(
"GET",
mockito::Matcher::Regex(r"/token\?service=.*".to_string()),
)
.with_status(200)
.with_body(r#"{"other":"field"}"#)
.create();
let agent = ureq::AgentBuilder::new()
.timeout(std::time::Duration::from_secs(10))
.build();
let result = get_bearer_token(&agent, &www_auth, None);
assert!(matches!(result, Err(OciError::AuthFailed { .. })));
let msg = format!("{:?}", result.err().unwrap());
assert!(msg.contains("no token"), "must mention no token: {msg}");
}
#[test]
fn get_bearer_token_handles_failed_http_call() {
let www_auth = r#"Bearer realm="http://127.0.0.1:1/token",service="svc""#;
let agent = ureq::AgentBuilder::new()
.timeout(std::time::Duration::from_millis(500))
.build();
let result = get_bearer_token(&agent, www_auth, None);
assert!(matches!(result, Err(OciError::AuthFailed { .. })));
}