[F] Webhook issues
This commit is contained in:
+49
-4
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_token_forms() {
|
||||
fn parses_value_tokens() {
|
||||
let config: Config = toml::from_str(
|
||||
r#"
|
||||
jobs = 8
|
||||
@@ -9,7 +9,7 @@ fn parses_token_forms() {
|
||||
[webhook]
|
||||
install = true
|
||||
url = "https://mirror.example.test/webhook"
|
||||
secret = { env = "WEBHOOK_SECRET" }
|
||||
secret = { value = "webhook-secret" }
|
||||
full_sync_interval_minutes = 60
|
||||
reachability_check_interval_minutes = 15
|
||||
|
||||
@@ -17,7 +17,7 @@ fn parses_token_forms() {
|
||||
name = "github"
|
||||
provider = "github"
|
||||
base_url = "https://github.com"
|
||||
token = { env = "GITHUB_TOKEN" }
|
||||
token = { value = "github-token" }
|
||||
|
||||
[[mirrors]]
|
||||
name = "personal"
|
||||
@@ -62,11 +62,28 @@ fn parses_token_forms() {
|
||||
assert_eq!(webhook.url, "https://mirror.example.test/webhook");
|
||||
assert_eq!(
|
||||
webhook.secret,
|
||||
TokenConfig::Env("WEBHOOK_SECRET".to_string())
|
||||
TokenConfig::Value("webhook-secret".to_string())
|
||||
);
|
||||
assert_eq!(webhook.full_sync_interval_minutes, Some(60));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_token_form_is_rejected() {
|
||||
let err = toml::from_str::<Config>(
|
||||
r#"
|
||||
[[sites]]
|
||||
name = "github"
|
||||
provider = "github"
|
||||
base_url = "https://github.com"
|
||||
token = { env = "GITHUB_TOKEN" }
|
||||
"#,
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
|
||||
assert!(err.contains("unknown variant") || err.contains("expected"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_defaults_jobs() {
|
||||
let config: Config = toml::from_str("").unwrap();
|
||||
@@ -206,6 +223,34 @@ fn validation_rejects_invalid_repo_filter_regex() {
|
||||
assert!(err.contains("invalid repo_whitelist regex"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_rejects_duplicate_mirror_endpoints() {
|
||||
let duplicate = EndpointConfig {
|
||||
site: "github".to_string(),
|
||||
kind: NamespaceKind::User,
|
||||
namespace: "alice".to_string(),
|
||||
};
|
||||
let config = Config {
|
||||
jobs: crate::config::DEFAULT_JOBS,
|
||||
sites: vec![site("github", ProviderKind::Github)],
|
||||
mirrors: vec![MirrorConfig {
|
||||
name: "broken".to_string(),
|
||||
endpoints: vec![duplicate.clone(), duplicate],
|
||||
sync_visibility: SyncVisibility::All,
|
||||
repo_whitelist: Vec::new(),
|
||||
repo_blacklist: Vec::new(),
|
||||
create_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
conflict_resolution: ConflictResolutionStrategy::Fail,
|
||||
}],
|
||||
webhook: None,
|
||||
};
|
||||
|
||||
let err = validate_config(&config).unwrap_err().to_string();
|
||||
|
||||
assert!(err.contains("duplicate endpoint"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_rejects_zero_jobs() {
|
||||
let mut config = Config {
|
||||
|
||||
+66
-2
@@ -62,6 +62,26 @@ fn group_paths_are_url_encoded_for_gitlab() {
|
||||
assert_eq!(urlencoding("parent/child group"), "parent%2Fchild+group");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn webhook_url_matching_tolerates_trailing_slash_and_default_port() {
|
||||
assert!(webhook_urls_match(
|
||||
"https://example.test",
|
||||
"https://example.test/"
|
||||
));
|
||||
assert!(webhook_urls_match(
|
||||
"https://example.test:443/webhook/",
|
||||
"https://EXAMPLE.test/webhook"
|
||||
));
|
||||
assert!(!webhook_urls_match(
|
||||
"https://example.test/webhook",
|
||||
"https://example.test/"
|
||||
));
|
||||
assert!(!webhook_urls_match(
|
||||
"https://example.test/webhook?token=one",
|
||||
"https://example.test/webhook?token=two"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_token_checks_user_endpoint_with_provider_auth_header() {
|
||||
let (api_url, handle) = one_request_server("200 OK", "{}", |request| {
|
||||
@@ -217,7 +237,7 @@ fn uninstall_webhook_deletes_matching_github_hook() {
|
||||
vec![
|
||||
(
|
||||
"200 OK",
|
||||
r#"[{"id":42,"config":{"url":"https://mirror.example.test/webhook"}}]"#,
|
||||
r#"[{"id":42,"url":"https://api.github.com/repos/alice/repo/hooks/42","config":{"url":"https://mirror.example.test/webhook"}}]"#,
|
||||
),
|
||||
("204 No Content", ""),
|
||||
],
|
||||
@@ -255,11 +275,55 @@ fn uninstall_webhook_deletes_matching_github_hook() {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uninstall_webhook_matches_github_hook_url_without_trailing_slash() {
|
||||
let (api_url, handle) = request_server(
|
||||
vec![
|
||||
(
|
||||
"200 OK",
|
||||
r#"[{"id":42,"url":"https://api.github.com/repos/alice/repo/hooks/42","config":{"url":"https://mirror.example.test"}}]"#,
|
||||
),
|
||||
("204 No Content", ""),
|
||||
],
|
||||
|index, request| match index {
|
||||
0 => assert!(
|
||||
request.starts_with("GET /repos/alice/repo/hooks "),
|
||||
"request was {request}"
|
||||
),
|
||||
1 => assert!(
|
||||
request.starts_with("DELETE /repos/alice/repo/hooks/42 "),
|
||||
"request was {request}"
|
||||
),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
);
|
||||
let site = SiteConfig {
|
||||
api_url: Some(api_url),
|
||||
..site(ProviderKind::Github, None)
|
||||
};
|
||||
let client = ProviderClient::new(&site).unwrap();
|
||||
|
||||
let removed = client
|
||||
.uninstall_webhook(
|
||||
&EndpointConfig {
|
||||
site: "github".to_string(),
|
||||
kind: NamespaceKind::User,
|
||||
namespace: "alice".to_string(),
|
||||
},
|
||||
"repo",
|
||||
"https://mirror.example.test/",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(removed);
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uninstall_webhook_reports_missing_github_hook() {
|
||||
let (api_url, handle) = one_request_server(
|
||||
"200 OK",
|
||||
r#"[{"id":42,"config":{"url":"https://old.example.test/webhook"}}]"#,
|
||||
r#"[{"id":42,"url":"https://api.github.com/repos/alice/repo/hooks/42","config":{"url":"https://old.example.test/webhook"}}]"#,
|
||||
|request| {
|
||||
assert!(
|
||||
request.starts_with("GET /repos/alice/repo/hooks "),
|
||||
|
||||
+47
-24
@@ -59,11 +59,11 @@ fn ref_state_persists_and_requires_exact_remote_ref_match() {
|
||||
let temp = tempfile::TempDir::new().unwrap();
|
||||
let mut refs = BTreeMap::new();
|
||||
refs.insert(
|
||||
"github_alice".to_string(),
|
||||
remote_key("github"),
|
||||
remote_ref_state("abc", &[("main", "111")]),
|
||||
);
|
||||
refs.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_key("gitea"),
|
||||
remote_ref_state("def", &[("main", "111")]),
|
||||
);
|
||||
let mut state = RefState::default();
|
||||
@@ -76,13 +76,13 @@ fn ref_state_persists_and_requires_exact_remote_ref_match() {
|
||||
|
||||
let mut changed_hash = refs.clone();
|
||||
changed_hash.insert(
|
||||
"github_alice".to_string(),
|
||||
remote_key("github"),
|
||||
remote_ref_state("changed", &[("main", "111")]),
|
||||
);
|
||||
assert!(!loaded.repo_matches("sync-1", "repo-a", &changed_hash));
|
||||
|
||||
let mut missing_remote = refs;
|
||||
missing_remote.remove("gitea_alice");
|
||||
missing_remote.remove(&remote_key("gitea"));
|
||||
assert!(!loaded.repo_matches("sync-1", "repo-a", &missing_remote));
|
||||
}
|
||||
|
||||
@@ -179,16 +179,16 @@ fn repo_deletion_decision_propagates_previous_synced_repo_deletion() {
|
||||
let mirror = test_mirror();
|
||||
let mut previous = BTreeMap::new();
|
||||
previous.insert(
|
||||
"github_alice".to_string(),
|
||||
remote_key("github"),
|
||||
remote_ref_state("a", &[("main", "111")]),
|
||||
);
|
||||
previous.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_key("gitea"),
|
||||
remote_ref_state("b", &[("main", "111")]),
|
||||
);
|
||||
let mut current = BTreeMap::new();
|
||||
current.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_key("gitea"),
|
||||
remote_ref_state("b", &[("main", "111")]),
|
||||
);
|
||||
|
||||
@@ -202,8 +202,8 @@ fn repo_deletion_decision_propagates_previous_synced_repo_deletion() {
|
||||
assert_eq!(
|
||||
decision,
|
||||
RepoDeletionDecision::Propagate {
|
||||
deleted_remotes: vec!["github_alice".to_string()],
|
||||
target_remotes: vec!["gitea_alice".to_string()],
|
||||
deleted_remotes: vec![remote_key("github")],
|
||||
target_remotes: vec![remote_key("gitea")],
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -213,16 +213,16 @@ fn repo_deletion_decision_conflicts_when_remaining_repo_changed() {
|
||||
let mirror = test_mirror();
|
||||
let mut previous = BTreeMap::new();
|
||||
previous.insert(
|
||||
"github_alice".to_string(),
|
||||
remote_key("github"),
|
||||
remote_ref_state("a", &[("main", "111")]),
|
||||
);
|
||||
previous.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_key("gitea"),
|
||||
remote_ref_state("b", &[("main", "111")]),
|
||||
);
|
||||
let mut current = BTreeMap::new();
|
||||
current.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_key("gitea"),
|
||||
remote_ref_state("changed", &[("main", "222")]),
|
||||
);
|
||||
|
||||
@@ -236,8 +236,8 @@ fn repo_deletion_decision_conflicts_when_remaining_repo_changed() {
|
||||
assert_eq!(
|
||||
decision,
|
||||
RepoDeletionDecision::Conflict {
|
||||
deleted_remotes: vec!["github_alice".to_string()],
|
||||
changed_remotes: vec!["gitea_alice".to_string()],
|
||||
deleted_remotes: vec![remote_key("github")],
|
||||
changed_remotes: vec![remote_key("gitea")],
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -247,11 +247,11 @@ fn repo_deletion_decision_removes_state_when_deleted_everywhere() {
|
||||
let mirror = test_mirror();
|
||||
let mut previous = BTreeMap::new();
|
||||
previous.insert(
|
||||
"github_alice".to_string(),
|
||||
remote_key("github"),
|
||||
remote_ref_state("a", &[("main", "111")]),
|
||||
);
|
||||
previous.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_key("gitea"),
|
||||
remote_ref_state("b", &[("main", "111")]),
|
||||
);
|
||||
|
||||
@@ -260,7 +260,7 @@ fn repo_deletion_decision_removes_state_when_deleted_everywhere() {
|
||||
assert_eq!(
|
||||
decision,
|
||||
RepoDeletionDecision::DeletedEverywhere {
|
||||
deleted_remotes: vec!["github_alice".to_string(), "gitea_alice".to_string()],
|
||||
deleted_remotes: vec![remote_key("github"), remote_key("gitea")],
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -270,7 +270,7 @@ fn repo_deletion_decision_removes_partial_state_when_deleted_everywhere() {
|
||||
let mirror = test_mirror();
|
||||
let mut previous = BTreeMap::new();
|
||||
previous.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_key("gitea"),
|
||||
remote_ref_state("b", &[("main", "111")]),
|
||||
);
|
||||
|
||||
@@ -279,7 +279,7 @@ fn repo_deletion_decision_removes_partial_state_when_deleted_everywhere() {
|
||||
assert_eq!(
|
||||
decision,
|
||||
RepoDeletionDecision::DeletedEverywhere {
|
||||
deleted_remotes: vec!["github_alice".to_string(), "gitea_alice".to_string()],
|
||||
deleted_remotes: vec![remote_key("github"), remote_key("gitea")],
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -289,12 +289,12 @@ fn repo_deletion_decision_ignores_repos_not_previously_synced_everywhere() {
|
||||
let mirror = test_mirror();
|
||||
let mut previous = BTreeMap::new();
|
||||
previous.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_key("gitea"),
|
||||
remote_ref_state("b", &[("main", "111")]),
|
||||
);
|
||||
let mut current = BTreeMap::new();
|
||||
current.insert(
|
||||
"gitea_alice".to_string(),
|
||||
remote_key("gitea"),
|
||||
remote_ref_state("b", &[("main", "111")]),
|
||||
);
|
||||
|
||||
@@ -316,7 +316,7 @@ fn filtered_sync_visibility_does_not_treat_state_only_repos_as_deleted() {
|
||||
ref_state.set_repo(
|
||||
&mirror.name,
|
||||
"private-repo",
|
||||
BTreeMap::from([("github_alice".to_string(), remote_ref_state("a", &[]))]),
|
||||
BTreeMap::from([(remote_key("github"), remote_ref_state("a", &[]))]),
|
||||
);
|
||||
|
||||
let repo_filter = mirror.repo_filter().unwrap();
|
||||
@@ -332,7 +332,7 @@ fn all_visibility_keeps_state_only_repos_for_deletion_detection() {
|
||||
ref_state.set_repo(
|
||||
&mirror.name,
|
||||
"deleted-repo",
|
||||
BTreeMap::from([("github_alice".to_string(), remote_ref_state("a", &[]))]),
|
||||
BTreeMap::from([(remote_key("github"), remote_ref_state("a", &[]))]),
|
||||
);
|
||||
|
||||
let repo_filter = mirror.repo_filter().unwrap();
|
||||
@@ -350,7 +350,7 @@ fn repo_name_filters_do_not_treat_state_only_repos_as_deleted() {
|
||||
ref_state.set_repo(
|
||||
&mirror.name,
|
||||
"private-repo",
|
||||
BTreeMap::from([("github_alice".to_string(), remote_ref_state("a", &[]))]),
|
||||
BTreeMap::from([(remote_key("github"), remote_ref_state("a", &[]))]),
|
||||
);
|
||||
|
||||
let names = sync_candidate_repo_names(&HashMap::new(), &ref_state, &mirror, &repo_filter);
|
||||
@@ -377,6 +377,25 @@ fn conflict_branch_prefixes_are_reversible_not_slug_collisions() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_remote_names_do_not_slug_collide() {
|
||||
let slash = EndpointConfig {
|
||||
site: "gitlab".to_string(),
|
||||
kind: crate::config::NamespaceKind::Group,
|
||||
namespace: "parent/child".to_string(),
|
||||
};
|
||||
let underscore = EndpointConfig {
|
||||
site: "gitlab".to_string(),
|
||||
kind: crate::config::NamespaceKind::Group,
|
||||
namespace: "parent_child".to_string(),
|
||||
};
|
||||
|
||||
assert_ne!(
|
||||
remote_name_for_endpoint(&slash),
|
||||
remote_name_for_endpoint(&underscore)
|
||||
);
|
||||
}
|
||||
|
||||
fn remote_ref_state(hash: &str, branches: &[(&str, &str)]) -> RemoteRefState {
|
||||
RemoteRefState {
|
||||
hash: hash.to_string(),
|
||||
@@ -425,6 +444,10 @@ fn endpoint(site: &str) -> EndpointConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_key(site: &str) -> String {
|
||||
remote_name_for_endpoint(&endpoint(site))
|
||||
}
|
||||
|
||||
fn endpoint_repo(site: &str) -> EndpointRepo {
|
||||
EndpointRepo {
|
||||
endpoint: endpoint(site),
|
||||
|
||||
@@ -342,7 +342,6 @@ fn blocked_webhook_install_is_skipped_and_recorded() {
|
||||
dry_run: false,
|
||||
},
|
||||
&state,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -401,7 +400,7 @@ fn duplicate_webhook_error_records_existing_installation() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_task_state_cache_is_only_used_for_sync() {
|
||||
fn install_task_rechecks_cached_installation() {
|
||||
let (api_url, handle) = request_server(
|
||||
vec![("200 OK", "[]"), ("201 Created", r#"{"id":1}"#)],
|
||||
|index, request| match index {
|
||||
@@ -442,7 +441,6 @@ fn install_task_state_cache_is_only_used_for_sync() {
|
||||
dry_run: false,
|
||||
},
|
||||
&state,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user