QuickDID is a high-performance AT Protocol identity resolution service written in Rust. It provides handle-to-DID resolution with Redis-backed caching and queue processing.
1//! Redis-backed caching handle resolver.
2//!
3//! This module provides a handle resolver that caches resolution results in Redis
4//! with configurable expiration times. Redis caching provides persistence across
5//! service restarts and allows sharing of cached results across multiple instances.
6
7use super::errors::HandleResolverError;
8use super::traits::HandleResolver;
9use crate::handle_resolution_result::HandleResolutionResult;
10use crate::metrics::SharedMetricsPublisher;
11use async_trait::async_trait;
12use atproto_identity::resolve::{InputType, parse_input};
13use deadpool_redis::{Pool as RedisPool, redis::AsyncCommands};
14use metrohash::MetroHash64;
15use std::hash::Hasher as _;
16use std::sync::Arc;
17
18/// Redis-backed caching handle resolver.
19///
20/// This resolver caches handle resolution results in Redis with a configurable TTL.
21/// Results are stored in a compact binary format using bincode serialization
22/// to minimize storage overhead.
23///
24/// # Features
25///
26/// - Persistent caching across service restarts
27/// - Shared cache across multiple service instances
28/// - Configurable TTL (default: 90 days)
29/// - Compact binary storage format
30/// - Graceful fallback if Redis is unavailable
31///
32/// # Example
33///
34/// ```no_run
35/// use std::sync::Arc;
36/// use deadpool_redis::Pool;
37/// use quickdid::handle_resolver::{create_base_resolver, create_redis_resolver, HandleResolver};
38/// use quickdid::metrics::NoOpMetricsPublisher;
39///
40/// # async fn example() {
41/// # use atproto_identity::resolve::HickoryDnsResolver;
42/// # use reqwest::Client;
43/// # let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[]));
44/// # let http_client = Client::new();
45/// # let metrics = Arc::new(NoOpMetricsPublisher);
46/// # let base_resolver = create_base_resolver(dns_resolver, http_client, metrics.clone());
47/// # let redis_pool: Pool = todo!();
48/// // Create with default 90-day TTL
49/// let resolver = create_redis_resolver(
50/// base_resolver,
51/// redis_pool,
52/// metrics
53/// );
54/// # }
55/// ```
56pub(super) struct RedisHandleResolver {
57 /// Base handle resolver to perform actual resolution
58 inner: Arc<dyn HandleResolver>,
59 /// Redis connection pool
60 pool: RedisPool,
61 /// Redis key prefix for handle resolution cache
62 key_prefix: String,
63 /// TTL for cache entries in seconds
64 ttl_seconds: u64,
65 /// Metrics publisher for telemetry
66 metrics: SharedMetricsPublisher,
67}
68
69impl RedisHandleResolver {
70 /// Create a new Redis-backed handle resolver with default 90-day TTL.
71 fn new(
72 inner: Arc<dyn HandleResolver>,
73 pool: RedisPool,
74 metrics: SharedMetricsPublisher,
75 ) -> Self {
76 Self::with_ttl(inner, pool, 90 * 24 * 60 * 60, metrics) // 90 days default
77 }
78
79 /// Create a new Redis-backed handle resolver with custom TTL.
80 fn with_ttl(
81 inner: Arc<dyn HandleResolver>,
82 pool: RedisPool,
83 ttl_seconds: u64,
84 metrics: SharedMetricsPublisher,
85 ) -> Self {
86 Self::with_full_config(inner, pool, "handle:".to_string(), ttl_seconds, metrics)
87 }
88
89 /// Create a new Redis-backed handle resolver with full configuration.
90 fn with_full_config(
91 inner: Arc<dyn HandleResolver>,
92 pool: RedisPool,
93 key_prefix: String,
94 ttl_seconds: u64,
95 metrics: SharedMetricsPublisher,
96 ) -> Self {
97 Self {
98 inner,
99 pool,
100 key_prefix,
101 ttl_seconds,
102 metrics,
103 }
104 }
105
106 /// Generate the Redis key for a handle.
107 ///
108 /// Uses MetroHash64 to generate a consistent hash of the handle
109 /// for use as the Redis key. This provides better key distribution
110 /// and avoids issues with special characters in handles.
111 fn make_key(&self, handle: &str) -> String {
112 let mut h = MetroHash64::default();
113 h.write(handle.as_bytes());
114 format!("{}{}", self.key_prefix, h.finish())
115 }
116
117 /// Get the TTL in seconds.
118 fn ttl_seconds(&self) -> u64 {
119 self.ttl_seconds
120 }
121
122 /// Purge a handle and its associated DID from the cache.
123 ///
124 /// This method removes both the handle->DID mapping and the reverse DID->handle mapping.
125 async fn purge_handle(&self, handle: &str) -> Result<(), HandleResolverError> {
126 let handle_key = self.make_key(handle);
127
128 match self.pool.get().await {
129 Ok(mut conn) => {
130 // First, try to get the cached result to find the associated DID
131 let cached: Option<Vec<u8>> = match conn.get(&handle_key).await {
132 Ok(value) => value,
133 Err(e) => {
134 tracing::warn!("Failed to get handle from Redis for purging: {}", e);
135 self.metrics.incr("resolver.redis.purge_get_error").await;
136 None
137 }
138 };
139
140 // If we found a cached result, extract the DID and delete both keys
141 if let Some(cached_bytes) = cached {
142 if let Ok(cached_result) = HandleResolutionResult::from_bytes(&cached_bytes) {
143 if let Some(did) = cached_result.to_did() {
144 let did_key = self.make_key(&did);
145
146 // Delete both the handle key and the DID key
147 let _: Result<(), _> = conn.del(&[&handle_key, &did_key]).await;
148
149 tracing::debug!("Purged handle {} and associated DID {}", handle, did);
150 self.metrics
151 .incr("resolver.redis.purge_handle_success")
152 .await;
153 } else {
154 // Just delete the handle key if no DID was resolved
155 let _: Result<(), _> = conn.del(&handle_key).await;
156 tracing::debug!("Purged unresolved handle {}", handle);
157 self.metrics
158 .incr("resolver.redis.purge_handle_unresolved")
159 .await;
160 }
161 } else {
162 // If we can't deserialize, just delete the handle key
163 let _: Result<(), _> = conn.del(&handle_key).await;
164 tracing::warn!("Purged handle {} with undeserializable data", handle);
165 self.metrics
166 .incr("resolver.redis.purge_handle_corrupt")
167 .await;
168 }
169 } else {
170 tracing::debug!("Handle {} not found in cache for purging", handle);
171 self.metrics
172 .incr("resolver.redis.purge_handle_not_found")
173 .await;
174 }
175
176 Ok(())
177 }
178 Err(e) => {
179 tracing::warn!("Failed to get Redis connection for purging: {}", e);
180 self.metrics
181 .incr("resolver.redis.purge_connection_error")
182 .await;
183 Err(HandleResolverError::ResolutionFailed(format!(
184 "Redis connection error: {}",
185 e
186 )))
187 }
188 }
189 }
190
191 /// Purge a DID and its associated handle from the cache.
192 ///
193 /// This method removes both the DID->handle mapping and the handle->DID mapping.
194 async fn purge_did(&self, did: &str) -> Result<(), HandleResolverError> {
195 let did_key = self.make_key(did);
196
197 match self.pool.get().await {
198 Ok(mut conn) => {
199 // First, try to get the associated handle from the reverse mapping
200 let handle_bytes: Option<Vec<u8>> = match conn.get(&did_key).await {
201 Ok(value) => value,
202 Err(e) => {
203 tracing::warn!("Failed to get DID from Redis for purging: {}", e);
204 self.metrics.incr("resolver.redis.purge_get_error").await;
205 None
206 }
207 };
208
209 // If we found a handle, delete both keys
210 if let Some(handle_bytes) = handle_bytes {
211 if let Ok(handle) = String::from_utf8(handle_bytes) {
212 let handle_key = self.make_key(&handle);
213
214 // Delete both the DID key and the handle key
215 let _: Result<(), _> = conn.del(&[&did_key, &handle_key]).await;
216
217 tracing::debug!("Purged DID {} and associated handle {}", did, handle);
218 self.metrics.incr("resolver.redis.purge_did_success").await;
219 } else {
220 // If we can't parse the handle, just delete the DID key
221 let _: Result<(), _> = conn.del(&did_key).await;
222 tracing::warn!("Purged DID {} with unparseable handle data", did);
223 self.metrics.incr("resolver.redis.purge_did_corrupt").await;
224 }
225 } else {
226 tracing::debug!("DID {} not found in cache for purging", did);
227 self.metrics
228 .incr("resolver.redis.purge_did_not_found")
229 .await;
230 }
231
232 Ok(())
233 }
234 Err(e) => {
235 tracing::warn!("Failed to get Redis connection for purging: {}", e);
236 self.metrics
237 .incr("resolver.redis.purge_connection_error")
238 .await;
239 Err(HandleResolverError::ResolutionFailed(format!(
240 "Redis connection error: {}",
241 e
242 )))
243 }
244 }
245 }
246}
247
248#[async_trait]
249impl HandleResolver for RedisHandleResolver {
250 async fn resolve(&self, s: &str) -> Result<(String, u64), HandleResolverError> {
251 let handle = s.to_string();
252 let key = self.make_key(&handle);
253
254 // Try to get from Redis cache first
255 match self.pool.get().await {
256 Ok(mut conn) => {
257 // Check if the key exists in Redis (stored as binary data)
258 let cached: Option<Vec<u8>> = match conn.get(&key).await {
259 Ok(value) => value,
260 Err(e) => {
261 self.metrics.incr("resolver.redis.get_error").await;
262 tracing::warn!("Failed to get handle from Redis cache: {}", e);
263 None
264 }
265 };
266
267 if let Some(cached_bytes) = cached {
268 // Deserialize the cached result
269 match HandleResolutionResult::from_bytes(&cached_bytes) {
270 Ok(cached_result) => {
271 if let Some(did) = cached_result.to_did() {
272 tracing::debug!("Cache hit for handle {}: {}", handle, did);
273 self.metrics.incr("resolver.redis.cache_hit").await;
274 return Ok((did, cached_result.timestamp));
275 } else {
276 tracing::debug!("Cache hit (not resolved) for handle {}", handle);
277 self.metrics
278 .incr("resolver.redis.cache_hit_not_resolved")
279 .await;
280 return Err(HandleResolverError::HandleNotFound);
281 }
282 }
283 Err(e) => {
284 tracing::warn!(
285 "Failed to deserialize cached result for handle {}: {}",
286 handle,
287 e
288 );
289 self.metrics.incr("resolver.redis.deserialize_error").await;
290 // Fall through to re-resolve if deserialization fails
291 }
292 }
293 }
294
295 // Not in cache, resolve through inner resolver
296 tracing::debug!("Cache miss for handle {}, resolving...", handle);
297 self.metrics.incr("resolver.redis.cache_miss").await;
298 let result = self.inner.resolve(s).await;
299
300 // Create and serialize resolution result
301 let resolution_result = match &result {
302 Ok((did, _timestamp)) => {
303 tracing::debug!(
304 "Caching successful resolution for handle {}: {}",
305 handle,
306 did
307 );
308 match HandleResolutionResult::success(did) {
309 Ok(res) => res,
310 Err(e) => {
311 tracing::warn!("Failed to create resolution result: {}", e);
312 self.metrics
313 .incr("resolver.redis.result_create_error")
314 .await;
315 return result;
316 }
317 }
318 }
319 Err(e) => {
320 tracing::debug!("Caching failed resolution for handle {}: {}", handle, e);
321 match HandleResolutionResult::not_resolved() {
322 Ok(res) => res,
323 Err(err) => {
324 tracing::warn!("Failed to create not_resolved result: {}", err);
325 self.metrics
326 .incr("resolver.redis.result_create_error")
327 .await;
328 return result;
329 }
330 }
331 }
332 };
333
334 // Serialize to bytes
335 match resolution_result.to_bytes() {
336 Ok(bytes) => {
337 // Set with expiration (ignore errors to not fail the resolution)
338 if let Err(e) = conn
339 .set_ex::<_, _, ()>(&key, bytes, self.ttl_seconds())
340 .await
341 {
342 tracing::warn!("Failed to cache handle resolution in Redis: {}", e);
343 self.metrics.incr("resolver.redis.cache_set_error").await;
344 } else {
345 self.metrics.incr("resolver.redis.cache_set").await;
346
347 // For successful resolutions, also store reverse DID -> handle mapping
348 if let Ok((did, _)) = &result {
349 let did_key = self.make_key(did);
350 if let Err(e) = conn
351 .set_ex::<_, _, ()>(
352 &did_key,
353 handle.as_bytes(),
354 self.ttl_seconds(),
355 )
356 .await
357 {
358 tracing::warn!(
359 "Failed to cache reverse DID->handle mapping in Redis: {}",
360 e
361 );
362 self.metrics
363 .incr("resolver.redis.reverse_cache_set_error")
364 .await;
365 } else {
366 tracing::debug!(
367 "Cached reverse mapping for DID {}: {}",
368 did,
369 handle
370 );
371 self.metrics.incr("resolver.redis.reverse_cache_set").await;
372 }
373 }
374 }
375 }
376 Err(e) => {
377 tracing::warn!(
378 "Failed to serialize resolution result for handle {}: {}",
379 handle,
380 e
381 );
382 self.metrics.incr("resolver.redis.serialize_error").await;
383 }
384 }
385
386 result
387 }
388 Err(e) => {
389 // Redis connection failed, fall back to inner resolver
390 tracing::warn!(
391 "Failed to get Redis connection, falling back to uncached resolution: {}",
392 e
393 );
394 self.metrics.incr("resolver.redis.connection_error").await;
395 self.inner.resolve(s).await
396 }
397 }
398 }
399
400 async fn purge(&self, subject: &str) -> Result<(), HandleResolverError> {
401 // Use atproto_identity's parse_input to properly identify the input type
402 let parsed_input = parse_input(subject)
403 .map_err(|_| HandleResolverError::InvalidSubject(subject.to_string()))?;
404 match parsed_input {
405 InputType::Handle(handle) => {
406 // It's a handle, purge using the lowercase version
407 self.purge_handle(&handle.to_lowercase()).await
408 }
409 InputType::Plc(did) | InputType::Web(did) => {
410 // It's a DID, purge the DID
411 self.purge_did(&did).await
412 }
413 }
414 }
415
416 async fn set(&self, handle: &str, did: &str) -> Result<(), HandleResolverError> {
417 // Normalize the handle to lowercase
418 let handle = handle.to_lowercase();
419 let handle_key = self.make_key(&handle);
420 let did_key = self.make_key(did);
421
422 match self.pool.get().await {
423 Ok(mut conn) => {
424 // Create a resolution result for the successful mapping
425 let resolution_result = match HandleResolutionResult::success(did) {
426 Ok(res) => res,
427 Err(e) => {
428 tracing::warn!(
429 "Failed to create resolution result for set operation: {}",
430 e
431 );
432 self.metrics
433 .incr("resolver.redis.set_result_create_error")
434 .await;
435 return Err(HandleResolverError::InvalidSubject(format!(
436 "Failed to create resolution result: {}",
437 e
438 )));
439 }
440 };
441
442 // Serialize to bytes
443 match resolution_result.to_bytes() {
444 Ok(bytes) => {
445 // Set the handle -> DID mapping with expiration
446 if let Err(e) = conn
447 .set_ex::<_, _, ()>(&handle_key, bytes, self.ttl_seconds())
448 .await
449 {
450 tracing::warn!("Failed to set handle->DID mapping in Redis: {}", e);
451 self.metrics.incr("resolver.redis.set_cache_error").await;
452 return Err(HandleResolverError::ResolutionFailed(format!(
453 "Failed to set cache: {}",
454 e
455 )));
456 }
457
458 // Set the reverse DID -> handle mapping
459 if let Err(e) = conn
460 .set_ex::<_, _, ()>(&did_key, handle.as_bytes(), self.ttl_seconds())
461 .await
462 {
463 tracing::warn!("Failed to set DID->handle mapping in Redis: {}", e);
464 self.metrics
465 .incr("resolver.redis.set_reverse_cache_error")
466 .await;
467 // Don't fail the operation, but log the warning
468 }
469
470 tracing::debug!("Set handle {} -> DID {} mapping in cache", handle, did);
471 self.metrics.incr("resolver.redis.set_success").await;
472 Ok(())
473 }
474 Err(e) => {
475 tracing::warn!(
476 "Failed to serialize resolution result for set operation: {}",
477 e
478 );
479 self.metrics
480 .incr("resolver.redis.set_serialize_error")
481 .await;
482 Err(HandleResolverError::InvalidSubject(format!(
483 "Failed to serialize: {}",
484 e
485 )))
486 }
487 }
488 }
489 Err(e) => {
490 tracing::warn!("Failed to get Redis connection for set operation: {}", e);
491 self.metrics
492 .incr("resolver.redis.set_connection_error")
493 .await;
494 Err(HandleResolverError::ResolutionFailed(format!(
495 "Redis connection error: {}",
496 e
497 )))
498 }
499 }
500 }
501}
502
503/// Create a new Redis-backed handle resolver with default 90-day TTL.
504///
505/// # Arguments
506///
507/// * `inner` - The underlying resolver to use for actual resolution
508/// * `pool` - Redis connection pool
509/// * `metrics` - Metrics publisher for telemetry
510///
511/// # Example
512///
513/// ```no_run
514/// use std::sync::Arc;
515/// use quickdid::handle_resolver::{create_base_resolver, create_redis_resolver, HandleResolver};
516/// use quickdid::cache::create_redis_pool;
517/// use quickdid::metrics::NoOpMetricsPublisher;
518///
519/// # async fn example() -> anyhow::Result<()> {
520/// # use atproto_identity::resolve::HickoryDnsResolver;
521/// # use reqwest::Client;
522/// # let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[]));
523/// # let http_client = Client::new();
524/// # let metrics = Arc::new(NoOpMetricsPublisher);
525/// let base = create_base_resolver(
526/// dns_resolver,
527/// http_client,
528/// metrics.clone(),
529/// );
530///
531/// let pool = create_redis_pool("redis://localhost:6379")?;
532/// let resolver = create_redis_resolver(base, pool, metrics);
533/// let (did, timestamp) = resolver.resolve("alice.bsky.social").await.unwrap();
534/// # Ok(())
535/// # }
536/// ```
537pub fn create_redis_resolver(
538 inner: Arc<dyn HandleResolver>,
539 pool: RedisPool,
540 metrics: SharedMetricsPublisher,
541) -> Arc<dyn HandleResolver> {
542 Arc::new(RedisHandleResolver::new(inner, pool, metrics))
543}
544
545/// Create a new Redis-backed handle resolver with custom TTL.
546///
547/// # Arguments
548///
549/// * `inner` - The underlying resolver to use for actual resolution
550/// * `pool` - Redis connection pool
551/// * `ttl_seconds` - TTL for cache entries in seconds
552/// * `metrics` - Metrics publisher for telemetry
553pub fn create_redis_resolver_with_ttl(
554 inner: Arc<dyn HandleResolver>,
555 pool: RedisPool,
556 ttl_seconds: u64,
557 metrics: SharedMetricsPublisher,
558) -> Arc<dyn HandleResolver> {
559 Arc::new(RedisHandleResolver::with_ttl(
560 inner,
561 pool,
562 ttl_seconds,
563 metrics,
564 ))
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570
571 // Mock handle resolver for testing
572 #[derive(Clone)]
573 struct MockHandleResolver {
574 should_fail: bool,
575 expected_did: String,
576 }
577
578 #[async_trait]
579 impl HandleResolver for MockHandleResolver {
580 async fn resolve(&self, _handle: &str) -> Result<(String, u64), HandleResolverError> {
581 if self.should_fail {
582 Err(HandleResolverError::MockResolutionFailure)
583 } else {
584 let timestamp = std::time::SystemTime::now()
585 .duration_since(std::time::UNIX_EPOCH)
586 .unwrap_or_default()
587 .as_secs();
588 Ok((self.expected_did.clone(), timestamp))
589 }
590 }
591 }
592
593 #[tokio::test]
594 async fn test_redis_handle_resolver_cache_hit() {
595 let pool = match crate::test_helpers::get_test_redis_pool() {
596 Some(p) => p,
597 None => return,
598 };
599
600 // Create mock resolver
601 let mock_resolver = Arc::new(MockHandleResolver {
602 should_fail: false,
603 expected_did: "did:plc:testuser123".to_string(),
604 });
605
606 // Create metrics publisher
607 let metrics = Arc::new(crate::metrics::NoOpMetricsPublisher);
608
609 // Create Redis-backed resolver with a unique key prefix for testing
610 let test_prefix = format!(
611 "test:handle:{}:",
612 std::time::SystemTime::now()
613 .duration_since(std::time::UNIX_EPOCH)
614 .unwrap()
615 .as_nanos()
616 );
617 let redis_resolver = RedisHandleResolver::with_full_config(
618 mock_resolver,
619 pool.clone(),
620 test_prefix.clone(),
621 3600,
622 metrics,
623 );
624
625 let test_handle = "alice.bsky.social";
626
627 // First resolution - should call inner resolver
628 let (result1, _timestamp1) = redis_resolver.resolve(test_handle).await.unwrap();
629 assert_eq!(result1, "did:plc:testuser123");
630
631 // Second resolution - should hit cache
632 let (result2, _timestamp2) = redis_resolver.resolve(test_handle).await.unwrap();
633 assert_eq!(result2, "did:plc:testuser123");
634
635 // Clean up test data
636 if let Ok(mut conn) = pool.get().await {
637 // Need to compute the hashed key like RedisHandleResolver does
638 let mut h = MetroHash64::default();
639 h.write(test_handle.as_bytes());
640 let key = format!("{}{}", test_prefix, h.finish());
641 let _: Result<(), _> = conn.del(key).await;
642 }
643 }
644
645 #[tokio::test]
646 async fn test_redis_handle_resolver_bidirectional_purge() {
647 let pool = match crate::test_helpers::get_test_redis_pool() {
648 Some(p) => p,
649 None => return,
650 };
651
652 // Create mock resolver
653 let mock_resolver = Arc::new(MockHandleResolver {
654 should_fail: false,
655 expected_did: "did:plc:testuser456".to_string(),
656 });
657
658 // Create metrics publisher
659 let metrics = Arc::new(crate::metrics::NoOpMetricsPublisher);
660
661 // Create Redis-backed resolver with a unique key prefix for testing
662 let test_prefix = format!(
663 "test:handle:{}:",
664 std::time::SystemTime::now()
665 .duration_since(std::time::UNIX_EPOCH)
666 .unwrap()
667 .as_nanos()
668 );
669 let redis_resolver = RedisHandleResolver::with_full_config(
670 mock_resolver,
671 pool.clone(),
672 test_prefix.clone(),
673 3600,
674 metrics,
675 );
676
677 let test_handle = "bob.bsky.social";
678 let expected_did = "did:plc:testuser456";
679
680 // First resolution - should call inner resolver and cache both directions
681 let (result1, _) = redis_resolver.resolve(test_handle).await.unwrap();
682 assert_eq!(result1, expected_did);
683
684 // Verify both keys exist in Redis
685 if let Ok(mut conn) = pool.get().await {
686 let mut h = MetroHash64::default();
687 h.write(test_handle.as_bytes());
688 let handle_key = format!("{}{}", test_prefix, h.finish());
689
690 let mut h2 = MetroHash64::default();
691 h2.write(expected_did.as_bytes());
692 let did_key = format!("{}{}", test_prefix, h2.finish());
693
694 // Check handle -> DID mapping exists
695 let handle_exists: bool = conn.exists(&handle_key).await.unwrap();
696 assert!(handle_exists, "Handle key should exist in cache");
697
698 // Check DID -> handle mapping exists
699 let did_exists: bool = conn.exists(&did_key).await.unwrap();
700 assert!(did_exists, "DID key should exist in cache");
701
702 // Test purge by handle using the trait method
703 redis_resolver.purge(test_handle).await.unwrap();
704
705 // Verify both keys were deleted
706 let handle_exists_after: bool = conn.exists(&handle_key).await.unwrap();
707 assert!(
708 !handle_exists_after,
709 "Handle key should be deleted after purge"
710 );
711
712 let did_exists_after: bool = conn.exists(&did_key).await.unwrap();
713 assert!(!did_exists_after, "DID key should be deleted after purge");
714 }
715
716 // Re-resolve to cache again
717 let (result2, _) = redis_resolver.resolve(test_handle).await.unwrap();
718 assert_eq!(result2, expected_did);
719
720 // Test purge by DID using the trait method
721 redis_resolver.purge(expected_did).await.unwrap();
722
723 // Verify both keys were deleted again
724 if let Ok(mut conn) = pool.get().await {
725 let mut h = MetroHash64::default();
726 h.write(test_handle.as_bytes());
727 let handle_key = format!("{}{}", test_prefix, h.finish());
728
729 let mut h2 = MetroHash64::default();
730 h2.write(expected_did.as_bytes());
731 let did_key = format!("{}{}", test_prefix, h2.finish());
732
733 let handle_exists: bool = conn.exists(&handle_key).await.unwrap();
734 assert!(
735 !handle_exists,
736 "Handle key should be deleted after DID purge"
737 );
738
739 let did_exists: bool = conn.exists(&did_key).await.unwrap();
740 assert!(!did_exists, "DID key should be deleted after DID purge");
741 }
742 }
743
744 #[tokio::test]
745 async fn test_redis_handle_resolver_purge_input_types() {
746 let pool = match crate::test_helpers::get_test_redis_pool() {
747 Some(p) => p,
748 None => return,
749 };
750
751 // Create mock resolver
752 let mock_resolver = Arc::new(MockHandleResolver {
753 should_fail: false,
754 expected_did: "did:plc:testuser789".to_string(),
755 });
756
757 // Create metrics publisher
758 let metrics = Arc::new(crate::metrics::NoOpMetricsPublisher);
759
760 // Create Redis-backed resolver with a unique key prefix for testing
761 let test_prefix = format!(
762 "test:handle:{}:",
763 std::time::SystemTime::now()
764 .duration_since(std::time::UNIX_EPOCH)
765 .unwrap()
766 .as_nanos()
767 );
768 let redis_resolver = RedisHandleResolver::with_full_config(
769 mock_resolver,
770 pool.clone(),
771 test_prefix.clone(),
772 3600,
773 metrics,
774 );
775
776 // Test different input formats
777 let test_cases = vec![
778 ("alice.bsky.social", "alice.bsky.social"), // Handle
779 ("ALICE.BSKY.SOCIAL", "alice.bsky.social"), // Handle (uppercase)
780 ("did:plc:abc123", "did:plc:abc123"), // PLC DID
781 ("did:web:example.com", "did:web:example.com"), // Web DID
782 ];
783
784 for (input, expected_key) in test_cases {
785 // Resolve first to cache it
786 if !input.starts_with("did:") {
787 let _ = redis_resolver.resolve(input).await;
788 }
789
790 // Test purging with different input formats
791 let result = redis_resolver.purge(input).await;
792 assert!(result.is_ok(), "Failed to purge {}: {:?}", input, result);
793
794 // Verify the key was handled correctly based on type
795 if let Ok(mut conn) = pool.get().await {
796 let mut h = MetroHash64::default();
797 h.write(expected_key.as_bytes());
798 let key = format!("{}{}", test_prefix, h.finish());
799
800 // After purge, key should not exist
801 let exists: bool = conn.exists(&key).await.unwrap_or(false);
802 assert!(!exists, "Key for {} should not exist after purge", input);
803 }
804 }
805 }
806
807 #[tokio::test]
808 async fn test_redis_handle_resolver_set_method() {
809 let pool = match crate::test_helpers::get_test_redis_pool() {
810 Some(p) => p,
811 None => return,
812 };
813
814 // Create mock resolver
815 let mock_resolver = Arc::new(MockHandleResolver {
816 should_fail: false,
817 expected_did: "did:plc:old".to_string(),
818 });
819
820 // Create metrics publisher
821 let metrics = Arc::new(crate::metrics::NoOpMetricsPublisher);
822
823 // Create Redis-backed resolver with a unique key prefix for testing
824 let test_prefix = format!(
825 "test:handle:{}:",
826 std::time::SystemTime::now()
827 .duration_since(std::time::UNIX_EPOCH)
828 .unwrap()
829 .as_nanos()
830 );
831 let redis_resolver = RedisHandleResolver::with_full_config(
832 mock_resolver,
833 pool.clone(),
834 test_prefix.clone(),
835 3600,
836 metrics,
837 );
838
839 let test_handle = "charlie.bsky.social";
840 let test_did = "did:plc:newuser123";
841
842 // Set the mapping using the trait method
843 redis_resolver.set(test_handle, test_did).await.unwrap();
844
845 // Verify the mapping by resolving the handle
846 let (resolved_did, _) = redis_resolver.resolve(test_handle).await.unwrap();
847 assert_eq!(resolved_did, test_did);
848
849 // Test that uppercase handles are normalized
850 redis_resolver
851 .set("DAVE.BSKY.SOCIAL", "did:plc:dave456")
852 .await
853 .unwrap();
854 let (resolved_did2, _) = redis_resolver.resolve("dave.bsky.social").await.unwrap();
855 assert_eq!(resolved_did2, "did:plc:dave456");
856
857 // Verify both forward and reverse mappings exist
858 if let Ok(mut conn) = pool.get().await {
859 let mut h = MetroHash64::default();
860 h.write(test_handle.as_bytes());
861 let handle_key = format!("{}{}", test_prefix, h.finish());
862
863 let mut h2 = MetroHash64::default();
864 h2.write(test_did.as_bytes());
865 let did_key = format!("{}{}", test_prefix, h2.finish());
866
867 // Check both keys exist
868 let handle_exists: bool = conn.exists(&handle_key).await.unwrap();
869 assert!(handle_exists, "Handle key should exist after set");
870
871 let did_exists: bool = conn.exists(&did_key).await.unwrap();
872 assert!(did_exists, "DID key should exist after set");
873
874 // Clean up test data
875 let _: Result<(), _> = conn.del(&[&handle_key, &did_key]).await;
876 }
877 }
878
879 #[tokio::test]
880 async fn test_redis_handle_resolver_cache_error() {
881 let pool = match crate::test_helpers::get_test_redis_pool() {
882 Some(p) => p,
883 None => return,
884 };
885
886 // Create mock resolver that fails
887 let mock_resolver = Arc::new(MockHandleResolver {
888 should_fail: true,
889 expected_did: String::new(),
890 });
891
892 // Create metrics publisher
893 let metrics = Arc::new(crate::metrics::NoOpMetricsPublisher);
894
895 // Create Redis-backed resolver with a unique key prefix for testing
896 let test_prefix = format!(
897 "test:handle:{}:",
898 std::time::SystemTime::now()
899 .duration_since(std::time::UNIX_EPOCH)
900 .unwrap()
901 .as_nanos()
902 );
903 let redis_resolver = RedisHandleResolver::with_full_config(
904 mock_resolver,
905 pool.clone(),
906 test_prefix.clone(),
907 3600,
908 metrics,
909 );
910
911 let test_handle = "error.bsky.social";
912
913 // First resolution - should fail and cache empty string
914 let result1 = redis_resolver.resolve(test_handle).await;
915 assert!(result1.is_err());
916
917 // Second resolution - should hit cache with error
918 let result2 = redis_resolver.resolve(test_handle).await;
919 assert!(result2.is_err());
920
921 // Clean up test data
922 if let Ok(mut conn) = pool.get().await {
923 // Need to compute the hashed key like RedisHandleResolver does
924 let mut h = MetroHash64::default();
925 h.write(test_handle.as_bytes());
926 let key = format!("{}{}", test_prefix, h.finish());
927 let _: Result<(), _> = conn.del(key).await;
928 }
929 }
930}