kych

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

handlers_integration.rs (63077B)


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