kych

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

commit d114e61d1c5aaad8e51002bb77c1b8053b87f759
parent 759e70eb33aa67a3b7ccb453f255f699138d9d26
Author: Henrique Chan Carvalho Machado <henriqueccmachado@tecnico.ulisboa.pt>
Date:   Mon,  3 Nov 2025 23:08:13 +0100

oauth2_gateway: add api, db, integration tests for /setup and /authorize

Diffstat:
Moauth2_gateway/Cargo.toml | 6++++--
Aoauth2_gateway/scripts/test_integration.sh | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Moauth2_gateway/tests/api_tests.rs | 296+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Aoauth2_gateway/tests/db_tests.rs | 729+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 1047 insertions(+), 54 deletions(-)

diff --git a/oauth2_gateway/Cargo.toml b/oauth2_gateway/Cargo.toml @@ -49,4 +49,6 @@ base64 = "0.22.1" sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono", "json"] } [dev-dependencies] -tempfile = "3.8" -\ No newline at end of file +tempfile = "3.8" +wiremock = "0.6" +serial_test = "3.2" +\ No newline at end of file diff --git a/oauth2_gateway/scripts/test_integration.sh b/oauth2_gateway/scripts/test_integration.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +set -e + +GATEWAY_URL="http://localhost:9090" +CLIENT_ID="test-exchange-001" +SCOPE="age_over_18" +QR_CODE_FILE="oauth2gw_qr_code.png" + +echo "================================================================" +echo "OAuth2 Gateway Integration Test" +echo "================================================================" +echo "" +echo "Prerequisites:" +echo " - OAuth2 Gateway running at $GATEWAY_URL" +echo " - Swiyu Verifier running" +echo " - Test database seeded with test-exchange-001 client" +echo " - qrencode installed (for QR code generation)" +echo "" +echo "================================================================" +echo "" + +echo "[1/2] Testing /setup endpoint..." +SETUP_RESPONSE=$(curl -s -X POST "$GATEWAY_URL/setup/$CLIENT_ID" \ + -H "Content-Type: application/json" \ + -d "{\"scope\": \"$SCOPE\"}") + +echo "Response: $SETUP_RESPONSE" + +NONCE=$(echo "$SETUP_RESPONSE" | jq -r '.nonce') +if [ -z "$NONCE" ] || [ "$NONCE" = "null" ]; then + echo "FAILED: No nonce returned" + exit 1 +fi +echo "SUCCESS: Received nonce: $NONCE" +echo "" + +echo "[2/2] Testing /authorize endpoint..." +AUTHORIZE_RESPONSE=$(curl -s -X GET "$GATEWAY_URL/authorize/$NONCE") + +echo "Response: $AUTHORIZE_RESPONSE" +echo "" + +VERIFICATION_URL=$(echo "$AUTHORIZE_RESPONSE" | jq -r '.verification_url') +VERIFICATION_ID=$(echo "$AUTHORIZE_RESPONSE" | jq -r '.verificationId') + +if [ -z "$VERIFICATION_URL" ] || [ "$VERIFICATION_URL" = "null" ]; then + echo "FAILED: No verification_url returned" + exit 1 +fi + +if [ -z "$VERIFICATION_ID" ] || [ "$VERIFICATION_ID" = "null" ]; then + echo "FAILED: No verificationId returned" + exit 1 +fi + +echo "SUCCESS: Received verification URL: $VERIFICATION_URL" +echo "SUCCESS: Received verification ID: $VERIFICATION_ID" +echo "" + +echo "Generating QR code..." +echo "$VERIFICATION_URL" | tee /dev/tty | xargs qrencode -o "$QR_CODE_FILE" +echo "QR code saved to: $QR_CODE_FILE" +echo "" + +open "$QR_CODE_FILE" + +echo "================================================================" +echo "Integration test completed successfully" +echo "================================================================" diff --git a/oauth2_gateway/tests/api_tests.rs b/oauth2_gateway/tests/api_tests.rs @@ -2,25 +2,68 @@ use axum::{routing::*, Router}; use axum_test::TestServer; use oauth2_gateway::{config::*, db, handlers, models::*, state::AppState}; use serde_json::json; +use wiremock::{MockServer, Mock, ResponseTemplate}; +use wiremock::matchers::{method, path}; +use serial_test::serial; -/// Create a test app with mock configuration -/// Note: These tests use handlers that return mock data (no real DB operations yet) -async fn create_test_app() -> Router { + +// API endpoint tests with Mocked Swiyu API and Database + +/// Helper to get test database URL +fn get_test_database_url() -> String { + std::env::var("TEST_DATABASE_URL") + .unwrap_or_else(|_| "postgresql://oauth2gw:password@localhost:5432/oauth2gw".to_string()) +} + +/// Helper to setup test database and clean data +async fn setup_test_db() -> sqlx::PgPool { + let pool = db::create_pool(&get_test_database_url()) + .await + .expect("Failed to connect to test database"); + clean_test_data(&pool).await; + pool +} + +/// Clean all test data (in correct FK order) +async fn clean_test_data(pool: &sqlx::PgPool) { + sqlx::query("DELETE FROM oauth2gw.notification_logs") + .execute(pool) + .await + .expect("Failed to clean notification_logs"); + sqlx::query("DELETE FROM oauth2gw.webhook_logs") + .execute(pool) + .await + .expect("Failed to clean webhook_logs"); + sqlx::query("DELETE FROM oauth2gw.access_tokens") + .execute(pool) + .await + .expect("Failed to clean access_tokens"); + sqlx::query("DELETE FROM oauth2gw.verification_sessions") + .execute(pool) + .await + .expect("Failed to clean verification_sessions"); + sqlx::query("DELETE FROM oauth2gw.clients") + .execute(pool) + .await + .expect("Failed to clean clients"); +} + +/// Helper to teardown test database +async fn teardown_test_db(pool: &sqlx::PgPool) { + clean_test_data(pool).await; +} + +/// Create test app with custom verifier base URL (for mock server) +async fn create_test_app(pool: sqlx::PgPool) -> Router { let config = Config { server: ServerConfig { host: "127.0.0.1".to_string(), - port: 8080, + port: 9090, }, database: DatabaseConfig { - // Use a test db - url: std::env::var("TEST_DATABASE_URL") - .unwrap_or_else(|_| "postgresql://localhost/oauth2gw_test".to_string()), + url: get_test_database_url(), }, }; - - let pool = db::create_pool(&config.database.url) - .await - .expect("Failed to connect to test database"); let state = AppState::new(config, pool); @@ -35,85 +78,234 @@ async fn create_test_app() -> Router { } #[tokio::test] -async fn test_health_check() { - let app = create_test_app().await; +#[serial] +async fn test_setup_with_real_database() { + let pool = setup_test_db().await; + + 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"); + + let app = create_test_app(pool.clone()).await; let server = TestServer::new(app).unwrap(); - let response = server.get("/health").await; + let response = server + .post("/setup/test-client-1") + .json(&json!({"scope": "first_name last_name date_of_birth"})) + .await; response.assert_status_ok(); - response.assert_json(&json!({ - "status": "healthy", - "service": "oauth2-gateway", - // "version": env!("CARGO_PKG_VERSION") - })); + let body: SetupResponse = response.json(); + assert!(!body.nonce.is_empty()); + + let session = db::sessions::get_session_by_nonce(&pool, &body.nonce) + .await + .expect("Failed to query session") + .expect("Session not found"); + + assert_eq!(session.scope, "first_name last_name date_of_birth"); + assert_eq!(session.status, db::sessions::SessionStatus::Pending); + + teardown_test_db(&pool).await; } #[tokio::test] -async fn test_setup_endpoint() { - let app = create_test_app().await; +#[serial] +async fn test_setup_with_nonexistent_client() { + let pool = setup_test_db().await; + let app = create_test_app(pool.clone()).await; let server = TestServer::new(app).unwrap(); let response = server - .post("/setup/test-client") - .json(&json!({ - "scope": "first_name last_name age_over_18" - })) + .post("/setup/nonexistent-client") + .json(&json!({"scope": "test"})) .await; - response.assert_status_ok(); + response.assert_status(axum::http::StatusCode::NOT_FOUND); + let body: ErrorResponse = response.json(); + assert_eq!(body.error, "client_not_found"); - // Check response has a nonce - let body: SetupResponse = response.json(); - assert!(!body.nonce.is_empty()); - println!("Generated nonce: {}", body.nonce); + teardown_test_db(&pool).await; } #[tokio::test] -async fn test_setup_different_clients() { - let app = create_test_app().await; +#[serial] +async fn test_authorize_successful_flow_with_mocked_swiyu() { + let pool = setup_test_db().await; + + let mock_server = MockServer::start().await; + + db::clients::register_client( + &pool, + "test-client-2", + "secret123", + "https://client.example.com/notify", + &mock_server.uri(), // Use mock verifier + Some("/management/api/verifications"), + ) + .await + .expect("Failed to register client"); + + // mock Swiyu response + Mock::given(method("POST")) + .and(path("/management/api/verifications")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "verificationId": "550e8400-e29b-41d4-a716-446655440000", + "verification_url": "https://wallet.example.com/verify?request=abc123", + "verification_deeplink": "swiyu://verify/abc123", + "state": "PENDING" + }))) + .mount(&mock_server) + .await; + + let app = create_test_app(pool.clone()).await; let server = TestServer::new(app).unwrap(); - let response1 = server - .post("/setup/client-1") - .json(&json!({"scope": "first_name"})) + let setup_response = server + .post("/setup/test-client-2") + .json(&json!({"scope": "first_name last_name"})) + .await; + + setup_response.assert_status_ok(); + let setup: SetupResponse = setup_response.json(); + + // Call authorize + let response = server + .get(&format!("/authorize/{}", setup.nonce)) .await; - let response2 = server - .post("/setup/client-2") - .json(&json!({"scope": "last_name"})) + response.assert_status_ok(); + let body: AuthorizeResponse = response.json(); + + assert_eq!(body.verification_url, "https://wallet.example.com/verify?request=abc123"); + assert_eq!(body.verification_id.to_string(), "550e8400-e29b-41d4-a716-446655440000"); + + let session = db::sessions::get_session_by_nonce(&pool, &setup.nonce) + .await + .expect("Failed to query session") + .expect("Session not found"); + + assert_eq!(session.status, db::sessions::SessionStatus::Authorized); + assert_eq!(session.verification_url, Some("https://wallet.example.com/verify?request=abc123".to_string())); + assert_eq!(session.request_id, Some("550e8400-e29b-41d4-a716-446655440000".to_string())); + + teardown_test_db(&pool).await; +} + +#[tokio::test] +#[serial] +async fn test_authorize_with_invalid_nonce() { + let pool = setup_test_db().await; + let app = create_test_app(pool.clone()).await; + let server = TestServer::new(app).unwrap(); + + let response = server + .get("/authorize/invalid-nonce-12345") .await; - response1.assert_status_ok(); - response2.assert_status_ok(); + response.assert_status(axum::http::StatusCode::NOT_FOUND); + let body: ErrorResponse = response.json(); + assert_eq!(body.error, "session_not_found"); + + teardown_test_db(&pool).await; +} + +#[tokio::test] +#[serial] +async fn test_authorize_with_expired_session() { + let pool = setup_test_db().await; + + let client = db::clients::register_client( + &pool, + "test-client-3", + "secret123", + "https://client.example.com/notify", + "https://verifier.example.com", + None, + ) + .await + .expect("Failed to register client"); + + // Create expired session (negative expiration) + sqlx::query( + r#" + INSERT INTO oauth2gw.verification_sessions (client_id, nonce, scope, expires_at, status) + VALUES ($1, $2, $3, CURRENT_TIMESTAMP - INTERVAL '1 hour', 'pending') + "# + ) + .bind(client.id) + .bind("expired-nonce") + .bind("test") + .execute(&pool) + .await + .expect("Failed to create expired session"); - let nonce1: SetupResponse = response1.json(); - let nonce2: SetupResponse = response2.json(); + let app = create_test_app(pool.clone()).await; + let server = TestServer::new(app).unwrap(); - assert_ne!(nonce1.nonce, nonce2.nonce); + let response = server + .get("/authorize/expired-nonce") + .await; + + response.assert_status(axum::http::StatusCode::BAD_REQUEST); + let body: ErrorResponse = response.json(); + assert_eq!(body.error, "session_expired"); + + teardown_test_db(&pool).await; } #[tokio::test] -async fn test_authorize_endpoint() { - let app = create_test_app().await; +#[serial] +async fn test_authorize_with_swiyu_api_error() { + let pool = setup_test_db().await; + let mock_server = MockServer::start().await; + + + db::clients::register_client( + &pool, + "test-client-4", + "secret123", + "https://client.example.com/notify", + &mock_server.uri(), + Some("/management/api/verifications"), + ) + .await + .expect("Failed to register client"); + + // mock Swiyu response + Mock::given(method("POST")) + .and(path("/management/api/verifications")) + .respond_with(ResponseTemplate::new(500).set_body_json(json!({ + "error": "internal_server_error" + }))) + .mount(&mock_server) + .await; + + let app = create_test_app(pool.clone()).await; let server = TestServer::new(app).unwrap(); - // Get a nonce from setup + // Setup and authorize let setup_response = server - .post("/setup/test-client") + .post("/setup/test-client-4") .json(&json!({"scope": "test"})) .await; let setup: SetupResponse = setup_response.json(); - // Authorize with that nonce let response = server .get(&format!("/authorize/{}", setup.nonce)) .await; - response.assert_status_ok(); + response.assert_status(axum::http::StatusCode::BAD_GATEWAY); + let body: ErrorResponse = response.json(); + assert_eq!(body.error, "verifier_error"); - let body: AuthorizeResponse = response.json(); - assert!(!body.verification_url.is_empty()); - println!("Verification URL: {}", body.verification_url); + teardown_test_db(&pool).await; } \ No newline at end of file diff --git a/oauth2_gateway/tests/db_tests.rs b/oauth2_gateway/tests/db_tests.rs @@ -0,0 +1,729 @@ +// 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::update_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::mark_session_verified(&pool, session.id) + .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::mark_session_completed(&pool, session.id) + .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::update_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::mark_session_completed(&pool, session.id) + .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; +}