summaryrefslogtreecommitdiff
path: root/server/src/tests.rs
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2025-01-03 01:28:54 +0100
committerJoel Klinghed <the_jk@spawned.biz>2025-01-03 01:28:54 +0100
commit7494db93b9262c3d8330fd11631e711a1642b8fc (patch)
tree565534ddaa990a861c0ef8a9439f7656fce7f132 /server/src/tests.rs
parent4b1f7fec1cf9d427234ff5bded79a6d18d5c88ce (diff)
Add initital tests
Also add /users endpoint.
Diffstat (limited to 'server/src/tests.rs')
-rw-r--r--server/src/tests.rs367
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);
+}