Test user authentication

This commit is contained in:
Savanni D'Gerinel 2024-12-31 12:56:05 -05:00
parent a33b94e5b3
commit a0f1a0b81c
7 changed files with 110 additions and 19 deletions

View File

@ -35,3 +35,4 @@ CREATE TABLE roles(
FOREIGN KEY(game_id) REFERENCES games(uuid) FOREIGN KEY(game_id) REFERENCES games(uuid)
); );
INSERT INTO users VALUES ('admin', 'admin', '', true, true);

View File

@ -11,7 +11,7 @@ use uuid::Uuid;
use crate::{ use crate::{
asset_db::{self, AssetId, Assets}, asset_db::{self, AssetId, Assets},
database::{CharacterId, Database, SessionId, UserId}, database::{CharacterId, Database, SessionId, UserId},
types::{AppError, FatalError, Game, Message, Tabletop, User, Rgb}, types::{AppError, FatalError, Game, Message, Rgb, Tabletop, User},
}; };
const DEFAULT_BACKGROUND_COLOR: Rgb = Rgb { const DEFAULT_BACKGROUND_COLOR: Rgb = Rgb {
@ -154,9 +154,7 @@ impl Core {
asset_db::Error::Inaccessible => { asset_db::Error::Inaccessible => {
AppError::Inaccessible(format!("{}", asset_id)) AppError::Inaccessible(format!("{}", asset_id))
} }
asset_db::Error::Unexpected(err) => { asset_db::Error::Unexpected(err) => AppError::Inaccessible(format!("{}", err)),
AppError::Inaccessible(format!("{}", err))
}
}), }),
) )
} }
@ -212,6 +210,25 @@ impl Core {
}); });
} }
pub async fn save_user(
&self,
uuid: Option<UserId>,
username: &str,
password: &str,
admin: bool,
enabled: bool,
) -> ResultExt<UserId, AppError, FatalError> {
let state = self.0.read().await;
match state
.db
.save_user(uuid, username, password, admin, enabled)
.await
{
Ok(uuid) => ok(uuid),
Err(err) => fatal(err),
}
}
pub async fn set_password( pub async fn set_password(
&self, &self,
uuid: UserId, uuid: UserId,
@ -290,7 +307,7 @@ mod test {
]); ]);
let memory_db: Option<PathBuf> = None; let memory_db: Option<PathBuf> = None;
let conn = DbConn::new(memory_db); let conn = DbConn::new(memory_db);
conn.save_user(None, "admin", "aoeu", true, true) conn.save_user(Some(UserId::from("admin")), "admin", "aoeu", true, true)
.await .await
.unwrap(); .unwrap();
conn.save_user(None, "gm_1", "aoeu", false, true) conn.save_user(None, "gm_1", "aoeu", false, true)

View File

@ -100,7 +100,7 @@ impl DiskDb {
) -> Result<UserId, FatalError> { ) -> Result<UserId, FatalError> {
match user_id { match user_id {
None => { None => {
let user_id = UserId::new(); let user_id = UserId::default();
let mut stmt = self let mut stmt = self
.conn .conn
.prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)") .prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)")

View File

@ -173,7 +173,7 @@ mod test {
let no_path: Option<PathBuf> = None; let no_path: Option<PathBuf> = None;
let db = DiskDb::new(no_path).unwrap(); let db = DiskDb::new(no_path).unwrap();
db.save_user(None, "admin", "abcdefg", true, true).unwrap(); db.save_user(Some(UserId::from("admin")), "admin", "abcdefg", true, true).unwrap();
let game_id = db.save_game(None, "Candela").unwrap(); let game_id = db.save_game(None, "Candela").unwrap();
(db, game_id) (db, game_id)
} }

View File

@ -6,15 +6,17 @@ use uuid::Uuid;
pub struct UserId(String); pub struct UserId(String);
impl UserId { impl UserId {
pub fn new() -> Self {
Self(format!("{}", Uuid::new_v4().hyphenated()))
}
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
&self.0 &self.0
} }
} }
impl Default for UserId {
fn default() -> Self {
Self(format!("{}", Uuid::new_v4().hyphenated()))
}
}
impl From<&str> for UserId { impl From<&str> for UserId {
fn from(s: &str) -> Self { fn from(s: &str) -> Self {
Self(s.to_owned()) Self(s.to_owned())

View File

@ -32,8 +32,8 @@ pub async fn healthcheck(core: Core) -> Vec<u8> {
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[typeshare] #[typeshare]
pub struct AuthRequest { pub struct AuthRequest {
username: String, pub username: String,
password: String, pub password: String,
} }
pub async fn check_password(core: Core, req: Json<AuthRequest>) -> (StatusCode, Json<Option<SessionId>>) { pub async fn check_password(core: Core, req: Json<AuthRequest>) -> (StatusCode, Json<Option<SessionId>>) {

View File

@ -1,4 +1,7 @@
use axum::{routing::{get, post}, Json, Router}; use axum::{
routing::{get, post},
Json, Router,
};
use crate::{ use crate::{
core::Core, core::Core,
@ -27,12 +30,19 @@ pub fn routes(core: Core) -> Router {
mod test { mod test {
use std::path::PathBuf; use std::path::PathBuf;
use axum::http::StatusCode;
use axum_test::TestServer; use axum_test::TestServer;
use cool_asserts::assert_matches;
use result_extended::ResultExt;
use crate::{asset_db::FsAssets, core::Core, database::DbConn};
use super::*; use super::*;
use crate::{
asset_db::FsAssets,
core::Core,
database::{Database, DbConn, SessionId, UserId},
};
fn setup() -> (Core, TestServer) { fn setup_without_admin() -> (Core, TestServer) {
let memory_db: Option<PathBuf> = None; let memory_db: Option<PathBuf> = None;
let conn = DbConn::new(memory_db); let conn = DbConn::new(memory_db);
let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn); let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
@ -41,14 +51,75 @@ mod test {
(core, server) (core, server)
} }
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)
}
#[tokio::test] #[tokio::test]
async fn it_returns_a_healthcheck() { async fn it_returns_a_healthcheck() {
let (_core, server) = setup(); let (core, server) = setup_without_admin();
let response = server.get("/api/v1/health").await; let response = server.get("/api/v1/health").await;
response.assert_status_ok(); response.assert_status_ok();
let b: crate::handlers::HealthCheck = response.json(); let b: crate::handlers::HealthCheck = response.json();
assert_eq!(b, crate::handlers::HealthCheck { ok: false }); assert_eq!(b, crate::handlers::HealthCheck { ok: false });
assert_matches!(
core.save_user(Some(UserId::from("admin")), "admin", "aoeu", true, true)
.await,
ResultExt::Ok(_)
);
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 });
}
#[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() {
unimplemented!();
} }
} }