kych

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

handlers_integration.rs (61961B)


      1 use anyhow::Result;
      2 use axum::{
      3     body::{Body, to_bytes},
      4     http::{Request, StatusCode},
      5     routing::{get, post},
      6     Router,
      7 };
      8 use kych_oauth2_gateway_lib::{
      9     config::{ClientConfig, Config, CryptoConfig, DatabaseConfig, ServerConfig, VcConfig},
     10     db::{authorization_codes, clients, sessions},
     11     handlers,
     12     models::{
     13         Constraint, Field, Filter, InputDescriptor, PresentationDefinition, SwiyuManagementResponse,
     14         SwiyuVerificationStatus, TokenResponse,
     15     },
     16     state::AppState,
     17 };
     18 use std::collections::HashSet;
     19 use mockito::Server;
     20 use serde_json::Value;
     21 use sqlx::{PgPool, postgres::PgPoolOptions};
     22 use tower::util::ServiceExt;
     23 use uuid::Uuid;
     24 
     25 async fn get_pool() -> Option<PgPool> {
     26     let url = match std::env::var("DATABASE_URL") {
     27         Ok(url) if !url.trim().is_empty() => url,
     28         _ => {
     29             eprintln!("DATABASE_URL not set; skipping handler integration tests.");
     30             return None;
     31         }
     32     };
     33 
     34     match PgPoolOptions::new().max_connections(5).connect(&url).await {
     35         Ok(pool) => Some(pool),
     36         Err(err) => {
     37             eprintln!("Failed to connect to DATABASE_URL; skipping tests: {}", err);
     38             None
     39         }
     40     }
     41 }
     42 
     43 fn test_config(database_url: &str) -> Config {
     44     Config {
     45         server: ServerConfig {
     46             host: Some("127.0.0.1".to_string()),
     47             port: Some(8080),
     48             socket_path: None,
     49             socket_mode: 0o666,
     50         },
     51         database: DatabaseConfig {
     52             url: database_url.to_string(),
     53         },
     54         crypto: CryptoConfig {
     55             nonce_bytes: 32,
     56             token_bytes: 32,
     57             authorization_code_bytes: 32,
     58             authorization_code_ttl_minutes: 10,
     59         },
     60         vc: VcConfig {
     61             vc_type: "betaid-sdjwt".to_string(),
     62             vc_format: "vc+sd-jwt".to_string(),
     63             vc_algorithms: vec!["ES256".to_string()],
     64             vc_claims: [
     65                 "first_name",
     66                 "last_name",
     67                 "family_name",
     68                 "given_name",
     69                 "birth_date",
     70                 "age_over_18",
     71             ]
     72             .iter()
     73             .map(|s| s.to_string())
     74             .collect::<HashSet<String>>(),
     75         },
     76         allowed_scopes: None,
     77         clients: Vec::<ClientConfig>::new(),
     78     }
     79 }
     80 
     81 fn build_app(state: AppState) -> Router {
     82     Router::new()
     83         .route("/config", get(handlers::config))
     84         .route("/setup/{client_id}", post(handlers::setup))
     85         .route("/authorize/{nonce}", get(handlers::authorize))
     86         .route("/token", post(handlers::token))
     87         .route("/info", get(handlers::info))
     88         .route("/notification", post(handlers::notification_webhook))
     89         .route("/status/{verification_id}", get(handlers::status))
     90         .route("/finalize/{verification_id}", get(handlers::finalize))
     91         .with_state(state)
     92 }
     93 
     94 struct TestClient {
     95     client: clients::Client,
     96     secret: String,
     97 }
     98 
     99 async fn create_test_client(pool: &PgPool) -> Result<TestClient> {
    100     let suffix = Uuid::new_v4().to_string();
    101     let client_id = format!("test-client-{}", suffix);
    102     let secret = format!("secret-{}", suffix);
    103     let verifier_url = "https://verifier.example";
    104     let redirect_uri = "https://example.com/callback";
    105     let accepted_issuer_dids = Some("did:example:issuer1,did:example:issuer2");
    106 
    107     let client = clients::register_client(
    108         pool,
    109         &client_id,
    110         &secret,
    111         verifier_url,
    112         None,
    113         redirect_uri,
    114         accepted_issuer_dids,
    115     )
    116     .await?;
    117 
    118     Ok(TestClient { client, secret })
    119 }
    120 
    121 async fn create_test_client_with_verifier(
    122     pool: &PgPool,
    123     verifier_url: &str,
    124 ) -> Result<TestClient> {
    125     let suffix = Uuid::new_v4().to_string();
    126     let client_id = format!("test-client-{}", suffix);
    127     let secret = format!("secret-{}", suffix);
    128     let redirect_uri = "https://example.com/callback";
    129     let accepted_issuer_dids = Some("did:example:issuer1,did:example:issuer2");
    130 
    131     let client = clients::register_client(
    132         pool,
    133         &client_id,
    134         &secret,
    135         verifier_url,
    136         None,
    137         redirect_uri,
    138         accepted_issuer_dids,
    139     )
    140     .await?;
    141 
    142     Ok(TestClient { client, secret })
    143 }
    144 
    145 async fn create_second_client(pool: &PgPool) -> Result<TestClient> {
    146     let suffix = Uuid::new_v4().to_string();
    147     let client_id = format!("test-client-b-{}", suffix);
    148     let secret = format!("secret-b-{}", suffix);
    149     let verifier_url = "https://verifier.example";
    150     let redirect_uri = "https://example.com/callback";
    151     let accepted_issuer_dids = Some("did:example:issuer1,did:example:issuer2");
    152 
    153     let client = clients::register_client(
    154         pool,
    155         &client_id,
    156         &secret,
    157         verifier_url,
    158         None,
    159         redirect_uri,
    160         accepted_issuer_dids,
    161     )
    162     .await?;
    163 
    164     Ok(TestClient { client, secret })
    165 }
    166 
    167 fn sample_presentation_definition() -> PresentationDefinition {
    168     PresentationDefinition {
    169         id: "pd-1".to_string(),
    170         name: None,
    171         purpose: None,
    172         format: None,
    173         input_descriptors: vec![InputDescriptor {
    174             id: "descriptor-1".to_string(),
    175             name: None,
    176             purpose: None,
    177             format: None,
    178             constraints: Constraint {
    179                 fields: vec![Field {
    180                     path: vec!["$.vct".to_string()],
    181                     id: None,
    182                     name: None,
    183                     purpose: None,
    184                     filter: Some(Filter {
    185                         filter_type: "string".to_string(),
    186                         const_value: Some("betaid-sdjwt".to_string()),
    187                     }),
    188                 }],
    189             },
    190         }],
    191     }
    192 }
    193 
    194 async fn get_session_status(pool: &PgPool, session_id: Uuid) -> Result<sessions::SessionStatus> {
    195     let status = sqlx::query_scalar::<_, sessions::SessionStatus>(
    196         r#"
    197         SELECT status
    198         FROM oauth2gw.verification_sessions
    199         WHERE id = $1
    200         "#,
    201     )
    202     .bind(session_id)
    203     .fetch_one(pool)
    204     .await?;
    205 
    206     Ok(status)
    207 }
    208 
    209 struct SessionData {
    210     client: clients::Client,
    211     secret: String,
    212     request_id: String,
    213     redirect_uri: String,
    214     state: String,
    215     authorization_code: String,
    216 }
    217 
    218 async fn setup_session_with_status(
    219     pool: &PgPool,
    220     status: sessions::SessionStatus,
    221 ) -> Result<SessionData> {
    222     let test_client = create_test_client(pool).await?;
    223     let client = &test_client.client;
    224 
    225     let nonce = format!("nonce-{}", Uuid::new_v4());
    226     let session = sessions::create_session(pool, &client.client_id, &nonce, 5)
    227         .await?
    228         .expect("client should exist");
    229 
    230     let redirect_uri = "https://example.com/callback".to_string();
    231     let state = "state-123".to_string();
    232     let authorize = sessions::get_session_for_authorize(
    233         pool,
    234         &nonce,
    235         &client.client_id,
    236         "first_name",
    237         &redirect_uri,
    238         &state,
    239     )
    240     .await?
    241     .expect("session should exist");
    242 
    243     let request_id = Uuid::new_v4().to_string();
    244     let _ = sessions::update_session_authorized(
    245         pool,
    246         authorize.session_id,
    247         "https://verifier.example/verify/1",
    248         None,
    249         &request_id,
    250         None,
    251     )
    252     .await?;
    253 
    254     let authorization_code = format!("code-{}", Uuid::new_v4());
    255     let issued = sessions::verify_session_and_issue_code(
    256         pool,
    257         session.id,
    258         status,
    259         &authorization_code,
    260         10,
    261         client.id,
    262         "",
    263         Some(&serde_json::json!({"vc": "data"})),
    264     )
    265     .await?;
    266     assert_eq!(issued, authorization_code);
    267 
    268     Ok(SessionData {
    269         client: client.clone(),
    270         secret: test_client.secret,
    271         request_id,
    272         redirect_uri,
    273         state,
    274         authorization_code,
    275     })
    276 }
    277 
    278 async fn setup_authorized_session(
    279     pool: &PgPool,
    280     verifier_url: &str,
    281 ) -> Result<(TestClient, Uuid, Uuid)> {
    282     let test_client = create_test_client_with_verifier(pool, verifier_url).await?;
    283     let nonce = format!("nonce-{}", Uuid::new_v4());
    284     let session = sessions::create_session(pool, &test_client.client.client_id, &nonce, 5)
    285         .await?
    286         .expect("client should exist");
    287 
    288     let verification_id = Uuid::new_v4();
    289     let _ = sessions::update_session_authorized(
    290         pool,
    291         session.id,
    292         "https://verifier.example/verify/1",
    293         None,
    294         &verification_id.to_string(),
    295         None,
    296     )
    297     .await?;
    298 
    299     Ok((test_client, session.id, verification_id))
    300 }
    301 
    302 fn form_body(pairs: &[(&str, &str)]) -> String {
    303     pairs
    304         .iter()
    305         .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
    306         .collect::<Vec<_>>()
    307         .join("&")
    308 }
    309 
    310 async fn assert_error_response(
    311     response: axum::response::Response,
    312     status: StatusCode,
    313     expected_error: &str,
    314 ) -> Result<()> {
    315     assert_eq!(response.status(), status);
    316     let bytes = to_bytes(response.into_body(), usize::MAX).await?;
    317     let json: Value = serde_json::from_slice(&bytes)?;
    318     let error = json.get("error").and_then(|v| v.as_str()).unwrap_or("");
    319     assert_eq!(error, expected_error);
    320     Ok(())
    321 }
    322 
    323 #[tokio::test]
    324 async fn test_config_endpoint() -> Result<()> {
    325     let Some(pool) = get_pool().await else { return Ok(()); };
    326     let config = test_config(&std::env::var("DATABASE_URL")?);
    327     let app = build_app(AppState::new(config, pool));
    328 
    329     let response = app
    330         .oneshot(Request::builder().uri("/config").body(Body::empty())?)
    331         .await?;
    332 
    333     assert_eq!(response.status(), StatusCode::OK);
    334 
    335     let bytes = to_bytes(response.into_body(), usize::MAX).await?;
    336     let json: Value = serde_json::from_slice(&bytes)?;
    337 
    338     assert_eq!(json.get("name").and_then(|v| v.as_str()), Some("kych-oauth2-gateway"));
    339     assert!(json.get("version").and_then(|v| v.as_str()).is_some());
    340     assert_eq!(json.get("status").and_then(|v| v.as_str()), Some("healthy"));
    341     assert_eq!(json.get("vc_type").and_then(|v| v.as_str()), Some("betaid-sdjwt"));
    342     assert_eq!(json.get("vc_format").and_then(|v| v.as_str()), Some("vc+sd-jwt"));
    343     assert!(json.get("vc_algorithms").and_then(|v| v.as_array()).is_some());
    344     assert!(json.get("vc_claims").and_then(|v| v.as_array()).is_some());
    345 
    346     Ok(())
    347 }
    348 
    349 #[tokio::test]
    350 async fn test_setup_unauthorized() -> Result<()> {
    351     let Some(pool) = get_pool().await else { return Ok(()); };
    352     let config = test_config(&std::env::var("DATABASE_URL")?);
    353     let app = build_app(AppState::new(config, pool));
    354 
    355     let response = app
    356         .oneshot(
    357             Request::builder()
    358                 .method("POST")
    359                 .uri("/setup/unknown-client")
    360                 .body(Body::empty())?,
    361         )
    362         .await?;
    363 
    364     assert_error_response(response, StatusCode::UNAUTHORIZED, "unauthorized").await?;
    365     Ok(())
    366 }
    367 
    368 #[tokio::test]
    369 async fn test_setup_success() -> Result<()> {
    370     let Some(pool) = get_pool().await else { return Ok(()); };
    371     let config = test_config(&std::env::var("DATABASE_URL")?);
    372     let app = build_app(AppState::new(config, pool.clone()));
    373 
    374     let test_client = create_test_client(&pool).await?;
    375 
    376     let response = app
    377         .oneshot(
    378             Request::builder()
    379                 .method("POST")
    380                 .uri(format!("/setup/{}", test_client.client.client_id))
    381                 .header("authorization", format!("Bearer {}", test_client.secret))
    382                 .body(Body::empty())?,
    383         )
    384         .await?;
    385 
    386     assert_eq!(response.status(), StatusCode::OK);
    387 
    388     let bytes = to_bytes(response.into_body(), usize::MAX).await?;
    389     let json: Value = serde_json::from_slice(&bytes)?;
    390     let nonce = json
    391         .get("nonce")
    392         .and_then(|v| v.as_str())
    393         .unwrap_or("");
    394     assert!(!nonce.is_empty());
    395     assert!(nonce.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_'));
    396 
    397     let _ = clients::delete_client(&pool, test_client.client.id).await?;
    398     Ok(())
    399 }
    400 
    401 #[tokio::test]
    402 async fn test_token_success_and_info() -> Result<()> {
    403     let Some(pool) = get_pool().await else { return Ok(()); };
    404     let config = test_config(&std::env::var("DATABASE_URL")?);
    405     let app = build_app(AppState::new(config, pool.clone()));
    406 
    407     let session = setup_session_with_status(&pool, sessions::SessionStatus::Verified).await?;
    408 
    409     let form = form_body(&[
    410         ("grant_type", "authorization_code"),
    411         ("code", &session.authorization_code),
    412         ("client_id", &session.client.client_id),
    413         ("client_secret", &session.secret),
    414         ("redirect_uri", &session.redirect_uri),
    415     ]);
    416 
    417     let response = app
    418         .clone()
    419         .oneshot(
    420             Request::builder()
    421                 .method("POST")
    422                 .uri("/token")
    423                 .header("content-type", "application/x-www-form-urlencoded")
    424                 .body(Body::from(form))?,
    425         )
    426         .await?;
    427 
    428     assert_eq!(response.status(), StatusCode::OK);
    429 
    430     let bytes = to_bytes(response.into_body(), usize::MAX).await?;
    431     let token: TokenResponse = serde_json::from_slice(&bytes)?;
    432     assert!(!token.access_token.is_empty());
    433     assert_eq!(token.token_type, "Bearer");
    434 
    435     let response = app
    436         .oneshot(
    437             Request::builder()
    438                 .method("GET")
    439                 .uri("/info")
    440                 .header("authorization", format!("Bearer {}", token.access_token))
    441                 .body(Body::empty())?,
    442         )
    443         .await?;
    444 
    445     assert_eq!(response.status(), StatusCode::OK);
    446     let bytes = to_bytes(response.into_body(), usize::MAX).await?;
    447     let json: Value = serde_json::from_slice(&bytes)?;
    448     assert_eq!(json.get("vc").and_then(|v| v.as_str()), Some("data"));
    449 
    450     let _ = clients::delete_client(&pool, session.client.id).await?;
    451     Ok(())
    452 }
    453 
    454 #[tokio::test]
    455 async fn test_token_redirect_uri_mismatch() -> Result<()> {
    456     let Some(pool) = get_pool().await else { return Ok(()); };
    457     let config = test_config(&std::env::var("DATABASE_URL")?);
    458     let app = build_app(AppState::new(config, pool.clone()));
    459 
    460     let session = setup_session_with_status(&pool, sessions::SessionStatus::Verified).await?;
    461 
    462     let form = form_body(&[
    463         ("grant_type", "authorization_code"),
    464         ("code", &session.authorization_code),
    465         ("client_id", &session.client.client_id),
    466         ("client_secret", &session.secret),
    467         ("redirect_uri", "https://example.com/wrong"),
    468     ]);
    469 
    470     let response = app
    471         .oneshot(
    472             Request::builder()
    473                 .method("POST")
    474                 .uri("/token")
    475                 .header("content-type", "application/x-www-form-urlencoded")
    476                 .body(Body::from(form))?,
    477         )
    478         .await?;
    479 
    480     assert_error_response(response, StatusCode::BAD_REQUEST, "invalid_grant").await?;
    481 
    482     let _ = clients::delete_client(&pool, session.client.id).await?;
    483     Ok(())
    484 }
    485 
    486 #[tokio::test]
    487 async fn test_status_and_finalize() -> Result<()> {
    488     let Some(pool) = get_pool().await else { return Ok(()); };
    489     let config = test_config(&std::env::var("DATABASE_URL")?);
    490     let app = build_app(AppState::new(config, pool.clone()));
    491 
    492     let session = setup_session_with_status(&pool, sessions::SessionStatus::Verified).await?;
    493 
    494     let response = app
    495         .clone()
    496         .oneshot(
    497             Request::builder()
    498                 .method("GET")
    499                 .uri(format!(
    500                     "/status/{}?state={}",
    501                     session.request_id, session.state
    502                 ))
    503                 .body(Body::empty())?,
    504         )
    505         .await?;
    506 
    507     assert_eq!(response.status(), StatusCode::OK);
    508     let bytes = to_bytes(response.into_body(), usize::MAX).await?;
    509     let json: Value = serde_json::from_slice(&bytes)?;
    510     assert_eq!(json.get("status").and_then(|v| v.as_str()), Some("verified"));
    511 
    512     let response = app
    513         .oneshot(
    514             Request::builder()
    515                 .method("GET")
    516                 .uri(format!(
    517                     "/finalize/{}?state={}",
    518                     session.request_id, session.state
    519                 ))
    520                 .body(Body::empty())?,
    521         )
    522         .await?;
    523 
    524     assert_eq!(response.status(), StatusCode::FOUND);
    525     let location = response
    526         .headers()
    527         .get("location")
    528         .and_then(|v| v.to_str().ok())
    529         .unwrap_or("");
    530     assert!(location.contains("code="));
    531     assert!(location.contains("state="));
    532 
    533     let _ = clients::delete_client(&pool, session.client.id).await?;
    534     Ok(())
    535 }
    536 
    537 #[tokio::test]
    538 async fn test_status_invalid_state() -> Result<()> {
    539     let Some(pool) = get_pool().await else { return Ok(()); };
    540     let config = test_config(&std::env::var("DATABASE_URL")?);
    541     let app = build_app(AppState::new(config, pool.clone()));
    542 
    543     let session = setup_session_with_status(&pool, sessions::SessionStatus::Verified).await?;
    544 
    545     let response = app
    546         .oneshot(
    547             Request::builder()
    548                 .method("GET")
    549                 .uri(format!(
    550                     "/status/{}?state={}",
    551                     session.request_id, "wrong-state"
    552                 ))
    553                 .body(Body::empty())?,
    554         )
    555         .await?;
    556 
    557     assert_error_response(response, StatusCode::FORBIDDEN, "invalid_state").await?;
    558 
    559     let _ = clients::delete_client(&pool, session.client.id).await?;
    560     Ok(())
    561 }
    562 
    563 #[tokio::test]
    564 async fn test_status_not_found() -> Result<()> {
    565     let Some(pool) = get_pool().await else { return Ok(()); };
    566     let config = test_config(&std::env::var("DATABASE_URL")?);
    567     let app = build_app(AppState::new(config, pool));
    568 
    569     let response = app
    570         .oneshot(
    571             Request::builder()
    572                 .method("GET")
    573                 .uri(format!("/status/{}?state=state", Uuid::new_v4()))
    574                 .body(Body::empty())?,
    575         )
    576         .await?;
    577 
    578     assert_error_response(response, StatusCode::NOT_FOUND, "session_not_found").await?;
    579     Ok(())
    580 }
    581 
    582 #[tokio::test]
    583 async fn test_finalize_invalid_state() -> Result<()> {
    584     let Some(pool) = get_pool().await else { return Ok(()); };
    585     let config = test_config(&std::env::var("DATABASE_URL")?);
    586     let app = build_app(AppState::new(config, pool.clone()));
    587 
    588     let session = setup_session_with_status(&pool, sessions::SessionStatus::Verified).await?;
    589 
    590     let response = app
    591         .oneshot(
    592             Request::builder()
    593                 .method("GET")
    594                 .uri(format!(
    595                     "/finalize/{}?state={}",
    596                     session.request_id, "wrong-state"
    597                 ))
    598                 .body(Body::empty())?,
    599         )
    600         .await?;
    601 
    602     assert_error_response(response, StatusCode::FORBIDDEN, "invalid_state").await?;
    603 
    604     let _ = clients::delete_client(&pool, session.client.id).await?;
    605     Ok(())
    606 }
    607 
    608 #[tokio::test]
    609 async fn test_finalize_not_verified() -> Result<()> {
    610     let Some(pool) = get_pool().await else { return Ok(()); };
    611     let config = test_config(&std::env::var("DATABASE_URL")?);
    612     let app = build_app(AppState::new(config, pool.clone()));
    613 
    614     let session = setup_session_with_status(&pool, sessions::SessionStatus::Failed).await?;
    615 
    616     let response = app
    617         .oneshot(
    618             Request::builder()
    619                 .method("GET")
    620                 .uri(format!(
    621                     "/finalize/{}?state={}",
    622                     session.request_id, session.state
    623                 ))
    624                 .body(Body::empty())?,
    625         )
    626         .await?;
    627 
    628     assert_error_response(response, StatusCode::BAD_REQUEST, "not_verified").await?;
    629 
    630     let _ = clients::delete_client(&pool, session.client.id).await?;
    631     Ok(())
    632 }
    633 
    634 #[tokio::test]
    635 async fn test_finalize_not_found() -> Result<()> {
    636     let Some(pool) = get_pool().await else { return Ok(()); };
    637     let config = test_config(&std::env::var("DATABASE_URL")?);
    638     let app = build_app(AppState::new(config, pool));
    639 
    640     let response = app
    641         .oneshot(
    642             Request::builder()
    643                 .method("GET")
    644                 .uri(format!("/finalize/{}?state=state", Uuid::new_v4()))
    645                 .body(Body::empty())?,
    646         )
    647         .await?;
    648 
    649     assert_error_response(response, StatusCode::NOT_FOUND, "session_not_found").await?;
    650     Ok(())
    651 }
    652 
    653 #[tokio::test]
    654 async fn test_token_invalid_grant_type() -> Result<()> {
    655     let Some(pool) = get_pool().await else { return Ok(()); };
    656     let config = test_config(&std::env::var("DATABASE_URL")?);
    657     let app = build_app(AppState::new(config, pool.clone()));
    658 
    659     let session = setup_session_with_status(&pool, sessions::SessionStatus::Verified).await?;
    660 
    661     let form = form_body(&[
    662         ("grant_type", "client_credentials"),
    663         ("code", &session.authorization_code),
    664         ("client_id", &session.client.client_id),
    665         ("client_secret", &session.secret),
    666         ("redirect_uri", &session.redirect_uri),
    667     ]);
    668 
    669     let response = app
    670         .oneshot(
    671             Request::builder()
    672                 .method("POST")
    673                 .uri("/token")
    674                 .header("content-type", "application/x-www-form-urlencoded")
    675                 .body(Body::from(form))?,
    676         )
    677         .await?;
    678 
    679     assert_error_response(response, StatusCode::BAD_REQUEST, "unsupported_grant_type").await?;
    680 
    681     let _ = clients::delete_client(&pool, session.client.id).await?;
    682     Ok(())
    683 }
    684 
    685 #[tokio::test]
    686 async fn test_token_invalid_client() -> Result<()> {
    687     let Some(pool) = get_pool().await else { return Ok(()); };
    688     let config = test_config(&std::env::var("DATABASE_URL")?);
    689     let app = build_app(AppState::new(config, pool.clone()));
    690 
    691     let session = setup_session_with_status(&pool, sessions::SessionStatus::Verified).await?;
    692 
    693     let form = form_body(&[
    694         ("grant_type", "authorization_code"),
    695         ("code", &session.authorization_code),
    696         ("client_id", &session.client.client_id),
    697         ("client_secret", "wrong-secret"),
    698         ("redirect_uri", &session.redirect_uri),
    699     ]);
    700 
    701     let response = app
    702         .oneshot(
    703             Request::builder()
    704                 .method("POST")
    705                 .uri("/token")
    706                 .header("content-type", "application/x-www-form-urlencoded")
    707                 .body(Body::from(form))?,
    708         )
    709         .await?;
    710 
    711     assert_error_response(response, StatusCode::UNAUTHORIZED, "invalid_client").await?;
    712 
    713     let _ = clients::delete_client(&pool, session.client.id).await?;
    714     Ok(())
    715 }
    716 
    717 #[tokio::test]
    718 async fn test_token_used_code_rejected() -> Result<()> {
    719     let Some(pool) = get_pool().await else { return Ok(()); };
    720     let config = test_config(&std::env::var("DATABASE_URL")?);
    721     let app = build_app(AppState::new(config, pool.clone()));
    722 
    723     let session = setup_session_with_status(&pool, sessions::SessionStatus::Verified).await?;
    724 
    725     let _ = authorization_codes::get_code_for_token_exchange(&pool, &session.authorization_code)
    726         .await?
    727         .expect("code should exist");
    728 
    729     let form = form_body(&[
    730         ("grant_type", "authorization_code"),
    731         ("code", &session.authorization_code),
    732         ("client_id", &session.client.client_id),
    733         ("client_secret", &session.secret),
    734         ("redirect_uri", &session.redirect_uri),
    735     ]);
    736 
    737     let response = app
    738         .oneshot(
    739             Request::builder()
    740                 .method("POST")
    741                 .uri("/token")
    742                 .header("content-type", "application/x-www-form-urlencoded")
    743                 .body(Body::from(form))?,
    744         )
    745         .await?;
    746 
    747     assert_error_response(response, StatusCode::BAD_REQUEST, "invalid_grant").await?;
    748 
    749     let _ = clients::delete_client(&pool, session.client.id).await?;
    750     Ok(())
    751 }
    752 
    753 #[tokio::test]
    754 async fn test_token_wrong_session_status() -> Result<()> {
    755     let Some(pool) = get_pool().await else { return Ok(()); };
    756     let config = test_config(&std::env::var("DATABASE_URL")?);
    757     let app = build_app(AppState::new(config, pool.clone()));
    758 
    759     let session = setup_session_with_status(&pool, sessions::SessionStatus::Failed).await?;
    760 
    761     let form = form_body(&[
    762         ("grant_type", "authorization_code"),
    763         ("code", &session.authorization_code),
    764         ("client_id", &session.client.client_id),
    765         ("client_secret", &session.secret),
    766         ("redirect_uri", &session.redirect_uri),
    767     ]);
    768 
    769     let response = app
    770         .oneshot(
    771             Request::builder()
    772                 .method("POST")
    773                 .uri("/token")
    774                 .header("content-type", "application/x-www-form-urlencoded")
    775                 .body(Body::from(form))?,
    776         )
    777         .await?;
    778 
    779     assert_error_response(response, StatusCode::BAD_REQUEST, "invalid_grant").await?;
    780 
    781     let _ = clients::delete_client(&pool, session.client.id).await?;
    782     Ok(())
    783 }
    784 
    785 #[tokio::test]
    786 async fn test_info_missing_authorization() -> Result<()> {
    787     let Some(pool) = get_pool().await else { return Ok(()); };
    788     let config = test_config(&std::env::var("DATABASE_URL")?);
    789     let app = build_app(AppState::new(config, pool));
    790 
    791     let response = app
    792         .oneshot(
    793             Request::builder()
    794                 .method("GET")
    795                 .uri("/info")
    796                 .body(Body::empty())?,
    797         )
    798         .await?;
    799 
    800     assert_error_response(response, StatusCode::UNAUTHORIZED, "invalid_token").await?;
    801     Ok(())
    802 }
    803 
    804 #[tokio::test]
    805 async fn test_token_code_for_different_client() -> Result<()> {
    806     let Some(pool) = get_pool().await else { return Ok(()); };
    807     let config = test_config(&std::env::var("DATABASE_URL")?);
    808     let app = build_app(AppState::new(config, pool.clone()));
    809 
    810     let session = setup_session_with_status(&pool, sessions::SessionStatus::Verified).await?;
    811     let other_client = create_second_client(&pool).await?;
    812 
    813     let form = form_body(&[
    814         ("grant_type", "authorization_code"),
    815         ("code", &session.authorization_code),
    816         ("client_id", &other_client.client.client_id),
    817         ("client_secret", &other_client.secret),
    818         ("redirect_uri", &session.redirect_uri),
    819     ]);
    820 
    821     let response = app
    822         .oneshot(
    823             Request::builder()
    824                 .method("POST")
    825                 .uri("/token")
    826                 .header("content-type", "application/x-www-form-urlencoded")
    827                 .body(Body::from(form))?,
    828         )
    829         .await?;
    830 
    831     assert_error_response(response, StatusCode::BAD_REQUEST, "invalid_grant").await?;
    832 
    833     let _ = clients::delete_client(&pool, session.client.id).await?;
    834     let _ = clients::delete_client(&pool, other_client.client.id).await?;
    835     Ok(())
    836 }
    837 
    838 #[tokio::test]
    839 async fn test_token_expired_code() -> Result<()> {
    840     let Some(pool) = get_pool().await else { return Ok(()); };
    841     let config = test_config(&std::env::var("DATABASE_URL")?);
    842     let app = build_app(AppState::new(config, pool.clone()));
    843 
    844     let session = setup_session_with_status(&pool, sessions::SessionStatus::Verified).await?;
    845 
    846     sqlx::query(
    847         r#"
    848         UPDATE oauth2gw.authorization_codes
    849         SET expires_at = NOW() - INTERVAL '1 minute'
    850         WHERE code = $1
    851         "#,
    852     )
    853     .bind(&session.authorization_code)
    854     .execute(&pool)
    855     .await?;
    856 
    857     let form = form_body(&[
    858         ("grant_type", "authorization_code"),
    859         ("code", &session.authorization_code),
    860         ("client_id", &session.client.client_id),
    861         ("client_secret", &session.secret),
    862         ("redirect_uri", &session.redirect_uri),
    863     ]);
    864 
    865     let response = app
    866         .oneshot(
    867             Request::builder()
    868                 .method("POST")
    869                 .uri("/token")
    870                 .header("content-type", "application/x-www-form-urlencoded")
    871                 .body(Body::from(form))?,
    872         )
    873         .await?;
    874 
    875     assert_error_response(response, StatusCode::BAD_REQUEST, "invalid_grant").await?;
    876 
    877     let _ = clients::delete_client(&pool, session.client.id).await?;
    878     Ok(())
    879 }
    880 
    881 #[tokio::test]
    882 async fn test_info_revoked_token() -> Result<()> {
    883     let Some(pool) = get_pool().await else { return Ok(()); };
    884     let config = test_config(&std::env::var("DATABASE_URL")?);
    885     let app = build_app(AppState::new(config, pool.clone()));
    886 
    887     let session = setup_session_with_status(&pool, sessions::SessionStatus::Verified).await?;
    888 
    889     let form = form_body(&[
    890         ("grant_type", "authorization_code"),
    891         ("code", &session.authorization_code),
    892         ("client_id", &session.client.client_id),
    893         ("client_secret", &session.secret),
    894         ("redirect_uri", &session.redirect_uri),
    895     ]);
    896 
    897     let response = app
    898         .clone()
    899         .oneshot(
    900             Request::builder()
    901                 .method("POST")
    902                 .uri("/token")
    903                 .header("content-type", "application/x-www-form-urlencoded")
    904                 .body(Body::from(form))?,
    905         )
    906         .await?;
    907 
    908     assert_eq!(response.status(), StatusCode::OK);
    909 
    910     let bytes = to_bytes(response.into_body(), usize::MAX).await?;
    911     let token: TokenResponse = serde_json::from_slice(&bytes)?;
    912 
    913     sqlx::query(
    914         r#"
    915         UPDATE oauth2gw.access_tokens
    916         SET revoked = TRUE, revoked_at = NOW()
    917         WHERE token = $1
    918         "#,
    919     )
    920     .bind(&token.access_token)
    921     .execute(&pool)
    922     .await?;
    923 
    924     let response = app
    925         .oneshot(
    926             Request::builder()
    927                 .method("GET")
    928                 .uri("/info")
    929                 .header("authorization", format!("Bearer {}", token.access_token))
    930                 .body(Body::empty())?,
    931         )
    932         .await?;
    933 
    934     assert_error_response(response, StatusCode::UNAUTHORIZED, "invalid_token").await?;
    935 
    936     let _ = clients::delete_client(&pool, session.client.id).await?;
    937     Ok(())
    938 }
    939 
    940 #[tokio::test]
    941 async fn test_authorize_success() -> Result<()> {
    942     let Some(pool) = get_pool().await else { return Ok(()); };
    943 
    944     let mut server = Server::new_async().await;
    945     let verifier_url = server.url();
    946     let config = test_config(&std::env::var("DATABASE_URL")?);
    947     let app = build_app(AppState::new(config, pool.clone()));
    948 
    949     let test_client = create_test_client_with_verifier(&pool, &verifier_url).await?;
    950 
    951     let nonce = format!("nonce-{}", Uuid::new_v4());
    952     let _session = sessions::create_session(&pool, &test_client.client.client_id, &nonce, 5)
    953         .await?
    954         .expect("client should exist");
    955 
    956     let verification_id = Uuid::new_v4();
    957     let response_body = SwiyuManagementResponse {
    958         id: verification_id,
    959         request_nonce: Some("req-nonce".to_string()),
    960         state: SwiyuVerificationStatus::Pending,
    961         verification_url: "https://verifier.example/verify/1".to_string(),
    962         verification_deeplink: Some("swiyu-verify://verify/1".to_string()),
    963         presentation_definition: sample_presentation_definition(),
    964         dcql_query: None,
    965         wallet_response: None,
    966     };
    967     let response_json = serde_json::to_string(&response_body)?;
    968 
    969     let _mock = server
    970         .mock("POST", "/management/api/verifications")
    971         .with_status(200)
    972         .with_header("content-type", "application/json")
    973         .with_body(response_json)
    974         .create_async()
    975         .await;
    976 
    977     let uri = format!(
    978         "/authorize/{}?response_type=code&client_id={}&redirect_uri={}&state={}&scope=first_name",
    979         nonce, test_client.client.client_id, test_client.client.redirect_uri, "state-123"
    980     );
    981 
    982     let response = app
    983         .oneshot(
    984             Request::builder()
    985                 .method("GET")
    986                 .uri(uri)
    987                 .body(Body::empty())?,
    988         )
    989         .await?;
    990 
    991     assert_eq!(response.status(), StatusCode::OK);
    992     let bytes = to_bytes(response.into_body(), usize::MAX).await?;
    993     let json: Value = serde_json::from_slice(&bytes)?;
    994     let verification_id_str = json
    995         .get("verificationId")
    996         .and_then(|v| v.as_str())
    997         .unwrap_or("");
    998     assert_eq!(verification_id_str, verification_id.to_string());
    999 
   1000     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1001     Ok(())
   1002 }
   1003 
   1004 #[tokio::test]
   1005 async fn test_authorize_html_response() -> Result<()> {
   1006     let Some(pool) = get_pool().await else { return Ok(()); };
   1007 
   1008     let mut server = Server::new_async().await;
   1009     let verifier_url = server.url();
   1010     let config = test_config(&std::env::var("DATABASE_URL")?);
   1011     let app = build_app(AppState::new(config, pool.clone()));
   1012 
   1013     let test_client = create_test_client_with_verifier(&pool, &verifier_url).await?;
   1014 
   1015     let nonce = format!("nonce-{}", Uuid::new_v4());
   1016     let _session = sessions::create_session(&pool, &test_client.client.client_id, &nonce, 5)
   1017         .await?
   1018         .expect("client should exist");
   1019 
   1020     let verification_id = Uuid::new_v4();
   1021     let response_body = SwiyuManagementResponse {
   1022         id: verification_id,
   1023         request_nonce: Some("req-nonce".to_string()),
   1024         state: SwiyuVerificationStatus::Pending,
   1025         verification_url: "https://verifier.example/verify/1".to_string(),
   1026         verification_deeplink: Some("swiyu-verify://verify/1".to_string()),
   1027         presentation_definition: sample_presentation_definition(),
   1028         dcql_query: None,
   1029         wallet_response: None,
   1030     };
   1031     let response_json = serde_json::to_string(&response_body)?;
   1032 
   1033     let _mock = server
   1034         .mock("POST", "/management/api/verifications")
   1035         .with_status(200)
   1036         .with_header("content-type", "application/json")
   1037         .with_body(response_json)
   1038         .create_async()
   1039         .await;
   1040 
   1041     let uri = format!(
   1042         "/authorize/{}?response_type=code&client_id={}&redirect_uri={}&state={}&scope=first_name",
   1043         nonce, test_client.client.client_id, test_client.client.redirect_uri, "state-123"
   1044     );
   1045 
   1046     let response = app
   1047         .oneshot(
   1048             Request::builder()
   1049                 .method("GET")
   1050                 .uri(uri)
   1051                 .header("accept", "text/html")
   1052                 .body(Body::empty())?,
   1053         )
   1054         .await?;
   1055 
   1056     assert_eq!(response.status(), StatusCode::OK);
   1057     let content_type = response
   1058         .headers()
   1059         .get("content-type")
   1060         .and_then(|v| v.to_str().ok())
   1061         .unwrap_or("");
   1062     assert!(content_type.contains("text/html"));
   1063     let csp = response
   1064         .headers()
   1065         .get("content-security-policy")
   1066         .and_then(|v| v.to_str().ok())
   1067         .unwrap_or("");
   1068     assert!(csp.contains("default-src 'self'"));
   1069     assert!(csp.contains("script-src 'self' 'unsafe-inline'"));
   1070     let bytes = to_bytes(response.into_body(), usize::MAX).await?;
   1071     let body = String::from_utf8(bytes.to_vec())?;
   1072     assert!(body.contains("Identity Verification"));
   1073     assert!(body.contains("https://verifier.example/verify/1"));
   1074 
   1075     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1076     Ok(())
   1077 }
   1078 
   1079 #[tokio::test]
   1080 async fn test_authorize_invalid_verification_url() -> Result<()> {
   1081     let Some(pool) = get_pool().await else { return Ok(()); };
   1082 
   1083     let mut server = Server::new_async().await;
   1084     let verifier_url = server.url();
   1085     let config = test_config(&std::env::var("DATABASE_URL")?);
   1086     let app = build_app(AppState::new(config, pool.clone()));
   1087 
   1088     let test_client = create_test_client_with_verifier(&pool, &verifier_url).await?;
   1089 
   1090     let nonce = format!("nonce-{}", Uuid::new_v4());
   1091     let _session = sessions::create_session(&pool, &test_client.client.client_id, &nonce, 5)
   1092         .await?
   1093         .expect("client should exist");
   1094 
   1095     let response_body = SwiyuManagementResponse {
   1096         id: Uuid::new_v4(),
   1097         request_nonce: Some("req-nonce".to_string()),
   1098         state: SwiyuVerificationStatus::Pending,
   1099         verification_url: "http://verifier.example/verify/1".to_string(),
   1100         verification_deeplink: Some("swiyu-verify://verify/1".to_string()),
   1101         presentation_definition: sample_presentation_definition(),
   1102         dcql_query: None,
   1103         wallet_response: None,
   1104     };
   1105     let response_json = serde_json::to_string(&response_body)?;
   1106 
   1107     let _mock = server
   1108         .mock("POST", "/management/api/verifications")
   1109         .with_status(200)
   1110         .with_header("content-type", "application/json")
   1111         .with_body(response_json)
   1112         .create_async()
   1113         .await;
   1114 
   1115     let uri = format!(
   1116         "/authorize/{}?response_type=code&client_id={}&redirect_uri={}&state={}&scope=first_name",
   1117         nonce, test_client.client.client_id, test_client.client.redirect_uri, "state-123"
   1118     );
   1119 
   1120     let response = app
   1121         .oneshot(
   1122             Request::builder()
   1123                 .method("GET")
   1124                 .uri(uri)
   1125                 .body(Body::empty())?,
   1126         )
   1127         .await?;
   1128 
   1129     assert_error_response(response, StatusCode::BAD_GATEWAY, "invalid_verification_url").await?;
   1130 
   1131     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1132     Ok(())
   1133 }
   1134 
   1135 #[tokio::test]
   1136 async fn test_authorize_invalid_verification_deeplink() -> Result<()> {
   1137     let Some(pool) = get_pool().await else { return Ok(()); };
   1138 
   1139     let mut server = Server::new_async().await;
   1140     let verifier_url = server.url();
   1141     let config = test_config(&std::env::var("DATABASE_URL")?);
   1142     let app = build_app(AppState::new(config, pool.clone()));
   1143 
   1144     let test_client = create_test_client_with_verifier(&pool, &verifier_url).await?;
   1145 
   1146     let nonce = format!("nonce-{}", Uuid::new_v4());
   1147     let _session = sessions::create_session(&pool, &test_client.client.client_id, &nonce, 5)
   1148         .await?
   1149         .expect("client should exist");
   1150 
   1151     let response_body = SwiyuManagementResponse {
   1152         id: Uuid::new_v4(),
   1153         request_nonce: Some("req-nonce".to_string()),
   1154         state: SwiyuVerificationStatus::Pending,
   1155         verification_url: "https://verifier.example/verify/1".to_string(),
   1156         verification_deeplink: Some("ftp://bad.example".to_string()),
   1157         presentation_definition: sample_presentation_definition(),
   1158         dcql_query: None,
   1159         wallet_response: None,
   1160     };
   1161     let response_json = serde_json::to_string(&response_body)?;
   1162 
   1163     let _mock = server
   1164         .mock("POST", "/management/api/verifications")
   1165         .with_status(200)
   1166         .with_header("content-type", "application/json")
   1167         .with_body(response_json)
   1168         .create_async()
   1169         .await;
   1170 
   1171     let uri = format!(
   1172         "/authorize/{}?response_type=code&client_id={}&redirect_uri={}&state={}&scope=first_name",
   1173         nonce, test_client.client.client_id, test_client.client.redirect_uri, "state-123"
   1174     );
   1175 
   1176     let response = app
   1177         .oneshot(
   1178             Request::builder()
   1179                 .method("GET")
   1180                 .uri(uri)
   1181                 .body(Body::empty())?,
   1182         )
   1183         .await?;
   1184 
   1185     assert_error_response(response, StatusCode::BAD_GATEWAY, "invalid_verification_deeplink").await?;
   1186 
   1187     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1188     Ok(())
   1189 }
   1190 
   1191 #[tokio::test]
   1192 async fn test_authorize_cached_html_response() -> Result<()> {
   1193     let Some(pool) = get_pool().await else { return Ok(()); };
   1194 
   1195     let verifier_url = "https://verifier.example".to_string();
   1196     let config = test_config(&std::env::var("DATABASE_URL")?);
   1197     let app = build_app(AppState::new(config, pool.clone()));
   1198 
   1199     let test_client = create_test_client_with_verifier(&pool, &verifier_url).await?;
   1200 
   1201     let nonce = format!("nonce-{}", Uuid::new_v4());
   1202     let session = sessions::create_session(&pool, &test_client.client.client_id, &nonce, 5)
   1203         .await?
   1204         .expect("client should exist");
   1205 
   1206     let verification_id = Uuid::new_v4().to_string();
   1207     let _ = sessions::update_session_authorized(
   1208         &pool,
   1209         session.id,
   1210         "https://verifier.example/verify/1",
   1211         Some("swiyu-verify://verify/1"),
   1212         &verification_id,
   1213         None,
   1214     )
   1215     .await?;
   1216 
   1217     let uri = format!(
   1218         "/authorize/{}?response_type=code&client_id={}&redirect_uri={}&state={}&scope=first_name",
   1219         nonce, test_client.client.client_id, test_client.client.redirect_uri, "state-123"
   1220     );
   1221 
   1222     let response = app
   1223         .oneshot(
   1224             Request::builder()
   1225                 .method("GET")
   1226                 .uri(uri)
   1227                 .header("accept", "text/html")
   1228                 .body(Body::empty())?,
   1229         )
   1230         .await?;
   1231 
   1232     assert_eq!(response.status(), StatusCode::OK);
   1233     let content_type = response
   1234         .headers()
   1235         .get("content-type")
   1236         .and_then(|v| v.to_str().ok())
   1237         .unwrap_or("");
   1238     assert!(content_type.contains("text/html"));
   1239     let csp = response
   1240         .headers()
   1241         .get("content-security-policy")
   1242         .and_then(|v| v.to_str().ok())
   1243         .unwrap_or("");
   1244     assert!(csp.contains("default-src 'self'"));
   1245     let bytes = to_bytes(response.into_body(), usize::MAX).await?;
   1246     let body = String::from_utf8(bytes.to_vec())?;
   1247     assert!(body.contains("Identity Verification"));
   1248     assert!(body.contains("https://verifier.example/verify/1"));
   1249 
   1250     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1251     Ok(())
   1252 }
   1253 
   1254 #[tokio::test]
   1255 async fn test_authorize_invalid_redirect_uri() -> Result<()> {
   1256     let Some(pool) = get_pool().await else { return Ok(()); };
   1257 
   1258     let verifier_url = "https://verifier.example".to_string();
   1259     let config = test_config(&std::env::var("DATABASE_URL")?);
   1260     let app = build_app(AppState::new(config, pool.clone()));
   1261 
   1262     let test_client = create_test_client_with_verifier(&pool, &verifier_url).await?;
   1263 
   1264     let nonce = format!("nonce-{}", Uuid::new_v4());
   1265     let _session = sessions::create_session(&pool, &test_client.client.client_id, &nonce, 5)
   1266         .await?
   1267         .expect("client should exist");
   1268 
   1269     let uri = format!(
   1270         "/authorize/{}?response_type=code&client_id={}&redirect_uri={}&state={}&scope=first_name",
   1271         nonce, test_client.client.client_id, "https://example.com/wrong", "state-123"
   1272     );
   1273 
   1274     let response = app
   1275         .oneshot(
   1276             Request::builder()
   1277                 .method("GET")
   1278                 .uri(uri)
   1279                 .body(Body::empty())?,
   1280         )
   1281         .await?;
   1282 
   1283     assert_error_response(response, StatusCode::BAD_REQUEST, "invalid_redirect_uri").await?;
   1284 
   1285     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1286     Ok(())
   1287 }
   1288 
   1289 #[tokio::test]
   1290 async fn test_authorize_session_expired() -> Result<()> {
   1291     let Some(pool) = get_pool().await else { return Ok(()); };
   1292 
   1293     let verifier_url = "https://verifier.example".to_string();
   1294     let config = test_config(&std::env::var("DATABASE_URL")?);
   1295     let app = build_app(AppState::new(config, pool.clone()));
   1296 
   1297     let test_client = create_test_client_with_verifier(&pool, &verifier_url).await?;
   1298 
   1299     let nonce = format!("nonce-{}", Uuid::new_v4());
   1300     let session = sessions::create_session(&pool, &test_client.client.client_id, &nonce, 5)
   1301         .await?
   1302         .expect("client should exist");
   1303 
   1304     sqlx::query(
   1305         r#"
   1306         UPDATE oauth2gw.verification_sessions
   1307         SET expires_at = NOW() - INTERVAL '1 minute'
   1308         WHERE id = $1
   1309         "#,
   1310     )
   1311     .bind(session.id)
   1312     .execute(&pool)
   1313     .await?;
   1314 
   1315     let uri = format!(
   1316         "/authorize/{}?response_type=code&client_id={}&redirect_uri={}&state={}&scope=first_name",
   1317         nonce, test_client.client.client_id, test_client.client.redirect_uri, "state-123"
   1318     );
   1319 
   1320     let response = app
   1321         .oneshot(
   1322             Request::builder()
   1323                 .method("GET")
   1324                 .uri(uri)
   1325                 .body(Body::empty())?,
   1326         )
   1327         .await?;
   1328 
   1329     assert_error_response(response, StatusCode::GONE, "session_expired").await?;
   1330 
   1331     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1332     Ok(())
   1333 }
   1334 
   1335 #[tokio::test]
   1336 async fn test_authorize_invalid_response_type() -> Result<()> {
   1337     let Some(pool) = get_pool().await else { return Ok(()); };
   1338 
   1339     let verifier_url = "https://verifier.example".to_string();
   1340     let config = test_config(&std::env::var("DATABASE_URL")?);
   1341     let app = build_app(AppState::new(config, pool.clone()));
   1342 
   1343     let test_client = create_test_client_with_verifier(&pool, &verifier_url).await?;
   1344 
   1345     let nonce = format!("nonce-{}", Uuid::new_v4());
   1346     let _session = sessions::create_session(&pool, &test_client.client.client_id, &nonce, 5)
   1347         .await?
   1348         .expect("client should exist");
   1349 
   1350     let uri = format!(
   1351         "/authorize/{}?response_type=token&client_id={}&redirect_uri={}&state={}&scope=first_name",
   1352         nonce, test_client.client.client_id, test_client.client.redirect_uri, "state-123"
   1353     );
   1354 
   1355     let response = app
   1356         .oneshot(
   1357             Request::builder()
   1358                 .method("GET")
   1359                 .uri(uri)
   1360                 .body(Body::empty())?,
   1361         )
   1362         .await?;
   1363 
   1364     assert_error_response(response, StatusCode::BAD_REQUEST, "invalid_request").await?;
   1365 
   1366     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1367     Ok(())
   1368 }
   1369 
   1370 #[tokio::test]
   1371 async fn test_authorize_invalid_scope() -> Result<()> {
   1372     let Some(pool) = get_pool().await else { return Ok(()); };
   1373 
   1374     let verifier_url = "https://verifier.example".to_string();
   1375 
   1376     let mut config = test_config(&std::env::var("DATABASE_URL")?);
   1377     config.allowed_scopes = Some(vec!["first_name".to_string(), "last_name".to_string()]);
   1378 
   1379     let app = build_app(AppState::new(config, pool.clone()));
   1380 
   1381     let test_client = create_test_client_with_verifier(&pool, &verifier_url).await?;
   1382 
   1383     let nonce = format!("nonce-{}", Uuid::new_v4());
   1384     let _session = sessions::create_session(&pool, &test_client.client.client_id, &nonce, 5)
   1385         .await?
   1386         .expect("client should exist");
   1387 
   1388     let uri = format!(
   1389         "/authorize/{}?response_type=code&client_id={}&redirect_uri={}&state={}&scope=invalid_scope",
   1390         nonce, test_client.client.client_id, test_client.client.redirect_uri, "state-123"
   1391     );
   1392 
   1393     let response = app
   1394         .oneshot(
   1395             Request::builder()
   1396                 .method("GET")
   1397                 .uri(uri)
   1398                 .body(Body::empty())?,
   1399         )
   1400         .await?;
   1401 
   1402     assert_error_response(response, StatusCode::BAD_REQUEST, "invalid_scope").await?;
   1403 
   1404     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1405     Ok(())
   1406 }
   1407 
   1408 #[tokio::test]
   1409 async fn test_authorize_session_status_conflict() -> Result<()> {
   1410     let Some(pool) = get_pool().await else { return Ok(()); };
   1411 
   1412     let verifier_url = "https://verifier.example".to_string();
   1413     let config = test_config(&std::env::var("DATABASE_URL")?);
   1414     let app = build_app(AppState::new(config, pool.clone()));
   1415 
   1416     let test_client = create_test_client_with_verifier(&pool, &verifier_url).await?;
   1417 
   1418     let nonce = format!("nonce-{}", Uuid::new_v4());
   1419     let session = sessions::create_session(&pool, &test_client.client.client_id, &nonce, 5)
   1420         .await?
   1421         .expect("client should exist");
   1422 
   1423     sqlx::query(
   1424         r#"
   1425         UPDATE oauth2gw.verification_sessions
   1426         SET status = 'completed'
   1427         WHERE id = $1
   1428         "#,
   1429     )
   1430     .bind(session.id)
   1431     .execute(&pool)
   1432     .await?;
   1433 
   1434     let uri = format!(
   1435         "/authorize/{}?response_type=code&client_id={}&redirect_uri={}&state={}&scope=first_name",
   1436         nonce, test_client.client.client_id, test_client.client.redirect_uri, "state-123"
   1437     );
   1438 
   1439     let response = app
   1440         .oneshot(
   1441             Request::builder()
   1442                 .method("GET")
   1443                 .uri(uri)
   1444                 .body(Body::empty())?,
   1445         )
   1446         .await?;
   1447 
   1448     assert_error_response(response, StatusCode::CONFLICT, "invalid_session_status").await?;
   1449 
   1450     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1451     Ok(())
   1452 }
   1453 
   1454 #[tokio::test]
   1455 async fn test_authorize_verifier_error() -> Result<()> {
   1456     let Some(pool) = get_pool().await else { return Ok(()); };
   1457 
   1458     let mut server = Server::new_async().await;
   1459     let verifier_url = server.url();
   1460     let config = test_config(&std::env::var("DATABASE_URL")?);
   1461     let app = build_app(AppState::new(config, pool.clone()));
   1462 
   1463     let test_client = create_test_client_with_verifier(&pool, &verifier_url).await?;
   1464 
   1465     let nonce = format!("nonce-{}", Uuid::new_v4());
   1466     let _session = sessions::create_session(&pool, &test_client.client.client_id, &nonce, 5)
   1467         .await?
   1468         .expect("client should exist");
   1469 
   1470     let _mock = server
   1471         .mock("POST", "/management/api/verifications")
   1472         .with_status(500)
   1473         .with_header("content-type", "application/json")
   1474         .with_body("{\"error\":\"boom\"}")
   1475         .create_async()
   1476         .await;
   1477 
   1478     let uri = format!(
   1479         "/authorize/{}?response_type=code&client_id={}&redirect_uri={}&state={}&scope=first_name",
   1480         nonce, test_client.client.client_id, test_client.client.redirect_uri, "state-123"
   1481     );
   1482 
   1483     let response = app
   1484         .oneshot(
   1485             Request::builder()
   1486                 .method("GET")
   1487                 .uri(uri)
   1488                 .body(Body::empty())?,
   1489         )
   1490         .await?;
   1491 
   1492     assert_error_response(response, StatusCode::BAD_GATEWAY, "verifier_error").await?;
   1493 
   1494     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1495     Ok(())
   1496 }
   1497 
   1498 #[tokio::test]
   1499 async fn test_authorize_verifier_invalid_json() -> Result<()> {
   1500     let Some(pool) = get_pool().await else { return Ok(()); };
   1501 
   1502     let mut server = Server::new_async().await;
   1503     let verifier_url = server.url();
   1504     let config = test_config(&std::env::var("DATABASE_URL")?);
   1505     let app = build_app(AppState::new(config, pool.clone()));
   1506 
   1507     let test_client = create_test_client_with_verifier(&pool, &verifier_url).await?;
   1508 
   1509     let nonce = format!("nonce-{}", Uuid::new_v4());
   1510     let _session = sessions::create_session(&pool, &test_client.client.client_id, &nonce, 5)
   1511         .await?
   1512         .expect("client should exist");
   1513 
   1514     let _mock = server
   1515         .mock("POST", "/management/api/verifications")
   1516         .with_status(200)
   1517         .with_header("content-type", "application/json")
   1518         .with_body("not-json")
   1519         .create_async()
   1520         .await;
   1521 
   1522     let uri = format!(
   1523         "/authorize/{}?response_type=code&client_id={}&redirect_uri={}&state={}&scope=first_name",
   1524         nonce, test_client.client.client_id, test_client.client.redirect_uri, "state-123"
   1525     );
   1526 
   1527     let response = app
   1528         .oneshot(
   1529             Request::builder()
   1530                 .method("GET")
   1531                 .uri(uri)
   1532                 .body(Body::empty())?,
   1533         )
   1534         .await?;
   1535 
   1536     assert_error_response(response, StatusCode::BAD_GATEWAY, "verifier_invalid_response").await?;
   1537 
   1538     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1539     Ok(())
   1540 }
   1541 
   1542 #[tokio::test]
   1543 async fn test_authorize_idempotent_cached_response() -> Result<()> {
   1544     let Some(pool) = get_pool().await else { return Ok(()); };
   1545 
   1546     let verifier_url = "https://verifier.example".to_string();
   1547     let config = test_config(&std::env::var("DATABASE_URL")?);
   1548     let app = build_app(AppState::new(config, pool.clone()));
   1549 
   1550     let test_client = create_test_client_with_verifier(&pool, &verifier_url).await?;
   1551 
   1552     let nonce = format!("nonce-{}", Uuid::new_v4());
   1553     let session = sessions::create_session(&pool, &test_client.client.client_id, &nonce, 5)
   1554         .await?
   1555         .expect("client should exist");
   1556 
   1557     let verification_id = Uuid::new_v4().to_string();
   1558     let _ = sessions::update_session_authorized(
   1559         &pool,
   1560         session.id,
   1561         "https://verifier.example/verify/1",
   1562         Some("swiyu-verify://verify/1"),
   1563         &verification_id,
   1564         None,
   1565     )
   1566     .await?;
   1567 
   1568     let uri = format!(
   1569         "/authorize/{}?response_type=code&client_id={}&redirect_uri={}&state={}&scope=first_name",
   1570         nonce, test_client.client.client_id, test_client.client.redirect_uri, "state-123"
   1571     );
   1572 
   1573     let response = app
   1574         .oneshot(
   1575             Request::builder()
   1576                 .method("GET")
   1577                 .uri(uri)
   1578                 .body(Body::empty())?,
   1579         )
   1580         .await?;
   1581 
   1582     assert_eq!(response.status(), StatusCode::OK);
   1583     let bytes = to_bytes(response.into_body(), usize::MAX).await?;
   1584     let json: Value = serde_json::from_slice(&bytes)?;
   1585     let verification_id_str = json
   1586         .get("verificationId")
   1587         .and_then(|v| v.as_str())
   1588         .unwrap_or("");
   1589     assert_eq!(verification_id_str, verification_id);
   1590     let verification_url = json
   1591         .get("verification_url")
   1592         .and_then(|v| v.as_str())
   1593         .unwrap_or("");
   1594     assert_eq!(verification_url, "https://verifier.example/verify/1");
   1595 
   1596     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1597     Ok(())
   1598 }
   1599 
   1600 #[tokio::test]
   1601 async fn test_notification_success() -> Result<()> {
   1602     let Some(pool) = get_pool().await else { return Ok(()); };
   1603 
   1604     let mut server = Server::new_async().await;
   1605     let verifier_url = server.url();
   1606     let config = test_config(&std::env::var("DATABASE_URL")?);
   1607     let app = build_app(AppState::new(config, pool.clone()));
   1608 
   1609     let test_client = create_test_client_with_verifier(&pool, &verifier_url).await?;
   1610 
   1611     let nonce = format!("nonce-{}", Uuid::new_v4());
   1612     let session = sessions::create_session(&pool, &test_client.client.client_id, &nonce, 5)
   1613         .await?
   1614         .expect("client should exist");
   1615 
   1616     let verification_id = Uuid::new_v4();
   1617     let _ = sessions::update_session_authorized(
   1618         &pool,
   1619         session.id,
   1620         "https://verifier.example/verify/1",
   1621         None,
   1622         &verification_id.to_string(),
   1623         None,
   1624     )
   1625     .await?;
   1626 
   1627     let response_body = SwiyuManagementResponse {
   1628         id: verification_id,
   1629         request_nonce: Some("req-nonce".to_string()),
   1630         state: SwiyuVerificationStatus::Success,
   1631         verification_url: "https://verifier.example/verify/1".to_string(),
   1632         verification_deeplink: Some("swiyu-verify://verify/1".to_string()),
   1633         presentation_definition: sample_presentation_definition(),
   1634         dcql_query: None,
   1635         wallet_response: Some(serde_json::json!({"vc": "data"})),
   1636     };
   1637     let response_json = serde_json::to_string(&response_body)?;
   1638 
   1639     let path = format!("/management/api/verifications/{}", verification_id);
   1640     let _mock = server
   1641         .mock("GET", path.as_str())
   1642         .with_status(200)
   1643         .with_header("content-type", "application/json")
   1644         .with_body(response_json)
   1645         .create_async()
   1646         .await;
   1647 
   1648     let webhook = serde_json::json!({
   1649         "verification_id": verification_id,
   1650         "timestamp": "2025-01-01T00:00:00Z",
   1651     });
   1652 
   1653     let response = app
   1654         .oneshot(
   1655             Request::builder()
   1656                 .method("POST")
   1657                 .uri("/notification")
   1658                 .header("content-type", "application/json")
   1659                 .body(Body::from(webhook.to_string()))?,
   1660         )
   1661         .await?;
   1662 
   1663     assert_eq!(response.status(), StatusCode::OK);
   1664 
   1665     let status = get_session_status(&pool, session.id).await?;
   1666     assert_eq!(status, sessions::SessionStatus::Verified);
   1667 
   1668     let code = authorization_codes::get_code_by_session(&pool, session.id)
   1669         .await?
   1670         .expect("authorization code should exist");
   1671     assert!(!code.code.is_empty());
   1672 
   1673     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1674     Ok(())
   1675 }
   1676 
   1677 #[tokio::test]
   1678 async fn test_notification_pending_ignored() -> Result<()> {
   1679     let Some(pool) = get_pool().await else { return Ok(()); };
   1680 
   1681     let mut server = Server::new_async().await;
   1682     let verifier_url = server.url();
   1683     let config = test_config(&std::env::var("DATABASE_URL")?);
   1684     let app = build_app(AppState::new(config, pool.clone()));
   1685 
   1686     let (test_client, session_id, verification_id) =
   1687         setup_authorized_session(&pool, &verifier_url).await?;
   1688 
   1689     let response_body = SwiyuManagementResponse {
   1690         id: verification_id,
   1691         request_nonce: Some("req-nonce".to_string()),
   1692         state: SwiyuVerificationStatus::Pending,
   1693         verification_url: "https://verifier.example/verify/1".to_string(),
   1694         verification_deeplink: Some("swiyu-verify://verify/1".to_string()),
   1695         presentation_definition: sample_presentation_definition(),
   1696         dcql_query: None,
   1697         wallet_response: None,
   1698     };
   1699     let response_json = serde_json::to_string(&response_body)?;
   1700 
   1701     let path = format!("/management/api/verifications/{}", verification_id);
   1702     let _mock = server
   1703         .mock("GET", path.as_str())
   1704         .with_status(200)
   1705         .with_header("content-type", "application/json")
   1706         .with_body(response_json)
   1707         .create_async()
   1708         .await;
   1709 
   1710     let webhook = serde_json::json!({
   1711         "verification_id": verification_id,
   1712         "timestamp": "2025-01-01T00:00:00Z",
   1713     });
   1714 
   1715     let response = app
   1716         .oneshot(
   1717             Request::builder()
   1718                 .method("POST")
   1719                 .uri("/notification")
   1720                 .header("content-type", "application/json")
   1721                 .body(Body::from(webhook.to_string()))?,
   1722         )
   1723         .await?;
   1724 
   1725     assert_eq!(response.status(), StatusCode::OK);
   1726 
   1727     let status = get_session_status(&pool, session_id).await?;
   1728     assert_eq!(status, sessions::SessionStatus::Authorized);
   1729     let code = authorization_codes::get_code_by_session(&pool, session_id).await?;
   1730     assert!(code.is_none());
   1731 
   1732     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1733     Ok(())
   1734 }
   1735 
   1736 #[tokio::test]
   1737 async fn test_notification_verifier_error() -> Result<()> {
   1738     let Some(pool) = get_pool().await else { return Ok(()); };
   1739 
   1740     let mut server = Server::new_async().await;
   1741     let verifier_url = server.url();
   1742     let config = test_config(&std::env::var("DATABASE_URL")?);
   1743     let app = build_app(AppState::new(config, pool.clone()));
   1744 
   1745     let (test_client, session_id, verification_id) =
   1746         setup_authorized_session(&pool, &verifier_url).await?;
   1747 
   1748     let path = format!("/management/api/verifications/{}", verification_id);
   1749     let _mock = server
   1750         .mock("GET", path.as_str())
   1751         .with_status(500)
   1752         .with_header("content-type", "application/json")
   1753         .with_body("{\"error\":\"boom\"}")
   1754         .create_async()
   1755         .await;
   1756 
   1757     let webhook = serde_json::json!({
   1758         "verification_id": verification_id,
   1759         "timestamp": "2025-01-01T00:00:00Z",
   1760     });
   1761 
   1762     let response = app
   1763         .oneshot(
   1764             Request::builder()
   1765                 .method("POST")
   1766                 .uri("/notification")
   1767                 .header("content-type", "application/json")
   1768                 .body(Body::from(webhook.to_string()))?,
   1769         )
   1770         .await?;
   1771 
   1772     assert_eq!(response.status(), StatusCode::OK);
   1773 
   1774     let status = get_session_status(&pool, session_id).await?;
   1775     assert_eq!(status, sessions::SessionStatus::Authorized);
   1776     let code = authorization_codes::get_code_by_session(&pool, session_id).await?;
   1777     assert!(code.is_none());
   1778 
   1779     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1780     Ok(())
   1781 }
   1782 
   1783 #[tokio::test]
   1784 async fn test_notification_verifier_invalid_json() -> Result<()> {
   1785     let Some(pool) = get_pool().await else { return Ok(()); };
   1786 
   1787     let mut server = Server::new_async().await;
   1788     let verifier_url = server.url();
   1789     let config = test_config(&std::env::var("DATABASE_URL")?);
   1790     let app = build_app(AppState::new(config, pool.clone()));
   1791 
   1792     let (test_client, session_id, verification_id) =
   1793         setup_authorized_session(&pool, &verifier_url).await?;
   1794 
   1795     let path = format!("/management/api/verifications/{}", verification_id);
   1796     let _mock = server
   1797         .mock("GET", path.as_str())
   1798         .with_status(200)
   1799         .with_header("content-type", "application/json")
   1800         .with_body("not-json")
   1801         .create_async()
   1802         .await;
   1803 
   1804     let webhook = serde_json::json!({
   1805         "verification_id": verification_id,
   1806         "timestamp": "2025-01-01T00:00:00Z",
   1807     });
   1808 
   1809     let response = app
   1810         .oneshot(
   1811             Request::builder()
   1812                 .method("POST")
   1813                 .uri("/notification")
   1814                 .header("content-type", "application/json")
   1815                 .body(Body::from(webhook.to_string()))?,
   1816         )
   1817         .await?;
   1818 
   1819     assert_eq!(response.status(), StatusCode::OK);
   1820 
   1821     let status = get_session_status(&pool, session_id).await?;
   1822     assert_eq!(status, sessions::SessionStatus::Authorized);
   1823     let code = authorization_codes::get_code_by_session(&pool, session_id).await?;
   1824     assert!(code.is_none());
   1825 
   1826     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1827     Ok(())
   1828 }
   1829 
   1830 #[tokio::test]
   1831 async fn test_notification_failed_verification() -> Result<()> {
   1832     let Some(pool) = get_pool().await else { return Ok(()); };
   1833 
   1834     let mut server = Server::new_async().await;
   1835     let verifier_url = server.url();
   1836     let config = test_config(&std::env::var("DATABASE_URL")?);
   1837     let app = build_app(AppState::new(config, pool.clone()));
   1838 
   1839     let (test_client, session_id, verification_id) =
   1840         setup_authorized_session(&pool, &verifier_url).await?;
   1841 
   1842     let response_body = SwiyuManagementResponse {
   1843         id: verification_id,
   1844         request_nonce: Some("req-nonce".to_string()),
   1845         state: SwiyuVerificationStatus::Failed,
   1846         verification_url: "https://verifier.example/verify/1".to_string(),
   1847         verification_deeplink: Some("swiyu-verify://verify/1".to_string()),
   1848         presentation_definition: sample_presentation_definition(),
   1849         dcql_query: None,
   1850         wallet_response: None,
   1851     };
   1852     let response_json = serde_json::to_string(&response_body)?;
   1853 
   1854     let path = format!("/management/api/verifications/{}", verification_id);
   1855     let _mock = server
   1856         .mock("GET", path.as_str())
   1857         .with_status(200)
   1858         .with_header("content-type", "application/json")
   1859         .with_body(response_json)
   1860         .create_async()
   1861         .await;
   1862 
   1863     let webhook = serde_json::json!({
   1864         "verification_id": verification_id,
   1865         "timestamp": "2025-01-01T00:00:00Z",
   1866     });
   1867 
   1868     let response = app
   1869         .oneshot(
   1870             Request::builder()
   1871                 .method("POST")
   1872                 .uri("/notification")
   1873                 .header("content-type", "application/json")
   1874                 .body(Body::from(webhook.to_string()))?,
   1875         )
   1876         .await?;
   1877 
   1878     assert_eq!(response.status(), StatusCode::OK);
   1879 
   1880     let status = get_session_status(&pool, session_id).await?;
   1881     assert_eq!(status, sessions::SessionStatus::Failed);
   1882 
   1883     let _ = clients::delete_client(&pool, test_client.client.id).await?;
   1884     Ok(())
   1885 }