use sova_auth::{AuthMigrator, Feature, Fortify};
use sova_core::{ResponseAssert, TestClient};
use sova_csrf::Csrf;
use sova_mail::Mail;
use sova_session::memory_sessions;
use sova_testing::{SqliteTestDb, TestApp};
const SECRET: &str = "test-fortify-secret-fortify-web!!";
async fn build() -> (SqliteTestDb, TestClient) {
let (_db, app) = TestApp::builder()
.migrator::<AuthMigrator>()
.env("FORTIFY_SECRET", SECRET)
.install(memory_sessions())
.install(Csrf::new())
.install(Mail::fake().from("Test <noreply@test.local>"))
.install(
Fortify::new()
.features(Feature::all().iter().copied())
.web_forms(true)
.no_api()
.public_url("http://127.0.0.1")
.app_name("Test")
.home("/home")
.login_redirect("/login")
.secret(SECRET),
)
.build()
.await;
let c = TestClient::tracked(app).await.expect("test client");
(_db, c)
}
fn csrf_from_html(body: &str) -> String {
let marker = "name=\"csrf\" value=\"";
let start = body
.find(marker)
.map(|i| i + marker.len())
.expect("csrf hidden field");
let rest = &body[start..];
let end = rest.find('"').expect("csrf value end");
rest[..end].to_string()
}
fn xsrf_cookie(res: &sova_core::Response) -> Option<String> {
let prefix = "XSRF-TOKEN=";
res.headers()
.get_all("set-cookie")
.iter()
.filter_map(|v| v.to_str().ok())
.find(|c| c.starts_with(prefix))
.map(|c| {
c.split(';')
.next()
.unwrap()
.trim_start_matches(prefix)
.to_string()
})
}
#[tokio::test]
async fn get_register_and_login_forms_return_html() {
let (_db, c) = build().await;
let reg = c.get("/register").await;
reg.assert_status(200);
let ct = reg
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
assert!(ct.contains("text/html"), "content-type={ct}");
let body = String::from_utf8_lossy(reg.body_bytes().unwrap()).into_owned();
assert!(body.contains("<form"), "{body}");
assert!(body.contains("name=\"email\""), "{body}");
assert!(body.contains("name=\"password\""), "{body}");
let csrf = csrf_from_html(&body);
assert!(!csrf.is_empty());
assert_eq!(xsrf_cookie(®).as_deref(), Some(csrf.as_str()));
let login = c.get("/login").await;
login.assert_status(200);
let body = String::from_utf8_lossy(login.body_bytes().unwrap()).into_owned();
assert!(body.contains("<form"), "{body}");
assert!(body.contains("name=\"email\""), "{body}");
assert!(!csrf_from_html(&body).is_empty());
}
#[tokio::test]
async fn post_register_and_login_urlencoded_with_csrf() {
let (_db, c) = build().await;
let reg_page = c.get("/register").await;
reg_page.assert_status(200);
let csrf = csrf_from_html(&String::from_utf8_lossy(reg_page.body_bytes().unwrap()));
let registered = c
.post("/register")
.form(&[
("csrf", csrf.as_str()),
("name", "Ada"),
("email", "ada@example.com"),
("password", "secret123"),
("password_confirmation", "secret123"),
])
.await;
assert_eq!(
registered.status_code().as_u16(),
303,
"register should redirect home"
);
assert_eq!(
registered.headers().get("location").and_then(|v| v.to_str().ok()),
Some("/home")
);
let login_page = c.get("/login").await;
let csrf = csrf_from_html(&String::from_utf8_lossy(login_page.body_bytes().unwrap()));
c.post("/logout")
.form(&[("csrf", csrf.as_str())])
.await
.assert_status(303);
let login_page = c.get("/login").await;
let csrf = csrf_from_html(&String::from_utf8_lossy(login_page.body_bytes().unwrap()));
let bad = c
.post("/login")
.form(&[
("csrf", csrf.as_str()),
("email", "ada@example.com"),
("password", "wrong-password"),
])
.await;
assert_eq!(bad.status_code().as_u16(), 303);
let login_page = c.get("/login").await;
let csrf = csrf_from_html(&String::from_utf8_lossy(login_page.body_bytes().unwrap()));
let ok = c
.post("/login")
.form(&[
("csrf", csrf.as_str()),
("email", "ada@example.com"),
("password", "secret123"),
])
.await;
assert_eq!(ok.status_code().as_u16(), 303);
assert_eq!(
ok.headers().get("location").and_then(|v| v.to_str().ok()),
Some("/home")
);
}
#[tokio::test]
async fn post_register_without_csrf_is_forbidden() {
let (_db, c) = build().await;
c.get("/register").await.assert_status(200);
let res = c
.post("/register")
.form(&[
("name", "Bob"),
("email", "bob@example.com"),
("password", "secret123"),
("password_confirmation", "secret123"),
])
.await;
assert_eq!(res.status_code().as_u16(), 403);
}