mirror of
https://github.com/Xevion/banner.git
synced 2026-01-31 02:23:34 -06:00
test: add comprehensive unit tests for query builder, CLI args, and config parsing
This commit is contained in:
@@ -276,6 +276,190 @@ fn format_time_parameter(duration: Duration) -> (String, String, String) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_defaults() {
|
||||
let q = SearchQuery::new();
|
||||
assert_eq!(q.get_max_results(), 8);
|
||||
assert!(q.get_subject().is_none());
|
||||
let params = q.to_params();
|
||||
assert_eq!(params.get("pageMaxSize").unwrap(), "8");
|
||||
assert_eq!(params.get("pageOffset").unwrap(), "0");
|
||||
assert_eq!(params.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_subject_param() {
|
||||
let params = SearchQuery::new().subject("CS").to_params();
|
||||
assert_eq!(params.get("txt_subject").unwrap(), "CS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_title_trims_whitespace() {
|
||||
let params = SearchQuery::new().title(" Intro to CS ").to_params();
|
||||
assert_eq!(params.get("txt_courseTitle").unwrap(), "Intro to CS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_crn_param() {
|
||||
let params = SearchQuery::new()
|
||||
.course_reference_number("12345")
|
||||
.to_params();
|
||||
assert_eq!(params.get("txt_courseReferenceNumber").unwrap(), "12345");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keywords_joined_with_spaces() {
|
||||
let params = SearchQuery::new()
|
||||
.keyword("data")
|
||||
.keyword("science")
|
||||
.to_params();
|
||||
assert_eq!(params.get("txt_keywordlike").unwrap(), "data science");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keywords_vec() {
|
||||
let params = SearchQuery::new()
|
||||
.keywords(vec!["machine".into(), "learning".into()])
|
||||
.to_params();
|
||||
assert_eq!(params.get("txt_keywordlike").unwrap(), "machine learning");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_only() {
|
||||
let params = SearchQuery::new().open_only(true).to_params();
|
||||
assert_eq!(params.get("chk_open_only").unwrap(), "true");
|
||||
|
||||
// open_only(false) still sets the param (it's `.is_some()` check)
|
||||
let params2 = SearchQuery::new().open_only(false).to_params();
|
||||
assert_eq!(params2.get("chk_open_only").unwrap(), "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_credits_range() {
|
||||
let params = SearchQuery::new().credits(3, 6).to_params();
|
||||
assert_eq!(params.get("txt_credithourlow").unwrap(), "3");
|
||||
assert_eq!(params.get("txt_credithourhigh").unwrap(), "6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_course_number_range() {
|
||||
let params = SearchQuery::new().course_numbers(3000, 3999).to_params();
|
||||
assert_eq!(params.get("txt_course_number_range").unwrap(), "3000");
|
||||
assert_eq!(params.get("txt_course_number_range_to").unwrap(), "3999");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pagination() {
|
||||
let params = SearchQuery::new().offset(20).max_results(10).to_params();
|
||||
assert_eq!(params.get("pageOffset").unwrap(), "20");
|
||||
assert_eq!(params.get("pageMaxSize").unwrap(), "10");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_time_9am() {
|
||||
let (h, m, mer) = format_time_parameter(Duration::from_secs(9 * 3600));
|
||||
assert_eq!(h, "9");
|
||||
assert_eq!(m, "0");
|
||||
assert_eq!(mer, "AM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_time_noon() {
|
||||
let (h, m, mer) = format_time_parameter(Duration::from_secs(12 * 3600));
|
||||
assert_eq!(h, "12");
|
||||
assert_eq!(m, "0");
|
||||
assert_eq!(mer, "PM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_time_1pm() {
|
||||
let (h, m, mer) = format_time_parameter(Duration::from_secs(13 * 3600));
|
||||
assert_eq!(h, "1");
|
||||
assert_eq!(m, "0");
|
||||
assert_eq!(mer, "PM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_time_930am() {
|
||||
let (h, m, mer) = format_time_parameter(Duration::from_secs(9 * 3600 + 30 * 60));
|
||||
assert_eq!(h, "9");
|
||||
assert_eq!(m, "30");
|
||||
assert_eq!(mer, "AM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_time_midnight() {
|
||||
let (h, m, mer) = format_time_parameter(Duration::from_secs(0));
|
||||
assert_eq!(h, "0");
|
||||
assert_eq!(m, "0");
|
||||
assert_eq!(mer, "AM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_params_in_query() {
|
||||
let params = SearchQuery::new()
|
||||
.start_time(Duration::from_secs(9 * 3600))
|
||||
.end_time(Duration::from_secs(17 * 3600))
|
||||
.to_params();
|
||||
assert_eq!(params.get("select_start_hour").unwrap(), "9");
|
||||
assert_eq!(params.get("select_start_ampm").unwrap(), "AM");
|
||||
assert_eq!(params.get("select_end_hour").unwrap(), "5");
|
||||
assert_eq!(params.get("select_end_ampm").unwrap(), "PM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_value_params() {
|
||||
let params = SearchQuery::new()
|
||||
.campus(vec!["MAIN".into(), "DT".into()])
|
||||
.attributes(vec!["HONORS".into()])
|
||||
.instructor(vec![1001, 1002])
|
||||
.to_params();
|
||||
assert_eq!(params.get("txt_campus").unwrap(), "MAIN,DT");
|
||||
assert_eq!(params.get("txt_attribute").unwrap(), "HONORS");
|
||||
assert_eq!(params.get("txt_instructor").unwrap(), "1001,1002");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_minimal() {
|
||||
let display = SearchQuery::new().to_string();
|
||||
assert_eq!(display, "offset=0, maxResults=8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_with_fields() {
|
||||
let display = SearchQuery::new()
|
||||
.subject("CS")
|
||||
.open_only(true)
|
||||
.max_results(10)
|
||||
.to_string();
|
||||
assert!(display.contains("subject=CS"));
|
||||
assert!(display.contains("openOnly=true"));
|
||||
assert!(display.contains("maxResults=10"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_query_param_count() {
|
||||
let params = SearchQuery::new()
|
||||
.subject("CS")
|
||||
.title("Intro")
|
||||
.course_reference_number("12345")
|
||||
.keyword("programming")
|
||||
.open_only(true)
|
||||
.credits(3, 4)
|
||||
.course_numbers(1000, 1999)
|
||||
.offset(0)
|
||||
.max_results(25)
|
||||
.to_params();
|
||||
// subject, title, crn, keyword, open_only, min_credits, max_credits,
|
||||
// course_number_range, course_number_range_to, pageOffset, pageMaxSize = 11
|
||||
assert_eq!(params.len(), 11);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SearchQuery {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
+76
@@ -102,3 +102,79 @@ const DEFAULT_TRACING_FORMAT: TracingFormat = TracingFormat::Json;
|
||||
fn default_tracing_format() -> TracingFormat {
|
||||
DEFAULT_TRACING_FORMAT
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn args_with_services(
|
||||
services: Option<Vec<ServiceName>>,
|
||||
disable: Option<Vec<ServiceName>>,
|
||||
) -> Args {
|
||||
Args {
|
||||
tracing: TracingFormat::Pretty,
|
||||
services,
|
||||
disable_services: disable,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_enables_all_services() {
|
||||
let result = determine_enabled_services(&args_with_services(None, None)).unwrap();
|
||||
assert_eq!(result.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_explicit_services_only_those() {
|
||||
let result =
|
||||
determine_enabled_services(&args_with_services(Some(vec![ServiceName::Web]), None))
|
||||
.unwrap();
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].as_str(), "web");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disable_bot_leaves_web_and_scraper() {
|
||||
let result =
|
||||
determine_enabled_services(&args_with_services(None, Some(vec![ServiceName::Bot])))
|
||||
.unwrap();
|
||||
assert_eq!(result.len(), 2);
|
||||
assert!(result.iter().all(|s| s.as_str() != "bot"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disable_all_leaves_empty() {
|
||||
let result = determine_enabled_services(&args_with_services(
|
||||
None,
|
||||
Some(vec![
|
||||
ServiceName::Bot,
|
||||
ServiceName::Web,
|
||||
ServiceName::Scraper,
|
||||
]),
|
||||
))
|
||||
.unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_both_specified_returns_error() {
|
||||
let result = determine_enabled_services(&args_with_services(
|
||||
Some(vec![ServiceName::Web]),
|
||||
Some(vec![ServiceName::Bot]),
|
||||
));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_service_name_as_str() {
|
||||
assert_eq!(ServiceName::Bot.as_str(), "bot");
|
||||
assert_eq!(ServiceName::Web.as_str(), "web");
|
||||
assert_eq!(ServiceName::Scraper.as_str(), "scraper");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_service_name_all() {
|
||||
let all = ServiceName::all();
|
||||
assert_eq!(all.len(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,3 +209,101 @@ where
|
||||
|
||||
deserializer.deserialize_any(DurationVisitor)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DurationWrapper {
|
||||
#[serde(deserialize_with = "deserialize_duration")]
|
||||
value: Duration,
|
||||
}
|
||||
|
||||
fn parse(json: &str) -> Result<Duration, String> {
|
||||
serde_json::from_str::<DurationWrapper>(json)
|
||||
.map(|w| w.value)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_from_integer_seconds() {
|
||||
let d = parse(r#"{"value": 30}"#).unwrap();
|
||||
assert_eq!(d, Duration::from_secs(30));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_from_string_seconds() {
|
||||
let d = parse(r#"{"value": "30s"}"#).unwrap();
|
||||
assert_eq!(d, Duration::from_secs(30));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_from_string_minutes() {
|
||||
let d = parse(r#"{"value": "2m"}"#).unwrap();
|
||||
assert_eq!(d, Duration::from_secs(120));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_from_string_milliseconds() {
|
||||
let d = parse(r#"{"value": "1500ms"}"#).unwrap();
|
||||
assert_eq!(d, Duration::from_millis(1500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_from_string_with_space() {
|
||||
let d = parse(r#"{"value": "2 m"}"#).unwrap();
|
||||
assert_eq!(d, Duration::from_secs(120));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_from_string_multiple_units() {
|
||||
let d = parse(r#"{"value": "1m 30s"}"#).unwrap();
|
||||
assert_eq!(d, Duration::from_secs(90));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_from_bare_number_string() {
|
||||
let d = parse(r#"{"value": "45"}"#).unwrap();
|
||||
assert_eq!(d, Duration::from_secs(45));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_zero() {
|
||||
let d = parse(r#"{"value": 0}"#).unwrap();
|
||||
assert_eq!(d, Duration::from_secs(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_negative_rejected() {
|
||||
let err = parse(r#"{"value": -5}"#).unwrap_err();
|
||||
assert!(err.contains("negative"), "expected negative error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_invalid_string_rejected() {
|
||||
let err = parse(r#"{"value": "notaduration"}"#).unwrap_err();
|
||||
assert!(
|
||||
err.contains("Invalid duration"),
|
||||
"expected invalid format error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_config_values() {
|
||||
assert_eq!(default_port(), 8080);
|
||||
assert_eq!(default_shutdown_timeout(), Duration::from_secs(8));
|
||||
assert_eq!(default_log_level(), "info");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_rate_limiting() {
|
||||
let rl = default_rate_limiting();
|
||||
assert_eq!(rl.session_rpm, 6);
|
||||
assert_eq!(rl.search_rpm, 30);
|
||||
assert_eq!(rl.metadata_rpm, 20);
|
||||
assert_eq!(rl.reset_rpm, 10);
|
||||
assert_eq!(rl.burst_allowance, 3);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user