Integration tests: homepage (WIP)

This commit is contained in:
Greg Burri 2025-04-30 01:39:11 +02:00
parent fb62288161
commit 1485110204
6 changed files with 556 additions and 41 deletions

View file

@ -49,3 +49,7 @@ lettre = { version = "0.11", default-features = false, features = [
] }
thiserror = "2"
[dev-dependencies]
axum-test = "17"
scraper = "0.23"

View file

@ -59,7 +59,6 @@ impl Connection {
Self::new_from_file(path).await
}
#[cfg(test)]
pub async fn new_in_memory() -> Result<Connection> {
Self::create_connection(SqlitePoolOptions::new().connect("sqlite::memory:").await?).await
}

View file

@ -29,13 +29,16 @@ const TRACING_LEVEL: tracing::Level = tracing::Level::INFO;
const TRACING_DISPLAY_THREAD: bool = false;
#[derive(Clone)]
pub struct Log {
_guard: Arc<WorkerGuard>,
directory: PathBuf,
pub enum Log {
FileAndStdout {
_guard: Arc<WorkerGuard>,
directory: PathBuf,
},
StdoutOnly,
}
impl Log {
pub fn new<P>(directory: P) -> Self
pub fn new_to_directory<P>(directory: P) -> Self
where
P: AsRef<Path>,
{
@ -68,51 +71,69 @@ impl Log {
.with(layer_stdout)
.init();
Log {
Log::FileAndStdout {
_guard: Arc::new(guard),
directory: directory.as_ref().to_path_buf(),
}
}
pub fn file_names(&self) -> std::io::Result<Vec<String>> {
fn dir_entry_to_string(entry: Result<DirEntry, io::Error>) -> String {
entry.map_or_else(
|err| format!("Unable to read entry: {}", err),
|entry| {
entry
.path()
.file_name()
.map_or("Unable to read filename".into(), |filename| {
filename
.to_str()
.map_or("Unable to read filename".into(), |filename| {
filename.to_string()
})
})
},
)
}
pub fn new_stdout_only() -> Self {
let layer_stdout = tracing_subscriber::fmt::layer()
.with_writer(std::io::stdout.with_max_level(TRACING_LEVEL))
.with_thread_ids(TRACING_DISPLAY_THREAD)
.with_thread_names(TRACING_DISPLAY_THREAD);
Ok(self
.directory
.read_dir()?
.map(dir_entry_to_string)
.sorted()
.rev()
.collect())
tracing_subscriber::Registry::default()
.with(layer_stdout)
.init();
Log::StdoutOnly
}
pub fn file_names(&self) -> std::io::Result<Vec<String>> {
match self {
Log::FileAndStdout { _guard, directory } => {
fn dir_entry_to_string(entry: Result<DirEntry, io::Error>) -> String {
entry.map_or_else(
|err| format!("Unable to read entry: {}", err),
|entry| {
entry.path().file_name().map_or(
"Unable to read filename".into(),
|filename| {
filename
.to_str()
.map_or("Unable to read filename".into(), |filename| {
filename.to_string()
})
},
)
},
)
}
Ok(directory
.read_dir()?
.map(dir_entry_to_string)
.sorted()
.rev()
.collect())
}
Log::StdoutOnly => Ok(vec![]),
}
}
/// Reads the content of a log file and return it as a vector of lines.
pub fn read_content(&self, filename: &str) -> std::io::Result<Vec<String>> {
let filepath = self.directory.join(filename);
if filepath.is_file() {
let file = File::open(filepath)?;
Ok(BufReader::new(file)
.lines()
.map(|l| l.unwrap_or_default())
.collect())
} else {
Ok(vec![])
match self {
Log::FileAndStdout { _guard, directory } => {
let filepath = directory.join(filename);
let file = File::open(filepath)?;
Ok(BufReader::new(file)
.lines()
.map(|l| l.unwrap_or_default())
.collect())
}
Log::StdoutOnly => Ok(vec![]),
}
}

View file

@ -15,7 +15,7 @@ use recipes::{
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = config::load();
let log = Log::new(&config.logs_directory);
let log = Log::new_to_directory(&config.logs_directory);
event!(Level::INFO, "Configuration: {:?}", config);

78
backend/tests/http.rs Normal file
View file

@ -0,0 +1,78 @@
use std::error::Error;
use axum_test::TestServer;
use scraper::Html;
use recipes::{app, config, data::db, log};
#[tokio::test]
async fn homepage() -> Result<(), Box<dyn Error>> {
// Arrange.
let state = common_state().await?;
let user_id = create_user(&state.db_connection, "president@spaceball.planet", "12345").await?;
let _recipe_id = create_recipe(&state.db_connection, user_id, "spaghetti").await?;
let server = TestServer::new(app::make_service(state))?;
// Act.
let response = server.get("/").await;
// Assert.
response.assert_status_ok();
// TODO: check if 'spaghetti' is in the list.
let document = Html::parse_document(&response.text());
assert_eq!(document.errors.len(), 0);
// println!("{:?}", document.errors);
// println!("{:?}", response);
Ok(())
}
async fn create_user(
db_connection: &db::Connection,
email: &str,
password: &str,
) -> Result<i64, Box<dyn Error>> {
if let db::user::SignUpResult::UserCreatedWaitingForValidation(token) = db_connection
.sign_up(email, password, chrono::Weekday::Mon)
.await?
{
if let db::user::ValidationResult::Ok(_, user_id) = db_connection
.validation(
&token,
chrono::Duration::hours(1),
"127.0.0.1",
"Mozilla/5.0",
)
.await?
{
Ok(user_id)
} else {
Err(Box::<dyn Error>::from("Unable to validate user"))
}
} else {
Err(Box::<dyn Error>::from("Unable to sign up"))
}
}
async fn create_recipe(
db_connection: &db::Connection,
user_id: i64,
title: &str,
) -> Result<i64, Box<dyn Error>> {
let recipe_id = db_connection.create_recipe(user_id).await?;
db_connection.set_recipe_title(recipe_id, title).await?;
db_connection.set_recipe_is_public(recipe_id, true).await?;
Ok(recipe_id)
}
async fn common_state() -> Result<app::AppState, Box<dyn Error>> {
let db_connection = db::Connection::new_in_memory().await?;
let config = config::Config::default();
let log = log::Log::new_stdout_only();
Ok(app::AppState {
config,
db_connection,
log,
})
}