From b8364a15dbc5972dd1fbf567171ca08d3efc648f Mon Sep 17 00:00:00 2001 From: Michael Vlach Date: Sat, 2 Dec 2023 10:09:45 +0100 Subject: [PATCH] [server] Add admin user #813 (#822) add admin user --- agdb_server/openapi/schema.json | 27 +++- agdb_server/src/api.rs | 2 +- agdb_server/src/app.rs | 5 +- agdb_server/src/config.rs | 2 + agdb_server/src/db.rs | 41 ++++-- agdb_server/src/db/server_db_storage.rs | 6 +- agdb_server/src/main.rs | 4 +- agdb_server/src/routes.rs | 10 ++ agdb_server/src/routes/admin.rs | 12 +- agdb_server/src/routes/user.rs | 14 +-- agdb_server/src/server_state.rs | 8 ++ agdb_server/src/user_id.rs | 32 ++++- agdb_server/tests/db_test.rs | 8 +- agdb_server/tests/framework/mod.rs | 160 +++++++++++++++++------- agdb_server/tests/server_test.rs | 19 ++- agdb_server/tests/user_test.rs | 10 +- 16 files changed, 265 insertions(+), 95 deletions(-) diff --git a/agdb_server/openapi/schema.json b/agdb_server/openapi/schema.json index 26827b32c..4c5c696ec 100644 --- a/agdb_server/openapi/schema.json +++ b/agdb_server/openapi/schema.json @@ -19,7 +19,12 @@ "200": { "description": "Server is shutting down" } - } + }, + "security": [ + { + "Token": [] + } + ] } }, "/api/v1/db/add": { @@ -146,6 +151,19 @@ ] } }, + "/api/v1/status": { + "get": { + "tags": [ + "crate::routes" + ], + "operationId": "status", + "responses": { + "200": { + "description": "Server is ready" + } + } + } + }, "/api/v1/user/change_password": { "post": { "tags": [ @@ -235,9 +253,9 @@ "200": { "description": "Login successful", "content": { - "application/json": { + "text/plain": { "schema": { - "$ref": "#/components/schemas/UserToken" + "type": "string" } } } @@ -321,9 +339,6 @@ "type": "string" } } - }, - "UserToken": { - "type": "string" } }, "securitySchemes": { diff --git a/agdb_server/src/api.rs b/agdb_server/src/api.rs index b91f927bc..1064a5ab2 100644 --- a/agdb_server/src/api.rs +++ b/agdb_server/src/api.rs @@ -7,6 +7,7 @@ use utoipa::OpenApi; #[derive(OpenApi)] #[openapi( paths( + crate::routes::status, crate::routes::admin::shutdown, crate::routes::user::create, crate::routes::user::login, @@ -19,7 +20,6 @@ use utoipa::OpenApi; components(schemas( crate::routes::user::ChangePassword, crate::routes::user::UserCredentials, - crate::routes::user::UserToken, crate::routes::db::DbType, crate::routes::db::ServerDatabase, crate::routes::db::ServerDatabaseName, diff --git a/agdb_server/src/app.rs b/agdb_server/src/app.rs index 913e00431..781e0f3c0 100644 --- a/agdb_server/src/app.rs +++ b/agdb_server/src/app.rs @@ -1,4 +1,5 @@ use crate::api::Api; +use crate::config::Config; use crate::db::DbPool; use crate::logger; use crate::routes; @@ -10,9 +11,10 @@ use tokio::sync::broadcast::Sender; use utoipa::OpenApi; use utoipa_rapidoc::RapiDoc; -pub(crate) fn app(shutdown_sender: Sender<()>, db_pool: DbPool) -> Router { +pub(crate) fn app(config: Config, shutdown_sender: Sender<()>, db_pool: DbPool) -> Router { let state = ServerState { db_pool, + config, shutdown_sender, }; @@ -40,6 +42,7 @@ pub(crate) fn app(shutdown_sender: Sender<()>, db_pool: DbPool) -> Router { "/v1", Router::new() .route("/test_error", routing::get(routes::test_error)) + .route("/status", routing::get(routes::status)) .nest("/admin", admin_router_v1) .nest("/user", user_router_v1) .nest("/db", db_router_v1), diff --git a/agdb_server/src/config.rs b/agdb_server/src/config.rs index 5de316081..fc2562257 100644 --- a/agdb_server/src/config.rs +++ b/agdb_server/src/config.rs @@ -11,6 +11,7 @@ const CONFIG_FILE: &str = "agdb_server.yaml"; pub(crate) struct ConfigImpl { pub(crate) host: String, pub(crate) port: u16, + pub(crate) admin: String, } pub(crate) fn new() -> ServerResult { @@ -21,6 +22,7 @@ pub(crate) fn new() -> ServerResult { let config = ConfigImpl { host: "127.0.0.1".to_string(), port: 3000, + admin: "admin".to_string(), }; std::fs::write(CONFIG_FILE, serde_yaml::to_string(&config)?)?; diff --git a/agdb_server/src/db.rs b/agdb_server/src/db.rs index c89928bcd..82cc08b08 100644 --- a/agdb_server/src/db.rs +++ b/agdb_server/src/db.rs @@ -1,7 +1,9 @@ mod server_db; mod server_db_storage; +use crate::config::Config; use crate::db::server_db::ServerDb; +use crate::password::Password; use crate::server_error::ServerResult; use agdb::DbId; use agdb::QueryBuilder; @@ -41,7 +43,7 @@ pub(crate) struct DbPoolImpl { pub(crate) struct DbPool(pub(crate) Arc); impl DbPool { - pub(crate) fn new() -> ServerResult { + pub(crate) fn new(config: &Config) -> ServerResult { let db_exists = Path::new("agdb_server.agdb").exists(); let db_pool = Self(Arc::new(DbPoolImpl { @@ -50,12 +52,37 @@ impl DbPool { })); if !db_exists { - db_pool.0.server_db.get_mut()?.exec_mut( - &QueryBuilder::insert() - .nodes() - .aliases(vec!["users", "dbs"]) - .query(), - )?; + let admin_password = Password::create(&config.admin, &config.admin); + + db_pool.0.server_db.get_mut()?.transaction_mut(|t| { + t.exec_mut( + &QueryBuilder::insert() + .nodes() + .aliases(vec!["users", "dbs"]) + .query(), + )?; + + let admin = t.exec_mut( + &QueryBuilder::insert() + .nodes() + .values(&DbUser { + db_id: None, + name: config.admin.clone(), + password: admin_password.password.to_vec(), + salt: admin_password.user_salt.to_vec(), + token: String::new(), + }) + .query(), + )?; + + t.exec_mut( + &QueryBuilder::insert() + .edges() + .from("users") + .to(admin) + .query(), + ) + })?; } Ok(db_pool) diff --git a/agdb_server/src/db/server_db_storage.rs b/agdb_server/src/db/server_db_storage.rs index 92aa29d97..7a3ec685f 100644 --- a/agdb_server/src/db/server_db_storage.rs +++ b/agdb_server/src/db/server_db_storage.rs @@ -126,7 +126,7 @@ mod tests { let _test_file_dot = TestFile::new(".file_storage.agdb"); let test_file2 = TestFile::new("file_storage_backup.agdb"); let mut storage = ServerDbStorage::new(&format!("file:{}", test_file.0))?; - println!("{:?}", storage); + format!("{:?}", storage); storage.backup(&test_file2.0)?; assert!(std::path::Path::new(&test_file2.0).exists()); storage.flush()?; @@ -145,7 +145,7 @@ mod tests { let _test_file_dot = TestFile::new(".mapped_storage.agdb"); let test_file2 = TestFile::new("mapped_storage_backup.agdb"); let mut storage = ServerDbStorage::new(&format!("mapped:{}", test_file.0))?; - println!("{:?}", storage); + format!("{:?}", storage); storage.backup(&test_file2.0)?; assert!(std::path::Path::new(&test_file2.0).exists()); storage.flush()?; @@ -161,7 +161,7 @@ mod tests { #[test] fn memory_storage() -> anyhow::Result<()> { let mut storage = ServerDbStorage::new("memory:db_test.agdb")?; - println!("{:?}", storage); + format!("{:?}", storage); storage.backup("backup_test")?; storage.flush()?; assert!(storage.is_empty()); diff --git a/agdb_server/src/main.rs b/agdb_server/src/main.rs index bd0b73e6e..2f7d00e20 100644 --- a/agdb_server/src/main.rs +++ b/agdb_server/src/main.rs @@ -21,9 +21,9 @@ async fn main() -> ServerResult { let (shutdown_sender, mut shutdown_receiver) = tokio::sync::broadcast::channel::<()>(1); let config = config::new()?; - let db_pool = DbPool::new()?; - let app = app::app(shutdown_sender, db_pool); + let db_pool = DbPool::new(&config)?; let address = format!("{}:{}", config.host, config.port); + let app = app::app(config, shutdown_sender, db_pool); tracing::info!("Listening at {address}"); let listener = tokio::net::TcpListener::bind(address).await?; diff --git a/agdb_server/src/routes.rs b/agdb_server/src/routes.rs index 023959db2..cdba67a85 100644 --- a/agdb_server/src/routes.rs +++ b/agdb_server/src/routes.rs @@ -4,6 +4,16 @@ pub(crate) mod admin; pub(crate) mod db; pub(crate) mod user; +#[utoipa::path(get, + path = "/api/v1/status", + responses( + (status = 200, description = "Server is ready"), + ) +)] +pub(crate) async fn status() -> StatusCode { + StatusCode::OK +} + pub(crate) async fn test_error() -> StatusCode { StatusCode::INTERNAL_SERVER_ERROR } diff --git a/agdb_server/src/routes/admin.rs b/agdb_server/src/routes/admin.rs index cdcdbbfe2..e9346c696 100644 --- a/agdb_server/src/routes/admin.rs +++ b/agdb_server/src/routes/admin.rs @@ -1,14 +1,19 @@ +use crate::user_id::AdminId; use axum::extract::State; use axum::http::StatusCode; use tokio::sync::broadcast::Sender; #[utoipa::path(get, path = "/api/v1/admin/shutdown", + security(("Token" = [])), responses( (status = 200, description = "Server is shutting down"), ) )] -pub(crate) async fn shutdown(State(shutdown_sender): State>) -> StatusCode { +pub(crate) async fn shutdown( + _admin_id: AdminId, + State(shutdown_sender): State>, +) -> StatusCode { match shutdown_sender.send(()) { Ok(_) => StatusCode::OK, Err(_) => StatusCode::INTERNAL_SERVER_ERROR, @@ -18,12 +23,13 @@ pub(crate) async fn shutdown(State(shutdown_sender): State>) -> Statu #[cfg(test)] mod tests { use super::*; + use agdb::DbId; #[tokio::test] async fn shutdown_test() -> anyhow::Result<()> { let (shutdown_sender, _shutdown_receiver) = tokio::sync::broadcast::channel::<()>(1); - let status = shutdown(State(shutdown_sender)).await; + let status = shutdown(AdminId(DbId(0)), State(shutdown_sender)).await; assert_eq!(status, StatusCode::OK); Ok(()) @@ -33,7 +39,7 @@ mod tests { async fn bad_shutdown() -> anyhow::Result<()> { let shutdown_sender = Sender::<()>::new(1); - let status = shutdown(State(shutdown_sender)).await; + let status = shutdown(AdminId(DbId(0)), State(shutdown_sender)).await; assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR); Ok(()) diff --git a/agdb_server/src/routes/user.rs b/agdb_server/src/routes/user.rs index 4161d8b71..3ad246226 100644 --- a/agdb_server/src/routes/user.rs +++ b/agdb_server/src/routes/user.rs @@ -6,7 +6,6 @@ use axum::extract::State; use axum::http::StatusCode; use axum::Json; use serde::Deserialize; -use serde::Serialize; use utoipa::ToSchema; use uuid::Uuid; @@ -16,9 +15,6 @@ pub(crate) struct UserCredentials { pub(crate) password: String, } -#[derive(Default, Serialize, ToSchema)] -pub(crate) struct UserToken(String); - #[derive(Deserialize, ToSchema)] pub(crate) struct ChangePassword { pub(crate) name: String, @@ -30,7 +26,7 @@ pub(crate) struct ChangePassword { path = "/api/v1/user/login", request_body = UserCredentials, responses( - (status = 200, description = "Login successful", body = UserToken), + (status = 200, description = "Login successful", body = String), (status = 401, description = "Bad password"), (status = 403, description = "User not found") ) @@ -38,25 +34,25 @@ pub(crate) struct ChangePassword { pub(crate) async fn login( State(db_pool): State, Json(request): Json, -) -> Result<(StatusCode, Json), ServerError> { +) -> Result<(StatusCode, String), ServerError> { let user = db_pool.find_user(&request.name); if user.is_err() { - return Ok((StatusCode::FORBIDDEN, Json::default())); + return Ok((StatusCode::FORBIDDEN, String::new())); } let user = user?; let pswd = Password::new(&user.name, &user.password, &user.salt)?; if !pswd.verify_password(&request.password) { - return Ok((StatusCode::UNAUTHORIZED, Json::default())); + return Ok((StatusCode::UNAUTHORIZED, String::new())); } let token_uuid = Uuid::new_v4(); let token = token_uuid.to_string(); db_pool.save_token(user.db_id.unwrap(), &token)?; - Ok((StatusCode::OK, Json(UserToken(token)))) + Ok((StatusCode::OK, token)) } #[utoipa::path(post, diff --git a/agdb_server/src/server_state.rs b/agdb_server/src/server_state.rs index 60ec9efcd..89a0249b4 100644 --- a/agdb_server/src/server_state.rs +++ b/agdb_server/src/server_state.rs @@ -1,3 +1,4 @@ +use crate::config::Config; use crate::db::DbPool; use axum::extract::FromRef; use tokio::sync::broadcast::Sender; @@ -5,6 +6,7 @@ use tokio::sync::broadcast::Sender; #[derive(Clone)] pub(crate) struct ServerState { pub(crate) db_pool: DbPool, + pub(crate) config: Config, pub(crate) shutdown_sender: Sender<()>, } @@ -19,3 +21,9 @@ impl FromRef for Sender<()> { input.shutdown_sender.clone() } } + +impl FromRef for Config { + fn from_ref(input: &ServerState) -> Self { + input.config.clone() + } +} diff --git a/agdb_server/src/user_id.rs b/agdb_server/src/user_id.rs index d344e6c55..0b1e545e5 100644 --- a/agdb_server/src/user_id.rs +++ b/agdb_server/src/user_id.rs @@ -1,3 +1,4 @@ +use crate::config::Config; use crate::db::DbPool; use crate::utilities; use agdb::DbId; @@ -12,6 +13,8 @@ use axum_extra::TypedHeader; pub(crate) struct UserId(pub(crate) DbId); +pub(crate) struct AdminId(pub(crate) DbId); + #[axum::async_trait] impl FromRequestParts for UserId where @@ -20,16 +23,41 @@ where { type Rejection = StatusCode; - async fn from_request_parts(parts: &mut Parts, db_pool: &S) -> Result { + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let bearer: TypedHeader> = parts.extract().await.map_err(unauthorized_error)?; - let id = DbPool::from_ref(db_pool) + let id = DbPool::from_ref(state) .find_user_id(utilities::unquote(bearer.token())) .map_err(unauthorized_error)?; Ok(Self(id)) } } +#[axum::async_trait] +impl FromRequestParts for AdminId +where + S: Send + Sync, + DbPool: FromRef, + Config: FromRef, +{ + type Rejection = StatusCode; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let admin_user = Config::from_ref(state).admin.clone(); + let admin = DbPool::from_ref(state) + .find_user(&admin_user) + .map_err(unauthorized_error)?; + let bearer: TypedHeader> = + parts.extract().await.map_err(unauthorized_error)?; + + if admin.token != utilities::unquote(bearer.token()) { + return Err(StatusCode::FORBIDDEN); + } + + Ok(Self(admin.db_id.unwrap())) + } +} + fn unauthorized_error(_: E) -> StatusCode { StatusCode::UNAUTHORIZED } diff --git a/agdb_server/tests/db_test.rs b/agdb_server/tests/db_test.rs index d810a5b0c..a2b876186 100644 --- a/agdb_server/tests/db_test.rs +++ b/agdb_server/tests/db_test.rs @@ -6,7 +6,7 @@ use std::path::Path; #[tokio::test] async fn add_database() -> anyhow::Result<()> { - let server = TestServer::new()?; + let server = TestServer::new().await?; let mut user = HashMap::new(); user.insert("name", "alice"); user.insert("password", "mypassword123"); @@ -36,7 +36,7 @@ async fn add_database() -> anyhow::Result<()> { #[tokio::test] async fn delete() -> anyhow::Result<()> { - let server = TestServer::new()?; + let server = TestServer::new().await?; let mut user = HashMap::new(); user.insert("name", "alice"); user.insert("password", "mypassword123"); @@ -112,7 +112,7 @@ async fn delete() -> anyhow::Result<()> { #[tokio::test] async fn list() -> anyhow::Result<()> { - let server = TestServer::new()?; + let server = TestServer::new().await?; let mut user = HashMap::new(); user.insert("name", "alice"); user.insert("password", "mypassword123"); @@ -162,7 +162,7 @@ async fn list() -> anyhow::Result<()> { #[tokio::test] async fn remove() -> anyhow::Result<()> { - let server = TestServer::new()?; + let server = TestServer::new().await?; let mut user = HashMap::new(); user.insert("name", "alice"); user.insert("password", "mypassword123"); diff --git a/agdb_server/tests/framework/mod.rs b/agdb_server/tests/framework/mod.rs index 7c2c2ae29..7ef2c7497 100644 --- a/agdb_server/tests/framework/mod.rs +++ b/agdb_server/tests/framework/mod.rs @@ -1,15 +1,21 @@ +use anyhow::anyhow; use assert_cmd::prelude::*; use reqwest::Client; use std::collections::HashMap; -use std::panic::Location; use std::path::Path; use std::process::Child; use std::process::Command; use std::sync::atomic::AtomicU16; +use std::time::Duration; +const BINARY: &str = "agdb_server"; const CONFIG_FILE: &str = "agdb_server.yaml"; -const HOST: &str = "http://127.0.0.1"; +const PROTOCOL: &str = "http"; +const HOST: &str = "127.0.0.1"; const DEFAULT_PORT: u16 = 3000; +const ADMIN: &str = "admin"; +const RETRY_TIMEOUT: Duration = Duration::from_secs(1); +const RETRY_ATTEMPS: u16 = 3; static PORT: AtomicU16 = AtomicU16::new(DEFAULT_PORT); pub struct TestServer { @@ -17,33 +23,24 @@ pub struct TestServer { pub port: u16, pub process: Child, pub client: Client, + pub admin: String, + pub admin_password: String, } impl TestServer { - #[track_caller] - pub fn new() -> anyhow::Result { - let caller = Location::caller(); - let dir = format!( - "{}.{}.{}.test", - Path::new(caller.file()) - .file_name() - .unwrap() - .to_str() - .unwrap(), - caller.line(), - caller.column() - ); - + pub async fn new() -> anyhow::Result { let port = PORT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let dir = format!("{BINARY}.{port}.test"); - Self::remove_dir_if_exists(&dir); + Self::remove_dir_if_exists(&dir)?; std::fs::create_dir(&dir)?; - let mut config = HashMap::<&str, serde_yaml::Value>::new(); - config.insert("host", "127.0.0.1".into()); - config.insert("port", port.into()); - if port != DEFAULT_PORT { + let mut config = HashMap::<&str, serde_yaml::Value>::new(); + config.insert("host", HOST.into()); + config.insert("port", port.into()); + config.insert("admin", ADMIN.into()); + let file = std::fs::File::options() .create_new(true) .write(true) @@ -51,18 +48,35 @@ impl TestServer { serde_yaml::to_writer(file, &config)?; } - let process = Command::cargo_bin("agdb_server")? - .current_dir(&dir) - .spawn()?; - + let process = Command::cargo_bin(BINARY)?.current_dir(&dir).spawn()?; let client = reqwest::Client::new(); - - Ok(Self { + let server = Self { dir, port, process, client, - }) + admin: ADMIN.to_string(), + admin_password: ADMIN.to_string(), + }; + + let mut error = anyhow!("Failed to start server"); + + for _ in 0..RETRY_ATTEMPS { + match server + .client + .get(format!("{}:{}/api/v1/status", Self::url_base(), port)) + .send() + .await + { + Ok(_) => return Ok(server), + Err(e) => { + error = e.into(); + } + } + std::thread::sleep(RETRY_TIMEOUT); + } + + Err(error) } pub async fn get(&self, uri: &str) -> anyhow::Result { @@ -75,6 +89,17 @@ impl TestServer { .as_u16()) } + pub async fn get_auth(&self, uri: &str, token: &str) -> anyhow::Result { + Ok(self + .client + .get(self.url(uri)) + .bearer_auth(token) + .send() + .await? + .status() + .as_u16()) + } + pub async fn get_auth_response(&self, uri: &str, token: &str) -> anyhow::Result<(u16, String)> { let response = self .client @@ -83,7 +108,7 @@ impl TestServer { .send() .await?; let status = response.status().as_u16(); - let response_content = String::from_utf8(response.bytes().await?.to_vec())?; + let response_content = response.text().await?; Ok((status, response_content)) } @@ -123,38 +148,77 @@ impl TestServer { ) -> anyhow::Result<(u16, String)> { let response = self.client.post(self.url(uri)).json(&json).send().await?; let status = response.status().as_u16(); - let response_content = String::from_utf8(response.bytes().await?.to_vec())?; + let response_content = response.text().await?; Ok((status, response_content)) } - fn remove_dir_if_exists(dir: &str) { + fn remove_dir_if_exists(dir: &str) -> anyhow::Result<()> { if Path::new(dir).exists() { - std::fs::remove_dir_all(dir).unwrap(); + std::fs::remove_dir_all(dir)?; + } + + Ok(()) + } + + fn shutdown_server(&mut self) -> anyhow::Result<()> { + if self.process.try_wait()?.is_none() { + let port = self.port; + let mut admin = HashMap::<&str, String>::new(); + admin.insert("name", self.admin.clone()); + admin.insert("password", self.admin_password.clone()); + + std::thread::spawn(move || -> anyhow::Result<()> { + let admin_token = reqwest::blocking::Client::new() + .post(format!("{}:{}/api/v1/user/login", Self::url_base(), port)) + .json(&admin) + .send()? + .text()?; + + assert_eq!( + reqwest::blocking::Client::new() + .get(format!( + "{}:{}/api/v1/admin/shutdown", + Self::url_base(), + port + )) + .bearer_auth(admin_token) + .send()? + .status() + .as_u16(), + 200 + ); + + Ok(()) + }) + .join() + .map_err(|e| anyhow!("{:?}", e))??; } + + assert!(self.process.wait()?.success()); + + Ok(()) } fn url(&self, uri: &str) -> String { - format!("{HOST}:{}/api/v1{uri}", self.port) + format!("{}:{}/api/v1{uri}", Self::url_base(), self.port) + } + + fn url_base() -> String { + format!("{PROTOCOL}://{HOST}") } } impl Drop for TestServer { fn drop(&mut self) { - let port = self.port; - std::thread::spawn(move || { - assert_eq!( - reqwest::blocking::get(format!("{HOST}:{}/api/v1/admin/shutdown", port)) - .unwrap() - .status() - .as_u16(), - 200 - ); - }) - .join() - .unwrap(); - - assert!(self.process.wait().unwrap().success()); - Self::remove_dir_if_exists(&self.dir); + let shutdown_result = Self::shutdown_server(self); + + if shutdown_result.is_err() { + let _ = self.process.kill(); + let _ = self.process.wait(); + } + + shutdown_result.unwrap(); + Self::remove_dir_if_exists(&self.dir).unwrap(); } } diff --git a/agdb_server/tests/server_test.rs b/agdb_server/tests/server_test.rs index 721cecf79..39dc3c090 100644 --- a/agdb_server/tests/server_test.rs +++ b/agdb_server/tests/server_test.rs @@ -2,24 +2,35 @@ pub mod framework; use crate::framework::TestServer; use assert_cmd::cargo::CommandCargoExt; +use std::collections::HashMap; use std::process::Command; #[tokio::test] async fn db_reuse_and_error() -> anyhow::Result<()> { - let mut server = TestServer::new()?; + let mut server = TestServer::new().await?; assert_eq!(server.get("/test_error").await?, 500); assert_eq!(server.get("/missing").await?, 404); - assert_eq!(server.get("/admin/shutdown").await?, 200); - assert!(server.process.wait().unwrap().success()); + assert_eq!(server.get("/admin/shutdown").await?, 401); + assert_eq!(server.get_auth("/admin/shutdown", "bad_token").await?, 403); + + let mut admin = HashMap::<&str, &str>::new(); + admin.insert("name", &server.admin); + admin.insert("password", &server.admin_password); + let token = server.post_response("/user/login", &admin).await?.1; + + assert_eq!(server.get_auth("/admin/shutdown", &token).await?, 200); + assert!(server.process.wait()?.success()); + server.process = Command::cargo_bin("agdb_server")? .current_dir(&server.dir) .spawn()?; + Ok(()) } #[tokio::test] async fn openapi() -> anyhow::Result<()> { - let server = TestServer::new()?; + let server = TestServer::new().await?; assert_eq!(server.get("").await?, 200); assert_eq!(server.get("/openapi.json").await?, 200); Ok(()) diff --git a/agdb_server/tests/user_test.rs b/agdb_server/tests/user_test.rs index ddc29ead7..a7538207f 100644 --- a/agdb_server/tests/user_test.rs +++ b/agdb_server/tests/user_test.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; #[tokio::test] async fn chnage_password() -> anyhow::Result<()> { - let server = TestServer::new()?; + let server = TestServer::new().await?; let mut user = HashMap::new(); user.insert("name", "alice"); user.insert("password", "mypassword123"); @@ -19,7 +19,7 @@ async fn chnage_password() -> anyhow::Result<()> { assert_eq!(server.post("/user/create", &user).await?, 201); //created let (status, token) = server.post_response("/user/login", &user).await?; assert_eq!(status, 200); //user/login succeeded - assert_eq!(token.len(), 38); + assert!(!token.is_empty()); assert_eq!(server.post("/user/change_password", &change).await?, 200); //ok assert_eq!(server.post("/user/change_password", &change).await?, 401); //invalid password assert_eq!(server.post("/user/login", &user).await?, 401); //invalid credentials @@ -30,7 +30,7 @@ async fn chnage_password() -> anyhow::Result<()> { #[tokio::test] async fn create() -> anyhow::Result<()> { - let server = TestServer::new()?; + let server = TestServer::new().await?; let mut user = HashMap::new(); user.insert("name", "a"); user.insert("password", ""); @@ -45,7 +45,7 @@ async fn create() -> anyhow::Result<()> { #[tokio::test] async fn login() -> anyhow::Result<()> { - let server = TestServer::new()?; + let server = TestServer::new().await?; let mut user = HashMap::new(); user.insert("name", "alice"); user.insert("password", "mypassword123"); @@ -56,6 +56,6 @@ async fn login() -> anyhow::Result<()> { user.insert("password", "mypassword123"); let (status, token) = server.post_response("/user/login", &user).await?; assert_eq!(status, 200); //user/login succeeded - assert_eq!(token.len(), 38); + assert!(!token.is_empty()); Ok(()) }