//! The webui answers with complete HTML documents.
//!
//! These tests pin what actually goes over the wire: every page must be a
//! full document served as `text/html`, and whatever a user picked as their
//! name must be escaped rather than served as markup - the profile path
//! segment is attacker-controlled.
mod support;
use frogue::entities::users;
use miette::IntoDiagnostic;
use sea_orm::{ActiveModelTrait, Database, DatabaseConnection, Set};
use support::{Server, free_port, get};
/// Inserts a user through the entity the server itself uses, so the test
/// breaks loudly when the schema changes instead of inserting into a table
/// the server no longer reads.
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("irrelevant".to_string()),
..Default::default()
};
user.insert(db)
.await
.expect("test user should be insertable");
}
#[tokio::test]
async fn index_renders_the_home_page() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
let response = get(port, "/");
assert_eq!(response.status, 200, "index");
assert_eq!(
response.content_type.as_deref(),
Some("text/html; charset=utf-8"),
"the index should be sent as HTML"
);
assert!(
response.body.starts_with("<!DOCTYPE html>"),
"the index should be a complete document, got: {}",
response.body
);
assert!(
response.body.contains("<title>Frogue</title>"),
"the index should carry its title, got: {}",
response.body
);
assert!(
response.body.contains(
"<h1><a href=\"/\">🐸 Frogue<span style=\"color: #ccc\">(Fed)</span></a></h1>"
),
"the index should carry the site header, got: {}",
response.body
);
assert!(
response
.body
.contains("Powered by <a href=\"#no-public-url-yet\"><em>Frogue</em></a> version "),
"the index should carry the site footer, got: {}",
response.body
);
Ok(())
}
#[tokio::test]
async fn profile_renders_the_user_name() -> 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()?;
insert_user(&db, "alice", "alice@example.org").await;
let response = get(port, "/~alice");
assert_eq!(response.status, 200, "profile");
assert_eq!(
response.content_type.as_deref(),
Some("text/html; charset=utf-8"),
"the profile should be sent as HTML"
);
assert!(
response.body.contains("<title>~alice - Frogue</title>"),
"the profile should carry its title, got: {}",
response.body
);
assert!(
response
.body
.contains("<h2><a class=\"frogue-username\" href=\"/~alice\">~alice</a></h2>"),
"the profile should show the user name, got: {}",
response.body
);
Ok(())
}
#[tokio::test]
async fn profile_escapes_the_user_name() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
let name = "<img src=x onerror=alert(1)>";
let db = Database::connect(server.database_url())
.await
.into_diagnostic()?;
insert_user(&db, name, "mallory@example.org").await;
// Percent-encoded, so the hostile name stays one path segment on its way
// to the route extractor.
let response = get(port, "/~%3Cimg%20src=x%20onerror=alert(1)%3E");
assert_eq!(response.status, 200, "profile");
assert!(
!response.body.contains("<img"),
"a user name must not be served as markup, got: {}",
response.body
);
assert!(
response.body.contains("<img src=x onerror=alert(1)>"),
"the escaped user name should still be readable, got: {}",
response.body
);
Ok(())
}
#[tokio::test]
async fn profile_of_an_unknown_user_is_not_found() -> miette::Result<()> {
let port = free_port();
let mut server = Server::spawn(&format!("127.0.0.1:{port}"));
server.wait_for_connection(port);
let response = get(port, "/~nobody");
assert_eq!(response.status, 404, "profile");
Ok(())
}
#[tokio::test]
async fn profile_is_only_served_under_the_tilde() -> 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()?;
insert_user(&db, "alice", "alice@example.org").await;
let response = get(port, "/alice");
assert_eq!(
response.status, 404,
"a profile should not be served without the tilde"
);
Ok(())
}