feat(cal): add MockCalendarBackend and wiremock CalDAV server test harness (GH-19) #48

Merged
KofTwentyTwo merged 1 commits from feature/GH-19-caldav-server-mocks into main 2026-07-23 00:50:11 +00:00
3 changed files with 136 additions and 0 deletions

No files matched your search

+5
View File
@@ -20,4 +20,9 @@ chrono.workspace = true
reqwest.workspace = true
quick-xml.workspace = true
[dev-dependencies]
tempfile.workspace = true
wiremock.workspace = true
+2
View File
@@ -1,9 +1,11 @@
//! iCalendar (RFC 5545), CalDAV (RFC 4791), and recurrence engine for Nuncio.
pub mod caldav;
pub mod mock;
pub mod parser;
pub mod rrule;
pub use caldav::CalDavClient;
pub use mock::MockCalendarBackend;
pub use parser::{CalendarError, IcalParserAdapter};
pub use rrule::RecurrenceEngine;
+129
View File
@@ -0,0 +1,129 @@
//! Deterministic mock calendar backend for offline testing and integration verification.
use nuncio_core::model::CalendarEvent;
use std::sync::{Arc, Mutex};
use crate::parser::CalendarError;
/// Thread-safe mock calendar backend for offline testing.
#[derive(Debug, Clone, Default)]
pub struct MockCalendarBackend {
events: Arc<Mutex<Vec<CalendarEvent>>>,
should_fail: Arc<Mutex<bool>>,
}
impl MockCalendarBackend {
/// Create a new `MockCalendarBackend`.
pub fn new() -> Self {
Self::default()
}
/// Configure the mock to simulate network failure errors.
pub fn set_should_fail(&self, fail: bool) {
if let Ok(mut flag) = self.should_fail.lock() {
*flag = fail;
}
}
/// Add a mock calendar event to storage.
pub fn add_event(&self, event: CalendarEvent) {
if let Ok(mut guard) = self.events.lock() {
guard.push(event);
}
}
/// Retrieve calendar events for a specific calendar collection within a time window.
pub fn list_events(
&self,
calendar_id: &str,
start_window: i64,
end_window: i64,
) -> Result<Vec<CalendarEvent>, CalendarError> {
if *self.should_fail.lock().unwrap() {
return Err(CalendarError::ParseFailed(
"simulated CalDAV network failure".to_string(),
));
}
let guard = self.events.lock().unwrap();
let matches = guard
.iter()
.filter(|e| e.calendar_id == calendar_id && e.start_time >= start_window && e.end_time <= end_window)
.cloned()
.collect();
Ok(matches)
}
}
#[cfg(test)]
mod tests {
use super::*;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[test]
fn mock_calendar_backend_operations() {
let mock = MockCalendarBackend::new();
let event = CalendarEvent {
id: "evt-m1".to_string(),
account_id: "acct-1".to_string(),
calendar_id: "cal-work".to_string(),
summary: "Mock Planning".to_string(),
start_time: 1700000000,
end_time: 1700003600,
rrule: None,
location: None,
};
mock.add_event(event.clone());
let events = mock
.list_events("cal-work", 1699999000, 1700004000)
.expect("list events succeeds");
assert_eq!(events.len(), 1);
assert_eq!(events[0].id, "evt-m1");
mock.set_should_fail(true);
assert!(mock.list_events("cal-work", 1699999000, 1700004000).is_err());
}
#[tokio::test]
async fn wiremock_caldav_report_simulation() {
let mock_server = MockServer::start().await;
let xml_response = r#"<?xml version="1.0" encoding="utf-8"?>
<d:multistatus xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:response>
<d:href>/calendars/work/evt.ics</d:href>
<d:propstat>
<d:prop>
<c:calendar-data>BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
SUMMARY:Wiremock Event
END:VEVENT
END:VCALENDAR</c:calendar-data>
</d:prop>
</d:propstat>
</d:response>
</d:multistatus>"#;
Mock::given(method("REPORT"))
.and(path("/calendars/work/"))
.respond_with(ResponseTemplate::new(207).set_body_string(xml_response))
.mount(&mock_server)
.await;
let client = reqwest::Client::new();
let resp = client
.request(
reqwest::Method::from_bytes(b"REPORT").unwrap(),
format!("{}/calendars/work/", mock_server.uri()),
)
.send()
.await
.unwrap();
assert_eq!(resp.status(), 207);
}
}