diff options
| author | Joel Klinghed <the_jk@spawned.biz> | 2025-01-03 01:28:54 +0100 |
|---|---|---|
| committer | Joel Klinghed <the_jk@spawned.biz> | 2025-01-03 01:28:54 +0100 |
| commit | 7494db93b9262c3d8330fd11631e711a1642b8fc (patch) | |
| tree | 565534ddaa990a861c0ef8a9439f7656fce7f132 /server/src/tests.rs | |
| parent | 4b1f7fec1cf9d427234ff5bded79a6d18d5c88ce (diff) | |
Add initital tests
Also add /users endpoint.
Diffstat (limited to 'server/src/tests.rs')
| -rw-r--r-- | server/src/tests.rs | 367 |
1 files changed, 367 insertions, 0 deletions
diff --git a/server/src/tests.rs b/server/src/tests.rs new file mode 100644 index 0000000..b6476a0 --- /dev/null +++ b/server/src/tests.rs @@ -0,0 +1,367 @@ +use rocket::figment::util::map; +use rocket::figment::value::{Map, Value}; +use rocket::http::{ContentType, Header, Status}; +use rocket::local::asynchronous::{Client, LocalRequest}; +use sqlx::mysql::{MySql, MySqlConnectOptions, MySqlPoolOptions}; +use sqlx::{Acquire, Executor, Pool}; +use std::sync::OnceLock; +use stdext::function_name; + +use crate::api_model; + +struct RealIP(&'static str); + +impl From<&RealIP> for Header<'static> { + fn from(ip: &RealIP) -> Header<'static> { + Header::new("X-Real-IP", ip.0) + } +} + +static FAKE_IP: RealIP = RealIP("127.0.1.10"); +static ANOTHER_FAKE_IP: RealIP = RealIP("192.168.0.1"); + +static MASTER_POOL: OnceLock<Pool<MySql>> = OnceLock::new(); + +fn find_password(url: &'_ str) -> Option<&'_ str> { + let protocol = url.find("://"); + if protocol.is_none() { + return None; + } + let specific = &url[protocol.unwrap() + 3..]; + let at = specific.find('@'); + if at.is_none() { + return None; + } + let auth = &specific[0..at.unwrap()]; + let colon = auth.find(':'); + if colon.is_none() { + return None; + } + return Some(&auth[colon.unwrap() + 1..]); +} + +fn make_db_name_safe(name: &str) -> String { + let mut ret = String::new(); + for c in name.chars() { + if c >= 'a' && c <= 'z' { + ret.push(c); + } else if c >= '0' && c <= '9' { + ret.push(c); + } else { + ret.push('_'); + } + } + return ret; +} + +async fn async_client_with_private_database(test_name: String) -> Client { + let base_figment = rocket::Config::figment(); + + let base_url_value = base_figment + .find_value("databases.eyeballs.url") + .expect("database_url"); + let base_url = base_url_value.as_str().expect("database_url as string"); + let base_options: MySqlConnectOptions = base_url.parse().expect("valid database_url"); + + let maybe_password = find_password(base_url); + + let database = + make_db_name_safe(&format!("_{}", test_name.trim_end_matches("::{{closure}}"))[..]); + + // Cannot get sqlx::test (0.7.4) to work with MySQL, always errors out + // with connection (already?) closed when closing the setup connection. + // So doing our own lazier setup where each test gets a db based on + // their name. + { + let mut pool_conn = MASTER_POOL + .get_or_init(|| { + let options: MySqlConnectOptions = + base_url_value.as_str().unwrap().parse().unwrap(); + + MySqlPoolOptions::new() + .max_connections(20) + .after_release(|_conn, _| Box::pin(async move { Ok(false) })) + .connect_lazy_with(options) + }) + .acquire() + .await + .unwrap(); + + let conn = pool_conn.acquire().await.unwrap(); + conn.execute(&format!("DROP DATABASE IF EXISTS {database}")[..]) + .await + .unwrap(); + conn.execute(&format!("CREATE DATABASE {database}")[..]) + .await + .unwrap(); + } + + let db_url = format!( + "mysql://{}{}@{}:{}/{}", + base_options.get_username(), + if let Some(password) = maybe_password { + format!(":{}", password) + } else { + "".to_string() + }, + base_options.get_host(), + base_options.get_port(), + database, + ); + + let db_config: Map<_, Value> = map! { + "url" => db_url.into(), + }; + + let figment = base_figment.merge(("databases", map!["eyeballs" => db_config])); + + Client::tracked(crate::rocket_from_config(figment)) + .await + .expect("valid rocket instance") +} + +async fn get_status_from<'a>(request: LocalRequest<'a>) -> api_model::StatusResponse { + request + .header(&FAKE_IP) + .dispatch() + .await + .into_json::<api_model::StatusResponse>() + .await + .unwrap() +} + +async fn get_status<'a>(client: &Client) -> api_model::StatusResponse { + get_status_from(client.get("/api/v1/status")).await +} + +async fn login(client: &Client) { + let login = get_status_from( + client + .post("/api/v1/login") + .body("username=user&password=password") + .header(ContentType::Form), + ) + .await; + assert_eq!(login.ok, true); +} + +async fn get_projects<'a>(client: &Client) -> api_model::Projects { + client + .get("/api/v1/projects") + .header(&FAKE_IP) + .dispatch() + .await + .into_json::<api_model::Projects>() + .await + .unwrap() +} + +async fn get_project_from<'a>(request: LocalRequest<'a>) -> api_model::Project { + request + .header(&FAKE_IP) + .dispatch() + .await + .into_json::<api_model::Project>() + .await + .unwrap() +} + +async fn get_users<'a>(client: &Client) -> api_model::Users { + client + .get("/api/v1/users") + .header(&FAKE_IP) + .dispatch() + .await + .into_json::<api_model::Users>() + .await + .unwrap() +} + +async fn new_project(client: &Client) -> api_model::Project { + get_project_from( + client + .post("/api/v1/project/new") + .json(&api_model::ProjectData { + title: Some("foo"), + description: Some("bar"), + }), + ) + .await +} + +#[rocket::async_test] +async fn test_not_logged_in_status() { + let client = async_client_with_private_database(function_name!().to_string()).await; + let not_logged_in = get_status(&client).await; + assert_eq!(not_logged_in.ok, false); +} + +#[rocket::async_test] +async fn test_login_status() { + let client = async_client_with_private_database(function_name!().to_string()).await; + + login(&client).await; + + let logged_in = get_status(&client).await; + assert_eq!(logged_in.ok, true); +} + +#[rocket::async_test] +async fn test_bad_login_status() { + let client = async_client_with_private_database(function_name!().to_string()).await; + + let bad_password = client + .post("/api/v1/login") + .body("username=user&password=incorrect") + .header(ContentType::Form) + .header(&FAKE_IP) + .dispatch() + .await; + assert_eq!(bad_password.status(), Status::Unauthorized); + + let bad_username = client + .post("/api/v1/login") + .body("username=incorrect&password=password") + .header(ContentType::Form) + .header(&FAKE_IP) + .dispatch() + .await; + assert_eq!(bad_username.status(), Status::Unauthorized); +} + +#[rocket::async_test] +async fn test_change_ip() { + let client = async_client_with_private_database(function_name!().to_string()).await; + + login(&client).await; + + let new_ip = client + .get("/api/v1/status") + .header(&ANOTHER_FAKE_IP) + .dispatch() + .await; + assert_eq!(new_ip.status(), Status::Unauthorized); +} + +#[rocket::async_test] +async fn test_logout() { + let client = async_client_with_private_database(function_name!().to_string()).await; + + login(&client).await; + + let logged_in = get_status(&client).await; + assert_eq!(logged_in.ok, true); + + let logout = get_status_from(client.get("/api/v1/logout")).await; + assert_eq!(logout.ok, true); + + let not_logged_in = get_status(&client).await; + assert_eq!(not_logged_in.ok, false); +} + +#[rocket::async_test] +async fn test_projects_empty() { + let client = async_client_with_private_database(function_name!().to_string()).await; + + login(&client).await; + + let projects = get_projects(&client).await; + assert_eq!(projects.total_count, 0); + assert_eq!(projects.more, false); + assert_eq!(projects.projects.len(), 0); +} + +#[rocket::async_test] +async fn test_project_new() { + let client = async_client_with_private_database(function_name!().to_string()).await; + + login(&client).await; + + let project = new_project(&client).await; + + assert_eq!(project.title, "foo"); + assert_eq!(project.description, "bar"); + assert_eq!(project.users.len(), 1); + let user = project.users.get(0).unwrap(); + assert_eq!(user.user.username, "user"); + assert_eq!(user.default_role, api_model::UserReviewRole::Reviewer); + assert_eq!(user.maintainer, true); + + let projects = get_projects(&client).await; + assert_eq!(projects.total_count, 1); + assert_eq!(projects.more, false); + assert_eq!(projects.projects.len(), 1); + let project_entry = projects.projects.get(0).unwrap(); + assert_eq!(project_entry.id, project.id); + assert_eq!(project_entry.title, project.title); + + let project2 = get_project_from(client.get(format!("/api/v1/project/{}", project.id))).await; + assert_eq!(project, project2); +} + +#[rocket::async_test] +async fn test_project_update() { + let client = async_client_with_private_database(function_name!().to_string()).await; + + login(&client).await; + + let project = get_project_from(client.post("/api/v1/project/new").json( + &api_model::ProjectData { + title: Some("foo"), + description: None, + }, + )) + .await; + + let project_url = format!("/api/v1/project/{}", project.id); + + let update = client + .post(project_url.clone()) + .json(&api_model::ProjectData { + title: None, + description: Some("bar"), + }) + .header(&FAKE_IP) + .dispatch() + .await; + assert_eq!(update.status(), Status::Ok); + + let updated_project = get_project_from(client.get(project_url)).await; + assert_eq!(updated_project.title, project.title); + assert_eq!(updated_project.description, "bar"); +} + +#[rocket::async_test] +async fn test_project_new_user() { + let client = async_client_with_private_database(function_name!().to_string()).await; + + login(&client).await; + + let project = new_project(&client).await; + let project_url = format!("/api/v1/project/{}", project.id); + + let users = get_users(&client).await; + let other = users.users.iter().find(|u| u.username == "other").unwrap(); + + let new = client + .post(format!("{project_url}/user/new?userid={}", other.id)) + .json(&api_model::ProjectUserEntryData { + default_role: Some(api_model::UserReviewRole::Watcher), + maintainer: Some(true), + }) + .header(&FAKE_IP) + .dispatch() + .await; + assert_eq!(new.status(), Status::Ok); + + let updated_project = get_project_from(client.get(project_url)).await; + assert_eq!(updated_project.users.len(), 2); + let other_entry = updated_project + .users + .iter() + .find(|ue| ue.user.id == other.id) + .unwrap(); + assert_eq!(other_entry.user, *other); + assert_eq!(other_entry.default_role, api_model::UserReviewRole::Watcher); + assert_eq!(other_entry.maintainer, true); +} |
