diff --git a/src/config.rs b/src/config.rs index c10906d..050f003 100644 --- a/src/config.rs +++ b/src/config.rs @@ -268,151 +268,5 @@ pub fn validate_config(config: &Config) -> Result<()> { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parses_token_forms() { - let config: Config = toml::from_str( - r#" - [webhook] - install = true - url = "https://mirror.example.test/webhook" - secret = { env = "WEBHOOK_SECRET" } - full_sync_interval_minutes = 60 - reachability_check_interval_minutes = 15 - - [[sites]] - name = "github" - provider = "github" - base_url = "https://github.com" - token = { env = "GITHUB_TOKEN" } - - [[mirrors]] - name = "personal" - create_missing = true - visibility = "private" - allow_force = false - - [[mirrors.endpoints]] - site = "github" - kind = "user" - namespace = "alice" - - [[mirrors.endpoints]] - site = "github" - kind = "org" - namespace = "example" - "#, - ) - .unwrap(); - - assert_eq!(config.sites.len(), 1); - assert_eq!(config.mirrors[0].endpoints.len(), 2); - let webhook = config.webhook.unwrap(); - assert!(webhook.install); - assert_eq!(webhook.url, "https://mirror.example.test/webhook"); - assert_eq!( - webhook.secret, - TokenConfig::Env("WEBHOOK_SECRET".to_string()) - ); - assert_eq!(webhook.full_sync_interval_minutes, Some(60)); - } - - #[test] - fn validation_rejects_unknown_sites_and_single_endpoint_groups() { - let config = Config { - sites: vec![site("github", ProviderKind::Github)], - mirrors: vec![MirrorConfig { - name: "broken".to_string(), - endpoints: vec![EndpointConfig { - site: "github".to_string(), - kind: NamespaceKind::User, - namespace: "alice".to_string(), - }], - create_missing: true, - visibility: Visibility::Private, - allow_force: false, - }], - webhook: None, - }; - let err = validate_config(&config).unwrap_err().to_string(); - assert!(err.contains("at least two endpoints")); - - let config = Config { - sites: vec![site("github", ProviderKind::Github)], - mirrors: vec![MirrorConfig { - name: "broken".to_string(), - endpoints: vec![ - EndpointConfig { - site: "github".to_string(), - kind: NamespaceKind::User, - namespace: "alice".to_string(), - }, - EndpointConfig { - site: "missing".to_string(), - kind: NamespaceKind::User, - namespace: "alice".to_string(), - }, - ], - create_missing: true, - visibility: Visibility::Private, - allow_force: false, - }], - webhook: None, - }; - let err = validate_config(&config).unwrap_err().to_string(); - assert!(err.contains("unknown site 'missing'")); - } - - #[test] - fn api_base_defaults_match_providers() { - assert_eq!( - site("github", ProviderKind::Github).api_base(), - "https://api.github.com" - ); - assert_eq!( - SiteConfig { - base_url: "https://github.example.test/".to_string(), - ..site("github-enterprise", ProviderKind::Github) - } - .api_base(), - "https://github.example.test/api/v3" - ); - assert_eq!( - SiteConfig { - base_url: "https://gitlab.example.test".to_string(), - ..site("gitlab", ProviderKind::Gitlab) - } - .api_base(), - "https://gitlab.example.test/api/v4" - ); - assert_eq!( - SiteConfig { - base_url: "https://gitea.example.test".to_string(), - ..site("gitea", ProviderKind::Gitea) - } - .api_base(), - "https://gitea.example.test/api/v1" - ); - assert_eq!( - SiteConfig { - base_url: "https://forgejo.example.test".to_string(), - ..site("forgejo", ProviderKind::Forgejo) - } - .api_base(), - "https://forgejo.example.test/api/v1" - ); - } - - fn site(name: &str, provider: ProviderKind) -> SiteConfig { - SiteConfig { - name: name.to_string(), - provider, - base_url: "https://github.com".to_string(), - api_url: None, - token: TokenConfig::Value("token".to_string()), - git_username: None, - } - } -} +#[path = "../tests/unit/config.rs"] +mod tests; diff --git a/src/git.rs b/src/git.rs index 89c614b..ccd91b9 100644 --- a/src/git.rs +++ b/src/git.rs @@ -677,4 +677,5 @@ pub fn safe_remote_name(value: &str) -> String { } #[cfg(test)] +#[path = "../tests/unit/git.rs"] mod tests; diff --git a/src/interactive.rs b/src/interactive.rs index 0866feb..68642f5 100644 --- a/src/interactive.rs +++ b/src/interactive.rs @@ -564,6 +564,7 @@ fn pat_instruction_lines(provider: &ProviderKind, base_url: &str) -> Vec } #[cfg(test)] +#[path = "../tests/unit/interactive_test_io.rs"] mod test_io; #[cfg(test)] use test_io::*; @@ -913,4 +914,5 @@ fn generate_webhook_secret() -> String { } #[cfg(test)] +#[path = "../tests/unit/interactive.rs"] mod tests; diff --git a/src/logging.rs b/src/logging.rs index c398c40..8b498fb 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -230,18 +230,5 @@ macro_rules! logln { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn repo_prefix_pads_and_truncates_to_fixed_width() { - assert_eq!(repo_prefix("api", 6), "api "); - assert_eq!(repo_prefix("very-long-repo", 8), "very-lo~"); - } - - #[test] - fn status_text_truncates_to_fixed_width() { - assert_eq!(truncate_status("short", 8), "short"); - assert_eq!(truncate_status("very-long-status", 8), "very-lo~"); - } -} +#[path = "../tests/unit/logging.rs"] +mod tests; diff --git a/src/main.rs b/src/main.rs index bf09ef0..a961b3e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -223,137 +223,5 @@ fn resolve_webhook_url(config: &Config, value: Option) -> Result } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn cli_config_opens_wizard() { - let cli = Cli::try_parse_from(["git-sync", "config"]).unwrap(); - - assert!(matches!(cli.command, Command::Config)); - } - - #[test] - fn cli_rejects_removed_config_subcommands() { - for args in [ - ["git-sync", "config", "wizard"].as_slice(), - ["git-sync", "config", "init"].as_slice(), - ["git-sync", "config", "show"].as_slice(), - ["git-sync", "config", "site", "list"].as_slice(), - ["git-sync", "config", "mirror", "list"].as_slice(), - ] { - assert!(Cli::try_parse_from(args).is_err()); - } - } - - #[test] - fn cli_accepts_sync_repo_pattern() { - let cli = Cli::try_parse_from([ - "git-sync", - "sync", - "--repo-pattern", - "^(foo|bar)-", - "--dry-run", - ]) - .unwrap(); - - let Command::Sync(args) = cli.command else { - panic!("parsed unexpected command"); - }; - assert_eq!(args.repo_pattern, Some("^(foo|bar)-".to_string())); - assert!(args.dry_run); - } - - #[test] - fn cli_accepts_sync_retry_failed() { - let cli = Cli::try_parse_from(["git-sync", "sync", "--retry-failed"]).unwrap(); - - let Command::Sync(args) = cli.command else { - panic!("parsed unexpected command"); - }; - assert!(args.retry_failed); - } - - #[test] - fn cli_accepts_sync_jobs() { - let cli = Cli::try_parse_from(["git-sync", "sync", "--jobs", "8"]).unwrap(); - - let Command::Sync(args) = cli.command else { - panic!("parsed unexpected command"); - }; - assert_eq!(args.jobs, 8); - } - - #[test] - fn cli_accepts_webhook_serve() { - let cli = Cli::try_parse_from([ - "git-sync", - "serve", - "--listen", - "127.0.0.1:9000", - "--secret-env", - "WEBHOOK_SECRET", - "--jobs", - "2", - "--full-sync-interval-minutes", - "30", - ]) - .unwrap(); - - let Command::Serve(args) = cli.command else { - panic!("parsed unexpected command"); - }; - assert_eq!(args.listen, "127.0.0.1:9000"); - assert_eq!(args.secret_env, Some("WEBHOOK_SECRET".to_string())); - assert_eq!(args.jobs, 2); - assert_eq!(args.full_sync_interval_minutes, Some(30)); - } - - #[test] - fn cli_accepts_webhook_install() { - let cli = Cli::try_parse_from([ - "git-sync", - "webhook", - "install", - "--url", - "https://mirror.example.test/webhook", - "--secret", - "secret", - "--group", - "sync-1", - "--repo-pattern", - "^repo$", - ]) - .unwrap(); - - let Command::Webhook(WebhookCommand::Install(args)) = cli.command else { - panic!("parsed unexpected command"); - }; - assert_eq!( - args.url, - Some("https://mirror.example.test/webhook".to_string()) - ); - assert_eq!(args.secret, Some("secret".to_string())); - assert_eq!(args.group, Some("sync-1".to_string())); - assert_eq!(args.repo_pattern, Some("^repo$".to_string())); - } - - #[test] - fn cli_accepts_webhook_uninstall() { - let cli = Cli::try_parse_from([ - "git-sync", - "webhook", - "uninstall", - "--group", - "sync-1", - "--dry-run", - ]) - .unwrap(); - - let Command::Webhook(WebhookCommand::Uninstall(args)) = cli.command else { - panic!("parsed unexpected command"); - }; - assert_eq!(args.group, Some("sync-1".to_string())); - assert!(args.dry_run); - } -} +#[path = "../tests/unit/cli.rs"] +mod tests; diff --git a/src/provider.rs b/src/provider.rs index 2675e97..23dfe98 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -747,4 +747,5 @@ pub fn repos_by_name(repos: Vec) -> HashMap bool { } #[cfg(test)] +#[path = "../tests/unit/webhook.rs"] mod tests; diff --git a/tests/unit/cli.rs b/tests/unit/cli.rs new file mode 100644 index 0000000..4ff3ece --- /dev/null +++ b/tests/unit/cli.rs @@ -0,0 +1,132 @@ +use super::*; + +#[test] +fn cli_config_opens_wizard() { + let cli = Cli::try_parse_from(["git-sync", "config"]).unwrap(); + + assert!(matches!(cli.command, Command::Config)); +} + +#[test] +fn cli_rejects_removed_config_subcommands() { + for args in [ + ["git-sync", "config", "wizard"].as_slice(), + ["git-sync", "config", "init"].as_slice(), + ["git-sync", "config", "show"].as_slice(), + ["git-sync", "config", "site", "list"].as_slice(), + ["git-sync", "config", "mirror", "list"].as_slice(), + ] { + assert!(Cli::try_parse_from(args).is_err()); + } +} + +#[test] +fn cli_accepts_sync_repo_pattern() { + let cli = Cli::try_parse_from([ + "git-sync", + "sync", + "--repo-pattern", + "^(foo|bar)-", + "--dry-run", + ]) + .unwrap(); + + let Command::Sync(args) = cli.command else { + panic!("parsed unexpected command"); + }; + assert_eq!(args.repo_pattern, Some("^(foo|bar)-".to_string())); + assert!(args.dry_run); +} + +#[test] +fn cli_accepts_sync_retry_failed() { + let cli = Cli::try_parse_from(["git-sync", "sync", "--retry-failed"]).unwrap(); + + let Command::Sync(args) = cli.command else { + panic!("parsed unexpected command"); + }; + assert!(args.retry_failed); +} + +#[test] +fn cli_accepts_sync_jobs() { + let cli = Cli::try_parse_from(["git-sync", "sync", "--jobs", "8"]).unwrap(); + + let Command::Sync(args) = cli.command else { + panic!("parsed unexpected command"); + }; + assert_eq!(args.jobs, 8); +} + +#[test] +fn cli_accepts_webhook_serve() { + let cli = Cli::try_parse_from([ + "git-sync", + "serve", + "--listen", + "127.0.0.1:9000", + "--secret-env", + "WEBHOOK_SECRET", + "--jobs", + "2", + "--full-sync-interval-minutes", + "30", + ]) + .unwrap(); + + let Command::Serve(args) = cli.command else { + panic!("parsed unexpected command"); + }; + assert_eq!(args.listen, "127.0.0.1:9000"); + assert_eq!(args.secret_env, Some("WEBHOOK_SECRET".to_string())); + assert_eq!(args.jobs, 2); + assert_eq!(args.full_sync_interval_minutes, Some(30)); +} + +#[test] +fn cli_accepts_webhook_install() { + let cli = Cli::try_parse_from([ + "git-sync", + "webhook", + "install", + "--url", + "https://mirror.example.test/webhook", + "--secret", + "secret", + "--group", + "sync-1", + "--repo-pattern", + "^repo$", + ]) + .unwrap(); + + let Command::Webhook(WebhookCommand::Install(args)) = cli.command else { + panic!("parsed unexpected command"); + }; + assert_eq!( + args.url, + Some("https://mirror.example.test/webhook".to_string()) + ); + assert_eq!(args.secret, Some("secret".to_string())); + assert_eq!(args.group, Some("sync-1".to_string())); + assert_eq!(args.repo_pattern, Some("^repo$".to_string())); +} + +#[test] +fn cli_accepts_webhook_uninstall() { + let cli = Cli::try_parse_from([ + "git-sync", + "webhook", + "uninstall", + "--group", + "sync-1", + "--dry-run", + ]) + .unwrap(); + + let Command::Webhook(WebhookCommand::Uninstall(args)) = cli.command else { + panic!("parsed unexpected command"); + }; + assert_eq!(args.group, Some("sync-1".to_string())); + assert!(args.dry_run); +} diff --git a/tests/unit/config.rs b/tests/unit/config.rs new file mode 100644 index 0000000..17c0ba7 --- /dev/null +++ b/tests/unit/config.rs @@ -0,0 +1,146 @@ +use super::*; + +#[test] +fn parses_token_forms() { + let config: Config = toml::from_str( + r#" + [webhook] + install = true + url = "https://mirror.example.test/webhook" + secret = { env = "WEBHOOK_SECRET" } + full_sync_interval_minutes = 60 + reachability_check_interval_minutes = 15 + + [[sites]] + name = "github" + provider = "github" + base_url = "https://github.com" + token = { env = "GITHUB_TOKEN" } + + [[mirrors]] + name = "personal" + create_missing = true + visibility = "private" + allow_force = false + + [[mirrors.endpoints]] + site = "github" + kind = "user" + namespace = "alice" + + [[mirrors.endpoints]] + site = "github" + kind = "org" + namespace = "example" + "#, + ) + .unwrap(); + + assert_eq!(config.sites.len(), 1); + assert_eq!(config.mirrors[0].endpoints.len(), 2); + let webhook = config.webhook.unwrap(); + assert!(webhook.install); + assert_eq!(webhook.url, "https://mirror.example.test/webhook"); + assert_eq!( + webhook.secret, + TokenConfig::Env("WEBHOOK_SECRET".to_string()) + ); + assert_eq!(webhook.full_sync_interval_minutes, Some(60)); +} + +#[test] +fn validation_rejects_unknown_sites_and_single_endpoint_groups() { + let config = Config { + sites: vec![site("github", ProviderKind::Github)], + mirrors: vec![MirrorConfig { + name: "broken".to_string(), + endpoints: vec![EndpointConfig { + site: "github".to_string(), + kind: NamespaceKind::User, + namespace: "alice".to_string(), + }], + create_missing: true, + visibility: Visibility::Private, + allow_force: false, + }], + webhook: None, + }; + let err = validate_config(&config).unwrap_err().to_string(); + assert!(err.contains("at least two endpoints")); + + let config = Config { + sites: vec![site("github", ProviderKind::Github)], + mirrors: vec![MirrorConfig { + name: "broken".to_string(), + endpoints: vec![ + EndpointConfig { + site: "github".to_string(), + kind: NamespaceKind::User, + namespace: "alice".to_string(), + }, + EndpointConfig { + site: "missing".to_string(), + kind: NamespaceKind::User, + namespace: "alice".to_string(), + }, + ], + create_missing: true, + visibility: Visibility::Private, + allow_force: false, + }], + webhook: None, + }; + let err = validate_config(&config).unwrap_err().to_string(); + assert!(err.contains("unknown site 'missing'")); +} + +#[test] +fn api_base_defaults_match_providers() { + assert_eq!( + site("github", ProviderKind::Github).api_base(), + "https://api.github.com" + ); + assert_eq!( + SiteConfig { + base_url: "https://github.example.test/".to_string(), + ..site("github-enterprise", ProviderKind::Github) + } + .api_base(), + "https://github.example.test/api/v3" + ); + assert_eq!( + SiteConfig { + base_url: "https://gitlab.example.test".to_string(), + ..site("gitlab", ProviderKind::Gitlab) + } + .api_base(), + "https://gitlab.example.test/api/v4" + ); + assert_eq!( + SiteConfig { + base_url: "https://gitea.example.test".to_string(), + ..site("gitea", ProviderKind::Gitea) + } + .api_base(), + "https://gitea.example.test/api/v1" + ); + assert_eq!( + SiteConfig { + base_url: "https://forgejo.example.test".to_string(), + ..site("forgejo", ProviderKind::Forgejo) + } + .api_base(), + "https://forgejo.example.test/api/v1" + ); +} + +fn site(name: &str, provider: ProviderKind) -> SiteConfig { + SiteConfig { + name: name.to_string(), + provider, + base_url: "https://github.com".to_string(), + api_url: None, + token: TokenConfig::Value("token".to_string()), + git_username: None, + } +} diff --git a/src/git/tests.rs b/tests/unit/git.rs similarity index 100% rename from src/git/tests.rs rename to tests/unit/git.rs diff --git a/src/interactive/tests.rs b/tests/unit/interactive.rs similarity index 100% rename from src/interactive/tests.rs rename to tests/unit/interactive.rs diff --git a/src/interactive/test_io.rs b/tests/unit/interactive_test_io.rs similarity index 100% rename from src/interactive/test_io.rs rename to tests/unit/interactive_test_io.rs diff --git a/tests/unit/logging.rs b/tests/unit/logging.rs new file mode 100644 index 0000000..5e64151 --- /dev/null +++ b/tests/unit/logging.rs @@ -0,0 +1,13 @@ +use super::*; + +#[test] +fn repo_prefix_pads_and_truncates_to_fixed_width() { + assert_eq!(repo_prefix("api", 6), "api "); + assert_eq!(repo_prefix("very-long-repo", 8), "very-lo~"); +} + +#[test] +fn status_text_truncates_to_fixed_width() { + assert_eq!(truncate_status("short", 8), "short"); + assert_eq!(truncate_status("very-long-status", 8), "very-lo~"); +} diff --git a/src/provider/tests.rs b/tests/unit/provider.rs similarity index 100% rename from src/provider/tests.rs rename to tests/unit/provider.rs diff --git a/src/sync/tests.rs b/tests/unit/sync.rs similarity index 100% rename from src/sync/tests.rs rename to tests/unit/sync.rs diff --git a/src/webhook/tests.rs b/tests/unit/webhook.rs similarity index 100% rename from src/webhook/tests.rs rename to tests/unit/webhook.rs