Skip to main content

mailrs_dav/
fixtures.rs

1//! In-memory [`CalendarStore`](crate::store::CalendarStore) and
2//! [`AddressBookStore`](crate::store::AddressBookStore) implementations
3//! suitable for tests, examples, and downstream-consumer test harnesses.
4//!
5//! **Intended use is testing.** Both stores keep every value in process
6//! memory and never persist across restarts; do not wire either into a real
7//! deployment.
8//!
9//! ## Quick start
10//!
11//! ```
12//! use mailrs_dav::fixtures::{InMemoryCalendarStore, EXAMPLE_USER, make_calendar};
13//!
14//! let store = InMemoryCalendarStore::new()
15//!     .with_calendar(EXAMPLE_USER, make_calendar(1, "Work"));
16//! ```
17//!
18//! ## What it gives you
19//!
20//! - Stateful in-memory storage with builder APIs — `with_calendar`,
21//!   `with_event`, `with_book`, `with_contact`.
22//! - Per-method error injection via `<method>_fails` setters so each error
23//!   path in your handler-driving code can be isolated in a single test.
24//! - Read-back helpers for assertions — `events_in(cal_id)`,
25//!   `contacts_in(book_id)`.
26//! - Convenience constructors — `make_calendar`, `make_event`, `make_book`,
27//!   `make_contact`.
28//! - Response-inspection helpers for testing handler output —
29//!   [`body_as_str`], [`header_value`].
30//!
31//! Used internally by this crate's integration tests; the same module is
32//! exposed to downstream consumers so they can drive their own handler tests
33//! without re-implementing the stores.
34
35use std::sync::RwLock;
36
37use async_trait::async_trait;
38
39use crate::store::{AddressBookStore, CalendarStore, StoreError};
40use crate::types::{AddressBook, Calendar, Contact, Event, PutResult};
41use crate::xml::etag_of;
42
43/// Convenience example user used by the constructors in this module. The
44/// store does not assume any particular value — callers can use their own.
45pub const EXAMPLE_USER: &str = "alice@example.com";
46
47// =====================================================================
48// Calendar store
49// =====================================================================
50
51/// In-memory [`CalendarStore`] backed by `Vec`s under an `RwLock`. Build
52/// via [`Self::new`] + chainable `with_*` setters.
53pub struct InMemoryCalendarStore {
54    inner: RwLock<CalInner>,
55}
56
57struct CalInner {
58    calendars: Vec<(String, Calendar)>, // (owner, Calendar)
59    events: Vec<(i64, Event)>,          // (calendar_id, Event)
60    default_created_for: Vec<String>,   // owners we auto-created a Default for
61
62    list_calendars_error: Option<String>,
63    get_calendar_error: Option<String>,
64    list_events_error: Option<String>,
65    get_event_error: Option<String>,
66    event_etag_error: Option<String>,
67    put_event_error: Option<String>,
68    delete_event_error: Option<String>,
69    ensure_default_error: Option<String>,
70}
71
72impl InMemoryCalendarStore {
73    /// Construct an empty store.
74    pub fn new() -> Self {
75        Self {
76            inner: RwLock::new(CalInner {
77                calendars: Vec::new(),
78                events: Vec::new(),
79                default_created_for: Vec::new(),
80                list_calendars_error: None,
81                get_calendar_error: None,
82                list_events_error: None,
83                get_event_error: None,
84                event_etag_error: None,
85                put_event_error: None,
86                delete_event_error: None,
87                ensure_default_error: None,
88            }),
89        }
90    }
91
92    /// Append a calendar owned by `owner`. Use [`make_calendar`] for a sane default shape.
93    pub fn with_calendar(self, owner: &str, cal: Calendar) -> Self {
94        self.inner
95            .write()
96            .unwrap()
97            .calendars
98            .push((owner.to_string(), cal));
99        self
100    }
101
102    /// Append an event to the given calendar id.
103    pub fn with_event(self, calendar_id: i64, event: Event) -> Self {
104        self.inner.write().unwrap().events.push((calendar_id, event));
105        self
106    }
107
108    /// Make [`CalendarStore::list_calendars`] return an error carrying `msg`.
109    pub fn list_calendars_fails(self, msg: &str) -> Self {
110        self.inner.write().unwrap().list_calendars_error = Some(msg.to_string());
111        self
112    }
113
114    /// Make [`CalendarStore::get_calendar`] return an error carrying `msg`.
115    pub fn get_calendar_fails(self, msg: &str) -> Self {
116        self.inner.write().unwrap().get_calendar_error = Some(msg.to_string());
117        self
118    }
119
120    /// Make [`CalendarStore::list_events`] return an error carrying `msg`.
121    pub fn list_events_fails(self, msg: &str) -> Self {
122        self.inner.write().unwrap().list_events_error = Some(msg.to_string());
123        self
124    }
125
126    /// Make [`CalendarStore::get_event`] return an error carrying `msg`.
127    pub fn get_event_fails(self, msg: &str) -> Self {
128        self.inner.write().unwrap().get_event_error = Some(msg.to_string());
129        self
130    }
131
132    /// Make [`CalendarStore::event_etag`] return an error carrying `msg`.
133    pub fn event_etag_fails(self, msg: &str) -> Self {
134        self.inner.write().unwrap().event_etag_error = Some(msg.to_string());
135        self
136    }
137
138    /// Make [`CalendarStore::put_event`] return an error carrying `msg`.
139    pub fn put_event_fails(self, msg: &str) -> Self {
140        self.inner.write().unwrap().put_event_error = Some(msg.to_string());
141        self
142    }
143
144    /// Make [`CalendarStore::delete_event`] return an error carrying `msg`.
145    pub fn delete_event_fails(self, msg: &str) -> Self {
146        self.inner.write().unwrap().delete_event_error = Some(msg.to_string());
147        self
148    }
149
150    /// Make the `ensure_default_*` trait method return an error carrying `msg`.
151    pub fn ensure_default_fails(self, msg: &str) -> Self {
152        self.inner.write().unwrap().ensure_default_error = Some(msg.to_string());
153        self
154    }
155
156    /// Read back every event currently stored for `calendar_id`. Tests use this to assert handler-driven mutations.
157    pub fn events_in(&self, calendar_id: i64) -> Vec<Event> {
158        self.inner
159            .read()
160            .unwrap()
161            .events
162            .iter()
163            .filter(|(c, _)| *c == calendar_id)
164            .map(|(_, e)| e.clone())
165            .collect()
166    }
167
168    /// `true` when [`CalendarStore::ensure_default_calendar`] fired for `user`.
169    pub fn default_calendar_was_created_for(&self, user: &str) -> bool {
170        self.inner
171            .read()
172            .unwrap()
173            .default_created_for
174            .iter()
175            .any(|u| u == user)
176    }
177}
178
179impl Default for InMemoryCalendarStore {
180    fn default() -> Self {
181        Self::new()
182    }
183}
184
185#[async_trait]
186impl CalendarStore for InMemoryCalendarStore {
187    async fn list_calendars(&self, user: &str) -> Result<Vec<Calendar>, StoreError> {
188        let inner = self.inner.read().unwrap();
189        if let Some(ref msg) = inner.list_calendars_error {
190            return Err(msg.clone().into());
191        }
192        Ok(inner
193            .calendars
194            .iter()
195            .filter(|(o, _)| o == user)
196            .map(|(_, c)| c.clone())
197            .collect())
198    }
199
200    async fn get_calendar(
201        &self,
202        user: &str,
203        calendar_name: &str,
204    ) -> Result<Option<Calendar>, StoreError> {
205        let inner = self.inner.read().unwrap();
206        if let Some(ref msg) = inner.get_calendar_error {
207            return Err(msg.clone().into());
208        }
209        Ok(inner
210            .calendars
211            .iter()
212            .find(|(o, c)| o == user && c.name == calendar_name)
213            .map(|(_, c)| c.clone()))
214    }
215
216    async fn list_events(&self, calendar_id: i64) -> Result<Vec<Event>, StoreError> {
217        let inner = self.inner.read().unwrap();
218        if let Some(ref msg) = inner.list_events_error {
219            return Err(msg.clone().into());
220        }
221        Ok(inner
222            .events
223            .iter()
224            .filter(|(c, _)| *c == calendar_id)
225            .map(|(_, e)| e.clone())
226            .collect())
227    }
228
229    async fn get_event(
230        &self,
231        calendar_id: i64,
232        uid: &str,
233    ) -> Result<Option<Event>, StoreError> {
234        let inner = self.inner.read().unwrap();
235        if let Some(ref msg) = inner.get_event_error {
236            return Err(msg.clone().into());
237        }
238        Ok(inner
239            .events
240            .iter()
241            .find(|(c, e)| *c == calendar_id && e.uid == uid)
242            .map(|(_, e)| e.clone()))
243    }
244
245    async fn event_etag(
246        &self,
247        calendar_id: i64,
248        uid: &str,
249    ) -> Result<Option<String>, StoreError> {
250        let inner = self.inner.read().unwrap();
251        if let Some(ref msg) = inner.event_etag_error {
252            return Err(msg.clone().into());
253        }
254        Ok(inner
255            .events
256            .iter()
257            .find(|(c, e)| *c == calendar_id && e.uid == uid)
258            .map(|(_, e)| e.etag.clone()))
259    }
260
261    async fn put_event(
262        &self,
263        calendar_id: i64,
264        uid: &str,
265        icalendar: &str,
266        etag: &str,
267    ) -> Result<PutResult, StoreError> {
268        let mut inner = self.inner.write().unwrap();
269        if let Some(ref msg) = inner.put_event_error {
270            return Err(msg.clone().into());
271        }
272        let pos = inner
273            .events
274            .iter()
275            .position(|(c, e)| *c == calendar_id && e.uid == uid);
276        let created = pos.is_none();
277        if let Some(p) = pos {
278            inner.events[p].1.icalendar = icalendar.to_string();
279            inner.events[p].1.etag = etag.to_string();
280        } else {
281            inner.events.push((
282                calendar_id,
283                Event {
284                    uid: uid.to_string(),
285                    etag: etag.to_string(),
286                    icalendar: icalendar.to_string(),
287                    summary: String::new(),
288                    dtstart: None,
289                    dtend: None,
290                },
291            ));
292        }
293        Ok(PutResult {
294            created,
295            etag: etag.to_string(),
296        })
297    }
298
299    async fn delete_event(
300        &self,
301        calendar_id: i64,
302        uid: &str,
303    ) -> Result<bool, StoreError> {
304        let mut inner = self.inner.write().unwrap();
305        if let Some(ref msg) = inner.delete_event_error {
306            return Err(msg.clone().into());
307        }
308        let before = inner.events.len();
309        inner
310            .events
311            .retain(|(c, e)| !(*c == calendar_id && e.uid == uid));
312        Ok(inner.events.len() < before)
313    }
314
315    async fn ensure_default_calendar(&self, user: &str) -> Result<(), StoreError> {
316        let mut inner = self.inner.write().unwrap();
317        if let Some(ref msg) = inner.ensure_default_error {
318            return Err(msg.clone().into());
319        }
320        let has_any = inner.calendars.iter().any(|(o, _)| o == user);
321        if !has_any {
322            let next_id = (inner.calendars.len() as i64) + 1;
323            inner.calendars.push((
324                user.to_string(),
325                Calendar {
326                    id: next_id,
327                    name: "Default".to_string(),
328                    color: String::new(),
329                    description: String::new(),
330                },
331            ));
332            inner.default_created_for.push(user.to_string());
333        }
334        Ok(())
335    }
336}
337
338// =====================================================================
339// AddressBook store
340// =====================================================================
341
342/// In-memory [`AddressBookStore`] backed by `Vec`s under an `RwLock`. Build
343/// via [`Self::new`] + chainable `with_*` setters.
344pub struct InMemoryAddressBookStore {
345    inner: RwLock<AbInner>,
346}
347
348struct AbInner {
349    books: Vec<(String, AddressBook)>,
350    contacts: Vec<(i64, Contact)>,
351    default_created_for: Vec<String>,
352
353    list_books_error: Option<String>,
354    get_book_error: Option<String>,
355    list_contacts_error: Option<String>,
356    get_contact_error: Option<String>,
357    contact_etag_error: Option<String>,
358    put_contact_error: Option<String>,
359    delete_contact_error: Option<String>,
360    ensure_default_error: Option<String>,
361}
362
363impl InMemoryAddressBookStore {
364    /// Construct an empty store.
365    pub fn new() -> Self {
366        Self {
367            inner: RwLock::new(AbInner {
368                books: Vec::new(),
369                contacts: Vec::new(),
370                default_created_for: Vec::new(),
371                list_books_error: None,
372                get_book_error: None,
373                list_contacts_error: None,
374                get_contact_error: None,
375                contact_etag_error: None,
376                put_contact_error: None,
377                delete_contact_error: None,
378                ensure_default_error: None,
379            }),
380        }
381    }
382
383    /// Append an address book owned by `owner`. Use [`make_book`] for a sane default shape.
384    pub fn with_book(self, owner: &str, book: AddressBook) -> Self {
385        self.inner
386            .write()
387            .unwrap()
388            .books
389            .push((owner.to_string(), book));
390        self
391    }
392
393    /// Append a contact to the given book id.
394    pub fn with_contact(self, book_id: i64, contact: Contact) -> Self {
395        self.inner.write().unwrap().contacts.push((book_id, contact));
396        self
397    }
398
399    /// Make [`AddressBookStore::list_address_books`] return an error carrying `msg`.
400    pub fn list_books_fails(self, msg: &str) -> Self {
401        self.inner.write().unwrap().list_books_error = Some(msg.to_string());
402        self
403    }
404
405    /// Make [`AddressBookStore::get_address_book`] return an error carrying `msg`.
406    pub fn get_book_fails(self, msg: &str) -> Self {
407        self.inner.write().unwrap().get_book_error = Some(msg.to_string());
408        self
409    }
410
411    /// Make [`AddressBookStore::list_contacts`] return an error carrying `msg`.
412    pub fn list_contacts_fails(self, msg: &str) -> Self {
413        self.inner.write().unwrap().list_contacts_error = Some(msg.to_string());
414        self
415    }
416
417    /// Make [`AddressBookStore::get_contact`] return an error carrying `msg`.
418    pub fn get_contact_fails(self, msg: &str) -> Self {
419        self.inner.write().unwrap().get_contact_error = Some(msg.to_string());
420        self
421    }
422
423    /// Make [`AddressBookStore::contact_etag`] return an error carrying `msg`.
424    pub fn contact_etag_fails(self, msg: &str) -> Self {
425        self.inner.write().unwrap().contact_etag_error = Some(msg.to_string());
426        self
427    }
428
429    /// Make [`AddressBookStore::put_contact`] return an error carrying `msg`.
430    pub fn put_contact_fails(self, msg: &str) -> Self {
431        self.inner.write().unwrap().put_contact_error = Some(msg.to_string());
432        self
433    }
434
435    /// Make [`AddressBookStore::delete_contact`] return an error carrying `msg`.
436    pub fn delete_contact_fails(self, msg: &str) -> Self {
437        self.inner.write().unwrap().delete_contact_error = Some(msg.to_string());
438        self
439    }
440
441    /// Make the `ensure_default_*` trait method return an error carrying `msg`.
442    pub fn ensure_default_fails(self, msg: &str) -> Self {
443        self.inner.write().unwrap().ensure_default_error = Some(msg.to_string());
444        self
445    }
446
447    /// Read back every contact currently stored for `book_id`.
448    pub fn contacts_in(&self, book_id: i64) -> Vec<Contact> {
449        self.inner
450            .read()
451            .unwrap()
452            .contacts
453            .iter()
454            .filter(|(b, _)| *b == book_id)
455            .map(|(_, c)| c.clone())
456            .collect()
457    }
458}
459
460impl Default for InMemoryAddressBookStore {
461    fn default() -> Self {
462        Self::new()
463    }
464}
465
466#[async_trait]
467impl AddressBookStore for InMemoryAddressBookStore {
468    async fn list_address_books(&self, user: &str) -> Result<Vec<AddressBook>, StoreError> {
469        let inner = self.inner.read().unwrap();
470        if let Some(ref msg) = inner.list_books_error {
471            return Err(msg.clone().into());
472        }
473        Ok(inner
474            .books
475            .iter()
476            .filter(|(o, _)| o == user)
477            .map(|(_, b)| b.clone())
478            .collect())
479    }
480
481    async fn get_address_book(
482        &self,
483        user: &str,
484        book_name: &str,
485    ) -> Result<Option<AddressBook>, StoreError> {
486        let inner = self.inner.read().unwrap();
487        if let Some(ref msg) = inner.get_book_error {
488            return Err(msg.clone().into());
489        }
490        Ok(inner
491            .books
492            .iter()
493            .find(|(o, b)| o == user && b.name == book_name)
494            .map(|(_, b)| b.clone()))
495    }
496
497    async fn list_contacts(&self, book_id: i64) -> Result<Vec<Contact>, StoreError> {
498        let inner = self.inner.read().unwrap();
499        if let Some(ref msg) = inner.list_contacts_error {
500            return Err(msg.clone().into());
501        }
502        Ok(inner
503            .contacts
504            .iter()
505            .filter(|(b, _)| *b == book_id)
506            .map(|(_, c)| c.clone())
507            .collect())
508    }
509
510    async fn get_contact(
511        &self,
512        book_id: i64,
513        uid: &str,
514    ) -> Result<Option<Contact>, StoreError> {
515        let inner = self.inner.read().unwrap();
516        if let Some(ref msg) = inner.get_contact_error {
517            return Err(msg.clone().into());
518        }
519        Ok(inner
520            .contacts
521            .iter()
522            .find(|(b, c)| *b == book_id && c.uid == uid)
523            .map(|(_, c)| c.clone()))
524    }
525
526    async fn contact_etag(
527        &self,
528        book_id: i64,
529        uid: &str,
530    ) -> Result<Option<String>, StoreError> {
531        let inner = self.inner.read().unwrap();
532        if let Some(ref msg) = inner.contact_etag_error {
533            return Err(msg.clone().into());
534        }
535        Ok(inner
536            .contacts
537            .iter()
538            .find(|(b, c)| *b == book_id && c.uid == uid)
539            .map(|(_, c)| c.etag.clone()))
540    }
541
542    async fn put_contact(
543        &self,
544        book_id: i64,
545        uid: &str,
546        vcard: &str,
547        etag: &str,
548    ) -> Result<PutResult, StoreError> {
549        let mut inner = self.inner.write().unwrap();
550        if let Some(ref msg) = inner.put_contact_error {
551            return Err(msg.clone().into());
552        }
553        let pos = inner
554            .contacts
555            .iter()
556            .position(|(b, c)| *b == book_id && c.uid == uid);
557        let created = pos.is_none();
558        if let Some(p) = pos {
559            inner.contacts[p].1.vcard = vcard.to_string();
560            inner.contacts[p].1.etag = etag.to_string();
561        } else {
562            inner.contacts.push((
563                book_id,
564                Contact {
565                    uid: uid.to_string(),
566                    etag: etag.to_string(),
567                    vcard: vcard.to_string(),
568                    fn_name: String::new(),
569                    email: String::new(),
570                },
571            ));
572        }
573        Ok(PutResult {
574            created,
575            etag: etag.to_string(),
576        })
577    }
578
579    async fn delete_contact(
580        &self,
581        book_id: i64,
582        uid: &str,
583    ) -> Result<bool, StoreError> {
584        let mut inner = self.inner.write().unwrap();
585        if let Some(ref msg) = inner.delete_contact_error {
586            return Err(msg.clone().into());
587        }
588        let before = inner.contacts.len();
589        inner
590            .contacts
591            .retain(|(b, c)| !(*b == book_id && c.uid == uid));
592        Ok(inner.contacts.len() < before)
593    }
594
595    async fn ensure_default_address_book(&self, user: &str) -> Result<(), StoreError> {
596        let mut inner = self.inner.write().unwrap();
597        if let Some(ref msg) = inner.ensure_default_error {
598            return Err(msg.clone().into());
599        }
600        let has = inner.books.iter().any(|(o, _)| o == user);
601        if !has {
602            let next_id = (inner.books.len() as i64) + 1;
603            inner.books.push((
604                user.to_string(),
605                AddressBook {
606                    id: next_id,
607                    name: "Default".to_string(),
608                    description: String::new(),
609                },
610            ));
611            inner.default_created_for.push(user.to_string());
612        }
613        Ok(())
614    }
615}
616
617// =====================================================================
618// Convenience constructors
619// =====================================================================
620
621/// Build a [`Calendar`] with sane defaults — given id and name, default color and description.
622pub fn make_calendar(id: i64, name: &str) -> Calendar {
623    Calendar {
624        id,
625        name: name.to_string(),
626        color: "#abcdef".to_string(),
627        description: format!("calendar {name}"),
628    }
629}
630
631/// Build an [`Event`] from a uid and raw iCalendar text, with etag computed via [`etag_of`].
632pub fn make_event(uid: &str, body: &str) -> Event {
633    Event {
634        uid: uid.to_string(),
635        etag: etag_of(body),
636        icalendar: body.to_string(),
637        summary: String::new(),
638        dtstart: None,
639        dtend: None,
640    }
641}
642
643/// Build an [`AddressBook`] with sane defaults.
644pub fn make_book(id: i64, name: &str) -> AddressBook {
645    AddressBook {
646        id,
647        name: name.to_string(),
648        description: format!("address book {name}"),
649    }
650}
651
652/// Build a [`Contact`] from a uid and raw vCard text, with etag computed via [`etag_of`].
653pub fn make_contact(uid: &str, vcard: &str) -> Contact {
654    Contact {
655        uid: uid.to_string(),
656        etag: etag_of(vcard),
657        vcard: vcard.to_string(),
658        fn_name: String::new(),
659        email: String::new(),
660    }
661}
662
663/// Read the response body as a UTF-8 string. Convenience for substring
664/// assertions on multistatus payloads.
665pub fn body_as_str(body: Vec<u8>) -> String {
666    String::from_utf8(body).expect("dav body is utf-8")
667}
668
669/// Find a header value (case-insensitive name match). Returns `None` when
670/// absent.
671pub fn header_value<'a>(
672    headers: &'a [(String, String)],
673    name: &str,
674) -> Option<&'a str> {
675    headers
676        .iter()
677        .find(|(k, _)| k.eq_ignore_ascii_case(name))
678        .map(|(_, v)| v.as_str())
679}