Skip to main content

fastmcp_rust/testing/fixtures/
auth.rs

1//! Authentication credential generators for testing.
2//!
3//! Provides utilities for generating test credentials:
4//! - Static bearer values
5//! - JWT-like values (not cryptographically secure)
6//! - API values
7//! - Session values
8
9use std::collections::HashMap;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12// ============================================================================
13// Helper Functions
14// ============================================================================
15
16/// Simple hex encoding for testing (avoids base64 dependency).
17fn hex_encode(data: &[u8]) -> String {
18    use std::fmt::Write;
19    data.iter()
20        .fold(String::with_capacity(data.len() * 2), |mut acc, b| {
21            let _ = write!(acc, "{b:02x}");
22            acc
23        })
24}
25
26/// Simple hex decoding for testing.
27fn hex_decode(s: &str) -> Option<Vec<u8>> {
28    if s.len() % 2 != 0 {
29        return None;
30    }
31    (0..s.len())
32        .step_by(2)
33        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
34        .collect()
35}
36
37// ============================================================================
38// Static Values
39// ============================================================================
40
41/// A valid static bearer value for testing.
42pub const VALID_BEARER_VALUE: &str = "test-bearer-value-12345";
43
44/// An invalid/expired bearer value for testing.
45pub const INVALID_BEARER_VALUE: &str = "invalid-value-00000";
46
47/// A valid API value for testing.
48pub const VALID_API_VALUE: &str = "test-api-value-abcdef123456";
49
50/// An invalid API value for testing.
51pub const INVALID_API_VALUE: &str = "invalid-api-value";
52
53/// A valid session value for testing.
54pub const VALID_SESSION_VALUE: &str = "sess-value-xyz789";
55
56// ============================================================================
57// Value Generation
58// ============================================================================
59
60/// Generates a random-looking value for testing.
61///
62/// Note: This is NOT cryptographically secure. Use only for testing.
63#[must_use]
64pub fn generate_test_value(prefix: &str, length: usize) -> String {
65    use std::time::SystemTime;
66
67    let timestamp = SystemTime::now()
68        .duration_since(UNIX_EPOCH)
69        .unwrap_or_default()
70        .as_nanos();
71
72    let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz0123456789".chars().collect();
73    let mut value = String::with_capacity(prefix.len() + length + 1);
74    value.push_str(prefix);
75    value.push('-');
76
77    let mut seed = timestamp as usize;
78    for _ in 0..length {
79        seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
80        let idx = seed % chars.len();
81        value.push(chars[idx]);
82    }
83
84    value
85}
86
87/// Generates a bearer value.
88#[must_use]
89pub fn generate_bearer_value() -> String {
90    generate_test_value("bear", 32)
91}
92
93/// Generates an API value.
94#[must_use]
95pub fn generate_api_value() -> String {
96    generate_test_value("api", 24)
97}
98
99/// Generates a session value.
100#[must_use]
101pub fn generate_session_value() -> String {
102    generate_test_value("sess", 16)
103}
104
105// ============================================================================
106// JWT-like Values (for testing only, not secure)
107// ============================================================================
108
109/// A test JWT-like structure for testing.
110///
111/// WARNING: This is NOT a real JWT implementation and should only be used for testing.
112#[derive(Debug, Clone)]
113pub struct TestJwt {
114    /// Header claims.
115    pub header: HashMap<String, String>,
116    /// Payload claims.
117    pub payload: HashMap<String, serde_json::Value>,
118    /// Test signature (not cryptographic).
119    pub signature: String,
120}
121
122impl TestJwt {
123    /// Creates a new test JWT with default header.
124    #[must_use]
125    pub fn new() -> Self {
126        let mut header = HashMap::new();
127        header.insert("alg".to_string(), "HS256".to_string());
128        header.insert("typ".to_string(), "JWT".to_string());
129
130        Self {
131            header,
132            payload: HashMap::new(),
133            signature: "test-signature".to_string(),
134        }
135    }
136
137    /// Sets the subject claim.
138    #[must_use]
139    pub fn subject(mut self, sub: impl Into<String>) -> Self {
140        self.payload
141            .insert("sub".to_string(), serde_json::json!(sub.into()));
142        self
143    }
144
145    /// Sets the issuer claim.
146    #[must_use]
147    pub fn issuer(mut self, iss: impl Into<String>) -> Self {
148        self.payload
149            .insert("iss".to_string(), serde_json::json!(iss.into()));
150        self
151    }
152
153    /// Sets the audience claim.
154    #[must_use]
155    pub fn audience(mut self, aud: impl Into<String>) -> Self {
156        self.payload
157            .insert("aud".to_string(), serde_json::json!(aud.into()));
158        self
159    }
160
161    /// Sets the expiration time (seconds from now).
162    #[must_use]
163    pub fn expires_in(mut self, seconds: u64) -> Self {
164        let exp = SystemTime::now()
165            .duration_since(UNIX_EPOCH)
166            .unwrap_or_default()
167            .as_secs()
168            + seconds;
169        self.payload
170            .insert("exp".to_string(), serde_json::json!(exp));
171        self
172    }
173
174    /// Sets the value as already expired.
175    #[must_use]
176    pub fn expired(mut self) -> Self {
177        let exp = SystemTime::now()
178            .duration_since(UNIX_EPOCH)
179            .unwrap_or_default()
180            .as_secs()
181            - 3600; // 1 hour ago
182        self.payload
183            .insert("exp".to_string(), serde_json::json!(exp));
184        self
185    }
186
187    /// Sets the issued-at time to now.
188    #[must_use]
189    pub fn issued_now(mut self) -> Self {
190        let iat = SystemTime::now()
191            .duration_since(UNIX_EPOCH)
192            .unwrap_or_default()
193            .as_secs();
194        self.payload
195            .insert("iat".to_string(), serde_json::json!(iat));
196        self
197    }
198
199    /// Adds a custom claim.
200    #[must_use]
201    pub fn claim(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
202        self.payload.insert(key.into(), value);
203        self
204    }
205
206    /// Encodes the value as a JWT-like string (hex encoded for testing).
207    ///
208    /// WARNING: This is NOT a real JWT - it uses simple hex encoding
209    /// without proper signing. Use only for testing.
210    #[must_use]
211    pub fn encode(&self) -> String {
212        let header_json = serde_json::to_string(&self.header).unwrap_or_default();
213        let payload_json = serde_json::to_string(&self.payload).unwrap_or_default();
214
215        // Use hex encoding for simplicity (testing only)
216        let header_hex = hex_encode(header_json.as_bytes());
217        let payload_hex = hex_encode(payload_json.as_bytes());
218        let sig_hex = hex_encode(self.signature.as_bytes());
219
220        format!("{header_hex}.{payload_hex}.{sig_hex}")
221    }
222
223    /// Checks if the value is expired (based on exp claim).
224    #[must_use]
225    pub fn is_expired(&self) -> bool {
226        if let Some(exp) = self.payload.get("exp") {
227            if let Some(exp_secs) = exp.as_u64() {
228                let now = SystemTime::now()
229                    .duration_since(UNIX_EPOCH)
230                    .unwrap_or_default()
231                    .as_secs();
232                return now >= exp_secs;
233            }
234        }
235        false
236    }
237}
238
239impl Default for TestJwt {
240    fn default() -> Self {
241        Self::new()
242    }
243}
244
245// ============================================================================
246// Pre-built JWT Values
247// ============================================================================
248
249/// Creates a valid test JWT with standard claims.
250#[must_use]
251pub fn valid_jwt() -> TestJwt {
252    TestJwt::new()
253        .subject("test-user")
254        .issuer("test-issuer")
255        .audience("test-audience")
256        .issued_now()
257        .expires_in(3600) // 1 hour
258}
259
260/// Creates an expired JWT for testing expiration handling.
261#[must_use]
262pub fn expired_jwt() -> TestJwt {
263    TestJwt::new()
264        .subject("test-user")
265        .issuer("test-issuer")
266        .expired()
267}
268
269/// Creates a JWT with custom roles claim.
270#[must_use]
271pub fn jwt_with_roles(roles: Vec<&str>) -> TestJwt {
272    let roles: Vec<String> = roles.into_iter().map(String::from).collect();
273    valid_jwt().claim("roles", serde_json::json!(roles))
274}
275
276/// Creates a JWT with admin role.
277#[must_use]
278pub fn admin_jwt() -> TestJwt {
279    jwt_with_roles(vec!["admin", "user"])
280}
281
282/// Creates a JWT with read-only role.
283#[must_use]
284pub fn readonly_jwt() -> TestJwt {
285    jwt_with_roles(vec!["readonly"])
286}
287
288// ============================================================================
289// Auth Header Helpers
290// ============================================================================
291
292/// Creates a Bearer auth header value.
293#[must_use]
294pub fn bearer_auth_header(value: &str) -> String {
295    format!("Bearer {value}")
296}
297
298/// Creates a Basic auth header value from username and password.
299///
300/// Note: Uses hex encoding instead of base64 for testing simplicity.
301/// Real implementations should use proper base64 encoding.
302#[must_use]
303pub fn basic_auth_header(username: &str, password: &str) -> String {
304    let credentials = format!("{username}:{password}");
305    let encoded = hex_encode(credentials.as_bytes());
306    format!("Basic {encoded}")
307}
308
309/// Creates an API value header value.
310#[must_use]
311pub fn api_value_header(value: &str) -> String {
312    value.to_string()
313}
314
315// ============================================================================
316// Test Credentials
317// ============================================================================
318
319/// Test credentials for basic auth.
320#[derive(Debug, Clone)]
321pub struct TestCredentials {
322    /// Username.
323    pub username: String,
324    /// Password.
325    pub password: String,
326}
327
328impl TestCredentials {
329    /// Creates new test credentials.
330    #[must_use]
331    pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
332        Self {
333            username: username.into(),
334            password: password.into(),
335        }
336    }
337
338    /// Creates valid test credentials.
339    #[must_use]
340    pub fn valid() -> Self {
341        Self::new("test-user", "test-credential-123")
342    }
343
344    /// Creates invalid test credentials.
345    #[must_use]
346    pub fn invalid() -> Self {
347        Self::new("invalid-user", "wrong-credential")
348    }
349
350    /// Creates admin test credentials.
351    #[must_use]
352    pub fn admin() -> Self {
353        Self::new("admin", "admin-credential-456")
354    }
355
356    /// Generates the Basic auth header for these credentials.
357    #[must_use]
358    pub fn to_basic_auth(&self) -> String {
359        basic_auth_header(&self.username, &self.password)
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn test_static_tokens() {
369        assert!(VALID_BEARER_VALUE.len() > 10);
370        assert!(INVALID_BEARER_VALUE.len() > 0);
371        assert!(VALID_API_VALUE.starts_with("test-api-"));
372        assert!(VALID_SESSION_VALUE.starts_with("sess-"));
373    }
374
375    #[test]
376    fn test_generate_test_token() {
377        let value1 = generate_test_value("test", 16);
378        let _value2 = generate_test_value("test", 16);
379
380        assert!(value1.starts_with("test-"));
381        assert!(value1.len() == "test-".len() + 16);
382        // Tokens should be different (though not guaranteed with this simple generator)
383        // Just verify they're generated correctly
384        assert!(
385            value1
386                .chars()
387                .all(|c| c.is_ascii_alphanumeric() || c == '-')
388        );
389    }
390
391    #[test]
392    fn test_generate_bearer_token() {
393        let value = generate_bearer_value();
394        assert!(value.starts_with("bear-"));
395    }
396
397    #[test]
398    fn test_generate_api_key() {
399        let value = generate_api_value();
400        assert!(value.starts_with("api-"));
401    }
402
403    #[test]
404    fn test_test_jwt_creation() {
405        let jwt = TestJwt::new()
406            .subject("user123")
407            .issuer("test-app")
408            .expires_in(3600);
409
410        assert!(jwt.payload.contains_key("sub"));
411        assert!(jwt.payload.contains_key("iss"));
412        assert!(jwt.payload.contains_key("exp"));
413    }
414
415    #[test]
416    fn test_test_jwt_encode() {
417        let jwt = valid_jwt();
418        let encoded = jwt.encode();
419
420        // JWT format: header.payload.signature
421        let parts: Vec<_> = encoded.split('.').collect();
422        assert_eq!(parts.len(), 3);
423    }
424
425    #[test]
426    fn test_test_jwt_expired() {
427        let expired = expired_jwt();
428        assert!(expired.is_expired());
429
430        let valid = valid_jwt();
431        assert!(!valid.is_expired());
432    }
433
434    #[test]
435    fn test_jwt_with_roles() {
436        let jwt = jwt_with_roles(vec!["admin", "user"]);
437        assert!(jwt.payload.contains_key("roles"));
438
439        let roles = jwt.payload.get("roles").unwrap();
440        let roles_arr = roles.as_array().unwrap();
441        assert_eq!(roles_arr.len(), 2);
442    }
443
444    #[test]
445    fn test_bearer_auth_header() {
446        let header = bearer_auth_header("value123");
447        assert_eq!(header, "Bearer value123");
448    }
449
450    #[test]
451    fn test_basic_auth_header() {
452        let header = basic_auth_header("user", "pass");
453        assert!(header.starts_with("Basic "));
454
455        // Decode and verify (using hex encoding)
456        let encoded = header.strip_prefix("Basic ").unwrap();
457        let decoded = hex_decode(encoded).unwrap();
458        let credentials = String::from_utf8(decoded).unwrap();
459        assert_eq!(credentials, "user:pass");
460    }
461
462    #[test]
463    fn test_test_credentials() {
464        let valid = TestCredentials::valid();
465        assert!(!valid.username.is_empty());
466        assert!(!valid.password.is_empty());
467
468        let admin = TestCredentials::admin();
469        assert_eq!(admin.username, "admin");
470    }
471
472    #[test]
473    fn test_credentials_to_basic_auth() {
474        let creds = TestCredentials::new("user", "pass");
475        let header = creds.to_basic_auth();
476        assert!(header.starts_with("Basic "));
477    }
478}