From dc4b4b3499d580f5c3cdb2d45f4e6999fff9bd52 Mon Sep 17 00:00:00 2001 From: philipp Date: Mon, 31 Jul 2023 16:25:07 +0200 Subject: [PATCH] add tests; Closes #30 --- src/model/boat.rs | 149 ++++++++++++++++++++++-- src/model/location.rs | 1 + src/model/rower.rs | 28 +++-- src/model/user.rs | 100 +++++++++++++--- src/tera/admin/boat.rs | 17 ++- templates/includes/forms/boat.html.tera | 2 +- 6 files changed, 253 insertions(+), 44 deletions(-) diff --git a/src/model/boat.rs b/src/model/boat.rs index 11fa625..6808650 100644 --- a/src/model/boat.rs +++ b/src/model/boat.rs @@ -50,7 +50,6 @@ pub struct BoatToAdd<'r> { #[derive(FromForm)] pub struct BoatToUpdate<'r> { - pub id: i32, pub name: &'r str, pub amount_seats: i64, pub year_built: Option, @@ -58,8 +57,8 @@ pub struct BoatToUpdate<'r> { pub default_shipmaster_only_steering: bool, pub skull: bool, pub external: bool, - pub location_id: Option, - pub owner: Option, + pub location_id: i64, + pub owner_id: Option, } impl Boat { @@ -143,7 +142,7 @@ ORDER BY amount_seats DESC .await.is_ok() } - pub async fn update(&self, db: &SqlitePool, boat: BoatToUpdate<'_>) -> bool { + pub async fn update(&self, db: &SqlitePool, boat: BoatToUpdate<'_>) -> Result<(), String> { sqlx::query!( "UPDATE boat SET name=?, amount_seats=?, year_built=?, boatbuilder=?, default_shipmaster_only_steering=?, skull=?, external=?, location_id=?, owner=? WHERE id=?", boat.name, @@ -154,12 +153,12 @@ ORDER BY amount_seats DESC boat.skull, boat.external, boat.location_id, - boat.owner, + boat.owner_id, self.id ) .execute(db) - .await - .is_ok() + .await.map_err(|e| e.to_string())?; + Ok(()) } pub async fn delete(&self, db: &SqlitePool) { @@ -173,12 +172,17 @@ ORDER BY amount_seats DESC #[cfg(test)] mod test { use crate::{ - model::boat::{Boat, BoatToAdd}, + model::{ + boat::{Boat, BoatToAdd}, + location::Location, + }, testdb, }; use sqlx::SqlitePool; + use super::BoatToUpdate; + #[sqlx::test] fn test_find_correct_id() { let pool = testdb!(); @@ -247,4 +251,133 @@ mod test { false ); } + + #[sqlx::test] + fn test_is_locked() { + let pool = testdb!(); + let res = Boat::find_by_id(&pool, 5) + .await + .unwrap() + .is_locked(&pool) + .await; + assert_eq!(res, true); + } + + #[sqlx::test] + fn test_is_not_locked() { + let pool = testdb!(); + let res = Boat::find_by_id(&pool, 4) + .await + .unwrap() + .is_locked(&pool) + .await; + assert_eq!(res, false); + } + + #[sqlx::test] + fn test_is_not_locked_no_damage() { + let pool = testdb!(); + let res = Boat::find_by_id(&pool, 3) + .await + .unwrap() + .is_locked(&pool) + .await; + assert_eq!(res, false); + } + + #[sqlx::test] + fn test_has_minor_damage() { + let pool = testdb!(); + let res = Boat::find_by_id(&pool, 4) + .await + .unwrap() + .has_minor_damage(&pool) + .await; + assert_eq!(res, true); + } + + #[sqlx::test] + fn test_has_no_minor_damage() { + let pool = testdb!(); + let res = Boat::find_by_id(&pool, 5) + .await + .unwrap() + .has_minor_damage(&pool) + .await; + assert_eq!(res, false); + } + + #[sqlx::test] + fn test_on_water() { + let pool = testdb!(); + let res = Boat::find_by_id(&pool, 2) + .await + .unwrap() + .on_water(&pool) + .await; + assert_eq!(res, true); + } + + #[sqlx::test] + fn test_not_on_water() { + let pool = testdb!(); + let res = Boat::find_by_id(&pool, 4) + .await + .unwrap() + .on_water(&pool) + .await; + assert_eq!(res, false); + } + + #[sqlx::test] + fn test_succ_update() { + let pool = testdb!(); + let boat = Boat::find_by_id(&pool, 1).await.unwrap(); + let location = Location::find_by_id(&pool, 1).await.unwrap(); + let update = BoatToUpdate { + name: "my-new-boat-name", + amount_seats: 3, + year_built: None, + boatbuilder: None, + default_shipmaster_only_steering: false, + skull: true, + external: false, + location_id: 1, + owner_id: None, + }; + + boat.update(&pool, update).await.unwrap(); + + let boat = Boat::find_by_id(&pool, 1).await.unwrap(); + assert_eq!(boat.name, "my-new-boat-name"); + } + + #[sqlx::test] + fn test_failed_update() { + let pool = testdb!(); + let boat = Boat::find_by_id(&pool, 1).await.unwrap(); + let location = Location::find_by_id(&pool, 1).await.unwrap(); + let update = BoatToUpdate { + name: "my-new-boat-name", + amount_seats: 3, + year_built: None, + boatbuilder: None, + default_shipmaster_only_steering: false, + skull: true, + external: false, + location_id: 999, + owner_id: None, + }; + + match boat.update(&pool, update).await { + Ok(_) => panic!("Update with invalid location should not succeed"), + Err(e) => assert_eq!( + e, + "error returned from database: (code: 787) FOREIGN KEY constraint failed" + ), + }; + + let boat = Boat::find_by_id(&pool, 1).await.unwrap(); + assert_eq!(boat.name, "Haichenbach"); + } } diff --git a/src/model/location.rs b/src/model/location.rs index a66d810..00ebcd7 100644 --- a/src/model/location.rs +++ b/src/model/location.rs @@ -1,3 +1,4 @@ +use rocket::form::FromFormField; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, SqlitePool}; diff --git a/src/model/rower.rs b/src/model/rower.rs index f2b4f78..f5048cc 100644 --- a/src/model/rower.rs +++ b/src/model/rower.rs @@ -1,7 +1,10 @@ use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; -use super::{logbook::Logbook, user::User}; +use super::{ + logbook::Logbook, + user::{MyNaiveDateTime, User}, +}; #[derive(FromRow, Debug, Serialize, Deserialize)] pub struct Rower { @@ -11,18 +14,29 @@ pub struct Rower { impl Rower { pub async fn for_log(db: &SqlitePool, log: &Logbook) -> Vec { - sqlx::query_as!( - User, + sqlx::query!( " - SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access - FROM user - WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?) - ", +SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access +FROM user +WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?) + ", log.id ) .fetch_all(db) .await .unwrap() + .into_iter() + .map(|row| User { + id: row.id, + name: row.name, + pw: row.pw, + is_cox: row.is_cox, + is_admin: row.is_admin, + is_guest: row.is_guest, + deleted: row.deleted, + last_access: row.last_access.map(MyNaiveDateTime), + }) + .collect() } pub async fn create(db: &mut Transaction<'_, Sqlite>, logbook_id: i64, rower_id: i64) { diff --git a/src/model/user.rs b/src/model/user.rs index 9182679..0394e37 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -1,21 +1,44 @@ use std::ops::Deref; use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; -use chrono::{Datelike, Local, NaiveDate}; +use chrono::{Datelike, Local, NaiveDate, NaiveDateTime}; use log::info; use rocket::{ async_trait, + form::{self, FromFormField, ValueField}, http::{Cookie, Status}, request::{self, FromRequest, Outcome}, time::{Duration, OffsetDateTime}, - Request, + FromForm, Request, }; use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, SqlitePool}; +use sqlx::{sqlite::SqliteValueRef, Decode, FromRow, SqlitePool}; use super::{log::Log, tripdetails::TripDetails, Day}; -#[derive(FromRow, Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] +pub struct MyNaiveDateTime(pub chrono::NaiveDateTime); + +impl<'r> Decode<'r, sqlx::Sqlite> for MyNaiveDateTime { + fn decode(value: SqliteValueRef<'r>) -> Result> { + let dt: NaiveDateTime = Decode::decode(value)?; + Ok(MyNaiveDateTime(dt)) + } +} + +#[rocket::async_trait] +impl<'r> FromFormField<'r> for MyNaiveDateTime { + fn from_value(field: ValueField<'r>) -> form::Result<'r, Self> { + let dt = NaiveDateTime::parse_from_str(field.value, "%Y-%m-%d %H:%M:%S"); + + match dt { + Ok(parsed_dt) => Ok(MyNaiveDateTime(parsed_dt)), + Err(_) => Err(rocket::form::Error::validation("Invalid date/time format.").into()), + } + } +} + +#[derive(FromRow, Debug, Serialize, Deserialize, FromForm)] pub struct User { pub id: i64, pub name: String, @@ -23,9 +46,8 @@ pub struct User { pub is_cox: bool, pub is_admin: bool, pub is_guest: bool, - #[serde(default = "bool::default")] pub deleted: bool, - pub last_access: Option, + pub last_access: Option, } #[derive(Debug)] @@ -65,8 +87,7 @@ impl User { } pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option { - sqlx::query_as!( - User, + let row = sqlx::query!( " SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access FROM user @@ -76,12 +97,22 @@ WHERE id like ? ) .fetch_one(db) .await - .ok() + .ok()?; + + Some(User { + id: row.id, + name: row.name, + pw: row.pw, + is_cox: row.is_cox, + is_admin: row.is_admin, + is_guest: row.is_guest, + deleted: row.deleted, + last_access: row.last_access.map(MyNaiveDateTime), + }) } pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option { - sqlx::query_as!( - User, + let row = sqlx::query!( " SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access FROM user @@ -91,7 +122,18 @@ WHERE name like ? ) .fetch_one(db) .await - .ok() + .ok()?; + + Some(User { + id: row.id, + name: row.name, + pw: row.pw, + is_cox: row.is_cox, + is_admin: row.is_admin, + is_guest: row.is_guest, + deleted: row.deleted, + last_access: row.last_access.map(MyNaiveDateTime), + }) } pub async fn on_water(&self, db: &SqlitePool) -> bool { @@ -122,8 +164,7 @@ WHERE name like ? } pub async fn all(db: &SqlitePool) -> Vec { - sqlx::query_as!( - User, + sqlx::query!( " SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access FROM user @@ -133,12 +174,23 @@ ORDER BY last_access DESC ) .fetch_all(db) .await - .unwrap() //TODO: fixme + .unwrap() + .into_iter() + .map(|row| User { + id: row.id, + name: row.name, + pw: row.pw, + is_cox: row.is_cox, + is_admin: row.is_admin, + is_guest: row.is_guest, + deleted: row.deleted, + last_access: row.last_access.map(MyNaiveDateTime), + }) + .collect() } pub async fn cox(db: &SqlitePool) -> Vec { - sqlx::query_as!( - User, + sqlx::query!( " SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access FROM user @@ -148,7 +200,19 @@ ORDER BY last_access DESC ) .fetch_all(db) .await - .unwrap() //TODO: fixme + .unwrap() + .into_iter() + .map(|row| User { + id: row.id, + name: row.name, + pw: row.pw, + is_cox: row.is_cox, + is_admin: row.is_admin, + is_guest: row.is_guest, + deleted: row.deleted, + last_access: row.last_access.map(MyNaiveDateTime), + }) + .collect() } pub async fn create(db: &SqlitePool, name: &str, is_guest: bool) -> bool { diff --git a/src/tera/admin/boat.rs b/src/tera/admin/boat.rs index b8007c9..ac728fa 100644 --- a/src/tera/admin/boat.rs +++ b/src/tera/admin/boat.rs @@ -50,25 +50,22 @@ async fn delete(db: &State, _admin: AdminUser, boat: i32) -> Flash", data = "")] async fn update( db: &State, data: Form>, + boat_id: i32, _admin: AdminUser, ) -> Flash { - let boat = Boat::find_by_id(db, data.id).await; + let boat = Boat::find_by_id(db, boat_id).await; let Some(boat) = boat else { - return Flash::error( - Redirect::to("/admin/boat"), - "Boat does not exist!", - ) + return Flash::error(Redirect::to("/admin/boat"), "Boat does not exist!"); }; - if !boat.update(db, data.into_inner()).await { - return Flash::error(Redirect::to("/admin/boat"), "Boat could not be updated!"); + match boat.update(db, data.into_inner()).await { + Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Successfully updated boat"), + Err(e) => Flash::error(Redirect::to("/admin/boat"), e), } - - Flash::success(Redirect::to("/admin/boat"), "Successfully updated boat") } #[post("/boat/new", data = "")] diff --git a/templates/includes/forms/boat.html.tera b/templates/includes/forms/boat.html.tera index a17718d..f29e633 100644 --- a/templates/includes/forms/boat.html.tera +++ b/templates/includes/forms/boat.html.tera @@ -22,7 +22,7 @@ {% macro edit(boat, uuid) %} -
+
{{ boat.name }}