- mail/ module (lettre, ru/en templates), links built only from PUBLIC_BASE_URL - migration 0006: accounts.email_verified_at, shared email_tokens table (verify + reset), existing accounts marked verified - reset mailer no longer logs tokens; RESET_MAIL_MODE=log is dev-only and refused with COOKIE_SECURE=true; Disabled by default answers 503 - forgot-password and resend-verification do their work in a background task (no timing oracle) - device linking requires a verified email; email_verified exposed via /me and gRPC - gateway rate limits, SMTP_* in compose and .env.example - frontend: verify, forgot-password, reset-password pages, verify banner, ru/en strings
106 lines
3.3 KiB
Rust
106 lines
3.3 KiB
Rust
mod common;
|
|
// A test root's submodules resolve against `tests/`, not the file's own
|
|
// directory — pin the path so `tests/` itself stays at 4 files.
|
|
#[path = "device_flow/links.rs"]
|
|
mod links;
|
|
|
|
use axum::http::StatusCode;
|
|
use serde_json::json;
|
|
|
|
#[tokio::test]
|
|
async fn full_device_link_flow() {
|
|
let pool = common::test_pool().await;
|
|
let server = common::test_server(accounts_service::build_app(
|
|
pool.clone(),
|
|
&common::test_config(),
|
|
));
|
|
let (account_id, email) = common::register_account(&server).await;
|
|
common::mark_verified(&pool, account_id).await;
|
|
|
|
let code_response: serde_json::Value = server.post("/device/code").await.json();
|
|
let device_code = code_response["device_code"].as_str().unwrap();
|
|
let user_code = code_response["user_code"].as_str().unwrap();
|
|
|
|
let poll_before = server
|
|
.post("/device/token")
|
|
.json(&json!({ "device_code": device_code }))
|
|
.await;
|
|
poll_before.assert_status(StatusCode::ACCEPTED);
|
|
|
|
server
|
|
.post("/device/confirm")
|
|
.add_header(common::ACCOUNT_ID_HEADER, account_id.to_string())
|
|
.json(&json!({ "user_code": user_code }))
|
|
.await
|
|
.assert_status_ok();
|
|
|
|
let poll_after = server
|
|
.post("/device/token")
|
|
.json(&json!({ "device_code": device_code }))
|
|
.await;
|
|
poll_after.assert_status_ok();
|
|
let token_body: serde_json::Value = poll_after.json();
|
|
let device_token = token_body["device_token"].as_str().unwrap();
|
|
assert!(
|
|
device_token.starts_with("lvd_"),
|
|
"opaque device token, got {device_token}"
|
|
);
|
|
|
|
// Single use: the same device_code cannot be redeemed twice.
|
|
server
|
|
.post("/device/token")
|
|
.json(&json!({ "device_code": device_code }))
|
|
.await
|
|
.assert_status(StatusCode::NOT_FOUND);
|
|
|
|
sqlx::query("DELETE FROM accounts WHERE email = $1")
|
|
.bind(&email)
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn confirm_without_identity_is_401() {
|
|
let pool = common::test_pool().await;
|
|
let server = common::test_server(accounts_service::build_app(
|
|
pool.clone(),
|
|
&common::test_config(),
|
|
));
|
|
let code: serde_json::Value = server.post("/device/code").await.json();
|
|
server
|
|
.post("/device/confirm")
|
|
.json(&json!({ "user_code": code["user_code"] }))
|
|
.await
|
|
.assert_status(StatusCode::UNAUTHORIZED);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn unknown_device_code_and_user_code_return_404() {
|
|
let pool = common::test_pool().await;
|
|
let server = common::test_server(accounts_service::build_app(
|
|
pool.clone(),
|
|
&common::test_config(),
|
|
));
|
|
let (account_id, email) = common::register_account(&server).await;
|
|
common::mark_verified(&pool, account_id).await;
|
|
|
|
server
|
|
.post("/device/token")
|
|
.json(&json!({ "device_code": "no-such-code" }))
|
|
.await
|
|
.assert_status(StatusCode::NOT_FOUND);
|
|
|
|
server
|
|
.post("/device/confirm")
|
|
.add_header(common::ACCOUNT_ID_HEADER, account_id.to_string())
|
|
.json(&json!({ "user_code": "ZZZZ-ZZZZ" }))
|
|
.await
|
|
.assert_status(StatusCode::NOT_FOUND);
|
|
|
|
sqlx::query("DELETE FROM accounts WHERE email = $1")
|
|
.bind(&email)
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
}
|