diff --git a/.github/workflows/dev-crates.yaml b/.github/workflows/dev-crates.yaml index 2c3b4c45..f87aa057 100644 --- a/.github/workflows/dev-crates.yaml +++ b/.github/workflows/dev-crates.yaml @@ -37,4 +37,14 @@ jobs: if [[ $(make rust-coverage-percent RUST_PKG=types) -lt ${{ env.MIN_CODE_COV }} ]]; then echo 'coverage dropped below ${{ env.MIN_CODE_COV }}%' exit 1 + fi + - name: crates/pg coverage report + run: | + make rust-coverage RUST_PKG=pg + echo "MIN_CODE_COV=$(yq .crates.pg.min_code_cov project.yaml)" >> $GITHUB_ENV + - name: fail crates/pg coverage under ${{ env.MIN_CODE_COV }}% + run: | + if [[ $(make rust-coverage-percent RUST_PKG=pg) -lt ${{ env.MIN_CODE_COV }} ]]; then + echo 'coverage dropped below ${{ env.MIN_CODE_COV }}%' + exit 1 fi \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index c941c499..0df3525f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,7 @@ "files.insertFinalNewline": false, "files.trimFinalNewlines": false, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "eslint.workingDirectories": [ "./client", @@ -78,5 +78,6 @@ "unused-export-let": "ignore", "a11y-click-events-have-key-events": "ignore", "a11y-no-noninteractive-tabindex": "ignore", - } + }, + "editor.inlineSuggest.showToolbar": "onHover" } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 20d2b006..04b818ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -347,6 +347,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown 0.12.3", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "deranged" version = "0.3.9" @@ -389,6 +402,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.27" @@ -405,6 +433,23 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" +[[package]] +name = "futures-executor" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" + [[package]] name = "futures-macro" version = "0.3.27" @@ -436,9 +481,11 @@ checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" dependencies = [ "futures-channel", "futures-core", + "futures-io", "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -837,6 +884,7 @@ dependencies = [ "async-trait", "bb8", "bb8-postgres", + "serial_test", "sqls", "tokio", "tokio-postgres", @@ -1170,6 +1218,31 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "serial_test" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +dependencies = [ + "dashmap", + "futures", + "lazy_static", + "log", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.36", +] + [[package]] name = "sha2" version = "0.10.6" diff --git a/README.md b/README.md index 66c94046..7e473c6f 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ encryption and replication are secondary **q.** what is the equation? **a.** *u* = transactions per second, *wi* = value conserved per transaction, *Mx!* = value visible in a combinatorial game +**q.** how does standardizing financial value as a conserved quantity protect individuals? +**a.** applying a conservation law to financial value protects producers and consumers from an abuse of government authority. when producers increase the [purchasing power](https://en.wikipedia.org/wiki/Information_content) of money by shipping useful r&d, consumer wealth increases. but government printing money, and government chartered "bankers" expecting money, are not the same types of events as producers shipping useful r&d. theyre not even the same types of events as producers shipping common goods and services. so when government authority is used to violate conservation by defining money as something you can just print, and [mix](https://en.wikipedia.org/wiki/Money_multiplier) with failing "bank" notes, the loss of information in money from these physically negative events steals away the purchasing power created by producers, the increased wealth of consumers, and the value of all property owned by individuals +government is not above failure, nor is it entitled to steal from the private sector to conceal its failure. improving government depends on failure [predicting](https://en.wikipedia.org/wiki/Time_travel_debugging) the individuals and laws that must be replaced. flying a flag and demanding loyalty before this step is just misdirection + **q.** will a government hosted payment app reduce my freedom? **a.** the government can already see your transactions. systemaccounting empowers you to see the transactions of your government. access to the realtime financial performance of your government helps protect you from electing individuals who exploit money printing, price manipulation and the absence of accountability to systematize the cost of their failures to everyone else @@ -169,7 +173,7 @@ encryption and replication are secondary public demonstration of the following use cases through a systemaccounting function: * expressing a [conservation law](https://en.wikipedia.org/wiki/Conservation_law) through a [data structure](https://github.com/systemaccounting/mxfactorial/blob/develop/mxfactorial.ipynb) disambiguates *delivered* value from *expected* value, and replaces [committees](https://www.federalreserve.gov/financial-stability.htm) with an automated [financial stability criterion](https://en.wikipedia.org/wiki/BIBO_stability) * producing a [scientific measure](http://www.systemaccounting.org/how_does_systemaccounting_produce_a_scientific_measure_of_the_cost_of_capital) of the equilibrium price of capital signals the demand for capital with an empirical rate of return instead of a [government defined word](https://www.systemaccounting.org/what_is_a_bank), and removes the ability of a central authority to [manipulate](https://en.wikipedia.org/wiki/Federal_funds_rate) the price of credit -* `SELECT SUM(price) FROM transactions WHERE time = NOW();` maximizes & protects for individuals a scientific standard the publicly-measured quarterly or annual '[GDP](https://en.wikipedia.org/wiki/Gross_domestic_product)' violates +* `SELECT SUM(price*quantity) FROM transactions WHERE time = NOW();` maximizes & protects for individuals a scientific standard the publicly-measured quarterly or annual '[GDP](https://en.wikipedia.org/wiki/Gross_domestic_product)' violates * where an industry is [chartered](http://www.occ.gov/topics/licensing/index-licensing.html), [protected](https://en.wikipedia.org/wiki/Bailout), and [primarily depended upon](http://www.opensecrets.org/industries./) by a government requiring [election assistance](https://en.wikipedia.org/wiki/Collusion), conserving value & liability (information) separates the balance sheets of governments from individuals, and eliminates [socializing](https://en.wikipedia.org/wiki/Externality#Negative) the [default risk](https://en.wikipedia.org/wiki/Liability_(financial_accounting)) of any individual or firm * establishing the conditions studied by [combinatorial game theory](https://en.wikipedia.org/wiki/Combinatorial_game_theory) through physics & data science ends public dependency on such resources as [credit ratings](https://en.wikipedia.org/wiki/Bond_credit_rating), [quarterly filings](https://en.wikipedia.org/wiki/Form_10-Q), the Consumer Price Index ([CPI](https://en.wikipedia.org/wiki/Consumer_price_index)), and the federal reserve economic data ([FRED](https://en.wikipedia.org/wiki/Federal_Reserve_Economic_Data)) platform * *accounts as projective coordinates*: structuring transactions between debiting and crediting users [across time](https://en.wikipedia.org/wiki/Homogeneous_coordinates) as a [binary logarithmic event](https://en.wikipedia.org/wiki/Binary_logarithm#Information_theory) creates a [2^n dimensional space](https://en.wikipedia.org/wiki/Clifford_algebra#Basis_and_dimension) in a data model diff --git a/crates/pg/Cargo.toml b/crates/pg/Cargo.toml index 00a951e5..31308c73 100644 --- a/crates/pg/Cargo.toml +++ b/crates/pg/Cargo.toml @@ -8,6 +8,9 @@ bb8 = "0.8.0" bb8-postgres = "0.8.1" tokio = "1.26.0" async-trait = "0.1.73" -tokio-postgres = {version = "0.7.7", features = ["with-chrono-0_4"]} +tokio-postgres = { version = "0.7.7", features = ["with-chrono-0_4"] } types = { path = "../types" } -sqls = { path = "../sqls" } \ No newline at end of file +sqls = { path = "../sqls" } + +[dev-dependencies] +serial_test = "*" \ No newline at end of file diff --git a/crates/pg/src/lib.rs b/crates/pg/src/lib.rs index c37f8d86..8faf2682 100644 --- a/crates/pg/src/lib.rs +++ b/crates/pg/src/lib.rs @@ -11,6 +11,7 @@ use types::{ account::{AccountProfile, AccountProfiles, AccountTrait}, account_role::AccountRole, rule::{RuleInstance, RuleInstanceTrait, RuleInstances}, + time::TZTime, }; pub struct DB; @@ -36,35 +37,6 @@ impl DB { } } -#[cfg(test)] -mod tests { - use super::*; - use std::env; - - #[test] - fn it_returns_a_conn_uri() { - env::set_var("PGUSER", "a"); - env::set_var("PGPASSWORD", "b"); - env::set_var("PGHOST", "c"); - env::set_var("PGPORT", "d"); - env::set_var("PGDATABASE", "e"); - let got = DB::create_conn_uri_from_env_vars(); - env::remove_var("PGUSER"); - env::remove_var("PGPASSWORD"); - env::remove_var("PGHOST"); - env::remove_var("PGPORT"); - env::remove_var("PGDATABASE"); - let want = String::from("postgresql://a:b@c:d/e"); - assert_eq!(got, want) - } - - #[test] - #[should_panic] - fn it_panics_from_unset_env_var() { - DB::get_env_var("NOT_SET"); - } -} - pub type DynConnPool = Arc; pub type DynDBConn = Arc; @@ -86,37 +58,30 @@ pub struct ConnectionPool(Pool>); impl DBConnPoolTrait for ConnectionPool { async fn get_conn(&self) -> DynDBConn { let conn = self.0.get_owned().await.unwrap(); // todo: handle error - Arc::new(DatabaseConnection(conn)) + Arc::new(Conn(Arc::new(DatabaseConnection(conn)))) } } - pub struct DatabaseConnection(PooledConnection<'static, PostgresConnectionManager>); +// for dependency injection, wrap tokio-postgres as a trait object +// inside a Conn, then impl service traits on Conn +pub struct Conn(Arc); + +// impl account service trait on Conn #[async_trait] -impl AccountTrait for DatabaseConnection { +impl AccountTrait for Conn { async fn get_account_profiles( &self, accounts: Vec, ) -> Result> { - // https://github.com/sfackler/rust-postgres/issues/133#issuecomment-659751392 - let mut params: Vec<&(dyn ToSql + Sync)> = Vec::new(); - - for a in accounts.iter() { - params.push(a) - } - let rows = self .0 - .query( - select_account_profiles_by_db_cr_accounts().as_str(), - ¶ms[..], - ) + .query_account_profiles(select_account_profiles_by_db_cr_accounts(), accounts) .await; - match rows { Err(rows) => Err(Box::new(rows)), Ok(rows) => { - let account_profiles = DatabaseConnection::from_account_profile_rows(rows); + let account_profiles = from_account_profile_rows(rows); Ok(account_profiles) } } @@ -125,16 +90,20 @@ impl AccountTrait for DatabaseConnection { async fn get_approvers_for_account(&self, account: String) -> Vec { let rows = self .0 - .query(select_approvers().as_str(), &[&account]) + .query_approvers(select_approvers(), account) .await .unwrap(); // todo: handle error - let account_approvers: Vec = rows.into_iter().map(|row| row.get(0)).collect(); + let account_approvers: Vec = rows + .into_iter() + .map(|row| row.get_string("approver")) + .collect(); account_approvers } } +// impl rule instance service trait on Conn #[async_trait] -impl RuleInstanceTrait for DatabaseConnection { +impl RuleInstanceTrait for Conn { async fn get_profile_state_rule_instances( &self, account_role: AccountRole, @@ -142,13 +111,14 @@ impl RuleInstanceTrait for DatabaseConnection { ) -> RuleInstances { let rows = self .0 - .query( - select_rule_instance_by_type_role_state().as_str(), - &[&"transaction_item", &account_role, &state_name], + .query_profile_state_rule_instances( + select_rule_instance_by_type_role_state(), + account_role, + state_name, ) .await .unwrap(); // todo: handle error - DatabaseConnection::from_rule_instance_rows(rows) + from_rule_instance_rows(rows) } async fn get_rule_instances_by_type_role_account( @@ -158,13 +128,14 @@ impl RuleInstanceTrait for DatabaseConnection { ) -> RuleInstances { let rows = self .0 - .query( - select_rule_instance_by_type_role_account().as_str(), - &[&"transaction_item", &account_role, &account], + .query_rule_instances_by_type_role_account( + select_rule_instance_by_type_role_account(), + account_role, + account, ) .await .unwrap(); // todo: handle error - DatabaseConnection::from_rule_instance_rows(rows) + from_rule_instance_rows(rows) } async fn get_approval_rule_instances( @@ -174,92 +145,576 @@ impl RuleInstanceTrait for DatabaseConnection { ) -> RuleInstances { let rows = self .0 - .query( - select_rule_instance_by_type_role_account().as_str(), - &[&"approval", &account_role, &account], + .query_approval_rule_instances( + select_rule_instance_by_type_role_account(), + account_role, + account, ) .await .unwrap(); // todo: handle error - DatabaseConnection::from_rule_instance_rows(rows) + from_rule_instance_rows(rows) } } +trait RowTrait { + fn get_opt_string(&self, idx: &str) -> Option; + fn get_string(&self, idx: &str) -> String; + fn get_vec_string(&self, idx: &str) -> Vec; + fn get_account_role(&self, idx: &str) -> AccountRole; + fn get_opt_tztime(&self, idx: &str) -> Option; +} + +impl RowTrait for Row { + fn get_opt_string(&self, idx: &str) -> Option { + self.get(idx) + } + fn get_string(&self, idx: &str) -> String { + self.get(idx) + } + fn get_vec_string(&self, idx: &str) -> Vec { + self.get(idx) + } + fn get_account_role(&self, idx: &str) -> AccountRole { + self.get(idx) + } + fn get_opt_tztime(&self, idx: &str) -> Option { + self.get(idx) + } +} + +fn from_account_profile_row(row: Box) -> AccountProfile { + AccountProfile { + // cadet todo: add a schema module declaring statics for + // column names and arrays of column names for each table + id: row.get_opt_string("id"), + account_name: row.get_string("account_name"), + description: row.get_opt_string("description"), + first_name: row.get_opt_string("first_name"), + middle_name: row.get_opt_string("middle_name"), + last_name: row.get_opt_string("last_name"), + country_name: row.get_string("country_name"), + street_number: row.get_opt_string("street_number"), + street_name: row.get_opt_string("street_name"), + floor_number: row.get_opt_string("floor_number"), + unit_number: row.get_opt_string("unit_number"), + city_name: row.get_string("city_name"), + county_name: row.get_opt_string("county_name"), + region_name: row.get_opt_string("region_name"), + state_name: row.get_string("state_name"), + postal_code: row.get_string("postal_code"), + latlng: row.get_opt_string("latlng"), + email_address: row.get_string("email_address"), + telephone_country_code: row.get_opt_string("telephone_country_code"), + telephone_area_code: row.get_opt_string("telephone_area_code"), + telephone_number: row.get_opt_string("telephone_number"), + occupation_id: row.get_opt_string("occupation_id"), + industry_id: row.get_opt_string("industry_id"), + } +} + +fn from_account_profile_rows(rows: Vec>) -> AccountProfiles { + rows.into_iter().map(from_account_profile_row).collect() +} + +fn from_rule_instance_row(row: Box) -> RuleInstance { + RuleInstance { + id: row.get_opt_string("id"), + rule_type: row.get_string("rule_type"), + rule_name: row.get_string("rule_name"), + rule_instance_name: row.get_string("rule_instance_name"), + variable_values: row.get_vec_string("variable_values"), + account_role: row.get_account_role("account_role"), + item_id: row.get_opt_string("item_id"), + price: row.get_opt_string("price"), + quantity: row.get_opt_string("quantity"), + unit_of_measurement: row.get_opt_string("unit_of_measurement"), + units_measured: row.get_opt_string("units_measured"), + account_name: row.get_opt_string("account_name"), + first_name: row.get_opt_string("first_name"), + middle_name: row.get_opt_string("middle_name"), + last_name: row.get_opt_string("last_name"), + country_name: row.get_opt_string("country_name"), + street_id: row.get_opt_string("street_id"), + street_name: row.get_opt_string("street_name"), + floor_number: row.get_opt_string("floor_number"), + unit_id: row.get_opt_string("unit_id"), + city_name: row.get_opt_string("city_name"), + county_name: row.get_opt_string("county_name"), + region_name: row.get_opt_string("region_name"), + state_name: row.get_opt_string("state_name"), + postal_code: row.get_opt_string("postal_code"), + latlng: row.get_opt_string("latlng"), + email_address: row.get_opt_string("email_address"), + telephone_country_code: row.get_opt_string("telephone_country_code"), + telephone_area_code: row.get_opt_string("telephone_area_code"), + telephone_number: row.get_opt_string("telephone_number"), + occupation_id: row.get_opt_string("occupation_id"), + industry_id: row.get_opt_string("industry_id"), + disabled_time: row.get_opt_tztime("disabled_time"), + removed_time: row.get_opt_tztime("removed_time"), + created_at: row.get_opt_tztime("created_at"), + } +} + +fn from_rule_instance_rows(rows: Vec>) -> RuleInstances { + rows.into_iter().map(from_rule_instance_row).collect() +} + +// dependency injection trait for tokio-postgres +#[async_trait] +trait DatabaseConnectionTrait { + async fn query_account_profiles( + &self, + sql_stmt: String, + accounts: Vec, + ) -> Result>, tokio_postgres::Error>; + + async fn query_approvers( + &self, + sql_stmt: String, + account: String, + ) -> Result>, tokio_postgres::Error>; + + async fn query_profile_state_rule_instances( + &self, + sql_stmt: String, + account_role: AccountRole, + state_name: String, + ) -> Result>, tokio_postgres::Error>; + + async fn query_rule_instances_by_type_role_account( + &self, + sql_stmt: String, + account_role: AccountRole, + account: String, + ) -> Result>, tokio_postgres::Error>; + + async fn query_approval_rule_instances( + &self, + sql_stmt: String, + account_role: AccountRole, + account: String, + ) -> Result>, tokio_postgres::Error>; +} + +// impl dependency injection trait using tokio-postgres +#[async_trait] +impl DatabaseConnectionTrait for DatabaseConnection { + async fn query_account_profiles( + &self, + sql_stmt: String, + accounts: Vec, + ) -> Result>, tokio_postgres::Error> { + // https://github.com/sfackler/rust-postgres/issues/133#issuecomment-659751392 + let mut params: Vec<&(dyn ToSql + Sync)> = Vec::new(); + + for a in accounts.iter() { + params.push(a) + } + + self.query(sql_stmt, ¶ms[..]).await + } + + async fn query_approvers( + &self, + sql_stmt: String, + account: String, + ) -> Result>, tokio_postgres::Error> { + self.query(sql_stmt, &[&account]).await + } + + async fn query_profile_state_rule_instances( + &self, + sql_stmt: String, + account_role: AccountRole, + state_name: String, + ) -> Result>, tokio_postgres::Error> { + self.query(sql_stmt, &[&"transaction_item", &account_role, &state_name]) + .await + } + + async fn query_rule_instances_by_type_role_account( + &self, + sql_stmt: String, + account_role: AccountRole, + account: String, + ) -> Result>, tokio_postgres::Error> { + self.query(sql_stmt, &[&"transaction_item", &account_role, &account]) + .await + } + async fn query_approval_rule_instances( + &self, + sql_stmt: String, + account_role: AccountRole, + account: String, + ) -> Result>, tokio_postgres::Error> { + self.query(sql_stmt, &[&"approval", &account_role, &account]) + .await + } +} + +// isolate tokio-postgres query dependency in this impl impl DatabaseConnection { - pub fn from_account_profile_row(row: Row) -> AccountProfile { - AccountProfile { - id: row.get("id"), - account_name: row.get("account_name"), - description: row.get("description"), - first_name: row.get("first_name"), - middle_name: row.get("middle_name"), - last_name: row.get("last_name"), - country_name: row.get("country_name"), - street_number: row.get("street_number"), - street_name: row.get("street_name"), - floor_number: row.get("floor_number"), - unit_number: row.get("unit_number"), - city_name: row.get("city_name"), - county_name: row.get("county_name"), - region_name: row.get("region_name"), - state_name: row.get("state_name"), - postal_code: row.get("postal_code"), - latlng: row.get("latlng"), - email_address: row.get("email_address"), - telephone_country_code: row.get("telephone_country_code"), - telephone_area_code: row.get("telephone_area_code"), - telephone_number: row.get("telephone_number"), - occupation_id: row.get("occupation_id"), - industry_id: row.get("industry_id"), + async fn query( + &self, + sql_stmt: String, + params: &[&(dyn ToSql + Sync)], + ) -> Result>, tokio_postgres::Error> { + self.0.query(sql_stmt.as_str(), params).await.map(|rows| { + rows.into_iter() + .map(|row| Box::new(row) as Box) + .collect() + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serial_test::serial; // concurrency avoided while using static mut TEST_ARGS for shared test state + use std::{env, vec}; + + fn account_profile_columns() -> Vec { + vec![ + String::from("id"), + String::from("account_name"), + String::from("description"), + String::from("first_name"), + String::from("middle_name"), + String::from("last_name"), + String::from("country_name"), + String::from("street_number"), + String::from("street_name"), + String::from("floor_number"), + String::from("unit_number"), + String::from("city_name"), + String::from("county_name"), + String::from("region_name"), + String::from("state_name"), + String::from("postal_code"), + String::from("latlng"), + String::from("email_address"), + String::from("telephone_country_code"), + String::from("telephone_area_code"), + String::from("telephone_number"), + String::from("occupation_id"), + String::from("industry_id"), + ] + } + + fn rule_instance_columns() -> Vec { + vec![ + String::from("id"), + String::from("rule_type"), + String::from("rule_name"), + String::from("rule_instance_name"), + String::from("variable_values"), + String::from("account_role"), + String::from("item_id"), + String::from("price"), + String::from("quantity"), + String::from("unit_of_measurement"), + String::from("units_measured"), + String::from("account_name"), + String::from("first_name"), + String::from("middle_name"), + String::from("last_name"), + String::from("country_name"), + String::from("street_id"), + String::from("street_name"), + String::from("floor_number"), + String::from("unit_id"), + String::from("city_name"), + String::from("county_name"), + String::from("region_name"), + String::from("state_name"), + String::from("postal_code"), + String::from("latlng"), + String::from("email_address"), + String::from("telephone_country_code"), + String::from("telephone_area_code"), + String::from("telephone_number"), + String::from("occupation_id"), + String::from("industry_id"), + String::from("disabled_time"), + String::from("removed_time"), + String::from("created_at"), + ] + } + + #[test] + fn it_returns_a_conn_uri() { + env::set_var("PGUSER", "a"); + env::set_var("PGPASSWORD", "b"); + env::set_var("PGHOST", "c"); + env::set_var("PGPORT", "d"); + env::set_var("PGDATABASE", "e"); + let got = DB::create_conn_uri_from_env_vars(); + env::remove_var("PGUSER"); + env::remove_var("PGPASSWORD"); + env::remove_var("PGHOST"); + env::remove_var("PGPORT"); + env::remove_var("PGDATABASE"); + let want = String::from("postgresql://a:b@c:d/e"); + assert_eq!(got, want) + } + + #[test] + #[should_panic] + fn it_panics_from_unset_env_var() { + DB::get_env_var("NOT_SET"); + } + + static mut TEST_ARGS: Vec = vec![]; + + #[derive(Clone, Copy)] + struct TestRow; + + impl TestRow { + fn add(&self, arg: &str) { + // test code only + unsafe { TEST_ARGS.push(String::from(arg)) } + } + + fn clear(&self) { + // test code only + unsafe { TEST_ARGS.clear() } } } - pub fn from_account_profile_rows(rows: Vec) -> AccountProfiles { - rows.into_iter() - .map(Self::from_account_profile_row) - .collect() - } - - pub fn from_rule_instance_row(row: Row) -> RuleInstance { - RuleInstance { - id: row.get("id"), - rule_type: row.get("rule_type"), - rule_name: row.get("rule_name"), - rule_instance_name: row.get("rule_instance_name"), - variable_values: row.get("variable_values"), - account_role: row.get("account_role"), - item_id: row.get("item_id"), - price: row.get("price"), - quantity: row.get("quantity"), - unit_of_measurement: row.get("unit_of_measurement"), - units_measured: row.get("units_measured"), - account_name: row.get("account_name"), - first_name: row.get("first_name"), - middle_name: row.get("middle_name"), - last_name: row.get("last_name"), - country_name: row.get("country_name"), - street_id: row.get("street_id"), - street_name: row.get("street_name"), - floor_number: row.get("floor_number"), - unit_id: row.get("unit_id"), - city_name: row.get("city_name"), - county_name: row.get("county_name"), - region_name: row.get("region_name"), - state_name: row.get("state_name"), - postal_code: row.get("postal_code"), - latlng: row.get("latlng"), - email_address: row.get("email_address"), - telephone_country_code: row.get("telephone_country_code"), - telephone_area_code: row.get("telephone_area_code"), - telephone_number: row.get("telephone_number"), - occupation_id: row.get("occupation_id"), - industry_id: row.get("industry_id"), - disabled_time: row.get("disabled_time"), - removed_time: row.get("removed_time"), - created_at: row.get("created_at"), + impl RowTrait for TestRow { + fn get_opt_string(&self, idx: &str) -> Option { + self.add(idx); + None + } + fn get_string(&self, idx: &str) -> String { + self.add(idx); + String::from("") + } + fn get_vec_string(&self, idx: &str) -> Vec { + self.add(idx); + vec![] + } + fn get_account_role(&self, idx: &str) -> AccountRole { + self.add(idx); + AccountRole::Creditor + } + fn get_opt_tztime(&self, idx: &str) -> Option { + self.add(idx); + None } } - pub fn from_rule_instance_rows(rows: Vec) -> RuleInstances { - rows.into_iter().map(Self::from_rule_instance_row).collect() + // struct TestDB; + + // impl DatabaseConnectionTrait for TestDB {} + + #[test] + #[serial] + fn from_account_profile_row_called_with_args() { + let test_row = TestRow; + + from_account_profile_row(Box::new(test_row)); + + let mut unsorted_got = unsafe { TEST_ARGS.clone() }; + + unsorted_got.sort(); + + let got = unsorted_got.clone(); + + let mut unsorted_want = account_profile_columns(); + + unsorted_want.sort(); + + let want = unsorted_want.clone(); + + assert_eq!(got, want); + + test_row.clone().clear() + } + + #[test] + #[serial] + fn from_account_profile_rows_called_with_args() { + let test_row_1 = TestRow; + let test_row_2 = TestRow; + + let test_rows: Vec> = vec![Box::new(test_row_1), Box::new(test_row_2)]; + + from_account_profile_rows(test_rows); + + let mut unsorted_got = unsafe { TEST_ARGS.clone() }; + + unsorted_got.sort(); + + let got = unsorted_got.clone(); + + // add first set of account_profile_columns + let mut unsorted_want = account_profile_columns(); + + // add second set of account_profile_columns + unsorted_want.append(&mut account_profile_columns()); + + unsorted_want.sort(); + + let want = unsorted_want.clone(); + + assert_eq!(got, want); + + test_row_1.clone().clear(); + } + + #[test] + #[serial] + fn from_rule_instance_row_called_with_args() { + let test_row = TestRow; + + from_rule_instance_row(Box::new(test_row)); + + let mut unsorted_got = unsafe { TEST_ARGS.clone() }; + + unsorted_got.sort(); + + let got = unsorted_got.clone(); + + let mut unsorted_want = rule_instance_columns(); + + unsorted_want.sort(); + + let want = unsorted_want.clone(); + + assert_eq!(got, want); + + test_row.clone().clear() + } + + #[test] + #[serial] + fn from_rule_instance_rows_called_with_args() { + let test_row_1 = TestRow; + let test_row_2 = TestRow; + + let test_rows: Vec> = vec![Box::new(test_row_1), Box::new(test_row_2)]; + + from_rule_instance_rows(test_rows); + + let mut unsorted_got = unsafe { TEST_ARGS.clone() }; + + unsorted_got.sort(); + + let got = unsorted_got.clone(); + + // add first set of rule_instance_columns + let mut unsorted_want = rule_instance_columns(); + + // add second set of rule_instance_columns + unsorted_want.append(&mut rule_instance_columns()); + + unsorted_want.sort(); + + let want = unsorted_want.clone(); + + assert_eq!(got, want); + + test_row_1.clone().clear(); + } + + struct TestDB; + + #[async_trait] + impl DatabaseConnectionTrait for TestDB { + async fn query_account_profiles( + &self, + sql_stmt: String, + accounts: Vec, + ) -> Result>, tokio_postgres::Error> { + assert_eq!(sql_stmt, select_account_profiles_by_db_cr_accounts()); + assert_eq!(accounts, vec!["a".to_string(), "b".to_string()]); + Ok(vec![]) + } + + async fn query_approvers( + &self, + sql_stmt: String, + account: String, + ) -> Result>, tokio_postgres::Error> { + assert_eq!(sql_stmt, select_approvers()); + assert_eq!(account, "a".to_string()); + Ok(vec![]) + } + + async fn query_profile_state_rule_instances( + &self, + sql_stmt: String, + account_role: AccountRole, + state_name: String, + ) -> Result>, tokio_postgres::Error> { + assert_eq!(sql_stmt, select_rule_instance_by_type_role_state()); + assert_eq!(account_role, AccountRole::Creditor); + assert_eq!(state_name, "a".to_string()); + Ok(vec![]) + } + + async fn query_rule_instances_by_type_role_account( + &self, + sql_stmt: String, + account_role: AccountRole, + account: String, + ) -> Result>, tokio_postgres::Error> { + assert_eq!(sql_stmt, select_rule_instance_by_type_role_account()); + assert_eq!(account_role, AccountRole::Creditor); + assert_eq!(account, "a".to_string()); + Ok(vec![]) + } + + async fn query_approval_rule_instances( + &self, + sql_stmt: String, + account_role: AccountRole, + account: String, + ) -> Result>, tokio_postgres::Error> { + assert_eq!(sql_stmt, select_rule_instance_by_type_role_account()); + assert_eq!(account_role, AccountRole::Creditor); + assert_eq!(account, "a".to_string()); + Ok(vec![]) + } + } + + #[test] + fn get_account_profiles_called_with_args() { + let test_conn = Conn(Arc::new(TestDB)); + let accounts = vec!["a".to_string(), "b".to_string()]; + let _ = test_conn.get_account_profiles(accounts); + } + + #[test] + fn get_approvers_for_account_called_with_args() { + let test_conn = Conn(Arc::new(TestDB)); + let account = "a".to_string(); + let _ = test_conn.get_approvers_for_account(account); + } + + #[test] + fn get_profile_state_rule_instances_called_with_args() { + let test_conn = Conn(Arc::new(TestDB)); + let account_role = AccountRole::Creditor; + let state_name = "a".to_string(); + let _ = test_conn.get_profile_state_rule_instances(account_role, state_name); + } + + #[test] + fn get_rule_instances_by_type_role_account_called_with_args() { + let test_conn = Conn(Arc::new(TestDB)); + let account_role = AccountRole::Creditor; + let account = "a".to_string(); + let _ = test_conn.get_rule_instances_by_type_role_account(account_role, account); + } + + #[test] + fn get_approval_rule_instances_called_with_args() { + let test_conn = Conn(Arc::new(TestDB)); + let account_role = AccountRole::Creditor; + let account = "a".to_string(); + let _ = test_conn.get_approval_rule_instances(account_role, account); } } diff --git a/docker/prod/rule-base.Dockerfile b/docker/prod/rule-base.Dockerfile index 6e9ad23a..4caafd30 100644 --- a/docker/prod/rule-base.Dockerfile +++ b/docker/prod/rule-base.Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.67-slim +FROM rust:1.75-slim WORKDIR /app diff --git a/project.yaml b/project.yaml index 271715aa..54842cb6 100644 --- a/project.yaml +++ b/project.yaml @@ -30,7 +30,7 @@ crates: params: [] pg: runtime: rust1.x - min_code_cov: null + min_code_cov: 40 type: lib env_var: null params: [] @@ -411,7 +411,7 @@ services: - PGDATABASE rule: runtime: provided.al2 - min_code_cov: 66 + min_code_cov: 63 type: app local_dev: true params: @@ -431,6 +431,9 @@ services: RUST_LOG: ssm: null default: info + RUST_BACKTRACE: + ssm: null + default: 1 get: - PGDATABASE - PGUSER @@ -445,6 +448,7 @@ services: - RULE_PORT - HOSTNAME_OR_IP - RUST_LOG + - RUST_BACKTRACE request-by-id: runtime: go1.x min_code_cov: null @@ -650,6 +654,13 @@ test: type: tool deploy: false env_var: + set: + SILENCE_EXEC_LOGS: + ssm: null + default: true + NODE_NO_WARNINGS: + ssm: null + default: 1 get: - GRAPHQL_URI - RULE_URL @@ -660,6 +671,8 @@ test: - TRANSACTIONS_BY_ACCOUNT_URL - TRANSACTION_BY_ID_URL - BALANCE_BY_ACCOUNT_URL + - SILENCE_EXEC_LOGS + - NODE_NO_WARNINGS params: [] pkg: lambda: diff --git a/services/request-create/cmd/main.go b/services/request-create/cmd/main.go index 92baaa45..faedf1e9 100644 --- a/services/request-create/cmd/main.go +++ b/services/request-create/cmd/main.go @@ -158,6 +158,7 @@ func testValues( log.Print("client request equal to rule response") // add authenticated values to rule tested transaction + // cadet todo: add AddAuthValues method to Transaction ruleTested.Transaction.Author = &e.AuthAccount ruleTested.Transaction.AuthorRole = &authorRole diff --git a/services/rule/src/main.rs b/services/rule/src/main.rs index fea98905..f7f96dc3 100644 --- a/services/rule/src/main.rs +++ b/services/rule/src/main.rs @@ -20,7 +20,7 @@ mod rules; const READINESS_CHECK_PATH: &str = "READINESS_CHECK_PATH"; async fn apply_transaction_item_rules( - conn: &DynDBConn, + conn: DynDBConn, role_sequence: RoleSequence, transaction_items: &TransactionItems, ) -> TransactionItems { @@ -106,7 +106,7 @@ async fn apply_transaction_item_rules( } async fn apply_approval_rules( - conn: &DynDBConn, + conn: DynDBConn, role_sequence: RoleSequence, transaction_items: &mut TransactionItems, approval_time: &TZTime, @@ -195,16 +195,16 @@ async fn apply_rules( } // get connection from pool - let conn = pool.get_conn().await; + let conn = pool.get_conn().await as DynDBConn; let mut rule_applied_tr_items = - apply_transaction_item_rules(&conn, role_sequence, &transaction_items).await; + apply_transaction_item_rules(conn.clone(), role_sequence, &transaction_items).await; // create an approval time to be used for all automated approvals let approval_time = TZTime::now(); apply_approval_rules( - &conn, + conn, role_sequence, &mut rule_applied_tr_items, &approval_time, @@ -575,7 +575,7 @@ mod tests { ]); // test function - let got = apply_transaction_item_rules(&db_conn_stub, DEBITOR_FIRST, &tr_items).await; + let got = apply_transaction_item_rules(db_conn_stub, DEBITOR_FIRST, &tr_items).await; // assert #1: // save length of transaction items vec @@ -710,7 +710,7 @@ mod tests { // test function apply_approval_rules( - &db_conn_stub, + db_conn_stub, DEBITOR_FIRST, &mut got_tr_items, &test_approval_time, diff --git a/test/package.json b/test/package.json index 0f0aa92f..3c3d178d 100644 --- a/test/package.json +++ b/test/package.json @@ -4,10 +4,10 @@ "description": "mxfactorial docker integration tests", "main": "index.js", "scripts": { - "test:local": "eval $(cat .env) SILENCE_EXEC_LOGS=true jest --config config/local/jest.local.config.ts --runInBand --detectOpenHandles", - "test:up": "eval $(cat .env) SILENCE_EXEC_LOGS=true jest --config config/docker/jest.docker.config.ts --runInBand --detectOpenHandles", - "test:docker": "eval $(cat .env) SILENCE_EXEC_LOGS=true jest --config config/docker/jest.docker.base.config.ts --runInBand --detectOpenHandles", - "test:cloud": "eval $(cat .env) SILENCE_EXEC_LOGS=true jest --config config/cloud/jest.cloud.config.ts --runInBand --detectOpenHandles" + "test:local": "eval $(cat .env) jest --config config/local/jest.local.config.ts --runInBand --detectOpenHandles", + "test:up": "eval $(cat .env) jest --config config/docker/jest.docker.config.ts --runInBand --detectOpenHandles", + "test:docker": "eval $(cat .env) jest --config config/docker/jest.docker.base.config.ts --runInBand --detectOpenHandles", + "test:cloud": "eval $(cat .env) jest --config config/cloud/jest.cloud.config.ts --runInBand --detectOpenHandles" }, "keywords": [], "author": "", @@ -27,4 +27,4 @@ "ts-node": "^10.9.1", "typescript": "^4.9.4" } -} +} \ No newline at end of file diff --git a/test/thunder-tests/thunderclient.json b/test/thunder-tests/thunderclient.json index 631545fd..5edd8480 100644 --- a/test/thunder-tests/thunderclient.json +++ b/test/thunder-tests/thunderclient.json @@ -326,16 +326,15 @@ "colId": "ad756c83-e4e1-4c6e-a602-fbaee6aaab6c", "containerId": "", "name": "rule healthcheck", - "url": "http://0.0.0.0:8080/healthz", + "url": "http://[::1]:10001/healthz", "method": "GET", "sortNum": 20000, "created": "2023-03-10T06:03:42.460Z", - "modified": "2023-03-10T06:03:42.460Z", + "modified": "2023-12-17T23:57:13.678Z", "headers": [ { - "name": "Accept", - "value": "*/*", - "isDisabled": true + "name": "content-length", + "value": "0" }, { "name": "User-Agent", @@ -351,11 +350,11 @@ "colId": "ad756c83-e4e1-4c6e-a602-fbaee6aaab6c", "containerId": "", "name": "rule", - "url": "{{RULE_URL}}", + "url": "http://[::1]:10001", "method": "POST", "sortNum": 30000, "created": "2023-03-11T06:01:49.273Z", - "modified": "2023-05-10T21:52:13.267Z", + "modified": "2023-12-17T23:57:55.243Z", "headers": [ { "name": "Content-Type",