use axum::{ extract::Path, http::{ header::{AUTHORIZATION, CONTENT_TYPE}, HeaderMap, Method, }, routing::{get, post, put}, Json, Router, }; use tower_http::cors::{Any, CorsLayer}; use crate::{ core::Core, database::UserId, handlers::{ check_password, create_game, create_user, get_user, get_users, healthcheck, set_password, wrap_handler, AuthRequest, CreateGameRequest, CreateUserRequest, SetPasswordRequest, }, }; pub fn routes(core: Core) -> Router { Router::new() .route( "/api/v1/health", get({ let core = core.clone(); move || async { let result = healthcheck(core).await; println!("status: {:?}", String::from_utf8(result.to_owned())); result } }) .layer( CorsLayer::new() .allow_methods([Method::GET]) .allow_origin(Any), ), ) .route( "/api/v1/auth", post({ let core = core.clone(); move |req: Json| wrap_handler(|| async { let password_result = check_password(core, req).await; println!("check_auth result: {:?}", password_result); password_result }) }) .layer( CorsLayer::new() .allow_methods([Method::POST]) .allow_headers([CONTENT_TYPE]) .allow_origin(Any), ), ) .route( // By default, just get the self user. "/api/v1/user", get({ let core = core.clone(); move |headers: HeaderMap| wrap_handler(|| get_user(core, headers, None)) }) .layer( CorsLayer::new() .allow_methods([Method::GET]) .allow_headers([AUTHORIZATION]) .allow_origin(Any), ) .put({ let core = core.clone(); move |headers: HeaderMap, req: Json| { let Json(req) = req; wrap_handler(|| create_user(core, headers, req)) } }), ) .route( "/api/v1/users", get({ let core = core.clone(); move |headers: HeaderMap| wrap_handler(|| get_users(core, headers)) }) .layer( CorsLayer::new() .allow_methods([Method::GET]) .allow_headers([AUTHORIZATION]) .allow_origin(Any), ), ) .route( "/api/v1/user/password", put({ let core = core.clone(); move |headers: HeaderMap, req: Json| { let Json(req) = req; wrap_handler(|| set_password(core, headers, req)) } }), ) .route( "/api/v1/user/:user_id", get({ let core = core.clone(); move |user_id: Path, headers: HeaderMap| { let Path(user_id) = user_id; wrap_handler(|| get_user(core, headers, Some(user_id))) } }), ) .route( "/api/v1/games", put({ let core = core.clone(); move |headers: HeaderMap, req: Json| { let Json(req) = req; wrap_handler(|| create_game(core, headers, req)) } }), ) } #[cfg(test)] mod test { use std::{path::PathBuf, time::Duration}; use axum::http::StatusCode; use axum_test::TestServer; use chrono::Utc; use cool_asserts::assert_matches; use result_extended::ResultExt; use super::*; use crate::{ asset_db::FsAssets, core::{AuthResponse, Core}, database::{Database, DbConn, GameId, SessionId, UserId}, handlers::CreateGameRequest, types::{AccountState, UserOverview}, }; async fn initialize_test_server() -> (Core, TestServer) { let password_exp = Utc::now() + Duration::from_secs(5); let memory_db: Option = None; let conn = DbConn::new(memory_db); let _admin_id = conn.create_user("admin", "aoeu", true, AccountState::PasswordReset(password_exp)).await.unwrap(); let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn); let app = routes(core.clone()); let server = TestServer::new(app).unwrap(); (core, server) } async fn setup_with_admin() -> (Core, TestServer) { let (core, server) = initialize_test_server().await; core.set_password(UserId::from("admin"), "aoeu".to_owned()) .await; (core, server) } async fn setup_with_disabled_user() -> (Core, TestServer) { let (core, server) = setup_with_admin().await; let uuid = match core.create_user("shephard").await { ResultExt::Ok(uuid) => uuid, ResultExt::Err(err) => panic!("{}", err), ResultExt::Fatal(err) => panic!("{}", err), }; core.disable_user(uuid).await; (core, server) } async fn setup_with_user() -> (Core, TestServer) { let (core, server) = setup_with_admin().await; let response = server .post("/api/v1/auth") .json(&AuthRequest { username: "admin".to_owned(), password: "aoeu".to_owned(), }) .await; response.assert_status_ok(); let session_id: Option = response.json(); let session_id = session_id.unwrap(); let response = server .put("/api/v1/user") .add_header("Authorization", format!("Bearer {}", session_id)) .json(&CreateUserRequest { username: "savanni".to_owned(), }) .await; response.assert_status_ok(); let response = server .put("/api/v1/user") .add_header("Authorization", format!("Bearer {}", session_id)) .json(&CreateUserRequest { username: "shephard".to_owned(), }) .await; response.assert_status_ok(); (core, server) } #[tokio::test] async fn it_returns_a_healthcheck() { let (_core, server) = initialize_test_server().await; let response = server.get("/api/v1/health").await; response.assert_status_ok(); let b: crate::handlers::HealthCheck = response.json(); assert_eq!(b, crate::handlers::HealthCheck { ok: true }); } #[ignore] #[tokio::test] async fn a_new_user_has_an_expired_password() { let (_core, server) = setup_with_admin().await; let response = server .post("/api/v1/auth") .add_header("Content-Type", "application/json") .json(&AuthRequest { username: "admin".to_owned(), password: "aoeu".to_owned(), }) .await; response.assert_status_ok(); let session_id = response.json::>().unwrap(); let session_id = match session_id { AuthResponse::PasswordReset(session_id) => session_id, AuthResponse::Success(_) => panic!("admin user password has already been set"), AuthResponse::Locked => panic!("admin user is already expired"), }; let response = server .put("/api/v1/user") .add_header("Authorization", format!("Bearer {}", session_id)) .json("savanni") .await; response.assert_status_ok(); let response = server .post("/api/v1/auth") .add_header("Content-Type", "application/json") .json(&AuthRequest { username: "savanni".to_owned(), password: "".to_owned(), }) .await; response.assert_status_ok(); let session = response.json::>().unwrap(); assert_matches!(session, AuthResponse::PasswordReset(_)); } #[ignore] #[tokio::test] async fn it_refuses_to_authenticate_a_disabled_user() { let (_core, _server) = setup_with_disabled_user().await; unimplemented!() } #[ignore] #[tokio::test] async fn it_forces_changing_expired_password() { let (_core, _server) = setup_with_user().await; unimplemented!() } #[tokio::test] async fn it_authenticates_a_user() { let (_core, server) = setup_with_admin().await; let response = server .post("/api/v1/auth") .json(&AuthRequest { username: "admin".to_owned(), password: "wrong".to_owned(), }) .await; response.assert_status(StatusCode::UNAUTHORIZED); let response = server .post("/api/v1/auth") .json(&AuthRequest { username: "unknown".to_owned(), password: "wrong".to_owned(), }) .await; response.assert_status(StatusCode::UNAUTHORIZED); let response = server .post("/api/v1/auth") .json(&AuthRequest { username: "admin".to_owned(), password: "aoeu".to_owned(), }) .await; response.assert_status_ok(); assert_matches!(response.json(), Some(AuthResponse::PasswordReset(_))); } #[tokio::test] async fn it_returns_user_profile() { let (_core, server) = setup_with_admin().await; let response = server.get("/api/v1/user").await; response.assert_status(StatusCode::UNAUTHORIZED); let response = server .post("/api/v1/auth") .json(&AuthRequest { username: "admin".to_owned(), password: "aoeu".to_owned(), }) .await; response.assert_status_ok(); let session_id = assert_matches!(response.json(), Some(AuthResponse::PasswordReset(session_id)) => session_id); println!("it_returns_user_profile: {}", session_id); let response = server .get("/api/v1/user") .add_header("Authorization", format!("Bearer {}", session_id)) .await; response.assert_status_ok(); let profile: Option = response.json(); let profile = profile.unwrap(); assert_eq!(profile.name, "admin"); } #[ignore] #[tokio::test] async fn a_user_can_get_any_user_profile() { let (core, server) = setup_with_user().await; let savanni = match core.user_by_username("savanni").await { ResultExt::Ok(Some(savanni)) => savanni, ResultExt::Ok(None) => panic!("user was not initialized"), ResultExt::Err(err) => panic!("{:?}", err), ResultExt::Fatal(err) => panic!("{:?}", err), }; let response = server .post("/api/v1/auth") .json(&AuthRequest { username: "savanni".to_owned(), password: "".to_owned(), }) .await; let session_id: Option = response.json(); let session_id = session_id.unwrap(); let response = server .get(&format!("/api/v1/user/{}", savanni.id)) .add_header("Authorization", format!("Bearer {}", session_id)) .await; response.assert_status_ok(); let profile: Option = response.json(); let profile = profile.unwrap(); assert_eq!(profile.name, "savanni"); let response = server .get("/api/v1/user/admin") .add_header("Authorization", format!("Bearer {}", session_id)) .await; response.assert_status_ok(); let profile: Option = response.json(); let profile = profile.unwrap(); assert_eq!(profile.name, "admin"); } #[ignore] #[tokio::test] async fn a_user_can_change_their_password() { let (_core, server) = setup_with_user().await; let response = server .post("/api/v1/auth") .json(&AuthRequest { username: "savanni".to_owned(), password: "".to_owned(), }) .await; let session_id: Option = response.json(); let session_id = session_id.unwrap(); let response = server .get("/api/v1/user") .add_header("Authorization", format!("Bearer {}", session_id)) .await; let profile = response.json::>().unwrap(); assert_eq!(profile.name, "savanni"); let response = server .put("/api/v1/user/password") .json(&SetPasswordRequest { password_1: "abcdefg".to_owned(), password_2: "abcd".to_owned(), }) .await; response.assert_status(StatusCode::UNAUTHORIZED); let response = server .put("/api/v1/user/password") .add_header("Authorization", format!("Bearer {}", session_id)) .json(&SetPasswordRequest { password_1: "abcdefg".to_owned(), password_2: "abcd".to_owned(), }) .await; response.assert_status(StatusCode::BAD_REQUEST); let response = server .put("/api/v1/user/password") .add_header("Authorization", format!("Bearer {}", session_id)) .json(&SetPasswordRequest { password_1: "abcdefg".to_owned(), password_2: "abcdefg".to_owned(), }) .await; response.assert_status(StatusCode::OK); } #[ignore] #[tokio::test] async fn a_user_can_create_a_game() { let (_core, server) = setup_with_user().await; let response = server .post("/api/v1/auth") .json(&AuthRequest { username: "savanni".to_owned(), password: "".to_owned(), }) .await; let session_id = response.json::>().unwrap(); let response = server .put("/api/v1/games") .add_header("Authorization", format!("Bearer {}", session_id)) .json(&CreateGameRequest { game_type: "Candela".to_owned(), game_name: "Circle of the Winter Solstice".to_owned(), }) .await; let _game_id = response.json::>().unwrap(); } #[ignore] #[tokio::test] async fn gms_can_invite_others_into_a_game() { unimplemented!(); } }