diff --git a/src/webhook.rs b/src/webhook.rs index 070add2..bc0f08f 100644 --- a/src/webhook.rs +++ b/src/webhook.rs @@ -15,7 +15,8 @@ use sha2::Sha256; use tiny_http::{Header, Method, Request, Response, Server, StatusCode}; use crate::config::{ - Config, EndpointConfig, MirrorConfig, ProviderKind, default_work_dir, validate_config, + Config, EndpointConfig, MirrorConfig, ProviderKind, RepoNameFilter, default_work_dir, + validate_config, }; use crate::provider::{EndpointRepo, ProviderClient, RemoteRepo}; use crate::state::{load_toml_or_default, save_toml}; @@ -201,8 +202,7 @@ pub fn install_webhooks(config: &Config, options: WebhookInstallOptions) -> Resu .with_context(|| format!("failed to list repos for {}", endpoint.label()))?; for repo in repos .into_iter() - .filter(|repo| mirror.sync_visibility.matches_private(repo.private)) - .filter(|repo| repo_filter.matches(&repo.name)) + .filter(|repo| webhook_repo_matches(mirror, &repo_filter, repo)) { tasks.push(WebhookInstallTask { site: site.clone(), @@ -326,8 +326,12 @@ pub fn ensure_configured_webhooks( } let secret = webhook.secret()?; let state = Arc::new(Mutex::new(load_webhook_state(work_dir)?)); + let repo_filter = mirror.repo_filter()?; let mut tasks = Vec::new(); for endpoint_repo in repos { + if !webhook_repo_matches(mirror, &repo_filter, &endpoint_repo.repo) { + continue; + } let Some(site) = config.site(&endpoint_repo.endpoint.site) else { continue; }; @@ -348,6 +352,14 @@ pub fn ensure_configured_webhooks( save_webhook_state(work_dir, &state) } +fn webhook_repo_matches( + mirror: &MirrorConfig, + repo_filter: &RepoNameFilter, + repo: &RemoteRepo, +) -> bool { + mirror.sync_visibility.matches_private(repo.private) && repo_filter.matches(&repo.name) +} + pub fn check_webhook_url_reachable(url: &str) -> Result<()> { let client = reqwest::blocking::Client::builder() .timeout(Duration::from_secs(10)) diff --git a/tests/unit/webhook.rs b/tests/unit/webhook.rs index fe60a8f..3d06243 100644 --- a/tests/unit/webhook.rs +++ b/tests/unit/webhook.rs @@ -2,7 +2,7 @@ use super::*; use crate::config::SyncVisibility; use crate::config::{ ConflictResolutionStrategy, EndpointConfig, MirrorConfig, NamespaceKind, SiteConfig, - TokenConfig, Visibility, + TokenConfig, Visibility, WebhookConfig, }; use std::io::{Read, Write}; use std::net::TcpListener; @@ -169,6 +169,124 @@ fn matching_jobs_respects_repo_name_filters() { assert_eq!(matching_jobs(&config, &webhook_event("random")).len(), 1); } +#[test] +fn install_webhooks_respects_visibility_and_repo_name_filters() { + let repos = r#"[ + {"name":"important-api","clone_url":"https://github.com/alice/important-api.git","private":false,"description":null,"owner":{"login":"alice"}}, + {"name":"important-private","clone_url":"https://github.com/alice/important-private.git","private":true,"description":null,"owner":{"login":"alice"}}, + {"name":"important-archive","clone_url":"https://github.com/alice/important-archive.git","private":false,"description":null,"owner":{"login":"alice"}}, + {"name":"random","clone_url":"https://github.com/alice/random.git","private":false,"description":null,"owner":{"login":"alice"}} + ]"#; + let (api_url, handle) = request_server( + vec![ + ("200 OK", repos), + ("200 OK", "[]"), + ("200 OK", "[]"), + ("201 Created", r#"{"id":1}"#), + ], + |index, request| match index { + 0 => assert!( + request + .starts_with("GET /user/repos?affiliation=owner&visibility=all&per_page=100 "), + "request was {request}" + ), + 1 => assert!( + request + .starts_with("GET /user/repos?affiliation=owner&visibility=all&per_page=100 "), + "request was {request}" + ), + 2 => assert!( + request.starts_with("GET /repos/alice/important-api/hooks "), + "request was {request}" + ), + 3 => assert!( + request.starts_with("POST /repos/alice/important-api/hooks "), + "request was {request}" + ), + _ => unreachable!(), + }, + ); + let temp = tempfile::TempDir::new().unwrap(); + let config = Config { + jobs: crate::config::DEFAULT_JOBS, + sites: vec![ + SiteConfig { + api_url: Some(api_url.clone()), + ..site("github", ProviderKind::Github) + }, + SiteConfig { + api_url: Some(api_url), + ..site("github-peer", ProviderKind::Github) + }, + ], + mirrors: vec![filtered_mirror()], + webhook: None, + }; + + install_webhooks( + &config, + WebhookInstallOptions { + url: "https://mirror.example.test/webhook".to_string(), + secret: "secret".to_string(), + dry_run: false, + work_dir: Some(temp.path().to_path_buf()), + jobs: 1, + }, + ) + .unwrap(); + + handle.join().unwrap(); +} + +#[test] +fn configured_webhook_install_respects_visibility_and_repo_name_filters() { + let (api_url, handle) = request_server( + vec![("200 OK", "[]"), ("201 Created", r#"{"id":1}"#)], + |index, request| match index { + 0 => assert!( + request.starts_with("GET /repos/alice/important-api/hooks "), + "request was {request}" + ), + 1 => assert!( + request.starts_with("POST /repos/alice/important-api/hooks "), + "request was {request}" + ), + _ => unreachable!(), + }, + ); + let temp = tempfile::TempDir::new().unwrap(); + let mirror = filtered_mirror(); + let endpoint = mirror.endpoints[0].clone(); + let config = Config { + jobs: crate::config::DEFAULT_JOBS, + sites: vec![ + SiteConfig { + api_url: Some(api_url), + ..site("github", ProviderKind::Github) + }, + site("github-peer", ProviderKind::Github), + ], + mirrors: vec![mirror.clone()], + webhook: Some(WebhookConfig { + install: true, + url: "https://mirror.example.test/webhook".to_string(), + secret: TokenConfig::Value("secret".to_string()), + full_sync_interval_minutes: None, + reachability_check_interval_minutes: None, + }), + }; + let repos = vec![ + endpoint_repo(&endpoint, "important-api", false), + endpoint_repo(&endpoint, "important-private", true), + endpoint_repo(&endpoint, "important-archive", false), + endpoint_repo(&endpoint, "random", false), + ]; + + ensure_configured_webhooks(&config, &mirror, &repos, temp.path(), 1).unwrap(); + + handle.join().unwrap(); +} + #[test] fn webhook_state_persists_installations() { let temp = tempfile::TempDir::new().unwrap(); @@ -414,6 +532,34 @@ fn site(name: &str, provider: ProviderKind) -> SiteConfig { } } +fn filtered_mirror() -> MirrorConfig { + MirrorConfig { + name: "sync-1".to_string(), + endpoints: vec![ + endpoint("github", NamespaceKind::User, "alice"), + endpoint("github-peer", NamespaceKind::User, "bob"), + ], + sync_visibility: SyncVisibility::Public, + repo_whitelist: vec!["^important-".to_string()], + repo_blacklist: vec!["-archive$".to_string()], + create_missing: true, + visibility: Visibility::Private, + conflict_resolution: ConflictResolutionStrategy::Fail, + } +} + +fn endpoint_repo(endpoint: &EndpointConfig, name: &str, private: bool) -> EndpointRepo { + EndpointRepo { + endpoint: endpoint.clone(), + repo: RemoteRepo { + name: name.to_string(), + clone_url: format!("https://github.com/alice/{name}.git"), + private, + description: None, + }, + } +} + fn webhook_event(repo: &str) -> WebhookEvent { WebhookEvent { provider: Some(ProviderKind::Github),