kych

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

handlers_integration.rs (61511B)


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