use std::net::IpAddr; use rocket::{ form::Form, get, http::{Cookie, CookieJar}, post, request::{self, FlashMessage, FromRequest}, response::{Flash, Redirect}, routes, time::{Duration, OffsetDateTime}, Request, Route, State, }; use rocket_dyn_templates::{context, Template}; use sqlx::SqlitePool; use tera::Context; use crate::model::{ boat::Boat, boatreservation::BoatReservation, log::Log, logbook::{ LogToAdd, LogToFinalize, LogToUpdate, Logbook, LogbookAdminUpdateError, LogbookCreateError, LogbookDeleteError, LogbookUpdateError, }, logtype::LogType, user::{AdminUser, DonauLinzUser, User, UserWithDetails, VorstandUser}, }; pub struct KioskCookie(()); #[rocket::async_trait] impl<'r> FromRequest<'r> for KioskCookie { type Error = std::convert::Infallible; async fn from_request(request: &'r Request<'_>) -> request::Outcome { match request.cookies().get_private("kiosk") { Some(_) => request::Outcome::Success(KioskCookie(())), None => request::Outcome::Forward(rocket::http::Status::SeeOther), } } } #[get("/", rank = 2)] async fn index( db: &State, flash: Option>, user: DonauLinzUser, ) -> Template { let boats = Boat::for_user(db, &user).await; let mut coxes: Vec = futures::future::join_all( User::cox(db) .await .into_iter() .map(|user| UserWithDetails::from_user(user, db)), ) .await; coxes.retain(|u| { u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into()) }); let mut users: Vec = futures::future::join_all( User::all(db) .await .into_iter() .map(|user| UserWithDetails::from_user(user, db)), ) .await; users.retain(|u| { u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into()) || u.user.name == "Externe Steuerperson" }); let logtypes = LogType::all(db).await; let distances = Logbook::distances(db).await; let on_water = Logbook::on_water(db).await; let mut context = Context::new(); if let Some(msg) = flash { context.insert("flash", &msg.into_inner()); } context.insert("boats", &boats); context.insert( "reservations", &BoatReservation::all_future_with_groups(db).await, ); context.insert("coxes", &coxes); context.insert("users", &users); context.insert("logtypes", &logtypes); context.insert( "loggedin_user", &UserWithDetails::from_user(user.into(), db).await, ); context.insert("on_water", &on_water); context.insert("distances", &distances); Template::render("log", context.into_json()) } #[get("/show", rank = 3)] async fn show(db: &State, user: DonauLinzUser) -> Template { let logs = Logbook::completed(db).await; Template::render( "log.completed", context!(logs, loggedin_user: &UserWithDetails::from_user(user.into(), db).await), ) } #[get("/show?", rank = 2)] async fn show_for_year(db: &State, user: AdminUser, year: i32) -> Template { let logs = Logbook::completed_in_year(db, year).await; Template::render( "log.completed", context!(logs, loggedin_user: &UserWithDetails::from_user(user.user, db).await), ) } #[get("/show")] async fn show_kiosk(db: &State, _kiosk: KioskCookie) -> Template { let logs = Logbook::completed(db).await; Template::render("log.completed", context!(logs, show_kiosk_header: true)) } #[get("/kiosk/ekrv2019/")] async fn new_kiosk( db: &State, cookies: &CookieJar<'_>, loc: String, ip: Option, ) -> Redirect { Log::create( db, format!("New kiosk cookie set for loc '{loc}' (IP={ip:?})"), ) .await; let mut cookie = Cookie::new("kiosk", loc); cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(12)); cookies.add_private(cookie); Redirect::to("/log") } #[get("/")] async fn kiosk( db: &State, flash: Option>, _kiosk: KioskCookie, ) -> Template { let boats = Boat::all(db).await; let mut coxes: Vec = futures::future::join_all( User::cox(db) .await .into_iter() .map(|user| UserWithDetails::from_user(user, db)), ) .await; coxes.retain(|u| { u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into()) }); let mut users: Vec = futures::future::join_all( User::all(db) .await .into_iter() .map(|user| UserWithDetails::from_user(user, db)), ) .await; users.retain(|u| { u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into()) }); let logtypes = LogType::all(db).await; let distances = Logbook::distances(db).await; let on_water = Logbook::on_water(db).await; let mut context = Context::new(); if let Some(msg) = flash { context.insert("flash", &msg.into_inner()); } context.insert("boats", &boats); context.insert( "reservations", &BoatReservation::all_future_with_groups(db).await, ); context.insert("coxes", &coxes); context.insert("users", &users); context.insert("logtypes", &logtypes); context.insert("on_water", &on_water); context.insert("distances", &distances); context.insert("show_kiosk_header", &true); Template::render("kiosk", context.into_json()) } async fn create_logbook( db: &SqlitePool, data: Form, user: &DonauLinzUser, ) -> Flash { match Logbook::create( db, data.into_inner(), user ) .await { Ok(msg) => Flash::success(Redirect::to("/log"), format!("Ausfahrt erfolgreich hinzugefügt{msg}")), Err(LogbookCreateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Boot schon am Wasser"), Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(Redirect::to("/log"), format!("Ruderer {} schon am Wasser", rower.name)), Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"),"Boot gesperrt"), Err(LogbookCreateError::BoatNotFound) => Flash::error(Redirect::to("/log"), "Boot gibt's ned"), Err(LogbookCreateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")), Err(LogbookCreateError::RowerCreateError(rower, e)) => Flash::error(Redirect::to("/log"), format!("Fehler bei Ruderer {rower}: {e}")), Err(LogbookCreateError::ArrivalNotAfterDeparture) => Flash::error(Redirect::to("/log"), "Ankunftszeit kann nicht vor der Abfahrtszeit sein"), Err(LogbookCreateError::UserNotAllowedToUseBoat) => Flash::error(Redirect::to("/log"), "Schiffsführer darf dieses Boot nicht verwenden"), Err(LogbookCreateError::SteeringPersonNotInRowers) => Flash::error(Redirect::to("/log"), "Steuerperson nicht in Liste der Ruderer!"), Err(LogbookCreateError::ShipmasterNotInRowers) => Flash::error(Redirect::to("/log"), "Schiffsführer nicht in Liste der Ruderer!"), Err(LogbookCreateError::NotYourEntry) => Flash::error(Redirect::to("/log"), "Nicht deine Ausfahrt!"), Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(Redirect::to("/log"), "Ankunftszeit gesetzt aber nicht Distanz + Strecke"), Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."), Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(Redirect::to("/log"), "Handsteuer-Status dieses Boots kann nicht verändert werden."), Err(LogbookCreateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")), Err(LogbookCreateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."), Err(LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"), } } #[post("/", data = "", rank = 2)] async fn create( db: &State, data: Form, user: DonauLinzUser, ) -> Flash { Log::create( db, format!("User {} tries to create log entry={:?}", &user.name, data), ) .await; create_logbook(db, data, &user).await } #[post("/", data = "")] async fn create_kiosk( db: &State, data: Form, _kiosk: KioskCookie, ) -> Flash { let Some(boat) = Boat::find_by_id(db, data.boat_id).await else { return Flash::error(Redirect::to("/log"), "Boot gibt's nicht"); }; let creator = if boat.amount_seats == 1 && boat.owner.is_some() { User::find_by_id(db, boat.owner.unwrap() as i32) .await .unwrap() } else if let Some(shipmaster) = data.shipmaster { User::find_by_id(db, shipmaster as i32).await.unwrap() } else { let Some(rower) = data.rowers.first() else { return Flash::error( Redirect::to("/log"), "Ausfahrt ohne Benutzer kann nicht angelegt werden.", ); }; User::find_by_id(db, *rower as i32).await.unwrap() }; Log::create( db, format!( "Kiosk tries to create log for shipmaster {} entry={:?}", creator.name, data ), ) .await; create_logbook(db, data, &DonauLinzUser(creator)).await //TODO: fixme } #[post("/update", data = "")] async fn update( db: &State, data: Form, user: VorstandUser, ) -> Flash { let data = data.into_inner(); let Some(logbook) = Logbook::find_by_id(db, data.id).await else { return Flash::error(Redirect::to("/log"), &format!("Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt", data.id)); }; match logbook.update(db, data.clone(), &user.0).await { Ok(()) => { Log::create( db, format!( "User {} updated log entry={:?} to {:?}", &user.name, logbook, data ), ) .await; Flash::success( Redirect::to("/log/show"), format!("Logbucheintrag erfolgreich bearbeitet"), ) } Err(LogbookAdminUpdateError::NotAllowed) => Flash::error( Redirect::to("/log/show"), format!("Du hast keine Erlaubnis, diesen Logbucheintrag zu bearbeiten!"), ), } } async fn home_logbook( db: &SqlitePool, data: Form, logbook_id: i64, user: &DonauLinzUser, ) -> Flash { let logbook: Option = Logbook::find_by_id(db, logbook_id).await; let Some(logbook) = logbook else { return Flash::error( Redirect::to("/admin/log"), format!("Log with ID {} does not exist!", logbook_id), ); }; match logbook.home(db,user, data.into_inner()).await { Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"), Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")), Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."), Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")), Err(LogbookUpdateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."), Err(LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"), Err(e) => Flash::error( Redirect::to("/log"), format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"), ), } } #[post("/", data = "")] async fn home_kiosk( db: &State, data: Form, logbook_id: i64, _kiosk: KioskCookie, ) -> Flash { let logbook = Logbook::find_by_id(db, logbook_id).await.unwrap(); //TODO: fixme Log::create( db, format!("Kiosk tries to finish log entry {logbook_id} {data:?}"), ) .await; home_logbook( db, data, logbook_id, &DonauLinzUser( User::find_by_id(db, logbook.shipmaster as i32) .await .unwrap(), ), //TODO: fixme ) .await } #[post("/", data = "", rank = 2)] async fn home( db: &State, data: Form, logbook_id: i64, user: DonauLinzUser, ) -> Flash { Log::create( db, format!( "User {} tries to finish log entry {logbook_id} {data:?}", &user.name ), ) .await; home_logbook(db, data, logbook_id, &user).await } #[get("//delete", rank = 2)] async fn delete(db: &State, logbook_id: i64, user: DonauLinzUser) -> Flash { let logbook = Logbook::find_by_id(db, logbook_id).await; if let Some(logbook) = logbook { Log::create( db, format!("User {} tries to delete log entry {logbook_id}", &user.name), ) .await; match logbook.delete(db, &user).await { Ok(_) => Flash::success( Redirect::to("/log"), format!("Eintrag {} gelöscht!", logbook_id), ), Err(LogbookDeleteError::NotYourEntry) => Flash::error( Redirect::to("/log"), "Du hast nicht die Berechtigung, den Eintrag zu löschen!", ), } } else { Flash::error( Redirect::to("/log"), format!("Logbook with ID {} could not be found!", logbook_id), ) } } #[get("//delete")] async fn delete_kiosk( db: &State, logbook_id: i64, _kiosk: KioskCookie, ) -> Flash { let logbook = Logbook::find_by_id(db, logbook_id).await; if let Some(logbook) = logbook { let cox = User::find_by_id(db, logbook.shipmaster as i32) .await .unwrap(); Log::create(db, format!("Kiosk tries to delete log entry {logbook_id} ")).await; match logbook.delete(db, &cox).await { Ok(_) => Flash::success( Redirect::to("/log"), format!("Eintrag {} gelöscht!", logbook_id), ), Err(LogbookDeleteError::NotYourEntry) => Flash::error( Redirect::to("/log"), "Du hast nicht die Berechtigung, den Eintrag zu löschen!", ), } } else { Flash::error( Redirect::to("/log"), format!("Logbook with ID {} could not be found!", logbook_id), ) } } pub fn routes() -> Vec { routes![ index, create, create_kiosk, home, kiosk, home_kiosk, new_kiosk, show, show_kiosk, show_for_year, delete, delete_kiosk, update ] } #[cfg(test)] mod test { use rocket::http::ContentType; use rocket::{http::Status, local::asynchronous::Client}; use sqlx::SqlitePool; use crate::model::logbook::Logbook; use crate::tera::{log::Boat, User}; use crate::testdb; #[sqlx::test] fn test_kiosk_cookie() { let db = testdb!(); let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); let client = Client::tracked(rocket).await.unwrap(); let req = client.get("/log"); let response = req.dispatch().await; assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.headers().get("Location").next(), Some("/auth")); let req = client.get("/log/kiosk/ekrv2019/Linz"); let response = req.dispatch().await; assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.headers().get("Location").next(), Some("/log")); let req = client.get("/log"); let response = req.dispatch().await; assert_eq!(response.status(), Status::Ok); let text = response.into_string().await.unwrap(); assert!(text.contains("Logbuch")); assert!(text.contains("Neue Ausfahrt")); //assert!(!text.contains("Ottensheim Boot")); } #[sqlx::test] fn test_kiosk_cookie_boat() { let db = testdb!(); let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); let client = Client::tracked(rocket).await.unwrap(); let req = client.get("/log/kiosk/ekrv2019/Ottensheim"); let response = req.dispatch().await; assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.headers().get("Location").next(), Some("/log")); let req = client.get("/log"); let response = req.dispatch().await; assert_eq!(response.status(), Status::Ok); let text = response.into_string().await.unwrap(); assert!(text.contains("Logbuch")); assert!(text.contains("Neue Ausfahrt")); assert!(text.contains("Ottensheim Boot")); } #[sqlx::test] fn test_index() { let db = testdb!(); let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); let client = Client::tracked(rocket).await.unwrap(); let login = client .post("/auth") .header(ContentType::Form) // Set the content type to form .body("name=admin&password=admin"); // Add the form data to the request body; login.dispatch().await; let req = client.get("/log"); let response = req.dispatch().await; let text = response.into_string().await.unwrap(); assert!(text.contains("Logbuch")); assert!(text.contains("Neue Ausfahrt")); } #[sqlx::test] fn test_show() { let db = testdb!(); let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); let client = Client::tracked(rocket).await.unwrap(); let login = client .post("/auth") .header(ContentType::Form) // Set the content type to form .body("name=admin&password=admin"); // Add the form data to the request body; login.dispatch().await; let req = client.get("/log/show"); let response = req.dispatch().await; let text = response.into_string().await.unwrap(); println!("{text:?}"); assert!(text.contains("Logbuch")); assert!(text.contains("Joe")); } #[sqlx::test] fn test_show_kiosk() { let db = testdb!(); let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); let client = Client::tracked(rocket).await.unwrap(); let req = client.get("/log/kiosk/ekrv2019/Linz"); let _ = req.dispatch().await; let req = client.get("/log/show"); let response = req.dispatch().await; let text = response.into_string().await.unwrap(); assert!(text.contains("Logbuch")); assert!(text.contains("Joe")); } #[sqlx::test] fn test_create() { let db = testdb!(); let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); let client = Client::tracked(rocket).await.unwrap(); let login = client .post("/auth") .header(ContentType::Form) // Set the content type to form .body("name=admin&password=admin"); // Add the form data to the request body; login.dispatch().await; let current_date = chrono::Local::now().format("%Y-%m-%d").to_string(); let req = client.post("/log").header(ContentType::Form).body(format!( "boat_id=1&shipmaster=4&departure={0}T10:00&steering_person=4&rowers[]=4", current_date )); let response = req.dispatch().await; assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.headers().get("Location").next(), Some("/log")); let flash_cookie = response .cookies() .get("_flash") .expect("Expected flash cookie"); assert_eq!( flash_cookie.value(), "7:successAusfahrt erfolgreich hinzugefügt" ); } #[sqlx::test] fn test_home_kiosk() { let db = testdb!(); let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); let client = Client::tracked(rocket).await.unwrap(); let req = client.get("/log/kiosk/ekrv2019/Linz"); let _ = req.dispatch().await; let current_date = chrono::Local::now().format("%Y-%m-%d").to_string(); let req = client .post("/log/1") .header(ContentType::Form) .body(format!("destination=Ottensheim&distance_in_km=25&shipmaster=2&steering_person=2&departure={0}T10:00&arrival={0}T12:00&rowers[]=2", current_date)); let response = req.dispatch().await; assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.headers().get("Location").next(), Some("/log")); let flash_cookie = response .cookies() .get("_flash") .expect("Expected flash cookie"); assert_eq!( flash_cookie.value(), "7:successAusfahrt korrekt eingetragen" ); } //Kiosk mode // i see all boats #[sqlx::test] fn test_kiosks_sees_all_boats() { let db = testdb!(); let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); let client = Client::tracked(rocket).await.unwrap(); let req = client.get("/log/kiosk/ekrv2019/Linz"); let _ = req.dispatch().await; let req = client.get("/log"); let response = req.dispatch().await; let text = response.into_string().await.unwrap(); //Sees all boats stationed in Linz assert!(text.contains("Haichenbach")); assert!(text.contains("Joe")); assert!(text.contains("Kaputtes Boot :-(")); assert!(text.contains("Sehr kaputtes Boot :-((")); assert!(text.contains("second_private_boat_from_rower")); assert!(text.contains("private_boat_from_rower")); //Doesn't see the one's in Ottensheim //assert!(!text.contains("Ottensheim Boot")); } #[sqlx::test] fn test_kiosks_can_start_trips_with_all_boats() { let db = testdb!(); let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); sqlx::query("DELETE FROM logbook;") .execute(&db) .await .unwrap(); let mut client = Client::tracked(rocket).await.unwrap(); let req = client.get("/log/kiosk/ekrv2019/Linz"); let _ = req.dispatch().await; can_start_and_end_trip(&db, &mut client, "Haichenbach".into(), "admin".into()).await; can_start_and_end_trip(&db, &mut client, "Joe".into(), "admin".into()).await; can_start_and_end_trip(&db, &mut client, "Kaputtes Boot :-(".into(), "admin".into()).await; cant_start_trip( &db, &mut client, "Sehr kaputtes Boot :-((".into(), "admin".into(), "Boot gesperrt".into(), ) .await; can_start_and_end_trip( &db, &mut client, "second_private_boat_from_rower".into(), "rower".into(), ) .await; } #[sqlx::test] fn test_shipowner_can_allow_others_to_drive() { let db = testdb!(); let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); sqlx::query("DELETE FROM logbook;") .execute(&db) .await .unwrap(); let client = Client::tracked(rocket).await.unwrap(); let login = client .post("/auth") .header(ContentType::Form) // Set the content type to form .body("name=rower&password=rower"); // Add the form data to the request body; login.dispatch().await; // Owner can start trip: let boat_id = Boat::find_by_name(&db, "private_boat_from_rower".into()) .await .unwrap() .id; let shipmaster_id = User::find_by_name(&db, "rower2".into()).await.unwrap().id; let current_date = chrono::Local::now().format("%Y-%m-%d").to_string(); let req = client.post("/log").header(ContentType::Form).body(format!( "boat_id={boat_id}&shipmaster={shipmaster_id}&departure={0}T10:00&steering_person={shipmaster_id}&rowers[]={shipmaster_id}", current_date )); let response = req.dispatch().await; assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.headers().get("Location").next(), Some("/log")); let flash_cookie = response .cookies() .get("_flash") .expect("Expected flash cookie"); assert_eq!( flash_cookie.value(), "7:successAusfahrt erfolgreich hinzugefügt" ); // Shipmaster can end it let log_id = Logbook::highest_id(&db).await; let login = client .post("/auth") .header(ContentType::Form) // Set the content type to form .body("name=rower2&password=rower"); // Add the form data to the request body; login.dispatch().await; let req = client .post(format!("/log/{log_id}")) .header(ContentType::Form) .body(format!("destination=Ottensheim&distance_in_km=25&shipmaster={shipmaster_id}&steering_person={shipmaster_id}&departure={0}T10:00&arrival={0}T12:00&rowers[]={shipmaster_id}", current_date)); let response = req.dispatch().await; assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.headers().get("Location").next(), Some("/log")); let flash_cookie = response .cookies() .get("_flash") .expect("Expected flash cookie"); assert_eq!( flash_cookie.value(), "7:successAusfahrt korrekt eingetragen" ); } #[sqlx::test] fn test_normal_user_sees_appropriate_boats() { let db = testdb!(); let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); let mut client = Client::tracked(rocket).await.unwrap(); let login = client .post("/auth") .header(ContentType::Form) // Set the content type to form .body("name=rower&password=rower"); // Add the form data to the request body; login.dispatch().await; let req = client.get("/log"); let response = req.dispatch().await; let text = response.into_string().await.unwrap(); sqlx::query("DELETE FROM logbook;") .execute(&db) .await .unwrap(); //Sees all 1x assert!(text.contains("Haichenbach")); can_start_and_end_trip(&db, &mut client, "Haichenbach".into(), "rower".into()).await; assert!(text.contains("private_boat_from_rower")); can_start_and_end_trip( &db, &mut client, "private_boat_from_rower".into(), "rower".into(), ) .await; assert!(text.contains("second_private_boat_from_rower")); can_start_and_end_trip( &db, &mut client, "second_private_boat_from_rower".into(), "rower".into(), ) .await; //Don't see anything else assert!(!text.contains("Joe")); cant_start_trip( &db, &mut client, "Joe".into(), "rower".into(), "Schiffsführer darf dieses Boot nicht verwenden".into(), ) .await; assert!(!text.contains("Kaputtes Boot :-(")); cant_start_trip( &db, &mut client, "Kaputtes Boot :-(".into(), "rower".into(), "Schiffsführer darf dieses Boot nicht verwenden".into(), ) .await; assert!(!text.contains("Sehr kaputtes Boot :-((")); cant_start_trip( &db, &mut client, "Sehr kaputtes Boot :-((".into(), "rower".into(), "Boot gesperrt".into(), ) .await; assert!(!text.contains("Ottensheim Boot")); cant_start_trip( &db, &mut client, "Ottensheim Boot".into(), "rower".into(), "Schiffsführer darf dieses Boot nicht verwenden".into(), ) .await; } #[sqlx::test] fn test_cox_sees_appropriate_boats() { let db = testdb!(); let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); let mut client = Client::tracked(rocket).await.unwrap(); let login = client .post("/auth") .header(ContentType::Form) // Set the content type to form .body("name=cox&password=cox"); // Add the form data to the request body; login.dispatch().await; sqlx::query("DELETE FROM logbook;") .execute(&db) .await .unwrap(); let req = client.get("/log"); let response = req.dispatch().await; let text = response.into_string().await.unwrap(); //Sees all 1x assert!(text.contains("Haichenbach")); can_start_and_end_trip(&db, &mut client, "Haichenbach".into(), "cox".into()).await; assert!(text.contains("Joe")); can_start_and_end_trip(&db, &mut client, "Joe".into(), "cox".into()).await; assert!(text.contains("Kaputtes Boot :-(")); can_start_and_end_trip(&db, &mut client, "Kaputtes Boot :-(".into(), "cox".into()).await; assert!(text.contains("Sehr kaputtes Boot :-((")); cant_start_trip( &db, &mut client, "Sehr kaputtes Boot :-((".into(), "cox".into(), "Boot gesperrt".into(), ) .await; assert!(text.contains("Ottensheim Boot")); can_start_and_end_trip(&db, &mut client, "Ottensheim Boot".into(), "cox".into()).await; //Can't use private boats assert!(!text.contains("private_boat_from_rower")); cant_start_trip( &db, &mut client, "private_boat_from_rower".into(), "cox".into(), "Schiffsführer darf dieses Boot nicht verwenden".into(), ) .await; assert!(!text.contains("second_private_boat_from_rower")); cant_start_trip( &db, &mut client, "second_private_boat_from_rower".into(), "cox".into(), "Schiffsführer darf dieses Boot nicht verwenden".into(), ) .await; } #[sqlx::test] fn test_cant_end_trip_other_user() { let db = testdb!(); let rocket = rocket::build().manage(db.clone()); let rocket = crate::tera::config(rocket); let client = Client::tracked(rocket).await.unwrap(); let login = client .post("/auth") .header(ContentType::Form) // Set the content type to form .body("name=rower2&password=rower"); // Add the form data to the request body; login.dispatch().await; let current_date = chrono::Local::now().format("%Y-%m-%d").to_string(); let req = client .post("/log/1") .header(ContentType::Form) .body(format!("destination=Ottensheim&distance_in_km=25&shipmaster=1&steering_person=1&departure={0}T10:00&arrival={0}T12:00&rowers[]=1", current_date)); let response = req.dispatch().await; assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.headers().get("Location").next(), Some("/log")); let flash_cookie = response .cookies() .get("_flash") .expect("Expected flash cookie"); assert_eq!( flash_cookie.value(), "5:errorEintrag 1 konnte nicht abgesendet werden (Fehler: NotYourEntry)!" ); } async fn can_start_and_end_trip( db: &SqlitePool, client: &mut Client, boat_name: String, shipmaster_name: String, ) { let boat_id = Boat::find_by_name(db, boat_name).await.unwrap().id; let shipmaster_id = User::find_by_name(db, &shipmaster_name).await.unwrap().id; let current_date = chrono::Local::now().format("%Y-%m-%d").to_string(); let req = client.post("/log").header(ContentType::Form).body(format!( "boat_id={boat_id}&shipmaster={shipmaster_id}&departure={current_date}T10:00&steering_person={shipmaster_id}&rowers[]={shipmaster_id}" )); let response = req.dispatch().await; assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.headers().get("Location").next(), Some("/log")); let flash_cookie = response .cookies() .get("_flash") .expect("Expected flash cookie"); assert_eq!( flash_cookie.value(), "7:successAusfahrt erfolgreich hinzugefügt" ); let log_id = Logbook::highest_id(db).await; let req = client .post(format!("/log/{log_id}")) .header(ContentType::Form) .body(format!("destination=Ottensheim&distance_in_km=25&shipmaster={shipmaster_id}&steering_person={shipmaster_id}&departure={current_date}T10:00&arrival={current_date}T12:00&rowers[]={shipmaster_id}")); let response = req.dispatch().await; assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.headers().get("Location").next(), Some("/log")); let flash_cookie = response .cookies() .get("_flash") .expect("Expected flash cookie"); assert_eq!( flash_cookie.value(), "7:successAusfahrt korrekt eingetragen" ); } async fn cant_start_trip( db: &SqlitePool, client: &mut Client, boat_name: String, shipmaster_name: String, reason: String, ) { let boat_id = Boat::find_by_name(db, boat_name).await.unwrap().id; let shipmaster_id = User::find_by_name(db, &shipmaster_name).await.unwrap().id; let current_date = chrono::Local::now().format("%Y-%m-%d").to_string(); let req = client.post("/log").header(ContentType::Form).body(format!( "boat_id={boat_id}&shipmaster={shipmaster_id}&departure={current_date}T10:00&steering_person={shipmaster_id}&rowers[]={shipmaster_id}" )); let response = req.dispatch().await; assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.headers().get("Location").next(), Some("/log")); let flash_cookie = response .cookies() .get("_flash") .expect("Expected flash cookie"); assert_eq!(flash_cookie.value(), format!("5:error{}", reason)); } }