// SPDX-FileCopyrightText: 2026 Ololduck <ololduck@vit.am> and contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
//! The admin navigation link.
//!
//! The nav carries an Admin link only for the instance administrator. The link
//! is hidden for anonymous visitors and for non-admin users, so the admin
//! area stays out of sight for everyone who isn't meant to reach it. The
//! badge shows the count of pending reports when it is greater than zero.
mod support;
use frogue::auth::hash_password;
use frogue::entities::users;
use miette::IntoDiagnostic;
use sea_orm::{ActiveModelTrait, Database, DatabaseConnection, Set};
use support::{Client, Server, free_port};
const PASSWORD: &str = "correct horse battery staple";
/// Inserts a user directly, the way the server's own registration does, so
/// the test breaks loudly when the schema changes instead of inserting into
/// a table the server no longer reads. The password hash is real so login
/// tests can verify sessions.
async fn insert_user(db: &DatabaseConnection, name: &str, email: &str) {
let user = users::ActiveModel {
uuid: Set(uuid::Uuid::new_v4()),
name: Set(name.to_string()),
email: Set(email.to_string()),
password_hash: Set(hash_password(PASSWORD).expect("test password hashes cleanly")),
..Default::default()
};
user.insert(db)
.await
.expect("test user should be insertable");
}
/// Registers a user through the form, the way the browser would. Returns the
/// client with the session cookie baked in.
fn register(client: &mut Client, name: &str, email: &str) {
let response = client.post_form(
"/register",
&[("name", name), ("email", email), ("password", PASSWORD)],
);
assert_eq!(
response.status, 303,
"registering should redirect on success"
);
assert!(
response.sets_cookie("frogue_session"),
"registering should start a session\n{:?}",
response.set_cookies()
);
}
#[tokio::test]
async fn anonymous_visitor_sees_no_admin_link() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
let response = support::get(port, "/");
assert_eq!(response.status, 200, "index");
assert!(
!response.body.contains("/admin"),
"an anonymous visitor must not see the Admin link, got:\n{}",
response.body
);
Ok(())
}
#[tokio::test]
async fn non_admin_user_sees_no_admin_link() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
let db = Database::connect(server.database_url())
.await
.into_diagnostic()?;
// The first registered user becomes user_id=1 (the admin). We need a
// non-admin, so insert one directly, then log in through the form as a
// second user registered on top.
insert_user(&db, "admin", "admin@example.org").await;
let mut client = Client::new(port);
register(&mut client, "bob", "bob@example.org");
let response = client.get("/");
assert_eq!(response.status, 200, "index");
assert!(
!response.body.contains("/admin"),
"a non-admin user must not see the Admin link, got:\n{}",
response.body
);
Ok(())
}
#[tokio::test]
async fn admin_user_sees_admin_link() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
// The first registered user is user_id=1, i.e. the admin.
let mut client = Client::new(port);
register(&mut client, "admin", "admin@example.org");
let response = client.get("/");
assert_eq!(response.status, 200, "index");
assert!(
response.body.contains(r#"<a href="/admin"#),
"the admin must see the Admin link in the nav, got:\n{}",
response.body
);
Ok(())
}
#[tokio::test]
async fn admin_link_is_hidden_from_non_admin_even_on_admin_pages() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
let db = Database::connect(server.database_url())
.await
.into_diagnostic()?;
// Prime the admin row (id=1) so the next registered user is not the admin.
insert_user(&db, "admin", "admin@example.org").await;
let mut client = Client::new(port);
register(&mut client, "bob", "bob@example.org");
// Try to reach /admin directly; it must refuse non-admins.
let response = client.get("/admin");
assert_eq!(
response.status, 403,
"/admin must be forbidden for non-admin users, got:\n{}",
response.body
);
Ok(())
}