diff --git a/agdb_server/openapi/schema.json b/agdb_server/openapi/schema.json index 58d9b9b6..8fcaece9 100644 --- a/agdb_server/openapi/schema.json +++ b/agdb_server/openapi/schema.json @@ -17,7 +17,7 @@ "operationId": "list", "responses": { "200": { - "description": "Ok", + "description": "ok", "content": { "application/json": { "schema": { @@ -28,6 +28,9 @@ } } } + }, + "401": { + "description": "unauthorized" } }, "security": [ @@ -44,8 +47,11 @@ ], "operationId": "shutdown", "responses": { - "200": { - "description": "Server is shutting down" + "204": { + "description": "server is shutting down" + }, + "401": { + "description": "unauthorized" } }, "security": [ @@ -72,17 +78,17 @@ "required": true }, "responses": { - "200": { - "description": "Password changed" + "201": { + "description": "password changed" }, "401": { - "description": "Invalid password" + "description": "unauthorized" }, - "403": { - "description": "User not found" + "461": { + "description": "password too short (<8)" }, - "462": { - "description": "Password too short (<8)" + "464": { + "description": "user not found" } }, "security": [ @@ -110,18 +116,26 @@ }, "responses": { "201": { - "description": "User created" + "description": "user created" + }, + "401": { + "description": "unauthorized" }, "461": { - "description": "Name too short (<3)" + "description": "password too short (<8)" }, "462": { - "description": "Password too short (<8)" + "description": "name too short (<3)" }, "463": { - "description": "User already exists" + "description": "user already exists" } - } + }, + "security": [ + { + "Token": [] + } + ] } }, "/api/v1/admin/user/list": { @@ -132,7 +146,7 @@ "operationId": "list", "responses": { "200": { - "description": "Ok", + "description": "ok", "content": { "application/json": { "schema": { @@ -143,6 +157,9 @@ } } } + }, + "401": { + "description": "unauthorized" } }, "security": [ @@ -170,13 +187,16 @@ }, "responses": { "201": { - "description": "Database added" + "description": "db added" }, - "403": { - "description": "Database already exists" + "401": { + "description": "unauthorized" }, - "461": { - "description": "Invalid database name" + "465": { + "description": "db already exists" + }, + "467": { + "description": "db invalid" } }, "security": [ @@ -203,11 +223,17 @@ "required": true }, "responses": { - "200": { - "description": "Database deleted" + "204": { + "description": "db deleted" + }, + "401": { + "description": "unauthorized" }, "403": { - "description": "Database not found for user" + "description": "user must be a db admin" + }, + "466": { + "description": "db not found" } }, "security": [ @@ -225,7 +251,7 @@ "operationId": "list", "responses": { "200": { - "description": "Ok", + "description": "ok", "content": { "application/json": { "schema": { @@ -236,6 +262,9 @@ } } } + }, + "401": { + "description": "unauthorized" } }, "security": [ @@ -262,11 +291,14 @@ "required": true }, "responses": { - "200": { - "description": "Database removed" + "204": { + "description": "db removed" }, - "403": { - "description": "Database not found for user" + "401": { + "description": "unauthorized" + }, + "466": { + "description": "db not found" } }, "security": [ @@ -294,19 +326,19 @@ }, "responses": { "201": { - "description": "User added" + "description": "user added" }, - "403": { - "description": "Can only be done by a db admin" + "401": { + "description": "unauthorized" }, - "461": { - "description": "Database not found" + "403": { + "description": "user must be a db admin / cannot add self" }, - "462": { - "description": "User not found" + "464": { + "description": "user not found" }, - "463": { - "description": "Cannot add self" + "466": { + "description": "db not found" } }, "security": [ @@ -346,17 +378,17 @@ "required": true }, "responses": { - "200": { - "description": "Password changed" + "201": { + "description": "password changed" }, "401": { - "description": "Invalid password" + "description": "invalid credentials" }, - "403": { - "description": "User not found" + "461": { + "description": "password too short (<8)" }, - "462": { - "description": "Password too short (<8)" + "464": { + "description": "user not found" } }, "security": [ @@ -384,7 +416,7 @@ }, "responses": { "200": { - "description": "Login successful", + "description": "login successful", "content": { "text/plain": { "schema": { @@ -394,10 +426,10 @@ } }, "401": { - "description": "Bad password" + "description": "invalid credentials" }, - "403": { - "description": "User not found" + "464": { + "description": "user not found" } } } diff --git a/agdb_server/src/db.rs b/agdb_server/src/db.rs index 5669efdf..f051c9e3 100644 --- a/agdb_server/src/db.rs +++ b/agdb_server/src/db.rs @@ -3,7 +3,10 @@ mod server_db_storage; use crate::config::Config; use crate::db::server_db::ServerDb; +use crate::db::server_db::ServerDbImpl; +use crate::error_code::ErrorCode; use crate::password::Password; +use crate::server_error::ServerError; use crate::server_error::ServerResult; use agdb::Comparison; use agdb::CountComparison; @@ -15,6 +18,7 @@ use std::collections::HashMap; use std::path::Path; use std::sync::Arc; use std::sync::RwLock; +use std::sync::RwLockReadGuard; use std::sync::RwLockWriteGuard; const SERVER_DB_NAME: &str = "mapped:agdb_server.agdb"; @@ -91,10 +95,16 @@ impl DbPool { } pub(crate) fn add_database(&self, user: DbId, database: Database) -> ServerResult { - let db = ServerDb::new(&format!("{}:{}", database.db_type, database.name))?; + let db = ServerDb::new(&format!("{}:{}", database.db_type, database.name)).map_err( + |mut e| { + e.status = ErrorCode::DbInvalid.into(); + e.description = format!("{}: {}", ErrorCode::DbInvalid.as_str(), e.description); + e + }, + )?; self.get_pool_mut()?.insert(database.name.clone(), db); - self.0.server_db.get_mut()?.transaction_mut(|t| { + self.db_mut()?.transaction_mut(|t| { let db = t.exec_mut(&QueryBuilder::insert().nodes().values(&database).query())?; t.exec_mut( @@ -110,7 +120,7 @@ impl DbPool { } pub(crate) fn add_database_user(&self, database: DbId, user: DbId, role: &str) -> ServerResult { - self.0.server_db.get_mut()?.exec_mut( + self.db_mut()?.exec_mut( &QueryBuilder::insert() .edges() .from(user) @@ -122,7 +132,7 @@ impl DbPool { } pub(crate) fn create_user(&self, user: DbUser) -> ServerResult { - self.0.server_db.get_mut()?.transaction_mut(|t| { + self.db_mut()?.transaction_mut(|t| { let user = t.exec_mut(&QueryBuilder::insert().nodes().values(&user).query())?; t.exec_mut( @@ -154,9 +164,7 @@ impl DbPool { pub(crate) fn find_databases(&self) -> ServerResult> { Ok(self - .0 - .server_db - .get()? + .db()? .exec( &QueryBuilder::select() .ids( @@ -173,9 +181,7 @@ impl DbPool { pub(crate) fn find_database_id(&self, name: &str) -> ServerResult { Ok(self - .0 - .server_db - .get()? + .db()? .exec( &QueryBuilder::search() .from("dbs") @@ -189,15 +195,16 @@ impl DbPool { )? .elements .get(0) - .ok_or(format!("Database '{name}' not found"))? + .ok_or(ServerError::new( + ErrorCode::DbNotFound.into(), + &format!("{}: {name}", ErrorCode::DbNotFound.as_str()), + ))? .id) } pub(crate) fn find_users(&self) -> ServerResult> { Ok(self - .0 - .server_db - .get()? + .db()? .exec( &QueryBuilder::select() .values(vec!["name".into()]) @@ -220,9 +227,7 @@ impl DbPool { pub(crate) fn find_user_databases(&self, user: DbId) -> ServerResult> { Ok(self - .0 - .server_db - .get()? + .db()? .exec( &QueryBuilder::select() .ids( @@ -247,6 +252,7 @@ impl DbPool { .ids( QueryBuilder::search() .from(user) + .limit(1) .where_() .distance(CountComparison::Equal(2)) .and() @@ -256,14 +262,18 @@ impl DbPool { ) .query(), )? + .elements + .get(0) + .ok_or(ServerError::new( + ErrorCode::DbNotFound.into(), + &format!("{}: {name}", ErrorCode::DbNotFound.as_str()), + ))? .try_into()?) } pub(crate) fn find_user(&self, name: &str) -> ServerResult { Ok(self - .0 - .server_db - .get()? + .db()? .exec( &QueryBuilder::select() .ids( @@ -279,14 +289,18 @@ impl DbPool { ) .query(), )? + .elements + .get(0) + .ok_or(ServerError::new( + ErrorCode::UserNotFound.into(), + &format!("{}: {name}", ErrorCode::UserNotFound.as_str()), + ))? .try_into()?) } pub(crate) fn find_user_id(&self, name: &str) -> ServerResult { Ok(self - .0 - .server_db - .get()? + .db()? .exec( &QueryBuilder::search() .from("users") @@ -300,15 +314,16 @@ impl DbPool { )? .elements .get(0) - .ok_or(format!("User '{name}' not found"))? + .ok_or(ServerError::new( + ErrorCode::UserNotFound.into(), + &format!("{}: {name}", ErrorCode::UserNotFound.as_str()), + ))? .id) } pub(crate) fn find_user_id_by_token(&self, token: &str) -> ServerResult { Ok(self - .0 - .server_db - .get()? + .db()? .exec( &QueryBuilder::search() .from("users") @@ -328,9 +343,7 @@ impl DbPool { pub(crate) fn is_db_admin(&self, user: DbId, db: DbId) -> ServerResult { Ok(self - .0 - .server_db - .get()? + .db()? .exec( &QueryBuilder::search() .from(user) @@ -348,16 +361,14 @@ impl DbPool { } pub(crate) fn remove_database(&self, db: Database) -> ServerResult { - self.0 - .server_db - .get_mut()? + self.db_mut()? .exec_mut(&QueryBuilder::remove().ids(db.db_id.unwrap()).query())?; Ok(self.get_pool_mut()?.remove(&db.name).unwrap()) } pub(crate) fn save_token(&self, user: DbId, token: &str) -> ServerResult { - self.0.server_db.get_mut()?.exec_mut( + self.db_mut()?.exec_mut( &QueryBuilder::insert() .values_uniform(vec![("token", token).into()]) .ids(user) @@ -367,9 +378,7 @@ impl DbPool { } pub(crate) fn save_user(&self, user: DbUser) -> ServerResult { - self.0 - .server_db - .get_mut()? + self.db_mut()? .exec_mut(&QueryBuilder::insert().element(&user).query())?; Ok(()) } @@ -381,4 +390,12 @@ impl DbPool { fn get_pool_mut(&self) -> ServerResult>> { Ok(self.0.pool.write()?) } + + fn db(&self) -> ServerResult> { + self.0.server_db.get() + } + + fn db_mut(&self) -> ServerResult> { + self.0.server_db.get_mut() + } } diff --git a/agdb_server/src/db/server_db.rs b/agdb_server/src/db/server_db.rs index fcb1618e..ba31ebe6 100644 --- a/agdb_server/src/db/server_db.rs +++ b/agdb_server/src/db/server_db.rs @@ -6,7 +6,7 @@ use std::sync::RwLock; use std::sync::RwLockReadGuard; use std::sync::RwLockWriteGuard; -type ServerDbImpl = DbImpl; +pub(crate) type ServerDbImpl = DbImpl; pub(crate) struct ServerDb(pub(crate) Arc>); impl ServerDb { diff --git a/agdb_server/src/error_code.rs b/agdb_server/src/error_code.rs new file mode 100644 index 00000000..23691b12 --- /dev/null +++ b/agdb_server/src/error_code.rs @@ -0,0 +1,53 @@ +use crate::server_error::ServerError; +use axum::http::StatusCode; + +pub(crate) enum ErrorCode { + PasswordTooShort, + NameTooShort, + UserNotFound, + UserExists, + DbExists, + DbNotFound, + DbInvalid, +} + +impl From for StatusCode { + fn from(value: ErrorCode) -> Self { + (&value).into() + } +} + +impl From<&ErrorCode> for StatusCode { + fn from(value: &ErrorCode) -> Self { + StatusCode::from_u16(match value { + ErrorCode::PasswordTooShort => 461, + ErrorCode::NameTooShort => 462, + ErrorCode::UserExists => 463, + ErrorCode::UserNotFound => 464, + ErrorCode::DbExists => 465, + ErrorCode::DbNotFound => 466, + ErrorCode::DbInvalid => 467, + }) + .unwrap() + } +} + +impl From for ServerError { + fn from(value: ErrorCode) -> Self { + ServerError::new((&value).into(), value.as_str()) + } +} + +impl ErrorCode { + pub(crate) fn as_str(&self) -> &str { + match self { + ErrorCode::PasswordTooShort => "password too short (<8)", + ErrorCode::NameTooShort => "name too short (<3)", + ErrorCode::UserExists => "user already exists", + ErrorCode::UserNotFound => "user not found", + ErrorCode::DbExists => "db already exists", + ErrorCode::DbNotFound => "db not found", + ErrorCode::DbInvalid => "db invalid", + } + } +} diff --git a/agdb_server/src/main.rs b/agdb_server/src/main.rs index 2f7d00e2..ddbcdeda 100644 --- a/agdb_server/src/main.rs +++ b/agdb_server/src/main.rs @@ -2,6 +2,7 @@ mod api; mod app; mod config; mod db; +mod error_code; mod logger; mod password; mod routes; diff --git a/agdb_server/src/password.rs b/agdb_server/src/password.rs index 909d5451..586cad6b 100644 --- a/agdb_server/src/password.rs +++ b/agdb_server/src/password.rs @@ -1,11 +1,11 @@ +use crate::error_code::ErrorCode; +use crate::server_error::ServerResult; use ring::digest; use ring::pbkdf2; use ring::rand::SecureRandom; use ring::rand::SystemRandom; use std::num::NonZeroU32; -use crate::server_error::ServerResult; - pub(crate) const PASSWORD_LEN: usize = digest::SHA256_OUTPUT_LEN; static ALGORITHM: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256; static PEPPER: &[u8; 16] = std::include_bytes!("../pepper"); @@ -84,6 +84,22 @@ impl Password { } } +pub(crate) fn validate_password(password: &str) -> ServerResult { + if password.len() < 8 { + Err(ErrorCode::PasswordTooShort.into()) + } else { + Ok(()) + } +} + +pub(crate) fn validate_username(name: &str) -> ServerResult { + if name.len() < 3 { + Err(ErrorCode::NameTooShort.into()) + } else { + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/agdb_server/src/routes/admin.rs b/agdb_server/src/routes/admin.rs index 39f33a3e..ff2dfef2 100644 --- a/agdb_server/src/routes/admin.rs +++ b/agdb_server/src/routes/admin.rs @@ -10,7 +10,8 @@ use tokio::sync::broadcast::Sender; path = "/api/v1/admin/shutdown", security(("Token" = [])), responses( - (status = 200, description = "Server is shutting down"), + (status = 204, description = "server is shutting down"), + (status = 401, description = "unauthorized"), ) )] pub(crate) async fn shutdown( @@ -18,7 +19,7 @@ pub(crate) async fn shutdown( State(shutdown_sender): State>, ) -> StatusCode { match shutdown_sender.send(()) { - Ok(_) => StatusCode::OK, + Ok(_) => StatusCode::NO_CONTENT, Err(_) => StatusCode::INTERNAL_SERVER_ERROR, } } @@ -34,7 +35,7 @@ mod tests { let status = shutdown(AdminId(DbId(0)), State(shutdown_sender)).await; - assert_eq!(status, StatusCode::OK); + assert_eq!(status, StatusCode::NO_CONTENT); Ok(()) } diff --git a/agdb_server/src/routes/admin/db.rs b/agdb_server/src/routes/admin/db.rs index b22caa17..5a1f7194 100644 --- a/agdb_server/src/routes/admin/db.rs +++ b/agdb_server/src/routes/admin/db.rs @@ -1,6 +1,6 @@ use crate::db::DbPool; use crate::routes::db::ServerDatabase; -use crate::server_error::ServerError; +use crate::server_error::ServerResponse; use crate::user_id::AdminId; use axum::extract::State; use axum::http::StatusCode; @@ -10,13 +10,14 @@ use axum::Json; path = "/api/v1/admin/db/list", security(("Token" = [])), responses( - (status = 200, description = "Ok", body = Vec) + (status = 200, description = "ok", body = Vec), + (status = 401, description = "unauthorized"), ) )] pub(crate) async fn list( _admin: AdminId, State(db_pool): State, -) -> Result<(StatusCode, Json>), ServerError> { +) -> ServerResponse<(StatusCode, Json>)> { let dbs = db_pool .find_databases()? .into_iter() diff --git a/agdb_server/src/routes/admin/user.rs b/agdb_server/src/routes/admin/user.rs index 3acf04fc..1161d584 100644 --- a/agdb_server/src/routes/admin/user.rs +++ b/agdb_server/src/routes/admin/user.rs @@ -1,8 +1,11 @@ use crate::db::DbPool; use crate::db::DbUser; +use crate::error_code::ErrorCode; +use crate::password; use crate::password::Password; use crate::routes::user::UserCredentials; use crate::server_error::ServerError; +use crate::server_error::ServerResponse; use crate::user_id::AdminId; use axum::extract::State; use axum::http::StatusCode; @@ -20,59 +23,51 @@ pub(crate) struct UserStatus { security(("Token" = [])), request_body = UserCredentials, responses( - (status = 200, description = "Password changed"), - (status = 401, description = "Invalid password"), - (status = 403, description = "User not found"), - (status = 462, description = "Password too short (<8)"), + (status = 201, description = "password changed"), + (status = 401, description = "unauthorized"), + (status = 461, description = "password too short (<8)"), + (status = 464, description = "user not found"), ) )] pub(crate) async fn change_password( _admin_id: AdminId, State(db_pool): State, Json(request): Json, -) -> Result { - if request.password.len() < 8 { - return Ok(StatusCode::from_u16(462_u16)?); - } - - let mut user = db_pool - .find_user(&request.name) - .map_err(|_| ServerError::new(StatusCode::FORBIDDEN, "User not found"))?; +) -> ServerResponse { + password::validate_password(&request.password)?; + let mut user = db_pool.find_user(&request.name)?; let pswd = Password::create(&request.name, &request.password); user.password = pswd.password.to_vec(); user.salt = pswd.user_salt.to_vec(); db_pool.save_user(user)?; - Ok(StatusCode::OK) + Ok(StatusCode::CREATED) } #[utoipa::path(post, path = "/api/v1/admin/user/create", request_body = UserCredentials, + security(("Token" = [])), responses( - (status = 201, description = "User created"), - (status = 461, description = "Name too short (<3)"), - (status = 462, description = "Password too short (<8)"), - (status = 463, description = "User already exists") + (status = 201, description = "user created"), + (status = 401, description = "unauthorized"), + (status = 461, description = "password too short (<8)"), + (status = 462, description = "name too short (<3)"), + (status = 463, description = "user already exists") ) )] pub(crate) async fn create( _admin_id: AdminId, State(db_pool): State, Json(request): Json, -) -> Result { - if request.name.len() < 3 { - return Ok(StatusCode::from_u16(461_u16)?); - } - - if request.password.len() < 8 { - return Ok(StatusCode::from_u16(462_u16)?); - } +) -> ServerResponse { + password::validate_username(&request.name)?; + password::validate_password(&request.password)?; if db_pool.find_user_id(&request.name).is_ok() { - return Ok(StatusCode::from_u16(463_u16)?); + return Err(ErrorCode::UserExists.into()); } let pswd = Password::create(&request.name, &request.password); @@ -92,7 +87,8 @@ pub(crate) async fn create( path = "/api/v1/admin/user/list", security(("Token" = [])), responses( - (status = 200, description = "Ok", body = Vec) + (status = 200, description = "ok", body = Vec), + (status = 401, description = "unauthorized"), ) )] pub(crate) async fn list( diff --git a/agdb_server/src/routes/db.rs b/agdb_server/src/routes/db.rs index b4434b25..cea9f946 100644 --- a/agdb_server/src/routes/db.rs +++ b/agdb_server/src/routes/db.rs @@ -2,7 +2,8 @@ pub(crate) mod user; use crate::db::Database; use crate::db::DbPool; -use crate::server_error::ServerError; +use crate::error_code::ErrorCode; +use crate::server_error::ServerResponse; use crate::user_id::UserId; use axum::extract::State; use axum::http::StatusCode; @@ -59,33 +60,28 @@ impl From for ServerDatabase { request_body = ServerDatabase, security(("Token" = [])), responses( - (status = 201, description = "Database added"), - (status = 403, description = "Database already exists"), - (status = 461, description = "Invalid database name"), + (status = 201, description = "db added"), + (status = 401, description = "unauthorized"), + (status = 465, description = "db already exists"), + (status = 467, description = "db invalid"), ) )] pub(crate) async fn add( user: UserId, State(db_pool): State, Json(request): Json, -) -> Result { +) -> ServerResponse { if db_pool.find_database_id(&request.name).is_ok() { - return Ok(StatusCode::FORBIDDEN); + return Err(ErrorCode::DbExists.into()); } - db_pool - .add_database( - user.0, - Database { - db_id: None, - name: request.name, - db_type: request.db_type.to_string(), - }, - ) - .map_err(|mut e| { - e.status = StatusCode::from_u16(461).unwrap(); - e - })?; + let db = Database { + db_id: None, + name: request.name, + db_type: request.db_type.to_string(), + }; + + db_pool.add_database(user.0, db)?; Ok(StatusCode::CREATED) } @@ -95,38 +91,40 @@ pub(crate) async fn add( request_body = ServerDatabaseName, security(("Token" = [])), responses( - (status = 200, description = "Database deleted"), - (status = 403, description = "Database not found for user"), + (status = 204, description = "db deleted"), + (status = 401, description = "unauthorized"), + (status = 403, description = "user must be a db admin"), + (status = 466, description = "db not found"), ) )] pub(crate) async fn delete( user: UserId, State(db_pool): State, Json(request): Json, -) -> Result { - let db = db_pool - .find_user_database(user.0, &request.name) - .map_err(|mut e| { - e.status = StatusCode::FORBIDDEN; - e - })?; +) -> ServerResponse { + let db = db_pool.find_user_database(user.0, &request.name)?; + + if !db_pool.is_db_admin(user.0, db.db_id.unwrap())? { + return Ok(StatusCode::FORBIDDEN); + } db_pool.delete_database(db)?; - Ok(StatusCode::OK) + Ok(StatusCode::NO_CONTENT) } #[utoipa::path(get, path = "/api/v1/db/list", security(("Token" = [])), responses( - (status = 200, description = "Ok", body = Vec) + (status = 200, description = "ok", body = Vec), + (status = 401, description = "unauthorized"), ) )] pub(crate) async fn list( user: UserId, State(db_pool): State, -) -> Result<(StatusCode, Json>), ServerError> { +) -> ServerResponse<(StatusCode, Json>)> { let dbs = db_pool .find_user_databases(user.0)? .into_iter() @@ -140,23 +138,23 @@ pub(crate) async fn list( request_body = ServerDatabaseName, security(("Token" = [])), responses( - (status = 200, description = "Database removed"), - (status = 403, description = "Database not found for user"), + (status = 204, description = "db removed"), + (status = 401, description = "unauthorized"), + (status = 466, description = "db not found"), ) )] pub(crate) async fn remove( user: UserId, State(db_pool): State, Json(request): Json, -) -> Result { - let db = db_pool - .find_user_database(user.0, &request.name) - .map_err(|mut e| { - e.status = StatusCode::FORBIDDEN; - e - })?; +) -> ServerResponse { + let db = db_pool.find_user_database(user.0, &request.name)?; + + if !db_pool.is_db_admin(user.0, db.db_id.unwrap())? { + return Ok(StatusCode::FORBIDDEN); + } db_pool.remove_database(db)?; - Ok(StatusCode::OK) + Ok(StatusCode::NO_CONTENT) } diff --git a/agdb_server/src/routes/db/user.rs b/agdb_server/src/routes/db/user.rs index 8c1862c6..a96d2112 100644 --- a/agdb_server/src/routes/db/user.rs +++ b/agdb_server/src/routes/db/user.rs @@ -1,5 +1,5 @@ use crate::db::DbPool; -use crate::server_error::ServerError; +use crate::server_error::ServerResponse; use crate::user_id::UserId; use axum::extract::State; use axum::http::StatusCode; @@ -38,38 +38,25 @@ impl Display for DbUserRole { request_body = AddDatabaseUser, security(("Token" = [])), responses( - (status = 201, description = "User added"), - (status = 403, description = "Can only be done by a db admin"), - (status = 461, description = "Database not found"), - (status = 462, description = "User not found"), - (status = 463, description = "Cannot add self"), + (status = 201, description = "user added"), + (status = 401, description = "unauthorized"), + (status = 403, description = "user must be a db admin / cannot add self"), + (status = 464, description = "user not found"), + (status = 466, description = "db not found"), ) )] pub(crate) async fn add( user: UserId, State(db_pool): State, Json(request): Json, -) -> Result { - let db = db_pool - .find_database_id(&request.database) - .map_err(|mut e| { - e.status = StatusCode::from_u16(461).unwrap(); - e - })?; +) -> ServerResponse { + let db = db_pool.find_database_id(&request.database)?; + let db_user = db_pool.find_user_id(&request.user)?; - if !db_pool.is_db_admin(user.0, db)? { + if !db_pool.is_db_admin(user.0, db)? || db_user == user.0 { return Ok(StatusCode::FORBIDDEN); } - let db_user = db_pool.find_user_id(&request.user).map_err(|mut e| { - e.status = StatusCode::from_u16(462).unwrap(); - e - })?; - - if db_user == user.0 { - return Ok(StatusCode::from_u16(463)?); - } - db_pool.add_database_user(db, db_user, &request.role.to_string())?; Ok(StatusCode::CREATED) diff --git a/agdb_server/src/routes/user.rs b/agdb_server/src/routes/user.rs index 59833430..552a07c8 100644 --- a/agdb_server/src/routes/user.rs +++ b/agdb_server/src/routes/user.rs @@ -1,6 +1,7 @@ use crate::db::DbPool; +use crate::password; use crate::password::Password; -use crate::server_error::ServerError; +use crate::server_error::ServerResponse; use axum::extract::State; use axum::http::StatusCode; use axum::Json; @@ -25,22 +26,16 @@ pub(crate) struct ChangePassword { path = "/api/v1/user/login", request_body = UserCredentials, responses( - (status = 200, description = "Login successful", body = String), - (status = 401, description = "Bad password"), - (status = 403, description = "User not found") + (status = 200, description = "login successful", body = String), + (status = 401, description = "invalid credentials"), + (status = 464, description = "user not found") ) )] pub(crate) async fn login( State(db_pool): State, Json(request): Json, -) -> Result<(StatusCode, String), ServerError> { - let user = db_pool.find_user(&request.name); - - if user.is_err() { - return Ok((StatusCode::FORBIDDEN, String::new())); - } - - let user = user?; +) -> ServerResponse<(StatusCode, String)> { + let user = db_pool.find_user(&request.name)?; let pswd = Password::new(&user.name, &user.password, &user.salt)?; if !pswd.verify_password(&request.password) { @@ -59,35 +54,30 @@ pub(crate) async fn login( security(("Token" = [])), request_body = ChangePassword, responses( - (status = 200, description = "Password changed"), - (status = 401, description = "Invalid password"), - (status = 403, description = "User not found"), - (status = 462, description = "Password too short (<8)"), + (status = 201, description = "password changed"), + (status = 401, description = "invalid credentials"), + (status = 461, description = "password too short (<8)"), + (status = 464, description = "user not found"), ) )] pub(crate) async fn change_password( State(db_pool): State, Json(request): Json, -) -> Result { - if request.new_password.len() < 8 { - return Ok(StatusCode::from_u16(462_u16)?); - } - - let mut user = db_pool - .find_user(&request.name) - .map_err(|_| ServerError::new(StatusCode::FORBIDDEN, "User not found"))?; +) -> ServerResponse { + password::validate_password(&request.new_password)?; + let mut user = db_pool.find_user(&request.name)?; let old_pswd = Password::new(&user.name, &user.password, &user.salt)?; if !old_pswd.verify_password(&request.password) { return Ok(StatusCode::UNAUTHORIZED); } - let pswd = Password::create(&request.name, &request.new_password); - user.password = pswd.password.to_vec(); - user.salt = pswd.user_salt.to_vec(); + let new_pswd = Password::create(&request.name, &request.new_password); + user.password = new_pswd.password.to_vec(); + user.salt = new_pswd.user_salt.to_vec(); db_pool.save_user(user)?; - Ok(StatusCode::OK) + Ok(StatusCode::CREATED) } diff --git a/agdb_server/src/server_error.rs b/agdb_server/src/server_error.rs index 475e19f7..dc692c04 100644 --- a/agdb_server/src/server_error.rs +++ b/agdb_server/src/server_error.rs @@ -9,6 +9,7 @@ pub(crate) struct ServerError { } pub(crate) type ServerResult = Result; +pub(crate) type ServerResponse = Result; impl IntoResponse for ServerError { fn into_response(self) -> Response { diff --git a/agdb_server/src/user_id.rs b/agdb_server/src/user_id.rs index cd2f66fd..e1c2ae03 100644 --- a/agdb_server/src/user_id.rs +++ b/agdb_server/src/user_id.rs @@ -25,10 +25,10 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let bearer: TypedHeader> = - parts.extract().await.map_err(unauthorized_error)?; + parts.extract().await.map_err(unauthorized)?; let id = DbPool::from_ref(state) .find_user_id_by_token(utilities::unquote(bearer.token())) - .map_err(unauthorized_error)?; + .map_err(unauthorized)?; Ok(Self(id)) } } @@ -46,18 +46,18 @@ where let admin_user = Config::from_ref(state).admin.clone(); let admin = DbPool::from_ref(state) .find_user(&admin_user) - .map_err(unauthorized_error)?; + .map_err(unauthorized)?; let bearer: TypedHeader> = - parts.extract().await.map_err(unauthorized_error)?; + parts.extract().await.map_err(unauthorized)?; if admin.token != utilities::unquote(bearer.token()) { - return Err(StatusCode::FORBIDDEN); + return Err(unauthorized(())); } Ok(Self(admin.db_id.unwrap())) } } -fn unauthorized_error(_: E) -> StatusCode { +fn unauthorized(_: E) -> StatusCode { StatusCode::UNAUTHORIZED } diff --git a/agdb_server/tests/admin_db_list_test.rs b/agdb_server/tests/admin_db_list_test.rs new file mode 100644 index 00000000..bda61d62 --- /dev/null +++ b/agdb_server/tests/admin_db_list_test.rs @@ -0,0 +1,52 @@ +pub mod framework; + +use crate::framework::Db; +use crate::framework::TestServer; +use crate::framework::ADMIN_DB_LIST_URI; +use crate::framework::NO_TOKEN; + +#[tokio::test] +async fn db_list() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let user1 = server.init_user("alice", "password123").await?; + let user2 = server.init_user("bob", "password456").await?; + server.init_db("db1", "memory", &user1).await?; + server.init_db("db2", "memory", &user2).await?; + let (status, list) = server + .get::>(ADMIN_DB_LIST_URI, &server.admin_token) + .await?; + assert_eq!(status, 200); + let mut list = list?; + list.sort(); + let expected = vec![ + Db { + name: "db1".to_string(), + db_type: "memory".to_string(), + }, + Db { + name: "db2".to_string(), + db_type: "memory".to_string(), + }, + ]; + assert_eq!(list, expected); + Ok(()) +} + +#[tokio::test] +async fn db_not_found() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let admin = server.init_admin().await?; + let (status, list) = server.get::>(ADMIN_DB_LIST_URI, &admin).await?; + assert_eq!(status, 200); + assert!(list?.is_empty()); + Ok(()) +} + +#[tokio::test] +async fn no_admin_token() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let (status, list) = server.get::>(ADMIN_DB_LIST_URI, NO_TOKEN).await?; + assert_eq!(status, 401); + assert!(list.is_err()); + Ok(()) +} diff --git a/agdb_server/tests/admin_test.rs b/agdb_server/tests/admin_test.rs deleted file mode 100644 index dda9b83d..00000000 --- a/agdb_server/tests/admin_test.rs +++ /dev/null @@ -1,168 +0,0 @@ -pub mod framework; - -use crate::framework::Db; -use crate::framework::TestServer; -use crate::framework::User; -use crate::framework::UserStatus; -use crate::framework::ADMIN_CHANGE_PASSWORD_URI; -use crate::framework::ADMIN_DB_LIST_URI; -use crate::framework::ADMIN_USER_LIST_URI; -use crate::framework::CREATE_USER_URI; -use crate::framework::DB_ADD_URI; -use crate::framework::LOGIN_URI; -use crate::framework::NO_TOKEN; - -#[tokio::test] -async fn create_user() -> anyhow::Result<()> { - let mut server = TestServer::new().await?; - let admin = server.init_admin().await?; - let bad = Some("bad".to_string()); - let user = User { - name: "alice", - password: "password123", - }; - let short_name = User { - name: "a", - password: "password123", - }; - let short_pswd = User { - name: "alice", - password: "pswd", - }; - - assert_eq!(server.post(CREATE_USER_URI, &user, NO_TOKEN).await?.0, 401); //permission denied - assert_eq!(server.post(CREATE_USER_URI, &user, &bad).await?.0, 403); //forbidden - assert_eq!( - server.post(CREATE_USER_URI, &short_name, &admin).await?.0, - 461 - ); //short name - assert_eq!( - server.post(CREATE_USER_URI, &short_pswd, &admin).await?.0, - 462 - ); //short password - assert_eq!(server.post(CREATE_USER_URI, &user, &admin).await?.0, 201); //created - assert_eq!(server.post(CREATE_USER_URI, &user, &admin).await?.0, 463); //user exists - Ok(()) -} - -#[tokio::test] -async fn change_password() -> anyhow::Result<()> { - let mut server = TestServer::new().await?; - let admin = server.init_admin().await?; - let bad = Some("bad".to_string()); - let user = User { - name: "alice", - password: "password123", - }; - let short = User { - name: "alice", - password: "pswd", - }; - let change = User { - name: "alice", - password: "mypassword456", - }; - let new_user = User { - name: "alice", - password: "mypassword456", - }; - let unknown_user = User { - name: "bob", - password: "mypassword456", - }; - - assert_eq!(server.post(CREATE_USER_URI, &user, &admin).await?.0, 201); //created - assert_eq!( - server - .post(ADMIN_CHANGE_PASSWORD_URI, &change, &bad) - .await? - .0, - 403 - ); //forbidden - assert_eq!( - server - .post(ADMIN_CHANGE_PASSWORD_URI, &unknown_user, &admin) - .await? - .0, - 403 - ); //user not found - assert_eq!( - server - .post(ADMIN_CHANGE_PASSWORD_URI, &short, &admin) - .await? - .0, - 462 - ); //short password - assert_eq!( - server - .post(ADMIN_CHANGE_PASSWORD_URI, &change, &admin) - .await? - .0, - 200 - ); //ok - assert_eq!(server.post(LOGIN_URI, &new_user, NO_TOKEN).await?.0, 200); - - Ok(()) -} - -#[tokio::test] -async fn db_list() -> anyhow::Result<()> { - let mut server = TestServer::new().await?; - let token1 = server.init_user("alice", "password123").await?; - let token2 = server.init_user("bob", "password456").await?; - let admin = server.init_admin().await?; - let bad = Some("bad".to_string()); - let db1 = Db { - name: "db1".to_string(), - db_type: "mapped".to_string(), - }; - let db2 = Db { - name: "db2".to_string(), - db_type: "mapped".to_string(), - }; - - assert_eq!(server.get::<()>(ADMIN_DB_LIST_URI, &bad).await?.0, 403); - let (status, list) = server.get::>(ADMIN_DB_LIST_URI, &admin).await?; - assert_eq!(status, 200); - assert!(list?.is_empty()); - - assert_eq!(server.post(DB_ADD_URI, &db1, &token1).await?.0, 201); - assert_eq!(server.post(DB_ADD_URI, &db2, &token2).await?.0, 201); - - let (status, list) = server.get::>(ADMIN_DB_LIST_URI, &admin).await?; - let mut list = list?; - list.sort(); - assert_eq!(status, 200); - assert_eq!(list, vec![db1, db2]); - - Ok(()) -} - -#[tokio::test] -async fn user_list() -> anyhow::Result<()> { - let mut server = TestServer::new().await?; - server.init_user("alice", "password123").await?; - server.init_user("bob", "password456").await?; - let admin = server.init_admin().await?; - let bad = Some("bad".to_string()); - let admin_user = UserStatus { - name: "admin".to_string(), - }; - let user1 = UserStatus { - name: "alice".to_string(), - }; - let user2 = UserStatus { - name: "bob".to_string(), - }; - - assert_eq!(server.get::<()>(ADMIN_USER_LIST_URI, &bad).await?.0, 403); //forbidden - let (status, list) = server - .get::>(ADMIN_USER_LIST_URI, &admin) - .await?; - let mut list = list?; - list.sort(); - assert_eq!(status, 200); //Ok - assert_eq!(list, vec![admin_user, user1, user2]); - - Ok(()) -} diff --git a/agdb_server/tests/admin_user_change_password_test.rs b/agdb_server/tests/admin_user_change_password_test.rs new file mode 100644 index 00000000..0927b260 --- /dev/null +++ b/agdb_server/tests/admin_user_change_password_test.rs @@ -0,0 +1,81 @@ +pub mod framework; + +use crate::framework::TestServer; +use crate::framework::User; +use crate::framework::ADMIN_CHANGE_PASSWORD_URI; +use crate::framework::NO_TOKEN; +use crate::framework::USER_LOGIN_URI; + +#[tokio::test] +async fn change_password() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + server.init_user("alice", "password123").await?; + let user = User { + name: "alice", + password: "password456", + }; + let admin = &server.admin_token; + assert_eq!( + server + .post(ADMIN_CHANGE_PASSWORD_URI, &user, admin) + .await? + .0, + 201 + ); + assert_eq!(server.post(USER_LOGIN_URI, &user, NO_TOKEN).await?.0, 200); + Ok(()) +} + +#[tokio::test] +async fn password_too_short() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + server.init_user("alice", "password123").await?; + let user = User { + name: "alice", + password: "pswd", + }; + let admin = &server.admin_token; + assert_eq!( + server + .post(ADMIN_CHANGE_PASSWORD_URI, &user, admin) + .await? + .0, + 461 + ); + Ok(()) +} + +#[tokio::test] +async fn user_not_found() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let user = User { + name: "alice", + password: "password456", + }; + let admin = server.init_admin().await?; + assert_eq!( + server + .post(ADMIN_CHANGE_PASSWORD_URI, &user, &admin) + .await? + .0, + 464 + ); + Ok(()) +} + +#[tokio::test] +async fn no_admin_token() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = User { + name: "alice", + password: "password456", + }; + assert_eq!( + server + .post(ADMIN_CHANGE_PASSWORD_URI, &user, NO_TOKEN) + .await? + .0, + 401 + ); + Ok(()) +} diff --git a/agdb_server/tests/admin_user_create_test.rs b/agdb_server/tests/admin_user_create_test.rs new file mode 100644 index 00000000..08102ac6 --- /dev/null +++ b/agdb_server/tests/admin_user_create_test.rs @@ -0,0 +1,81 @@ +pub mod framework; + +use crate::framework::TestServer; +use crate::framework::User; +use crate::framework::ADMIN_USER_CREATE_URI; +use crate::framework::NO_TOKEN; + +#[tokio::test] +async fn create_user() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let user = User { + name: "alice", + password: "password123", + }; + let admin = server.init_admin().await?; + assert_eq!( + server.post(ADMIN_USER_CREATE_URI, &user, &admin).await?.0, + 201 + ); + Ok(()) +} + +#[tokio::test] +async fn name_too_short() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let user = User { + name: "a", + password: "password123", + }; + let admin = server.init_admin().await?; + assert_eq!( + server.post(ADMIN_USER_CREATE_URI, &user, &admin).await?.0, + 462 + ); + Ok(()) +} + +#[tokio::test] +async fn password_too_short() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let user = User { + name: "alice", + password: "pswd", + }; + let admin = server.init_admin().await?; + assert_eq!( + server.post(ADMIN_USER_CREATE_URI, &user, &admin).await?.0, + 461 + ); + Ok(()) +} + +#[tokio::test] +async fn user_already_exists() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let user = User { + name: "alice", + password: "password123", + }; + server.init_user(user.name, user.password).await?; + let admin = &server.admin_token; + assert_eq!( + server.post(ADMIN_USER_CREATE_URI, &user, admin).await?.0, + 463 + ); + Ok(()) +} + +#[tokio::test] +async fn no_admin_token() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = User { + name: "alice", + password: "password123", + }; + assert_eq!( + server.post(ADMIN_USER_CREATE_URI, &user, NO_TOKEN).await?.0, + 401 + ); + Ok(()) +} diff --git a/agdb_server/tests/admin_user_list_test.rs b/agdb_server/tests/admin_user_list_test.rs new file mode 100644 index 00000000..ef7f7ca1 --- /dev/null +++ b/agdb_server/tests/admin_user_list_test.rs @@ -0,0 +1,59 @@ +pub mod framework; + +use crate::framework::TestServer; +use crate::framework::UserStatus; +use crate::framework::ADMIN_USER_LIST_URI; +use crate::framework::NO_TOKEN; + +#[tokio::test] +async fn user_list() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + server.init_user("alice", "password123").await?; + server.init_user("bob", "password456").await?; + let admin = &server.admin_token; + let (status, list) = server + .get::>(ADMIN_USER_LIST_URI, admin) + .await?; + assert_eq!(status, 200); + let mut list = list?; + list.sort(); + let expected = vec![ + UserStatus { + name: "admin".to_string(), + }, + UserStatus { + name: "alice".to_string(), + }, + UserStatus { + name: "bob".to_string(), + }, + ]; + assert_eq!(list, expected); + Ok(()) +} + +#[tokio::test] +async fn only_admin() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let admin = server.init_admin().await?; + let (status, list) = server + .get::>(ADMIN_USER_LIST_URI, &admin) + .await?; + assert_eq!(status, 200); + let expected = vec![UserStatus { + name: "admin".to_string(), + }]; + assert_eq!(list?, expected); + Ok(()) +} + +#[tokio::test] +async fn no_admin_token() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let (status, list) = server + .get::>(ADMIN_USER_LIST_URI, NO_TOKEN) + .await?; + assert_eq!(status, 401); + assert!(list.is_err()); + Ok(()) +} diff --git a/agdb_server/tests/db_add_test.rs b/agdb_server/tests/db_add_test.rs new file mode 100644 index 00000000..dbf97b3a --- /dev/null +++ b/agdb_server/tests/db_add_test.rs @@ -0,0 +1,56 @@ +pub mod framework; + +use crate::framework::Db; +use crate::framework::TestServer; +use crate::framework::DB_ADD_URI; +use crate::framework::NO_TOKEN; +use std::path::Path; + +#[tokio::test] +async fn add() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + let db = Db { + name: "mydb".to_string(), + db_type: "file".to_string(), + }; + assert_eq!(server.post(DB_ADD_URI, &db, &token).await?.0, 201); + assert!(Path::new(&server.dir).join(db.name).exists()); + Ok(()) +} + +#[tokio::test] +async fn db_already_exists() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + let db = Db { + name: "mydb".to_string(), + db_type: "memory".to_string(), + }; + assert_eq!(server.post(DB_ADD_URI, &db, &token).await?.0, 201); + assert_eq!(server.post(DB_ADD_URI, &db, &token).await?.0, 465); + Ok(()) +} + +#[tokio::test] +async fn db_invalid() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + let db = Db { + name: "".to_string(), + db_type: "mapped".to_string(), + }; + assert_eq!(server.post(DB_ADD_URI, &db, &token).await?.0, 467); + Ok(()) +} + +#[tokio::test] +async fn no_token() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let db = Db { + name: "mydb".to_string(), + db_type: "mapped".to_string(), + }; + assert_eq!(server.post(DB_ADD_URI, &db, NO_TOKEN).await?.0, 401); + Ok(()) +} diff --git a/agdb_server/tests/db_delete_test.rs b/agdb_server/tests/db_delete_test.rs new file mode 100644 index 00000000..0da026db --- /dev/null +++ b/agdb_server/tests/db_delete_test.rs @@ -0,0 +1,134 @@ +pub mod framework; + +use crate::framework::AddUser; +use crate::framework::Db; +use crate::framework::TestServer; +use crate::framework::DB_DELETE_URI; +use crate::framework::DB_LIST_URI; +use crate::framework::DB_USER_ADD_URI; +use crate::framework::NO_TOKEN; +use serde::Deserialize; +use serde::Serialize; +use std::path::Path; + +#[derive(Serialize, Deserialize)] +struct DeleteDb<'a> { + name: &'a str, +} + +#[tokio::test] +async fn delete() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + server.init_db("my_db", "mapped", &token).await?; + assert!(Path::new(&server.dir).join("my_db").exists()); + let del = DeleteDb { name: "my_db" }; + assert_eq!(server.post(DB_DELETE_URI, &del, &token).await?.0, 204); + let (_, list) = server.get::>(DB_LIST_URI, &token).await?; + assert_eq!(list?, vec![]); + assert!(!Path::new(&server.dir).join("my_db").exists()); + Ok(()) +} + +#[tokio::test] +async fn db_not_found() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + let del = DeleteDb { name: "my_db" }; + assert_eq!(server.post(DB_DELETE_URI, &del, &token).await?.0, 466); + Ok(()) +} + +#[tokio::test] +async fn other_user() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + server.init_db("my_db", "mapped", &token).await?; + let other = server.init_user("bob", "password456").await?; + let del = DeleteDb { name: "my_db" }; + assert_eq!(server.post(DB_DELETE_URI, &del, &other).await?.0, 466); + let (_, list) = server.get::>(DB_LIST_URI, &token).await?; + let expected = vec![Db { + name: "my_db".to_string(), + db_type: "mapped".to_string(), + }]; + assert_eq!(list?, expected); + assert!(Path::new(&server.dir).join("my_db").exists()); + Ok(()) +} + +#[tokio::test] +async fn with_read_role() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + server.init_db("my_db", "mapped", &token).await?; + let reader = server.init_user("bob", "password456").await?; + let role = AddUser { + database: "my_db", + user: "bob", + role: "read", + }; + assert_eq!(server.post(DB_USER_ADD_URI, &role, &token).await?.0, 201); + let del = DeleteDb { name: "my_db" }; + assert_eq!(server.post(DB_DELETE_URI, &del, &reader).await?.0, 403); + let (_, list) = server.get::>(DB_LIST_URI, &token).await?; + let expected = vec![Db { + name: "my_db".to_string(), + db_type: "mapped".to_string(), + }]; + assert_eq!(list?, expected); + assert!(Path::new(&server.dir).join("my_db").exists()); + Ok(()) +} + +#[tokio::test] +async fn with_write_role() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + server.init_db("my_db", "mapped", &token).await?; + let reader = server.init_user("bob", "password456").await?; + let role = AddUser { + database: "my_db", + user: "bob", + role: "write", + }; + assert_eq!(server.post(DB_USER_ADD_URI, &role, &token).await?.0, 201); + let del = DeleteDb { name: "my_db" }; + assert_eq!(server.post(DB_DELETE_URI, &del, &reader).await?.0, 403); + let (_, list) = server.get::>(DB_LIST_URI, &token).await?; + let expected = vec![Db { + name: "my_db".to_string(), + db_type: "mapped".to_string(), + }]; + assert_eq!(list?, expected); + assert!(Path::new(&server.dir).join("my_db").exists()); + Ok(()) +} + +#[tokio::test] +async fn with_admin_role() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + server.init_db("my_db", "memory", &token).await?; + let reader = server.init_user("bob", "password456").await?; + let role = AddUser { + database: "my_db", + user: "bob", + role: "admin", + }; + assert_eq!(server.post(DB_USER_ADD_URI, &role, &token).await?.0, 201); + let del = DeleteDb { name: "my_db" }; + assert_eq!(server.post(DB_DELETE_URI, &del, &reader).await?.0, 204); + let (_, list) = server.get::>(DB_LIST_URI, &token).await?; + assert_eq!(list?, vec![]); + assert!(!Path::new(&server.dir).join("my_db").exists()); + Ok(()) +} + +#[tokio::test] +async fn no_token() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let del = DeleteDb { name: "my_db" }; + assert_eq!(server.post(DB_DELETE_URI, &del, NO_TOKEN).await?.0, 401); + Ok(()) +} diff --git a/agdb_server/tests/db_list_test.rs b/agdb_server/tests/db_list_test.rs new file mode 100644 index 00000000..08f0b0b0 --- /dev/null +++ b/agdb_server/tests/db_list_test.rs @@ -0,0 +1,49 @@ +pub mod framework; + +use crate::framework::Db; +use crate::framework::TestServer; +use crate::framework::DB_LIST_URI; +use crate::framework::NO_TOKEN; + +#[tokio::test] +async fn list() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + server.init_db("my_db", "memory", &token).await?; + server.init_db("my_other_db", "memory", &token).await?; + let expected = vec![ + Db { + name: "my_db".to_string(), + db_type: "memory".to_string(), + }, + Db { + name: "my_other_db".to_string(), + db_type: "memory".to_string(), + }, + ]; + let (status, list) = server.get::>(DB_LIST_URI, &token).await?; + assert_eq!(status, 200); + let mut list = list?; + list.sort(); + assert_eq!(list, expected); + Ok(()) +} + +#[tokio::test] +async fn list_empty() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + let (status, list) = server.get::>(DB_LIST_URI, &token).await?; + assert_eq!(status, 200); + assert_eq!(list?, vec![]); + Ok(()) +} + +#[tokio::test] +async fn list_no_token() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let (status, list) = server.get::>(DB_LIST_URI, NO_TOKEN).await?; + assert_eq!(status, 401); + assert!(list.is_err()); + Ok(()) +} diff --git a/agdb_server/tests/db_remove_test.rs b/agdb_server/tests/db_remove_test.rs new file mode 100644 index 00000000..b05ed910 --- /dev/null +++ b/agdb_server/tests/db_remove_test.rs @@ -0,0 +1,131 @@ +pub mod framework; + +use crate::framework::AddUser; +use crate::framework::Db; +use crate::framework::TestServer; +use crate::framework::DB_LIST_URI; +use crate::framework::DB_REMOVE_URI; +use crate::framework::DB_USER_ADD_URI; +use crate::framework::NO_TOKEN; +use serde::Deserialize; +use serde::Serialize; +use std::path::Path; + +#[derive(Serialize, Deserialize)] +struct RemoveDb<'a> { + name: &'a str, +} + +#[tokio::test] +async fn delete() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + server.init_db("my_db", "mapped", &token).await?; + assert!(Path::new(&server.dir).join("my_db").exists()); + let del = RemoveDb { name: "my_db" }; + assert_eq!(server.post(DB_REMOVE_URI, &del, &token).await?.0, 204); + let (_, list) = server.get::>(DB_LIST_URI, &token).await?; + assert_eq!(list?, vec![]); + assert!(Path::new(&server.dir).join("my_db").exists()); + Ok(()) +} + +#[tokio::test] +async fn db_not_found() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + let del = RemoveDb { name: "my_db" }; + assert_eq!(server.post(DB_REMOVE_URI, &del, &token).await?.0, 466); + Ok(()) +} + +#[tokio::test] +async fn other_user() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + server.init_db("my_db", "mapped", &token).await?; + let other = server.init_user("bob", "password456").await?; + let del = RemoveDb { name: "my_db" }; + assert_eq!(server.post(DB_REMOVE_URI, &del, &other).await?.0, 466); + let (_, list) = server.get::>(DB_LIST_URI, &token).await?; + let expected = vec![Db { + name: "my_db".to_string(), + db_type: "mapped".to_string(), + }]; + assert_eq!(list?, expected); + Ok(()) +} + +#[tokio::test] +async fn with_read_role() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + server.init_db("my_db", "mapped", &token).await?; + let reader = server.init_user("bob", "password456").await?; + let role = AddUser { + database: "my_db", + user: "bob", + role: "read", + }; + assert_eq!(server.post(DB_USER_ADD_URI, &role, &token).await?.0, 201); + let del = RemoveDb { name: "my_db" }; + assert_eq!(server.post(DB_REMOVE_URI, &del, &reader).await?.0, 403); + let (_, list) = server.get::>(DB_LIST_URI, &token).await?; + let expected = vec![Db { + name: "my_db".to_string(), + db_type: "mapped".to_string(), + }]; + assert_eq!(list?, expected); + Ok(()) +} + +#[tokio::test] +async fn with_write_role() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + server.init_db("my_db", "mapped", &token).await?; + let reader = server.init_user("bob", "password456").await?; + let role = AddUser { + database: "my_db", + user: "bob", + role: "write", + }; + assert_eq!(server.post(DB_USER_ADD_URI, &role, &token).await?.0, 201); + let del = RemoveDb { name: "my_db" }; + assert_eq!(server.post(DB_REMOVE_URI, &del, &reader).await?.0, 403); + let (_, list) = server.get::>(DB_LIST_URI, &token).await?; + let expected = vec![Db { + name: "my_db".to_string(), + db_type: "mapped".to_string(), + }]; + assert_eq!(list?, expected); + Ok(()) +} + +#[tokio::test] +async fn with_admin_role() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + server.init_db("my_db", "mapped", &token).await?; + let reader = server.init_user("bob", "password456").await?; + let role = AddUser { + database: "my_db", + user: "bob", + role: "admin", + }; + assert_eq!(server.post(DB_USER_ADD_URI, &role, &token).await?.0, 201); + let del = RemoveDb { name: "my_db" }; + assert_eq!(server.post(DB_REMOVE_URI, &del, &reader).await?.0, 204); + let (_, list) = server.get::>(DB_LIST_URI, &token).await?; + assert_eq!(list?, vec![]); + assert!(Path::new(&server.dir).join("my_db").exists()); + Ok(()) +} + +#[tokio::test] +async fn no_token() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let del = RemoveDb { name: "my_db" }; + assert_eq!(server.post(DB_REMOVE_URI, &del, NO_TOKEN).await?.0, 401); + Ok(()) +} diff --git a/agdb_server/tests/db_test.rs b/agdb_server/tests/db_test.rs deleted file mode 100644 index a7f42961..00000000 --- a/agdb_server/tests/db_test.rs +++ /dev/null @@ -1,268 +0,0 @@ -pub mod framework; - -use crate::framework::Db; -use crate::framework::TestServer; -use crate::framework::DB_ADD_URI; -use crate::framework::DB_DELETE_URI; -use crate::framework::DB_LIST_URI; -use crate::framework::DB_REMOVE_URI; -use crate::framework::DB_USER_ADD_URI; -use serde::Deserialize; -use serde::Serialize; -use std::path::Path; - -#[derive(Serialize, Deserialize)] -struct DeleteDb<'a> { - name: &'a str, -} - -#[derive(Serialize, Deserialize)] -struct AddUser<'a> { - user: &'a str, - database: &'a str, - role: &'a str, -} - -#[tokio::test] -async fn add_database() -> anyhow::Result<()> { - let mut server = TestServer::new().await?; - let token = server.init_user("alice", "mypassword123").await?; - let bad_token = Some("bad".to_string()); - let db1 = Db { - name: "db1".to_string(), - db_type: "memory".to_string(), - }; - let db2 = Db { - name: "db2".to_string(), - db_type: "file".to_string(), - }; - let db3 = Db { - name: "db3".to_string(), - db_type: "mapped".to_string(), - }; - let bad_name = Db { - name: "".to_string(), - db_type: "mapped".to_string(), - }; - - assert_eq!(server.post(DB_ADD_URI, &db1, &bad_token).await?.0, 401); //unauthorized - assert_eq!(server.post(DB_ADD_URI, &db1, &token).await?.0, 201); //created - assert_eq!(server.post(DB_ADD_URI, &db2, &token).await?.0, 201); //created - assert_eq!(server.post(DB_ADD_URI, &db3, &token).await?.0, 201); //created - assert_eq!(server.post(DB_ADD_URI, &db1, &token).await?.0, 403); //database exists - assert_eq!(server.post(DB_ADD_URI, &bad_name, &token).await?.0, 461); //invalid db type - - Ok(()) -} - -#[tokio::test] -async fn add_user() -> anyhow::Result<()> { - let mut server = TestServer::new().await?; - let token = server.init_user("alice", "mypassword123").await?; - let token2: Option = server.init_user("bob", "mypassword456").await?; - server.init_user("chad", "mypassword789").await?; - let bad_token = Some("bad".to_string()); - let db = Db { - name: "db1".to_string(), - db_type: "mapped".to_string(), - }; - let add_read = AddUser { - database: "db1", - user: "bob", - role: "read", - }; - let add_write = AddUser { - database: "db1", - user: "bob", - role: "write", - }; - let add_chad = AddUser { - database: "db1", - user: "chad", - role: "read", - }; - let add_admin = AddUser { - database: "db1", - user: "bob", - role: "admin", - }; - let add_self = AddUser { - database: "db1", - user: "alice", - role: "read", - }; - let no_user = AddUser { - database: "db1", - user: "cat", - role: "read", - }; - let no_db = AddUser { - database: "db_missing", - user: "bob", - role: "read", - }; - assert_eq!(server.post(DB_ADD_URI, &db, &token).await?.0, 201); //created - let (status, list) = server.get::>(DB_LIST_URI, &token2).await?; - assert_eq!(status, 200); //Ok - let list = list?; - assert_eq!(list, vec![]); - assert_eq!( - server.post(DB_USER_ADD_URI, &add_read, &bad_token).await?.0, - 401 - ); //forbidden - assert_eq!(server.post(DB_USER_ADD_URI, &no_db, &token).await?.0, 461); //missing db - assert_eq!(server.post(DB_USER_ADD_URI, &no_user, &token).await?.0, 462); //missing user - assert_eq!( - server.post(DB_USER_ADD_URI, &add_self, &token).await?.0, - 463 - ); //self - assert_eq!( - server.post(DB_USER_ADD_URI, &add_read, &token).await?.0, - 201 - ); //created - assert_eq!( - server.post(DB_USER_ADD_URI, &add_chad, &token2).await?.0, - 403 - ); //not an admin - assert_eq!( - server.post(DB_USER_ADD_URI, &add_write, &token).await?.0, - 201 - ); //created - assert_eq!( - server.post(DB_USER_ADD_URI, &add_chad, &token2).await?.0, - 403 - ); //not an admin - assert_eq!( - server.post(DB_USER_ADD_URI, &add_admin, &token).await?.0, - 201 - ); //created - let (status, list) = server.get::>(DB_LIST_URI, &token2).await?; - let list = list?; - assert_eq!(status, 200); //Ok - assert_eq!(list, vec![db]); - assert_eq!( - server.post(DB_USER_ADD_URI, &add_chad, &token2).await?.0, - 201 - ); //created - - Ok(()) -} - -#[tokio::test] -async fn delete() -> anyhow::Result<()> { - let mut server = TestServer::new().await?; - let token = server.init_user("alice", "mypassword123").await?; - let bad_token = Some("bad".to_string()); - let db1 = Db { - name: "db1".to_string(), - db_type: "mapped".to_string(), - }; - let db2 = Db { - name: "db2".to_string(), - db_type: "file".to_string(), - }; - let db3 = Db { - name: "db1".to_string(), - db_type: "memory".to_string(), - }; - let del1 = DeleteDb { name: "db1" }; - let del2 = DeleteDb { name: "db2" }; - - assert_eq!(server.post(DB_DELETE_URI, &del1, &bad_token).await?.0, 401); //unauthorized - assert_eq!(server.post(DB_ADD_URI, &db1, &token).await?.0, 201); //created - assert_eq!(server.post(DB_ADD_URI, &db2, &token).await?.0, 201); //created - - assert!(Path::new(&server.dir).join(del1.name).exists()); - assert!(Path::new(&server.dir).join(del2.name).exists()); - - assert_eq!(server.post(DB_DELETE_URI, &del1, &token).await?.0, 200); - - assert!(!Path::new(&server.dir).join(del1.name).exists()); - assert!(Path::new(&server.dir).join(del2.name).exists()); - - assert_eq!(server.post(DB_DELETE_URI, &del1, &token).await?.0, 403); //cannot delete (already deleted) - assert_eq!(server.post(DB_ADD_URI, &db3, &token).await?.0, 201); //created - assert_eq!(server.post(DB_DELETE_URI, &del1, &token).await?.0, 200); //ok - - let (status, list) = server.get::>(DB_LIST_URI, &token).await?; - let mut list = list?; - list.sort(); - assert_eq!(status, 200); //ok - assert_eq!(list, vec![db2.clone()]); - - let token2 = server.init_user("bob", "mypassword456").await?; - assert_eq!(server.post(DB_DELETE_URI, &db2, &token2).await?.0, 403); //forbidden - - Ok(()) -} - -#[tokio::test] -async fn list() -> anyhow::Result<()> { - let mut server = TestServer::new().await?; - let token = server.init_user("alice", "mypassword123").await?; - let bad = Some("bad".to_string()); - let db1 = Db { - name: "db1".to_string(), - db_type: "memory".to_string(), - }; - let db2 = Db { - name: "db2".to_string(), - db_type: "memory".to_string(), - }; - - assert_eq!(server.get::<()>(DB_LIST_URI, &bad).await?.0, 401); //unauthorized - let (status, list) = server.get::>(DB_LIST_URI, &token).await?; - assert_eq!(status, 200); //Ok - assert_eq!(list?, vec![]); - assert_eq!(server.post(DB_ADD_URI, &db1, &token).await?.0, 201); //created - assert_eq!(server.post(DB_ADD_URI, &db2, &token).await?.0, 201); //created - let (status, list) = server.get::>(DB_LIST_URI, &token).await?; - let mut list = list?; - list.sort(); - assert_eq!(status, 200); //Ok - assert_eq!(list, vec![db1, db2]); - - Ok(()) -} - -#[tokio::test] -async fn remove() -> anyhow::Result<()> { - let mut server = TestServer::new().await?; - let token = server.init_user("alice", "mypassword123").await?; - let bad_token = Some("bad".to_string()); - let db1 = Db { - name: "db1".to_string(), - db_type: "mapped".to_string(), - }; - let db2 = Db { - name: "db2".to_string(), - db_type: "file".to_string(), - }; - let del1 = DeleteDb { name: "db1" }; - let del2 = DeleteDb { name: "db2" }; - - assert_eq!(server.post(DB_REMOVE_URI, &del1, &bad_token).await?.0, 401); //unauthorized - assert_eq!(server.post(DB_ADD_URI, &db1, &token).await?.0, 201); //created - assert_eq!(server.post(DB_ADD_URI, &db2, &token).await?.0, 201); //created - - assert!(Path::new(&server.dir).join(del1.name).exists()); - assert!(Path::new(&server.dir).join(del2.name).exists()); - - assert_eq!(server.post(DB_REMOVE_URI, &del1, &token).await?.0, 200); - - assert!(Path::new(&server.dir).join(del1.name).exists()); - assert!(Path::new(&server.dir).join(del2.name).exists()); - - assert_eq!(server.post(DB_REMOVE_URI, &del1, &token).await?.0, 403); //cannot delete (already deleted) - - let (status, list) = server.get::>(DB_LIST_URI, &token).await?; - let mut list = list?; - list.sort(); - assert_eq!(status, 200); //ok - assert_eq!(list, vec![db2.clone()]); - - let token2 = server.init_user("bob", "mypassword456").await?; - assert_eq!(server.post(DB_REMOVE_URI, &db2, &token2).await?.0, 403); //forbidden - - Ok(()) -} diff --git a/agdb_server/tests/db_user_add_test.rs b/agdb_server/tests/db_user_add_test.rs new file mode 100644 index 00000000..778907c0 --- /dev/null +++ b/agdb_server/tests/db_user_add_test.rs @@ -0,0 +1,150 @@ +pub mod framework; + +use crate::framework::AddUser; +use crate::framework::Db; +use crate::framework::TestServer; +use crate::framework::DB_LIST_URI; +use crate::framework::DB_USER_ADD_URI; +use crate::framework::NO_TOKEN; + +#[tokio::test] +async fn add_reader() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + let reader = server.init_user("bob", "password465").await?; + server.init_db("my_db", "memory", &token).await?; + let role = AddUser { + database: "my_db", + user: "bob", + role: "read", + }; + let (_, list) = server.get::>(DB_LIST_URI, &reader).await?; + assert_eq!(list?, vec![]); + assert_eq!(server.post(DB_USER_ADD_URI, &role, &token).await?.0, 201); + let (_, list) = server.get::>(DB_LIST_URI, &reader).await?; + let expected = vec![Db { + name: "my_db".to_string(), + db_type: "memory".to_string(), + }]; + assert_eq!(list?, expected); + Ok(()) +} + +#[tokio::test] +async fn add_writer() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + let reader = server.init_user("bob", "password465").await?; + server.init_db("my_db", "memory", &token).await?; + let role = AddUser { + database: "my_db", + user: "bob", + role: "write", + }; + let (_, list) = server.get::>(DB_LIST_URI, &reader).await?; + assert_eq!(list?, vec![]); + assert_eq!(server.post(DB_USER_ADD_URI, &role, &token).await?.0, 201); + let (_, list) = server.get::>(DB_LIST_URI, &reader).await?; + let expected = vec![Db { + name: "my_db".to_string(), + db_type: "memory".to_string(), + }]; + assert_eq!(list?, expected); + Ok(()) +} + +#[tokio::test] +async fn add_admin() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + let reader = server.init_user("bob", "password465").await?; + server.init_db("my_db", "memory", &token).await?; + let role = AddUser { + database: "my_db", + user: "bob", + role: "admin", + }; + let (_, list) = server.get::>(DB_LIST_URI, &reader).await?; + assert_eq!(list?, vec![]); + assert_eq!(server.post(DB_USER_ADD_URI, &role, &token).await?.0, 201); + let (_, list) = server.get::>(DB_LIST_URI, &reader).await?; + let expected = vec![Db { + name: "my_db".to_string(), + db_type: "memory".to_string(), + }]; + assert_eq!(list?, expected); + Ok(()) +} + +#[tokio::test] +async fn add_self() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + server.init_db("my_db", "memory", &token).await?; + let role = AddUser { + database: "my_db", + user: "alice", + role: "read", + }; + assert_eq!(server.post(DB_USER_ADD_URI, &role, &token).await?.0, 403); + Ok(()) +} + +#[tokio::test] +async fn add_admin_as_non_admin() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + let writer = server.init_user("bob", "password465").await?; + let other: Option = server.init_user("chad", "password789").await?; + server.init_db("my_db", "memory", &token).await?; + let role = AddUser { + database: "my_db", + user: "chad", + role: "write", + }; + let (_, list) = server.get::>(DB_LIST_URI, &other).await?; + assert_eq!(list?, vec![]); + assert_eq!(server.post(DB_USER_ADD_URI, &role, &writer).await?.0, 403); + let (_, list) = server.get::>(DB_LIST_URI, &other).await?; + assert_eq!(list?, vec![]); + Ok(()) +} + +#[tokio::test] +async fn db_not_found() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + let role = AddUser { + database: "my_db", + user: "bob", + role: "read", + }; + assert_eq!(server.post(DB_USER_ADD_URI, &role, &token).await?.0, 466); + Ok(()) +} + +#[tokio::test] +async fn user_not_found() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + let token = server.init_user("alice", "password123").await?; + server.init_db("my_db", "memory", &token).await?; + let role = AddUser { + database: "my_db", + user: "bob", + role: "read", + }; + assert_eq!(server.post(DB_USER_ADD_URI, &role, &token).await?.0, 464); + Ok(()) +} + +#[tokio::test] +async fn no_token() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let role = AddUser { + database: "my_db", + user: "bob", + role: "admin", + }; + assert_eq!(server.post(DB_USER_ADD_URI, &role, NO_TOKEN).await?.0, 401); + Ok(()) +} diff --git a/agdb_server/tests/framework/mod.rs b/agdb_server/tests/framework/mod.rs index ca4a69ce..c7931322 100644 --- a/agdb_server/tests/framework/mod.rs +++ b/agdb_server/tests/framework/mod.rs @@ -11,8 +11,8 @@ use std::process::Command; use std::sync::atomic::AtomicU16; use std::time::Duration; -pub const CHANGE_PASSWORD_URI: &str = "/user/change_password"; -pub const CREATE_USER_URI: &str = "/admin/user/create"; +pub const USER_CHANGE_PASSWORD_URI: &str = "/user/change_password"; +pub const ADMIN_USER_CREATE_URI: &str = "/admin/user/create"; pub const DB_ADD_URI: &str = "/db/add"; pub const DB_USER_ADD_URI: &str = "/db/user/add"; pub const DB_DELETE_URI: &str = "/db/delete"; @@ -21,7 +21,7 @@ pub const ADMIN_DB_LIST_URI: &str = "/admin/db/list"; pub const ADMIN_USER_LIST_URI: &str = "/admin/user/list"; pub const ADMIN_CHANGE_PASSWORD_URI: &str = "/admin/user/change_password"; pub const DB_REMOVE_URI: &str = "/db/remove"; -pub const LOGIN_URI: &str = "/user/login"; +pub const USER_LOGIN_URI: &str = "/user/login"; pub const SHUTDOWN_URI: &str = "/admin/shutdown"; pub const STATUS_URI: &str = "/status"; @@ -36,6 +36,13 @@ const RETRY_ATTEMPS: u16 = 3; pub const NO_TOKEN: &Option = &None; static PORT: AtomicU16 = AtomicU16::new(DEFAULT_PORT); +#[derive(Serialize, Deserialize)] +pub struct AddUser<'a> { + pub user: &'a str, + pub database: &'a str, + pub role: &'a str, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct Db { pub name: String, @@ -141,10 +148,11 @@ impl TestServer { } pub async fn init_admin(&mut self) -> anyhow::Result> { - let mut admin = HashMap::<&str, &str>::new(); - admin.insert("name", &self.admin); - admin.insert("password", &self.admin_password); - let response = self.post(LOGIN_URI, &admin, &None).await?; + let admin = User { + name: &self.admin, + password: &self.admin_password, + }; + let response = self.post(USER_LOGIN_URI, &admin, &None).await?; assert_eq!(response.0, 200); self.admin_token = Some(response.1); Ok(self.admin_token.clone()) @@ -155,23 +163,36 @@ impl TestServer { name: &str, password: &str, ) -> anyhow::Result> { - let mut user = HashMap::<&str, &str>::new(); - user.insert("name", name); - user.insert("password", password); + let user = User { name, password }; if self.admin_token.is_none() { self.init_admin().await?; } assert_eq!( - self.post(CREATE_USER_URI, &user, &self.admin_token.clone()) + self.post(ADMIN_USER_CREATE_URI, &user, &self.admin_token.clone()) .await? .0, 201 ); - let response = self.post(LOGIN_URI, &user, &None).await?; + let response = self.post(USER_LOGIN_URI, &user, &None).await?; assert_eq!(response.0, 200); Ok(Some(response.1)) } + pub async fn init_db( + &self, + name: &str, + db_type: &str, + user_token: &Option, + ) -> anyhow::Result<()> { + let db = Db { + name: name.to_string(), + db_type: db_type.to_string(), + }; + let (status, _) = self.post(DB_ADD_URI, &db, user_token).await?; + assert_eq!(status, 201); + Ok(()) + } + pub async fn post( &self, uri: &str, @@ -210,7 +231,11 @@ impl TestServer { t } else { reqwest::blocking::Client::new() - .post(format!("{}:{}/api/v1{LOGIN_URI}", Self::url_base(), port)) + .post(format!( + "{}:{}/api/v1{USER_LOGIN_URI}", + Self::url_base(), + port + )) .json(&admin) .send()? .text()? @@ -227,7 +252,7 @@ impl TestServer { .send()? .status() .as_u16(), - 200 + 204 ); Ok(()) diff --git a/agdb_server/tests/server_test.rs b/agdb_server/tests/server_test.rs index 7ba059ae..70ac8f99 100644 --- a/agdb_server/tests/server_test.rs +++ b/agdb_server/tests/server_test.rs @@ -8,24 +8,50 @@ use assert_cmd::cargo::CommandCargoExt; use std::process::Command; #[tokio::test] -async fn db_reuse_and_error() -> anyhow::Result<()> { - let mut server = TestServer::new().await?; - assert_eq!(server.get::<()>(STATUS_URI, NO_TOKEN).await?.0, 200); +async fn error() -> anyhow::Result<()> { + let server = TestServer::new().await?; assert_eq!(server.get::<()>("/test_error", NO_TOKEN).await?.0, 500); + Ok(()) +} + +#[tokio::test] +async fn missing() -> anyhow::Result<()> { + let server = TestServer::new().await?; assert_eq!(server.get::<()>("/missing", NO_TOKEN).await?.0, 404); + Ok(()) +} + +#[tokio::test] +async fn status() -> anyhow::Result<()> { + let server = TestServer::new().await?; + assert_eq!(server.get::<()>(STATUS_URI, NO_TOKEN).await?.0, 200); + Ok(()) +} + +#[tokio::test] +async fn shutdown_no_token() -> anyhow::Result<()> { + let server = TestServer::new().await?; assert_eq!(server.get::<()>(SHUTDOWN_URI, NO_TOKEN).await?.0, 401); + Ok(()) +} - let bad = Some("bad".to_string()); - assert_eq!(server.get::<()>(SHUTDOWN_URI, &bad).await?.0, 403); +#[tokio::test] +async fn shutdown_bad_token() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let token = Some("bad".to_string()); + assert_eq!(server.get::<()>(SHUTDOWN_URI, &token).await?.0, 401); + Ok(()) +} +#[tokio::test] +async fn db_reuse() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; let token = server.init_admin().await?; - assert_eq!(server.get::<()>(SHUTDOWN_URI, &token).await?.0, 200); + assert_eq!(server.get::<()>(SHUTDOWN_URI, &token).await?.0, 204); assert!(server.process.wait()?.success()); - server.process = Command::cargo_bin("agdb_server")? .current_dir(&server.dir) .spawn()?; - Ok(()) } diff --git a/agdb_server/tests/user_change_password_test.rs b/agdb_server/tests/user_change_password_test.rs new file mode 100644 index 00000000..f3f15af2 --- /dev/null +++ b/agdb_server/tests/user_change_password_test.rs @@ -0,0 +1,92 @@ +pub mod framework; + +use crate::framework::ChangePassword; +use crate::framework::TestServer; +use crate::framework::User; +use crate::framework::NO_TOKEN; +use crate::framework::USER_CHANGE_PASSWORD_URI; +use crate::framework::USER_LOGIN_URI; + +#[tokio::test] +async fn change_password() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + server.init_user("alice", "password123").await?; + let change = ChangePassword { + name: "alice", + password: "password123", + new_password: "password456", + }; + let user = User { + name: "alice", + password: "password456", + }; + assert_eq!( + server + .post(USER_CHANGE_PASSWORD_URI, &change, NO_TOKEN) + .await? + .0, + 201 + ); + assert_eq!(server.post(USER_LOGIN_URI, &user, NO_TOKEN).await?.0, 200); + + Ok(()) +} + +#[tokio::test] +async fn invalid_credentials() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + server.init_user("alice", "password123").await?; + let change = ChangePassword { + name: "alice", + password: "bad_password", + new_password: "password456", + }; + assert_eq!( + server + .post(USER_CHANGE_PASSWORD_URI, &change, NO_TOKEN) + .await? + .0, + 401 + ); + + Ok(()) +} + +#[tokio::test] +async fn password_too_short() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + server.init_user("alice", "password123").await?; + let change = ChangePassword { + name: "alice", + password: "password123", + new_password: "pswd", + }; + assert_eq!( + server + .post(USER_CHANGE_PASSWORD_URI, &change, NO_TOKEN) + .await? + .0, + 461 + ); + + Ok(()) +} + +#[tokio::test] +async fn user_not_found() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let change = ChangePassword { + name: "alice", + password: "password123", + new_password: "password456", + }; + assert_eq!( + server + .post(USER_CHANGE_PASSWORD_URI, &change, NO_TOKEN) + .await? + .0, + 464 + ); + + Ok(()) +} diff --git a/agdb_server/tests/user_login_test.rs b/agdb_server/tests/user_login_test.rs new file mode 100644 index 00000000..8eed8dbc --- /dev/null +++ b/agdb_server/tests/user_login_test.rs @@ -0,0 +1,51 @@ +pub mod framework; + +use crate::framework::TestServer; +use crate::framework::User; +use crate::framework::NO_TOKEN; +use crate::framework::USER_LOGIN_URI; + +#[tokio::test] +async fn login() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + server.init_user("alice", "password123").await?; + let user = User { + name: "alice", + password: "password123", + }; + let (status, token) = server.post(USER_LOGIN_URI, &user, NO_TOKEN).await?; + assert_eq!(status, 200); + assert!(!token.is_empty()); + + Ok(()) +} + +#[tokio::test] +async fn invalid_credentials() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + server.init_user("alice", "password123").await?; + let user = User { + name: "alice", + password: "password456", + }; + let (status, token) = server.post(USER_LOGIN_URI, &user, NO_TOKEN).await?; + assert_eq!(status, 401); + assert!(token.is_empty()); + + Ok(()) +} + +#[tokio::test] +async fn user_not_found() -> anyhow::Result<()> { + let mut server = TestServer::new().await?; + server.init_user("alice", "password123").await?; + let user = User { + name: "alice", + password: "password456", + }; + let (status, token) = server.post(USER_LOGIN_URI, &user, NO_TOKEN).await?; + assert_eq!(status, 401); + assert!(token.is_empty()); + + Ok(()) +} diff --git a/agdb_server/tests/user_test.rs b/agdb_server/tests/user_test.rs deleted file mode 100644 index 009106f1..00000000 --- a/agdb_server/tests/user_test.rs +++ /dev/null @@ -1,76 +0,0 @@ -pub mod framework; - -use crate::framework::ChangePassword; -use crate::framework::TestServer; -use crate::framework::User; -use crate::framework::CHANGE_PASSWORD_URI; -use crate::framework::CREATE_USER_URI; -use crate::framework::LOGIN_URI; -use crate::framework::NO_TOKEN; - -#[tokio::test] -async fn chnage_password() -> anyhow::Result<()> { - let mut server = TestServer::new().await?; - let admin = server.init_admin().await?; - let bad = Some("bad".to_string()); - let user = User { - name: "alice", - password: "mypassword123", - }; - let new_user = User { - name: "alice", - password: "mypassword456", - }; - let change = ChangePassword { - name: "alice", - password: "mypassword123", - new_password: "mypassword456", - }; - let short = ChangePassword { - name: "alice", - password: "mypassword123", - new_password: "pswd", //too short - }; - - assert_eq!( - server.post(CHANGE_PASSWORD_URI, &short, NO_TOKEN).await?.0, - 462 - ); //password too short - assert_eq!( - server.post(CHANGE_PASSWORD_URI, &change, &bad).await?.0, - 403 - ); //fordbidden - assert_eq!(server.post(CREATE_USER_URI, &user, &admin).await?.0, 201); //user created - assert_eq!( - server.post(CHANGE_PASSWORD_URI, &change, NO_TOKEN).await?.0, - 200 - ); //ok - assert_eq!( - server.post(CHANGE_PASSWORD_URI, &change, NO_TOKEN).await?.0, - 401 - ); //invalid password - assert_eq!(server.post(LOGIN_URI, &user, NO_TOKEN).await?.0, 401); //invalid credentials - assert_eq!(server.post(LOGIN_URI, &new_user, NO_TOKEN).await?.0, 200); //ok - Ok(()) -} - -#[tokio::test] -async fn login() -> anyhow::Result<()> { - let mut server = TestServer::new().await?; - let admin = server.init_admin().await?; - let user = User { - name: "alice", - password: "mypassword123", - }; - let bad_pswd = User { - name: "alice", - password: "mypassword456", - }; - assert_eq!(server.post(LOGIN_URI, &user, NO_TOKEN).await?.0, 403); //unknown user - assert_eq!(server.post(CREATE_USER_URI, &user, &admin).await?.0, 201); //created - assert_eq!(server.post(LOGIN_URI, &bad_pswd, NO_TOKEN).await?.0, 401); //bad password - let (status, token) = server.post("/user/login", &user, NO_TOKEN).await?; - assert_eq!(status, 200); //user/login succeeded - assert!(!token.is_empty()); - Ok(()) -}