1#![deny(missing_docs)]
36#![cfg_attr(docsrs, feature(doc_cfg))]
38
39use std::{collections::HashMap, fmt, sync::RwLock, time::Duration};
42
43use cookie::{Cookie as RawCookie, Expiration};
44use cookie_store::{Cookie as StoredCookie, CookieStore as Store, StoreAction};
45use http::HeaderValue;
46use time::OffsetDateTime;
47use url::Url;
48
49pub const DEFAULT_MAX_AGE: Duration = Duration::from_secs(400 * 24 * 60 * 60);
51pub const DEFAULT_MAX_SIZE: usize = 4096;
56pub const DEFAULT_MAX_PER_HOST: usize = 180;
58pub const DEFAULT_MAX_TOTAL: usize = 3000;
60
61#[derive(Debug, Clone)]
63pub struct CookieLimits {
64 pub max_age: Duration,
66 pub max_size: usize,
68 pub max_per_host: usize,
73 pub max_total: usize,
77}
78
79impl Default for CookieLimits {
80 fn default() -> Self {
81 Self {
82 max_age: DEFAULT_MAX_AGE,
83 max_size: DEFAULT_MAX_SIZE,
84 max_per_host: DEFAULT_MAX_PER_HOST,
85 max_total: DEFAULT_MAX_TOTAL,
86 }
87 }
88}
89
90type CookieKey = (String, String, String);
93
94fn key_of(cookie: &StoredCookie<'static>) -> CookieKey {
95 (
96 String::from(&cookie.domain),
97 String::from(&cookie.path),
98 cookie.name().to_owned(),
99 )
100}
101
102fn is_secure(url: &Url) -> bool {
108 url.scheme() == "https"
109}
110
111#[derive(Debug)]
113pub struct FaithJar {
114 limits: CookieLimits,
115 inner: RwLock<Inner>,
116}
117
118#[derive(Debug, Default)]
119struct Inner {
120 store: Store,
121 order: HashMap<CookieKey, u64>,
124 next_seq: u64,
125}
126
127impl FaithJar {
128 pub fn new(limits: CookieLimits) -> Self {
130 Self {
131 limits,
132 inner: RwLock::new(Inner::default()),
133 }
134 }
135
136 pub fn add_cookie_str(&self, cookie: &str, url: &Url) -> Result<(), CookieRejected> {
138 let raw = RawCookie::parse(cookie.to_owned()).map_err(|_| CookieRejected::Malformed)?;
139 self.store_one(raw, url)
140 }
141
142 fn store_one(&self, raw: RawCookie<'static>, url: &Url) -> Result<(), CookieRejected> {
144 let raw = self.sanitise(raw, url)?;
145 let mut inner = self.inner.write().unwrap();
146 inner.insert(&raw, url, &self.limits);
147 Ok(())
148 }
149
150 fn sanitise(
153 &self,
154 mut raw: RawCookie<'static>,
155 url: &Url,
156 ) -> Result<RawCookie<'static>, CookieRejected> {
157 if raw.name().len() + raw.value().len() > self.limits.max_size {
158 return Err(CookieRejected::TooLarge);
159 }
160
161 if !prefix_allows(&raw, url) {
162 return Err(CookieRejected::PrefixUnmet);
163 }
164
165 clamp_expiry(&mut raw, self.limits.max_age);
166 Ok(raw)
167 }
168}
169
170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
172#[non_exhaustive]
173pub enum CookieRejected {
174 Malformed,
176 TooLarge,
178 PrefixUnmet,
180}
181
182impl fmt::Display for CookieRejected {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 f.write_str(match self {
185 Self::Malformed => "the cookie did not parse",
186 Self::TooLarge => "the cookie is over the size limit",
187 Self::PrefixUnmet => "the cookie's name prefix requires more than it carries",
188 })
189 }
190}
191
192impl std::error::Error for CookieRejected {}
193
194fn prefix_allows(raw: &RawCookie<'_>, url: &Url) -> bool {
199 let secure = raw.secure().unwrap_or(false) && is_secure(url);
200
201 if raw.name().starts_with("__Host-") {
202 return secure && raw.domain().is_none() && raw.path() == Some("/");
204 }
205
206 if raw.name().starts_with("__Secure-") {
207 return secure;
208 }
209
210 true
211}
212
213fn clamp_expiry(raw: &mut RawCookie<'static>, max_age: Duration) {
218 let limit = time::Duration::try_from(max_age).unwrap_or(time::Duration::MAX);
219
220 if let Some(max_age) = raw.max_age() {
221 if max_age > limit {
222 raw.set_max_age(limit);
223 }
224
225 return;
226 }
227
228 if let Some(Expiration::DateTime(expires)) = raw.expires() {
229 let latest = OffsetDateTime::now_utc().saturating_add(limit);
230 if expires > latest {
231 raw.set_expires(latest);
232 }
233 }
234}
235
236impl Inner {
237 fn insert(&mut self, raw: &RawCookie<'static>, url: &Url, limits: &CookieLimits) {
238 let Ok(parsed) = StoredCookie::try_from_raw_cookie(raw, url) else {
241 return;
242 };
243 let key = key_of(&parsed);
244
245 match self.store.insert_raw(raw, url) {
246 Ok(StoreAction::Inserted | StoreAction::UpdatedExisting) => {
250 if !self.order.contains_key(&key) {
251 self.order.insert(key.clone(), self.next_seq);
252 self.next_seq += 1;
253 }
254 }
255 Ok(StoreAction::ExpiredExisting) | Err(_) => return,
258 }
259
260 self.enforce(&key.0, limits);
261 }
262
263 fn enforce(&mut self, domain: &str, limits: &CookieLimits) {
268 if self.count(Some(domain)) > limits.max_per_host {
269 self.purge_expired();
270 self.evict_oldest(Some(domain), limits.max_per_host);
271 }
272
273 if self.count(None) > limits.max_total {
274 self.purge_expired();
275 self.evict_oldest(None, limits.max_total);
276 }
277 }
278
279 fn count(&self, domain: Option<&str>) -> usize {
280 match domain {
281 None => self.order.len(),
282 Some(domain) => self.order.keys().filter(|(d, ..)| d == domain).count(),
283 }
284 }
285
286 fn purge_expired(&mut self) {
288 let expired: Vec<CookieKey> = self
289 .store
290 .iter_any()
291 .filter(|cookie| cookie.is_expired())
292 .map(key_of)
293 .collect();
294
295 for key in expired {
296 self.remove(&key);
297 }
298 }
299
300 fn evict_oldest(&mut self, domain: Option<&str>, limit: usize) {
302 let mut scoped: Vec<(u64, CookieKey)> = self
303 .order
304 .iter()
305 .filter(|((d, ..), _)| domain.is_none_or(|domain| d == domain))
306 .map(|(key, seq)| (*seq, key.clone()))
307 .collect();
308
309 let excess = scoped.len().saturating_sub(limit);
310 if excess == 0 {
311 return;
312 }
313
314 scoped.sort_unstable();
315 for (_, key) in scoped.into_iter().take(excess) {
316 self.remove(&key);
317 }
318 }
319
320 fn remove(&mut self, key: &CookieKey) {
322 let (domain, path, name) = key;
323 self.store.remove(domain, path, name);
324 self.order.remove(key);
325 }
326}
327
328impl FaithJar {
329 pub fn store_response_cookies<'h>(
334 &self,
335 cookie_headers: impl Iterator<Item = &'h HeaderValue>,
336 url: &Url,
337 ) {
338 for header in cookie_headers {
339 let Ok(header) = std::str::from_utf8(header.as_bytes()) else {
340 continue;
341 };
342
343 let Ok(raw) = RawCookie::parse(header.to_owned()) else {
344 continue;
345 };
346
347 let _ = self.store_one(raw, url);
348 }
349 }
350
351 pub fn request_cookie_header(&self, url: &Url) -> Option<HeaderValue> {
353 let inner = self.inner.read().unwrap();
354 let cookies = inner
355 .store
356 .get_request_values(url)
357 .map(|(name, value)| format!("{name}={value}"))
358 .collect::<Vec<_>>()
359 .join("; ");
360
361 if cookies.is_empty() {
362 return None;
363 }
364
365 HeaderValue::from_str(&cookies).ok()
366 }
367}
368
369#[cfg(feature = "reqwest")]
370impl reqwest::cookie::CookieStore for FaithJar {
371 fn set_cookies(&self, cookie_headers: &mut dyn Iterator<Item = &HeaderValue>, url: &Url) {
372 self.store_response_cookies(cookie_headers, url);
373 }
374
375 fn cookies(&self, url: &Url) -> Option<HeaderValue> {
376 self.request_cookie_header(url)
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 fn url(url: &str) -> Url {
385 Url::parse(url).unwrap()
386 }
387
388 fn jar() -> FaithJar {
389 FaithJar::new(CookieLimits::default())
390 }
391
392 fn jar_with(limits: CookieLimits) -> FaithJar {
393 FaithJar::new(limits)
394 }
395
396 fn sent(jar: &FaithJar, url: &str) -> Option<String> {
398 jar.request_cookie_header(&self::url(url))
399 .map(|value| value.to_str().unwrap().to_owned())
400 }
401
402 fn stored_count(jar: &FaithJar) -> usize {
403 jar.inner.read().unwrap().store.iter_unexpired().count()
404 }
405
406 #[test]
409 fn host_prefix_accepted_when_fully_qualified() {
410 let jar = jar();
411 jar.add_cookie_str("__Host-a=1; Secure; Path=/", &url("https://example.com/"))
412 .expect("the cookie is stored");
413 assert_eq!(
414 sent(&jar, "https://example.com/"),
415 Some("__Host-a=1".into())
416 );
417 }
418
419 #[test]
420 fn host_prefix_rejected_without_secure_attribute() {
421 let jar = jar();
422 assert_eq!(
423 jar.add_cookie_str("__Host-a=1; Path=/", &url("https://example.com/")),
424 Err(CookieRejected::PrefixUnmet)
425 );
426 assert_eq!(sent(&jar, "https://example.com/"), None);
427 }
428
429 #[test]
430 fn host_prefix_rejected_over_insecure_transport() {
431 let jar = jar();
432 assert_eq!(
433 jar.add_cookie_str("__Host-a=1; Secure; Path=/", &url("http://example.com/")),
434 Err(CookieRejected::PrefixUnmet)
435 );
436 assert_eq!(sent(&jar, "http://example.com/"), None);
437 }
438
439 #[test]
440 fn host_prefix_rejected_with_domain_attribute() {
441 let jar = jar();
442 assert_eq!(
443 jar.add_cookie_str(
444 "__Host-a=1; Secure; Path=/; Domain=example.com",
445 &url("https://example.com/"),
446 ),
447 Err(CookieRejected::PrefixUnmet)
448 );
449 assert_eq!(sent(&jar, "https://example.com/"), None);
450 }
451
452 #[test]
453 fn host_prefix_rejected_with_non_root_path() {
454 let jar = jar();
455 assert_eq!(
456 jar.add_cookie_str(
457 "__Host-a=1; Secure; Path=/app",
458 &url("https://example.com/app"),
459 ),
460 Err(CookieRejected::PrefixUnmet)
461 );
462 assert_eq!(sent(&jar, "https://example.com/app"), None);
463 }
464
465 #[test]
466 fn host_prefix_rejected_without_path_attribute() {
467 let jar = jar();
468 assert_eq!(
469 jar.add_cookie_str("__Host-a=1; Secure", &url("https://example.com/")),
470 Err(CookieRejected::PrefixUnmet)
471 );
472 assert_eq!(sent(&jar, "https://example.com/"), None);
473 }
474
475 #[test]
476 fn secure_prefix_accepted_with_secure_attribute() {
477 let jar = jar();
478 jar.add_cookie_str("__Secure-a=1; Secure", &url("https://example.com/"))
479 .expect("the cookie is stored");
480 assert_eq!(
481 sent(&jar, "https://example.com/"),
482 Some("__Secure-a=1".into())
483 );
484 }
485
486 #[test]
487 fn secure_prefix_allows_domain_and_path_unlike_host() {
488 let jar = jar();
489 jar.add_cookie_str(
490 "__Secure-a=1; Secure; Path=/app; Domain=example.com",
491 &url("https://example.com/app"),
492 )
493 .expect("the cookie is stored");
494 assert_eq!(
495 sent(&jar, "https://sub.example.com/app"),
496 Some("__Secure-a=1".into())
497 );
498 }
499
500 #[test]
501 fn secure_prefix_rejected_without_secure_attribute() {
502 let jar = jar();
503 assert_eq!(
504 jar.add_cookie_str("__Secure-a=1", &url("https://example.com/")),
505 Err(CookieRejected::PrefixUnmet)
506 );
507 assert_eq!(sent(&jar, "https://example.com/"), None);
508 }
509
510 #[test]
511 fn secure_prefix_rejected_over_insecure_transport() {
512 let jar = jar();
513 assert_eq!(
514 jar.add_cookie_str("__Secure-a=1; Secure", &url("http://example.com/")),
515 Err(CookieRejected::PrefixUnmet)
516 );
517 assert_eq!(sent(&jar, "http://example.com/"), None);
518 }
519
520 #[test]
521 fn prefixes_are_matched_case_sensitively() {
522 let jar = jar();
523 jar.add_cookie_str("__host-a=1", &url("http://example.com/"))
526 .expect("the cookie is stored");
527 jar.add_cookie_str("__SECURE-b=2", &url("http://example.com/"))
528 .expect("the cookie is stored");
529
530 let sent = sent(&jar, "http://example.com/").unwrap();
531 assert!(sent.contains("__host-a=1"), "{sent}");
532 assert!(sent.contains("__SECURE-b=2"), "{sent}");
533 }
534
535 #[test]
536 fn unprefixed_cookies_are_unaffected() {
537 let jar = jar();
538 jar.add_cookie_str("a=1", &url("http://example.com/"))
539 .expect("the cookie is stored");
540 assert_eq!(sent(&jar, "http://example.com/"), Some("a=1".into()));
541 }
542
543 #[test]
546 fn over_long_max_age_is_reduced_to_the_limit() {
547 let jar = jar();
548 let over = DEFAULT_MAX_AGE.as_secs() * 2;
549 jar.add_cookie_str(
550 &format!("a=1; Max-Age={over}"),
551 &url("https://example.com/"),
552 )
553 .expect("the cookie is stored");
554
555 let inner = jar.inner.read().unwrap();
556 let cookie = inner.store.get("example.com", "/", "a").unwrap();
557 let limit = OffsetDateTime::now_utc() + time::Duration::try_from(DEFAULT_MAX_AGE).unwrap();
558 assert!(
559 cookie.expires_by(&(limit + time::Duration::minutes(1))),
560 "expiry should have been reduced to the limit"
561 );
562 }
563
564 #[test]
565 fn over_long_expires_is_reduced_to_the_limit() {
566 let jar = jar();
567 jar.add_cookie_str(
568 "a=1; Expires=Fri, 31 Dec 9999 23:59:59 GMT",
569 &url("https://example.com/"),
570 )
571 .expect("the cookie is stored");
572
573 let inner = jar.inner.read().unwrap();
574 let cookie = inner.store.get("example.com", "/", "a").unwrap();
575 let limit = OffsetDateTime::now_utc() + time::Duration::try_from(DEFAULT_MAX_AGE).unwrap();
576 assert!(
577 cookie.expires_by(&(limit + time::Duration::minutes(1))),
578 "expiry should have been reduced to the limit"
579 );
580 }
581
582 #[test]
583 fn shorter_expiry_is_left_alone() {
584 let jar = jar();
585 jar.add_cookie_str("a=1; Max-Age=60", &url("https://example.com/"))
586 .expect("the cookie is stored");
587
588 let inner = jar.inner.read().unwrap();
589 let cookie = inner.store.get("example.com", "/", "a").unwrap();
590 assert!(cookie.expires_by(&(OffsetDateTime::now_utc() + time::Duration::minutes(2))));
591 assert!(!cookie.expires_by(&(OffsetDateTime::now_utc() + time::Duration::seconds(30))));
592 }
593
594 #[test]
595 fn session_cookie_stays_a_session_cookie() {
596 let jar = jar();
597 jar.add_cookie_str("a=1", &url("https://example.com/"))
598 .expect("the cookie is stored");
599
600 let inner = jar.inner.read().unwrap();
601 let cookie = inner.store.get("example.com", "/", "a").unwrap();
602 assert!(!cookie.is_persistent(), "should not have gained an expiry");
603 }
604
605 #[test]
606 fn max_age_takes_precedence_over_expires() {
607 let jar = jar();
608 jar.add_cookie_str(
611 "a=1; Max-Age=60; Expires=Fri, 31 Dec 9999 23:59:59 GMT",
612 &url("https://example.com/"),
613 )
614 .expect("the cookie is stored");
615
616 let inner = jar.inner.read().unwrap();
617 let cookie = inner.store.get("example.com", "/", "a").unwrap();
618 assert!(cookie.expires_by(&(OffsetDateTime::now_utc() + time::Duration::minutes(2))));
619 }
620
621 #[test]
622 fn expiring_a_cookie_still_works() {
623 let jar = jar();
625 jar.add_cookie_str("a=1", &url("https://example.com/"))
626 .expect("the cookie is stored");
627 assert_eq!(sent(&jar, "https://example.com/"), Some("a=1".into()));
628
629 jar.add_cookie_str("a=1; Max-Age=0", &url("https://example.com/"))
630 .expect("the cookie is stored");
631 assert_eq!(sent(&jar, "https://example.com/"), None);
632 }
633
634 #[test]
635 fn max_age_limit_is_configurable() {
636 let jar = jar_with(CookieLimits {
637 max_age: Duration::from_secs(60),
638 ..Default::default()
639 });
640 jar.add_cookie_str("a=1; Max-Age=86400", &url("https://example.com/"))
641 .expect("the cookie is stored");
642
643 let inner = jar.inner.read().unwrap();
644 let cookie = inner.store.get("example.com", "/", "a").unwrap();
645 assert!(cookie.expires_by(&(OffsetDateTime::now_utc() + time::Duration::minutes(2))));
646 }
647
648 #[test]
651 fn oversized_cookie_is_rejected() {
652 let jar = jar();
653 let value = "x".repeat(DEFAULT_MAX_SIZE);
654 let _ = jar.add_cookie_str(&format!("a={value}"), &url("https://example.com/"));
655 assert_eq!(sent(&jar, "https://example.com/"), None);
656 }
657
658 #[test]
659 fn cookie_at_exactly_the_size_cap_is_stored() {
660 let jar = jar();
661 let value = "x".repeat(DEFAULT_MAX_SIZE - 1);
662 let _ = jar.add_cookie_str(&format!("a={value}"), &url("https://example.com/"));
663 assert_eq!(stored_count(&jar), 1);
664 }
665
666 #[test]
667 fn size_cap_counts_name_and_value_together() {
668 let jar = jar_with(CookieLimits {
669 max_size: 10,
670 ..Default::default()
671 });
672 assert_eq!(
674 jar.add_cookie_str("name=1234567", &url("https://example.com/")),
675 Err(CookieRejected::TooLarge)
676 );
677 assert_eq!(sent(&jar, "https://example.com/"), None);
678
679 jar.add_cookie_str("name=123456", &url("https://example.com/"))
680 .expect("the cookie is stored");
681 assert_eq!(
682 sent(&jar, "https://example.com/"),
683 Some("name=123456".into())
684 );
685 }
686
687 #[test]
688 fn size_cap_does_not_count_attributes() {
689 let jar = jar_with(CookieLimits {
690 max_size: 10,
691 ..Default::default()
692 });
693 jar.add_cookie_str(
694 "name=12345; Path=/; Secure; HttpOnly",
695 &url("https://example.com/"),
696 )
697 .expect("the cookie is stored");
698 assert_eq!(stored_count(&jar), 1);
699 }
700
701 #[test]
704 fn per_host_cap_evicts_the_oldest() {
705 let jar = jar_with(CookieLimits {
706 max_per_host: 3,
707 ..Default::default()
708 });
709 for n in 0..5 {
710 jar.add_cookie_str(&format!("c{n}=1"), &url("https://example.com/"))
711 .expect("the cookie is stored");
712 }
713
714 let sent = sent(&jar, "https://example.com/").unwrap();
715 assert_eq!(stored_count(&jar), 3, "{sent}");
716 assert!(!sent.contains("c0="), "oldest should have gone: {sent}");
717 assert!(
718 !sent.contains("c1="),
719 "next-oldest should have gone: {sent}"
720 );
721 assert!(sent.contains("c4=1"), "newest should have stayed: {sent}");
722 }
723
724 #[test]
725 fn per_host_cap_is_per_domain() {
726 let jar = jar_with(CookieLimits {
727 max_per_host: 2,
728 ..Default::default()
729 });
730 for n in 0..3 {
731 jar.add_cookie_str(&format!("c{n}=1"), &url("https://one.example/"))
732 .expect("the cookie is stored");
733 jar.add_cookie_str(&format!("c{n}=1"), &url("https://two.example/"))
734 .expect("the cookie is stored");
735 }
736
737 assert_eq!(stored_count(&jar), 4, "each domain keeps its own allowance");
738 }
739
740 #[test]
741 fn refreshing_a_cookie_keeps_its_place_in_the_order() {
742 let jar = jar_with(CookieLimits {
743 max_per_host: 2,
744 ..Default::default()
745 });
746 jar.add_cookie_str("a=1", &url("https://example.com/"))
747 .expect("the cookie is stored");
748 jar.add_cookie_str("b=1", &url("https://example.com/"))
749 .expect("the cookie is stored");
750 jar.add_cookie_str("a=2", &url("https://example.com/"))
753 .expect("the cookie is stored");
754 jar.add_cookie_str("c=1", &url("https://example.com/"))
755 .expect("the cookie is stored");
756
757 let sent = sent(&jar, "https://example.com/").unwrap();
758 assert!(
759 !sent.contains("a="),
760 "a was oldest and should have gone: {sent}"
761 );
762 assert!(sent.contains("b=1"), "{sent}");
763 assert!(sent.contains("c=1"), "{sent}");
764 }
765
766 #[test]
767 fn expired_cookies_are_purged_before_live_ones_are_evicted() {
768 let jar = jar_with(CookieLimits {
769 max_per_host: 3,
770 ..Default::default()
771 });
772 jar.add_cookie_str("dead1=1; Max-Age=1", &url("https://example.com/"))
774 .expect("the cookie is stored");
775 jar.add_cookie_str("dead2=1; Max-Age=1", &url("https://example.com/"))
776 .expect("the cookie is stored");
777 jar.add_cookie_str("live1=1", &url("https://example.com/"))
778 .expect("the cookie is stored");
779
780 std::thread::sleep(Duration::from_millis(1100));
781
782 jar.add_cookie_str("live2=1", &url("https://example.com/"))
784 .expect("the cookie is stored");
785
786 let sent = sent(&jar, "https://example.com/").unwrap();
787 assert!(sent.contains("live1=1"), "{sent}");
788 assert!(sent.contains("live2=1"), "{sent}");
789 assert_eq!(stored_count(&jar), 2, "{sent}");
790 }
791
792 #[test]
793 fn whole_jar_cap_bounds_cookies_spread_across_domains() {
794 let jar = jar_with(CookieLimits {
795 max_per_host: 100,
796 max_total: 5,
797 ..Default::default()
798 });
799 for n in 0..10 {
800 jar.add_cookie_str("a=1", &url(&format!("https://host{n}.example/")))
801 .expect("the cookie is stored");
802 }
803
804 assert_eq!(stored_count(&jar), 5);
805 assert_eq!(
806 sent(&jar, "https://host0.example/"),
807 None,
808 "oldest domain evicted"
809 );
810 assert_eq!(sent(&jar, "https://host9.example/"), Some("a=1".into()));
811 }
812
813 #[test]
814 fn domain_scoped_cookies_are_evictable() {
815 let jar = jar_with(CookieLimits {
819 max_per_host: 2,
820 ..Default::default()
821 });
822 for n in 0..4 {
823 jar.add_cookie_str(
824 &format!("c{n}=1; Domain=example.com"),
825 &url("https://example.com/"),
826 )
827 .expect("the cookie is stored");
828 }
829
830 let sent = sent(&jar, "https://www.example.com/").unwrap();
831 assert_eq!(stored_count(&jar), 2, "{sent}");
832 assert!(!sent.contains("c0="), "oldest should have gone: {sent}");
833 assert!(sent.contains("c3=1"), "newest should have stayed: {sent}");
834 }
835
836 #[test]
837 fn a_cap_of_zero_stores_nothing() {
838 let jar = jar_with(CookieLimits {
839 max_per_host: 0,
840 ..Default::default()
841 });
842 jar.add_cookie_str("a=1", &url("https://example.com/"))
843 .expect("the cookie is stored");
844 assert_eq!(sent(&jar, "https://example.com/"), None);
845 }
846
847 #[test]
850 fn cookies_are_scoped_to_their_domain() {
851 let jar = jar();
852 jar.add_cookie_str("a=1", &url("https://example.com/"))
853 .expect("the cookie is stored");
854 assert_eq!(sent(&jar, "https://elsewhere.example/"), None);
855 }
856
857 #[test]
858 fn set_cookies_applies_the_same_rules_as_add_cookie() {
859 let jar = jar();
860 let headers = [
861 HeaderValue::from_static("__Host-good=1; Secure; Path=/"),
862 HeaderValue::from_static("__Host-bad=1; Path=/"),
863 ];
864
865 jar.store_response_cookies(headers.iter(), &url("https://example.com/"));
866
867 let sent = sent(&jar, "https://example.com/").unwrap();
868 assert!(sent.contains("__Host-good=1"), "{sent}");
869 assert!(!sent.contains("__Host-bad"), "{sent}");
870 }
871
872 #[test]
873 fn an_unparseable_cookie_is_refused() {
874 let jar = jar();
875 assert_eq!(
876 jar.add_cookie_str("", &url("https://example.com/")),
877 Err(CookieRejected::Malformed)
878 );
879 assert_eq!(sent(&jar, "https://example.com/"), None);
880 }
881}