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.
at main 930 lines 37 kB view raw
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}