use chrono::Utc;
use git_bot_feedback::{
FileAnnotation, OutputVariable, RestApiClient, RestApiRateLimitHeaders, RestClientError,
ReviewOptions, ThreadCommentOptions,
client::{LocalClient, init_client},
};
use mockito::{Matcher, Server};
use reqwest::{
Client, Method, StatusCode,
header::{HeaderMap, HeaderName, HeaderValue},
};
use url::Url;
mod common;
use common::logger_init;
#[derive(Default)]
struct RateLimitTestParams {
secondary: bool,
has_remaining_count: bool,
bad_remaining_count: bool,
has_reset_timestamp: bool,
bad_reset_timestamp: bool,
has_retry_interval: bool,
bad_retry_interval: bool,
}
async fn simulate_rate_limit(test_params: &RateLimitTestParams) {
let rate_limit_headers = RestApiRateLimitHeaders {
reset: "reset".to_string(),
remaining: "remaining".to_string(),
retry: "retry".to_string(),
};
logger_init();
log::set_max_level(log::LevelFilter::Debug);
let mut server = Server::new_async().await;
let client = Client::new();
let reset_timestamp = (Utc::now().timestamp() + 60).to_string();
let mut mock = server
.mock("GET", "/")
.match_body(Matcher::Any)
.expect_at_least(1)
.expect_at_most(5)
.with_status(429);
if test_params.has_remaining_count {
mock = mock.with_header(
&rate_limit_headers.remaining,
if test_params.secondary {
"1"
} else if test_params.bad_remaining_count {
"X"
} else {
"0"
},
);
}
if test_params.has_reset_timestamp {
mock = mock.with_header(
&rate_limit_headers.reset,
if test_params.bad_reset_timestamp {
"X"
} else {
&reset_timestamp
},
);
}
if test_params.secondary && test_params.has_retry_interval {
mock.with_header(
&rate_limit_headers.retry,
if test_params.bad_retry_interval {
"X"
} else {
"0"
},
)
.create();
} else {
mock.create();
}
unsafe {
std::env::set_var("GITHUB_ACTIONS", "false");
}
let test_client = init_client().unwrap();
assert!(!test_client.is_debug_enabled());
assert!(test_client.event_name().is_none());
let request = test_client
.make_api_request(
&client,
Url::parse(&server.url()).unwrap(),
Method::GET,
None,
None,
)
.unwrap();
let result = test_client
.send_api_request(&client, request, &rate_limit_headers)
.await;
let err = match result {
Ok(response) => {
let result = response.error_for_status();
result.map_err(RestClientError::Request).unwrap_err()
}
Err(e) => e,
};
if let RestClientError::Request(e) = err {
assert!(matches!(e.status(), Some(StatusCode::TOO_MANY_REQUESTS)));
} else if test_params.bad_retry_interval
|| test_params.bad_remaining_count
|| test_params.bad_reset_timestamp
{
assert!(matches!(err, RestClientError::HeaderParseInt(_)));
} else if test_params.has_reset_timestamp {
assert!(matches!(err, RestClientError::RateLimitPrimary(_)));
} else if test_params.secondary {
assert!(matches!(err, RestClientError::RateLimitSecondary));
} else {
assert!(matches!(err, RestClientError::RateLimitNoReset));
}
}
#[tokio::test]
async fn rate_limit_secondary() {
simulate_rate_limit(&RateLimitTestParams {
secondary: true,
has_retry_interval: true,
..Default::default()
})
.await;
}
#[tokio::test]
async fn rate_limit_bad_retry() {
simulate_rate_limit(&RateLimitTestParams {
secondary: true,
has_retry_interval: true,
bad_retry_interval: true,
..Default::default()
})
.await;
}
#[tokio::test]
async fn rate_limit_primary() {
simulate_rate_limit(&RateLimitTestParams {
has_remaining_count: true,
has_reset_timestamp: true,
..Default::default()
})
.await;
}
#[tokio::test]
async fn rate_limit_no_reset() {
simulate_rate_limit(&RateLimitTestParams {
has_remaining_count: true,
..Default::default()
})
.await;
}
#[tokio::test]
async fn rate_limit_bad_reset() {
simulate_rate_limit(&RateLimitTestParams {
has_remaining_count: true,
has_reset_timestamp: true,
bad_reset_timestamp: true,
..Default::default()
})
.await;
}
#[tokio::test]
async fn rate_limit_bad_count() {
simulate_rate_limit(&RateLimitTestParams {
has_remaining_count: true,
bad_remaining_count: true,
..Default::default()
})
.await;
}
#[tokio::test]
async fn dummy_coverage() {
let mut test_client = LocalClient;
let log_group_name = "Dummy test";
test_client.start_log_group(log_group_name);
test_client
.post_thread_comment(ThreadCommentOptions {
comment: "some comment text".to_string(),
..Default::default()
})
.await
.unwrap();
test_client.append_step_summary("").unwrap();
test_client
.write_output_variables(&[OutputVariable {
name: "key".to_string(),
value: "value".to_string(),
}])
.unwrap();
assert!(!test_client.is_pr_event());
test_client.set_user_agent("user_agent").unwrap();
let mut options = ReviewOptions::default();
test_client.cull_pr_reviews(&mut options).await.unwrap();
test_client.post_pr_review(&options).await.unwrap();
let annotation = FileAnnotation::default();
test_client.write_file_annotations(&[annotation]).unwrap();
test_client.end_log_group(log_group_name);
}
#[test]
fn bad_link_header() {
let mut headers = HeaderMap::with_capacity(1);
assert!(
headers
.insert("link", HeaderValue::from_str("; rel=\"next\"").unwrap())
.is_none()
);
logger_init();
log::set_max_level(log::LevelFilter::Debug);
let test_client = LocalClient;
let result = test_client.try_next_page(&headers);
assert!(result.is_none());
}
#[test]
fn bad_link_domain() {
let mut headers = HeaderMap::with_capacity(1);
assert!(
headers
.insert(
"link",
HeaderValue::from_str("<not a domain>; rel=\"next\"").unwrap()
)
.is_none()
);
logger_init();
log::set_max_level(log::LevelFilter::Debug);
let test_client = LocalClient;
let result = test_client.try_next_page(&headers);
assert!(result.is_none());
}
#[test]
fn mk_request() {
let client = Client::new();
let url = "https://127.0.0.1";
let method = Method::GET;
let data = "text".to_string();
let header_value = HeaderValue::from_str("value").unwrap();
let headers = Some(HeaderMap::from_iter([(
HeaderName::from_static("key"),
header_value.clone(),
)]));
let test_client = LocalClient;
let request = test_client
.make_api_request(
&client,
Url::parse(url).unwrap(),
method,
Some(data.clone()),
headers.clone(),
)
.unwrap();
assert_eq!(request.body().unwrap().as_bytes(), Some(data.as_bytes()));
assert!(
request
.headers()
.get("key")
.is_some_and(|v| *v == header_value)
);
}
#[tokio::test]
#[cfg(feature = "file-changes")]
async fn list_file_changes() {
use std::{env, fs::OpenOptions, io::Write, process::Command};
use common::logger_init;
use git_bot_feedback::{FileFilter, LinesChangedOnly};
use tempfile::TempDir;
let tmp_dir = TempDir::new().unwrap();
Command::new("git")
.current_dir(tmp_dir.path())
.args([
"clone",
"--depth=2", "--branch=v0.1.4", "https://github.com/2bndy5/git-bot-feedback.git",
])
.output()
.unwrap();
env::set_current_dir(tmp_dir.path().join("git-bot-feedback")).unwrap();
logger_init();
log::set_max_level(log::LevelFilter::Debug);
let client = LocalClient;
let file_filter = FileFilter::new(&[], &["toml", "md"], None);
let changes = client
.get_list_of_changed_files(&file_filter, &LinesChangedOnly::On, None, false)
.await
.unwrap();
assert_eq!(changes.len(), 2);
let expected_changed_file = String::from("Cargo.toml");
assert!(changes.contains_key(&expected_changed_file));
assert_eq!(
changes.get(&expected_changed_file).unwrap().added_lines,
vec![4] );
let mut cargo_toml = OpenOptions::new()
.append(true)
.open(&expected_changed_file)
.unwrap();
cargo_toml.write_all(b"# Dummy change").unwrap();
cargo_toml.sync_all().unwrap();
Command::new("git")
.args(["add", &expected_changed_file])
.output()
.unwrap();
let changes = client
.get_list_of_changed_files(&file_filter, &LinesChangedOnly::On, None, false)
.await
.unwrap();
assert_eq!(changes.len(), 1);
assert!(changes.contains_key(&expected_changed_file));
let added_lines = &changes.get(&expected_changed_file).unwrap().added_lines;
assert_eq!(added_lines.len(), 1);
assert_ne!(*added_lines.first().unwrap(), 4);
assert!(matches!(
client
.get_list_of_changed_files(
&file_filter,
&LinesChangedOnly::Diff,
Some("1.0".to_string()),
true
)
.await
.unwrap_err(),
RestClientError::GitCommand(_)
));
let changes = client
.get_list_of_changed_files(
&file_filter,
&LinesChangedOnly::On,
Some(1.to_string()),
true,
)
.await
.unwrap();
assert!(changes.contains_key(&expected_changed_file));
assert_eq!(changes.len(), 2);
let valid_ref = {
let git_out = Command::new("git")
.args(["rev-parse", "HEAD~1"])
.output()
.unwrap()
.stdout;
String::from_utf8_lossy(&git_out).trim().to_string()
};
let changes = client
.get_list_of_changed_files(&file_filter, &LinesChangedOnly::On, Some(valid_ref), true)
.await
.unwrap();
assert!(changes.contains_key(&expected_changed_file));
assert_eq!(changes.len(), 2);
}