use serde_json::json;
use wiremock::matchers::{body_json, method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
use super::crud::client;
use crate::api::{NotificationListOptions, TaskListOptions};
use crate::{entities, types};
fn list() -> ResponseTemplate {
ResponseTemplate::new(200).set_body_json(json!([]))
}
fn envelope() -> ResponseTemplate {
ResponseTemplate::new(200).set_body_json(json!({ "success": true }))
}
#[tokio::test]
async fn appointments_get_sends_range_and_repeated_resources() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/appointment"))
.and(query_param("startDate", "2026-01-01T00:00:00Z"))
.and(query_param("endDate", "2026-01-31T00:00:00Z"))
.respond_with(list())
.expect(1)
.mount(&server)
.await;
client(&server)
.await
.appointments
.get(
"2026-01-01T00:00:00Z",
"2026-01-31T00:00:00Z",
&["R1", "R2"],
)
.await
.unwrap();
let reqs = server.received_requests().await.unwrap();
let q = reqs[0].url.query().unwrap_or("").to_string();
assert!(
q.contains("resources=R1") && q.contains("resources=R2"),
"{q}"
);
}
#[tokio::test]
async fn get_by_id_escapes_path() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/appointment/APT%201%2F2"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({ "id": 1 })))
.expect(1)
.mount(&server)
.await;
client(&server)
.await
.appointments
.get_by_id("APT 1/2")
.await
.unwrap();
let raw = server.received_requests().await.unwrap()[0]
.url
.path()
.to_string();
assert_eq!(raw, "/appointment/APT%201%2F2");
}
#[tokio::test]
async fn jobs_get_by_job_no_and_keeps_crud() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/job/JOB-001"))
.respond_with(
ResponseTemplate::new(200).set_body_json(json!({ "id": 7, "jobNo": "JOB-001" })),
)
.expect(1)
.mount(&server)
.await;
let c = client(&server).await;
let dto = c.jobs.get("JOB-001").await.unwrap();
assert_eq!(dto.job_no.as_deref(), Some("JOB-001"));
assert_eq!(c.jobs.route(), "/job");
}
#[tokio::test]
async fn tasks_list_forwards_only_set_filters() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/task"))
.respond_with(list())
.mount(&server)
.await;
let c = client(&server).await;
c.tasks
.list(&TaskListOptions {
is_open: Some(true),
page: Some(2),
page_size: Some(50),
})
.await
.unwrap();
c.tasks.list(&TaskListOptions::default()).await.unwrap();
let reqs = server.received_requests().await.unwrap();
let q0 = reqs[0].url.query().unwrap();
assert!(
q0.contains("isOpen=true") && q0.contains("page=2") && q0.contains("pageSize=50"),
"{q0}"
);
assert_eq!(reqs[1].url.query(), None);
}
#[tokio::test]
async fn notifications_get_sends_page_limit_and_optional_filters() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/notification"))
.and(query_param("page", "1"))
.and(query_param("limit", "20"))
.and(query_param("sort", "date desc"))
.respond_with(list())
.expect(1)
.mount(&server)
.await;
client(&server)
.await
.notifications
.get(
1,
20,
&NotificationListOptions {
sort: Some("date desc".into()),
..Default::default()
},
)
.await
.unwrap();
}
#[tokio::test]
async fn date_range_accessors_pass_dates_through() {
for (route, call) in [
("/resourceCapacity", 0),
("/timeEntry", 1),
("/timeSession", 2),
] {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path(route))
.and(query_param("start", "2026-03-01"))
.and(query_param("end", "2026-03-31"))
.respond_with(list())
.expect(1)
.mount(&server)
.await;
let c = client(&server).await;
match call {
0 => {
c.resource_capacities
.get("2026-03-01", "2026-03-31")
.await
.unwrap();
}
1 => {
c.time_entries
.get("2026-03-01", "2026-03-31")
.await
.unwrap();
}
_ => {
c.time_sessions
.get("2026-03-01", "2026-03-31")
.await
.unwrap();
}
}
}
}
#[tokio::test]
async fn read_only_accessors_hit_their_routes() {
let server = MockServer::start().await;
for r in [
"/calendar",
"/resourcetype",
"/appointmentDependency",
"/appointmentField",
] {
Mock::given(method("GET"))
.and(path(r))
.respond_with(list())
.expect(1)
.mount(&server)
.await;
}
let c = client(&server).await;
c.calendars.get_all().await.unwrap();
c.resource_types.get_all().await.unwrap();
c.appointment_dependencies.get_all().await.unwrap();
c.appointment_fields.get_all().await.unwrap();
}
#[tokio::test]
async fn task_budget_entries_create_and_delete() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/taskBudgetEntry"))
.respond_with(envelope())
.expect(1)
.mount(&server)
.await;
Mock::given(method("DELETE"))
.and(path("/taskBudgetEntry"))
.respond_with(envelope())
.expect(1)
.mount(&server)
.await;
let c = client(&server).await;
let e = entities::TaskBudgetEntry {
job_no: "JOB-001".into(),
task_no: "T-1".into(),
delta_in_seconds: 3600,
..Default::default()
};
c.task_budget_entries.create(&e).await.unwrap();
c.task_budget_entries.delete_many(&[e]).await.unwrap();
let body = server.received_requests().await.unwrap()[1].body.clone();
assert!(body.starts_with(b"["), "batch delete must send an array");
}
#[tokio::test]
async fn task_assignments_create_and_delete_single_request_with_empty_200() {
let server = MockServer::start().await;
let req = types::TaskAssignmentRequest {
job_no: Some("JOB-001".into()),
task_no: Some("T-1".into()),
resource_no: Some("RES-1".into()),
..Default::default()
};
Mock::given(method("POST"))
.and(path("/taskAssignment"))
.and(body_json(
json!({ "jobNo": "JOB-001", "taskNo": "T-1", "resourceNo": "RES-1" }),
))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
Mock::given(method("DELETE"))
.and(path("/taskAssignment"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let c = client(&server).await;
c.task_assignments.create(&req).await.unwrap();
c.task_assignments.delete(&req).await.unwrap();
}