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