1mod bundle;
9mod rate_limit;
10mod secondary;
11mod url;
12
13pub use bundle::{RedisOptions, RedisRustAuthOptions, RedisRustAuthStores, RedisStores};
14pub use rate_limit::{RedisRateLimitOptions, RedisRateLimitStore};
15pub use secondary::{RedisSecondaryStorage, RedisSecondaryStorageOptions};
16
17pub const VERSION: &str = env!("CARGO_PKG_VERSION");
19
20pub(crate) async fn connect_manager(
21 redis_url: &str,
22) -> Result<redis::aio::ConnectionManager, rustauth_core::error::RustAuthError> {
23 let redis_url = url::normalize_redis_url(redis_url);
24 let client = redis::Client::open(redis_url.as_ref())
25 .map_err(|error| rustauth_core::error::RustAuthError::Adapter(error.to_string()))?;
26 redis::aio::ConnectionManager::new(client)
27 .await
28 .map_err(|error| rustauth_core::error::RustAuthError::Adapter(error.to_string()))
29}
30
31#[cfg(test)]
32mod tests {
33 use super::*;
34
35 #[test]
36 fn normalizes_valkey_urls_to_redis_urls() {
37 assert_eq!(
38 url::normalize_redis_url("valkey://localhost:6379").as_ref(),
39 "redis://localhost:6379"
40 );
41 assert_eq!(
42 url::normalize_redis_url("valkeys://localhost:6380").as_ref(),
43 "rediss://localhost:6380"
44 );
45 }
46
47 #[test]
48 fn leaves_non_valkey_urls_unchanged() {
49 assert_eq!(
50 url::normalize_redis_url("redis://localhost:6379").as_ref(),
51 "redis://localhost:6379"
52 );
53 assert_eq!(
54 url::normalize_redis_url("rediss://localhost:6380").as_ref(),
55 "rediss://localhost:6380"
56 );
57 assert_eq!(
58 url::normalize_redis_url("unix:///tmp/redis.sock").as_ref(),
59 "unix:///tmp/redis.sock"
60 );
61 }
62
63 #[test]
64 fn rate_limit_script_uses_current_hash_set_command() {
65 use crate::rate_limit::RATE_LIMIT_SCRIPT;
66
67 assert!(RATE_LIMIT_SCRIPT.contains("HSET"));
68 assert!(!RATE_LIMIT_SCRIPT.contains("HMSET"));
69 }
70
71 #[test]
72 fn rate_limit_script_resets_only_after_window_elapses() {
73 use crate::rate_limit::RATE_LIMIT_SCRIPT;
74
75 assert!(RATE_LIMIT_SCRIPT.contains("(now - last_request) > window"));
76 assert!(!RATE_LIMIT_SCRIPT.contains("(now - last_request) >= window"));
77 }
78
79 #[test]
80 fn scan_pattern_escapes_redis_glob_metacharacters() {
81 use crate::url::secondary_storage_scan_pattern;
82
83 assert_eq!(
84 secondary_storage_scan_pattern(r"tenant:*?[]\:"),
85 r"tenant:\*\?\[\]\\:*"
86 );
87 }
88
89 #[test]
90 fn secondary_storage_uses_separate_key_namespace() {
91 let options = RedisSecondaryStorageOptions {
92 key_prefix: "test:".to_owned(),
93 scan_count: 100,
94 };
95 let key = format!("{}secondary:{}", options.key_prefix, "session:token");
96
97 assert_eq!(key, "test:secondary:session:token");
98 }
99
100 #[cfg(any(feature = "rustls", feature = "native-tls"))]
101 #[test]
102 fn tls_urls_open_as_tls_connections() -> Result<(), redis::RedisError> {
103 for url in ["rediss://localhost:6379", "valkeys://localhost:6380"] {
104 let client = redis::Client::open(url::normalize_redis_url(url).as_ref())?;
105 assert!(
106 matches!(
107 client.get_connection_info().addr,
108 redis::ConnectionAddr::TcpTls { .. }
109 ),
110 "{url} should open as a TLS connection"
111 );
112 }
113 Ok(())
114 }
115}