2024-12-31 17:56:05 +00:00
|
|
|
use axum::{
|
2025-01-01 04:47:40 +00:00
|
|
|
extract::Path,
|
2025-01-03 16:59:33 +00:00
|
|
|
http::{header::CONTENT_TYPE, HeaderMap, Method, StatusCode},
|
2025-01-01 05:19:12 +00:00
|
|
|
routing::{get, post, put},
|
2024-12-31 17:56:05 +00:00
|
|
|
Json, Router,
|
|
|
|
};
|
2025-01-03 16:59:33 +00:00
|
|
|
use tower_http::cors::{Any, CorsLayer};
|
2024-12-31 06:12:26 +00:00
|
|
|
|
|
|
|
use crate::{
|
|
|
|
core::Core,
|
2025-01-03 03:34:10 +00:00
|
|
|
database::{SessionId, UserId},
|
2024-12-31 21:34:59 +00:00
|
|
|
handlers::{
|
2025-01-03 03:34:10 +00:00
|
|
|
check_password, create_game, create_user, get_user, healthcheck, set_password,
|
|
|
|
wrap_handler, AuthRequest, CreateGameRequest, CreateUserRequest, SetPasswordRequest,
|
2024-12-31 21:34:59 +00:00
|
|
|
},
|
2024-12-31 06:12:26 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
pub fn routes(core: Core) -> Router {
|
|
|
|
Router::new()
|
|
|
|
.route(
|
|
|
|
"/api/v1/health",
|
|
|
|
get({
|
|
|
|
let core = core.clone();
|
|
|
|
move || healthcheck(core)
|
2025-01-03 16:59:33 +00:00
|
|
|
})
|
|
|
|
.layer(
|
|
|
|
CorsLayer::new()
|
|
|
|
.allow_methods([Method::GET])
|
|
|
|
.allow_origin(Any),
|
|
|
|
),
|
2024-12-31 06:12:26 +00:00
|
|
|
)
|
|
|
|
.route(
|
|
|
|
"/api/v1/auth",
|
|
|
|
post({
|
|
|
|
let core = core.clone();
|
2025-01-03 03:34:10 +00:00
|
|
|
move |req: Json<AuthRequest>| wrap_handler(|| check_password(core, req))
|
2025-01-03 16:59:33 +00:00
|
|
|
}).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))
|
|
|
|
})
|
|
|
|
.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/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))
|
|
|
|
}
|
2025-01-02 16:57:17 +00:00
|
|
|
}),
|
|
|
|
)
|
2024-12-31 06:12:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
2024-12-31 17:56:05 +00:00
|
|
|
use axum::http::StatusCode;
|
2024-12-31 06:12:26 +00:00
|
|
|
use axum_test::TestServer;
|
2024-12-31 17:56:05 +00:00
|
|
|
use cool_asserts::assert_matches;
|
|
|
|
use result_extended::ResultExt;
|
2024-12-31 06:12:26 +00:00
|
|
|
|
|
|
|
use super::*;
|
2024-12-31 17:56:05 +00:00
|
|
|
use crate::{
|
|
|
|
asset_db::FsAssets,
|
|
|
|
core::Core,
|
2025-01-02 16:57:17 +00:00
|
|
|
database::{Database, DbConn, GameId, SessionId, UserId},
|
2025-01-02 19:45:06 +00:00
|
|
|
handlers::CreateGameRequest,
|
|
|
|
types::UserProfile,
|
2024-12-31 17:56:05 +00:00
|
|
|
};
|
2024-12-31 06:12:26 +00:00
|
|
|
|
2024-12-31 17:56:05 +00:00
|
|
|
fn setup_without_admin() -> (Core, TestServer) {
|
2024-12-31 06:12:26 +00:00
|
|
|
let memory_db: Option<PathBuf> = None;
|
|
|
|
let conn = DbConn::new(memory_db);
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2024-12-31 17:56:05 +00:00
|
|
|
async fn setup_admin_enabled() -> (Core, TestServer) {
|
|
|
|
let memory_db: Option<PathBuf> = None;
|
|
|
|
let conn = DbConn::new(memory_db);
|
|
|
|
conn.save_user(Some(UserId::from("admin")), "admin", "aoeu", true, true)
|
|
|
|
.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)
|
|
|
|
}
|
|
|
|
|
2025-01-01 04:47:40 +00:00
|
|
|
async fn setup_with_user() -> (Core, TestServer) {
|
|
|
|
let (core, server) = setup_admin_enabled().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;
|
2025-01-02 16:57:17 +00:00
|
|
|
response.assert_status_ok();
|
2025-01-01 04:47:40 +00:00
|
|
|
|
2025-01-02 16:57:17 +00:00
|
|
|
let response = server
|
|
|
|
.put("/api/v1/user")
|
|
|
|
.add_header("Authorization", format!("Bearer {}", session_id))
|
|
|
|
.json(&CreateUserRequest {
|
|
|
|
username: "shephard".to_owned(),
|
|
|
|
})
|
|
|
|
.await;
|
2025-01-01 04:47:40 +00:00
|
|
|
response.assert_status_ok();
|
|
|
|
|
|
|
|
(core, server)
|
|
|
|
}
|
|
|
|
|
2024-12-31 06:12:26 +00:00
|
|
|
#[tokio::test]
|
|
|
|
async fn it_returns_a_healthcheck() {
|
2024-12-31 17:56:05 +00:00
|
|
|
let (core, server) = setup_without_admin();
|
|
|
|
|
2024-12-31 06:12:26 +00:00
|
|
|
let response = server.get("/api/v1/health").await;
|
|
|
|
response.assert_status_ok();
|
2024-12-31 17:56:05 +00:00
|
|
|
let b: crate::handlers::HealthCheck = response.json();
|
|
|
|
assert_eq!(b, crate::handlers::HealthCheck { ok: false });
|
|
|
|
|
|
|
|
assert_matches!(
|
|
|
|
core.save_user(Some(UserId::from("admin")), "admin", "aoeu", true, true)
|
|
|
|
.await,
|
|
|
|
ResultExt::Ok(_)
|
|
|
|
);
|
2024-12-31 06:12:26 +00:00
|
|
|
|
2024-12-31 17:56:05 +00:00
|
|
|
let response = server.get("/api/v1/health").await;
|
|
|
|
response.assert_status_ok();
|
2024-12-31 06:12:26 +00:00
|
|
|
let b: crate::handlers::HealthCheck = response.json();
|
2024-12-31 17:56:05 +00:00
|
|
|
assert_eq!(b, crate::handlers::HealthCheck { ok: true });
|
|
|
|
}
|
2024-12-31 06:12:26 +00:00
|
|
|
|
2024-12-31 17:56:05 +00:00
|
|
|
#[tokio::test]
|
|
|
|
async fn it_authenticates_a_user() {
|
|
|
|
let (_core, server) = setup_admin_enabled().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();
|
|
|
|
let session_id: Option<SessionId> = response.json();
|
|
|
|
assert!(session_id.is_some());
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn it_returns_user_profile() {
|
2024-12-31 19:09:21 +00:00
|
|
|
let (_core, server) = setup_admin_enabled().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: 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;
|
|
|
|
response.assert_status_ok();
|
|
|
|
let profile: Option<UserProfile> = response.json();
|
|
|
|
let profile = profile.unwrap();
|
2025-01-02 19:45:06 +00:00
|
|
|
assert_eq!(profile.id, UserId::from("admin"));
|
|
|
|
assert_eq!(profile.name, "admin");
|
2024-12-31 19:09:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn an_admin_can_create_a_user() {
|
2025-01-01 04:47:40 +00:00
|
|
|
// All of the contents of this test are basically required for any test on individual
|
|
|
|
// users, so I moved it all into the setup code.
|
|
|
|
let (_core, _server) = setup_with_user().await;
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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<UserProfile> = response.json();
|
|
|
|
let profile = profile.unwrap();
|
2025-01-02 19:45:06 +00:00
|
|
|
assert_eq!(profile.name, "savanni");
|
2025-01-01 04:47:40 +00:00
|
|
|
|
|
|
|
let response = server
|
|
|
|
.get("/api/v1/user/admin")
|
|
|
|
.add_header("Authorization", format!("Bearer {}", session_id))
|
|
|
|
.await;
|
|
|
|
response.assert_status_ok();
|
|
|
|
let profile: Option<UserProfile> = response.json();
|
|
|
|
let profile = profile.unwrap();
|
2025-01-02 19:45:06 +00:00
|
|
|
assert_eq!(profile.name, "admin");
|
2025-01-01 04:47:40 +00:00
|
|
|
}
|
|
|
|
|
2025-01-01 05:19:12 +00:00
|
|
|
#[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<UserProfile>>().unwrap();
|
2025-01-02 19:45:06 +00:00
|
|
|
assert_eq!(profile.name, "savanni");
|
2025-01-01 05:19:12 +00:00
|
|
|
|
|
|
|
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
|
2025-01-02 16:57:17 +00:00
|
|
|
.put("/api/v1/user/password")
|
2025-01-01 05:19:12 +00:00
|
|
|
.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);
|
|
|
|
}
|
|
|
|
|
2024-12-31 21:34:59 +00:00
|
|
|
#[tokio::test]
|
|
|
|
async fn a_user_can_create_a_game() {
|
2025-01-02 16:57:17 +00:00
|
|
|
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();
|
2024-12-31 21:34:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[ignore]
|
|
|
|
#[tokio::test]
|
|
|
|
async fn gms_can_invite_others_into_a_game() {
|
|
|
|
unimplemented!();
|
|
|
|
}
|
2024-12-31 06:12:26 +00:00
|
|
|
}
|