use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use serde_json::{Value, json};
use tower::ServiceExt;
use vta_service::test_support::{TestAppContext, build_test_app};
async fn request(router: &axum::Router, req: Request<Body>) -> (StatusCode, Value) {
let resp = router.clone().oneshot(req).await.expect("request failed");
let status = resp.status();
let body = resp.into_body().collect().await.unwrap().to_bytes();
let json: Value = serde_json::from_slice(&body)
.unwrap_or_else(|_| json!({"raw": String::from_utf8_lossy(&body).to_string()}));
(status, json)
}
fn post_json(uri: &str, body: Value) -> Request<Body> {
Request::builder()
.method("POST")
.uri(uri)
.header("content-type", "application/json")
.header("x-forwarded-for", "203.0.113.1")
.body(Body::from(body.to_string()))
.unwrap()
}
fn post_raw(uri: &str, body: String) -> Request<Body> {
Request::builder()
.method("POST")
.uri(uri)
.header("content-type", "text/plain")
.header("x-forwarded-for", "203.0.113.1")
.body(Body::from(body))
.unwrap()
}
#[tokio::test]
async fn challenge_endpoint_issues_session_and_persists_it() {
let (router, ctx) = build_test_app().await;
let did = "did:key:z6MkChallengeTester";
let entry = vti_common::acl::AclEntry::new(did, vti_common::acl::Role::Admin, "test")
.with_created_at(1);
vti_common::acl::store_acl_entry(&ctx.acl_ks, &entry)
.await
.expect("seed admin ACL");
let (status, body) = request(&router, post_json("/auth/challenge", json!({"did": did}))).await;
assert_eq!(
status,
StatusCode::OK,
"challenge issuance must succeed for an ACL-permitted DID; got body: {body}"
);
let session_id = body["sessionId"].as_str().expect("sessionId in response");
let challenge = body["challenge"].as_str().expect("challenge in response");
assert!(
body["expiresAt"].as_str().is_some(),
"canonical shape includes expiresAt: {body}"
);
assert!(!session_id.is_empty(), "session_id must be non-empty");
assert!(!challenge.is_empty(), "challenge must be non-empty");
let session_row = vti_common::auth::session::get_session(&ctx.sessions_ks, session_id)
.await
.expect("session lookup");
let session = session_row.expect("session row was persisted");
assert_eq!(
session.did, did,
"persisted session must record the DID that requested the challenge"
);
assert_eq!(
session.challenge, challenge,
"persisted challenge must match the one returned to the client (so `/auth/` can verify the signature against the same nonce the client signed)"
);
}
#[tokio::test]
async fn plaintext_didcomm_with_forged_sender_is_rejected() {
use vta_service::test_support::{TestAppOptions, build_offline_atm, build_test_app_with};
let (router, ctx) = build_test_app_with(TestAppOptions {
atm: Some(build_offline_atm().await),
..Default::default()
})
.await;
let admin_did = "did:key:z6MkForgedAdminTarget";
let entry = vti_common::acl::AclEntry::new(admin_did, vti_common::acl::Role::Admin, "test")
.with_created_at(1);
vti_common::acl::store_acl_entry(&ctx.acl_ks, &entry)
.await
.expect("seed admin ACL");
let (status, body) = request(
&router,
post_json("/auth/challenge", json!({"did": admin_did})),
)
.await;
assert_eq!(status, StatusCode::OK, "challenge issuance: {body}");
let session_id = body["sessionId"].as_str().expect("sessionId");
let challenge = body["challenge"].as_str().expect("challenge");
let forged = json!({
"id": "attacker-supplied-id",
"typ": "application/didcomm-plain+json",
"type": "https://trusttasks.org/spec/auth/authenticate/0.1",
"from": admin_did,
"to": ["did:key:z6MkVtaServiceUnderTest"],
"body": { "challenge": challenge, "session_id": session_id },
});
let (status, body) = request(&router, post_raw("/auth/", forged.to_string())).await;
assert_eq!(
status,
StatusCode::UNAUTHORIZED,
"a plaintext DIDComm message with a forged sender must be rejected, not issued an admin JWT; got body: {body}"
);
let err = body["error"].as_str().unwrap_or_default();
assert!(
err.contains("authenticated (authcrypt) DIDComm envelope"),
"401 must be attributable to the plaintext/authcrypt guard, got: {body}"
);
assert!(
body.get("tokens").is_none() && body.get("access_token").is_none(),
"no token may be issued for a forged plaintext message: {body}"
);
let session = vti_common::auth::session::get_session(&ctx.sessions_ks, session_id)
.await
.expect("session lookup")
.expect("challenge session still present");
assert_eq!(
session.state,
vti_common::auth::session::SessionState::ChallengeSent,
"forged authenticate attempt must not transition the session to Authenticated"
);
}
#[tokio::test]
async fn refresh_endpoint_rejects_malformed_token_with_401() {
let (router, _ctx) = build_test_app().await;
let (status, _body) = request(
&router,
post_json(
"/auth/refresh",
json!({"refresh_token": "not-a-real-refresh-token"}),
),
)
.await;
assert_eq!(
status,
StatusCode::UNAUTHORIZED,
"malformed refresh token must surface as 401, not 500"
);
}
#[tokio::test]
async fn refresh_endpoint_rejects_unknown_token_with_401() {
let (router, _ctx) = build_test_app().await;
let (status, _body) = request(
&router,
post_json(
"/auth/refresh",
json!({"refresh_token": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}),
),
)
.await;
assert_eq!(
status,
StatusCode::UNAUTHORIZED,
"unknown refresh token must surface as 401"
);
}
#[tokio::test]
async fn plaintext_didcomm_refresh_is_rejected() {
use vta_service::test_support::{TestAppOptions, build_offline_atm, build_test_app_with};
let (router, _ctx) = build_test_app_with(TestAppOptions {
atm: Some(build_offline_atm().await),
..Default::default()
})
.await;
let forged = json!({
"id": "attacker-supplied-id",
"typ": "application/didcomm-plain+json",
"type": "https://trusttasks.org/spec/auth/refresh/0.1",
"from": "did:key:z6MkForgedAdminTarget",
"to": ["did:key:z6MkVtaServiceUnderTest"],
"body": { "refresh_token": "stolen-or-guessed-token" },
});
let (status, body) = request(&router, post_raw("/auth/refresh", forged.to_string())).await;
assert_eq!(
status,
StatusCode::UNAUTHORIZED,
"a plaintext DIDComm refresh must be rejected by the authcrypt guard; got: {body}"
);
let err = body["error"].as_str().unwrap_or_default();
assert!(
err.contains("authenticated (authcrypt) DIDComm envelope"),
"401 must be attributable to the refresh authcrypt guard, got: {body}"
);
assert!(
body.get("tokens").is_none(),
"no token may be issued for a forged plaintext refresh: {body}"
);
}
#[tokio::test]
async fn test_app_context_exposes_required_keyspaces() {
let (_router, ctx) = build_test_app().await;
let _: &TestAppContext = &ctx;
let _sessions = ctx.sessions_ks.clone();
let _acl = ctx.acl_ks.clone();
let _jwt = ctx.jwt_keys.clone();
}