🐸 Frogue(Fed)

~ololduck/Frogue

Raw

// 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(())
}