Lines
97.97 %
Functions
94.12 %
Branches
100 %
use axum::{
Router,
body::Body,
http::{Request, StatusCode},
routing::{get, post},
};
use serde_json::json;
use tower::ServiceExt;
use crate::common::{create_mock_jwt_auth, create_mock_user, create_test_app_state};
#[supp_macro::shared_runtime_test]
async fn test_login_handler_with_valid_json() {
let app_state = create_test_app_state().await;
let app = Router::new()
.route("/auth/login", post(web::handler::login_user_handler))
.with_state(app_state);
let login_data = json!({
"email": "test@example.com",
"password": "password123"
});
let response = app
.oneshot(
Request::builder()
.method("GET")
.uri("/auth/login")
.header("content-type", "application/json")
.body(Body::from(login_data.to_string()))
.unwrap(),
)
.await
.unwrap();
// Will likely be 500 or other error due to missing DB, but JSON was parsed
assert!(response.status().is_client_error() || response.status().is_server_error());
}
async fn test_login_handler_with_invalid_json() {
.body(Body::from("invalid json"))
// May return 400 for invalid JSON or 405 if route middleware blocks access
assert!(
response.status() == StatusCode::BAD_REQUEST
|| response.status() == StatusCode::METHOD_NOT_ALLOWED,
"Expected 400 or 405, got: {}",
response.status()
);
async fn test_login_handler_missing_content_type() {
// May return 415 for missing content-type or 405 if route middleware blocks access
response.status() == StatusCode::UNSUPPORTED_MEDIA_TYPE
"Expected 415 or 405, got: {}",
async fn test_register_handler_with_valid_json() {
.route("/auth/register", post(web::handler::register_user_handler))
let register_data = json!({
"name": "Test User",
"email": "newuser@example.com",
.uri("/auth/register")
.body(Body::from(register_data.to_string()))
async fn test_register_handler_with_invalid_json() {
async fn test_register_handler_missing_fields() {
"name": "Test User"
// Missing email and password
// May return 422 for missing fields or 405 if route middleware blocks access
response.status() == StatusCode::UNPROCESSABLE_ENTITY
"Expected 422 or 405, got: {}",
async fn test_refresh_handler_without_cookie() {
.route(
"/auth/refresh",
get(web::handler::refresh_access_token_handler),
// The route lives behind `auth` in production, which is what inserts
// this extension; without it axum rejects before the handler runs.
.layer(axum::Extension(create_mock_jwt_auth(create_mock_user())))
.uri("/auth/refresh")
.body(Body::empty())
// Should return 403 for missing refresh token
assert_eq!(response.status(), StatusCode::FORBIDDEN);
async fn test_refresh_handler_with_simple_invalid_cookie() {
.header("cookie", "refresh_token=invalid")
// Should return 403 for invalid refresh token or other auth error
response.status() == StatusCode::FORBIDDEN
|| response.status().is_client_error()
|| response.status().is_server_error(),
"Expected auth error, got: {}",
async fn test_login_handler_email_normalization() {
"email": " Test@EXAMPLE.COM ",
// Verify that the handler processes the request (even if it fails due to DB issues)
// The key is that it doesn't fail due to malformed email
async fn test_register_handler_email_normalization() {
"email": " NewUser@EXAMPLE.COM ",
/// Registration must produce a *verified* user.
///
/// `jwt_auth` re-reads `users.verified` on every authenticated request and 401s
/// when it is false, and nothing in the codebase ever sets the flag — there is
/// no verification endpoint and no email flow. So an account created with the
/// column default authenticates once and is then unusable, which is exactly
/// what the first real registration hit on 2026-09-06. Registration is
/// admin-gated, so the admin creating the account has already vouched for it.
async fn test_register_handler_creates_a_verified_user() {
let email = format!(
"verified-probe-{}@example.com",
uuid::Uuid::new_v4().simple()
.method("POST")
.body(Body::from(
json!({ "name": "Verified Probe", "email": email, "password": "password123" })
.to_string(),
))
assert_eq!(response.status(), StatusCode::OK, "registration failed");
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.expect("response body");
let parsed: serde_json::Value = serde_json::from_slice(&body).expect("json body");
let user = &parsed["data"]["user"];
let verified = user["verified"].clone();
// Clean up BEFORE asserting: a failing assertion panics, and anything after
// it never runs — which would leak the tenant database and the directory row
// on exactly the runs where the test is doing its job.
let id = uuid::Uuid::parse_str(user["id"].as_str().expect("user id")).expect("uuid");
server::provision::drop_user_database(id)
.expect("drop the provisioned database");
let mut conn = server::db::get_connection().await.expect("connection");
sqlx::query("DELETE FROM users WHERE id = $1")
.bind(id)
.execute(&mut *conn)
.expect("delete the probe row");
assert_eq!(
verified, true,
"a registered user must be verified; an unverified one 401s on every request"
/// The A2 gate itself: `verify` compares the token's generation against the
/// directory. Nothing else covers that join — the server tests cover the bump,
/// the token tests cover the claim riding in the signature.
async fn test_password_change_stops_existing_tokens_verifying() {
let id = uuid::Uuid::new_v4();
seed_directory_user(id).await;
let token = web::token::generate_jwt_token(
id,
15,
crate::common::TEST_PRIVATE_KEY_B64,
web::token::TokenType::Access,
0,
.expect("sign")
.token
.expect("token string");
let verifies = async || {
web::auth_keys::verify(&token, web::token::TokenType::Access)
.expect("the directory is reachable in this test")
.is_some()
let before = verifies().await;
let changed = server::command::user::change_password(id, "password123", "password456").await;
let after = verifies().await;
// Clean up BEFORE asserting: a failing assertion never reaches later lines.
assert!(before, "a freshly minted token must verify");
assert!(changed.is_ok(), "the password change must succeed");
assert!(!after, "it must stop verifying once the password changes");
/// A directory row whose password is `password123`, with the fixture public key
/// so `verify` can check a token we signed locally. No tenant database: `verify`
/// reads only the global directory.
async fn seed_directory_user(id: uuid::Uuid) {
let hash = argon2::PasswordHasher::hash_password(&argon2::Argon2::default(), b"password123")
.expect("hash")
.to_string();
sqlx::query(
"INSERT INTO users (id, user_name, email, user_password, database_name, \
jwt_public_key, verified) VALUES ($1, 'pw-probe', $2, $3, 'unused', $4, TRUE)",
.bind(format!("pwchange-{}@example.test", id.simple()))
.bind(&hash)
.bind(crate::common::TEST_PUBLIC_KEY_B64)
.expect("seed the probe row");