Integration tests: homepage (WIP)
This commit is contained in:
parent
fb62288161
commit
1485110204
6 changed files with 556 additions and 41 deletions
|
|
@ -49,3 +49,7 @@ lettre = { version = "0.11", default-features = false, features = [
|
|||
] }
|
||||
|
||||
thiserror = "2"
|
||||
|
||||
[dev-dependencies]
|
||||
axum-test = "17"
|
||||
scraper = "0.23"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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![]),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
78
backend/tests/http.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue