monorepo/visions/server/src/routes.rs

455 lines
15 KiB
Rust

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<AuthRequest>| 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<CreateUserRequest>| {
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<SetPasswordRequest>| {
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<UserId>, 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<CreateGameRequest>| {
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<PathBuf> = 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<SessionId> = 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::<Option<AuthResponse>>().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::<Option<AuthResponse>>().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<UserOverview> = 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<SessionId> = 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<UserOverview> = 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<UserOverview> = 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<SessionId> = 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::<Option<UserOverview>>().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::<Option<SessionId>>().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::<Option<GameId>>().unwrap();
}
#[ignore]
#[tokio::test]
async fn gms_can_invite_others_into_a_game() {
unimplemented!();
}
}