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:
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;
-}