oauth2-passkey 0.6.1

OAuth2 and Passkey authentication library for Rust web applications
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
//! Test utilities module for shared test initialization and helpers
//!
//! This module provides centralized test setup functionality that can be used
//! across all test modules in the crate to ensure consistent environment
//! configuration and database initialization.
//!
//! ## Simplified Approach
//! Since SQLite functions now ensure tables exist at the point of use,
//! test utilities only need basic initialization without complex retry logic.

use std::sync::Once;

use crate::passkey::CredentialId;
use crate::session::UserId;

/// Centralized test initialization for all tests across the entire crate
///
/// This function ensures that:
/// 1. Test environment variables are loaded from .env_test (with fallback to .env) - **ONCE**
/// 2. All database stores are initialized - **SIMPLE**
///
/// ## Simple Database Initialization
/// SQLite functions now ensure tables exist when called, so we only need basic store setup.
///
/// ## Usage
/// ```rust
/// use crate::test_utils::init_test_environment;
///
/// #[tokio::test]
/// async fn my_test() {
///     init_test_environment().await;
///     // ... test code that requires database access
/// }
/// ```
pub async fn init_test_environment() {
    // Environment setup (synchronous, runs once)
    static ENV_INIT: Once = Once::new();
    ENV_INIT.call_once(|| {
        // All tests now use .env_test - unit tests inject URLs directly to avoid HTTP requests
        println!("🧪 Loading test environment (.env_test)");

        // Use override method to force .env_test values to override existing environment
        if dotenvy::from_filename_override(".env_test").is_err() {
            dotenvy::dotenv().ok();
        }

        // Clean up any existing test database file based on GENERIC_DATA_STORE_URL
        if let Some(db_path) = extract_sqlite_file_path()
            && std::fs::remove_file(&db_path).is_err()
        {
            // File doesn't exist or can't be removed - that's okay
        }
    });

    // Simple database initialization
    ensure_database_initialized().await;
}

/// Extract SQLite database file path from GENERIC_DATA_STORE_URL environment variable
///
/// Parses the GENERIC_DATA_STORE_URL to extract the file path for SQLite databases.
/// Supports formats like:
/// - `sqlite:/path/to/file.db`
/// - `sqlite:./relative/path.db`
/// - `sqlite:/tmp/test.db`
///
/// Returns None for non-SQLite URLs or if the URL cannot be parsed.
fn extract_sqlite_file_path() -> Option<String> {
    if let Ok(url) = std::env::var("GENERIC_DATA_STORE_URL") {
        extract_sqlite_file_path_from_url(&url)
    } else {
        None
    }
}

/// Ensures database is properly initialized and creates a first user if none exists
async fn ensure_database_initialized() {
    use crate::oauth2::OAuth2Store;
    use crate::passkey::PasskeyStore;
    use crate::userdb::UserStore;

    // Initialize stores - log errors but don't panic in tests
    if let Err(e) = UserStore::init().await {
        eprintln!("Warning: Failed to initialize UserStore: {e}");
    }
    if let Err(e) = OAuth2Store::init().await {
        eprintln!("Warning: Failed to initialize OAuth2Store: {e}");
    }
    if let Err(e) = PasskeyStore::init().await {
        eprintln!("Warning: Failed to initialize PasskeyStore: {e}");
    }

    // Create a first user if no users exist (with both OAuth2 and Passkey credentials)
    create_first_user_if_needed().await;
}

/// Creates a first test user with passkey credential if no users exist in the database
/// Uses proper race-condition handling to ensure only one first user is created
async fn create_first_user_if_needed() {
    use crate::userdb::{User, UserStore};
    use std::sync::LazyLock;
    use tokio::sync::Mutex;

    // Use a mutex to prevent race conditions when creating the first user
    static FIRST_USER_CREATION_MUTEX: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));

    let _guard = FIRST_USER_CREATION_MUTEX.lock().await;

    // Double-check pattern: check again after acquiring the mutex
    match UserStore::get_all_users().await {
        Ok(users) if users.is_empty() => {
            // No users exist, create a first test user with passkey credential
            let mut first_user = User::new(
                "first-user".to_string(),
                "first-user@example.com".to_string(),
                "First User".to_string(),
            );
            first_user.is_admin = true; // First user should be admin

            match UserStore::upsert_user(first_user).await {
                Ok(created_user) => {
                    println!("✅ Created first test user with sequence_number = 1");

                    // Create OAuth2 account for authentication testing
                    create_first_user_oauth2_account(&created_user.id).await;

                    // Create Passkey credential for authentication testing
                    create_first_user_passkey_credential(&created_user.id).await;
                }
                Err(e) => {
                    eprintln!("Warning: Failed to create first test user: {e}");
                }
            }
        }
        Ok(users) => {
            // Users already exist, log the first user for debugging
            if let Some(first) = users.iter().find(|u| u.sequence_number == Some(1)) {
                println!("✅ First user already exists: {}", first.id);

                // Ensure first user has admin privileges (update if needed)
                if !first.has_admin_privileges() {
                    println!("🔧 Updating first user to have admin privileges...");
                    let mut updated_user = first.clone();
                    updated_user.is_admin = true;
                    if let Err(e) = UserStore::upsert_user(updated_user).await {
                        eprintln!("Warning: Failed to update first user admin status: {e}");
                    } else {
                        println!("✅ First user now has admin privileges");
                    }
                }

                // Ensure first user has OAuth2 account for authentic testing
                ensure_first_user_has_oauth2_account(&first.id).await;

                // Ensure first user has Passkey credential for authentic testing
                ensure_first_user_has_passkey_credential(&first.id).await;
            }
        }
        Err(e) => {
            eprintln!("Warning: Failed to check existing users: {e}");
        }
    }
}

/// Creates a test OAuth2 account for the first user to enable authentic authentication testing
async fn create_first_user_oauth2_account(user_id: &str) {
    use crate::oauth2::{OAuth2Account, OAuth2Store};
    use chrono::Utc;

    let now = Utc::now();
    let provider = "google";
    let provider_user_id = format!("{provider}_first-user-test-google-id");
    let test_oauth2_account = OAuth2Account {
        sequence_number: None,
        id: "first-user-oauth2-account".to_string(),
        user_id: user_id.to_string(),
        provider: provider.to_string(),
        provider_user_id: provider_user_id.to_string(),
        name: "First User".to_string(),
        email: "first-user@example.com".to_string(),
        picture: Some("https://example.com/avatar/first-user.jpg".to_string()),
        metadata: serde_json::json!({"test_account": true, "created_by": "test_utils"}),
        created_at: now,
        updated_at: now,
    };

    match OAuth2Store::upsert_oauth2_account(test_oauth2_account).await {
        Ok(_account) => {
            println!("✅ Created first user OAuth2 account for testing");
        }
        Err(e) => {
            eprintln!("Warning: Failed to create first user OAuth2 account: {e}");
        }
    }
}

/// Ensures the first user has an OAuth2 account (for cases where user exists but OAuth2 account doesn't)  
async fn ensure_first_user_has_oauth2_account(user_id: &str) {
    use crate::oauth2::OAuth2Store;

    // Check if first user already has OAuth2 accounts
    let user_id_typed = match UserId::new(user_id.to_string()) {
        Ok(id) => id,
        Err(e) => {
            eprintln!("Warning: Failed to create UserId for '{user_id}': {e}");
            return;
        }
    };

    match OAuth2Store::get_oauth2_accounts(user_id_typed).await {
        Ok(accounts) if accounts.is_empty() => {
            // No OAuth2 accounts exist for first user, create one
            println!("ℹ️ First user exists but has no OAuth2 account, creating one...");
            create_first_user_oauth2_account(user_id).await;
        }
        Ok(accounts) => {
            println!("✅ First user has {} OAuth2 account(s)", accounts.len());
        }
        Err(e) => {
            eprintln!("Warning: Failed to check first user OAuth2 accounts: {e}");
        }
    }
}

/// Creates a test Passkey credential for the first user to enable authentic authentication testing
///
/// This creates a WebAuthn credential with a fixed public key that corresponds to the private key
/// used in mock authentication. This ensures signature verification works correctly in integration tests.
///
/// **Credential Flow**: Store public key → Mock auth signs with private key → Verification succeeds
async fn create_first_user_passkey_credential(user_id: &str) {
    // Use the internal passkey module imports since this is test utility code
    use crate::passkey::{PasskeyCredential, PasskeyStore};
    use chrono::Utc;

    let now = Utc::now();

    // Get the fixed public key that corresponds to FIRST_USER_PRIVATE_KEY in integration tests
    // This mathematical relationship enables proper WebAuthn signature verification
    let public_key = generate_first_user_public_key();

    // Create a test passkey credential with consistent key for testing
    // Note: We construct the user entity directly here since it's internal test code
    let test_passkey_credential = PasskeyCredential {
        sequence_number: None,
        credential_id: "first-user-test-passkey-credential".to_string(),
        user_id: user_id.to_string(),
        public_key,
        aaguid: "00000000-0000-0000-0000-000000000000".to_string(), // Test AAGUID
        rp_id: "localhost".to_string(),
        counter: 0,
        user: serde_json::from_value(serde_json::json!({
            "user_handle": "first-user-handle",
            "name": "first-user@example.com",
            "displayName": "First User"
        }))
        .expect("Valid user entity JSON"),
        created_at: now,
        updated_at: now,
        last_used_at: now,
    };

    match PasskeyStore::store_credential(
        CredentialId::new("first-user-test-passkey-credential".to_string())
            .expect("Valid credential ID"),
        test_passkey_credential,
    )
    .await
    {
        Ok(_) => {
            println!("✅ Created first user Passkey credential for testing");
        }
        Err(e) => {
            eprintln!("Warning: Failed to create first user Passkey credential: {e}");
        }
    }
}

/// Get the fixed ECDSA P-256 public key for the first user credentials
///
/// This public key is mathematically derived from the private key stored in `fixtures.rs`.
/// Both are part of the same cryptographic key pair used for WebAuthn signature verification.
///
/// **Key Derivation**: This public key = FIRST_USER_PRIVATE_KEY × Generator Point (P-256 curve)
/// **Format**: Base64url-encoded uncompressed point coordinates (64 bytes: 32-byte X + 32-byte Y)
/// **Usage**: Stored in database credential → verifies signatures created by corresponding private key
///
/// 🔗 **Related**: See `fixtures.rs::first_user_key_pair()` for the matching private key
fn generate_first_user_public_key() -> String {
    // Fixed ECDSA P-256 public key in WebAuthn format (base64url-encoded coordinates)
    // Corresponds to private key in FIRST_USER_PRIVATE_KEY (fixtures.rs)
    // Coordinate breakdown: BB...Ps = X(32 bytes) + Y(32 bytes) in base64url
    "BBtOg4PEjnY2yQkrPjL832Obw0qJxiR-vIoUjjMmkKbyNjO4tT3blJAlPI5Y39nDiNkn7UnkCFZIS39cYp9nLPs"
        .to_string()
}

/// Ensures the first user has a Passkey credential (for cases where user exists but credential doesn't)
async fn ensure_first_user_has_passkey_credential(user_id: &str) {
    use crate::passkey::{CredentialSearchField, PasskeyStore};

    // Check if first user already has Passkey credentials
    let user_id_typed = match crate::session::UserId::new(user_id.to_string()) {
        Ok(id) => id,
        Err(e) => {
            eprintln!("Warning: Failed to create UserId for '{user_id}': {e}");
            return;
        }
    };

    match PasskeyStore::get_credentials_by(CredentialSearchField::UserId(user_id_typed)).await {
        Ok(credentials) if credentials.is_empty() => {
            // No Passkey credentials exist for first user, create one
            println!("ℹ️ First user exists but has no Passkey credential, creating one...");
            create_first_user_passkey_credential(user_id).await;
        }
        Ok(credentials) => {
            println!(
                "✅ First user has {} Passkey credential(s)",
                credentials.len()
            );
        }
        Err(e) => {
            eprintln!("Warning: Failed to check first user Passkey credentials: {e}");
        }
    }
}

/// Restores the first user (sequence_number=1) after deletion in tests.
///
/// Tests that exercise the "escape hatch" (deleting the first user) need to restore
/// the first user afterward so subsequent serial tests are not affected.
/// Uses raw SQL to explicitly set sequence_number=1 since SQLite AUTOINCREMENT
/// will not reuse previously deleted sequence numbers for auto-generated values.
/// Also restores the first user's OAuth2 account and passkey credential.
pub(crate) async fn restore_first_user_after_deletion() {
    use crate::storage::GENERIC_DATA_STORE;
    use crate::userdb::DB_TABLE_USERS;

    // Restore user row with explicit sequence_number=1 via raw SQL
    {
        let store = GENERIC_DATA_STORE.lock().await;
        let pool = store.as_sqlite().expect("Test database must be SQLite");
        let table_name = DB_TABLE_USERS.as_str();
        let now = chrono::Utc::now();

        sqlx::query(&format!(
            r#"
            INSERT OR REPLACE INTO {table_name}
                (sequence_number, id, account, label, is_admin, created_at, updated_at)
            VALUES (1, 'first-user', 'first-user@example.com', 'First User', true, ?, ?)
            "#
        ))
        .bind(now)
        .bind(now)
        .execute(pool)
        .await
        .expect("Failed to restore first user with sequence_number=1");
    }
    // Store lock is dropped here before calling other store functions

    // Restore associated OAuth2 account and passkey credential
    create_first_user_oauth2_account("first-user").await;
    create_first_user_passkey_credential("first-user").await;
}

/// Extract SQLite database file path from a database URL string
///
/// Parses a database URL to extract the file path for SQLite databases.
/// Supports formats like:
/// - `sqlite:/path/to/file.db`
/// - `sqlite:./relative/path.db`
/// - `sqlite:/tmp/test.db`
///
/// Returns None for non-SQLite URLs or if the URL cannot be parsed.
fn extract_sqlite_file_path_from_url(url: &str) -> Option<String> {
    if url.starts_with("sqlite:") {
        let path = url.strip_prefix("sqlite:")?;

        // Handle different SQLite URL formats
        if path.starts_with("file:") {
            // Handle sqlite:file:path?options format
            let file_path = path.strip_prefix("file:")?;
            // Extract path before any query parameters
            let path_only = file_path.split('?').next()?;
            // Skip special in-memory databases
            if path_only.contains(":memory:") {
                return None;
            }
            Some(path_only.to_string())
        } else {
            // Handle sqlite:path format
            let path = path.strip_prefix("//").unwrap_or(path);
            // Skip special in-memory databases
            if path.contains(":memory:") {
                return None;
            }
            Some(path.to_string())
        }
    } else {
        None
    }
}

/// Get the test origin from environment variables, with fallback to default test value
///
/// This function retrieves the ORIGIN environment variable which should be set in .env_test
/// for consistent test environment configuration. Falls back to localhost if not set.
pub fn get_test_origin() -> String {
    std::env::var("ORIGIN").unwrap_or_else(|_| "http://127.0.0.1:3000".to_string())
}

/// Spawn the current test binary as a child process with a specific env var set.
///
/// Used for testing LazyLock config validation: each test spawns a child process
/// with a specific env var value, then checks if the child panicked or succeeded.
/// The `__TEST_ENV_VAR_CHILD` env var distinguishes parent (spawns child) from
/// child (evaluates the LazyLock variable).
pub fn run_child_with_env(
    test_name: &str,
    env_name: &str,
    env_value: &str,
) -> std::process::Output {
    std::process::Command::new(std::env::current_exe().unwrap())
        .args([test_name, "--exact", "--nocapture"])
        .env("__TEST_ENV_VAR_CHILD", "1")
        .env(env_name, env_value)
        .output()
        .expect("Failed to spawn child process")
}

/// Spawn the current test binary as a child process with a specific env var removed.
///
/// Used for testing default values when an env var is not set.
pub fn run_child_without_env(test_name: &str, env_name: &str) -> std::process::Output {
    std::process::Command::new(std::env::current_exe().unwrap())
        .args([test_name, "--exact", "--nocapture"])
        .env("__TEST_ENV_VAR_CHILD", "1")
        .env_remove(env_name)
        .output()
        .expect("Failed to spawn child process")
}

#[cfg(test)]
mod tests;