//! Authentication: first-boot setup, login, session, logout, disabling. mod common; use std::sync::Arc; use axum::http::StatusCode; use common::*; use serde_json::json; #[tokio::test] async fn first_boot_me_reports_first_boot() { let env = Env::new().await; let c = Client::new(env.app.clone()); let r = c.get("/api/auth/me").await; assert_eq!(r.status, StatusCode::OK); let j = r.json(); assert_eq!(j["first_boot"], true); assert!(j["user"].is_null()); assert_eq!(j["roots"], json!([])); } #[tokio::test] async fn setup_validates_input() { let env = Env::new().await; let c = Client::new(env.app.clone()); // Blank name. let r = c .post_json( "/api/auth/setup", &json!({ "name": " ", "password": "longenough1" }), ) .await; assert_eq!(r.status, StatusCode::BAD_REQUEST); // Name too long (65 chars). let r = c .post_json( "/api/auth/setup", &json!({ "name": "a".repeat(65), "password": "longenough1" }), ) .await; assert_eq!(r.status, StatusCode::BAD_REQUEST); // Password too short. let r = c .post_json( "/api/auth/setup", &json!({ "name": "admin", "password": "short" }), ) .await; assert_eq!(r.status, StatusCode::BAD_REQUEST); // Nothing was created. assert_eq!(c.get("/api/auth/me").await.json()["first_boot"], true); } #[tokio::test] async fn setup_creates_admin_and_sets_cookie() { let env = Env::new().await; let admin = env.admin().await; // /me reports the admin with the whole root (rw). let r = admin.get("/api/auth/me").await; assert_eq!(r.status, StatusCode::OK); let j = r.json(); assert_eq!(j["first_boot"], false); assert_eq!(j["user"]["name"], "admin"); assert_eq!(j["user"]["is_admin"], true); let roots = j["roots"].as_array().unwrap(); assert_eq!(roots.len(), 1); assert_eq!(roots[0]["path"], "."); assert_eq!(roots[0]["mode"], "rw"); // The whole root is named after the root folder itself (`--root-name` // overrides this; see `root_name_is_configurable`). assert_eq!(roots[0]["name"], env.state.root_name); assert_eq!( roots[0]["name"], env.root.path().file_name().unwrap().to_str().unwrap() ); // Setup is only available on first boot. let anon = Client::new(env.app.clone()); let r = anon .post_json( "/api/auth/setup", &json!({ "name": "x", "password": "longenough1" }), ) .await; assert_eq!(r.status, StatusCode::CONFLICT); } #[tokio::test] async fn login_flows() { let env = Env::new().await; let _admin = env.admin().await; let c = Client::new(env.app.clone()); let r = c .post_json( "/api/auth/login", &json!({ "name": "admin", "password": "wrongpass1" }), ) .await; assert_eq!(r.status, StatusCode::UNAUTHORIZED); let r = c .post_json( "/api/auth/login", &json!({ "name": "ghost", "password": "whatever1" }), ) .await; assert_eq!(r.status, StatusCode::UNAUTHORIZED); let r = c .post_json( "/api/auth/login", &json!({ "name": "admin", "password": "admin1234" }), ) .await; assert_eq!(r.status, StatusCode::OK); assert!(session_cookie(&r).is_some()); } #[tokio::test] async fn me_requires_valid_session() { let env = Env::new().await; let _admin = env.admin().await; // No cookie at all. let anon = Client::new(env.app.clone()); assert_eq!( anon.get("/api/auth/me").await.status, StatusCode::UNAUTHORIZED ); // Bogus session token. let mut bogus = Client::new(env.app.clone()); bogus.set_cookie("not-a-real-token"); assert_eq!( bogus.get("/api/auth/me").await.status, StatusCode::UNAUTHORIZED ); } #[tokio::test] async fn logout_invalidates_session() { let env = Env::new().await; let admin = env.admin().await; let r = admin.post_json("/api/auth/logout", &json!({})).await; assert_eq!(r.status, StatusCode::OK); // The Set-Cookie header clears the cookie. assert!( r.header("set-cookie") .unwrap() .starts_with("fbng_session=;") ); // The old cookie no longer authenticates. assert_eq!( admin.get("/api/auth/me").await.status, StatusCode::UNAUTHORIZED ); } #[tokio::test] async fn disabled_user_loses_session_and_cannot_login() { let env = Env::new().await; let admin = env.admin().await; create_user(&admin, "bob", "bobpass123", &[("docs", "rw")]).await; let bob = login(&env, "bob", "bobpass123").await; assert_eq!(bob.get("/api/auth/me").await.status, StatusCode::OK); // Admin disables bob. let id = user_id(&admin, "bob").await; let r = admin .put_json( &format!("/api/admin/users/{id}"), &json!({ "active": false }), ) .await; assert_eq!(r.status, StatusCode::OK); // bob's live session is rejected… assert_eq!( bob.get("/api/auth/me").await.status, StatusCode::UNAUTHORIZED ); // …and he cannot log in again. let anon = Client::new(env.app.clone()); let r = anon .post_json( "/api/auth/login", &json!({ "name": "bob", "password": "bobpass123" }), ) .await; assert_eq!(r.status, StatusCode::UNAUTHORIZED); } #[tokio::test] async fn root_name_is_configurable() { let mut env = Env::new().await; // Rebuild the router with a custom root name (what `--root-name` / // `FILEBROWSER_ROOT_NAME` sets on the state). let state = Arc::new(server::error::AppState { db: env.state.db.clone(), root: env.state.root.clone(), root_name: "Media".to_string(), public_url: None, thumbs: None, }); env.app = server::api::router(state.clone()); env.state = state; let admin = env.admin().await; let j = admin.get("/api/auth/me").await.json(); assert_eq!(j["roots"][0]["name"], "Media"); // Subroots keep their own folder name. create_user(&admin, "bob", "bobpass123", &[("docs/inner", "rw")]).await; let bob = login(&env, "bob", "bobpass123").await; assert_eq!( bob.get("/api/auth/me").await.json()["roots"][0]["name"], "inner" ); } #[tokio::test] async fn public_url_reaches_client() { let mut env = Env::new().await; let state = Arc::new(server::error::AppState { db: env.state.db.clone(), root: env.state.root.clone(), root_name: env.state.root_name.clone(), public_url: Some("https://files.example.com".parse().unwrap()), thumbs: None, }); env.app = server::api::router(state.clone()); env.state = state; let admin = env.admin().await; assert_eq!( admin.get("/api/auth/me").await.json()["public_url"], "https://files.example.com" ); } #[tokio::test] async fn default_root_is_validated_and_dropped_with_its_root() { let env = Env::new().await; let admin = env.admin().await; create_user( &admin, "bob", "bobpass123", &[("docs", "rw"), ("src", "ro")], ) .await; let bob = login(&env, "bob", "bobpass123").await; let me = bob.get("/api/auth/me").await.json(); assert_eq!(me["user"]["default_root_id"], json!(null)); let src_id = me["roots"][1]["id"].as_i64().unwrap(); let admin_root_id = admin.get("/api/auth/me").await.json()["roots"][0]["id"] .as_i64() .unwrap(); // A root of another user → 400. let r = bob .put_json("/api/auth/me", &json!({ "default_root_id": admin_root_id })) .await; assert_eq!(r.status, StatusCode::BAD_REQUEST, "{}", r.text()); // One of bob's own roots is echoed back. let r = bob .put_json("/api/auth/me", &json!({ "default_root_id": src_id })) .await; assert_eq!(r.status, StatusCode::OK, "{}", r.text()); assert_eq!(r.json()["user"]["default_root_id"], json!(src_id)); assert_eq!( bob.get("/api/auth/me").await.json()["user"]["default_root_id"], json!(src_id) ); // The admin takes that root away: /me no longer names it. let id = user_id(&admin, "bob").await; let r = admin .put_json( &format!("/api/admin/users/{id}"), &json!({ "roots": [{ "path": "docs", "mode": "rw" }] }), ) .await; assert_eq!(r.status, StatusCode::OK); assert_eq!( bob.get("/api/auth/me").await.json()["user"]["default_root_id"], json!(null) ); // null clears it explicitly. let r = bob .put_json("/api/auth/me", &json!({ "default_root_id": null })) .await; assert_eq!(r.status, StatusCode::OK); assert_eq!(r.json()["user"]["default_root_id"], json!(null)); }