git-next/crates/forge-forgejo/src/tests.rs
Paul Campbell df352443b7
All checks were successful
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Rust / build (push) Successful in 1m21s
feat: GitDir tracks when repo is cloned by git-next
2024-07-06 15:08:13 +01:00

797 lines
29 KiB
Rust

//
use git_next_config as config;
use git_next_git as git;
mod forgejo {
use super::*;
use assert2::let_assert;
use crate::ForgeJo;
use git::ForgeLike as _;
#[test]
fn should_return_name() {
let fs = given::a_filesystem();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), given::a_network());
assert_eq!(forge.name(), "forgejo");
}
mod is_message_authorised {
use super::*;
#[test]
fn should_return_true_with_valid_header() {
let fs = given::a_filesystem();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), given::a_network());
let auth = given::a_webhook_auth();
let message = given::a_webhook_message(given::Header::Valid(auth.clone()));
assert!(forge.is_message_authorised(&message, &auth));
}
#[test]
fn should_return_false_with_missing_header() {
let fs = given::a_filesystem();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), given::a_network());
let auth = given::a_webhook_auth();
let message = given::a_webhook_message(given::Header::Missing);
assert!(!forge.is_message_authorised(&message, &auth));
}
#[test]
fn should_return_false_with_non_basic_prefix() {
let fs = given::a_filesystem();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), given::a_network());
let auth = given::a_webhook_auth();
let message = given::a_webhook_message(given::Header::NonBasic);
assert!(!forge.is_message_authorised(&message, &auth));
}
#[test]
fn should_return_false_with_non_ulid_value() {
let fs = given::a_filesystem();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), given::a_network());
let auth = given::a_webhook_auth();
let message = given::a_webhook_message(given::Header::NonUlid);
assert!(!forge.is_message_authorised(&message, &auth));
}
#[test]
fn should_return_false_with_wrong_ulid_value() {
let fs = given::a_filesystem();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), given::a_network());
let auth = given::a_webhook_auth();
let message = given::a_webhook_message(given::Header::WrongUlid);
assert!(!forge.is_message_authorised(&message, &auth));
}
}
mod parse_webhook_body {
use serde_json::json;
use super::*;
#[test]
fn should_parse_valid_body() {
let fs = given::a_filesystem();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), given::a_network());
let repo_branches = given::repo_branches();
let next = repo_branches.next();
let sha = given::a_name();
let message = given::a_name();
let body = config::webhook::forge_notification::Body::new(
json!({"ref":format!("refs/heads/{next}"),"after":sha,"head_commit":{"message":message}})
.to_string(),
);
let_assert!(Ok(push) = forge.parse_webhook_body(&body));
assert_eq!(push.sha(), sha);
assert_eq!(push.message(), message);
assert_eq!(
push.branch(&repo_branches),
Some(config::webhook::push::Branch::Next)
);
}
#[test]
fn should_error_invalid_body() {
let fs = given::a_filesystem();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), given::a_network());
let body =
config::webhook::forge_notification::Body::new(r#"{"type":"invalid"}"#.to_string());
let_assert!(Err(_) = forge.parse_webhook_body(&body));
}
}
mod commit_status {
use git_next_git::forge::commit::Status;
use kxio::network::StatusCode;
use super::*;
#[tokio::test]
async fn should_return_pass_for_success() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let commit = given::a_commit();
let mut net = given::a_network();
given::a_commit_state("success", &mut net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, Status::Pass);
}
#[tokio::test]
async fn should_return_pending_for_pending() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let commit = given::a_commit();
let mut net = given::a_network();
given::a_commit_state("pending", &mut net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, Status::Pending);
}
#[tokio::test]
async fn should_return_fail_for_failure() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let commit = given::a_commit();
let mut net = given::a_network();
given::a_commit_state("failure", &mut net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, Status::Fail);
}
#[tokio::test]
async fn should_return_fail_for_error() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let commit = given::a_commit();
let mut net = given::a_network();
given::a_commit_state("error", &mut net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, Status::Fail);
}
#[tokio::test]
async fn should_return_pending_for_blank() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let commit = given::a_commit();
let mut net = given::a_network();
given::a_commit_state("", &mut net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, Status::Pending);
}
#[tokio::test]
async fn should_return_pending_for_no_statuses() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let commit = given::a_commit();
let mut net = given::a_network();
net.add_get_response(
&given::a_commit_status_url(&repo_details, &commit),
StatusCode::OK,
"",
);
let forge = given::a_forgejo_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, Status::Pending);
}
#[tokio::test]
async fn should_return_pending_for_network_error() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let commit = given::a_commit();
let mut net = given::a_network();
net.add_get_error(
&given::a_commit_status_url(&repo_details, &commit),
"boom today",
);
let forge = given::a_forgejo_forge(&repo_details, net);
assert_eq!(forge.commit_status(&commit).await, Status::Pending);
}
}
mod list_webhooks {
use git_next_config::WebhookId;
use git_next_git::ForgeLike as _;
use crate::tests::forgejo::with;
use super::*;
#[tokio::test]
async fn should_return_a_list_of_matching_webhooks() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let webhook_url = given::any_webhook_url();
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
use secrecy::ExposeSecret;
let token = repo_details.forge.token().expose_secret();
let hook_id_1 = given::a_forgejo_webhook_id();
let hook_id_2 = given::a_forgejo_webhook_id();
let hook_id_3 = given::a_forgejo_webhook_id();
let mut net = given::a_network();
let mut args = with::WebhookArgs {
net: &mut net,
hostname,
repo_path,
token,
};
// page 1 with three items
with::get_webhooks_by_page(
1,
&[
with::ReturnedWebhook::new(hook_id_1, &webhook_url),
with::ReturnedWebhook::new(hook_id_2, &webhook_url),
with::ReturnedWebhook::new(hook_id_3, &given::any_webhook_url()),
],
&mut args,
);
// page 2 with no items - stops pagination
with::get_webhooks_by_page(2, &[], &mut args);
let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Ok(result) = forge.list_webhooks(&webhook_url).await);
assert_eq!(
result,
vec![
WebhookId::new(format!("{hook_id_1}")),
WebhookId::new(format!("{hook_id_2}"))
]
);
}
#[tokio::test]
async fn should_return_any_network_error() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let webhook_url = given::a_webhook_url(&given::a_forge_alias(), &given::a_repo_alias());
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
use secrecy::ExposeSecret;
let token = repo_details.forge.token().expose_secret();
let mut net = given::a_network();
net.add_get_error(
format!("https://{hostname}/api/v1/repos/{repo_path}/hooks?page=1&token={token}")
.as_str(),
"error_message",
);
let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Err(_) = forge.list_webhooks(&webhook_url).await);
}
}
mod unregister_webhook {
use super::*;
use git_next_git::ForgeLike;
use kxio::network::StatusCode;
#[tokio::test]
async fn should_delete_webhook() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
use secrecy::ExposeSecret;
let token = repo_details.forge.token().expose_secret();
let webhook_id = given::a_webhook_id();
let mut net = given::a_network();
net.add_delete_response(
format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks/{webhook_id}?token={token}"
)
.as_str(),
StatusCode::OK,
"",
);
let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Ok(_) = forge.unregister_webhook(&webhook_id).await);
}
#[tokio::test]
async fn should_return_error_on_network_error() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
use secrecy::ExposeSecret;
let token = repo_details.forge.token().expose_secret();
let webhook_id = given::a_webhook_id();
let mut net = given::a_network();
net.add_delete_error(
format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks/{webhook_id}?token={token}"
)
.as_str(),
"error",
);
let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Err(err) = forge.unregister_webhook(&webhook_id).await);
assert!(
matches!(err, git::forge::webhook::Error::FailedToUnregister(_)),
"{err:?}"
);
}
}
mod register_webhook {
use crate::tests::forgejo::with;
use super::*;
use git_next_config::WebhookId;
use kxio::network::StatusCode;
use serde_json::json;
#[tokio::test]
async fn should_register_a_new_webhook() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
assert!(
repo_details.repo_config.is_some(),
"repo_details needs to have repo_config for this test"
);
let webhook_url =
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
use secrecy::ExposeSecret;
let token = repo_details.forge.token().expose_secret();
let mut net = given::a_network();
let mut args = with::WebhookArgs {
net: &mut net,
hostname,
repo_path,
token,
};
// there are no existing matching webhooks
with::get_webhooks_by_page(1, &[], &mut args);
// register the webhook will succeed
let webhook_id = given::a_forgejo_webhook_id();
net.add_post_response(
format!("https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}").as_str(),
StatusCode::OK,
json!({"id": webhook_id, "config":{}}).to_string().as_str(),
);
let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Ok(registered_webhook) = forge.register_webhook(&webhook_url).await);
assert_eq!(
registered_webhook.id(),
&WebhookId::new(format!("{webhook_id}"))
);
}
#[tokio::test]
async fn should_abort_if_repo_config_missing() {
let fs = given::a_filesystem();
let mut repo_details = given::repo_details(&fs);
repo_details.repo_config.take();
assert!(
repo_details.repo_config.is_none(),
"repo_details needs to NOT have repo_config for this test"
);
let webhook_url =
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let net = given::a_network();
let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Err(err) = forge.register_webhook(&webhook_url).await);
assert!(
matches!(err, git::forge::webhook::Error::NoRepoConfig),
"{err:?}"
);
}
#[tokio::test]
async fn should_unregister_existing_webhooks_before_registering() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
assert!(
repo_details.repo_config.is_some(),
"repo_details needs to have repo_config for this test"
);
let webhook_url =
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
use secrecy::ExposeSecret;
let token = repo_details.forge.token().expose_secret();
let mut net = given::a_network();
let mut args = with::WebhookArgs {
net: &mut net,
hostname,
repo_path,
token,
};
let hook1 = with::ReturnedWebhook::new(given::a_forgejo_webhook_id(), &webhook_url);
let hook2 = with::ReturnedWebhook::new(given::a_forgejo_webhook_id(), &webhook_url);
let hook3 = with::ReturnedWebhook::new(
given::a_forgejo_webhook_id(),
&given::any_webhook_url(),
);
let hooks = [hook1, hook2, hook3];
// there are three existing webhooks, two are matching webhooks
with::get_webhooks_by_page(1, &hooks, &mut args);
with::get_webhooks_by_page(2, &[], &mut args);
// should unregister 1 and 2, but not 3
with::unregister_webhook(&hooks[0], &mut args);
with::unregister_webhook(&hooks[1], &mut args);
// register the webhook will succeed
let webhook_id = given::a_forgejo_webhook_id();
net.add_post_response(
format!("https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}").as_str(),
StatusCode::OK,
json!({"id": webhook_id, "config":{}}).to_string().as_str(),
);
let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Ok(registered_webhook) = forge.register_webhook(&webhook_url).await);
assert_eq!(
registered_webhook.id(),
&WebhookId::new(format!("{webhook_id}"))
);
}
#[tokio::test]
async fn should_return_error_if_empty_network_response() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
assert!(
repo_details.repo_config.is_some(),
"repo_details needs to have repo_config for this test"
);
let webhook_url =
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
use secrecy::ExposeSecret;
let token = repo_details.forge.token().expose_secret();
let mut net = given::a_network();
let mut args = with::WebhookArgs {
net: &mut net,
hostname,
repo_path,
token,
};
// there are no existing matching webhooks
with::get_webhooks_by_page(1, &[], &mut args);
// register the webhook will return empty response
net.add_post_response(
format!("https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}").as_str(),
StatusCode::OK,
json!({}).to_string().as_str(), // empty response
);
let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Err(err) = forge.register_webhook(&webhook_url).await);
assert!(
matches!(err, git::forge::webhook::Error::FailedToRegister(_)),
"{err:?}"
);
}
#[tokio::test]
async fn should_return_error_on_network_error() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
assert!(
repo_details.repo_config.is_some(),
"repo_details needs to have repo_config for this test"
);
let webhook_url =
given::a_webhook_url(repo_details.forge.forge_alias(), &repo_details.repo_alias);
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
use secrecy::ExposeSecret;
let token = repo_details.forge.token().expose_secret();
let mut net = given::a_network();
let mut args = with::WebhookArgs {
net: &mut net,
hostname,
repo_path,
token,
};
// there are no existing matching webhooks
with::get_webhooks_by_page(1, &[], &mut args);
// register the webhook will return empty response
net.add_post_error(
format!("https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}").as_str(),
"error",
);
let forge = given::a_forgejo_forge(&repo_details, net);
let_assert!(Err(err) = forge.register_webhook(&webhook_url).await);
assert!(
matches!(err, git::forge::webhook::Error::FailedToRegister(_)),
"{err:?}"
);
}
}
mod with {
use serde::Serialize;
use serde_json::json;
use git_next_config::{server::WebhookUrl, Hostname, RepoPath};
use kxio::network::{self, StatusCode};
pub fn get_webhooks_by_page(
page: u8,
response: &[ReturnedWebhook],
args: &mut WebhookArgs,
) {
let hostname = args.hostname;
let repo_path = args.repo_path;
let token = args.token;
args.net.add_get_response(
format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks?page={page}&token={token}"
)
.as_str(),
StatusCode::OK,
json!(response).to_string().as_str(),
);
}
pub fn unregister_webhook(hook1: &ReturnedWebhook, args: &mut WebhookArgs) {
let webhook_id = hook1.id;
let hostname = args.hostname;
let repo_path = args.repo_path;
let token = args.token;
args.net.add_delete_response(
format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks/{webhook_id}?token={token}"
)
.as_str(),
StatusCode::OK,
"",
);
}
pub struct WebhookArgs<'a> {
pub net: &'a mut network::MockNetwork,
pub hostname: &'a Hostname,
pub repo_path: &'a RepoPath,
pub token: &'a str,
}
#[derive(Debug, Serialize)]
pub struct ReturnedWebhook {
pub id: i64,
pub config: Config,
}
impl ReturnedWebhook {
pub fn new(id: i64, url: &WebhookUrl) -> Self {
Self {
id,
config: Config { url: url.clone() },
}
}
}
#[derive(Debug, Serialize)]
pub struct Config {
pub url: WebhookUrl,
}
}
mod given {
use std::collections::BTreeMap;
use kxio::network::{MockNetwork, StatusCode};
use serde_json::json;
use super::*;
pub fn a_commit_state(
state: impl AsRef<str>,
net: &mut MockNetwork,
repo_details: &git::RepoDetails,
commit: &git::Commit,
) {
let response = json!({"state":state.as_ref()});
net.add_get_response(
a_commit_status_url(repo_details, commit).as_str(),
StatusCode::OK,
response.to_string().as_str(),
)
}
pub fn a_commit_status_url(
repo_details: &git::RepoDetails,
commit: &git::Commit,
) -> String {
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
use secrecy::ExposeSecret;
let token = repo_details.forge.token().expose_secret();
format!(
"https://{hostname}/api/v1/repos/{repo_path}/commits/{commit}/status?token={token}"
)
}
pub enum Header {
Valid(config::WebhookAuth),
Missing,
NonBasic,
NonUlid,
WrongUlid,
}
pub fn a_webhook_message(header: Header) -> config::ForgeNotification {
config::ForgeNotification::new(
given::a_forge_alias(),
given::a_repo_alias(),
given::webhook_headers(header),
given::a_webhook_message_body(),
)
}
pub fn webhook_headers(header: Header) -> BTreeMap<String, String> {
let mut headers = BTreeMap::new();
match header {
Header::Valid(auth) => {
headers.insert("authorization".to_string(), format!("Basic {auth}"));
}
Header::Missing => { /* don't add any header */ }
Header::NonBasic => {
headers.insert("authorization".to_string(), "Non-Basic".to_string());
}
Header::NonUlid => {
headers.insert("authorization".to_string(), "Basic 123456".to_string());
}
Header::WrongUlid => {
headers.insert(
"authorization".to_string(),
format!("Basic {}", config::WebhookAuth::generate()),
);
}
}
headers
}
pub fn a_forgejo_forge(
repo_details: &git::RepoDetails,
net: impl Into<kxio::network::Network>,
) -> ForgeJo {
ForgeJo::new(repo_details.clone(), net.into())
}
pub fn a_forgejo_webhook_id() -> i64 {
use rand::RngCore as _;
rand::thread_rng().next_u32().into()
}
pub fn a_webhook_auth() -> config::WebhookAuth {
config::WebhookAuth::generate()
}
pub fn a_webhook_message_body() -> config::webhook::forge_notification::Body {
config::webhook::forge_notification::Body::new(a_name())
}
pub fn repo_branches() -> config::RepoBranches {
config::RepoBranches::new(a_name(), a_name(), a_name())
}
pub fn a_forge_alias() -> config::ForgeAlias {
config::ForgeAlias::new(a_name())
}
pub fn a_repo_alias() -> config::RepoAlias {
config::RepoAlias::new(a_name())
}
pub fn a_network() -> kxio::network::MockNetwork {
kxio::network::MockNetwork::new()
}
pub fn a_webhook_url(
forge_alias: &config::ForgeAlias,
repo_alias: &config::RepoAlias,
) -> git_next_config::server::WebhookUrl {
config::server::Webhook::new(a_name()).url(forge_alias, repo_alias)
}
pub fn any_webhook_url() -> git_next_config::server::WebhookUrl {
a_webhook_url(&a_forge_alias(), &a_repo_alias())
}
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_webhook_id() -> config::WebhookId {
config::WebhookId::new(a_name())
}
pub fn a_branch_name() -> config::BranchName {
config::BranchName::new(a_name())
}
pub fn a_git_dir(fs: &kxio::fs::FileSystem) -> config::GitDir {
let dir_name = a_name();
let dir = fs.base().join(dir_name);
config::GitDir::new(dir, config::git_dir::StoragePathType::Internal)
}
pub fn a_forge_config() -> config::ForgeConfig {
config::ForgeConfig::new(
config::ForgeType::MockForge,
a_name(),
a_name(),
a_name(),
Default::default(), // no repos
)
}
pub fn a_server_repo_config() -> config::ServerRepoConfig {
let main = a_branch_name().unwrap();
let next = a_branch_name().unwrap();
let dev = a_branch_name().unwrap();
config::ServerRepoConfig::new(
format!("{}/{}", a_name(), a_name()),
main.clone(),
None,
Some(main),
Some(next),
Some(dev),
)
}
pub fn a_commit() -> git::Commit {
git::Commit::new(a_commit_sha(), a_commit_message())
}
pub fn a_commit_message() -> git::commit::Message {
git::commit::Message::new(a_name())
}
pub fn a_commit_sha() -> git::commit::Sha {
git::commit::Sha::new(a_name())
}
pub fn a_filesystem() -> kxio::fs::FileSystem {
kxio::fs::temp().unwrap_or_else(|e| panic!("{}", e))
}
pub fn repo_details(fs: &kxio::fs::FileSystem) -> git::RepoDetails {
let generation = git::Generation::default();
let repo_alias = a_repo_alias();
let server_repo_config = a_server_repo_config();
let forge_alias = a_forge_alias();
let forge_config = a_forge_config();
let gitdir = a_git_dir(fs);
git::RepoDetails::new(
generation,
&repo_alias,
&server_repo_config,
&forge_alias,
&forge_config,
gitdir,
)
}
}
}