1use std::sync::Mutex;
30
31use axum::body::{Body, Bytes};
32use axum::http::header::{CONTENT_TYPE, COOKIE, LOCATION, SET_COOKIE};
33use axum::http::{HeaderMap, HeaderName, HeaderValue, Method, Request, StatusCode};
34use http_body_util::BodyExt;
35use serde::Serialize;
36use serde::de::DeserializeOwned;
37use tower::ServiceExt;
38
39use crate::auth::{User, login};
40use crate::db::{Db, DbValue, ToDbValue, quote};
41use crate::mail::{Mail, Mailer};
42use crate::{App, AppState, Config, Environment, Kernel};
43
44pub struct TestApp {
46 kernel: Kernel,
47 cookie: Mutex<Option<String>>,
48 offset: std::sync::atomic::AtomicI64,
50 _storage: tempfile::TempDir,
51}
52
53impl TestApp {
54 pub async fn new(app: App) -> Self {
56 Self::with_config(app, |_| {}).await
57 }
58
59 pub async fn with_config(app: App, configure: impl FnOnce(&mut Config)) -> Self {
61 let storage = tempfile::tempdir().expect("a temporary storage directory");
62 let mut config = Config {
63 env: Environment::Testing,
64 key: Some(crate::generate_key()),
65 storage_path: storage.path().to_path_buf(),
66 ..Config::default()
67 };
68 configure(&mut config);
69 let kernel = app.config(config).boot().await.expect("the app boots");
70 kernel.migrate().await.expect("migrations run");
71 Self {
72 kernel,
73 cookie: Mutex::new(None),
74 offset: std::sync::atomic::AtomicI64::new(0),
75 _storage: storage,
76 }
77 }
78
79 pub fn kernel(&self) -> &Kernel {
81 &self.kernel
82 }
83
84 pub fn state(&self) -> &AppState {
86 self.kernel.state()
87 }
88
89 pub fn db(&self) -> &Db {
91 self.kernel.db()
92 }
93
94 pub fn mailer(&self) -> &Mailer {
96 self.kernel.mailer()
97 }
98
99 pub fn fake_http(&self) -> crate::http::FakeHttp {
102 self.state().http.fake()
103 }
104
105 pub fn sent_mail(&self) -> Vec<Mail> {
107 self.kernel.mailer().sent()
108 }
109
110 pub async fn run_jobs(&self) -> usize {
112 self.at_travelled_time(self.kernel.run_jobs())
113 .await
114 .expect("the queue runs")
115 }
116
117 pub async fn run_scheduled(&self, name: &str) -> crate::Result {
129 self.at_travelled_time(self.kernel.run_scheduled(name))
130 .await
131 }
132
133 pub fn travel(&self, by: std::time::Duration) -> &Self {
149 let seconds = i64::try_from(by.as_secs()).unwrap_or(i64::MAX);
150 self.offset
151 .fetch_add(seconds, std::sync::atomic::Ordering::SeqCst);
152 self
153 }
154
155 pub fn travel_back(&self) -> &Self {
157 self.offset.store(0, std::sync::atomic::Ordering::SeqCst);
158 self
159 }
160
161 pub async fn at_travelled_time<F: std::future::Future>(&self, fut: F) -> F::Output {
164 let offset = self.offset.load(std::sync::atomic::Ordering::SeqCst);
165 crate::clock::with_offset(offset, fut).await
166 }
167
168 pub fn fake_events(&self) -> &Self {
171 let mut events = self
172 .state()
173 .fakes
174 .events
175 .lock()
176 .unwrap_or_else(|e| e.into_inner());
177 events.get_or_insert_with(Vec::new);
178 self
179 }
180
181 pub fn emitted<E: crate::events::Event>(&self) -> Vec<E> {
183 let events = self
184 .state()
185 .fakes
186 .events
187 .lock()
188 .unwrap_or_else(|e| e.into_inner());
189 events
190 .iter()
191 .flatten()
192 .filter_map(|event| event.downcast_ref::<E>().cloned())
193 .collect()
194 }
195
196 #[track_caller]
198 pub fn assert_emitted<E: crate::events::Event>(&self, check: impl Fn(&E) -> bool) -> &Self {
199 let emitted = self.emitted::<E>();
200 assert!(
201 emitted.iter().any(check),
202 "no matching {} was emitted ({} of that type)",
203 std::any::type_name::<E>(),
204 emitted.len()
205 );
206 self
207 }
208
209 #[track_caller]
211 pub fn assert_not_emitted<E: crate::events::Event>(&self) -> &Self {
212 let emitted = self.emitted::<E>();
213 assert!(
214 emitted.is_empty(),
215 "{} {} emitted",
216 emitted.len(),
217 std::any::type_name::<E>()
218 );
219 self
220 }
221
222 pub fn fake_notifications(&self) -> &Self {
225 let mut sent = self
226 .state()
227 .fakes
228 .notifications
229 .lock()
230 .unwrap_or_else(|e| e.into_inner());
231 sent.get_or_insert_with(Vec::new);
232 self
233 }
234
235 pub fn notifications(&self) -> Vec<crate::SentNotification> {
237 let sent = self
238 .state()
239 .fakes
240 .notifications
241 .lock()
242 .unwrap_or_else(|e| e.into_inner());
243 sent.iter().flatten().cloned().collect()
244 }
245
246 #[track_caller]
248 pub fn assert_notified(&self, user: &User, kind: &str) -> &Self {
249 let sent = self.notifications();
250 assert!(
251 sent.iter()
252 .any(|n| n.kind == kind && n.to.user().is_some_and(|u| u.id == user.id)),
253 "user {} got no `{kind}` notification; sent: {:?}",
254 user.id,
255 sent.iter().map(|n| n.kind).collect::<Vec<_>>()
256 );
257 self
258 }
259
260 #[track_caller]
263 pub fn assert_notified_to(&self, address: &str, kind: &str) -> &Self {
264 let sent = self.notifications();
265 assert!(
266 sent.iter()
267 .any(|n| n.kind == kind && n.to.has_address(address)),
268 "`{address}` got no `{kind}` notification"
269 );
270 self
271 }
272
273 pub fn fake_broadcasts(&self) -> &Self {
276 let mut sent = self
277 .state()
278 .fakes
279 .broadcasts
280 .lock()
281 .unwrap_or_else(|e| e.into_inner());
282 sent.get_or_insert_with(Vec::new);
283 self
284 }
285
286 pub fn broadcasts(&self) -> Vec<crate::SentBroadcast> {
288 let sent = self
289 .state()
290 .fakes
291 .broadcasts
292 .lock()
293 .unwrap_or_else(|e| e.into_inner());
294 sent.iter().flatten().cloned().collect()
295 }
296
297 #[track_caller]
300 pub fn assert_broadcast(
301 &self,
302 event: &str,
303 check: impl Fn(&crate::SentBroadcast) -> bool,
304 ) -> &Self {
305 let sent = self.broadcasts();
306 assert!(
307 sent.iter().any(|b| b.event == event && check(b)),
308 "no matching `{event}` was broadcast; sent: {:?}",
309 sent.iter()
310 .map(|b| (b.event.as_str(), b.data.to_string()))
311 .collect::<Vec<_>>()
312 );
313 self
314 }
315
316 #[track_caller]
318 pub fn assert_nothing_notified(&self) -> &Self {
319 let sent = self.notifications();
320 assert!(sent.is_empty(), "{} notification(s) were sent", sent.len());
321 self
322 }
323
324 pub fn session_get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
326 self.session().get(key)
327 }
328
329 #[track_caller]
331 pub fn assert_session_has(&self, key: &str) -> &Self {
332 assert!(
333 self.session().get::<serde_json::Value>(key).is_some(),
334 "the session has no `{key}`"
335 );
336 self
337 }
338
339 #[track_caller]
341 pub fn assert_session_missing(&self, key: &str) -> &Self {
342 assert!(
343 self.session().get::<serde_json::Value>(key).is_none(),
344 "the session has `{key}`"
345 );
346 self
347 }
348
349 #[track_caller]
351 pub fn assert_authenticated(&self, user: Option<&User>) -> &Self {
352 let id: Option<i64> = self.session().get(crate::auth::AUTH_ID);
353 match (id, user) {
354 (None, _) => panic!("expected a logged-in session, it's a guest"),
355 (Some(id), Some(user)) => assert_eq!(id, user.id, "logged in as another user"),
356 _ => {}
357 }
358 self
359 }
360
361 #[track_caller]
363 pub fn assert_guest(&self) -> &Self {
364 let id: Option<i64> = self.session().get(crate::auth::AUTH_ID);
365 assert!(id.is_none(), "expected a guest, logged in as user {id:?}");
366 self
367 }
368
369 pub async fn serve(&self) -> String {
373 let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
374 .await
375 .expect("a free port");
376 let url = format!("http://{}", listener.local_addr().expect("an address"));
377 let router = self.kernel.router();
378 tokio::spawn(async move {
379 let _ = axum::serve(
380 listener,
381 router.into_make_service_with_connect_info::<std::net::SocketAddr>(),
382 )
383 .await;
384 });
385 url
386 }
387
388 pub async fn run_all_jobs(&self) -> usize {
392 let mut ran = 0;
393 for _ in 0..1000 {
394 crate::db::sql("UPDATE jobs SET available_at = 0 WHERE reserved_at IS NULL")
395 .execute(self.db())
396 .await
397 .expect("the jobs table can be written");
398 let now = self.run_jobs().await;
399 if now == 0 {
400 break;
401 }
402 ran += now;
403 }
404 ran
405 }
406
407 pub async fn queued_jobs(&self) -> Vec<String> {
409 crate::db::sql("SELECT job FROM jobs ORDER BY id")
410 .scalars(self.db())
411 .await
412 .expect("the jobs table can be read")
413 }
414
415 pub fn acting_as(&self, user: &User) -> &Self {
417 self.at_travelled_time_sync(|| {
418 let session = self.session();
419 login(&session, user, None).expect("the session accepts the login");
420 self.set_cookie(crate::session::cookie_pair(self.state(), &session));
421 });
422 self
423 }
424
425 pub fn confirm_password(&self) -> &Self {
428 self.at_travelled_time_sync(|| {
429 let session = self.session();
430 crate::auth::account::mark_confirmed(&session).expect("the session accepts it");
431 self.set_cookie(crate::session::cookie_pair(self.state(), &session));
432 });
433 self
434 }
435
436 pub fn logout(&self) -> &Self {
438 *self.cookie.lock().unwrap_or_else(|e| e.into_inner()) = None;
439 self
440 }
441
442 pub fn session_cookie(&self) -> Option<String> {
446 self.cookie
447 .lock()
448 .unwrap_or_else(|e| e.into_inner())
449 .clone()
450 }
451
452 pub fn use_session_cookie(&self, cookie: Option<String>) -> &Self {
454 *self.cookie.lock().unwrap_or_else(|e| e.into_inner()) = cookie;
455 self
456 }
457
458 fn session(&self) -> crate::Session {
459 let cookie = self
460 .cookie
461 .lock()
462 .unwrap_or_else(|e| e.into_inner())
463 .clone();
464 self.at_travelled_time_sync(|| crate::session::from_cookie(self.state(), cookie.as_deref()))
467 }
468
469 fn at_travelled_time_sync<T>(&self, f: impl FnOnce() -> T) -> T {
470 let offset = self.offset.load(std::sync::atomic::Ordering::SeqCst);
471 crate::clock::with_offset_sync(offset, f)
472 }
473
474 fn set_cookie(&self, pair: String) {
475 *self.cookie.lock().unwrap_or_else(|e| e.into_inner()) = Some(pair);
476 }
477
478 pub fn csrf_token(&self) -> String {
480 self.at_travelled_time_sync(|| {
481 let session = self.session();
482 self.set_cookie(crate::session::cookie_pair(self.state(), &session));
483 session.token()
484 })
485 }
486
487 pub fn request(&self) -> TestRequest<'_> {
489 TestRequest {
490 app: self,
491 headers: Vec::new(),
492 csrf: true,
493 }
494 }
495
496 pub fn htmx(&self) -> TestRequest<'_> {
498 self.request().htmx()
499 }
500
501 pub async fn get(&self, uri: &str) -> TestResponse {
503 self.request().get(uri).await
504 }
505
506 pub async fn post(&self, uri: &str, form: &[(&str, &str)]) -> TestResponse {
508 self.request().post(uri, form).await
509 }
510
511 pub async fn put(&self, uri: &str, form: &[(&str, &str)]) -> TestResponse {
513 self.request().put(uri, form).await
514 }
515
516 pub async fn patch(&self, uri: &str, form: &[(&str, &str)]) -> TestResponse {
518 self.request().patch(uri, form).await
519 }
520
521 pub async fn delete(&self, uri: &str) -> TestResponse {
523 self.request().delete(uri).await
524 }
525
526 pub async fn post_json(&self, uri: &str, body: &impl Serialize) -> TestResponse {
528 self.request().post_json(uri, body).await
529 }
530
531 pub async fn post_multipart(
533 &self,
534 uri: &str,
535 fields: &[(&str, &str)],
536 files: &[(&str, &str, &[u8])],
537 ) -> TestResponse {
538 self.request().post_multipart(uri, fields, files).await
539 }
540
541 pub async fn post_body(
543 &self,
544 uri: &str,
545 content_type: &str,
546 body: impl Into<Vec<u8>>,
547 ) -> TestResponse {
548 self.request()
549 .without_csrf()
550 .post_body(uri, content_type, body)
551 .await
552 }
553
554 async fn where_count(&self, table: &str, values: &[(&str, &(dyn ToDbValue + Sync))]) -> i64 {
555 let mut sql = format!("SELECT COUNT(*) FROM {}", quote(table));
556 let clauses: Vec<String> = values
557 .iter()
558 .map(|(column, value)| match value.to_db_value() {
559 DbValue::Null => format!("{} IS NULL", quote(column)),
560 _ => format!("{} = ?", quote(column)),
561 })
562 .collect();
563 if !clauses.is_empty() {
564 sql.push_str(" WHERE ");
565 sql.push_str(&clauses.join(" AND "));
566 }
567 crate::db::sql(sql)
568 .bind_all(
569 values
570 .iter()
571 .map(|(_, v)| v.to_db_value())
572 .filter(|v| *v != DbValue::Null),
573 )
574 .scalar(self.db())
575 .await
576 .unwrap_or_else(|err| panic!("could not query `{table}`: {err}"))
577 }
578
579 pub async fn assert_database_has(
581 &self,
582 table: &str,
583 values: &[(&str, &(dyn ToDbValue + Sync))],
584 ) {
585 if self.where_count(table, values).await == 0 {
586 panic!("expected `{table}` to have a row with {}", describe(values));
587 }
588 }
589
590 pub async fn assert_database_missing(
592 &self,
593 table: &str,
594 values: &[(&str, &(dyn ToDbValue + Sync))],
595 ) {
596 let count = self.where_count(table, values).await;
597 if count > 0 {
598 panic!(
599 "expected `{table}` to have no row with {}, found {count}",
600 describe(values)
601 );
602 }
603 }
604
605 pub async fn assert_database_count(&self, table: &str, expected: i64) {
607 let count = self.where_count(table, &[]).await;
608 assert_eq!(count, expected, "rows in `{table}`");
609 }
610
611 pub fn assert_mail_sent(&self, to: &str, subject: &str) {
613 let sent = self.sent_mail();
614 if !sent
615 .iter()
616 .any(|m| m.is_for(to) && m.subject.contains(subject))
617 {
618 let list: Vec<String> = sent
619 .iter()
620 .map(|m| format!("{} ({})", m.subject, m.to.join(", ")))
621 .collect();
622 panic!("no mail to {to} about \"{subject}\"; sent: {list:?}");
623 }
624 }
625}
626
627fn describe(values: &[(&str, &(dyn ToDbValue + Sync))]) -> String {
628 values
629 .iter()
630 .map(|(c, v)| format!("{c} = {:?}", v.to_db_value()))
631 .collect::<Vec<_>>()
632 .join(", ")
633}
634
635pub struct TestRequest<'a> {
637 app: &'a TestApp,
638 headers: Vec<(String, String)>,
639 csrf: bool,
640}
641
642impl TestRequest<'_> {
643 pub fn header(mut self, name: &str, value: &str) -> Self {
645 self.headers.push((name.to_owned(), value.to_owned()));
646 self
647 }
648
649 pub fn htmx(self) -> Self {
651 self.header("hx-request", "true")
652 }
653
654 pub fn json(self) -> Self {
656 self.header("accept", "application/json")
657 }
658
659 pub fn without_csrf(mut self) -> Self {
661 self.csrf = false;
662 self
663 }
664
665 pub async fn get(self, uri: &str) -> TestResponse {
667 self.send(Method::GET, uri, None, Body::empty()).await
668 }
669
670 pub async fn post(self, uri: &str, form: &[(&str, &str)]) -> TestResponse {
672 self.form(Method::POST, uri, form).await
673 }
674
675 pub async fn put(self, uri: &str, form: &[(&str, &str)]) -> TestResponse {
677 self.form(Method::PUT, uri, form).await
678 }
679
680 pub async fn patch(self, uri: &str, form: &[(&str, &str)]) -> TestResponse {
682 self.form(Method::PATCH, uri, form).await
683 }
684
685 pub async fn delete(self, uri: &str) -> TestResponse {
687 self.send(Method::DELETE, uri, None, Body::empty()).await
688 }
689
690 pub async fn post_multipart(
693 self,
694 uri: &str,
695 fields: &[(&str, &str)],
696 files: &[(&str, &str, &[u8])],
697 ) -> TestResponse {
698 const BOUNDARY: &str = "renox-test-boundary-7d1f";
699 let mut body = Vec::new();
700 for (name, value) in fields {
701 body.extend_from_slice(
702 format!("--{BOUNDARY}\r\nContent-Disposition: form-data; name=\"{name}\"\r\n\r\n{value}\r\n")
703 .as_bytes(),
704 );
705 }
706 for (name, file_name, bytes) in files {
707 body.extend_from_slice(
708 format!(
709 "--{BOUNDARY}\r\nContent-Disposition: form-data; name=\"{name}\"; filename=\"{file_name}\"\r\n\
710 Content-Type: application/octet-stream\r\n\r\n"
711 )
712 .as_bytes(),
713 );
714 body.extend_from_slice(bytes);
715 body.extend_from_slice(b"\r\n");
716 }
717 body.extend_from_slice(format!("--{BOUNDARY}--\r\n").as_bytes());
718 let content_type = format!("multipart/form-data; boundary={BOUNDARY}");
719 self.send(Method::POST, uri, Some(&content_type), Body::from(body))
720 .await
721 }
722
723 pub async fn post_body(
726 self,
727 uri: &str,
728 content_type: &str,
729 body: impl Into<Vec<u8>>,
730 ) -> TestResponse {
731 self.send(
732 Method::POST,
733 uri,
734 Some(content_type),
735 Body::from(body.into()),
736 )
737 .await
738 }
739
740 pub async fn post_json(self, uri: &str, body: &impl Serialize) -> TestResponse {
742 let body = serde_json::to_vec(body).expect("the body serializes");
743 self.send(
744 Method::POST,
745 uri,
746 Some("application/json"),
747 Body::from(body),
748 )
749 .await
750 }
751
752 async fn form(self, method: Method, uri: &str, form: &[(&str, &str)]) -> TestResponse {
753 let body = form_urlencoded::Serializer::new(String::new())
754 .extend_pairs(form)
755 .finish();
756 self.send(
757 method,
758 uri,
759 Some("application/x-www-form-urlencoded"),
760 Body::from(body),
761 )
762 .await
763 }
764
765 async fn send(
766 self,
767 method: Method,
768 uri: &str,
769 content_type: Option<&str>,
770 body: Body,
771 ) -> TestResponse {
772 let app = self.app;
773 let token = (self.csrf && method != Method::GET).then(|| app.csrf_token());
774 let mut req = Request::builder().method(method).uri(uri);
775 if let Some(cookie) = app.cookie.lock().unwrap_or_else(|e| e.into_inner()).clone() {
776 req = req.header(COOKIE, cookie);
777 }
778 if let Some(token) = token {
779 req = req.header(crate::CSRF_HEADER, token);
780 }
781 if let Some(content_type) = content_type {
782 req = req.header(CONTENT_TYPE, content_type);
783 }
784 for (name, value) in &self.headers {
785 req = req.header(name.as_str(), value.as_str());
786 }
787 let request = app
788 .kernel
789 .router()
790 .oneshot(req.body(body).expect("a valid request"));
791 let res = app
792 .at_travelled_time(request)
793 .await
794 .expect("the router answers");
795 let view = res
796 .extensions()
797 .get::<crate::view::RenderedView>()
798 .map(|v| v.0.clone());
799
800 let session_cookie = format!("{}=", app.state().config.session_cookie);
801 for set in res.headers().get_all(SET_COOKIE) {
802 let pair = set
803 .to_str()
804 .unwrap_or_default()
805 .split(';')
806 .next()
807 .unwrap_or_default();
808 if pair.starts_with(&session_cookie) {
809 app.set_cookie(pair.to_owned());
810 }
811 }
812 let status = res.status();
813 let headers = res.headers().clone();
814 let body = res
815 .into_body()
816 .collect()
817 .await
818 .expect("the body can be read")
819 .to_bytes();
820 TestResponse {
821 status,
822 headers,
823 body,
824 view,
825 }
826 }
827}
828
829#[derive(Debug, Clone)]
832pub struct TestResponse {
833 pub status: StatusCode,
835 pub headers: HeaderMap,
837 pub body: Bytes,
839 pub view: Option<String>,
841}
842
843impl TestResponse {
844 pub fn text(&self) -> String {
846 String::from_utf8_lossy(&self.body).into_owned()
847 }
848
849 pub fn json<T: DeserializeOwned>(&self) -> T {
851 serde_json::from_slice(&self.body).unwrap_or_else(|err| {
852 panic!(
853 "the body is not the expected JSON ({err}):\n{}",
854 self.excerpt()
855 )
856 })
857 }
858
859 pub fn header(&self, name: &str) -> Option<&str> {
861 self.headers
862 .get(HeaderName::from_bytes(name.as_bytes()).ok()?)
863 .and_then(|v: &HeaderValue| v.to_str().ok())
864 }
865
866 fn excerpt(&self) -> String {
867 let text = self.text();
868 match text.char_indices().nth(600) {
869 Some((i, _)) => format!("{}…", &text[..i]),
870 None => text,
871 }
872 }
873
874 #[track_caller]
876 pub fn assert_status(&self, expected: u16) -> &Self {
877 if self.status.as_u16() != expected {
878 panic!(
879 "expected status {expected}, got {}:\n{}",
880 self.status,
881 self.excerpt()
882 );
883 }
884 self
885 }
886
887 #[track_caller]
889 pub fn assert_ok(&self) -> &Self {
890 self.assert_status(200)
891 }
892
893 #[track_caller]
895 pub fn assert_not_found(&self) -> &Self {
896 self.assert_status(404)
897 }
898
899 #[track_caller]
901 pub fn assert_forbidden(&self) -> &Self {
902 self.assert_status(403)
903 }
904
905 #[track_caller]
907 pub fn assert_unauthorized(&self) -> &Self {
908 self.assert_status(401)
909 }
910
911 #[track_caller]
913 pub fn assert_redirect(&self, to: &str) -> &Self {
914 if !self.status.is_redirection() {
915 panic!(
916 "expected a redirect to {to}, got {}:\n{}",
917 self.status,
918 self.excerpt()
919 );
920 }
921 let location = self.header(LOCATION.as_str()).unwrap_or_default();
922 if location != to {
923 panic!("expected a redirect to {to}, got one to {location}");
924 }
925 self
926 }
927
928 #[track_caller]
930 pub fn assert_hx_redirect(&self, to: &str) -> &Self {
931 let location = self.header("hx-redirect").unwrap_or_default();
932 if location != to {
933 panic!(
934 "expected HX-Redirect to {to}, got `{location}` ({})",
935 self.status
936 );
937 }
938 self
939 }
940
941 #[track_caller]
943 pub fn assert_see(&self, text: &str) -> &Self {
944 if !self.text().contains(text) {
945 panic!("expected to see {text:?} in:\n{}", self.excerpt());
946 }
947 self
948 }
949
950 #[track_caller]
952 pub fn assert_dont_see(&self, text: &str) -> &Self {
953 if self.text().contains(text) {
954 panic!("expected not to see {text:?} in:\n{}", self.excerpt());
955 }
956 self
957 }
958
959 #[track_caller]
962 pub fn assert_view(&self, name: &str) -> &Self {
963 assert_eq!(self.view.as_deref(), Some(name), "the view rendered");
964 self
965 }
966
967 pub fn json_path(&self, path: &str) -> serde_json::Value {
970 let body: serde_json::Value = serde_json::from_slice(&self.body).unwrap_or_default();
971 path.split('.')
972 .filter(|part| !part.is_empty())
973 .fold(body, |value, part| match part.parse::<usize>() {
974 Ok(i) if value.is_array() => value.get(i).cloned().unwrap_or_default(),
975 _ => value.get(part).cloned().unwrap_or_default(),
976 })
977 }
978
979 #[track_caller]
982 pub fn assert_json_path(&self, path: &str, expected: impl Serialize) -> &Self {
983 let expected = serde_json::to_value(expected).expect("a JSON value");
984 assert_eq!(self.json_path(path), expected, "JSON at `{path}`");
985 self
986 }
987
988 #[track_caller]
992 pub fn assert_json(&self, expected: serde_json::Value) -> &Self {
993 let body: serde_json::Value = serde_json::from_slice(&self.body).unwrap_or_default();
994 assert!(
995 json_contains(&body, &expected),
996 "the JSON body doesn't contain {expected}; it's {body}"
997 );
998 self
999 }
1000
1001 #[track_caller]
1003 pub fn assert_header(&self, name: &str, value: &str) -> &Self {
1004 let actual = self.header(name);
1005 if actual != Some(value) {
1006 panic!("expected header {name}: {value}, got {actual:?}");
1007 }
1008 self
1009 }
1010
1011 #[track_caller]
1013 pub fn assert_invalid(&self, field: &str) -> &Self {
1014 self.assert_status(422);
1015 let body: serde_json::Value = self.json();
1016 if body["errors"][field]
1017 .as_array()
1018 .is_none_or(|e| e.is_empty())
1019 {
1020 panic!(
1021 "expected a validation error for `{field}`, got {}",
1022 body["errors"]
1023 );
1024 }
1025 self
1026 }
1027}
1028
1029fn json_contains(actual: &serde_json::Value, expected: &serde_json::Value) -> bool {
1031 use serde_json::Value;
1032 match (actual, expected) {
1033 (Value::Object(a), Value::Object(e)) => e
1034 .iter()
1035 .all(|(k, v)| a.get(k).is_some_and(|av| json_contains(av, v))),
1036 (Value::Array(a), Value::Array(e)) => {
1037 a.len() == e.len() && a.iter().zip(e).all(|(av, ev)| json_contains(av, ev))
1038 }
1039 _ => actual == expected,
1040 }
1041}