1
use axum::{
2
    Router,
3
    body::Body,
4
    http::{Request, StatusCode},
5
    routing::{get, post},
6
};
7
use serde_json::json;
8
use tower::ServiceExt;
9

            
10
use crate::common::{create_mock_jwt_auth, create_mock_user, create_test_app_state};
11

            
12
#[supp_macro::shared_runtime_test]
13
1
async fn test_login_handler_with_valid_json() {
14
1
    let app_state = create_test_app_state().await;
15
1
    let app = Router::new()
16
1
        .route("/auth/login", post(web::handler::login_user_handler))
17
1
        .with_state(app_state);
18

            
19
1
    let login_data = json!({
20
1
        "email": "test@example.com",
21
1
        "password": "password123"
22
    });
23

            
24
1
    let response = app
25
1
        .oneshot(
26
1
            Request::builder()
27
1
                .method("GET")
28
1
                .uri("/auth/login")
29
1
                .header("content-type", "application/json")
30
1
                .body(Body::from(login_data.to_string()))
31
1
                .unwrap(),
32
1
        )
33
1
        .await
34
1
        .unwrap();
35

            
36
    // Will likely be 500 or other error due to missing DB, but JSON was parsed
37
1
    assert!(response.status().is_client_error() || response.status().is_server_error());
38
}
39

            
40
#[supp_macro::shared_runtime_test]
41
1
async fn test_login_handler_with_invalid_json() {
42
1
    let app_state = create_test_app_state().await;
43
1
    let app = Router::new()
44
1
        .route("/auth/login", post(web::handler::login_user_handler))
45
1
        .with_state(app_state);
46

            
47
1
    let response = app
48
1
        .oneshot(
49
1
            Request::builder()
50
1
                .method("GET")
51
1
                .uri("/auth/login")
52
1
                .header("content-type", "application/json")
53
1
                .body(Body::from("invalid json"))
54
1
                .unwrap(),
55
1
        )
56
1
        .await
57
1
        .unwrap();
58

            
59
    // May return 400 for invalid JSON or 405 if route middleware blocks access
60
1
    assert!(
61
1
        response.status() == StatusCode::BAD_REQUEST
62
1
            || response.status() == StatusCode::METHOD_NOT_ALLOWED,
63
        "Expected 400 or 405, got: {}",
64
        response.status()
65
    );
66
}
67

            
68
#[supp_macro::shared_runtime_test]
69
1
async fn test_login_handler_missing_content_type() {
70
1
    let app_state = create_test_app_state().await;
71
1
    let app = Router::new()
72
1
        .route("/auth/login", post(web::handler::login_user_handler))
73
1
        .with_state(app_state);
74

            
75
1
    let login_data = json!({
76
1
        "email": "test@example.com",
77
1
        "password": "password123"
78
    });
79

            
80
1
    let response = app
81
1
        .oneshot(
82
1
            Request::builder()
83
1
                .method("GET")
84
1
                .uri("/auth/login")
85
1
                .body(Body::from(login_data.to_string()))
86
1
                .unwrap(),
87
1
        )
88
1
        .await
89
1
        .unwrap();
90

            
91
    // May return 415 for missing content-type or 405 if route middleware blocks access
92
1
    assert!(
93
1
        response.status() == StatusCode::UNSUPPORTED_MEDIA_TYPE
94
1
            || response.status() == StatusCode::METHOD_NOT_ALLOWED,
95
        "Expected 415 or 405, got: {}",
96
        response.status()
97
    );
98
}
99

            
100
#[supp_macro::shared_runtime_test]
101
1
async fn test_register_handler_with_valid_json() {
102
1
    let app_state = create_test_app_state().await;
103
1
    let app = Router::new()
104
1
        .route("/auth/register", post(web::handler::register_user_handler))
105
1
        .with_state(app_state);
106

            
107
1
    let register_data = json!({
108
1
        "name": "Test User",
109
1
        "email": "newuser@example.com",
110
1
        "password": "password123"
111
    });
112

            
113
1
    let response = app
114
1
        .oneshot(
115
1
            Request::builder()
116
1
                .method("GET")
117
1
                .uri("/auth/register")
118
1
                .header("content-type", "application/json")
119
1
                .body(Body::from(register_data.to_string()))
120
1
                .unwrap(),
121
1
        )
122
1
        .await
123
1
        .unwrap();
124

            
125
    // Will likely be 500 or other error due to missing DB, but JSON was parsed
126
1
    assert!(response.status().is_client_error() || response.status().is_server_error());
127
}
128

            
129
#[supp_macro::shared_runtime_test]
130
1
async fn test_register_handler_with_invalid_json() {
131
1
    let app_state = create_test_app_state().await;
132
1
    let app = Router::new()
133
1
        .route("/auth/register", post(web::handler::register_user_handler))
134
1
        .with_state(app_state);
135

            
136
1
    let response = app
137
1
        .oneshot(
138
1
            Request::builder()
139
1
                .method("GET")
140
1
                .uri("/auth/register")
141
1
                .header("content-type", "application/json")
142
1
                .body(Body::from("invalid json"))
143
1
                .unwrap(),
144
1
        )
145
1
        .await
146
1
        .unwrap();
147

            
148
    // May return 400 for invalid JSON or 405 if route middleware blocks access
149
1
    assert!(
150
1
        response.status() == StatusCode::BAD_REQUEST
151
1
            || response.status() == StatusCode::METHOD_NOT_ALLOWED,
152
        "Expected 400 or 405, got: {}",
153
        response.status()
154
    );
155
}
156

            
157
#[supp_macro::shared_runtime_test]
158
1
async fn test_register_handler_missing_fields() {
159
1
    let app_state = create_test_app_state().await;
160
1
    let app = Router::new()
161
1
        .route("/auth/register", post(web::handler::register_user_handler))
162
1
        .with_state(app_state);
163

            
164
1
    let register_data = json!({
165
1
        "name": "Test User"
166
        // Missing email and password
167
    });
168

            
169
1
    let response = app
170
1
        .oneshot(
171
1
            Request::builder()
172
1
                .method("GET")
173
1
                .uri("/auth/register")
174
1
                .header("content-type", "application/json")
175
1
                .body(Body::from(register_data.to_string()))
176
1
                .unwrap(),
177
1
        )
178
1
        .await
179
1
        .unwrap();
180

            
181
    // May return 422 for missing fields or 405 if route middleware blocks access
182
1
    assert!(
183
1
        response.status() == StatusCode::UNPROCESSABLE_ENTITY
184
1
            || response.status() == StatusCode::METHOD_NOT_ALLOWED,
185
        "Expected 422 or 405, got: {}",
186
        response.status()
187
    );
188
}
189

            
190
#[supp_macro::shared_runtime_test]
191
1
async fn test_refresh_handler_without_cookie() {
192
1
    let app_state = create_test_app_state().await;
193
1
    let app = Router::new()
194
1
        .route(
195
1
            "/auth/refresh",
196
1
            get(web::handler::refresh_access_token_handler),
197
        )
198
        // The route lives behind `auth` in production, which is what inserts
199
        // this extension; without it axum rejects before the handler runs.
200
1
        .layer(axum::Extension(create_mock_jwt_auth(create_mock_user())))
201
1
        .with_state(app_state);
202

            
203
1
    let response = app
204
1
        .oneshot(
205
1
            Request::builder()
206
1
                .method("GET")
207
1
                .uri("/auth/refresh")
208
1
                .body(Body::empty())
209
1
                .unwrap(),
210
1
        )
211
1
        .await
212
1
        .unwrap();
213

            
214
    // Should return 403 for missing refresh token
215
1
    assert_eq!(response.status(), StatusCode::FORBIDDEN);
216
}
217

            
218
#[supp_macro::shared_runtime_test]
219
1
async fn test_refresh_handler_with_simple_invalid_cookie() {
220
1
    let app_state = create_test_app_state().await;
221
1
    let app = Router::new()
222
1
        .route(
223
1
            "/auth/refresh",
224
1
            get(web::handler::refresh_access_token_handler),
225
        )
226
        // The route lives behind `auth` in production, which is what inserts
227
        // this extension; without it axum rejects before the handler runs.
228
1
        .layer(axum::Extension(create_mock_jwt_auth(create_mock_user())))
229
1
        .with_state(app_state);
230

            
231
1
    let response = app
232
1
        .oneshot(
233
1
            Request::builder()
234
1
                .method("GET")
235
1
                .uri("/auth/refresh")
236
1
                .header("cookie", "refresh_token=invalid")
237
1
                .body(Body::empty())
238
1
                .unwrap(),
239
1
        )
240
1
        .await
241
1
        .unwrap();
242

            
243
    // Should return 403 for invalid refresh token or other auth error
244
1
    assert!(
245
1
        response.status() == StatusCode::FORBIDDEN
246
1
            || response.status().is_client_error()
247
            || response.status().is_server_error(),
248
        "Expected auth error, got: {}",
249
        response.status()
250
    );
251
}
252

            
253
#[supp_macro::shared_runtime_test]
254
1
async fn test_login_handler_email_normalization() {
255
1
    let app_state = create_test_app_state().await;
256
1
    let app = Router::new()
257
1
        .route("/auth/login", post(web::handler::login_user_handler))
258
1
        .with_state(app_state);
259

            
260
1
    let login_data = json!({
261
1
        "email": "  Test@EXAMPLE.COM  ",
262
1
        "password": "password123"
263
    });
264

            
265
1
    let response = app
266
1
        .oneshot(
267
1
            Request::builder()
268
1
                .method("GET")
269
1
                .uri("/auth/login")
270
1
                .header("content-type", "application/json")
271
1
                .body(Body::from(login_data.to_string()))
272
1
                .unwrap(),
273
1
        )
274
1
        .await
275
1
        .unwrap();
276

            
277
    // Verify that the handler processes the request (even if it fails due to DB issues)
278
    // The key is that it doesn't fail due to malformed email
279
1
    assert!(response.status().is_client_error() || response.status().is_server_error());
280
}
281

            
282
#[supp_macro::shared_runtime_test]
283
1
async fn test_register_handler_email_normalization() {
284
1
    let app_state = create_test_app_state().await;
285
1
    let app = Router::new()
286
1
        .route("/auth/register", post(web::handler::register_user_handler))
287
1
        .with_state(app_state);
288

            
289
1
    let register_data = json!({
290
1
        "name": "Test User",
291
1
        "email": "  NewUser@EXAMPLE.COM  ",
292
1
        "password": "password123"
293
    });
294

            
295
1
    let response = app
296
1
        .oneshot(
297
1
            Request::builder()
298
1
                .method("GET")
299
1
                .uri("/auth/register")
300
1
                .header("content-type", "application/json")
301
1
                .body(Body::from(register_data.to_string()))
302
1
                .unwrap(),
303
1
        )
304
1
        .await
305
1
        .unwrap();
306

            
307
    // Verify that the handler processes the request (even if it fails due to DB issues)
308
    // The key is that it doesn't fail due to malformed email
309
1
    assert!(response.status().is_client_error() || response.status().is_server_error());
310
}
311

            
312
/// Registration must produce a *verified* user.
313
///
314
/// `jwt_auth` re-reads `users.verified` on every authenticated request and 401s
315
/// when it is false, and nothing in the codebase ever sets the flag — there is
316
/// no verification endpoint and no email flow. So an account created with the
317
/// column default authenticates once and is then unusable, which is exactly
318
/// what the first real registration hit on 2026-09-06. Registration is
319
/// admin-gated, so the admin creating the account has already vouched for it.
320
#[supp_macro::shared_runtime_test]
321
1
async fn test_register_handler_creates_a_verified_user() {
322
1
    let app_state = create_test_app_state().await;
323
1
    let app = Router::new()
324
1
        .route("/auth/register", post(web::handler::register_user_handler))
325
1
        .with_state(app_state);
326

            
327
1
    let email = format!(
328
        "verified-probe-{}@example.com",
329
1
        uuid::Uuid::new_v4().simple()
330
    );
331
1
    let response = app
332
1
        .oneshot(
333
1
            Request::builder()
334
1
                .method("POST")
335
1
                .uri("/auth/register")
336
1
                .header("content-type", "application/json")
337
1
                .body(Body::from(
338
1
                    json!({ "name": "Verified Probe", "email": email, "password": "password123" })
339
1
                        .to_string(),
340
1
                ))
341
1
                .unwrap(),
342
1
        )
343
1
        .await
344
1
        .unwrap();
345

            
346
1
    assert_eq!(response.status(), StatusCode::OK, "registration failed");
347

            
348
1
    let body = axum::body::to_bytes(response.into_body(), usize::MAX)
349
1
        .await
350
1
        .expect("response body");
351
1
    let parsed: serde_json::Value = serde_json::from_slice(&body).expect("json body");
352
1
    let user = &parsed["data"]["user"];
353

            
354
1
    let verified = user["verified"].clone();
355

            
356
    // Clean up BEFORE asserting: a failing assertion panics, and anything after
357
    // it never runs — which would leak the tenant database and the directory row
358
    // on exactly the runs where the test is doing its job.
359
1
    let id = uuid::Uuid::parse_str(user["id"].as_str().expect("user id")).expect("uuid");
360
1
    server::provision::drop_user_database(id)
361
1
        .await
362
1
        .expect("drop the provisioned database");
363
1
    let mut conn = server::db::get_connection().await.expect("connection");
364
1
    sqlx::query("DELETE FROM users WHERE id = $1")
365
1
        .bind(id)
366
1
        .execute(&mut *conn)
367
1
        .await
368
1
        .expect("delete the probe row");
369

            
370
1
    assert_eq!(
371
        verified, true,
372
        "a registered user must be verified; an unverified one 401s on every request"
373
    );
374
}
375

            
376
/// The A2 gate itself: `verify` compares the token's generation against the
377
/// directory. Nothing else covers that join — the server tests cover the bump,
378
/// the token tests cover the claim riding in the signature.
379
#[supp_macro::shared_runtime_test]
380
1
async fn test_password_change_stops_existing_tokens_verifying() {
381
1
    let id = uuid::Uuid::new_v4();
382
1
    seed_directory_user(id).await;
383

            
384
1
    let token = web::token::generate_jwt_token(
385
1
        id,
386
1
        15,
387
1
        crate::common::TEST_PRIVATE_KEY_B64,
388
1
        web::token::TokenType::Access,
389
1
        0,
390
1
    )
391
1
    .expect("sign")
392
1
    .token
393
1
    .expect("token string");
394
4
    let verifies = async || {
395
2
        web::auth_keys::verify(&token, web::token::TokenType::Access)
396
2
            .await
397
2
            .expect("the directory is reachable in this test")
398
2
            .is_some()
399
4
    };
400

            
401
1
    let before = verifies().await;
402
1
    let changed = server::command::user::change_password(id, "password123", "password456").await;
403
1
    let after = verifies().await;
404

            
405
    // Clean up BEFORE asserting: a failing assertion never reaches later lines.
406
1
    let mut conn = server::db::get_connection().await.expect("connection");
407
1
    sqlx::query("DELETE FROM users WHERE id = $1")
408
1
        .bind(id)
409
1
        .execute(&mut *conn)
410
1
        .await
411
1
        .expect("delete the probe row");
412

            
413
1
    assert!(before, "a freshly minted token must verify");
414
1
    assert!(changed.is_ok(), "the password change must succeed");
415
1
    assert!(!after, "it must stop verifying once the password changes");
416
}
417

            
418
/// A directory row whose password is `password123`, with the fixture public key
419
/// so `verify` can check a token we signed locally. No tenant database: `verify`
420
/// reads only the global directory.
421
1
async fn seed_directory_user(id: uuid::Uuid) {
422
1
    let hash = argon2::PasswordHasher::hash_password(&argon2::Argon2::default(), b"password123")
423
1
        .expect("hash")
424
1
        .to_string();
425
1
    let mut conn = server::db::get_connection().await.expect("connection");
426
1
    sqlx::query(
427
1
        "INSERT INTO users (id, user_name, email, user_password, database_name, \
428
1
         jwt_public_key, verified) VALUES ($1, 'pw-probe', $2, $3, 'unused', $4, TRUE)",
429
1
    )
430
1
    .bind(id)
431
1
    .bind(format!("pwchange-{}@example.test", id.simple()))
432
1
    .bind(&hash)
433
1
    .bind(crate::common::TEST_PUBLIC_KEY_B64)
434
1
    .execute(&mut *conn)
435
1
    .await
436
1
    .expect("seed the probe row");
437
1
}