diff --git a/.spellcheck.dic b/.spellcheck.dic index 345c2901..556ade0f 100644 --- a/.spellcheck.dic +++ b/.spellcheck.dic @@ -74,3 +74,4 @@ user_budget_modify {server_state_id} {user_budget_id} select_project_budgets_by_project_from_db +_not_found diff --git a/.sqlx/query-2e92552d8fe6f1dfaf20660711219ec67218992bf5bfd72deefe69f778aa608c.json b/.sqlx/query-2e92552d8fe6f1dfaf20660711219ec67218992bf5bfd72deefe69f778aa608c.json new file mode 100644 index 00000000..a9b6dddf --- /dev/null +++ b/.sqlx/query-2e92552d8fe6f1dfaf20660711219ec67218992bf5bfd72deefe69f778aa608c.json @@ -0,0 +1,114 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT\n s.id as id,\n s.begin as begin,\n s.end as end,\n ss.instance_id as instance_id,\n ss.instance_name as instance_name,\n f.id as flavor,\n f.name as flavor_name,\n ss.status as status,\n u.id as user,\n u.name as username\n FROM\n accounting_state as s,\n accounting_serverstate as ss,\n resources_flavor as f,\n user_user as u\n WHERE\n ss.flavor_id = f.id AND\n ss.user_id = u.id AND\n ss.state_ptr_id = s.id AND\n ss.instance_id = ? AND\n u.id = ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 1, + "name": "begin", + "type_info": { + "type": "Datetime", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "max_size": 26 + } + }, + { + "ordinal": 2, + "name": "end", + "type_info": { + "type": "Datetime", + "flags": "BINARY", + "max_size": 26 + } + }, + { + "ordinal": 3, + "name": "instance_id", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 144 + } + }, + { + "ordinal": 4, + "name": "instance_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 1020 + } + }, + { + "ordinal": 5, + "name": "flavor", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 20 + } + }, + { + "ordinal": 6, + "name": "flavor_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 256 + } + }, + { + "ordinal": 7, + "name": "status", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 72 + } + }, + { + "ordinal": 8, + "name": "user", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 9, + "name": "username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 1020 + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "2e92552d8fe6f1dfaf20660711219ec67218992bf5bfd72deefe69f778aa608c" +} diff --git a/.sqlx/query-a072acb2bdd46dc575f5696d388e117a374a58c355b1864b775ba4931466cb9e.json b/.sqlx/query-a072acb2bdd46dc575f5696d388e117a374a58c355b1864b775ba4931466cb9e.json new file mode 100644 index 00000000..5f774fe4 --- /dev/null +++ b/.sqlx/query-a072acb2bdd46dc575f5696d388e117a374a58c355b1864b775ba4931466cb9e.json @@ -0,0 +1,114 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT\n s.id as id,\n s.begin as begin,\n s.end as end,\n ss.instance_id as instance_id,\n ss.instance_name as instance_name,\n f.id as flavor,\n f.name as flavor_name,\n ss.status as status,\n u.id as user,\n u.name as username\n FROM\n accounting_state as s,\n accounting_serverstate as ss,\n resources_flavor as f,\n user_user as u\n WHERE\n ss.flavor_id = f.id AND\n ss.user_id = u.id AND\n ss.state_ptr_id = s.id AND\n ss.instance_id = ? AND\n u.project_id = ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 1, + "name": "begin", + "type_info": { + "type": "Datetime", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "max_size": 26 + } + }, + { + "ordinal": 2, + "name": "end", + "type_info": { + "type": "Datetime", + "flags": "BINARY", + "max_size": 26 + } + }, + { + "ordinal": 3, + "name": "instance_id", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 144 + } + }, + { + "ordinal": 4, + "name": "instance_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 1020 + } + }, + { + "ordinal": 5, + "name": "flavor", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 20 + } + }, + { + "ordinal": 6, + "name": "flavor_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 256 + } + }, + { + "ordinal": 7, + "name": "status", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 72 + } + }, + { + "ordinal": 8, + "name": "user", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 9, + "name": "username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 1020 + } + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "a072acb2bdd46dc575f5696d388e117a374a58c355b1864b775ba4931466cb9e" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index c73bebba..7b393b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,83 @@ This is the combined changelog of all contained `lrzcc` crates. ## [Unreleased] -... +### lrzcc-api + +#### Dependencies +- run cargo update + +#### Database +- move insert_flavor_into_db to database module +- move insert_server_state_into_db to database module +- add missing sqlx try_froms to ServerStateRow id fields +- adjust all getters for new NotFoundErrors +- add select_server_states_by_server_and_project_from_db +- add select_server_states_by_server_and_user_from_db + +#### Error +- match messages for all NotFoundError variants +- add NotFoundOnlyError with impls + +#### Endpoints +- revise getters for new NotFoundErrors +- remove done todo comment +- use require_master_user_or_return_not_found in user_get +- correct authorization check in server_state_get +- homogenize errors of server_state_list +- complete server_state_list endpoint + +#### Authorization +- add require_*_or_return_not_found functions + +### lrzcc-test + +#### Dependencies +- add chrono dependency +- add anyhow dependency +- run cargo update + +#### Tests +- add TestApp.setup_test_flavor +- add server state create tests +- reuse api::database::insert_flavor_into_db in test +- add TestApp.setup_test_server_state +- add server state delete tests +- add helper assert_equal_server_states +- use assert_equal_server_states in server state create tests +- add server_state_get tests +- fix some linting issues in server_state_get test +- adjust tests for new NotFoundErrors +- fix minor linting issues +- correct expected not found message in server_state_delete test +- use assert_equal_server_states in server_state_get tests +- revise expected not found error messages +- add equal_server_states and (assert_)contains_server_state +- add first few server_state_list tests +- add server_state_modify tests +- add TestApp.setup_test_server_state_with_server_id +- add e2e_lib_server_state_list_server_filter_works_across_projects_for_admin_user +- add e2e_lib_server_state_list_server_filter_stays_within_project_for_master_user +- e2e_lib_master_user_can_combine_server_state_list_filters +- e2e_lib_admin_user_can_combine_server_state_list_filters + +### lrzcc-lib + +#### Dependencies +- run cargo update + +#### Fixes +- add missing trailing slash in server_state_modify url + +### lrzcc-wire + +#### Dependencies +- run cargo update + +### lrzcc-cli + +#### Dependencies +- run cargo update + ## [lrzcc-test-v0.2.1] - 2024-11-22 diff --git a/Cargo.lock b/Cargo.lock index 7d7a9a93..2b0a879c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -459,9 +459,9 @@ checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "bytestring" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" +checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" dependencies = [ "bytes", ] @@ -1155,9 +1155,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hashlink" @@ -1534,7 +1534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.1", + "hashbrown 0.15.2", ] [[package]] @@ -1551,9 +1551,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jobserver" @@ -1607,9 +1607,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.164" +version = "0.2.165" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" +checksum = "fcb4d3d38eab6c5239a362fa8bae48c03baf980a6e7079f063942d563ef3533e" [[package]] name = "libm" @@ -1636,9 +1636,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "litrs" @@ -1745,7 +1745,9 @@ dependencies = [ name = "lrzcc-test" version = "0.2.1" dependencies = [ + "anyhow", "cargo-husky", + "chrono", "lrzcc", "lrzcc-api", "lrzcc-wire", @@ -2068,9 +2070,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pem-rfc7468" @@ -2485,9 +2487,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" dependencies = [ "const-oid", "digest", @@ -2549,9 +2551,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.17" +version = "0.23.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f1a745511c54ba6d4465e8d5dfbd81b45791756de28d4981af70d6dca128f1e" +checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f" dependencies = [ "once_cell", "ring", @@ -3427,9 +3429,9 @@ dependencies = [ [[package]] name = "tracing-bunyan-formatter" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5c266b9ac83dedf0e0385ad78514949e6d89491269e7065bee51d2bb8ec7373" +checksum = "2d637245a0d8774bd48df6482e086c59a8b5348a910c3b0579354045a9d82411" dependencies = [ "ahash", "gethostname", @@ -3445,9 +3447,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -3564,9 +3566,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -4013,9 +4015,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -4025,9 +4027,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", @@ -4058,18 +4060,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", diff --git a/api/src/authorization.rs b/api/src/authorization.rs index 3d14bb50..0c2db79a 100644 --- a/api/src/authorization.rs +++ b/api/src/authorization.rs @@ -1,4 +1,4 @@ -use crate::error::AuthOnlyError; +use crate::error::{AuthOnlyError, NotFoundOnlyError}; use lrzcc_wire::user::User; pub fn require_admin_user(user: &User) -> Result<(), AuthOnlyError> { @@ -10,6 +10,15 @@ pub fn require_admin_user(user: &User) -> Result<(), AuthOnlyError> { Ok(()) } +pub fn require_admin_user_or_return_not_found( + user: &User, +) -> Result<(), NotFoundOnlyError> { + if !user.is_staff { + return Err(NotFoundOnlyError::NotFoundError); + } + Ok(()) +} + pub fn require_master_user( user: &User, project_id: u32, @@ -23,15 +32,34 @@ pub fn require_master_user( Ok(()) } +pub fn require_master_user_or_return_not_found( + user: &User, + project_id: u32, +) -> Result<(), NotFoundOnlyError> { + if !user.is_staff && (user.role != 2 || user.project != project_id) { + return Err(NotFoundOnlyError::NotFoundError); + } + Ok(()) +} + pub fn require_project_user( user: &User, project_id: u32, ) -> Result<(), AuthOnlyError> { if !user.is_staff && user.project != project_id { return Err(AuthOnlyError::AuthorizationError( - "Admin or master user privileges for respective project required" - .to_string(), + "Must be admin or user of respective project".to_string(), )); } Ok(()) } + +pub fn require_project_user_or_return_not_found( + user: &User, + project_id: u32, +) -> Result<(), NotFoundOnlyError> { + if !user.is_staff && user.project != project_id { + return Err(NotFoundOnlyError::NotFoundError); + } + Ok(()) +} diff --git a/api/src/database/accounting/server_state.rs b/api/src/database/accounting/server_state.rs index a76cd95e..e2d5f065 100644 --- a/api/src/database/accounting/server_state.rs +++ b/api/src/database/accounting/server_state.rs @@ -1,19 +1,24 @@ -use crate::error::{NotFoundOrUnexpectedApiError, UnexpectedOnlyError}; +use crate::error::{ + MinimalApiError, NotFoundOrUnexpectedApiError, UnexpectedOnlyError, +}; use anyhow::Context; use chrono::{DateTime, Utc}; -use lrzcc_wire::accounting::ServerState; +use lrzcc_wire::accounting::{ServerState, ServerStateCreateData}; use sqlx::{Executor, FromRow, MySql, Transaction}; #[derive(FromRow)] pub struct ServerStateRow { + #[sqlx(try_from = "i32")] pub id: u32, pub begin: DateTime, pub end: Option>, pub instance_id: String, pub instance_name: String, + #[sqlx(try_from = "i64")] pub flavor: u32, pub flavor_name: String, pub status: String, + #[sqlx(try_from = "i32")] pub user: u32, pub username: String, } @@ -84,9 +89,7 @@ pub async fn select_server_state_from_db( ) -> Result { select_maybe_server_state_from_db(transaction, server_state_id) .await? - .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( - "Server state with given ID not found".to_string(), - )) + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError) } #[tracing::instrument( @@ -321,3 +324,211 @@ pub async fn select_server_states_by_server_from_db( .collect::>(); Ok(rows) } + +#[tracing::instrument( + name = "select_server_states_by_server_and_project_from_db", + skip(transaction) +)] +pub async fn select_server_states_by_server_and_project_from_db( + transaction: &mut Transaction<'_, MySql>, + server_id: String, + project_id: u64, +) -> Result, UnexpectedOnlyError> { + let query = sqlx::query!( + r#" + SELECT + s.id as id, + s.begin as begin, + s.end as end, + ss.instance_id as instance_id, + ss.instance_name as instance_name, + f.id as flavor, + f.name as flavor_name, + ss.status as status, + u.id as user, + u.name as username + FROM + accounting_state as s, + accounting_serverstate as ss, + resources_flavor as f, + user_user as u + WHERE + ss.flavor_id = f.id AND + ss.user_id = u.id AND + ss.state_ptr_id = s.id AND + ss.instance_id = ? AND + u.project_id = ? + "#, + server_id, + project_id + ); + let rows = transaction + .fetch_all(query) + .await + .context("Failed to execute select query")? + .into_iter() + .map(|r| ServerStateRow::from_row(&r)) + .collect::, _>>() + .context("Failed to convert row to server state")? + .into_iter() + .map(|r| ServerState { + id: r.id, + begin: r.begin.fixed_offset(), + end: r.end.map(|end| end.fixed_offset()), + instance_id: r.instance_id, + instance_name: r.instance_name, + flavor: r.flavor, + flavor_name: r.flavor_name, + status: r.status, + user: r.user, + username: r.username, + }) + .collect::>(); + Ok(rows) +} + +#[tracing::instrument( + name = "select_server_states_by_server_and_user_from_db", + skip(transaction) +)] +pub async fn select_server_states_by_server_and_user_from_db( + transaction: &mut Transaction<'_, MySql>, + server_id: String, + user_id: u64, +) -> Result, UnexpectedOnlyError> { + let query = sqlx::query!( + r#" + SELECT + s.id as id, + s.begin as begin, + s.end as end, + ss.instance_id as instance_id, + ss.instance_name as instance_name, + f.id as flavor, + f.name as flavor_name, + ss.status as status, + u.id as user, + u.name as username + FROM + accounting_state as s, + accounting_serverstate as ss, + resources_flavor as f, + user_user as u + WHERE + ss.flavor_id = f.id AND + ss.user_id = u.id AND + ss.state_ptr_id = s.id AND + ss.instance_id = ? AND + u.id = ? + "#, + server_id, + user_id + ); + let rows = transaction + .fetch_all(query) + .await + .context("Failed to execute select query")? + .into_iter() + .map(|r| ServerStateRow::from_row(&r)) + .collect::, _>>() + .context("Failed to convert row to server state")? + .into_iter() + .map(|r| ServerState { + id: r.id, + begin: r.begin.fixed_offset(), + end: r.end.map(|end| end.fixed_offset()), + instance_id: r.instance_id, + instance_name: r.instance_name, + flavor: r.flavor, + flavor_name: r.flavor_name, + status: r.status, + user: r.user, + username: r.username, + }) + .collect::>(); + Ok(rows) +} + +pub struct NewServerState { + pub begin: DateTime, + pub end: Option>, + pub instance_id: String, // UUIDv4 + pub instance_name: String, + pub flavor: u32, + // TODO we need an enum here + pub status: String, + pub user: u32, +} + +// TODO really validate data +impl TryFrom for NewServerState { + type Error = String; + + fn try_from(data: ServerStateCreateData) -> Result { + Ok(Self { + begin: data.begin.to_utc(), + end: data.end.map(|d| d.to_utc()), + instance_id: data.instance_id, + instance_name: data.instance_name, + flavor: data.flavor, + status: data.status, + user: data.user, + }) + } +} + +#[tracing::instrument( + name = "insert_server_state_into_db", + skip(new_server_state, transaction) +)] +pub async fn insert_server_state_into_db( + transaction: &mut Transaction<'_, MySql>, + new_server_state: &NewServerState, +) -> Result { + // TODO: MariaDB 10.5 introduced INSERT ... RETURNING + let query1 = sqlx::query!( + r#" + INSERT IGNORE INTO accounting_state (begin, end) + VALUES (?, ?) + "#, + new_server_state.begin, + new_server_state.end, + ); + let result1 = transaction + .execute(query1) + .await + .context("Failed to execute insert query")?; + if result1.rows_affected() == 0 { + return Err(MinimalApiError::ValidationError( + "Failed to insert new state, a conflicting entry exists" + .to_string(), + )); + } + let id = result1.last_insert_id(); + // TODO: MariaDB 10.5 introduced INSERT ... RETURNING + let query2 = sqlx::query!( + r#" + INSERT IGNORE INTO accounting_serverstate ( + state_ptr_id, instance_id, instance_name, status, flavor_id, user_id + ) + VALUES (?, ?, ?, ?, ?, ?) + "#, + id, + new_server_state.instance_id, + new_server_state.instance_name, + new_server_state.status, + new_server_state.flavor, + new_server_state.user + ); + let result2 = transaction + .execute(query2) + .await + .context("Failed to execute insert query")?; + if result2.rows_affected() == 0 { + return Err(MinimalApiError::ValidationError( + "Failed to insert new server state, a conflicting entry exists" + .to_string(), + )); + } + Ok(id) +} diff --git a/api/src/database/budgeting/project_budget.rs b/api/src/database/budgeting/project_budget.rs index 94adcdb4..af516e90 100644 --- a/api/src/database/budgeting/project_budget.rs +++ b/api/src/database/budgeting/project_budget.rs @@ -45,9 +45,7 @@ pub async fn select_project_budget_from_db( ) -> Result { select_maybe_project_budget_from_db(transaction, project_budget_id) .await? - .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( - "Project budget with given ID not found".to_string(), - )) + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError) } #[tracing::instrument( diff --git a/api/src/database/budgeting/user_budget.rs b/api/src/database/budgeting/user_budget.rs index 47369281..a86a3570 100644 --- a/api/src/database/budgeting/user_budget.rs +++ b/api/src/database/budgeting/user_budget.rs @@ -42,9 +42,7 @@ pub async fn select_user_budget_from_db( ) -> Result { select_maybe_user_budget_from_db(transaction, user_budget_id) .await? - .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( - "User budget with given ID not found".to_string(), - )) + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError) } #[tracing::instrument( diff --git a/api/src/database/pricing/flavor_price.rs b/api/src/database/pricing/flavor_price.rs index ab3d88dc..612c3c1e 100644 --- a/api/src/database/pricing/flavor_price.rs +++ b/api/src/database/pricing/flavor_price.rs @@ -68,9 +68,7 @@ pub async fn select_flavor_price_from_db( ) -> Result { select_maybe_flavor_price_from_db(transaction, flavor_price_id) .await? - .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( - "Flavor price with given ID not found".to_string(), - )) + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError) } #[tracing::instrument( diff --git a/api/src/database/quota/flavor_quota.rs b/api/src/database/quota/flavor_quota.rs index d3ca6f26..38573ec3 100644 --- a/api/src/database/quota/flavor_quota.rs +++ b/api/src/database/quota/flavor_quota.rs @@ -53,9 +53,7 @@ pub async fn select_flavor_quota_from_db( ) -> Result { select_maybe_flavor_quota_from_db(transaction, flavor_quota_id) .await? - .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( - "Flavor quota with given ID not found".to_string(), - )) + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError) } #[tracing::instrument( diff --git a/api/src/database/resources/flavor.rs b/api/src/database/resources/flavor.rs index 07240ca8..d723a508 100644 --- a/api/src/database/resources/flavor.rs +++ b/api/src/database/resources/flavor.rs @@ -1,7 +1,9 @@ -use crate::error::{NotFoundOrUnexpectedApiError, UnexpectedOnlyError}; +use crate::error::{ + MinimalApiError, NotFoundOrUnexpectedApiError, UnexpectedOnlyError, +}; use anyhow::Context; use lrzcc_wire::resources::{ - Flavor, FlavorDetailed, FlavorGroupMinimal, FlavorMinimal, + Flavor, FlavorCreateData, FlavorDetailed, FlavorGroupMinimal, FlavorMinimal, }; use sqlx::{Executor, FromRow, MySql, Transaction}; @@ -47,9 +49,7 @@ pub async fn select_flavor_name_from_db( ) -> Result { select_maybe_flavor_name_from_db(transaction, flavor_id) .await? - .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( - "User with given ID not found".to_string(), - )) + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError) } #[tracing::instrument(name = "select_maybe_flavor_from_db", skip(transaction))] @@ -87,9 +87,7 @@ pub async fn select_flavor_from_db( ) -> Result { select_maybe_flavor_from_db(transaction, flavor_id) .await? - .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( - "Flavor with given ID not found".to_string(), - )) + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError) } #[tracing::instrument( @@ -182,9 +180,7 @@ pub async fn select_flavor_detail_from_db( ) -> Result { select_maybe_flavor_detail_from_db(transaction, user_id) .await? - .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( - "Flavor with given ID or linked project not found".to_string(), - )) + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError) } #[tracing::instrument(name = "select_all_flavors_from_db", skip(transaction))] @@ -279,3 +275,37 @@ pub async fn select_flavors_by_flavor_group_from_db( .context("Failed to convert row to flavor")?; Ok(rows) } + +#[tracing::instrument( + name = "insert_flavor_into_db", + skip(new_flavor, transaction) +)] +pub async fn insert_flavor_into_db( + transaction: &mut Transaction<'_, MySql>, + new_flavor: &FlavorCreateData, +) -> Result { + // TODO: MariaDB 10.5 introduced INSERT ... RETURNING + let query = sqlx::query!( + r#" + INSERT IGNORE INTO resources_flavor (name, openstack_id, weight, group_id) + VALUES (?, ?, ?, ?) + "#, + new_flavor.name, + new_flavor.openstack_id, + new_flavor.weight, + new_flavor.group, + ); + let result = transaction + .execute(query) + .await + .context("Failed to execute insert query")?; + // TODO: what about non-existing project_id? + if result.rows_affected() == 0 { + return Err(MinimalApiError::ValidationError( + "Failed to insert new flavor group, a conflicting entry exists" + .to_string(), + )); + } + let id = result.last_insert_id(); + Ok(id) +} diff --git a/api/src/database/resources/flavor_group.rs b/api/src/database/resources/flavor_group.rs index 8ef2ed0b..e6871fae 100644 --- a/api/src/database/resources/flavor_group.rs +++ b/api/src/database/resources/flavor_group.rs @@ -48,9 +48,7 @@ pub async fn select_flavor_group_name_from_db( ) -> Result { select_maybe_flavor_group_name_from_db(transaction, flavor_group_id) .await? - .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( - "Flavor group with given ID not found".to_string(), - )) + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError) } #[derive(Clone, Debug, PartialEq, FromRow)] @@ -129,9 +127,7 @@ pub async fn select_flavor_group_from_db( ) -> Result { select_maybe_flavor_group_from_db(transaction, flavor_group_id) .await? - .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( - "Flavor group with given ID not found".to_string(), - )) + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError) } #[tracing::instrument( diff --git a/api/src/database/user/project.rs b/api/src/database/user/project.rs index ce5f24c0..71d63c3e 100644 --- a/api/src/database/user/project.rs +++ b/api/src/database/user/project.rs @@ -40,9 +40,7 @@ pub async fn select_project_from_db( ) -> Result { select_maybe_project_from_db(transaction, project_id) .await? - .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( - "Project with given ID not found".to_string(), - )) + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError) } #[tracing::instrument( @@ -84,9 +82,7 @@ pub async fn select_project_minimal_from_db( ) -> Result { select_maybe_project_minimal_from_db(transaction, project_id) .await? - .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( - "Project with given ID not found".to_string(), - )) + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError) } #[tracing::instrument( @@ -132,9 +128,7 @@ pub async fn select_project_name_from_db( ) -> Result { select_maybe_project_name_from_db(transaction, project_id) .await? - .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( - "Project with given ID not found".to_string(), - )) + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError) } #[tracing::instrument(name = "select_all_projects_from_db", skip(transaction))] diff --git a/api/src/database/user/user.rs b/api/src/database/user/user.rs index 201e5e9e..26597ab5 100644 --- a/api/src/database/user/user.rs +++ b/api/src/database/user/user.rs @@ -45,9 +45,7 @@ pub async fn select_user_name_from_db( ) -> Result { select_maybe_user_name_from_db(transaction, user_id) .await? - .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( - "User with given ID not found".to_string(), - )) + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError) } #[tracing::instrument(name = "select_all_users_from_db", skip(transaction))] @@ -199,9 +197,7 @@ pub async fn select_user_detail_from_db( ) -> Result { select_maybe_user_detail_from_db(transaction, user_id) .await? - .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( - "User with given ID or linked project not found".to_string(), - )) + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError) } #[tracing::instrument(name = "select_maybe_user_from_db", skip(transaction))] @@ -246,9 +242,7 @@ pub async fn select_user_from_db( ) -> Result { select_maybe_user_from_db(transaction, user_id) .await? - .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( - "User with given ID not found".to_string(), - )) + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError) } #[tracing::instrument( diff --git a/api/src/error.rs b/api/src/error.rs index 36498847..a7f5fade 100644 --- a/api/src/error.rs +++ b/api/src/error.rs @@ -54,8 +54,10 @@ pub async fn not_found() -> Result { pub enum OptionApiError { #[error("{0}")] ValidationError(String), - #[error("{0}")] - NotFoundError(String), + // NOTE: Do not change this string, because different not found + // messages can lead to information leakage + #[error("Resource not found")] + NotFoundError, #[error("{0}")] AuthorizationError(String), #[error(transparent)] @@ -74,8 +76,8 @@ impl ResponseError for OptionApiError { OptionApiError::ValidationError(message) => { (StatusCode::BAD_REQUEST, message.clone()) } - OptionApiError::NotFoundError(message) => { - (StatusCode::NOT_FOUND, message.clone()) + OptionApiError::NotFoundError => { + (StatusCode::NOT_FOUND, self.to_string()) } OptionApiError::AuthorizationError(message) => { (StatusCode::FORBIDDEN, message.clone()) @@ -131,9 +133,7 @@ impl From for OptionApiError { impl From for OptionApiError { fn from(value: NotFoundOrUnexpectedApiError) -> Self { match value { - NotFoundOrUnexpectedApiError::NotFoundError(message) => { - Self::NotFoundError(message) - } + NotFoundOrUnexpectedApiError::NotFoundError => Self::NotFoundError, NotFoundOrUnexpectedApiError::UnexpectedError(error) => { Self::UnexpectedError(error) } @@ -244,8 +244,10 @@ impl std::fmt::Debug for MinimalApiError { #[derive(thiserror::Error)] pub enum NotFoundOrUnexpectedApiError { - #[error("{0}")] - NotFoundError(String), + // NOTE: Do not change this string, because different not found + // messages can lead to information leakage + #[error("Resource not found")] + NotFoundError, #[error(transparent)] UnexpectedError(#[from] anyhow::Error), } @@ -259,8 +261,8 @@ impl std::fmt::Debug for NotFoundOrUnexpectedApiError { impl ResponseError for NotFoundOrUnexpectedApiError { fn error_response(&self) -> HttpResponse { let (status_code, message) = match self { - NotFoundOrUnexpectedApiError::NotFoundError(message) => { - (StatusCode::NOT_FOUND, message.clone()) + NotFoundOrUnexpectedApiError::NotFoundError => { + (StatusCode::NOT_FOUND, self.to_string()) } NotFoundOrUnexpectedApiError::UnexpectedError(_) => ( StatusCode::INTERNAL_SERVER_ERROR, @@ -323,6 +325,48 @@ impl From for NormalApiError { } } +#[derive(thiserror::Error)] +pub enum NotFoundOnlyError { + // NOTE: Do not change this string, because different not found messages + // messages can lead to information leakage + #[error("Resource not found")] + NotFoundError, +} + +impl std::fmt::Debug for NotFoundOnlyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + error_chain_fmt(self, f) + } +} + +impl ResponseError for NotFoundOnlyError { + fn error_response(&self) -> HttpResponse { + let (status_code, message) = match self { + NotFoundOnlyError::NotFoundError => { + (StatusCode::NOT_FOUND, self.to_string()) + } + }; + HttpResponse::build(status_code) + .insert_header(( + CONTENT_TYPE, + HeaderValue::from_static("application/json"), + )) + // TODO: handle unwrap + .body( + serde_json::to_string(&ErrorResponse { detail: message }) + .unwrap(), + ) + } +} + +impl From for OptionApiError { + fn from(value: NotFoundOnlyError) -> Self { + match value { + NotFoundOnlyError::NotFoundError => Self::NotFoundError, + } + } +} + #[derive(thiserror::Error)] pub enum UnexpectedOnlyError { #[error(transparent)] diff --git a/api/src/routes/accounting/server_state/create.rs b/api/src/routes/accounting/server_state/create.rs index a3af7918..1eedde4b 100644 --- a/api/src/routes/accounting/server_state/create.rs +++ b/api/src/routes/accounting/server_state/create.rs @@ -1,44 +1,16 @@ use crate::authorization::require_admin_user; use crate::database::{ + accounting::server_state::{insert_server_state_into_db, NewServerState}, resources::flavor::select_flavor_name_from_db, user::user::select_user_name_from_db, }; -use crate::error::{MinimalApiError, NormalApiError, OptionApiError}; +use crate::error::{NormalApiError, OptionApiError}; use actix_web::web::{Data, Json, ReqData}; use actix_web::HttpResponse; use anyhow::Context; -use chrono::{DateTime, Utc}; use lrzcc_wire::accounting::{ServerState, ServerStateCreateData}; use lrzcc_wire::user::{Project, User}; -use sqlx::{Executor, MySql, MySqlPool, Transaction}; - -pub struct NewServerState { - pub begin: DateTime, - pub end: Option>, - pub instance_id: String, // UUIDv4 - pub instance_name: String, - pub flavor: u32, - // TODO we need an enum here - pub status: String, - pub user: u32, -} - -// TODO really validate data -impl TryFrom for NewServerState { - type Error = String; - - fn try_from(data: ServerStateCreateData) -> Result { - Ok(Self { - begin: data.begin.to_utc(), - end: data.end.map(|d| d.to_utc()), - instance_id: data.instance_id, - instance_name: data.instance_name, - flavor: data.flavor, - status: data.status, - user: data.user, - }) - } -} +use sqlx::MySqlPool; #[tracing::instrument(name = "server_state_create")] pub async fn server_state_create( @@ -89,59 +61,3 @@ pub async fn server_state_create( .content_type("application/json") .json(server_state_created)) } - -#[tracing::instrument( - name = "insert_server_state_into_db", - skip(new_server_state, transaction) -)] -pub async fn insert_server_state_into_db( - transaction: &mut Transaction<'_, MySql>, - new_server_state: &NewServerState, -) -> Result { - // TODO: MariaDB 10.5 introduced INSERT ... RETURNING - let query1 = sqlx::query!( - r#" - INSERT IGNORE INTO accounting_state (begin, end) - VALUES (?, ?) - "#, - new_server_state.begin, - new_server_state.end, - ); - let result1 = transaction - .execute(query1) - .await - .context("Failed to execute insert query")?; - if result1.rows_affected() == 0 { - return Err(MinimalApiError::ValidationError( - "Failed to insert new state, a conflicting entry exists" - .to_string(), - )); - } - let id = result1.last_insert_id(); - // TODO: MariaDB 10.5 introduced INSERT ... RETURNING - let query2 = sqlx::query!( - r#" - INSERT IGNORE INTO accounting_serverstate ( - state_ptr_id, instance_id, instance_name, status, flavor_id, user_id - ) - VALUES (?, ?, ?, ?, ?, ?) - "#, - id, - new_server_state.instance_id, - new_server_state.instance_name, - new_server_state.status, - new_server_state.flavor, - new_server_state.user - ); - let result2 = transaction - .execute(query2) - .await - .context("Failed to execute insert query")?; - if result2.rows_affected() == 0 { - return Err(MinimalApiError::ValidationError( - "Failed to insert new server state, a conflicting entry exists" - .to_string(), - )); - } - Ok(id) -} diff --git a/api/src/routes/accounting/server_state/get.rs b/api/src/routes/accounting/server_state/get.rs index 6a4355a8..e826ec47 100644 --- a/api/src/routes/accounting/server_state/get.rs +++ b/api/src/routes/accounting/server_state/get.rs @@ -1,6 +1,7 @@ use super::ServerStateIdParam; -use crate::authorization::require_admin_user; +use crate::authorization::require_master_user_or_return_not_found; use crate::database::accounting::server_state::select_server_state_from_db; +use crate::database::user::user::select_user_from_db; use crate::error::OptionApiError; use actix_web::web::{Data, Path, ReqData}; use actix_web::HttpResponse; @@ -17,7 +18,6 @@ pub async fn server_state_get( params: Path, // TODO: is the ValidationError variant ever used? ) -> Result { - require_admin_user(&user)?; let mut transaction = db_pool .begin() .await @@ -27,14 +27,17 @@ pub async fn server_state_get( params.server_state_id as u64, ) .await?; + let server_state_user = + select_user_from_db(&mut transaction, server_state.user as u64).await?; transaction .commit() .await .context("Failed to commit transaction")?; - if server_state.user != user.id && !user.is_staff { - return Err(OptionApiError::NotFoundError( - "Server state not found".to_string(), - )); + if server_state.user != user.id { + require_master_user_or_return_not_found( + &user, + server_state_user.project, + )?; } Ok(HttpResponse::Ok() .content_type("application/json") diff --git a/api/src/routes/accounting/server_state/list.rs b/api/src/routes/accounting/server_state/list.rs index 983437f3..fe823420 100644 --- a/api/src/routes/accounting/server_state/list.rs +++ b/api/src/routes/accounting/server_state/list.rs @@ -1,11 +1,16 @@ -use crate::authorization::{require_admin_user, require_master_user}; +use crate::authorization::{ + require_admin_user, require_master_user, + require_master_user_or_return_not_found, +}; use crate::database::accounting::server_state::{ select_all_server_states_from_db, select_server_states_by_project_from_db, + select_server_states_by_server_and_project_from_db, + select_server_states_by_server_and_user_from_db, select_server_states_by_server_from_db, select_server_states_by_user_from_db, }; use crate::database::user::user::select_user_from_db; -use crate::error::NormalApiError; +use crate::error::OptionApiError; use actix_web::web::{Data, Query, ReqData}; use actix_web::HttpResponse; use anyhow::Context; @@ -19,7 +24,7 @@ pub async fn server_state_list( project: ReqData, db_pool: Data, params: Query, -) -> Result { +) -> Result { let mut transaction = db_pool .begin() .await @@ -29,23 +34,52 @@ pub async fn server_state_list( select_all_server_states_from_db(&mut transaction).await? } else if let Some(project_id) = params.project { require_master_user(&user, project_id)?; - select_server_states_by_project_from_db( - &mut transaction, - project_id as u64, - ) - .await? + if let Some(server_id) = params.server.clone() { + select_server_states_by_server_and_project_from_db( + &mut transaction, + server_id, + project_id as u64, + ) + .await? + } else { + select_server_states_by_project_from_db( + &mut transaction, + project_id as u64, + ) + .await? + } } else if let Some(user_id) = params.user { - let user = select_user_from_db(&mut transaction, user_id as u64) + let user1 = select_user_from_db(&mut transaction, user_id as u64) .await .context("Failed to select user")?; - require_master_user(&user, user.project)?; - select_server_states_by_user_from_db(&mut transaction, user.id as u64) + require_master_user_or_return_not_found(&user, user1.project)?; + if let Some(server_id) = params.server.clone() { + select_server_states_by_server_and_user_from_db( + &mut transaction, + server_id, + user1.id as u64, + ) + .await? + } else { + select_server_states_by_user_from_db( + &mut transaction, + user1.id as u64, + ) .await? + } } else if let Some(server_id) = params.server.clone() { - // TODO: can we make this master user accessible? - require_admin_user(&user)?; - select_server_states_by_server_from_db(&mut transaction, server_id) + if require_admin_user(&user).is_ok() { + select_server_states_by_server_from_db(&mut transaction, server_id) + .await? + } else { + require_master_user(&user, project.id)?; + select_server_states_by_server_and_project_from_db( + &mut transaction, + server_id, + project.id as u64, + ) .await? + } } else { select_server_states_by_user_from_db(&mut transaction, user.id as u64) .await? diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs index 35ddb4ba..9fa96c3c 100644 --- a/api/src/routes/mod.rs +++ b/api/src/routes/mod.rs @@ -22,7 +22,6 @@ pub use user::*; // - pricing::flavor_price::modify // - quota::flavor_quota::modify // - accounting::server_state::modify -// - accounting::server_state::get // - resources::flavor_group::get // - resources::flavor::get // - quota::flavor_quota::get diff --git a/api/src/routes/quota/flavor_quota/get.rs b/api/src/routes/quota/flavor_quota/get.rs index 6047b66b..73a0c394 100644 --- a/api/src/routes/quota/flavor_quota/get.rs +++ b/api/src/routes/quota/flavor_quota/get.rs @@ -32,9 +32,7 @@ pub async fn flavor_quota_get( .await .context("Failed to commit transaction")?; if flavor_quota.user != user.id && !user.is_staff { - return Err(OptionApiError::NotFoundError( - "Flavor quota not found".to_string(), - )); + return Err(OptionApiError::NotFoundError); } Ok(HttpResponse::Ok() .content_type("application/json") diff --git a/api/src/routes/resources/flavor/create.rs b/api/src/routes/resources/flavor/create.rs index 3df09827..8a84f2eb 100644 --- a/api/src/routes/resources/flavor/create.rs +++ b/api/src/routes/resources/flavor/create.rs @@ -1,6 +1,7 @@ use crate::authorization::require_admin_user; +use crate::database::resources::flavor::insert_flavor_into_db; use crate::database::resources::flavor_group::select_flavor_group_name_from_db; -use crate::error::{MinimalApiError, OptionApiError}; +use crate::error::OptionApiError; use actix_web::web::{Data, Json, ReqData}; use actix_web::HttpResponse; use anyhow::Context; @@ -8,7 +9,7 @@ use lrzcc_wire::resources::{ FlavorCreateData, FlavorDetailed, FlavorGroupMinimal, }; use lrzcc_wire::user::{Project, User}; -use sqlx::{Executor, MySql, MySqlPool, Transaction}; +use sqlx::MySqlPool; #[tracing::instrument(name = "flavor_create")] pub async fn flavor_create( @@ -50,37 +51,3 @@ pub async fn flavor_create( .content_type("application/json") .json(flavor_created)) } - -#[tracing::instrument( - name = "insert_flavor_into_db", - skip(new_flavor, transaction) -)] -pub async fn insert_flavor_into_db( - transaction: &mut Transaction<'_, MySql>, - new_flavor: &FlavorCreateData, -) -> Result { - // TODO: MariaDB 10.5 introduced INSERT ... RETURNING - let query = sqlx::query!( - r#" - INSERT IGNORE INTO resources_flavor (name, openstack_id, weight, group_id) - VALUES (?, ?, ?, ?) - "#, - new_flavor.name, - new_flavor.openstack_id, - new_flavor.weight, - new_flavor.group, - ); - let result = transaction - .execute(query) - .await - .context("Failed to execute insert query")?; - // TODO: what about non-existing project_id? - if result.rows_affected() == 0 { - return Err(MinimalApiError::ValidationError( - "Failed to insert new flavor group, a conflicting entry exists" - .to_string(), - )); - } - let id = result.last_insert_id(); - Ok(id) -} diff --git a/api/src/routes/user/project/get.rs b/api/src/routes/user/project/get.rs index 1611007d..4fbba7a3 100644 --- a/api/src/routes/user/project/get.rs +++ b/api/src/routes/user/project/get.rs @@ -1,5 +1,5 @@ use super::ProjectIdParam; -use crate::authorization::require_admin_user; +use crate::authorization::require_admin_user_or_return_not_found; use crate::database::resources::flavor_group::select_minimal_flavor_groups_by_project_id_from_db; use crate::database::user::project::select_project_from_db; use crate::database::user::user::select_minimal_users_by_project_id_from_db; @@ -19,7 +19,7 @@ pub async fn project_get( // TODO: is the ValidationError variant ever used? ) -> Result { if params.project_id != project.id { - require_admin_user(&user)?; + require_admin_user_or_return_not_found(&user)?; } let mut transaction = db_pool .begin() diff --git a/api/src/routes/user/user/get.rs b/api/src/routes/user/user/get.rs index fc172be1..ca6242ee 100644 --- a/api/src/routes/user/user/get.rs +++ b/api/src/routes/user/user/get.rs @@ -1,5 +1,5 @@ use super::UserIdParam; -use crate::authorization::require_master_user; +use crate::authorization::require_master_user_or_return_not_found; use crate::database::user::user::select_user_detail_from_db; use crate::error::OptionApiError; use actix_web::web::{Data, Path, ReqData}; @@ -28,7 +28,7 @@ pub async fn user_get( .await .context("Failed to commit transaction")?; if user2.id != user.id { - require_master_user(&user, user2.project.id)?; + require_master_user_or_return_not_found(&user, user2.project.id)?; } Ok(HttpResponse::Ok() diff --git a/lib/src/accounting/server_state.rs b/lib/src/accounting/server_state.rs index 42039822..5eb6942b 100644 --- a/lib/src/accounting/server_state.rs +++ b/lib/src/accounting/server_state.rs @@ -236,7 +236,7 @@ impl ServerStateApi { pub fn modify(&self, id: u32) -> ServerStateModifyRequest { // TODO use Url.join - let url = format!("{}/{}", self.url, id); + let url = format!("{}/{}/", self.url, id); ServerStateModifyRequest::new(url.as_ref(), &self.client, id) } diff --git a/test/Cargo.toml b/test/Cargo.toml index bb7a5b76..cccd7130 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -25,6 +25,8 @@ uuid = { version = "1.11", features = ["v4", "serde"] } once_cell = "1" wiremock = "0.6" rand = "0.8" +chrono = "0.4" +anyhow = "1.0" [dependencies.sqlx] version = "0.8" diff --git a/test/src/lib.rs b/test/src/lib.rs index e401e608..897ad08c 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -1,6 +1,15 @@ +use anyhow::Context; +use chrono::{DateTime, FixedOffset, Utc}; use lrzcc_api::configuration::{get_configuration, DatabaseSettings}; +use lrzcc_api::database::accounting::server_state::{ + insert_server_state_into_db, NewServerState, +}; +use lrzcc_api::database::resources::flavor::insert_flavor_into_db; +use lrzcc_api::error::MinimalApiError; use lrzcc_api::startup::{get_connection_pool, Application}; use lrzcc_api::telemetry::{get_subscriber, init_subscriber}; +use lrzcc_wire::accounting::ServerState; +use lrzcc_wire::resources::{Flavor, FlavorCreateData}; use lrzcc_wire::user::{Project, User}; use once_cell::sync::Lazy; use rand::distributions::Alphanumeric; @@ -189,6 +198,120 @@ impl TestApp { Ok(test_project) } + + pub async fn setup_test_flavor(&self) -> Result { + let mut transaction = self + .db_pool + .begin() + .await + .expect("Failed to begin transaction."); + let flavor_create = FlavorCreateData { + name: random_alphanumeric_string(10), + openstack_id: random_uuid(), + group: None, + weight: None, + }; + let flavor_id = insert_flavor_into_db(&mut transaction, &flavor_create) + .await? as u32; + transaction + .commit() + .await + .context("Failed to commit transaction")?; + let flavor = Flavor { + id: flavor_id, + name: flavor_create.name, + openstack_id: flavor_create.openstack_id, + group: None, + group_name: None, + weight: 0, + }; + Ok(flavor) + } + + pub async fn setup_test_server_state( + &self, + flavor: &Flavor, + user: &User, + ) -> Result { + let mut transaction = self + .db_pool + .begin() + .await + .expect("Failed to begin transaction."); + let begin = DateTime::::from(Utc::now()); + let new_server_state = NewServerState { + begin: begin.to_utc(), + end: None, + instance_id: random_uuid(), + instance_name: random_alphanumeric_string(10), + flavor: flavor.id, + status: "ACTIVE".to_string(), + user: user.id, + }; + let server_state_id = + insert_server_state_into_db(&mut transaction, &new_server_state) + .await? as u32; + transaction + .commit() + .await + .context("Failed to commit transaction")?; + let server_state = ServerState { + id: server_state_id, + begin, + end: None, + instance_id: new_server_state.instance_id, + instance_name: new_server_state.instance_name, + flavor: new_server_state.flavor, + flavor_name: flavor.name.clone(), + status: new_server_state.status, + user: user.id, + username: user.name.clone(), + }; + Ok(server_state) + } + + pub async fn setup_test_server_state_with_server_id( + &self, + flavor: &Flavor, + user: &User, + server_id: &str, + ) -> Result { + let mut transaction = self + .db_pool + .begin() + .await + .expect("Failed to begin transaction."); + let begin = DateTime::::from(Utc::now()); + let new_server_state = NewServerState { + begin: begin.to_utc(), + end: None, + instance_id: server_id.to_string(), + instance_name: random_alphanumeric_string(10), + flavor: flavor.id, + status: "ACTIVE".to_string(), + user: user.id, + }; + let server_state_id = + insert_server_state_into_db(&mut transaction, &new_server_state) + .await? as u32; + transaction + .commit() + .await + .context("Failed to commit transaction")?; + let server_state = ServerState { + id: server_state_id, + begin, + end: None, + instance_id: new_server_state.instance_id, + instance_name: new_server_state.instance_name, + flavor: new_server_state.flavor, + flavor_name: flavor.name.clone(), + status: new_server_state.status, + user: user.id, + username: user.name.clone(), + }; + Ok(server_state) + } } pub async fn spawn_app() -> TestApp { diff --git a/test/tests/accounting/mod.rs b/test/tests/accounting/mod.rs new file mode 100644 index 00000000..24bd533b --- /dev/null +++ b/test/tests/accounting/mod.rs @@ -0,0 +1 @@ +mod server_state; diff --git a/test/tests/accounting/server_state/create.rs b/test/tests/accounting/server_state/create.rs new file mode 100644 index 00000000..7aa5f4ba --- /dev/null +++ b/test/tests/accounting/server_state/create.rs @@ -0,0 +1,308 @@ +use super::assert_equal_server_states; +use chrono::{DateTime, FixedOffset, Utc}; +use lrzcc::{Api, Token}; +use lrzcc_test::{random_alphanumeric_string, random_uuid, spawn_app}; +use std::str::FromStr; +use tokio::task::spawn_blocking; + +#[tokio::test] +async fn e2e_lib_server_state_create_denies_access_to_normal_user() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(0, 0, 1) + .await + .expect("Failed to setup test project"); + let user = test_project.normals[0].user.clone(); + let token = test_project.normals[0].token.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let begin = DateTime::::from(Utc::now()); + let instance_id = random_uuid(); + let instance_name = random_alphanumeric_string(10); + let status = "ACTIVE".to_string(); + let create = client + .server_state + .create( + begin, + instance_id, + instance_name, + flavor.id, + status, + user.id, + ) + .send(); + + // assert + assert!(create.is_err()); + assert_eq!( + create.unwrap_err().to_string(), + format!("Admin privileges required") + ); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_server_state_create_denies_access_to_master_user() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(0, 1, 0) + .await + .expect("Failed to setup test project"); + let user = test_project.masters[0].user.clone(); + let token = test_project.masters[0].token.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let begin = DateTime::::from(Utc::now()); + let instance_id = random_uuid(); + let instance_name = random_alphanumeric_string(10); + let status = "ACTIVE".to_string(); + let create = client + .server_state + .create( + begin, + instance_id, + instance_name, + flavor.id, + status, + user.id, + ) + .send(); + + // assert + assert!(create.is_err()); + assert_eq!( + create.unwrap_err().to_string(), + format!("Admin privileges required") + ); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_server_state_create_works() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(1, 0, 0) + .await + .expect("Failed to setup test project"); + let user = test_project.admins[0].user.clone(); + let token = test_project.admins[0].token.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let begin = DateTime::::from(Utc::now()); + let instance_id = random_uuid(); + let instance_name = random_alphanumeric_string(10); + let status = "ACTIVE".to_string(); + let created = client + .server_state + .create( + begin, + instance_id.clone(), + instance_name.clone(), + flavor.id, + status.clone(), + user.id, + ) + .send() + .unwrap(); + + // assert + assert_eq!(begin, created.begin); + assert_eq!(instance_id, created.instance_id); + assert_eq!(instance_name, created.instance_name); + assert_eq!(flavor.id, created.flavor); + assert_eq!(flavor.name, created.flavor_name); + assert_eq!(status, created.status); + assert_eq!(user.id, created.user); + assert_eq!(user.name, created.username); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_server_state_create_and_get_works() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(1, 0, 0) + .await + .expect("Failed to setup test project"); + let user = test_project.admins[0].user.clone(); + let token = test_project.admins[0].token.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act and assert 1 - create + let begin = DateTime::::from(Utc::now()); + let instance_id = random_uuid(); + let instance_name = random_alphanumeric_string(10); + let status = "ACTIVE".to_string(); + let created = client + .server_state + .create( + begin, + instance_id.clone(), + instance_name.clone(), + flavor.id, + status.clone(), + user.id, + ) + .send() + .unwrap(); + assert_eq!(begin, created.begin); + assert_eq!(instance_id, created.instance_id); + assert_eq!(instance_name, created.instance_name); + assert_eq!(flavor.id, created.flavor); + assert_eq!(flavor.name, created.flavor_name); + assert_eq!(status, created.status); + assert_eq!(user.id, created.user); + assert_eq!(user.name, created.username); + + // act and assert 2 - get + let retrieved = client.server_state.get(created.id).unwrap(); + assert_equal_server_states(&retrieved, &created); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_server_state_create_and_list_works() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(1, 0, 0) + .await + .expect("Failed to setup test project"); + let user = test_project.admins[0].user.clone(); + let token = test_project.admins[0].token.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act and assert 1 - create + let begin = DateTime::::from(Utc::now()); + let instance_id = random_uuid(); + let instance_name = random_alphanumeric_string(10); + let status = "ACTIVE".to_string(); + let created = client + .server_state + .create( + begin, + instance_id.clone(), + instance_name.clone(), + flavor.id, + status.clone(), + user.id, + ) + .send() + .unwrap(); + assert_eq!(begin, created.begin); + assert_eq!(instance_id, created.instance_id); + assert_eq!(instance_name, created.instance_name); + assert_eq!(flavor.id, created.flavor); + assert_eq!(flavor.name, created.flavor_name); + assert_eq!(status, created.status); + assert_eq!(user.id, created.user); + assert_eq!(user.name, created.username); + + // act and assert 2 - list + let server_states = client.server_state.list().all().send().unwrap(); + assert_eq!(server_states.len(), 1); + assert_equal_server_states(&server_states[0], &created); + }) + .await + .unwrap(); +} diff --git a/test/tests/accounting/server_state/delete.rs b/test/tests/accounting/server_state/delete.rs new file mode 100644 index 00000000..c4d34dd2 --- /dev/null +++ b/test/tests/accounting/server_state/delete.rs @@ -0,0 +1,146 @@ +use lrzcc::{Api, Token}; +use lrzcc_test::spawn_app; +use std::str::FromStr; +use tokio::task::spawn_blocking; + +#[tokio::test] +async fn e2e_lib_server_state_delete_denies_access_to_normal_user() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(0, 0, 1) + .await + .expect("Failed to setup test project"); + let user = test_project.normals[0].user.clone(); + let token = test_project.normals[0].token.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_state = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let delete = client.server_state.delete(server_state.id); + + // assert + assert!(delete.is_err()); + assert_eq!( + delete.unwrap_err().to_string(), + format!("Admin privileges required") + ); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_server_state_delete_denies_access_to_master_user() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(0, 1, 0) + .await + .expect("Failed to setup test project"); + let user = test_project.masters[0].user.clone(); + let token = test_project.masters[0].token.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_state = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let delete = client.server_state.delete(server_state.id); + + // assert + assert!(delete.is_err()); + assert_eq!( + delete.unwrap_err().to_string(), + format!("Admin privileges required") + ); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_server_state_delete_works() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(1, 0, 0) + .await + .expect("Failed to setup test project"); + let user = test_project.admins[0].user.clone(); + let token = test_project.admins[0].token.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_state = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act and assert 1 - delete + client.server_state.delete(server_state.id).unwrap(); + + // act and assert 2 - get + let get = client.server_state.get(server_state.id); + assert!(get.is_err()); + assert_eq!( + get.unwrap_err().to_string(), + "Resource not found".to_string() + ); + }) + .await + .unwrap(); +} diff --git a/test/tests/accounting/server_state/get.rs b/test/tests/accounting/server_state/get.rs new file mode 100644 index 00000000..aca77b15 --- /dev/null +++ b/test/tests/accounting/server_state/get.rs @@ -0,0 +1,276 @@ +use super::assert_equal_server_states; +use lrzcc::{Api, Token}; +use lrzcc_test::spawn_app; +use std::str::FromStr; +use tokio::task::spawn_blocking; + +// Permission matrix: +// own state state from own project other state +// admin user X X X +// master user X X - +// normal user X - - + +#[tokio::test] +async fn e2e_lib_normal_user_can_get_own_server_state() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(0, 0, 1) + .await + .expect("Failed to setup test project"); + let user = test_project.normals[0].user.clone(); + let token = test_project.normals[0].token.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_state = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let retrieved = client.server_state.get(server_state.id).unwrap(); + + // assert + assert_equal_server_states(&retrieved, &server_state); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_normal_user_cannot_get_other_server_state() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(0, 0, 2) + .await + .expect("Failed to setup test project"); + let user = test_project.normals[0].user.clone(); + let token = test_project.normals[0].token.clone(); + let user2 = test_project.normals[1].user.clone(); + let test_project2 = server + .setup_test_project(0, 0, 1) + .await + .expect("Failed to setup test project"); + let user3 = test_project2.normals[0].user.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_state_2 = server + .setup_test_server_state(&flavor, &user2) + .await + .expect("Failed to setup test server state 1"); + let server_state_3 = server + .setup_test_server_state(&flavor, &user3) + .await + .expect("Failed to setup test server state 2"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + for server_state in [&server_state_2, &server_state_3] { + // act + let get = client.server_state.get(server_state.id); + + // assert + assert!(get.is_err()); + assert_eq!( + get.unwrap_err().to_string(), + "Resource not found".to_string() + ); + } + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_master_user_can_get_own_projects_server_states() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(0, 1, 1) + .await + .expect("Failed to setup test project"); + let user = test_project.masters[0].user.clone(); + let token = test_project.masters[0].token.clone(); + let user2 = test_project.normals[0].user.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_state_1 = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state 1"); + let server_state_2 = server + .setup_test_server_state(&flavor, &user2) + .await + .expect("Failed to setup test server state 2"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let retrieved1 = client.server_state.get(server_state_1.id).unwrap(); + let retrieved2 = client.server_state.get(server_state_2.id).unwrap(); + + // assert + assert_equal_server_states(&retrieved1, &server_state_1); + assert_equal_server_states(&retrieved2, &server_state_2); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_master_user_cannot_get_other_projects_server_states() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(0, 1, 0) + .await + .expect("Failed to setup test project"); + let test_project2 = server + .setup_test_project(0, 0, 1) + .await + .expect("Failed to setup test project"); + let user = test_project.masters[0].user.clone(); + let token = test_project.masters[0].token.clone(); + let user2 = test_project2.normals[0].user.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_state = server + .setup_test_server_state(&flavor, &user2) + .await + .expect("Failed to setup test server state"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let get = client.server_state.get(server_state.id); + + // assert + assert!(get.is_err()); + assert_eq!( + get.unwrap_err().to_string(), + "Resource not found".to_string() + ); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_admin_can_get_all_kinds_of_users() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(1, 0, 1) + .await + .expect("Failed to setup test project"); + let test_project2 = server + .setup_test_project(0, 0, 1) + .await + .expect("Failed to setup test project"); + let user = test_project.admins[0].user.clone(); + let token = test_project.admins[0].token.clone(); + let user2 = test_project.normals[0].user.clone(); + let user3 = test_project2.normals[0].user.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_state_1 = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state 1"); + let server_state_2 = server + .setup_test_server_state(&flavor, &user2) + .await + .expect("Failed to setup test server state 2"); + let server_state_3 = server + .setup_test_server_state(&flavor, &user3) + .await + .expect("Failed to setup test server state 3"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let retrieved1 = client.server_state.get(server_state_1.id).unwrap(); + let retrieved2 = client.server_state.get(server_state_2.id).unwrap(); + let retrieved3 = client.server_state.get(server_state_3.id).unwrap(); + + // assert + assert_equal_server_states(&retrieved1, &server_state_1); + assert_equal_server_states(&retrieved2, &server_state_2); + assert_equal_server_states(&retrieved3, &server_state_3); + }) + .await + .unwrap(); +} diff --git a/test/tests/accounting/server_state/list.rs b/test/tests/accounting/server_state/list.rs new file mode 100644 index 00000000..b1b4b770 --- /dev/null +++ b/test/tests/accounting/server_state/list.rs @@ -0,0 +1,651 @@ +use super::assert_contains_server_state; +use lrzcc::{Api, Token}; +use lrzcc_test::{random_uuid, spawn_app}; +use std::str::FromStr; +use tokio::task::spawn_blocking; + +#[tokio::test] +async fn e2e_lib_normal_user_can_list_own_server_states() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(0, 0, 2) + .await + .expect("Failed to setup test project"); + let user = test_project.normals[0].user.clone(); + let token = test_project.normals[0].token.clone(); + let user2 = test_project.normals[1].user.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_state1 = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state 1"); + let server_state2 = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state 2"); + let _server_state3 = server + .setup_test_server_state(&flavor, &user2) + .await + .expect("Failed to setup test server state 3"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let server_states = client.server_state.list().send().unwrap(); + + // assert + assert_eq!(server_states.len(), 2); + assert_contains_server_state(&server_states, &server_state1); + assert_contains_server_state(&server_states, &server_state2); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_normal_user_cannot_use_other_server_state_list_filters() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(0, 0, 1) + .await + .expect("Failed to setup test project"); + let user = test_project.normals[0].user.clone(); + let project = test_project.project.clone(); + let token = test_project.normals[0].token.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let list1 = client.server_state.list().all().send(); + let list2 = client.server_state.list().user(user.id).send(); + let list3 = client.server_state.list().project(project.id).send(); + + // assert + assert!(list1.is_err()); + assert!(list2.is_err()); + assert!(list3.is_err()); + assert_eq!( + list1.unwrap_err().to_string(), + format!("Admin privileges required") + ); + assert_eq!( + list2.unwrap_err().to_string(), + "Resource not found".to_string() + ); + assert_eq!( + list3.unwrap_err().to_string(), + format!("Admin or master user privileges for respective project required") + ); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_master_user_cannot_use_other_server_state_all_filter() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(0, 0, 1) + .await + .expect("Failed to setup test project"); + let user = test_project.normals[0].user.clone(); + let token = test_project.normals[0].token.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let list = client.server_state.list().all().send(); + + // assert + assert!(list.is_err()); + assert_eq!( + list.unwrap_err().to_string(), + format!("Admin privileges required") + ); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_master_user_can_list_own_projects_and_users_server_states() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(0, 1, 1) + .await + .expect("Failed to setup test project"); + let user = test_project.masters[0].user.clone(); + let token = test_project.masters[0].token.clone(); + let user2 = test_project.normals[0].user.clone(); + let project = test_project.project.clone(); + let test_project2 = server + .setup_test_project(0, 0, 1) + .await + .expect("Failed to setup test project"); + let user3 = test_project2.normals[0].user.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_state1 = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state 1"); + let server_state2 = server + .setup_test_server_state(&flavor, &user2) + .await + .expect("Failed to setup test server state 1"); + let _server_state3 = server + .setup_test_server_state(&flavor, &user3) + .await + .expect("Failed to setup test server state 1"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let server_states1 = client.server_state.list().send().unwrap(); + let server_states2 = + client.server_state.list().user(user2.id).send().unwrap(); + let server_states3 = client + .server_state + .list() + .project(project.id) + .send() + .unwrap(); + + // assert + assert_eq!(server_states1.len(), 1); + assert_contains_server_state(&server_states1, &server_state1); + assert_eq!(server_states2.len(), 1); + assert_contains_server_state(&server_states2, &server_state2); + assert_eq!(server_states3.len(), 2); + assert_contains_server_state(&server_states3, &server_state1); + assert_contains_server_state(&server_states3, &server_state2); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_server_state_list_server_filter_stays_within_project_for_master_user( +) { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(0, 1, 1) + .await + .expect("Failed to setup test project"); + let user = test_project.masters[0].user.clone(); + let token = test_project.masters[0].token.clone(); + let user2 = test_project.normals[0].user.clone(); + let test_project2 = server + .setup_test_project(0, 0, 1) + .await + .expect("Failed to setup test project"); + let user3 = test_project2.normals[0].user.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_id = random_uuid(); + let server_state1 = server + .setup_test_server_state_with_server_id(&flavor, &user, &server_id) + .await + .expect("Failed to setup test server state 1"); + let server_state2 = server + .setup_test_server_state_with_server_id(&flavor, &user2, &server_id) + .await + .expect("Failed to setup test server state 2"); + let _server_state3 = server + .setup_test_server_state_with_server_id(&flavor, &user3, &server_id) + .await + .expect("Failed to setup test server state 3"); + let _server_state4 = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state 4"); + let _server_state5 = server + .setup_test_server_state(&flavor, &user2) + .await + .expect("Failed to setup test server state 5"); + let _server_state6 = server + .setup_test_server_state(&flavor, &user3) + .await + .expect("Failed to setup test server state 6"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let server_states = client + .server_state + .list() + .server(&server_id) + .send() + .unwrap(); + + // assert + assert_eq!(server_states.len(), 2); + assert_contains_server_state(&server_states, &server_state1); + assert_contains_server_state(&server_states, &server_state2); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_server_state_list_server_filter_works_across_projects_for_admin_user( +) { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(1, 0, 1) + .await + .expect("Failed to setup test project"); + let user = test_project.admins[0].user.clone(); + let token = test_project.admins[0].token.clone(); + let user2 = test_project.normals[0].user.clone(); + let test_project2 = server + .setup_test_project(0, 0, 1) + .await + .expect("Failed to setup test project"); + let user3 = test_project2.normals[0].user.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_id = random_uuid(); + let server_state1 = server + .setup_test_server_state_with_server_id(&flavor, &user, &server_id) + .await + .expect("Failed to setup test server state 1"); + let server_state2 = server + .setup_test_server_state_with_server_id(&flavor, &user2, &server_id) + .await + .expect("Failed to setup test server state 2"); + let server_state3 = server + .setup_test_server_state_with_server_id(&flavor, &user3, &server_id) + .await + .expect("Failed to setup test server state 3"); + let _server_state4 = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state 4"); + let _server_state5 = server + .setup_test_server_state(&flavor, &user2) + .await + .expect("Failed to setup test server state 5"); + let _server_state6 = server + .setup_test_server_state(&flavor, &user3) + .await + .expect("Failed to setup test server state 6"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let server_states = client + .server_state + .list() + .server(&server_id) + .send() + .unwrap(); + + // assert + assert_eq!(server_states.len(), 3); + assert_contains_server_state(&server_states, &server_state1); + assert_contains_server_state(&server_states, &server_state2); + assert_contains_server_state(&server_states, &server_state3); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_admin_user_can_use_any_user_list_filters() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(1, 0, 1) + .await + .expect("Failed to setup test project"); + let user = test_project.admins[0].user.clone(); + let token = test_project.admins[0].token.clone(); + let user2 = test_project.normals[0].user.clone(); + let project = test_project.project.clone(); + let test_project2 = server + .setup_test_project(0, 1, 1) + .await + .expect("Failed to setup test project"); + let user3 = test_project2.masters[0].user.clone(); + let user4 = test_project2.normals[0].user.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_state1 = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state 1"); + let server_state2 = server + .setup_test_server_state(&flavor, &user2) + .await + .expect("Failed to setup test server state 2"); + let server_state3 = server + .setup_test_server_state(&flavor, &user3) + .await + .expect("Failed to setup test server state 3"); + let server_state4 = server + .setup_test_server_state(&flavor, &user4) + .await + .expect("Failed to setup test server state 4"); + let server_state5 = server + .setup_test_server_state(&flavor, &user4) + .await + .expect("Failed to setup test server state 5"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let server_states1 = client.server_state.list().send().unwrap(); + let server_states2 = client + .server_state + .list() + .server(&server_state5.instance_id) + .send() + .unwrap(); + let server_states3 = + client.server_state.list().user(user2.id).send().unwrap(); + let server_states4 = client + .server_state + .list() + .project(project.id) + .send() + .unwrap(); + let server_states5 = client.server_state.list().all().send().unwrap(); + + // assert + assert_eq!(server_states1.len(), 1); + assert_contains_server_state(&server_states1, &server_state1); + assert_eq!(server_states2.len(), 1); + assert_contains_server_state(&server_states2, &server_state5); + assert_eq!(server_states3.len(), 1); + assert_contains_server_state(&server_states3, &server_state2); + assert_eq!(server_states4.len(), 2); + assert_contains_server_state(&server_states4, &server_state1); + assert_contains_server_state(&server_states4, &server_state2); + assert_eq!(server_states5.len(), 5); + assert_contains_server_state(&server_states5, &server_state1); + assert_contains_server_state(&server_states5, &server_state2); + assert_contains_server_state(&server_states5, &server_state3); + assert_contains_server_state(&server_states5, &server_state4); + assert_contains_server_state(&server_states5, &server_state5); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_master_user_can_combine_server_state_list_filters() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(0, 1, 1) + .await + .expect("Failed to setup test project"); + let user = test_project.masters[0].user.clone(); + let token = test_project.masters[0].token.clone(); + let project = test_project.project.clone(); + let user2 = test_project.normals[0].user.clone(); + let test_project2 = server + .setup_test_project(0, 0, 1) + .await + .expect("Failed to setup test project"); + let user3 = test_project2.normals[0].user.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_id = random_uuid(); + let server_state1 = server + .setup_test_server_state_with_server_id(&flavor, &user, &server_id) + .await + .expect("Failed to setup test server state 1"); + let server_state2 = server + .setup_test_server_state_with_server_id(&flavor, &user2, &server_id) + .await + .expect("Failed to setup test server state 2"); + let _server_state3 = server + .setup_test_server_state_with_server_id(&flavor, &user3, &server_id) + .await + .expect("Failed to setup test server state 3"); + let _server_state4 = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state 4"); + let _server_state5 = server + .setup_test_server_state(&flavor, &user2) + .await + .expect("Failed to setup test server state 5"); + let _server_state6 = server + .setup_test_server_state(&flavor, &user3) + .await + .expect("Failed to setup test server state 6"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let server_states1 = client + .server_state + .list() + .project(project.id) + .server(&server_id) + .send() + .unwrap(); + let server_states2 = client + .server_state + .list() + .user(user.id) + .server(&server_id) + .send() + .unwrap(); + + // assert + assert_eq!(server_states1.len(), 2); + assert_contains_server_state(&server_states1, &server_state1); + assert_contains_server_state(&server_states1, &server_state2); + assert_eq!(server_states2.len(), 1); + assert_contains_server_state(&server_states2, &server_state1); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_admin_user_can_combine_server_state_list_filters() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(1, 0, 1) + .await + .expect("Failed to setup test project"); + let user = test_project.admins[0].user.clone(); + let token = test_project.admins[0].token.clone(); + let project = test_project.project.clone(); + let user2 = test_project.normals[0].user.clone(); + let test_project2 = server + .setup_test_project(0, 0, 1) + .await + .expect("Failed to setup test project"); + let user3 = test_project2.normals[0].user.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_id = random_uuid(); + let server_state1 = server + .setup_test_server_state_with_server_id(&flavor, &user, &server_id) + .await + .expect("Failed to setup test server state 1"); + let server_state2 = server + .setup_test_server_state_with_server_id(&flavor, &user2, &server_id) + .await + .expect("Failed to setup test server state 2"); + let _server_state3 = server + .setup_test_server_state_with_server_id(&flavor, &user3, &server_id) + .await + .expect("Failed to setup test server state 3"); + let _server_state4 = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state 4"); + let _server_state5 = server + .setup_test_server_state(&flavor, &user2) + .await + .expect("Failed to setup test server state 5"); + let _server_state6 = server + .setup_test_server_state(&flavor, &user3) + .await + .expect("Failed to setup test server state 6"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let server_states1 = client + .server_state + .list() + .project(project.id) + .server(&server_id) + .send() + .unwrap(); + let server_states2 = client + .server_state + .list() + .user(user.id) + .server(&server_id) + .send() + .unwrap(); + + // assert + assert_eq!(server_states1.len(), 2); + assert_contains_server_state(&server_states1, &server_state1); + assert_contains_server_state(&server_states1, &server_state2); + assert_eq!(server_states2.len(), 1); + assert_contains_server_state(&server_states2, &server_state1); + }) + .await + .unwrap(); +} diff --git a/test/tests/accounting/server_state/mod.rs b/test/tests/accounting/server_state/mod.rs new file mode 100644 index 00000000..46f863d5 --- /dev/null +++ b/test/tests/accounting/server_state/mod.rs @@ -0,0 +1,45 @@ +mod create; +mod delete; +mod get; +mod list; +mod modify; + +use lrzcc_wire::accounting::ServerState; + +pub fn equal_server_states( + server_state_1: &ServerState, + server_state_2: &ServerState, +) -> bool { + server_state_1.id == server_state_2.id + && (server_state_1.begin - server_state_2.begin).num_milliseconds() < 1 + && server_state_1.instance_id == server_state_2.instance_id + && server_state_1.instance_name == server_state_2.instance_name + && server_state_1.flavor == server_state_2.flavor + && server_state_1.flavor_name == server_state_2.flavor_name + && server_state_1.status == server_state_2.status + && server_state_1.user == server_state_2.user + && server_state_1.username == server_state_2.username +} + +pub fn assert_equal_server_states( + server_state_1: &ServerState, + server_state_2: &ServerState, +) { + assert!(equal_server_states(server_state_1, server_state_2)); +} + +pub fn contains_server_state( + server_states: &[ServerState], + server_state: &ServerState, +) -> bool { + server_states + .iter() + .any(|s| equal_server_states(s, server_state)) +} + +pub fn assert_contains_server_state( + server_states: &[ServerState], + server_state: &ServerState, +) { + assert!(contains_server_state(server_states, server_state)); +} diff --git a/test/tests/accounting/server_state/modify.rs b/test/tests/accounting/server_state/modify.rs new file mode 100644 index 00000000..51618885 --- /dev/null +++ b/test/tests/accounting/server_state/modify.rs @@ -0,0 +1,151 @@ +use super::assert_equal_server_states; +use lrzcc::{Api, Token}; +use lrzcc_test::{random_alphanumeric_string, random_uuid, spawn_app}; +use std::str::FromStr; +use tokio::task::spawn_blocking; + +#[tokio::test] +async fn e2e_lib_server_state_modify_denies_access_to_normal_user() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(0, 0, 1) + .await + .expect("Failed to setup test project"); + let user = test_project.normals[0].user.clone(); + let token = test_project.normals[0].token.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_state = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let modify = client.server_state.modify(server_state.id).send(); + + // assert + assert!(modify.is_err()); + assert_eq!( + modify.unwrap_err().to_string(), + format!("Admin privileges required") + ); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_server_state_modify_denies_access_to_master_user() { + // arrange + let server = spawn_app().await; + let test_project = server + .setup_test_project(0, 1, 0) + .await + .expect("Failed to setup test project"); + let user = test_project.masters[0].user.clone(); + let token = test_project.masters[0].token.clone(); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_state = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act + let modify = client.server_state.modify(server_state.id).send(); + + // assert + assert!(modify.is_err()); + assert_eq!( + modify.unwrap_err().to_string(), + format!("Admin privileges required") + ); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn e2e_lib_server_state_modify_and_get_works() { + // arrange + let server = spawn_app().await; + let (user, _project, token) = server + .setup_test_user_and_project(true) + .await + .expect("Failed to setup test user and project."); + server + .mock_keystone_auth(&token, &user.openstack_id, &user.name) + .mount(&server.keystone_server) + .await; + let flavor = server + .setup_test_flavor() + .await + .expect("Failed to setup test flavor"); + let server_state = server + .setup_test_server_state(&flavor, &user) + .await + .expect("Failed to setup test server state"); + + spawn_blocking(move || { + // arrange + let client = Api::new( + format!("{}/api", &server.address), + Token::from_str(&token).unwrap(), + None, + None, + ) + .unwrap(); + + // act and assert 1 - modify + let instance_id = random_uuid(); + let instance_name = random_alphanumeric_string(10); + let modified = client + .server_state + .modify(server_state.id) + .instance_id(instance_id.clone()) + .instance_name(instance_name.clone()) + .send() + .unwrap(); + assert_eq!(instance_id, modified.instance_id); + assert_eq!(instance_name, modified.instance_name); + + // act and assert 2 - get + let retrieved = client.server_state.get(modified.id).unwrap(); + assert_equal_server_states(&modified, &retrieved); + }) + .await + .unwrap(); +} diff --git a/test/tests/main.rs b/test/tests/main.rs index 97c35fe4..95075f08 100644 --- a/test/tests/main.rs +++ b/test/tests/main.rs @@ -1,2 +1,3 @@ +mod accounting; mod hello; mod user; diff --git a/test/tests/user/project/delete.rs b/test/tests/user/project/delete.rs index 00015827..c5750f33 100644 --- a/test/tests/user/project/delete.rs +++ b/test/tests/user/project/delete.rs @@ -139,10 +139,7 @@ async fn e2e_lib_project_create_get_delete_get_works() { // act and assert 4 - get let get = client.project.get(created.id); assert!(get.is_err()); - assert_eq!( - get.unwrap_err().to_string(), - format!("Project with given ID not found") - ); + assert_eq!(get.unwrap_err().to_string(), format!("Resource not found")); }) .await .unwrap(); diff --git a/test/tests/user/project/get.rs b/test/tests/user/project/get.rs index f14e9b83..abf8f01d 100644 --- a/test/tests/user/project/get.rs +++ b/test/tests/user/project/get.rs @@ -73,10 +73,8 @@ async fn e2e_lib_user_cannot_get_other_project() { // assert assert!(get.is_err()); - assert_eq!( - get.unwrap_err().to_string(), - format!("Admin privileges required") - ); + // TODO: can be also check the HTTP status code? + assert_eq!(get.unwrap_err().to_string(), format!("Resource not found")); }) .await .unwrap(); diff --git a/test/tests/user/user/delete.rs b/test/tests/user/user/delete.rs index dd4eb09f..90691e81 100644 --- a/test/tests/user/user/delete.rs +++ b/test/tests/user/user/delete.rs @@ -148,10 +148,7 @@ async fn e2e_lib_user_create_get_delete_get_works() { // act and assert 4 - get let get = client.user.get(created.id); assert!(get.is_err()); - assert_eq!( - get.unwrap_err().to_string(), - format!("User with given ID or linked project not found") - ); + assert_eq!(get.unwrap_err().to_string(), format!("Resource not found")); }) .await .unwrap(); diff --git a/test/tests/user/user/get.rs b/test/tests/user/user/get.rs index 5921bc6d..88cc2fbd 100644 --- a/test/tests/user/user/get.rs +++ b/test/tests/user/user/get.rs @@ -75,7 +75,7 @@ async fn e2e_lib_normal_user_cannot_get_other_users() { ) .unwrap(); - for user in vec![&user2, &user3] { + for user in [&user2, &user3] { // act let get = client.user.get(user.id); @@ -83,7 +83,7 @@ async fn e2e_lib_normal_user_cannot_get_other_users() { assert!(get.is_err()); assert_eq!( get.unwrap_err().to_string(), - format!("Admin or master user privileges for respective project required") + "Resource not found".to_string() ); } }) @@ -166,7 +166,7 @@ async fn e2e_lib_master_user_cannot_get_other_projects_users() { assert!(get.is_err()); assert_eq!( get.unwrap_err().to_string(), - format!("Admin or master user privileges for respective project required") + "Resource not found".to_string() ); }) .await