kych

OAuth 2.0 API for Swiyu to enable Taler integration of Swiyu for KYC (experimental)
Log | Files | Refs

commit 355f3ef7eec2a6d5e0b54e78447ed6b5775e284b
parent ab54d2589a575f587c6ac3eea3d3a8bfec93fbc3
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date:   Sun, 23 Nov 2025 19:04:13 +0100

oauth2_gateway: update db tests, add client management cli tests

Diffstat:
Aoauth2_gateway/tests/client_cli.rs | 271+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aoauth2_gateway/tests/db.rs | 443+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Doauth2_gateway/tests/db_tests.rs | 729-------------------------------------------------------------------------------
3 files changed, 714 insertions(+), 729 deletions(-)

diff --git a/oauth2_gateway/tests/client_cli.rs b/oauth2_gateway/tests/client_cli.rs @@ -0,0 +1,271 @@ +//! Integration tests for client-mgmt CLI + +use oauth2_gateway::db; +use sqlx::PgPool; + +async fn setup_pool() -> PgPool { + let database_url = std::env::var("TEST_DATABASE_URL") + .unwrap_or_else(|_| "postgresql://oauth2gw:password@localhost:5432/oauth2gw".to_string()); + db::create_pool(&database_url).await.unwrap() +} + +async fn cleanup_clients(pool: &PgPool) { + sqlx::query("DELETE FROM oauth2gw.clients") + .execute(pool) + .await + .unwrap(); +} + +#[tokio::test] +#[serial_test::serial] +async fn test_cli_client_create_and_list() { + let pool = setup_pool().await; + cleanup_clients(&pool).await; + + // Create a client + let client = db::clients::register_client( + &pool, + "test-cli-client", + "secret123", + "https://example.com/webhook", + "https://verifier.example.com", + None, + ) + .await + .unwrap(); + + assert_eq!(client.client_id, "test-cli-client"); + assert_eq!(client.webhook_url, "https://example.com/webhook"); + + // List clients + let clients = db::clients::list_clients(&pool).await.unwrap(); + assert_eq!(clients.len(), 1); + assert_eq!(clients[0].client_id, "test-cli-client"); + + cleanup_clients(&pool).await; +} + +#[tokio::test] +#[serial_test::serial] +async fn test_cli_client_show() { + let pool = setup_pool().await; + cleanup_clients(&pool).await; + + // Create a client + let created = db::clients::register_client( + &pool, + "show-test-client", + "secret456", + "https://example.com/hook", + "https://verifier.example.com", + Some("/custom/api/path"), + ) + .await + .unwrap(); + + // Show client by client_id + let found = db::clients::get_client_by_id(&pool, "show-test-client") + .await + .unwrap() + .unwrap(); + + assert_eq!(found.id, created.id); + assert_eq!(found.client_id, "show-test-client"); + assert_eq!(found.verifier_management_api_path, "/custom/api/path"); + + cleanup_clients(&pool).await; +} + +#[tokio::test] +#[serial_test::serial] +async fn test_cli_client_update() { + let pool = setup_pool().await; + cleanup_clients(&pool).await; + + // Create a client + let created = db::clients::register_client( + &pool, + "update-test-client", + "secret789", + "https://old-webhook.com", + "https://old-verifier.com", + None, + ) + .await + .unwrap(); + + // Update the client + let updated = db::clients::update_client( + &pool, + created.id, + Some("https://new-webhook.com"), + Some("https://new-verifier.com"), + Some("/new/api/path"), + ) + .await + .unwrap(); + + assert_eq!(updated.webhook_url, "https://new-webhook.com"); + assert_eq!(updated.verifier_url, "https://new-verifier.com"); + assert_eq!(updated.verifier_management_api_path, "/new/api/path"); + + cleanup_clients(&pool).await; +} + +#[tokio::test] +#[serial_test::serial] +async fn test_cli_client_update_partial() { + let pool = setup_pool().await; + cleanup_clients(&pool).await; + + // Create a client + let created = db::clients::register_client( + &pool, + "partial-update-client", + "secret", + "https://webhook.com", + "https://verifier.com", + None, + ) + .await + .unwrap(); + + // Update only webhook_url + let updated = db::clients::update_client( + &pool, + created.id, + Some("https://updated-webhook.com"), + None, + None, + ) + .await + .unwrap(); + + assert_eq!(updated.webhook_url, "https://updated-webhook.com"); + assert_eq!(updated.verifier_url, "https://verifier.com"); // unchanged + assert_eq!(updated.verifier_management_api_path, "/management/api/verifications"); // default + + cleanup_clients(&pool).await; +} + +#[tokio::test] +#[serial_test::serial] +async fn test_cli_client_delete() { + let pool = setup_pool().await; + cleanup_clients(&pool).await; + + // Create a client + let created = db::clients::register_client( + &pool, + "delete-test-client", + "secret", + "https://webhook.com", + "https://verifier.com", + None, + ) + .await + .unwrap(); + + // Verify it exists + let found = db::clients::get_client_by_id(&pool, "delete-test-client") + .await + .unwrap(); + assert!(found.is_some()); + + // Delete the client + let deleted = db::clients::delete_client(&pool, created.id).await.unwrap(); + assert!(deleted); + + // Verify it's gone + let not_found = db::clients::get_client_by_id(&pool, "delete-test-client") + .await + .unwrap(); + assert!(not_found.is_none()); + + cleanup_clients(&pool).await; +} + +#[tokio::test] +#[serial_test::serial] +async fn test_cli_client_not_found() { + let pool = setup_pool().await; + cleanup_clients(&pool).await; + + // Try to find non-existent client + let not_found = db::clients::get_client_by_id(&pool, "nonexistent-client") + .await + .unwrap(); + assert!(not_found.is_none()); + + cleanup_clients(&pool).await; +} + +#[tokio::test] +#[serial_test::serial] +async fn test_cli_client_duplicate_id() { + let pool = setup_pool().await; + cleanup_clients(&pool).await; + + // Create first client + db::clients::register_client( + &pool, + "duplicate-client", + "secret1", + "https://webhook1.com", + "https://verifier1.com", + None, + ) + .await + .unwrap(); + + // Try to create duplicate + let result = db::clients::register_client( + &pool, + "duplicate-client", + "secret2", + "https://webhook2.com", + "https://verifier2.com", + None, + ) + .await; + + assert!(result.is_err()); + + cleanup_clients(&pool).await; +} + +#[tokio::test] +#[serial_test::serial] +async fn test_cli_list_empty() { + let pool = setup_pool().await; + cleanup_clients(&pool).await; + + let clients = db::clients::list_clients(&pool).await.unwrap(); + assert!(clients.is_empty()); +} + +#[tokio::test] +#[serial_test::serial] +async fn test_cli_list_multiple_clients() { + let pool = setup_pool().await; + cleanup_clients(&pool).await; + + // Create multiple clients + for i in 1..=3 { + db::clients::register_client( + &pool, + &format!("client-{}", i), + &format!("secret-{}", i), + &format!("https://webhook{}.com", i), + &format!("https://verifier{}.com", i), + None, + ) + .await + .unwrap(); + } + + let clients = db::clients::list_clients(&pool).await.unwrap(); + assert_eq!(clients.len(), 3); + + cleanup_clients(&pool).await; +} diff --git a/oauth2_gateway/tests/db.rs b/oauth2_gateway/tests/db.rs @@ -0,0 +1,443 @@ +// Database tests for OAuth2 Gateway +// Requires TEST_DATABASE_URL environment variable or uses default connection. + +use oauth2_gateway::db; +use sqlx::PgPool; +use serial_test::serial; + +fn get_test_database_url() -> String { + std::env::var("TEST_DATABASE_URL") + .unwrap_or_else(|_| "postgresql://oauth2gw:password@localhost:5432/oauth2gw".to_string()) +} + +async fn setup_test_db() -> PgPool { + let pool = db::create_pool(&get_test_database_url()) + .await + .expect("Failed to connect to test database"); + clean_test_data(&pool).await; + pool +} + +async fn clean_test_data(pool: &PgPool) { + let _ = sqlx::query("DELETE FROM oauth2gw.notification_pending_webhooks").execute(pool).await; + let _ = sqlx::query("DELETE FROM oauth2gw.authorization_codes").execute(pool).await; + let _ = sqlx::query("DELETE FROM oauth2gw.access_tokens").execute(pool).await; + let _ = sqlx::query("DELETE FROM oauth2gw.verification_sessions").execute(pool).await; + let _ = sqlx::query("DELETE FROM oauth2gw.clients").execute(pool).await; +} + +async fn teardown_test_db(pool: &PgPool) { + clean_test_data(pool).await; +} + +#[tokio::test] +#[serial] +async fn test_client_registration() { + let pool = setup_test_db().await; + + let client = db::clients::register_client( + &pool, + "test-exchange-1", + "secret123", + "https://exchange.example.com/kyc/webhook", + "https://verifier.swiyu.io", + None, + ) + .await + .expect("Failed to register client"); + + assert_eq!(client.client_id, "test-exchange-1"); + assert_eq!(client.webhook_url, "https://exchange.example.com/kyc/webhook"); + assert_eq!(client.verifier_url, "https://verifier.swiyu.io"); + assert_eq!(client.verifier_management_api_path, "/management/api/verifications"); + + teardown_test_db(&pool).await; +} + +#[tokio::test] +#[serial] +async fn test_client_lookup_by_client_id() { + let pool = setup_test_db().await; + + let registered = db::clients::register_client( + &pool, + "lookup-test", + "secret456", + "https://example.com/webhook", + "https://verifier.example.com", + Some("/custom/path"), + ) + .await + .unwrap(); + + let found = db::clients::get_client_by_id(&pool, "lookup-test") + .await + .unwrap() + .expect("Client not found"); + + assert_eq!(found.id, registered.id); + assert_eq!(found.verifier_management_api_path, "/custom/path"); + + teardown_test_db(&pool).await; +} + +#[tokio::test] +#[serial] +async fn test_client_not_found() { + let pool = setup_test_db().await; + + let result = db::clients::get_client_by_id(&pool, "nonexistent") + .await + .unwrap(); + + assert!(result.is_none()); + + teardown_test_db(&pool).await; +} + +#[tokio::test] +#[serial] +async fn test_session_creation() { + let pool = setup_test_db().await; + + let _client = db::clients::register_client( + &pool, + "session-client", + "secret", + "https://example.com/webhook", + "https://verifier.example.com", + None, + ) + .await + .unwrap(); + + let session = db::sessions::create_session( + &pool, + "session-client", + "nonce-abc123", + "first_name last_name age_over_18", + 15, + ) + .await + .expect("Failed to create session") + .expect("Session should be created"); + + assert_eq!(session.nonce, "nonce-abc123"); + assert_eq!(session.scope, "first_name last_name age_over_18"); + assert_eq!(session.status, db::sessions::SessionStatus::Pending); + assert!(session.verification_url.is_none()); + assert!(session.request_id.is_none()); + assert!(session.verifier_nonce.is_none()); + + teardown_test_db(&pool).await; +} + +#[tokio::test] +#[serial] +async fn test_get_session_for_authorize() { + let pool = setup_test_db().await; + + let _client = db::clients::register_client( + &pool, + "authorize-client", + "secret", + "https://example.com/webhook", + "https://verifier.example.com", + Some("/custom/api/path"), + ) + .await + .unwrap(); + + let session = db::sessions::create_session( + &pool, + "authorize-client", + "authorize-test-nonce", + "first_name last_name", + 15, + ) + .await + .unwrap() + .unwrap(); + + // Fetch session with client data + let data = db::sessions::get_session_for_authorize( + &pool, + "authorize-test-nonce", + "authorize-client", + ) + .await + .unwrap() + .expect("Session should be found"); + + assert_eq!(data.session_id, session.id); + assert_eq!(data.status, db::sessions::SessionStatus::Pending); + assert_eq!(data.scope, "first_name last_name"); + assert_eq!(data.verifier_url, "https://verifier.example.com"); + assert_eq!(data.verifier_management_api_path, "/custom/api/path"); + assert!(data.verification_url.is_none()); + + // Wrong client_id should return None + let not_found = db::sessions::get_session_for_authorize( + &pool, + "authorize-test-nonce", + "wrong-client", + ) + .await + .unwrap(); + assert!(not_found.is_none()); + + teardown_test_db(&pool).await; +} + +#[tokio::test] +#[serial] +async fn test_authorization_code_creation_and_exchange() { + let pool = setup_test_db().await; + + let _client = db::clients::register_client( + &pool, + "code-client", + "secret", + "https://example.com/webhook", + "https://verifier.example.com", + None, + ) + .await + .unwrap(); + + let session = db::sessions::create_session( + &pool, + "code-client", + "code-nonce", + "scope", + 15, + ) + .await + .unwrap() + .unwrap(); + + // Create authorization code + let code = db::authorization_codes::create_authorization_code( + &pool, + session.id, + "auth-code-xyz123", + 10, + ) + .await + .unwrap(); + + assert_eq!(code.code, "auth-code-xyz123"); + assert_eq!(code.session_id, session.id); + assert!(!code.used); + + // Exchange code - first time should mark as used + let exchange1 = db::authorization_codes::get_code_for_token_exchange( + &pool, + "auth-code-xyz123", + ) + .await + .unwrap() + .expect("Code should be found"); + + assert!(!exchange1.was_already_used); // First use + assert_eq!(exchange1.session_id, session.id); + assert!(exchange1.existing_token.is_none()); + + // Exchange code - second time should show as already used + let exchange2 = db::authorization_codes::get_code_for_token_exchange( + &pool, + "auth-code-xyz123", + ) + .await + .unwrap() + .expect("Code should still be found"); + + assert!(exchange2.was_already_used); // Already used + + teardown_test_db(&pool).await; +} + +#[tokio::test] +#[serial] +async fn test_create_token_and_complete_session() { + let pool = setup_test_db().await; + + let _client = db::clients::register_client( + &pool, + "complete-client", + "secret", + "https://example.com/webhook", + "https://verifier.example.com", + None, + ) + .await + .unwrap(); + + let session = db::sessions::create_session( + &pool, + "complete-client", + "complete-nonce", + "scope", + 15, + ) + .await + .unwrap() + .unwrap(); + + // Create token and complete session atomically + let token = db::tokens::create_token_and_complete_session( + &pool, + session.id, + "atomic-token-abc", + 3600, + ) + .await + .unwrap(); + + assert_eq!(token.token, "atomic-token-abc"); + assert_eq!(token.session_id, session.id); + + // Verify session was updated to completed + let data = db::sessions::get_session_for_authorize( + &pool, + "complete-nonce", + "complete-client", + ) + .await + .unwrap() + .unwrap(); + + assert_eq!(data.status, db::sessions::SessionStatus::Completed); + + teardown_test_db(&pool).await; +} + +#[tokio::test] +#[serial] +async fn test_notification_webhook_queue() { + let pool = setup_test_db().await; + + let client = db::clients::register_client( + &pool, + "webhook-client", + "secret", + "https://example.com/webhook", + "https://verifier.example.com", + None, + ) + .await + .unwrap(); + + let session = db::sessions::create_session( + &pool, + "webhook-client", + "webhook-nonce", + "scope", + 15, + ) + .await + .unwrap() + .unwrap(); + + // Create authorization code first (needed for webhook join) + db::authorization_codes::create_authorization_code( + &pool, + session.id, + "webhook-code", + 10, + ) + .await + .unwrap(); + + // Insert pending webhook + let serial = db::notification_webhooks::insert_pending_webhook( + &pool, + session.id, + client.id, + "https://example.com/webhook", + r#"{"nonce":"webhook-nonce","status":"verified"}"#, + ) + .await + .unwrap(); + + assert!(serial > 0); + + // Fetch pending webhooks + let pending = db::notification_webhooks::get_pending_webhooks(&pool, 100) + .await + .unwrap(); + + assert_eq!(pending.len(), 1); + assert_eq!(pending[0].session_id, session.id); + assert_eq!(pending[0].code, "webhook-code"); + assert_eq!(pending[0].url, "https://example.com/webhook"); + + // Delete webhook after "successful delivery" + let deleted = db::notification_webhooks::delete_webhook(&pool, serial) + .await + .unwrap(); + assert!(deleted); + + // Should be empty now + let pending_after = db::notification_webhooks::get_pending_webhooks(&pool, 100) + .await + .unwrap(); + assert!(pending_after.is_empty()); + + teardown_test_db(&pool).await; +} + +#[tokio::test] +#[serial] +async fn test_get_token_with_session() { + let pool = setup_test_db().await; + + let _client = db::clients::register_client( + &pool, + "info-client", + "secret", + "https://example.com/webhook", + "https://verifier.example.com", + None, + ) + .await + .unwrap(); + + let session = db::sessions::create_session( + &pool, + "info-client", + "info-nonce", + "scope", + 15, + ) + .await + .unwrap() + .unwrap(); + + // Create token + db::tokens::create_token_and_complete_session( + &pool, + session.id, + "info-token-xyz", + 3600, + ) + .await + .unwrap(); + + // Fetch token with session data + let data = db::tokens::get_token_with_session(&pool, "info-token-xyz") + .await + .unwrap() + .expect("Token should be found"); + + assert!(!data.revoked); + assert_eq!(data.session_status, db::sessions::SessionStatus::Completed); + + // Non-existent token returns None + let not_found = db::tokens::get_token_with_session(&pool, "nonexistent-token") + .await + .unwrap(); + assert!(not_found.is_none()); + + teardown_test_db(&pool).await; +} diff --git a/oauth2_gateway/tests/db_tests.rs b/oauth2_gateway/tests/db_tests.rs @@ -1,729 +0,0 @@ -// Database integration tests for OAuth2 Gateway -// -// These tests require a PostgreSQL database to be running and already migrated. -// Run scripts/setup_test_db.sh before running tests. -// Set the TEST_DATABASE_URL environment variable or use the default. - -use oauth2_gateway::db; -use sqlx::PgPool; -use serial_test::serial; - -// Test database URL - can be overridden with TEST_DATABASE_URL env var -fn get_test_database_url() -> String { - std::env::var("TEST_DATABASE_URL") - .unwrap_or_else(|_| "postgresql://oauth2gw:password@localhost/oauth2gw".to_string()) -} - -/// Setup test database: create pool and clean existing data -async fn setup_test_db() -> PgPool { - let database_url = get_test_database_url(); - let pool = db::create_pool(&database_url) - .await - .expect("Failed to connect to test database"); - - // Clean up test data (but keep schema) - clean_test_data(&pool).await; - - pool -} - -/// Clean all data from tables (but keep schema) -async fn clean_test_data(pool: &PgPool) { - // Delete in order to respect foreign key constraints - let _ = sqlx::query("DELETE FROM oauth2gw.notification_logs").execute(pool).await; - let _ = sqlx::query("DELETE FROM oauth2gw.webhook_logs").execute(pool).await; - let _ = sqlx::query("DELETE FROM oauth2gw.access_tokens").execute(pool).await; - let _ = sqlx::query("DELETE FROM oauth2gw.verification_sessions").execute(pool).await; - let _ = sqlx::query("DELETE FROM oauth2gw.clients").execute(pool).await; -} - -/// Teardown test database: clean test data -async fn teardown_test_db(pool: &PgPool) { - clean_test_data(pool).await; -} - -// ============================================================================ -// Test 1: Client Management -// ============================================================================ - -#[tokio::test] -#[serial] -async fn test_client_registration() { - let pool = setup_test_db().await; - - let client = db::clients::register_client( - &pool, - "test-client-1", - "secret123", - "https://client.example.com/notify", - "https://verifier.example.com", - None, - ) - .await - .expect("Failed to register client"); - - assert_eq!(client.client_id, "test-client-1"); - assert_eq!(client.client_secret, "secret123"); - assert_eq!(client.notification_url, "https://client.example.com/notify"); - assert_eq!(client.verifier_base_url, "https://verifier.example.com"); - assert_eq!(client.verifier_management_api_path, "/management/api/verifications"); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_client_lookup() { - let pool = setup_test_db().await; - - let registered_client = db::clients::register_client( - &pool, - "lookup-test-client", - "secret456", - "https://client.example.com/notify", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - // Lookup by client_id - let found_client = db::clients::get_client_by_id(&pool, "lookup-test-client") - .await - .unwrap() - .expect("Client not found"); - - assert_eq!(found_client.id, registered_client.id); - assert_eq!(found_client.client_id, "lookup-test-client"); - - // Lookup by UUID - let found_by_uuid = db::clients::get_client_by_uuid(&pool, registered_client.id) - .await - .unwrap() - .expect("Client not found by UUID"); - - assert_eq!(found_by_uuid.client_id, "lookup-test-client"); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_client_authentication() { - let pool = setup_test_db().await; - - db::clients::register_client( - &pool, - "auth-test-client", - "correct-password", - "https://client.example.com/notify", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - // Authenticate with correct password - let auth_success = db::clients::authenticate_client(&pool, - "auth-test-client", - "correct-password") - .await - .unwrap(); - assert!(auth_success.is_some(), "Authentication should succeed"); - - // Authenticate with wrong password - let auth_fail = db::clients::authenticate_client(&pool, - "auth-test-client", - "wrong-password") - .await - .unwrap(); - assert!(auth_fail.is_none(), "Authentication should fail with wrong password"); - - // Authenticate non-existent client - let auth_not_found = db::clients::authenticate_client(&pool, - "non-existent", - "password") - .await - .unwrap(); - assert!(auth_not_found.is_none(), "Authentication should fail for non-existent client"); - - teardown_test_db(&pool).await; -} - -// ============================================================================ -// Test 3: Verification Session Lifecycle -// ============================================================================ - -#[tokio::test] -#[serial] -async fn test_session_creation() { - let pool = setup_test_db().await; - - let client = db::clients::register_client( - &pool, - "session-test-client", - "secret", - "https://client.example.com/notify", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - // Create session - let session = db::sessions::create_session( - &pool, - client.id, - "test-nonce-123", - "first_name last_name age_over_18", - 15, // 15 minutes expiration - ) - .await - .expect("Failed to create session"); - - assert_eq!(session.nonce, "test-nonce-123"); - assert_eq!(session.scope, "first_name last_name age_over_18"); - assert_eq!(session.status, db::sessions::SessionStatus::Pending); - assert!(session.verification_url.is_none()); - assert!(session.request_id.is_none()); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_session_lookup_by_nonce() { - let pool = setup_test_db().await; - - let client = db::clients::register_client( - &pool, - "nonce-lookup-client", - "secret", - "https://client.example.com/notify", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - let created_session = db::sessions::create_session( - &pool, - client.id, - "unique-nonce-456", - "scope", - 15, - ) - .await - .unwrap(); - - // Lookup by nonce - let found_session = db::sessions::get_session_by_nonce(&pool, - "unique-nonce-456") - .await - .unwrap() - .expect("Session not found"); - - assert_eq!(found_session.id, created_session.id); - assert_eq!(found_session.nonce, "unique-nonce-456"); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_session_status_transitions() { - let pool = setup_test_db().await; - - let client = db::clients::register_client( - &pool, - "status-test-client", - "secret", - "https://client.example.com/notify", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - let session = db::sessions::create_session( - &pool, - client.id, - "status-nonce-789", - "scope", - 15, - ) - .await - .unwrap(); - - // Initial status: pending - assert_eq!(session.status, db::sessions::SessionStatus::Pending); - - // Transition to authorized - db::sessions::set_session_authorized( - &pool, - session.id, - "https://verifier.example.com/verify?request=abc", - "swiyu-request-id-123", - ) - .await - .unwrap(); - - let updated = db::sessions::get_session_by_nonce(&pool, - "status-nonce-789") - .await - .unwrap() - .unwrap(); - assert_eq!(updated.status, db::sessions::SessionStatus::Authorized); - assert_eq!(updated.verification_url.unwrap(), "https://verifier.example.com/verify?request=abc"); - assert_eq!(updated.request_id.unwrap(), "swiyu-request-id-123"); - assert!(updated.authorized_at.is_some()); - - // Transition to verified - db::sessions::update_session_status_with_timestamp(&pool, session.id, db::sessions::SessionStatus::Verified) - .await - .unwrap(); - - let verified = db::sessions::get_session_by_nonce(&pool, - "status-nonce-789") - .await - .unwrap() - .unwrap(); - assert_eq!(verified.status, db::sessions::SessionStatus::Verified); - assert!(verified.verified_at.is_some()); - - // Transition to completed - db::sessions::update_session_status_with_timestamp(&pool, session.id, db::sessions::SessionStatus::Completed) - .await - .unwrap(); - - let completed = db::sessions::get_session_by_nonce(&pool, - "status-nonce-789") - .await - .unwrap() - .unwrap(); - assert_eq!(completed.status, db::sessions::SessionStatus::Completed); - assert!(completed.completed_at.is_some()); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_session_lookup_by_request_id() { - let pool = setup_test_db().await; - - let client = db::clients::register_client( - &pool, - "request-id-client", - "secret", - "https://client.example.com/notify", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - let session = db::sessions::create_session( - &pool, - client.id, - "request-id-nonce", - "scope", - 15, - ) - .await - .unwrap(); - - // Update with request_id - db::sessions::set_session_authorized( - &pool, - session.id, - "https://verify.url", - "swiyu-request-xyz", - ) - .await - .unwrap(); - - // Lookup by request_id - let found = db::sessions::get_session_by_request_id(&pool, "swiyu-request-xyz") - .await - .unwrap() - .expect("Session not found by request_id"); - - assert_eq!(found.id, session.id); - assert_eq!(found.request_id.unwrap(), "swiyu-request-xyz"); - - teardown_test_db(&pool).await; -} - -// ============================================================================ -// Test 4: Access Token Management -// ============================================================================ - -#[tokio::test] -#[serial] -async fn test_access_token_creation() { - let pool = setup_test_db().await; - - let client = db::clients::register_client( - &pool, - "token-test-client", - "secret", - "https://client.example.com/notify", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - let session = db::sessions::create_session( - &pool, - client.id, - "token-nonce", - "scope", - 15, - ) - .await - .unwrap(); - - // Create access token - let token = db::tokens::create_access_token( - &pool, - session.id, - "bearer-token-abc123", - 3600, // 1 hour - ) - .await - .unwrap(); - - assert_eq!(token.token, "bearer-token-abc123"); - assert_eq!(token.token_type, "Bearer"); - assert_eq!(token.session_id, session.id); - assert_eq!(token.revoked, false); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_access_token_verification() { - let pool = setup_test_db().await; - - let client = db::clients::register_client( - &pool, - "verify-token-client", - "secret", - "https://client.example.com/notify", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - let session = db::sessions::create_session( - &pool, - client.id, - "verify-nonce", - "scope", - 15, - ) - .await - .unwrap(); - - // Create token - db::tokens::create_access_token( - &pool, - session.id, - "valid-token-xyz", - 3600, - ) - .await - .unwrap(); - - // Verify valid token - let verified = db::tokens::verify_access_token(&pool, - "valid-token-xyz") - .await - .unwrap(); - assert!(verified.is_some(), "Token should be valid"); - - // Verify non-existent token - let not_found = db::tokens::verify_access_token(&pool, - "non-existent-token") - .await - .unwrap(); - assert!(not_found.is_none(), "Token should not be found"); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_access_token_revocation() { - let pool = setup_test_db().await; - - let client = db::clients::register_client( - &pool, - "revoke-token-client", - "secret", - "https://client.example.com/notify", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - let session = db::sessions::create_session( - &pool, - client.id, - "revoke-nonce", - "scope", - 15, - ) - .await - .unwrap(); - - db::tokens::create_access_token( - &pool, - session.id, - "token-to-revoke", - 3600, - ) - .await - .unwrap(); - - // Token should be valid initially - let valid = db::tokens::verify_access_token(&pool, "token-to-revoke") - .await - .unwrap(); - assert!(valid.is_some()); - - // Revoke token - let revoked = db::tokens::revoke_token(&pool, "token-to-revoke") - .await - .unwrap(); - assert!(revoked, "Token should be revoked"); - - // Token should be invalid after revocation - let invalid = db::tokens::verify_access_token(&pool, "token-to-revoke") - .await - .unwrap(); - assert!(invalid.is_none(), "Revoked token should be invalid"); - - teardown_test_db(&pool).await; -} - -// ============================================================================ -// Test 5: Audit Logging -// ============================================================================ - -#[tokio::test] -#[serial] -async fn test_webhook_logging() { - let pool = setup_test_db().await; - - let client = db::clients::register_client( - &pool, - "webhook-log-client", - "secret", - "https://client.example.com/notify", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - let session = db::sessions::create_session( - &pool, - client.id, - "webhook-nonce", - "scope", - 15, - ) - .await - .unwrap(); - - let payload = serde_json::json!({ - "nonce": "webhook-nonce", - "verification_complete": true - }); - - // Log webhook received - let log_id = db::logs::log_webhook_received( - &pool, - Some("swiyu-req-123"), - Some(session.id), - &payload, - ) - .await - .unwrap(); - - // Mark as processed - db::logs::mark_webhook_processed(&pool, log_id, 200) - .await - .unwrap(); - - // Query logs - let logs = db::logs::get_webhook_logs_for_session(&pool, session.id) - .await - .unwrap(); - - assert_eq!(logs.len(), 1); - assert_eq!(logs[0].processed, true); - assert_eq!(logs[0].status_code.unwrap(), 200); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_notification_logging() { - let pool = setup_test_db().await; - - let client = db::clients::register_client( - &pool, - "notif-log-client", - "secret", - "https://client.example.com/notify", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - let session = db::sessions::create_session( - &pool, - client.id, - "notif-nonce", - "scope", - 15, - ) - .await - .unwrap(); - - let payload = serde_json::json!({ - "nonce": "notif-nonce", - "verification_complete": true - }); - - // Log notification sent - let log_id = db::logs::log_notification_sent( - &pool, - session.id, - client.id, - "https://client.example.com/notify", - &payload, - ) - .await - .unwrap(); - - // Mark as successful - db::logs::mark_notification_success(&pool, log_id, 200) - .await - .unwrap(); - - // Query logs - let logs = db::logs::get_notification_logs_for_session(&pool, session.id) - .await - .unwrap(); - - assert_eq!(logs.len(), 1); - assert_eq!(logs[0].success, true); - assert_eq!(logs[0].status_code.unwrap(), 200); - - teardown_test_db(&pool).await; -} - -// ============================================================================ -// Test 6: Garbage Collection -// ============================================================================ - -#[tokio::test] -#[serial] -async fn test_expired_session_marking() { - let pool = setup_test_db().await; - - let client = db::clients::register_client( - &pool, - "gc-client", - "secret", - "https://client.example.com/notify", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - // Create session with negative expiration (already expired) - db::sessions::create_session( - &pool, - client.id, - "expired-nonce", - "scope", - -10, // Expired 10 minutes ago - ) - .await - .unwrap(); - - // Mark expired sessions - let marked = db::sessions::mark_expired_sessions(&pool) - .await - .unwrap(); - - assert!(marked > 0, "Should mark at least one session as expired"); - - // Verify session is marked as expired - let session = db::sessions::get_session_by_nonce(&pool, "expired-nonce") - .await - .unwrap() - .unwrap(); - assert_eq!(session.status, db::sessions::SessionStatus::Expired); - - teardown_test_db(&pool).await; -} - -#[tokio::test] -#[serial] -async fn test_old_session_deletion() { - let pool = setup_test_db().await; - - let client = db::clients::register_client( - &pool, - "delete-gc-client", - "secret", - "https://client.example.com/notify", - "https://verifier.example.com", - None, - ) - .await - .unwrap(); - - let session = db::sessions::create_session( - &pool, - client.id, - "old-nonce", - "scope", - 15, - ) - .await - .unwrap(); - - // Mark as completed - db::sessions::update_session_status_with_timestamp(&pool, session.id, db::sessions::SessionStatus::Completed) - .await - .unwrap(); - - // Delete sessions older than 0 days (should delete everything) - let deleted = db::sessions::delete_old_sessions(&pool, 0) - .await - .unwrap(); - - assert!(deleted > 0, "Should delete at least one session"); - - // Verify session is deleted - let not_found = db::sessions::get_session_by_nonce(&pool, "old-nonce") - .await - .unwrap(); - assert!(not_found.is_none(), "Session should be deleted"); - - teardown_test_db(&pool).await; -}