[O] Cleanup codebase
This commit is contained in:
+3
-771
@@ -10,11 +10,6 @@ use dialoguer::{Confirm, Input, Password, Select, theme::ColorfulTheme};
|
||||
use reqwest::blocking::Client;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(test)]
|
||||
use anyhow::bail;
|
||||
#[cfg(test)]
|
||||
use std::io::{BufRead, Write};
|
||||
|
||||
use crate::config::{
|
||||
Config, EndpointConfig, MirrorConfig, NamespaceKind, ProviderKind, SiteConfig, TokenConfig,
|
||||
Visibility, WebhookConfig,
|
||||
@@ -569,382 +564,9 @@ fn pat_instruction_lines(provider: &ProviderKind, base_url: &str) -> Vec<String>
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn run_config_wizard_with_io<R, W>(
|
||||
mut config: Config,
|
||||
reader: &mut R,
|
||||
writer: &mut W,
|
||||
) -> Result<Config>
|
||||
where
|
||||
R: BufRead,
|
||||
W: Write,
|
||||
{
|
||||
writeln!(writer, "git-sync configuration wizard")?;
|
||||
if config.mirrors.is_empty() {
|
||||
add_sync_group(reader, writer, &mut config)?;
|
||||
write_sync_groups(&config, writer)?;
|
||||
} else {
|
||||
write_sync_groups(&config, writer)?;
|
||||
}
|
||||
|
||||
loop {
|
||||
match prompt_wizard_action(reader, writer)? {
|
||||
WizardAction::AddSyncGroup => {
|
||||
add_sync_group(reader, writer, &mut config)?;
|
||||
write_sync_groups(&config, writer)?;
|
||||
}
|
||||
WizardAction::DeleteSyncGroup => {
|
||||
if delete_sync_group(reader, writer, &mut config)? {
|
||||
write_sync_groups(&config, writer)?;
|
||||
}
|
||||
}
|
||||
WizardAction::Done => break,
|
||||
}
|
||||
}
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
mod test_io;
|
||||
#[cfg(test)]
|
||||
fn add_sync_group<R, W>(reader: &mut R, writer: &mut W, config: &mut Config) -> Result<()>
|
||||
where
|
||||
R: BufRead,
|
||||
W: Write,
|
||||
{
|
||||
let mut endpoints = Vec::new();
|
||||
let first = prompt_target(reader, writer, "Profile/org URL")?;
|
||||
endpoints.push(ensure_credentials(config, first, reader, writer)?);
|
||||
let second = prompt_target(reader, writer, "Profile/org URL to sync with")?;
|
||||
endpoints.push(ensure_credentials(config, second, reader, writer)?);
|
||||
|
||||
while prompt_bool(
|
||||
reader,
|
||||
writer,
|
||||
"Add a third endpoint for 3-way sync?",
|
||||
false,
|
||||
)? {
|
||||
let next = prompt_target(reader, writer, "Additional profile/org URL")?;
|
||||
endpoints.push(ensure_credentials(config, next, reader, writer)?);
|
||||
}
|
||||
|
||||
config.upsert_mirror(MirrorConfig {
|
||||
name: next_mirror_name(config),
|
||||
endpoints,
|
||||
create_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
allow_force: false,
|
||||
});
|
||||
prompt_webhook_setup(reader, writer, config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn prompt_webhook_setup<R, W>(reader: &mut R, writer: &mut W, config: &mut Config) -> Result<()>
|
||||
where
|
||||
R: BufRead,
|
||||
W: Write,
|
||||
{
|
||||
if config
|
||||
.webhook
|
||||
.as_ref()
|
||||
.is_some_and(|webhook| webhook.install)
|
||||
{
|
||||
writeln!(writer, "Webhooks already enabled.")?;
|
||||
return Ok(());
|
||||
}
|
||||
writeln!(
|
||||
writer,
|
||||
"Install webhooks? Strongly recommended because immediate sync greatly reduces conflicts."
|
||||
)?;
|
||||
if !prompt_bool(reader, writer, "Install webhook?", true)? {
|
||||
return Ok(());
|
||||
}
|
||||
let url = prompt_required(reader, writer, "Webhook URL reachable by providers")?;
|
||||
if let Err(error) = validate_url(&url) {
|
||||
bail!(error);
|
||||
}
|
||||
let full_sync_interval_minutes = if prompt_bool(
|
||||
reader,
|
||||
writer,
|
||||
"Run periodic full sync while serve is running?",
|
||||
true,
|
||||
)? {
|
||||
Some(
|
||||
prompt_with_default(reader, writer, "Full sync interval in minutes", "60")?
|
||||
.parse::<u64>()
|
||||
.context("full sync interval must be a number")?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
config.webhook = Some(WebhookConfig {
|
||||
install: true,
|
||||
url,
|
||||
secret: TokenConfig::Value("test-webhook-secret".to_string()),
|
||||
full_sync_interval_minutes,
|
||||
reachability_check_interval_minutes: Some(15),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn prompt_wizard_action<R, W>(reader: &mut R, writer: &mut W) -> Result<WizardAction>
|
||||
where
|
||||
R: BufRead,
|
||||
W: Write,
|
||||
{
|
||||
loop {
|
||||
writeln!(writer, "What would you like to do?")?;
|
||||
writeln!(writer, " 1. Add another sync group")?;
|
||||
writeln!(writer, " 2. Delete an existing group")?;
|
||||
writeln!(writer, " 3. Done")?;
|
||||
write!(writer, "Choose an option: ")?;
|
||||
writer.flush()?;
|
||||
let value = read_line(reader)?.trim().to_ascii_lowercase();
|
||||
match value.as_str() {
|
||||
"1" | "add" | "add another sync group" => return Ok(WizardAction::AddSyncGroup),
|
||||
"2" | "delete" | "delete an existing group" => {
|
||||
return Ok(WizardAction::DeleteSyncGroup);
|
||||
}
|
||||
"3" | "done" | "finish" => return Ok(WizardAction::Done),
|
||||
_ => writeln!(writer, "Enter 1, 2, or 3.")?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn delete_sync_group<R, W>(reader: &mut R, writer: &mut W, config: &mut Config) -> Result<bool>
|
||||
where
|
||||
R: BufRead,
|
||||
W: Write,
|
||||
{
|
||||
if config.mirrors.is_empty() {
|
||||
writeln!(writer, "No sync groups to delete.")?;
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
loop {
|
||||
writeln!(writer, "Delete sync group")?;
|
||||
for (index, option) in sync_group_summaries(config).iter().enumerate() {
|
||||
writeln!(writer, " {}. {}", index + 1, option)?;
|
||||
}
|
||||
writeln!(writer, " {}. Back", config.mirrors.len() + 1)?;
|
||||
write!(writer, "Choose a sync group: ")?;
|
||||
writer.flush()?;
|
||||
let value = read_line(reader)?.trim().to_ascii_lowercase();
|
||||
if value == "b" || value == "back" {
|
||||
return Ok(false);
|
||||
}
|
||||
match value.parse::<usize>() {
|
||||
Ok(index) if (1..=config.mirrors.len()).contains(&index) => {
|
||||
let name = config.mirrors[index - 1].name.clone();
|
||||
config.remove_mirror(&name)?;
|
||||
writeln!(writer, "deleted sync group {index}")?;
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(index) if index == config.mirrors.len() + 1 => return Ok(false),
|
||||
_ => writeln!(writer, "Enter a sync group number, or choose Back.")?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn prompt_target<R, W>(reader: &mut R, writer: &mut W, prompt: &str) -> Result<ProfileTarget>
|
||||
where
|
||||
R: BufRead,
|
||||
W: Write,
|
||||
{
|
||||
let url = prompt_required(reader, writer, prompt)?;
|
||||
let parsed = parse_profile_url(&url)?;
|
||||
let provider = known_provider_from_host(&parsed.host).unwrap_or_else(|| {
|
||||
prompt_provider(reader, writer, &parsed.base_url).expect("provider prompt failed")
|
||||
});
|
||||
Ok(ProfileTarget {
|
||||
base_url: parsed.base_url,
|
||||
provider,
|
||||
namespace: parsed.namespace,
|
||||
kind: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn ensure_credentials<R, W>(
|
||||
config: &mut Config,
|
||||
target: ProfileTarget,
|
||||
reader: &mut R,
|
||||
writer: &mut W,
|
||||
) -> Result<EndpointConfig>
|
||||
where
|
||||
R: BufRead,
|
||||
W: Write,
|
||||
{
|
||||
if let Some(site) = config.sites.iter().find(|site| {
|
||||
site.provider == target.provider
|
||||
&& trim_url_end(&site.base_url) == trim_url_end(&target.base_url)
|
||||
}) {
|
||||
let kind = target.kind.clone().unwrap_or_else(|| {
|
||||
prompt_namespace_kind(reader, writer, &target.namespace).expect("kind prompt failed")
|
||||
});
|
||||
let endpoint = target_endpoint(&target, kind, site.name.clone());
|
||||
writeln!(
|
||||
writer,
|
||||
"Using existing credentials for {}",
|
||||
target_display(&target)
|
||||
)?;
|
||||
return Ok(endpoint);
|
||||
}
|
||||
|
||||
for line in pat_instruction_lines(&target.provider, &target.base_url) {
|
||||
writeln!(writer, "{line}")?;
|
||||
}
|
||||
let token = prompt_required(reader, writer, "PAT token")?;
|
||||
let site = SiteConfig {
|
||||
name: default_site_name(config, &target.base_url, &target.provider),
|
||||
provider: target.provider.clone(),
|
||||
base_url: target.base_url.clone(),
|
||||
api_url: None,
|
||||
token: TokenConfig::Value(token),
|
||||
git_username: None,
|
||||
};
|
||||
let site_name = site.name.clone();
|
||||
config.upsert_site(site);
|
||||
let kind = target.kind.clone().unwrap_or_else(|| {
|
||||
prompt_namespace_kind(reader, writer, &target.namespace).expect("kind prompt failed")
|
||||
});
|
||||
Ok(target_endpoint(&target, kind, site_name))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn prompt_provider<R, W>(reader: &mut R, writer: &mut W, base_url: &str) -> Result<ProviderKind>
|
||||
where
|
||||
R: BufRead,
|
||||
W: Write,
|
||||
{
|
||||
loop {
|
||||
let value = prompt_required(reader, writer, &format!("Provider for {base_url}"))?;
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"github" => return Ok(ProviderKind::Github),
|
||||
"gitlab" => return Ok(ProviderKind::Gitlab),
|
||||
"gitea" => return Ok(ProviderKind::Gitea),
|
||||
"forgejo" => return Ok(ProviderKind::Forgejo),
|
||||
_ => writeln!(
|
||||
writer,
|
||||
"Provider must be github, gitlab, gitea, or forgejo."
|
||||
)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn prompt_namespace_kind<R, W>(
|
||||
reader: &mut R,
|
||||
writer: &mut W,
|
||||
namespace: &str,
|
||||
) -> Result<NamespaceKind>
|
||||
where
|
||||
R: BufRead,
|
||||
W: Write,
|
||||
{
|
||||
loop {
|
||||
let value = prompt_with_default(reader, writer, &format!("What is {namespace}?"), "user")?;
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"user" => return Ok(NamespaceKind::User),
|
||||
"org" | "organization" => return Ok(NamespaceKind::Org),
|
||||
"group" => return Ok(NamespaceKind::Group),
|
||||
_ => writeln!(writer, "Namespace kind must be user, org, or group.")?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn write_sync_groups<W>(config: &Config, writer: &mut W) -> Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
writeln!(writer, "Sync groups")?;
|
||||
if config.mirrors.is_empty() {
|
||||
writeln!(writer, "No sync groups configured.")?;
|
||||
return Ok(());
|
||||
}
|
||||
for (index, mirror) in config.mirrors.iter().enumerate() {
|
||||
writeln!(
|
||||
writer,
|
||||
"{}. {}",
|
||||
index + 1,
|
||||
sync_group_summary(config, mirror)
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn prompt_bool<R, W>(reader: &mut R, writer: &mut W, label: &str, default: bool) -> Result<bool>
|
||||
where
|
||||
R: BufRead,
|
||||
W: Write,
|
||||
{
|
||||
let default_label = if default { "Y/n" } else { "y/N" };
|
||||
loop {
|
||||
write!(writer, "{label} [{default_label}]: ")?;
|
||||
writer.flush()?;
|
||||
let value = read_line(reader)?.trim().to_ascii_lowercase();
|
||||
match value.as_str() {
|
||||
"" => return Ok(default),
|
||||
"y" | "yes" | "true" => return Ok(true),
|
||||
"n" | "no" | "false" => return Ok(false),
|
||||
_ => writeln!(writer, "Enter yes or no.")?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn prompt_required<R, W>(reader: &mut R, writer: &mut W, label: &str) -> Result<String>
|
||||
where
|
||||
R: BufRead,
|
||||
W: Write,
|
||||
{
|
||||
loop {
|
||||
write!(writer, "{label}: ")?;
|
||||
writer.flush()?;
|
||||
let value = read_line(reader)?.trim().to_string();
|
||||
if !value.is_empty() {
|
||||
return Ok(value);
|
||||
}
|
||||
writeln!(writer, "A value is required.")?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn prompt_with_default<R, W>(
|
||||
reader: &mut R,
|
||||
writer: &mut W,
|
||||
label: &str,
|
||||
default: &str,
|
||||
) -> Result<String>
|
||||
where
|
||||
R: BufRead,
|
||||
W: Write,
|
||||
{
|
||||
write!(writer, "{label} [{default}]: ")?;
|
||||
writer.flush()?;
|
||||
let value = read_line(reader)?.trim().to_string();
|
||||
if value.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn read_line<R>(reader: &mut R) -> Result<String>
|
||||
where
|
||||
R: BufRead,
|
||||
{
|
||||
let mut value = String::new();
|
||||
let bytes = reader.read_line(&mut value)?;
|
||||
if bytes == 0 {
|
||||
bail!("unexpected end of input while reading interactive configuration");
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
use test_io::*;
|
||||
|
||||
fn parse_profile_url(value: &str) -> Result<ParsedProfileUrl> {
|
||||
let normalized = ensure_url_scheme(value);
|
||||
@@ -1291,394 +913,4 @@ fn generate_webhook_secret() -> String {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
fn wizard_builds_sync_group_from_profile_urls() {
|
||||
let input = [
|
||||
"https://github.com/hykilpikonna",
|
||||
"gh-token",
|
||||
"",
|
||||
"https://gitea.example.test/azalea",
|
||||
"gt-token",
|
||||
"",
|
||||
"n",
|
||||
"n",
|
||||
"3",
|
||||
]
|
||||
.join("\n")
|
||||
+ "\n";
|
||||
let mut reader = Cursor::new(input.as_bytes());
|
||||
let mut output = Vec::new();
|
||||
|
||||
let config =
|
||||
run_config_wizard_with_io(Config::default(), &mut reader, &mut output).unwrap();
|
||||
|
||||
assert_eq!(config.sites.len(), 2);
|
||||
assert_eq!(config.sites[0].name, "github");
|
||||
assert_eq!(config.sites[0].provider, ProviderKind::Github);
|
||||
assert_eq!(config.sites[0].base_url, "https://github.com");
|
||||
assert_eq!(
|
||||
config.sites[0].token,
|
||||
TokenConfig::Value("gh-token".to_string())
|
||||
);
|
||||
assert_eq!(config.sites[1].name, "gitea-example-test");
|
||||
assert_eq!(config.sites[1].provider, ProviderKind::Gitea);
|
||||
assert_eq!(config.sites[1].base_url, "https://gitea.example.test");
|
||||
|
||||
assert_eq!(config.mirrors.len(), 1);
|
||||
assert_eq!(config.mirrors[0].name, "sync-1");
|
||||
assert_eq!(config.mirrors[0].endpoints.len(), 2);
|
||||
assert_eq!(config.mirrors[0].endpoints[0].site, "github");
|
||||
assert_eq!(config.mirrors[0].endpoints[0].kind, NamespaceKind::User);
|
||||
assert_eq!(config.mirrors[0].endpoints[0].namespace, "hykilpikonna");
|
||||
assert_eq!(config.mirrors[0].endpoints[1].site, "gitea-example-test");
|
||||
assert_eq!(config.mirrors[0].endpoints[1].namespace, "azalea");
|
||||
assert!(config.mirrors[0].create_missing);
|
||||
assert_eq!(config.mirrors[0].visibility, Visibility::Private);
|
||||
assert!(!config.mirrors[0].allow_force);
|
||||
|
||||
let output = String::from_utf8(output).unwrap();
|
||||
assert!(output.contains("1. github.com/hykilpikonna <-> gitea.example.test/azalea"));
|
||||
assert!(output.contains("Add another sync group"));
|
||||
assert!(output.contains("Delete an existing group"));
|
||||
assert!(output.contains("Done"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wizard_can_build_three_way_sync() {
|
||||
let input = [
|
||||
"https://github.com/alice",
|
||||
"gh-token",
|
||||
"",
|
||||
"https://gitlab.com/alice",
|
||||
"gl-token",
|
||||
"",
|
||||
"y",
|
||||
"https://gitea.example.test/alice",
|
||||
"gt-token",
|
||||
"",
|
||||
"n",
|
||||
"n",
|
||||
"3",
|
||||
]
|
||||
.join("\n")
|
||||
+ "\n";
|
||||
let mut reader = Cursor::new(input.as_bytes());
|
||||
let mut output = Vec::new();
|
||||
|
||||
let config =
|
||||
run_config_wizard_with_io(Config::default(), &mut reader, &mut output).unwrap();
|
||||
|
||||
assert_eq!(config.mirrors.len(), 1);
|
||||
assert_eq!(config.mirrors[0].endpoints.len(), 3);
|
||||
assert_eq!(config.sites.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wizard_can_enable_webhooks() {
|
||||
let input = [
|
||||
"https://github.com/alice",
|
||||
"gh-token",
|
||||
"",
|
||||
"https://gitea.example.test/alice",
|
||||
"gt-token",
|
||||
"",
|
||||
"n",
|
||||
"y",
|
||||
"https://mirror.example.test/webhook",
|
||||
"y",
|
||||
"30",
|
||||
"3",
|
||||
]
|
||||
.join("\n")
|
||||
+ "\n";
|
||||
let mut reader = Cursor::new(input.as_bytes());
|
||||
let mut output = Vec::new();
|
||||
|
||||
let config =
|
||||
run_config_wizard_with_io(Config::default(), &mut reader, &mut output).unwrap();
|
||||
|
||||
let webhook = config.webhook.unwrap();
|
||||
assert!(webhook.install);
|
||||
assert_eq!(webhook.url, "https://mirror.example.test/webhook");
|
||||
assert_eq!(webhook.full_sync_interval_minutes, Some(30));
|
||||
assert_eq!(webhook.reachability_check_interval_minutes, Some(15));
|
||||
assert_eq!(
|
||||
webhook.secret,
|
||||
TokenConfig::Value("test-webhook-secret".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wizard_reuses_existing_credentials_for_same_instance() {
|
||||
let config = Config {
|
||||
sites: vec![SiteConfig {
|
||||
name: "github".to_string(),
|
||||
provider: ProviderKind::Github,
|
||||
base_url: "https://github.com".to_string(),
|
||||
api_url: None,
|
||||
token: TokenConfig::Value("existing".to_string()),
|
||||
git_username: None,
|
||||
}],
|
||||
mirrors: Vec::new(),
|
||||
webhook: None,
|
||||
};
|
||||
let input = [
|
||||
"https://github.com/alice",
|
||||
"",
|
||||
"https://github.com/bob",
|
||||
"",
|
||||
"n",
|
||||
"n",
|
||||
"3",
|
||||
]
|
||||
.join("\n")
|
||||
+ "\n";
|
||||
let mut reader = Cursor::new(input.as_bytes());
|
||||
let mut output = Vec::new();
|
||||
|
||||
let updated = run_config_wizard_with_io(config, &mut reader, &mut output).unwrap();
|
||||
|
||||
assert_eq!(updated.sites.len(), 1);
|
||||
assert_eq!(updated.mirrors[0].endpoints[0].site, "github");
|
||||
assert_eq!(updated.mirrors[0].endpoints[1].site, "github");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wizard_starts_existing_config_at_sync_group_menu() {
|
||||
let config = Config {
|
||||
sites: vec![
|
||||
SiteConfig {
|
||||
name: "github".to_string(),
|
||||
provider: ProviderKind::Github,
|
||||
base_url: "https://github.com".to_string(),
|
||||
api_url: None,
|
||||
token: TokenConfig::Value("existing-gh".to_string()),
|
||||
git_username: None,
|
||||
},
|
||||
SiteConfig {
|
||||
name: "gitea".to_string(),
|
||||
provider: ProviderKind::Gitea,
|
||||
base_url: "https://gitea.example.test".to_string(),
|
||||
api_url: None,
|
||||
token: TokenConfig::Value("existing-gt".to_string()),
|
||||
git_username: None,
|
||||
},
|
||||
],
|
||||
mirrors: vec![MirrorConfig {
|
||||
name: "sync-1".to_string(),
|
||||
endpoints: vec![
|
||||
EndpointConfig {
|
||||
site: "github".to_string(),
|
||||
kind: NamespaceKind::User,
|
||||
namespace: "alice".to_string(),
|
||||
},
|
||||
EndpointConfig {
|
||||
site: "gitea".to_string(),
|
||||
kind: NamespaceKind::User,
|
||||
namespace: "alice".to_string(),
|
||||
},
|
||||
],
|
||||
create_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
allow_force: false,
|
||||
}],
|
||||
webhook: None,
|
||||
};
|
||||
let mut reader = Cursor::new(b"3\n".as_slice());
|
||||
let mut output = Vec::new();
|
||||
|
||||
let updated = run_config_wizard_with_io(config, &mut reader, &mut output).unwrap();
|
||||
|
||||
assert_eq!(updated.mirrors.len(), 1);
|
||||
let output = String::from_utf8(output).unwrap();
|
||||
assert!(output.contains("1. github.com/alice <-> gitea.example.test/alice"));
|
||||
assert!(output.contains("What would you like to do?"));
|
||||
assert!(!output.contains("Profile/org URL:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wizard_deletes_existing_sync_group_from_menu() {
|
||||
let config = Config {
|
||||
sites: vec![
|
||||
SiteConfig {
|
||||
name: "github".to_string(),
|
||||
provider: ProviderKind::Github,
|
||||
base_url: "https://github.com".to_string(),
|
||||
api_url: None,
|
||||
token: TokenConfig::Value("existing-gh".to_string()),
|
||||
git_username: None,
|
||||
},
|
||||
SiteConfig {
|
||||
name: "gitea".to_string(),
|
||||
provider: ProviderKind::Gitea,
|
||||
base_url: "https://gitea.example.test".to_string(),
|
||||
api_url: None,
|
||||
token: TokenConfig::Value("existing-gt".to_string()),
|
||||
git_username: None,
|
||||
},
|
||||
],
|
||||
mirrors: vec![MirrorConfig {
|
||||
name: "sync-1".to_string(),
|
||||
endpoints: vec![
|
||||
EndpointConfig {
|
||||
site: "github".to_string(),
|
||||
kind: NamespaceKind::User,
|
||||
namespace: "alice".to_string(),
|
||||
},
|
||||
EndpointConfig {
|
||||
site: "gitea".to_string(),
|
||||
kind: NamespaceKind::User,
|
||||
namespace: "alice".to_string(),
|
||||
},
|
||||
],
|
||||
create_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
allow_force: false,
|
||||
}],
|
||||
webhook: None,
|
||||
};
|
||||
let input = ["2", "1", "3"].join("\n") + "\n";
|
||||
let mut reader = Cursor::new(input.as_bytes());
|
||||
let mut output = Vec::new();
|
||||
|
||||
let updated = run_config_wizard_with_io(config, &mut reader, &mut output).unwrap();
|
||||
|
||||
assert!(updated.mirrors.is_empty());
|
||||
let output = String::from_utf8(output).unwrap();
|
||||
assert!(output.contains("Delete sync group"));
|
||||
assert!(output.contains("2. Back"));
|
||||
assert!(output.contains("deleted sync group 1"));
|
||||
assert!(output.contains("No sync groups configured."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wizard_can_go_back_from_delete_menu() {
|
||||
let config = Config {
|
||||
sites: vec![
|
||||
SiteConfig {
|
||||
name: "github".to_string(),
|
||||
provider: ProviderKind::Github,
|
||||
base_url: "https://github.com".to_string(),
|
||||
api_url: None,
|
||||
token: TokenConfig::Value("existing-gh".to_string()),
|
||||
git_username: None,
|
||||
},
|
||||
SiteConfig {
|
||||
name: "gitea".to_string(),
|
||||
provider: ProviderKind::Gitea,
|
||||
base_url: "https://gitea.example.test".to_string(),
|
||||
api_url: None,
|
||||
token: TokenConfig::Value("existing-gt".to_string()),
|
||||
git_username: None,
|
||||
},
|
||||
],
|
||||
mirrors: vec![MirrorConfig {
|
||||
name: "sync-1".to_string(),
|
||||
endpoints: vec![
|
||||
EndpointConfig {
|
||||
site: "github".to_string(),
|
||||
kind: NamespaceKind::User,
|
||||
namespace: "alice".to_string(),
|
||||
},
|
||||
EndpointConfig {
|
||||
site: "gitea".to_string(),
|
||||
kind: NamespaceKind::User,
|
||||
namespace: "alice".to_string(),
|
||||
},
|
||||
],
|
||||
create_missing: true,
|
||||
visibility: Visibility::Private,
|
||||
allow_force: false,
|
||||
}],
|
||||
webhook: None,
|
||||
};
|
||||
let input = ["2", "2", "3"].join("\n") + "\n";
|
||||
let mut reader = Cursor::new(input.as_bytes());
|
||||
let mut output = Vec::new();
|
||||
|
||||
let updated = run_config_wizard_with_io(config, &mut reader, &mut output).unwrap();
|
||||
|
||||
assert_eq!(updated.mirrors.len(), 1);
|
||||
let output = String::from_utf8(output).unwrap();
|
||||
assert!(output.contains("2. Back"));
|
||||
assert!(!output.contains("deleted sync group"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wizard_reports_eof_instead_of_looping() {
|
||||
let mut reader = Cursor::new(b"".as_slice());
|
||||
let mut output = Vec::new();
|
||||
|
||||
let err = run_config_wizard_with_io(Config::default(), &mut reader, &mut output)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
|
||||
assert!(err.contains("unexpected end of input"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_urls_are_parsed_into_base_and_namespace() {
|
||||
let parsed = parse_profile_url("github.com/alice").unwrap();
|
||||
assert_eq!(parsed.base_url, "https://github.com");
|
||||
assert_eq!(parsed.host, "github.com");
|
||||
assert_eq!(parsed.namespace, "alice");
|
||||
|
||||
let parsed = parse_profile_url("https://gitlab.example.test:8443/groups/team").unwrap();
|
||||
assert_eq!(parsed.base_url, "https://gitlab.example.test:8443");
|
||||
assert_eq!(parsed.namespace, "groups/team");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn site_names_are_derived_from_urls_and_made_unique() {
|
||||
let mut config = Config::default();
|
||||
assert_eq!(
|
||||
default_site_name(&config, "https://github.com", &ProviderKind::Github),
|
||||
"github"
|
||||
);
|
||||
assert_eq!(
|
||||
default_site_name(
|
||||
&config,
|
||||
"https://git.my-company.com:3000",
|
||||
&ProviderKind::Gitea
|
||||
),
|
||||
"git-my-company"
|
||||
);
|
||||
|
||||
config.upsert_site(SiteConfig {
|
||||
name: "github".to_string(),
|
||||
provider: ProviderKind::Github,
|
||||
base_url: "https://github.com".to_string(),
|
||||
api_url: None,
|
||||
token: TokenConfig::Value("token".to_string()),
|
||||
git_username: None,
|
||||
});
|
||||
assert_eq!(
|
||||
default_site_name(&config, "https://github.com", &ProviderKind::Github),
|
||||
"github-2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_creation_urls_are_provider_specific() {
|
||||
assert_eq!(
|
||||
token_creation_url(&ProviderKind::Github, "https://github.com/"),
|
||||
"https://github.com/settings/tokens"
|
||||
);
|
||||
assert_eq!(
|
||||
token_creation_url(&ProviderKind::Gitlab, "https://gitlab.example.test"),
|
||||
"https://gitlab.example.test/-/user_settings/personal_access_tokens?name=git-sync&scopes=api"
|
||||
);
|
||||
assert_eq!(
|
||||
token_creation_url(&ProviderKind::Gitea, "gitea.example.test"),
|
||||
"https://gitea.example.test/user/settings/applications"
|
||||
);
|
||||
assert_eq!(
|
||||
token_creation_url(&ProviderKind::Forgejo, "forgejo.example.test"),
|
||||
"https://forgejo.example.test/user/settings/applications"
|
||||
);
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
Reference in New Issue
Block a user