mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 13:50:31 +00:00
test: add 82 unit tests across 7 modules
Coverage targets: - activity_log: ActionType display, ActivityLog push/recent/all/bounded - audit: AuditEntry, AuditLog write/read with tempdir - config: defaults for all sub-configs, env overrides, TOML parsing - dashboard_metrics: record_download/upload, cache_hit_rate, persistence - error: constructors, Display, IntoResponse for all variants - metrics: detect_registry for all protocol paths - repo_index: paginate, RegistryIndex basics, RepoIndex invalidate Total tests: 103 -> 185
This commit is contained in:
@@ -99,3 +99,139 @@ impl Default for ActivityLog {
|
||||
Self::new(50)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_action_type_display() {
|
||||
assert_eq!(ActionType::Pull.to_string(), "PULL");
|
||||
assert_eq!(ActionType::Push.to_string(), "PUSH");
|
||||
assert_eq!(ActionType::CacheHit.to_string(), "CACHE");
|
||||
assert_eq!(ActionType::ProxyFetch.to_string(), "PROXY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_type_equality() {
|
||||
assert_eq!(ActionType::Pull, ActionType::Pull);
|
||||
assert_ne!(ActionType::Pull, ActionType::Push);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_activity_entry_new() {
|
||||
let entry = ActivityEntry::new(
|
||||
ActionType::Pull,
|
||||
"nginx:latest".to_string(),
|
||||
"docker",
|
||||
"LOCAL",
|
||||
);
|
||||
assert_eq!(entry.action, ActionType::Pull);
|
||||
assert_eq!(entry.artifact, "nginx:latest");
|
||||
assert_eq!(entry.registry, "docker");
|
||||
assert_eq!(entry.source, "LOCAL");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_activity_log_push_and_len() {
|
||||
let log = ActivityLog::new(10);
|
||||
assert!(log.is_empty());
|
||||
assert_eq!(log.len(), 0);
|
||||
|
||||
log.push(ActivityEntry::new(
|
||||
ActionType::Push,
|
||||
"test:v1".to_string(),
|
||||
"docker",
|
||||
"LOCAL",
|
||||
));
|
||||
assert!(!log.is_empty());
|
||||
assert_eq!(log.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_activity_log_recent() {
|
||||
let log = ActivityLog::new(10);
|
||||
for i in 0..5 {
|
||||
log.push(ActivityEntry::new(
|
||||
ActionType::Pull,
|
||||
format!("image:{}", i),
|
||||
"docker",
|
||||
"LOCAL",
|
||||
));
|
||||
}
|
||||
|
||||
let recent = log.recent(3);
|
||||
assert_eq!(recent.len(), 3);
|
||||
// newest first
|
||||
assert_eq!(recent[0].artifact, "image:4");
|
||||
assert_eq!(recent[1].artifact, "image:3");
|
||||
assert_eq!(recent[2].artifact, "image:2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_activity_log_all() {
|
||||
let log = ActivityLog::new(10);
|
||||
for i in 0..3 {
|
||||
log.push(ActivityEntry::new(
|
||||
ActionType::Pull,
|
||||
format!("pkg:{}", i),
|
||||
"npm",
|
||||
"PROXY",
|
||||
));
|
||||
}
|
||||
|
||||
let all = log.all();
|
||||
assert_eq!(all.len(), 3);
|
||||
assert_eq!(all[0].artifact, "pkg:2"); // newest first
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_activity_log_bounded_size() {
|
||||
let log = ActivityLog::new(3);
|
||||
for i in 0..5 {
|
||||
log.push(ActivityEntry::new(
|
||||
ActionType::Pull,
|
||||
format!("item:{}", i),
|
||||
"cargo",
|
||||
"CACHE",
|
||||
));
|
||||
}
|
||||
|
||||
assert_eq!(log.len(), 3);
|
||||
let all = log.all();
|
||||
// oldest entries should be dropped
|
||||
assert_eq!(all[0].artifact, "item:4");
|
||||
assert_eq!(all[1].artifact, "item:3");
|
||||
assert_eq!(all[2].artifact, "item:2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_activity_log_recent_more_than_available() {
|
||||
let log = ActivityLog::new(10);
|
||||
log.push(ActivityEntry::new(
|
||||
ActionType::Push,
|
||||
"one".to_string(),
|
||||
"maven",
|
||||
"LOCAL",
|
||||
));
|
||||
|
||||
let recent = log.recent(100);
|
||||
assert_eq!(recent.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_activity_log_default() {
|
||||
let log = ActivityLog::default();
|
||||
assert!(log.is_empty());
|
||||
// default capacity is 50
|
||||
for i in 0..60 {
|
||||
log.push(ActivityEntry::new(
|
||||
ActionType::Pull,
|
||||
format!("x:{}", i),
|
||||
"docker",
|
||||
"LOCAL",
|
||||
));
|
||||
}
|
||||
assert_eq!(log.len(), 50);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,3 +71,69 @@ impl AuditLog {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_audit_entry_new() {
|
||||
let entry = AuditEntry::new(
|
||||
"push",
|
||||
"admin",
|
||||
"nginx:latest",
|
||||
"docker",
|
||||
"uploaded manifest",
|
||||
);
|
||||
assert_eq!(entry.action, "push");
|
||||
assert_eq!(entry.actor, "admin");
|
||||
assert_eq!(entry.artifact, "nginx:latest");
|
||||
assert_eq!(entry.registry, "docker");
|
||||
assert_eq!(entry.detail, "uploaded manifest");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_log_new_and_path() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let log = AuditLog::new(tmp.path().to_str().unwrap());
|
||||
assert!(log.path().ends_with("audit.jsonl"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_log_write_entry() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let log = AuditLog::new(tmp.path().to_str().unwrap());
|
||||
|
||||
let entry = AuditEntry::new("pull", "user1", "lodash", "npm", "downloaded");
|
||||
log.log(entry);
|
||||
|
||||
// Verify file contains the entry
|
||||
let content = std::fs::read_to_string(log.path()).unwrap();
|
||||
assert!(content.contains(r#""action":"pull""#));
|
||||
assert!(content.contains(r#""actor":"user1""#));
|
||||
assert!(content.contains(r#""artifact":"lodash""#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_log_multiple_entries() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let log = AuditLog::new(tmp.path().to_str().unwrap());
|
||||
|
||||
log.log(AuditEntry::new("push", "admin", "a", "docker", ""));
|
||||
log.log(AuditEntry::new("pull", "user", "b", "npm", ""));
|
||||
log.log(AuditEntry::new("delete", "admin", "c", "maven", ""));
|
||||
|
||||
let content = std::fs::read_to_string(log.path()).unwrap();
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
assert_eq!(lines.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_entry_serialization() {
|
||||
let entry = AuditEntry::new("push", "ci", "app:v1", "docker", "ci build");
|
||||
let json = serde_json::to_string(&entry).unwrap();
|
||||
assert!(json.contains(r#""action":"push""#));
|
||||
assert!(json.contains(r#""ts":""#));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,4 +666,348 @@ mod tests {
|
||||
assert_eq!(config.rate_limit.upload_burst, 1000);
|
||||
assert_eq!(config.rate_limit.auth_burst, 5); // default
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_auth_header() {
|
||||
let header = basic_auth_header("user:pass");
|
||||
assert_eq!(header, "Basic dXNlcjpwYXNz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_auth_header_empty() {
|
||||
let header = basic_auth_header("");
|
||||
assert!(header.starts_with("Basic "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_default() {
|
||||
let config = Config::default();
|
||||
assert_eq!(config.server.host, "127.0.0.1");
|
||||
assert_eq!(config.server.port, 4000);
|
||||
assert_eq!(config.server.body_limit_mb, 2048);
|
||||
assert!(config.server.public_url.is_none());
|
||||
assert_eq!(config.storage.path, "data/storage");
|
||||
assert_eq!(config.storage.mode, StorageMode::Local);
|
||||
assert_eq!(config.storage.bucket, "registry");
|
||||
assert_eq!(config.storage.s3_region, "us-east-1");
|
||||
assert!(!config.auth.enabled);
|
||||
assert_eq!(config.auth.htpasswd_file, "users.htpasswd");
|
||||
assert_eq!(config.auth.token_storage, "data/tokens");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_maven_config_default() {
|
||||
let m = MavenConfig::default();
|
||||
assert_eq!(m.proxy_timeout, 30);
|
||||
assert_eq!(m.proxies.len(), 1);
|
||||
assert_eq!(m.proxies[0].url(), "https://repo1.maven.org/maven2");
|
||||
assert!(m.proxies[0].auth().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_npm_config_default() {
|
||||
let n = NpmConfig::default();
|
||||
assert_eq!(n.proxy, Some("https://registry.npmjs.org".to_string()));
|
||||
assert!(n.proxy_auth.is_none());
|
||||
assert_eq!(n.proxy_timeout, 30);
|
||||
assert_eq!(n.metadata_ttl, 300);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pypi_config_default() {
|
||||
let p = PypiConfig::default();
|
||||
assert_eq!(p.proxy, Some("https://pypi.org/simple/".to_string()));
|
||||
assert!(p.proxy_auth.is_none());
|
||||
assert_eq!(p.proxy_timeout, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_docker_config_default() {
|
||||
let d = DockerConfig::default();
|
||||
assert_eq!(d.proxy_timeout, 60);
|
||||
assert_eq!(d.upstreams.len(), 1);
|
||||
assert_eq!(d.upstreams[0].url, "https://registry-1.docker.io");
|
||||
assert!(d.upstreams[0].auth.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_raw_config_default() {
|
||||
let r = RawConfig::default();
|
||||
assert!(r.enabled);
|
||||
assert_eq!(r.max_file_size, 104_857_600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auth_config_default() {
|
||||
let a = AuthConfig::default();
|
||||
assert!(!a.enabled);
|
||||
assert_eq!(a.htpasswd_file, "users.htpasswd");
|
||||
assert_eq!(a.token_storage, "data/tokens");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_maven_proxy_entry_simple() {
|
||||
let entry = MavenProxyEntry::Simple("https://repo.example.com".to_string());
|
||||
assert_eq!(entry.url(), "https://repo.example.com");
|
||||
assert!(entry.auth().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_maven_proxy_entry_full() {
|
||||
let entry = MavenProxyEntry::Full(MavenProxy {
|
||||
url: "https://private.repo.com".to_string(),
|
||||
auth: Some("user:secret".to_string()),
|
||||
});
|
||||
assert_eq!(entry.url(), "https://private.repo.com");
|
||||
assert_eq!(entry.auth(), Some("user:secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_maven_proxy_entry_full_no_auth() {
|
||||
let entry = MavenProxyEntry::Full(MavenProxy {
|
||||
url: "https://repo.com".to_string(),
|
||||
auth: None,
|
||||
});
|
||||
assert_eq!(entry.url(), "https://repo.com");
|
||||
assert!(entry.auth().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_storage_mode_default() {
|
||||
let mode = StorageMode::default();
|
||||
assert_eq!(mode, StorageMode::Local);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_override_server() {
|
||||
let mut config = Config::default();
|
||||
std::env::set_var("NORA_HOST", "0.0.0.0");
|
||||
std::env::set_var("NORA_PORT", "8080");
|
||||
std::env::set_var("NORA_PUBLIC_URL", "registry.example.com");
|
||||
std::env::set_var("NORA_BODY_LIMIT_MB", "4096");
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(config.server.host, "0.0.0.0");
|
||||
assert_eq!(config.server.port, 8080);
|
||||
assert_eq!(
|
||||
config.server.public_url,
|
||||
Some("registry.example.com".to_string())
|
||||
);
|
||||
assert_eq!(config.server.body_limit_mb, 4096);
|
||||
std::env::remove_var("NORA_HOST");
|
||||
std::env::remove_var("NORA_PORT");
|
||||
std::env::remove_var("NORA_PUBLIC_URL");
|
||||
std::env::remove_var("NORA_BODY_LIMIT_MB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_override_storage() {
|
||||
let mut config = Config::default();
|
||||
std::env::set_var("NORA_STORAGE_MODE", "s3");
|
||||
std::env::set_var("NORA_STORAGE_PATH", "/data/nora");
|
||||
std::env::set_var("NORA_STORAGE_BUCKET", "my-bucket");
|
||||
std::env::set_var("NORA_STORAGE_S3_REGION", "eu-west-1");
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(config.storage.mode, StorageMode::S3);
|
||||
assert_eq!(config.storage.path, "/data/nora");
|
||||
assert_eq!(config.storage.bucket, "my-bucket");
|
||||
assert_eq!(config.storage.s3_region, "eu-west-1");
|
||||
std::env::remove_var("NORA_STORAGE_MODE");
|
||||
std::env::remove_var("NORA_STORAGE_PATH");
|
||||
std::env::remove_var("NORA_STORAGE_BUCKET");
|
||||
std::env::remove_var("NORA_STORAGE_S3_REGION");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_override_auth() {
|
||||
let mut config = Config::default();
|
||||
std::env::set_var("NORA_AUTH_ENABLED", "true");
|
||||
std::env::set_var("NORA_AUTH_HTPASSWD_FILE", "/etc/nora/users");
|
||||
std::env::set_var("NORA_AUTH_TOKEN_STORAGE", "/data/tokens");
|
||||
config.apply_env_overrides();
|
||||
assert!(config.auth.enabled);
|
||||
assert_eq!(config.auth.htpasswd_file, "/etc/nora/users");
|
||||
assert_eq!(config.auth.token_storage, "/data/tokens");
|
||||
std::env::remove_var("NORA_AUTH_ENABLED");
|
||||
std::env::remove_var("NORA_AUTH_HTPASSWD_FILE");
|
||||
std::env::remove_var("NORA_AUTH_TOKEN_STORAGE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_override_maven_proxies() {
|
||||
let mut config = Config::default();
|
||||
std::env::set_var(
|
||||
"NORA_MAVEN_PROXIES",
|
||||
"https://repo1.com,https://repo2.com|user:pass",
|
||||
);
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(config.maven.proxies.len(), 2);
|
||||
assert_eq!(config.maven.proxies[0].url(), "https://repo1.com");
|
||||
assert!(config.maven.proxies[0].auth().is_none());
|
||||
assert_eq!(config.maven.proxies[1].url(), "https://repo2.com");
|
||||
assert_eq!(config.maven.proxies[1].auth(), Some("user:pass"));
|
||||
std::env::remove_var("NORA_MAVEN_PROXIES");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_override_docker_upstreams() {
|
||||
let mut config = Config::default();
|
||||
std::env::set_var(
|
||||
"NORA_DOCKER_UPSTREAMS",
|
||||
"https://mirror.gcr.io,https://private.io|token123",
|
||||
);
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(config.docker.upstreams.len(), 2);
|
||||
assert_eq!(config.docker.upstreams[0].url, "https://mirror.gcr.io");
|
||||
assert!(config.docker.upstreams[0].auth.is_none());
|
||||
assert_eq!(config.docker.upstreams[1].url, "https://private.io");
|
||||
assert_eq!(
|
||||
config.docker.upstreams[1].auth,
|
||||
Some("token123".to_string())
|
||||
);
|
||||
std::env::remove_var("NORA_DOCKER_UPSTREAMS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_override_npm() {
|
||||
let mut config = Config::default();
|
||||
std::env::set_var("NORA_NPM_PROXY", "https://npm.company.com");
|
||||
std::env::set_var("NORA_NPM_PROXY_AUTH", "user:token");
|
||||
std::env::set_var("NORA_NPM_PROXY_TIMEOUT", "60");
|
||||
std::env::set_var("NORA_NPM_METADATA_TTL", "600");
|
||||
config.apply_env_overrides();
|
||||
assert_eq!(
|
||||
config.npm.proxy,
|
||||
Some("https://npm.company.com".to_string())
|
||||
);
|
||||
assert_eq!(config.npm.proxy_auth, Some("user:token".to_string()));
|
||||
assert_eq!(config.npm.proxy_timeout, 60);
|
||||
assert_eq!(config.npm.metadata_ttl, 600);
|
||||
std::env::remove_var("NORA_NPM_PROXY");
|
||||
std::env::remove_var("NORA_NPM_PROXY_AUTH");
|
||||
std::env::remove_var("NORA_NPM_PROXY_TIMEOUT");
|
||||
std::env::remove_var("NORA_NPM_METADATA_TTL");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_override_raw() {
|
||||
let mut config = Config::default();
|
||||
std::env::set_var("NORA_RAW_ENABLED", "false");
|
||||
std::env::set_var("NORA_RAW_MAX_FILE_SIZE", "524288000");
|
||||
config.apply_env_overrides();
|
||||
assert!(!config.raw.enabled);
|
||||
assert_eq!(config.raw.max_file_size, 524288000);
|
||||
std::env::remove_var("NORA_RAW_ENABLED");
|
||||
std::env::remove_var("NORA_RAW_MAX_FILE_SIZE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_override_rate_limit() {
|
||||
let mut config = Config::default();
|
||||
std::env::set_var("NORA_RATE_LIMIT_ENABLED", "false");
|
||||
std::env::set_var("NORA_RATE_LIMIT_AUTH_RPS", "10");
|
||||
std::env::set_var("NORA_RATE_LIMIT_GENERAL_BURST", "500");
|
||||
config.apply_env_overrides();
|
||||
assert!(!config.rate_limit.enabled);
|
||||
assert_eq!(config.rate_limit.auth_rps, 10);
|
||||
assert_eq!(config.rate_limit.general_burst, 500);
|
||||
std::env::remove_var("NORA_RATE_LIMIT_ENABLED");
|
||||
std::env::remove_var("NORA_RATE_LIMIT_AUTH_RPS");
|
||||
std::env::remove_var("NORA_RATE_LIMIT_GENERAL_BURST");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_from_toml_full() {
|
||||
let toml = r#"
|
||||
[server]
|
||||
host = "0.0.0.0"
|
||||
port = 8080
|
||||
public_url = "nora.example.com"
|
||||
body_limit_mb = 4096
|
||||
|
||||
[storage]
|
||||
mode = "s3"
|
||||
path = "/data"
|
||||
s3_url = "http://minio:9000"
|
||||
bucket = "artifacts"
|
||||
s3_region = "eu-central-1"
|
||||
|
||||
[auth]
|
||||
enabled = true
|
||||
htpasswd_file = "/etc/nora/users.htpasswd"
|
||||
|
||||
[raw]
|
||||
enabled = false
|
||||
max_file_size = 500000000
|
||||
"#;
|
||||
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
assert_eq!(config.server.host, "0.0.0.0");
|
||||
assert_eq!(config.server.port, 8080);
|
||||
assert_eq!(
|
||||
config.server.public_url,
|
||||
Some("nora.example.com".to_string())
|
||||
);
|
||||
assert_eq!(config.server.body_limit_mb, 4096);
|
||||
assert_eq!(config.storage.mode, StorageMode::S3);
|
||||
assert_eq!(config.storage.s3_url, "http://minio:9000");
|
||||
assert_eq!(config.storage.bucket, "artifacts");
|
||||
assert!(config.auth.enabled);
|
||||
assert!(!config.raw.enabled);
|
||||
assert_eq!(config.raw.max_file_size, 500000000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_from_toml_minimal() {
|
||||
let toml = r#"
|
||||
[server]
|
||||
host = "127.0.0.1"
|
||||
port = 4000
|
||||
|
||||
[storage]
|
||||
mode = "local"
|
||||
"#;
|
||||
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
// Defaults should be filled
|
||||
assert_eq!(config.storage.path, "data/storage");
|
||||
assert_eq!(config.maven.proxies.len(), 1);
|
||||
assert_eq!(
|
||||
config.npm.proxy,
|
||||
Some("https://registry.npmjs.org".to_string())
|
||||
);
|
||||
assert_eq!(config.docker.upstreams.len(), 1);
|
||||
assert!(config.raw.enabled);
|
||||
assert!(!config.auth.enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_toml_docker_upstreams() {
|
||||
let toml = r#"
|
||||
[server]
|
||||
host = "127.0.0.1"
|
||||
port = 4000
|
||||
|
||||
[storage]
|
||||
mode = "local"
|
||||
|
||||
[docker]
|
||||
proxy_timeout = 120
|
||||
|
||||
[[docker.upstreams]]
|
||||
url = "https://mirror.gcr.io"
|
||||
|
||||
[[docker.upstreams]]
|
||||
url = "https://private.registry.io"
|
||||
auth = "user:pass"
|
||||
"#;
|
||||
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
assert_eq!(config.docker.proxy_timeout, 120);
|
||||
assert_eq!(config.docker.upstreams.len(), 2);
|
||||
assert!(config.docker.upstreams[0].auth.is_none());
|
||||
assert_eq!(
|
||||
config.docker.upstreams[1].auth,
|
||||
Some("user:pass".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,3 +216,146 @@ impl Default for DashboardMetrics {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_new_defaults() {
|
||||
let m = DashboardMetrics::new();
|
||||
assert_eq!(m.downloads.load(Ordering::Relaxed), 0);
|
||||
assert_eq!(m.uploads.load(Ordering::Relaxed), 0);
|
||||
assert_eq!(m.cache_hits.load(Ordering::Relaxed), 0);
|
||||
assert_eq!(m.cache_misses.load(Ordering::Relaxed), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_record_download_all_registries() {
|
||||
let m = DashboardMetrics::new();
|
||||
for reg in &["docker", "npm", "maven", "cargo", "pypi", "raw"] {
|
||||
m.record_download(reg);
|
||||
}
|
||||
assert_eq!(m.downloads.load(Ordering::Relaxed), 6);
|
||||
assert_eq!(m.docker_downloads.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(m.npm_downloads.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(m.maven_downloads.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(m.cargo_downloads.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(m.pypi_downloads.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(m.raw_downloads.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_record_download_unknown_registry() {
|
||||
let m = DashboardMetrics::new();
|
||||
m.record_download("unknown");
|
||||
assert_eq!(m.downloads.load(Ordering::Relaxed), 1);
|
||||
// no per-registry counter should increment
|
||||
assert_eq!(m.docker_downloads.load(Ordering::Relaxed), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_record_upload() {
|
||||
let m = DashboardMetrics::new();
|
||||
m.record_upload("docker");
|
||||
m.record_upload("maven");
|
||||
m.record_upload("raw");
|
||||
assert_eq!(m.uploads.load(Ordering::Relaxed), 3);
|
||||
assert_eq!(m.docker_uploads.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(m.maven_uploads.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(m.raw_uploads.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_record_upload_unknown_registry() {
|
||||
let m = DashboardMetrics::new();
|
||||
m.record_upload("npm"); // npm has no upload counter
|
||||
assert_eq!(m.uploads.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_hit_rate_zero() {
|
||||
let m = DashboardMetrics::new();
|
||||
assert_eq!(m.cache_hit_rate(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_hit_rate_all_hits() {
|
||||
let m = DashboardMetrics::new();
|
||||
m.record_cache_hit();
|
||||
m.record_cache_hit();
|
||||
assert_eq!(m.cache_hit_rate(), 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_hit_rate_mixed() {
|
||||
let m = DashboardMetrics::new();
|
||||
m.record_cache_hit();
|
||||
m.record_cache_miss();
|
||||
assert_eq!(m.cache_hit_rate(), 50.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_registry_downloads() {
|
||||
let m = DashboardMetrics::new();
|
||||
m.record_download("docker");
|
||||
m.record_download("docker");
|
||||
m.record_download("npm");
|
||||
assert_eq!(m.get_registry_downloads("docker"), 2);
|
||||
assert_eq!(m.get_registry_downloads("npm"), 1);
|
||||
assert_eq!(m.get_registry_downloads("cargo"), 0);
|
||||
assert_eq!(m.get_registry_downloads("unknown"), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_registry_uploads() {
|
||||
let m = DashboardMetrics::new();
|
||||
m.record_upload("docker");
|
||||
assert_eq!(m.get_registry_uploads("docker"), 1);
|
||||
assert_eq!(m.get_registry_uploads("maven"), 0);
|
||||
assert_eq!(m.get_registry_uploads("unknown"), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_persistence_save_and_load() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let path = tmp.path().to_str().unwrap();
|
||||
|
||||
// Create metrics, record some data, save
|
||||
{
|
||||
let m = DashboardMetrics::with_persistence(path);
|
||||
m.record_download("docker");
|
||||
m.record_download("docker");
|
||||
m.record_upload("maven");
|
||||
m.record_cache_hit();
|
||||
m.save();
|
||||
}
|
||||
|
||||
// Load in new instance
|
||||
{
|
||||
let m = DashboardMetrics::with_persistence(path);
|
||||
assert_eq!(m.downloads.load(Ordering::Relaxed), 2);
|
||||
assert_eq!(m.uploads.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(m.docker_downloads.load(Ordering::Relaxed), 2);
|
||||
assert_eq!(m.maven_uploads.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(m.cache_hits.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_persistence_missing_file() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let path = tmp.path().to_str().unwrap();
|
||||
|
||||
// Should work even without existing metrics.json
|
||||
let m = DashboardMetrics::with_persistence(path);
|
||||
assert_eq!(m.downloads.load(Ordering::Relaxed), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default() {
|
||||
let m = DashboardMetrics::default();
|
||||
assert_eq!(m.downloads.load(Ordering::Relaxed), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,4 +124,77 @@ mod tests {
|
||||
let err = AppError::NotFound("image not found".to_string());
|
||||
assert_eq!(err.to_string(), "Not found: image not found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_constructors() {
|
||||
let err = AppError::not_found("missing");
|
||||
assert!(matches!(err, AppError::NotFound(_)));
|
||||
assert_eq!(err.to_string(), "Not found: missing");
|
||||
|
||||
let err = AppError::bad_request("invalid input");
|
||||
assert!(matches!(err, AppError::BadRequest(_)));
|
||||
assert_eq!(err.to_string(), "Bad request: invalid input");
|
||||
|
||||
let err = AppError::unauthorized("no token");
|
||||
assert!(matches!(err, AppError::Unauthorized(_)));
|
||||
assert_eq!(err.to_string(), "Unauthorized: no token");
|
||||
|
||||
let err = AppError::internal("db crashed");
|
||||
assert!(matches!(err, AppError::Internal(_)));
|
||||
assert_eq!(err.to_string(), "Internal error: db crashed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_display_storage() {
|
||||
let err = AppError::Storage(StorageError::NotFound);
|
||||
assert!(err.to_string().contains("Storage error"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_display_validation() {
|
||||
let err = AppError::Validation(ValidationError::PathTraversal);
|
||||
assert!(err.to_string().contains("Validation error"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_into_response_not_found() {
|
||||
let err = AppError::NotFound("gone".to_string());
|
||||
let response = err.into_response();
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_into_response_bad_request() {
|
||||
let err = AppError::BadRequest("bad".to_string());
|
||||
let response = err.into_response();
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_into_response_unauthorized() {
|
||||
let err = AppError::Unauthorized("nope".to_string());
|
||||
let response = err.into_response();
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_into_response_internal() {
|
||||
let err = AppError::Internal("boom".to_string());
|
||||
let response = err.into_response();
|
||||
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_into_response_storage_not_found() {
|
||||
let err = AppError::Storage(StorageError::NotFound);
|
||||
let response = err.into_response();
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_into_response_validation() {
|
||||
let err = AppError::Validation(ValidationError::EmptyInput);
|
||||
let response = err.into_response();
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,3 +148,56 @@ pub fn record_storage_op(operation: &str, success: bool) {
|
||||
.with_label_values(&[operation, status])
|
||||
.inc();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_detect_registry_docker() {
|
||||
assert_eq!(detect_registry("/v2/nginx/manifests/latest"), "docker");
|
||||
assert_eq!(detect_registry("/v2/"), "docker");
|
||||
assert_eq!(
|
||||
detect_registry("/v2/library/alpine/blobs/sha256:abc"),
|
||||
"docker"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_registry_maven() {
|
||||
assert_eq!(detect_registry("/maven2/com/example/artifact"), "maven");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_registry_npm() {
|
||||
assert_eq!(detect_registry("/npm/lodash"), "npm");
|
||||
assert_eq!(detect_registry("/npm/@scope/package"), "npm");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_registry_cargo() {
|
||||
assert_eq!(detect_registry("/cargo/api/v1/crates"), "cargo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_registry_pypi() {
|
||||
assert_eq!(detect_registry("/simple/requests/"), "pypi");
|
||||
assert_eq!(
|
||||
detect_registry("/packages/requests/1.0/requests-1.0.tar.gz"),
|
||||
"pypi"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_registry_ui() {
|
||||
assert_eq!(detect_registry("/ui/dashboard"), "ui");
|
||||
assert_eq!(detect_registry("/ui"), "ui");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_registry_other() {
|
||||
assert_eq!(detect_registry("/health"), "other");
|
||||
assert_eq!(detect_registry("/ready"), "other");
|
||||
assert_eq!(detect_registry("/unknown/path"), "other");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,3 +361,164 @@ pub fn paginate<T: Clone>(data: &[T], page: usize, limit: usize) -> (Vec<T>, usi
|
||||
let end = (start + limit).min(total);
|
||||
(data[start..end].to_vec(), total)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_paginate_first_page() {
|
||||
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
let (page, total) = paginate(&data, 1, 3);
|
||||
assert_eq!(page, vec![1, 2, 3]);
|
||||
assert_eq!(total, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paginate_second_page() {
|
||||
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
let (page, total) = paginate(&data, 2, 3);
|
||||
assert_eq!(page, vec![4, 5, 6]);
|
||||
assert_eq!(total, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paginate_last_page_partial() {
|
||||
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
let (page, total) = paginate(&data, 4, 3);
|
||||
assert_eq!(page, vec![10]);
|
||||
assert_eq!(total, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paginate_beyond_range() {
|
||||
let data = vec![1, 2, 3];
|
||||
let (page, total) = paginate(&data, 5, 3);
|
||||
assert!(page.is_empty());
|
||||
assert_eq!(total, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paginate_empty_data() {
|
||||
let data: Vec<i32> = vec![];
|
||||
let (page, total) = paginate(&data, 1, 10);
|
||||
assert!(page.is_empty());
|
||||
assert_eq!(total, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paginate_page_zero() {
|
||||
// page 0 with saturating_sub becomes 0, so start = 0
|
||||
let data = vec![1, 2, 3];
|
||||
let (page, _) = paginate(&data, 0, 2);
|
||||
assert_eq!(page, vec![1, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paginate_large_limit() {
|
||||
let data = vec![1, 2, 3];
|
||||
let (page, total) = paginate(&data, 1, 100);
|
||||
assert_eq!(page, vec![1, 2, 3]);
|
||||
assert_eq!(total, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_registry_index_new() {
|
||||
let idx = RegistryIndex::new();
|
||||
assert_eq!(idx.count(), 0);
|
||||
assert!(idx.is_dirty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_registry_index_invalidate() {
|
||||
let idx = RegistryIndex::new();
|
||||
// Initially dirty
|
||||
assert!(idx.is_dirty());
|
||||
|
||||
// Set data clears dirty
|
||||
idx.set(vec![RepoInfo {
|
||||
name: "test".to_string(),
|
||||
versions: 1,
|
||||
size: 100,
|
||||
updated: "2026-01-01".to_string(),
|
||||
}]);
|
||||
assert!(!idx.is_dirty());
|
||||
assert_eq!(idx.count(), 1);
|
||||
|
||||
// Invalidate makes it dirty again
|
||||
idx.invalidate();
|
||||
assert!(idx.is_dirty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_registry_index_get_cached() {
|
||||
let idx = RegistryIndex::new();
|
||||
idx.set(vec![
|
||||
RepoInfo {
|
||||
name: "a".to_string(),
|
||||
versions: 2,
|
||||
size: 200,
|
||||
updated: "today".to_string(),
|
||||
},
|
||||
RepoInfo {
|
||||
name: "b".to_string(),
|
||||
versions: 1,
|
||||
size: 100,
|
||||
updated: "yesterday".to_string(),
|
||||
},
|
||||
]);
|
||||
|
||||
let cached = idx.get_cached();
|
||||
assert_eq!(cached.len(), 2);
|
||||
assert_eq!(cached[0].name, "a");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_registry_index_default() {
|
||||
let idx = RegistryIndex::default();
|
||||
assert_eq!(idx.count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repo_index_new() {
|
||||
let idx = RepoIndex::new();
|
||||
let (d, m, n, c, p) = idx.counts();
|
||||
assert_eq!((d, m, n, c, p), (0, 0, 0, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repo_index_invalidate() {
|
||||
let idx = RepoIndex::new();
|
||||
// Should not panic for any registry
|
||||
idx.invalidate("docker");
|
||||
idx.invalidate("maven");
|
||||
idx.invalidate("npm");
|
||||
idx.invalidate("cargo");
|
||||
idx.invalidate("pypi");
|
||||
idx.invalidate("unknown"); // should be a no-op
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repo_index_default() {
|
||||
let idx = RepoIndex::default();
|
||||
let (d, m, n, c, p) = idx.counts();
|
||||
assert_eq!((d, m, n, c, p), (0, 0, 0, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_sorted_vec() {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
map.insert("zebra".to_string(), (3usize, 100u64, 0u64));
|
||||
map.insert("alpha".to_string(), (1, 50, 1700000000));
|
||||
|
||||
let result = to_sorted_vec(map);
|
||||
assert_eq!(result.len(), 2);
|
||||
assert_eq!(result[0].name, "alpha");
|
||||
assert_eq!(result[0].versions, 1);
|
||||
assert_eq!(result[0].size, 50);
|
||||
assert_ne!(result[0].updated, "N/A");
|
||||
assert_eq!(result[1].name, "zebra");
|
||||
assert_eq!(result[1].versions, 3);
|
||||
assert_eq!(result[1].updated, "N/A"); // modified = 0
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user