use super::*;
use assert2::let_assert;
use secrecy::ExposeSecret;
use std::collections::BTreeMap;
use std::path::PathBuf;
use crate::{
s,
server::{AppConfig, Http, Storage},
webhook::push::Branch,
};
mod url;
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
type TestResult = Result<()>;
mod server_repo_config {
use super::*;
#[test]
fn should_not_return_repo_config_when_no_branches() {
let main = None;
let next = None;
let dev = None;
let src = ServerRepoConfig::new(given::a_name(), given::a_name(), None, main, next, dev);
let_assert!(None = src.repo_config());
}
#[test]
fn should_return_repo_config_when_branches() {
let main = given::a_name();
let next = given::a_name();
let dev = given::a_name();
let src = ServerRepoConfig::new(
given::a_name(),
given::a_name(),
None,
Some(main.clone()),
Some(next.clone()),
Some(dev.clone()),
);
let_assert!(Some(rc) = src.repo_config());
assert_eq!(
rc,
RepoConfig::new(RepoBranches::new(main, next, dev), RepoConfigSource::Server)
);
}
#[test]
fn should_return_repo() {
let repo_path = given::a_name();
let src = ServerRepoConfig::new(
repo_path.clone(),
given::a_name(),
None,
Some(given::a_name()),
Some(given::a_name()),
Some(given::a_name()),
);
assert_eq!(src.repo(), RepoPath::new(repo_path));
}
#[test]
fn should_return_branch() {
let branch = given::a_name();
let src = ServerRepoConfig::new(
given::a_name(),
branch.clone(),
None,
Some(given::a_name()),
Some(given::a_name()),
Some(given::a_name()),
);
assert_eq!(src.branch(), BranchName::new(branch));
}
#[test]
fn should_return_gitdir() {
let gitdir = given::a_name();
let src = ServerRepoConfig::new(
given::a_name(),
given::a_name(),
Some(gitdir.clone().into()),
Some(given::a_name()),
Some(given::a_name()),
Some(given::a_name()),
);
assert_eq!(
src.gitdir(),
Some(GitDir::new(
PathBuf::default().join(gitdir),
git_dir::StoragePathType::External
))
);
}
}
mod repo_config {
use super::*;
#[test]
fn should_parse_toml() -> TestResult {
let main = given::a_name();
let next = given::a_name();
let dev = given::a_name();
let toml = format!(
r#"
[branches]
main = "{main}"
next = "{next}"
dev = "{dev}"
"#
);
let rc = RepoConfig::parse(toml.as_str())?;
assert_eq!(
rc,
RepoConfig::new(
RepoBranches::new(main, next, dev),
RepoConfigSource::Repo )
);
Ok(())
}
#[test]
fn should_return_branches() {
let main = given::a_name();
let next = given::a_name();
let dev = given::a_name();
let branches = RepoBranches::new(main, next, dev);
let repo_config = RepoConfig::new(branches.clone(), RepoConfigSource::Repo);
assert_eq!(repo_config.branches(), &branches);
}
#[test]
fn should_return_source() {
let main = given::a_name();
let next = given::a_name();
let dev = given::a_name();
let repo_config =
RepoConfig::new(RepoBranches::new(main, next, dev), RepoConfigSource::Repo);
assert_eq!(repo_config.source(), RepoConfigSource::Repo);
}
}
mod forge_config {
use given::maybe_a_number;
use super::*;
#[test]
fn should_return_repos() {
let forge_type = ForgeType::MockForge;
let hostname = given::a_name();
let user = given::a_name();
let token = given::a_name();
let red_name = format!("a-{}", given::a_name());
let blue_name = format!("b-{}", given::a_name());
let red = ServerRepoConfig::new(red_name.clone(), given::a_name(), None, None, None, None);
let blue =
ServerRepoConfig::new(blue_name.clone(), given::a_name(), None, None, None, None);
let mut repos = BTreeMap::new();
repos.insert(red_name.clone(), red.clone());
repos.insert(blue_name.clone(), blue.clone());
let fc = ForgeConfig::new(forge_type, hostname, user, token, maybe_a_number(), repos);
let returned_repos = fc.repos().collect::<Vec<_>>();
assert_eq!(
returned_repos,
vec![
(RepoAlias::new(red_name.as_str()), &red),
(RepoAlias::new(blue_name.as_str()), &blue),
]
);
}
#[test]
fn should_return_forge_type() {
let forge_type = ForgeType::MockForge;
let hostname = given::a_name();
let user = given::a_name();
let token = given::a_name();
let repos = BTreeMap::new();
let fc = ForgeConfig::new(forge_type, hostname, user, token, maybe_a_number(), repos);
assert_eq!(fc.forge_type(), ForgeType::MockForge);
}
#[test]
fn should_return_hostname() {
let forge_type = ForgeType::MockForge;
let hostname = given::a_name();
let user = given::a_name();
let token = given::a_name();
let repos = BTreeMap::new();
let fc = ForgeConfig::new(
forge_type,
hostname.clone(),
user,
token,
maybe_a_number(),
repos,
);
assert_eq!(fc.hostname(), Hostname::new(hostname));
}
#[test]
fn should_return_user() {
let forge_type = ForgeType::MockForge;
let hostname = given::a_name();
let user = given::a_name();
let token = given::a_name();
let repos = BTreeMap::new();
let fc = ForgeConfig::new(
forge_type,
hostname,
user.clone(),
token,
maybe_a_number(),
repos,
);
assert_eq!(fc.user(), User::new(user));
}
#[test]
fn should_return_token() {
let forge_type = ForgeType::MockForge;
let hostname = given::a_name();
let user = given::a_name();
let token = given::a_name();
let repos = BTreeMap::new();
let fc = ForgeConfig::new(
forge_type,
hostname,
user,
token.clone(),
maybe_a_number(),
repos,
);
assert_eq!(fc.token().expose_secret(), token.as_str());
}
#[test]
fn should_return_repo() {
let forge_type = ForgeType::MockForge;
let hostname = given::a_name();
let user = given::a_name();
let token = given::a_name();
let red_name = given::a_name();
let blue_name = given::a_name();
let red = ServerRepoConfig::new(red_name.clone(), given::a_name(), None, None, None, None);
let blue =
ServerRepoConfig::new(blue_name.clone(), given::a_name(), None, None, None, None);
let mut repos = BTreeMap::new();
repos.insert(red_name.clone(), red.clone());
repos.insert(blue_name, blue);
let fc = ForgeConfig::new(forge_type, hostname, user, token, maybe_a_number(), repos);
let returned_repo = fc.get_repo(red_name.as_str());
assert_eq!(returned_repo, Some(&red),);
}
}
mod forge_details {
use super::*;
use secrecy::ExposeSecret;
#[test]
fn should_return_forge_alias() {
let forge_type = ForgeType::MockForge;
let hostname = Hostname::new(given::a_name());
let user = User::new(given::a_name());
let token = ApiToken::new(given::a_name().into());
let forge_alias = ForgeAlias::new(given::a_name());
let forge_details = ForgeDetails::new(
forge_alias.clone(),
forge_type,
hostname,
user,
token,
given::maybe_a_number().map(CommitCount::from),
);
let result = forge_details.forge_alias();
assert_eq!(result, &forge_alias);
}
#[test]
fn should_return_forge_type() {
let forge_type = ForgeType::MockForge;
let hostname = Hostname::new(given::a_name());
let user = User::new(given::a_name());
let token = ApiToken::new(given::a_name().into());
let forge_name = ForgeAlias::new(given::a_name());
let forge_details = ForgeDetails::new(
forge_name,
forge_type,
hostname,
user,
token,
given::maybe_a_number().map(CommitCount::from),
);
let result = forge_details.forge_type();
assert_eq!(result, forge_type);
}
#[test]
fn should_return_hostname() {
let forge_type = ForgeType::MockForge;
let hostname = Hostname::new(given::a_name());
let user = User::new(given::a_name());
let token = ApiToken::new(given::a_name().into());
let forge_name = ForgeAlias::new(given::a_name());
let forge_details = ForgeDetails::new(
forge_name,
forge_type,
hostname.clone(),
user,
token,
given::maybe_a_number().map(CommitCount::from),
);
let result = forge_details.hostname();
assert_eq!(result, &hostname);
}
#[test]
fn should_return_user() {
let forge_type = ForgeType::MockForge;
let hostname = Hostname::new(given::a_name());
let user = User::new(given::a_name());
let token = ApiToken::new(given::a_name().into());
let forge_name = ForgeAlias::new(given::a_name());
let forge_details = ForgeDetails::new(
forge_name,
forge_type,
hostname,
user.clone(),
token,
given::maybe_a_number().map(CommitCount::from),
);
let result = forge_details.user();
assert_eq!(result, &user);
}
#[test]
fn should_return_token() {
let forge_type = ForgeType::MockForge;
let hostname = Hostname::new(given::a_name());
let user = User::new(given::a_name());
let token = ApiToken::new(given::a_name().into());
let forge_name = ForgeAlias::new(given::a_name());
let forge_details = ForgeDetails::new(
forge_name,
forge_type,
hostname,
user,
token.clone(),
given::maybe_a_number().map(CommitCount::from),
);
let result = forge_details.token();
assert_eq!(result.expose_secret(), token.expose_secret());
}
#[test]
fn with_hostname_should_return_new_instance() {
let forge_type = ForgeType::MockForge;
let hostname = Hostname::new(given::a_name());
let other_hostname = Hostname::new(given::a_name());
let user = User::new(given::a_name());
let token = ApiToken::new(given::a_name().into());
let forge_name = ForgeAlias::new(given::a_name());
let forge_details = ForgeDetails::new(
forge_name,
forge_type,
hostname,
user,
token,
given::maybe_a_number().map(CommitCount::from),
);
let result = forge_details.with_hostname(other_hostname.clone());
assert_eq!(result.hostname(), &other_hostname);
}
#[test]
fn should_convert_from_name_and_config() {
let forge_type = ForgeType::MockForge;
let hostname_value = given::a_name();
let hostname = Hostname::new(hostname_value.clone());
let user_value = given::a_name();
let user = User::new(user_value.clone());
let token_value = given::a_name();
let token = ApiToken::new(token_value.clone().into());
let max_dev_commits = given::maybe_a_number();
let forge_alias = ForgeAlias::new(given::a_name());
let forge_config = ForgeConfig::new(
forge_type,
hostname_value,
user_value,
token_value,
max_dev_commits,
BTreeMap::new(),
);
let forge_details = ForgeDetails::from((&forge_alias, &forge_config));
assert_eq!(forge_details.forge_alias(), &forge_alias);
assert_eq!(forge_details.hostname(), &hostname);
assert_eq!(forge_details.user(), &user);
assert_eq!(forge_details.token().expose_secret(), token.expose_secret());
assert_eq!(
forge_details
.max_dev_commits()
.map(|commit_count| commit_count.clone().peel()),
max_dev_commits
);
}
}
mod forge_name {
use super::*;
use std::path::PathBuf;
#[test]
fn should_convert_to_pathbuf() {
let name = given::a_name();
let forge_alias = ForgeAlias::new(name.clone());
let pathbuf: PathBuf = (&forge_alias).into();
assert_eq!(pathbuf, PathBuf::new().join(name));
}
}
mod forge_type {
use super::*;
#[test]
fn should_display_as_string() {
assert_eq!(s!(ForgeType::MockForge), "MockForge");
}
}
mod gitdir {
use super::*;
use std::path::PathBuf;
#[test]
fn should_return_pathbuf() {
let pathbuf = PathBuf::default().join(given::a_name());
let gitdir = GitDir::new(pathbuf.clone(), git_dir::StoragePathType::Internal);
let result = gitdir.to_path_buf();
assert_eq!(result, pathbuf);
}
#[test]
fn should_display() {
let pathbuf = PathBuf::default().join("foo");
let gitdir = GitDir::new(pathbuf, git_dir::StoragePathType::Internal);
let result = s!(gitdir);
assert_eq!(result, "foo");
}
#[test]
fn should_convert_to_pathbuf_from_ref() {
let path = given::a_name();
let pathbuf = PathBuf::default().join(path);
let gitdir: GitDir = GitDir::new(pathbuf.clone(), git_dir::StoragePathType::Internal);
let result: PathBuf = (&gitdir).into();
assert_eq!(result, pathbuf);
}
}
mod repo_branches {
use super::*;
#[test]
fn should_return_main() {
let main = given::a_name();
let next = given::a_name();
let dev = given::a_name();
let repo_branches = RepoBranches::new(main.clone(), next, dev);
assert_eq!(repo_branches.main(), BranchName::new(main));
}
#[test]
fn should_return_next() {
let main = given::a_name();
let next = given::a_name();
let dev = given::a_name();
let repo_branches = RepoBranches::new(main, next.clone(), dev);
assert_eq!(repo_branches.next(), BranchName::new(next));
}
#[test]
fn should_return_dev() {
let main = given::a_name();
let next = given::a_name();
let dev = given::a_name();
let repo_branches = RepoBranches::new(main, next, dev.clone());
assert_eq!(repo_branches.dev(), BranchName::new(dev));
}
}
mod server {
use super::*;
mod load {
use super::*;
#[test]
fn load_should_parse_app_config() -> TestResult {
let app_config = given::an_app_config();
let fs = kxio::fs::temp()?;
let_assert!(Ok(()) = write_app_config(&app_config, &fs), "write");
let_assert!(Ok(config) = AppConfig::load(&fs), "load");
assert_eq!(config, app_config, "compare");
Ok(())
}
fn write_app_config(app_config: &AppConfig, fs: &kxio::fs::FileSystem) -> TestResult {
let http = &app_config.listen_socket_addr()?;
let http_addr = http.ip();
let http_port = app_config.listen_socket_addr()?.port();
let listen_url = app_config.listen().url();
let storage_path = app_config.storage().path();
let shout = app_config.shout();
let shout_webhook_url = shout.webhook_url().unwrap_or_default();
let shout_webhook_secret = shout
.webhook_secret()
.map(|secret| s!(secret.expose_secret()))
.unwrap_or_default();
let_assert!(Some(shout_email) = shout.email());
let shout_email_from = shout_email.from();
let shout_email_to = shout_email.to();
let_assert!(Some(shout_email_smtp) = shout_email.smtp());
let shout_email_smtp_hostname = shout_email_smtp.hostname();
let shout_email_smtp_username = shout_email_smtp.username();
let shout_email_smtp_password = shout_email_smtp.password();
let forge_alias = app_config
.forges()
.next()
.map(|(fa, _)| fa)
.ok_or("forge missing")?;
let forge_default = app_config
.forge
.get(forge_alias.as_ref())
.ok_or("forge missing")?;
let forge_type = forge_default.forge_type();
let forge_hostname = forge_default.hostname();
let forge_user = forge_default.user();
let forge_token = s!(forge_default.token().expose_secret());
let optional_max_dev_commits = forge_default
.max_dev_commits()
.map(CommitCount::peel)
.map_or_else(String::new, |max_dev_commits| {
format!("max_dev_commits = {max_dev_commits}")
});
let mut repos: Vec<String> = vec![];
for (repo_alias, server_repo_config) in forge_default.repos() {
let repo_path = server_repo_config.repo();
let branch = server_repo_config.branch();
let gitdir = server_repo_config
.gitdir()
.map(|gitdir| gitdir.to_path_buf())
.map(|pathbuf| s!(pathbuf.display()))
.map(|gitdir| format!(r#", gitdir = "{gitdir}" "#))
.unwrap_or_default();
let repo_server_config = server_repo_config
.repo_config()
.map(|src| {
let branches = src.branches();
let main = branches.main();
let next = branches.next();
let dev = branches.dev();
format!(r#", main = "{main}", next = "{next}", dev = "{dev}""#)
})
.unwrap_or_default();
repos.push(format!(r#"{repo_alias} = {{ repo = "{repo_path}", branch = "{branch}"{gitdir}{repo_server_config} }}"#));
}
let repos = repos.join("\n");
let file_contents = &format!(
r#"
[listen]
http = {{ addr = "{http_addr}", port = {http_port} }}
url = "{listen_url}"
[shout]
webhook = {{ url = "{shout_webhook_url}", secret = "{shout_webhook_secret}" }}
desktop = true
[shout.email]
from = "{shout_email_from}"
to = "{shout_email_to}"
[shout.email.smtp]
hostname = "{shout_email_smtp_hostname}"
username = "{shout_email_smtp_username}"
password = "{shout_email_smtp_password}"
[storage]
path = {storage_path:?}
[forge.{forge_alias}]
forge_type = "{forge_type}"
hostname = "{forge_hostname}"
user = "{forge_user}"
token = "{forge_token}"
{optional_max_dev_commits}
[forge.{forge_alias}.repos]
{repos}
"#
);
println!("{file_contents}");
fs.file(&fs.base().join("git-next-server.toml"))
.write(file_contents.as_str())?;
Ok(())
}
}
}
mod registered_webhook {
use super::*;
#[test]
fn should_return_id() {
let id = given::a_webhook_id();
let auth = WebhookAuth::generate();
let rw = RegisteredWebhook::new(id.clone(), auth);
assert_eq!(rw.id(), &id);
}
#[test]
fn should_return_auth() {
let id = given::a_webhook_id();
let auth = WebhookAuth::generate();
let rw = RegisteredWebhook::new(id, auth.clone());
assert_eq!(rw.auth(), &auth);
}
}
mod webhook {
use super::*;
mod message {
use super::*;
#[test]
fn should_return_forge_alias() {
let forge_alias = given::a_forge_alias();
let message = ForgeNotification::new(
forge_alias.clone(),
given::a_repo_alias(),
BTreeMap::default(),
given::a_webhook_message_body(),
);
assert_eq!(message.forge_alias(), &forge_alias);
}
#[test]
fn should_return_repo_alias() {
let repo_alias = given::a_repo_alias();
let message = ForgeNotification::new(
given::a_forge_alias(),
repo_alias.clone(),
BTreeMap::default(),
given::a_webhook_message_body(),
);
assert_eq!(message.repo_alias(), &repo_alias);
}
#[test]
fn should_return_body() {
let body = given::a_webhook_message_body();
let message = ForgeNotification::new(
given::a_forge_alias(),
given::a_repo_alias(),
BTreeMap::default(),
body.clone(),
);
assert_eq!(message.body().as_bytes(), body.as_bytes());
}
#[test]
fn should_return_header() {
let key = given::a_name();
let value = given::a_name();
let mut headers = BTreeMap::default();
headers.insert(key.clone(), value.clone());
let message = ForgeNotification::new(
given::a_forge_alias(),
given::a_repo_alias(),
headers,
given::a_webhook_message_body(),
);
assert_eq!(message.header(&key), Some(value.as_str()));
}
}
}
mod push {
use super::*;
#[test]
fn should_return_main_branch() {
let repo_branches = given::some_repo_branches();
let sha = given::a_name();
let message = given::a_name();
let push_event = {
let branch_name = repo_branches.main();
crate::webhook::Push::new(branch_name, sha, message)
};
assert_eq!(push_event.branch(&repo_branches), Some(Branch::Main));
}
#[test]
fn should_return_next_branch() {
let repo_branches = given::some_repo_branches();
let sha = given::a_name();
let message = given::a_name();
let push_event = {
let branch_name = repo_branches.next();
crate::webhook::Push::new(branch_name, sha, message)
};
assert_eq!(push_event.branch(&repo_branches), Some(Branch::Next));
}
#[test]
fn should_return_dev_branch() {
let repo_branches = given::some_repo_branches();
let sha = given::a_name();
let message = given::a_name();
let push_event = {
let branch_name = repo_branches.dev();
crate::webhook::Push::new(branch_name, sha, message)
};
assert_eq!(push_event.branch(&repo_branches), Some(Branch::Dev));
}
#[test]
fn should_not_return_other_branches() {
let repo_branches = given::some_repo_branches();
let sha = given::a_name();
let message = given::a_name();
let push_event = {
let branch_name = BranchName::new(given::a_name());
crate::webhook::Push::new(branch_name, sha, message)
};
assert_eq!(push_event.branch(&repo_branches), None);
}
#[test]
fn should_return_sha() {
let repo_branches = given::some_repo_branches();
let sha = given::a_name();
let message = given::a_name();
let push_event = {
let branch_name = repo_branches.main();
let sha = sha.clone();
crate::webhook::Push::new(branch_name, sha, message)
};
assert_eq!(push_event.sha(), sha);
}
#[test]
fn should_return_message() {
let repo_branches = given::some_repo_branches();
let sha = given::a_name();
let message = given::a_name();
let push_event = {
let branch_name = repo_branches.main();
let message = message.clone();
crate::webhook::Push::new(branch_name, sha, message)
};
assert_eq!(push_event.message(), message);
}
}
mod given {
use crate::server::{EmailConfig, Listen, ListenUrl, OutboundWebhook, Shout, SmtpConfig};
use super::*;
use rand::Rng as _;
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
pub fn a_name() -> String {
use rand::Rng;
use std::iter;
fn generate(len: usize) -> String {
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let mut rng = rand::thread_rng();
let one_char = || CHARSET[rng.gen_range(0..CHARSET.len())] as char;
iter::repeat_with(one_char).take(len).collect()
}
generate(5)
}
pub fn a_number() -> u32 {
use rand::Rng;
let mut rng = rand::thread_rng();
rng.gen_range(0..100)
}
pub fn maybe_a_number() -> Option<u32> {
use rand::Rng;
let mut rng = rand::thread_rng();
if Rng::gen_ratio(&mut rng, 1, 2) {
Some(a_number())
} else {
None
}
}
pub fn an_app_config() -> AppConfig {
AppConfig::new(
a_listen(),
a_shout(),
a_server_storage(),
some_forge_configs(),
)
}
pub fn an_http() -> Http {
let address = format!(
"{}.{}.{}.{}",
an_octet(),
an_octet(),
an_octet(),
an_octet()
);
Http::new(address, a_port())
}
pub fn an_octet() -> u8 {
rand::thread_rng().gen()
}
pub fn a_port() -> u16 {
rand::thread_rng().gen()
}
pub fn a_listen() -> Listen {
Listen::new(an_http(), a_listen_url())
}
pub fn a_listen_url() -> ListenUrl {
ListenUrl::new(a_name())
}
pub fn an_outbound_webhook() -> OutboundWebhook {
OutboundWebhook::new(a_name(), a_name())
}
pub fn an_email_config() -> EmailConfig {
EmailConfig::new(
a_name(),
a_name(),
Some(SmtpConfig::new(a_name(), a_name(), a_name())),
)
}
pub fn a_server_storage() -> Storage {
Storage::new(a_name().into())
}
pub fn a_shout() -> Shout {
Shout::new(
Some(an_outbound_webhook()),
Some(an_email_config()),
Some(true),
)
}
pub fn some_forge_configs() -> BTreeMap<String, ForgeConfig> {
[(a_name(), a_forge_config())].into()
}
pub fn a_forge_config() -> ForgeConfig {
ForgeConfig::new(
ForgeType::MockForge,
a_name(), a_name(), a_name(), maybe_a_number(),
some_server_repo_configs(),
)
}
pub fn some_server_repo_configs() -> BTreeMap<String, ServerRepoConfig> {
[(a_name(), a_server_repo_config())].into()
}
pub fn a_pathbuf() -> PathBuf {
let name = a_name();
let path = Path::new(name.as_str());
path.to_path_buf()
}
pub fn a_server_repo_config() -> ServerRepoConfig {
ServerRepoConfig::new(
a_name(), a_name(), Some(a_pathbuf()), Some(a_name()), Some(a_name()), Some(a_name()), )
}
pub fn a_webhook_id() -> WebhookId {
WebhookId::new(a_name())
}
pub fn a_forge_alias() -> ForgeAlias {
ForgeAlias::new(a_name())
}
pub fn a_repo_alias() -> RepoAlias {
RepoAlias::new(a_name())
}
pub fn a_webhook_message_body() -> crate::webhook::forge_notification::Body {
crate::webhook::forge_notification::Body::new(a_name())
}
pub fn some_repo_branches() -> RepoBranches {
RepoBranches::new(
a_name(), a_name(), a_name(), )
}
}