Skip to main content

rusmes_auth/backends/
ldap.rs

1//! LDAP authentication backend
2
3use crate::AuthBackend;
4use async_trait::async_trait;
5use ldap3::{Ldap, LdapConnAsync, LdapConnSettings, Scope, SearchEntry};
6use rusmes_proto::Username;
7use std::collections::HashMap;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11/// Configuration for LDAP authentication
12#[derive(Debug, Clone)]
13pub struct LdapConfig {
14    /// LDAP server URL (e.g., ldap://localhost:389 or ldaps://localhost:636)
15    pub server_url: String,
16    /// Base DN for user searches
17    pub base_dn: String,
18    /// User search filter (e.g., (uid={username}))
19    pub user_filter: String,
20    /// Bind DN pattern for direct bind (e.g., "uid={username},ou=users,dc=example,dc=com")
21    /// If set, this is used instead of search + bind
22    pub bind_dn_pattern: Option<String>,
23    /// Bind DN for initial connection (if needed for search operations)
24    pub bind_dn: Option<String>,
25    /// Bind password for initial connection
26    pub bind_password: Option<String>,
27    /// Group base DN for membership checks
28    pub group_base_dn: Option<String>,
29    /// Group membership filter
30    pub group_filter: Option<String>,
31    /// Required group DN for authentication
32    pub required_group: Option<String>,
33    /// Connection timeout in seconds
34    pub timeout_secs: u64,
35    /// Enable connection pooling
36    pub pool_size: usize,
37    /// Enable TLS/STARTTLS
38    pub use_tls: bool,
39    /// Skip TLS certificate verification (for testing only)
40    pub tls_skip_verify: bool,
41}
42
43impl Default for LdapConfig {
44    fn default() -> Self {
45        Self {
46            server_url: "ldap://localhost:389".to_string(),
47            base_dn: "dc=example,dc=com".to_string(),
48            user_filter: "(uid={username})".to_string(),
49            bind_dn_pattern: None,
50            bind_dn: None,
51            bind_password: None,
52            group_base_dn: None,
53            group_filter: None,
54            required_group: None,
55            timeout_secs: 10,
56            pool_size: 5,
57            use_tls: false,
58            tls_skip_verify: false,
59        }
60    }
61}
62
63/// Connection pool for LDAP connections
64struct ConnectionPool {
65    config: LdapConfig,
66    connections: Arc<RwLock<Vec<Ldap>>>,
67    max_size: usize,
68}
69
70impl ConnectionPool {
71    fn new(config: LdapConfig) -> Self {
72        let max_size = config.pool_size;
73        Self {
74            config,
75            connections: Arc::new(RwLock::new(Vec::new())),
76            max_size,
77        }
78    }
79
80    async fn get_connection(&self) -> anyhow::Result<Ldap> {
81        // Try to get from pool
82        {
83            let mut pool = self.connections.write().await;
84            if let Some(conn) = pool.pop() {
85                return Ok(conn);
86            }
87        }
88
89        // Create new connection
90        self.create_connection().await
91    }
92
93    async fn create_connection(&self) -> anyhow::Result<Ldap> {
94        let settings = LdapConnSettings::new()
95            .set_conn_timeout(std::time::Duration::from_secs(self.config.timeout_secs));
96
97        let (conn, mut ldap) =
98            LdapConnAsync::with_settings(settings, &self.config.server_url).await?;
99
100        ldap3::drive!(conn);
101
102        // Bind if credentials provided
103        if let (Some(bind_dn), Some(bind_password)) =
104            (&self.config.bind_dn, &self.config.bind_password)
105        {
106            ldap.simple_bind(bind_dn, bind_password).await?;
107        }
108
109        Ok(ldap)
110    }
111
112    async fn return_connection(&self, conn: Ldap) {
113        let mut pool = self.connections.write().await;
114        if pool.len() < self.max_size {
115            pool.push(conn);
116        }
117    }
118}
119
120/// LDAP authentication backend
121pub struct LdapBackend {
122    config: LdapConfig,
123    pool: ConnectionPool,
124    user_cache: Arc<RwLock<HashMap<String, bool>>>,
125}
126
127impl LdapBackend {
128    /// Create a new LDAP authentication backend
129    pub fn new(config: LdapConfig) -> Self {
130        let pool = ConnectionPool::new(config.clone());
131        Self {
132            config,
133            pool,
134            user_cache: Arc::new(RwLock::new(HashMap::new())),
135        }
136    }
137
138    /// Get user DN either by pattern or search
139    async fn get_user_dn(&self, username: &str) -> anyhow::Result<Option<String>> {
140        // If bind_dn_pattern is configured, use it directly
141        if let Some(pattern) = &self.config.bind_dn_pattern {
142            let dn = pattern.replace("{username}", username);
143            return Ok(Some(dn));
144        }
145
146        // Otherwise, search for the user
147        self.search_user(username).await
148    }
149
150    /// Search for a user in LDAP
151    async fn search_user(&self, username: &str) -> anyhow::Result<Option<String>> {
152        let mut ldap = self.pool.get_connection().await?;
153
154        let filter = self.config.user_filter.replace("{username}", username);
155
156        let timeout = tokio::time::Duration::from_secs(self.config.timeout_secs);
157        let result = tokio::time::timeout(
158            timeout,
159            ldap.search(&self.config.base_dn, Scope::Subtree, &filter, vec!["dn"]),
160        )
161        .await
162        .map_err(|_| anyhow::anyhow!("LDAP search timeout"))??;
163
164        let (entries, _res) = result.success()?;
165
166        let dn = if entries.is_empty() {
167            None
168        } else {
169            let entry = SearchEntry::construct(entries[0].clone());
170            Some(entry.dn)
171        };
172
173        self.pool.return_connection(ldap).await;
174
175        Ok(dn)
176    }
177
178    /// Attempt to bind as user
179    async fn bind_as_user(&self, dn: &str, password: &str) -> anyhow::Result<bool> {
180        let mut ldap = self.pool.create_connection().await?;
181
182        let timeout = tokio::time::Duration::from_secs(self.config.timeout_secs);
183        match tokio::time::timeout(timeout, ldap.simple_bind(dn, password)).await {
184            Ok(Ok(_)) => {
185                let _ = ldap.unbind().await;
186                Ok(true)
187            }
188            Ok(Err(_)) => Ok(false),
189            Err(_) => Err(anyhow::anyhow!("LDAP bind timeout")),
190        }
191    }
192
193    /// Check group membership
194    async fn check_group_membership(&self, username: &str) -> anyhow::Result<bool> {
195        if self.config.required_group.is_none() {
196            return Ok(true);
197        }
198
199        let group_base = match &self.config.group_base_dn {
200            Some(base) => base,
201            None => &self.config.base_dn,
202        };
203
204        let filter = match &self.config.group_filter {
205            Some(f) => f.replace("{username}", username),
206            None => format!("(memberUid={})", username),
207        };
208
209        let mut ldap = self.pool.get_connection().await?;
210
211        let timeout = tokio::time::Duration::from_secs(self.config.timeout_secs);
212        let result = tokio::time::timeout(
213            timeout,
214            ldap.search(group_base, Scope::Subtree, &filter, vec!["dn"]),
215        )
216        .await
217        .map_err(|_| anyhow::anyhow!("LDAP group search timeout"))??;
218
219        let (entries, _res) = result.success()?;
220
221        self.pool.return_connection(ldap).await;
222
223        if let Some(required_group) = &self.config.required_group {
224            Ok(entries.iter().any(|entry| {
225                let e = SearchEntry::construct(entry.clone());
226                &e.dn == required_group
227            }))
228        } else {
229            Ok(!entries.is_empty())
230        }
231    }
232}
233
234#[async_trait]
235impl AuthBackend for LdapBackend {
236    async fn authenticate(&self, username: &Username, password: &str) -> anyhow::Result<bool> {
237        // Get user DN (either by pattern or search)
238        let dn = match self.get_user_dn(&username.to_string()).await? {
239            Some(dn) => dn,
240            None => return Ok(false),
241        };
242
243        // Try to bind as user
244        let bind_success = self.bind_as_user(&dn, password).await?;
245        if !bind_success {
246            return Ok(false);
247        }
248
249        // Check group membership if required
250        if !self.check_group_membership(&username.to_string()).await? {
251            return Ok(false);
252        }
253
254        // Cache successful authentication
255        self.user_cache
256            .write()
257            .await
258            .insert(username.to_string(), true);
259
260        Ok(true)
261    }
262
263    async fn verify_identity(&self, username: &Username) -> anyhow::Result<bool> {
264        // Check cache first
265        {
266            let cache = self.user_cache.read().await;
267            if cache.contains_key(&username.to_string()) {
268                return Ok(true);
269            }
270        }
271
272        // Search in LDAP
273        let exists = self.search_user(&username.to_string()).await?.is_some();
274
275        if exists {
276            self.user_cache
277                .write()
278                .await
279                .insert(username.to_string(), true);
280        }
281
282        Ok(exists)
283    }
284
285    async fn list_users(&self) -> anyhow::Result<Vec<Username>> {
286        let mut ldap = self.pool.get_connection().await?;
287
288        let filter = self.config.user_filter.replace("{username}", "*");
289        let result = ldap
290            .search(
291                &self.config.base_dn,
292                Scope::Subtree,
293                &filter,
294                vec!["uid", "mail"],
295            )
296            .await?;
297
298        let (entries, _res) = result.success()?;
299
300        self.pool.return_connection(ldap).await;
301
302        let users = entries
303            .into_iter()
304            .filter_map(|entry| {
305                let e = SearchEntry::construct(entry);
306                e.attrs
307                    .get("uid")
308                    .and_then(|uids| uids.first().and_then(|uid| Username::new(uid.clone()).ok()))
309            })
310            .collect();
311
312        Ok(users)
313    }
314
315    async fn create_user(&self, _username: &Username, _password: &str) -> anyhow::Result<()> {
316        Err(anyhow::anyhow!(
317            "LDAP backend does not support user creation (read-only)"
318        ))
319    }
320
321    async fn delete_user(&self, _username: &Username) -> anyhow::Result<()> {
322        Err(anyhow::anyhow!(
323            "LDAP backend does not support user deletion (read-only)"
324        ))
325    }
326
327    async fn change_password(
328        &self,
329        _username: &Username,
330        _new_password: &str,
331    ) -> anyhow::Result<()> {
332        Err(anyhow::anyhow!(
333            "LDAP backend does not support password changes (read-only)"
334        ))
335    }
336}
337
338impl Clone for LdapBackend {
339    fn clone(&self) -> Self {
340        Self {
341            config: self.config.clone(),
342            pool: ConnectionPool::new(self.config.clone()),
343            user_cache: Arc::clone(&self.user_cache),
344        }
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn test_ldap_config_default() {
354        let config = LdapConfig::default();
355        assert_eq!(config.server_url, "ldap://localhost:389");
356        assert_eq!(config.base_dn, "dc=example,dc=com");
357        assert_eq!(config.user_filter, "(uid={username})");
358        assert_eq!(config.timeout_secs, 10);
359        assert_eq!(config.pool_size, 5);
360    }
361
362    #[test]
363    fn test_ldap_config_custom() {
364        let config = LdapConfig {
365            server_url: "ldaps://ldap.example.com:636".to_string(),
366            base_dn: "ou=users,dc=example,dc=org".to_string(),
367            user_filter: "(mail={username}@example.com)".to_string(),
368            bind_dn_pattern: None,
369            bind_dn: Some("cn=admin,dc=example,dc=org".to_string()),
370            bind_password: Some("secret".to_string()),
371            group_base_dn: Some("ou=groups,dc=example,dc=org".to_string()),
372            group_filter: Some(
373                "(&(objectClass=groupOfNames)(member=uid={username},ou=users,dc=example,dc=org))"
374                    .to_string(),
375            ),
376            required_group: Some("cn=mail-users,ou=groups,dc=example,dc=org".to_string()),
377            timeout_secs: 30,
378            pool_size: 10,
379            use_tls: true,
380            tls_skip_verify: false,
381        };
382        assert_eq!(config.timeout_secs, 30);
383        assert_eq!(config.pool_size, 10);
384    }
385
386    #[test]
387    fn test_connection_pool_creation() {
388        let config = LdapConfig::default();
389        let pool = ConnectionPool::new(config.clone());
390        assert_eq!(pool.max_size, config.pool_size);
391    }
392
393    #[tokio::test]
394    async fn test_ldap_backend_creation() {
395        let config = LdapConfig::default();
396        let backend = LdapBackend::new(config);
397        let cache = backend.user_cache.read().await;
398        assert_eq!(cache.len(), 0);
399    }
400
401    #[tokio::test]
402    async fn test_user_filter_substitution() {
403        let config = LdapConfig {
404            user_filter: "(uid={username})".to_string(),
405            ..Default::default()
406        };
407        let filter = config.user_filter.replace("{username}", "testuser");
408        assert_eq!(filter, "(uid=testuser)");
409    }
410
411    #[tokio::test]
412    async fn test_group_filter_substitution() {
413        let config = LdapConfig {
414            group_filter: Some("(memberUid={username})".to_string()),
415            ..Default::default()
416        };
417        let filter = config
418            .group_filter
419            .unwrap()
420            .replace("{username}", "testuser");
421        assert_eq!(filter, "(memberUid=testuser)");
422    }
423
424    #[tokio::test]
425    async fn test_verify_identity_cache() {
426        let backend = LdapBackend::new(LdapConfig::default());
427        let _username = Username::new("cached_user".to_string()).unwrap();
428
429        // Insert into cache
430        backend
431            .user_cache
432            .write()
433            .await
434            .insert("cached_user".to_string(), true);
435
436        // Should return true from cache
437        let cache = backend.user_cache.read().await;
438        assert!(cache.contains_key("cached_user"));
439    }
440
441    #[tokio::test]
442    async fn test_create_user_not_supported() {
443        let backend = LdapBackend::new(LdapConfig::default());
444        let username = Username::new("newuser".to_string()).unwrap();
445        let result = backend.create_user(&username, "password").await;
446        assert!(result.is_err());
447        assert!(result.unwrap_err().to_string().contains("read-only"));
448    }
449
450    #[tokio::test]
451    async fn test_delete_user_not_supported() {
452        let backend = LdapBackend::new(LdapConfig::default());
453        let username = Username::new("user".to_string()).unwrap();
454        let result = backend.delete_user(&username).await;
455        assert!(result.is_err());
456        assert!(result.unwrap_err().to_string().contains("read-only"));
457    }
458
459    #[tokio::test]
460    async fn test_change_password_not_supported() {
461        let backend = LdapBackend::new(LdapConfig::default());
462        let username = Username::new("user".to_string()).unwrap();
463        let result = backend.change_password(&username, "newpass").await;
464        assert!(result.is_err());
465        assert!(result.unwrap_err().to_string().contains("read-only"));
466    }
467
468    #[test]
469    fn test_ldap_config_with_bind_credentials() {
470        let config = LdapConfig {
471            bind_dn: Some("cn=admin,dc=example,dc=com".to_string()),
472            bind_password: Some("admin_password".to_string()),
473            ..Default::default()
474        };
475        assert!(config.bind_dn.is_some());
476        assert!(config.bind_password.is_some());
477    }
478
479    #[test]
480    fn test_ldap_config_without_bind_credentials() {
481        let config = LdapConfig::default();
482        assert!(config.bind_dn.is_none());
483        assert!(config.bind_password.is_none());
484    }
485
486    #[test]
487    fn test_required_group_configuration() {
488        let config = LdapConfig {
489            required_group: Some("cn=email-users,ou=groups,dc=example,dc=com".to_string()),
490            ..Default::default()
491        };
492        assert!(config.required_group.is_some());
493    }
494
495    #[test]
496    fn test_group_base_dn_configuration() {
497        let config = LdapConfig {
498            group_base_dn: Some("ou=groups,dc=example,dc=com".to_string()),
499            ..Default::default()
500        };
501        assert!(config.group_base_dn.is_some());
502    }
503
504    #[tokio::test]
505    async fn test_connection_pool_size() {
506        let config = LdapConfig {
507            pool_size: 3,
508            ..Default::default()
509        };
510        let pool = ConnectionPool::new(config);
511        assert_eq!(pool.max_size, 3);
512    }
513
514    #[test]
515    fn test_complex_user_filter() {
516        let config = LdapConfig {
517            user_filter: "(&(objectClass=person)(|(uid={username})(mail={username})))".to_string(),
518            ..Default::default()
519        };
520        let filter = config.user_filter.replace("{username}", "john");
521        assert!(filter.contains("uid=john"));
522        assert!(filter.contains("mail=john"));
523    }
524
525    #[test]
526    fn test_complex_group_filter() {
527        let config = LdapConfig {
528            group_filter: Some(
529                "(&(objectClass=groupOfNames)(member=uid={username},ou=users,dc=example,dc=com))"
530                    .to_string(),
531            ),
532            ..Default::default()
533        };
534        let filter = config.group_filter.unwrap().replace("{username}", "jane");
535        assert!(filter.contains("uid=jane"));
536    }
537
538    #[test]
539    fn test_timeout_configuration() {
540        let config = LdapConfig {
541            timeout_secs: 60,
542            ..Default::default()
543        };
544        assert_eq!(config.timeout_secs, 60);
545    }
546
547    #[test]
548    fn test_ldaps_url() {
549        let config = LdapConfig {
550            server_url: "ldaps://secure-ldap.example.com:636".to_string(),
551            ..Default::default()
552        };
553        assert!(config.server_url.starts_with("ldaps://"));
554    }
555
556    #[tokio::test]
557    async fn test_cache_empty_on_creation() {
558        let backend = LdapBackend::new(LdapConfig::default());
559        let cache = backend.user_cache.read().await;
560        assert!(cache.is_empty());
561    }
562
563    #[tokio::test]
564    async fn test_cache_insertion() {
565        let backend = LdapBackend::new(LdapConfig::default());
566        backend
567            .user_cache
568            .write()
569            .await
570            .insert("user1".to_string(), true);
571        backend
572            .user_cache
573            .write()
574            .await
575            .insert("user2".to_string(), true);
576
577        let cache = backend.user_cache.read().await;
578        assert_eq!(cache.len(), 2);
579        assert!(cache.contains_key("user1"));
580        assert!(cache.contains_key("user2"));
581    }
582
583    #[test]
584    fn test_bind_dn_pattern() {
585        let config = LdapConfig {
586            bind_dn_pattern: Some("uid={username},ou=users,dc=example,dc=com".to_string()),
587            ..Default::default()
588        };
589        assert!(config.bind_dn_pattern.is_some());
590        let dn = config
591            .bind_dn_pattern
592            .unwrap()
593            .replace("{username}", "alice");
594        assert_eq!(dn, "uid=alice,ou=users,dc=example,dc=com");
595    }
596
597    #[test]
598    fn test_bind_dn_pattern_with_email() {
599        let config = LdapConfig {
600            bind_dn_pattern: Some(
601                "mail={username}@example.com,ou=users,dc=example,dc=com".to_string(),
602            ),
603            ..Default::default()
604        };
605        let dn = config.bind_dn_pattern.unwrap().replace("{username}", "bob");
606        assert_eq!(dn, "mail=bob@example.com,ou=users,dc=example,dc=com");
607    }
608
609    #[test]
610    fn test_tls_configuration() {
611        let config = LdapConfig {
612            use_tls: true,
613            server_url: "ldaps://ldap.example.com:636".to_string(),
614            ..Default::default()
615        };
616        assert!(config.use_tls);
617        assert!(config.server_url.starts_with("ldaps://"));
618    }
619
620    #[test]
621    fn test_tls_skip_verify_configuration() {
622        let config = LdapConfig {
623            use_tls: true,
624            tls_skip_verify: true,
625            ..Default::default()
626        };
627        assert!(config.use_tls);
628        assert!(config.tls_skip_verify);
629    }
630
631    #[test]
632    fn test_multiple_user_filter_patterns() {
633        let config = LdapConfig {
634            user_filter:
635                "(&(objectClass=inetOrgPerson)(|(uid={username})(mail={username}@example.com)))"
636                    .to_string(),
637            ..Default::default()
638        };
639        let filter = config.user_filter.replace("{username}", "charlie");
640        assert!(filter.contains("uid=charlie"));
641        assert!(filter.contains("mail=charlie@example.com"));
642    }
643
644    #[test]
645    fn test_memberof_group_filter() {
646        let config = LdapConfig {
647            group_filter: Some("(memberOf=cn=mail-users,ou=groups,dc=example,dc=com)".to_string()),
648            ..Default::default()
649        };
650        assert!(config.group_filter.is_some());
651    }
652
653    #[tokio::test]
654    async fn test_connection_pool_multiple_returns() {
655        let config = LdapConfig {
656            pool_size: 2,
657            ..Default::default()
658        };
659        let pool = ConnectionPool::new(config);
660
661        // Simulate returning connections
662        let connections = pool.connections.read().await;
663        assert_eq!(connections.len(), 0);
664    }
665
666    #[test]
667    fn test_ldap_config_clone() {
668        let config1 = LdapConfig {
669            server_url: "ldap://test.com:389".to_string(),
670            timeout_secs: 20,
671            ..Default::default()
672        };
673        let config2 = config1.clone();
674        assert_eq!(config1.server_url, config2.server_url);
675        assert_eq!(config1.timeout_secs, config2.timeout_secs);
676    }
677
678    #[test]
679    fn test_empty_group_base_dn_fallback() {
680        let config = LdapConfig {
681            base_dn: "dc=example,dc=org".to_string(),
682            group_base_dn: None,
683            ..Default::default()
684        };
685        let group_base = config.group_base_dn.as_ref().unwrap_or(&config.base_dn);
686        assert_eq!(group_base, "dc=example,dc=org");
687    }
688
689    #[test]
690    fn test_custom_timeout() {
691        let config = LdapConfig {
692            timeout_secs: 5,
693            ..Default::default()
694        };
695        assert_eq!(config.timeout_secs, 5);
696    }
697
698    #[test]
699    fn test_large_pool_size() {
700        let config = LdapConfig {
701            pool_size: 100,
702            ..Default::default()
703        };
704        assert_eq!(config.pool_size, 100);
705    }
706
707    #[tokio::test]
708    async fn test_cache_concurrent_access() {
709        let backend = LdapBackend::new(LdapConfig::default());
710
711        // Simulate concurrent writes
712        let backend1 = backend.clone();
713        let backend2 = backend.clone();
714
715        let handle1 = tokio::spawn(async move {
716            backend1
717                .user_cache
718                .write()
719                .await
720                .insert("user_a".to_string(), true);
721        });
722
723        let handle2 = tokio::spawn(async move {
724            backend2
725                .user_cache
726                .write()
727                .await
728                .insert("user_b".to_string(), true);
729        });
730
731        let _ = handle1.await;
732        let _ = handle2.await;
733
734        let cache = backend.user_cache.read().await;
735        assert!(cache.len() <= 2);
736    }
737
738    #[test]
739    fn test_ldap_url_with_port() {
740        let config = LdapConfig {
741            server_url: "ldap://ldap.company.com:389".to_string(),
742            ..Default::default()
743        };
744        assert!(config.server_url.contains(":389"));
745    }
746
747    #[test]
748    fn test_ldaps_url_with_port() {
749        let config = LdapConfig {
750            server_url: "ldaps://ldap.company.com:636".to_string(),
751            ..Default::default()
752        };
753        assert!(config.server_url.contains(":636"));
754    }
755
756    #[test]
757    fn test_active_directory_user_filter() {
758        let config = LdapConfig {
759            user_filter: "(&(objectClass=user)(sAMAccountName={username}))".to_string(),
760            ..Default::default()
761        };
762        let filter = config.user_filter.replace("{username}", "jdoe");
763        assert!(filter.contains("sAMAccountName=jdoe"));
764    }
765
766    #[test]
767    fn test_active_directory_group_filter() {
768        let config = LdapConfig {
769            group_filter: Some("(&(objectClass=group)(member={dn}))".to_string()),
770            ..Default::default()
771        };
772        assert!(config.group_filter.is_some());
773    }
774
775    #[test]
776    fn test_posix_group_filter() {
777        let config = LdapConfig {
778            group_filter: Some("(&(objectClass=posixGroup)(memberUid={username}))".to_string()),
779            ..Default::default()
780        };
781        let filter = config
782            .group_filter
783            .unwrap()
784            .replace("{username}", "user123");
785        assert!(filter.contains("memberUid=user123"));
786    }
787
788    #[tokio::test]
789    async fn test_ldap_backend_clone() {
790        let backend1 = LdapBackend::new(LdapConfig::default());
791        let backend2 = backend1.clone();
792
793        // Both should share the same cache
794        backend1
795            .user_cache
796            .write()
797            .await
798            .insert("test".to_string(), true);
799        let cache2 = backend2.user_cache.read().await;
800        assert!(cache2.contains_key("test"));
801    }
802}