From cc1d04744f7f4da045d6e994b19d501c3e692c9d Mon Sep 17 00:00:00 2001 From: max funk Date: Thu, 28 Dec 2023 20:17:43 -0700 Subject: [PATCH 01/12] add cargo test dep --- crates/pg/Cargo.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 From 1c5166d2ff6c71672c1afcac1c52c049d3d0c7bc Mon Sep 17 00:00:00 2001 From: max funk Date: Thu, 28 Dec 2023 20:18:05 -0700 Subject: [PATCH 02/12] cargo lockfile --- Cargo.lock | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) 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" From 5841c7a1541213f744f53250c350e416986f06c9 Mon Sep 17 00:00:00 2001 From: max funk Date: Thu, 28 Dec 2023 20:18:40 -0700 Subject: [PATCH 03/12] add dependency injection, stubs and unit tests to pg crate --- crates/pg/src/lib.rs | 715 +++++++++++++++++++++++++++++++------- services/rule/src/main.rs | 14 +- 2 files changed, 592 insertions(+), 137 deletions(-) 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/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, From 3338a4d28d6c7b1dd3199b1e752402a8c54fc933 Mon Sep 17 00:00:00 2001 From: max funk Date: Thu, 28 Dec 2023 20:19:27 -0700 Subject: [PATCH 04/12] min pg crate test coverage --- .github/workflows/dev-crates.yaml | 10 ++++++++++ project.yaml | 15 ++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) 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/project.yaml b/project.yaml index 271715aa..6ef154d3 100644 --- a/project.yaml +++ b/project.yaml @@ -30,7 +30,7 @@ crates: params: [] pg: runtime: rust1.x - min_code_cov: null + min_code_cov: 41 type: lib env_var: null 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: From e0c8ce885fa7007bb87e6c636622894f2015c062 Mon Sep 17 00:00:00 2001 From: max funk Date: Thu, 28 Dec 2023 20:19:56 -0700 Subject: [PATCH 05/12] readme faq --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 From b3a65add7737744dafa36e98c484227ef187d8f9 Mon Sep 17 00:00:00 2001 From: max funk Date: Thu, 28 Dec 2023 20:20:10 -0700 Subject: [PATCH 06/12] comment cadet go task --- services/request-create/cmd/main.go | 1 + 1 file changed, 1 insertion(+) 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 From bdbebece452be94dd354966195d14c6e854062ea Mon Sep 17 00:00:00 2001 From: max funk Date: Thu, 28 Dec 2023 20:21:11 -0700 Subject: [PATCH 07/12] remove integration test var --- test/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 From c46c4a2a54d44dff59bd3d30d6d5b606362f07c2 Mon Sep 17 00:00:00 2001 From: max funk Date: Thu, 28 Dec 2023 20:21:47 -0700 Subject: [PATCH 08/12] ipv6 in client editor request --- test/thunder-tests/thunderclient.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) 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", From e874951f72e8a073fd9c91ce5e9647be699c5ca7 Mon Sep 17 00:00:00 2001 From: max funk Date: Thu, 28 Dec 2023 20:21:53 -0700 Subject: [PATCH 09/12] editor config --- .vscode/settings.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 From a0cac03b592d26d9021711975147b12ba33c5c80 Mon Sep 17 00:00:00 2001 From: max funk Date: Thu, 28 Dec 2023 20:26:50 -0700 Subject: [PATCH 10/12] 40 percent pg crate min code coverage --- project.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.yaml b/project.yaml index 6ef154d3..e98fb5ba 100644 --- a/project.yaml +++ b/project.yaml @@ -30,7 +30,7 @@ crates: params: [] pg: runtime: rust1.x - min_code_cov: 41 + min_code_cov: 40 type: lib env_var: null params: [] From 532898baebfcb823467e1e045ab540f7b96d033b Mon Sep 17 00:00:00 2001 From: max funk Date: Thu, 28 Dec 2023 20:28:54 -0700 Subject: [PATCH 11/12] 63 percent rule service min code coverage --- project.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.yaml b/project.yaml index e98fb5ba..54842cb6 100644 --- a/project.yaml +++ b/project.yaml @@ -411,7 +411,7 @@ services: - PGDATABASE rule: runtime: provided.al2 - min_code_cov: 66 + min_code_cov: 63 type: app local_dev: true params: From 99d4cd2e59eab299f9e179032740064eb6062f5a Mon Sep 17 00:00:00 2001 From: max funk Date: Thu, 28 Dec 2023 21:19:02 -0700 Subject: [PATCH 12/12] current rust in base docker image --- docker/prod/rule-base.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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