From 69bf0b5dffb2235904c46c1d969b9451f667cc91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Thu, 9 Jun 2022 15:28:15 +0200 Subject: [PATCH] More tests, documentation and prepare to add other payment methods --- Cargo.lock | 30 +- actors/database_manager/Cargo.toml | 2 + .../database_manager/src/account_addresses.rs | 2 +- actors/database_manager/src/accounts.rs | 14 +- actors/database_manager/src/lib.rs | 70 +++- .../database_manager/src/order_addresses.rs | 6 +- actors/database_manager/src/order_items.rs | 4 +- actors/database_manager/src/orders.rs | 10 +- actors/database_manager/src/photos.rs | 6 +- actors/database_manager/src/product_photos.rs | 6 +- actors/database_manager/src/products.rs | 8 +- .../src/shopping_cart_items.rs | 10 +- actors/database_manager/src/shopping_carts.rs | 8 +- actors/database_manager/src/stocks.rs | 10 +- actors/database_manager/src/tokens.rs | 35 +- actors/payment_manager/src/lib.rs | 82 ++--- .../payment_manager/src/pay_u_adapter/mod.rs | 69 ++++ actors/token_manager/Cargo.toml | 3 + actors/token_manager/src/lib.rs | 304 +++++++++++++++--- api/src/routes/admin/api_v1/mod.rs | 2 +- api/src/routes/public/api_v1/restricted.rs | 2 +- api/src/routes/public/api_v1/unrestricted.rs | 4 +- scripts/test.sh | 2 +- shared/model/src/lib.rs | 16 +- shared/testx/src/lib.rs | 15 +- 25 files changed, 554 insertions(+), 166 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dfdca3b..3eff9ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -559,9 +559,9 @@ checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" [[package]] name = "async-trait" -version = "0.1.53" +version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" dependencies = [ "proc-macro2", "quote", @@ -1183,6 +1183,7 @@ version = "0.1.0" dependencies = [ "actix 0.13.0", "actix-rt", + "async-trait", "chrono", "config", "fake", @@ -3127,11 +3128,11 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.38" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -4003,13 +4004,13 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.94" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -4219,6 +4220,7 @@ dependencies = [ "rand_core", "serde", "sha2", + "testx", "thiserror", "tokio", "tracing", @@ -4565,6 +4567,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +[[package]] +name = "unicode-ident" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" + [[package]] name = "unicode-normalization" version = "0.1.19" @@ -4580,12 +4588,6 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" -[[package]] -name = "unicode-xid" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" - [[package]] name = "unicode_categories" version = "0.1.1" diff --git a/actors/database_manager/Cargo.toml b/actors/database_manager/Cargo.toml index 36fb64d..4e00a21 100644 --- a/actors/database_manager/Cargo.toml +++ b/actors/database_manager/Cargo.toml @@ -31,5 +31,7 @@ itertools = { version = "0.10.3" } serde = { version = "1.0", features = ["derive"] } +async-trait = { version = "0.1.56" } + [dev-dependencies] testx = { path = "../../shared/testx" } diff --git a/actors/database_manager/src/account_addresses.rs b/actors/database_manager/src/account_addresses.rs index a35f535..50e651a 100644 --- a/actors/database_manager/src/account_addresses.rs +++ b/actors/database_manager/src/account_addresses.rs @@ -248,7 +248,7 @@ mod test { #[actix::test] async fn full_check() { - testx::db_t!(t); + testx::db_t_ref!(t); // account let account = test_create_account(&mut t).await; diff --git a/actors/database_manager/src/accounts.rs b/actors/database_manager/src/accounts.rs index d28463a..a1f3a0f 100644 --- a/actors/database_manager/src/accounts.rs +++ b/actors/database_manager/src/accounts.rs @@ -268,7 +268,7 @@ mod tests { #[actix::test] async fn create_account() { - testx::db_t!(t); + testx::db_t_ref!(t); let login: String = fake::faker::internet::en::Username().fake(); let email: String = fake::faker::internet::en::FreeEmail().fake(); @@ -302,7 +302,7 @@ mod tests { #[actix::test] async fn all_accounts() { - testx::db_t!(t); + testx::db_t_ref!(t); test_create_account(&mut t, None, None, None).await; test_create_account(&mut t, None, None, None).await; @@ -316,7 +316,7 @@ mod tests { #[actix::test] async fn update_account_without_pass() { - testx::db_t!(t); + testx::db_t_ref!(t); let original_login: String = fake::faker::internet::en::Username().fake(); let original_email: String = fake::faker::internet::en::FreeEmail().fake(); @@ -364,7 +364,7 @@ mod tests { #[actix::test] async fn update_account_with_pass() { - testx::db_t!(t); + testx::db_t_ref!(t); let original_login: String = fake::faker::internet::en::Username().fake(); let original_email: String = fake::faker::internet::en::FreeEmail().fake(); @@ -413,7 +413,7 @@ mod tests { #[actix::test] async fn find() { - testx::db_t!(t); + testx::db_t_ref!(t); let account = test_create_account(&mut t, None, None, None).await; @@ -432,7 +432,7 @@ mod tests { #[actix::test] async fn find_identity_email() { - testx::db_t!(t); + testx::db_t_ref!(t); let account = test_create_account(&mut t, None, None, None).await; @@ -452,7 +452,7 @@ mod tests { #[actix::test] async fn find_identity_login() { - testx::db_t!(t); + testx::db_t_ref!(t); let account = test_create_account(&mut t, None, None, None).await; diff --git a/actors/database_manager/src/lib.rs b/actors/database_manager/src/lib.rs index bb974f2..24d8808 100644 --- a/actors/database_manager/src/lib.rs +++ b/actors/database_manager/src/lib.rs @@ -72,6 +72,16 @@ macro_rules! db_async_handler { Box::pin(async { $inner_async(msg, pool).await }.into_actor(self)) } } + + #[async_trait::async_trait] + impl $crate::Queue<$msg> for crate::Transaction { + type Result = $res; + + async fn handle(&mut self, msg: $msg) -> Result { + let t = &mut self.t; + $async(msg, t).await + } + } }; } @@ -159,6 +169,7 @@ pub type Result = std::result::Result; pub struct Database { pool: PgPool, + config: SharedAppConfig, } pub type SharedDatabase = actix::Addr; @@ -167,6 +178,7 @@ impl Clone for Database { fn clone(&self) -> Self { Self { pool: self.pool.clone(), + config: self.config.clone(), } } } @@ -178,18 +190,55 @@ impl Database { tracing::error!("Failed to connect to database. {e:?}"); std::process::exit(1); }); - Database { pool } + Self { pool, config } } pub fn pool(&self) -> &PgPool { &self.pool } + + pub async fn begin(&self) -> sqlx::Result { + Ok(Transaction { + t: self.pool.begin().await?, + }) + } } impl Actor for Database { type Context = Context; } +pub struct Transaction { + t: sqlx::Transaction<'static, sqlx::Postgres>, +} + +impl Transaction { + pub async fn commit(self) -> Result<()> { + self.t.commit().await.map_err(|e| { + tracing::error!("{e:?}"); + dbg!(e); + Error::TransactionFailed + })?; + Ok(()) + } + + pub async fn rollback(self) -> Result<()> { + self.t.rollback().await.map_err(|e| { + tracing::error!("{e:?}"); + dbg!(e); + Error::TransactionFailed + })?; + Ok(()) + } +} + +#[async_trait::async_trait] +pub trait Queue { + type Result; + + async fn handle(&mut self, msg: Msg) -> Result; +} + /// Multi-query load for large amount of records to read /// /// Examples @@ -203,7 +252,11 @@ impl Actor for Database { /// &mut t, /// "SELECT id, name FROM products WHERE ", /// " id = " -/// ); +/// ) +/// // order by id +/// .with_sorting("id ASC") +/// // 100 rows per db query +/// .with_size(100); /// let products: Vec = multi.load(4, vec![1, 2, 3, 4].into_iter(), |_| Error::All.into()) /// .await.unwrap(); /// t.commit().await.unwrap(); @@ -214,6 +267,7 @@ pub struct MultiLoad<'transaction, 'transaction2, 'header, 'condition, T> { header: &'header str, condition: &'condition str, sort: Option, + size: usize, __phantom: std::marker::PhantomData, } @@ -232,6 +286,7 @@ where header, condition, sort: None, + size: 20, __phantom: Default::default(), } } @@ -241,6 +296,11 @@ where self } + pub fn with_size(mut self, size: usize) -> Self { + self.size = size; + self + } + pub async fn load<'query, Error, Ids>( &mut self, len: usize, @@ -252,12 +312,14 @@ where Error: Fn(sqlx::Error) -> crate::Error, { let mut res = Vec::new(); + let size = self.size; for ids in items.fold( Vec::>::with_capacity(len), |mut v, id| { - if matches!(v.last().map(|v| v.len()), Some(20) | None) { - v.push(Vec::with_capacity(20)); + let last_len = v.last().map(|v| v.len()); + if last_len == Some(size) || last_len == None { + v.push(Vec::with_capacity(size)); } v.last_mut().unwrap().push(id); v diff --git a/actors/database_manager/src/order_addresses.rs b/actors/database_manager/src/order_addresses.rs index 6e6008f..5f6684b 100644 --- a/actors/database_manager/src/order_addresses.rs +++ b/actors/database_manager/src/order_addresses.rs @@ -184,7 +184,7 @@ mod tests { #[actix::test] async fn create() { - testx::db_t!(t); + testx::db_t_ref!(t); test_order_address(&mut t).await; @@ -193,7 +193,7 @@ mod tests { #[actix::test] async fn update() { - testx::db_t!(t); + testx::db_t_ref!(t); let original = test_order_address(&mut t).await; let updated = super::update_order_address( @@ -231,7 +231,7 @@ mod tests { } async fn order_address() { - testx::db_t!(t); + testx::db_t_ref!(t); test_order_address(&mut t).await; diff --git a/actors/database_manager/src/order_items.rs b/actors/database_manager/src/order_items.rs index 59a3bcf..b1e270b 100644 --- a/actors/database_manager/src/order_items.rs +++ b/actors/database_manager/src/order_items.rs @@ -284,7 +284,7 @@ mod tests { #[actix::test] async fn create() { - testx::db_t!(t); + testx::db_t_ref!(t); test_order_item(&mut t, None).await; @@ -293,7 +293,7 @@ mod tests { #[actix::test] async fn order_items() { - testx::db_t!(t); + testx::db_t_ref!(t); let order1 = test_order(&mut t).await; test_order_item(&mut t, Some(order1.id)).await; diff --git a/actors/database_manager/src/orders.rs b/actors/database_manager/src/orders.rs index 14a47c7..1697f4c 100644 --- a/actors/database_manager/src/orders.rs +++ b/actors/database_manager/src/orders.rs @@ -404,7 +404,7 @@ mod tests { #[actix::test] async fn empty_order_without_cart() { - testx::db_t!(t); + testx::db_t_ref!(t); let address_id = test_order_address(&mut t).await.id; test_empty_order_without_cart(&mut t, None, address_id).await; @@ -414,7 +414,7 @@ mod tests { #[actix::test] async fn empty_order_with_cart() { - testx::db_t!(t); + testx::db_t_ref!(t); let buyer_id = test_account(&mut t).await.id; let address_id = test_order_address(&mut t).await.id; @@ -440,7 +440,7 @@ mod tests { #[actix::test] async fn non_empty_order_with_cart() { - testx::db_t!(t); + testx::db_t_ref!(t); let buyer_id = test_account(&mut t).await.id; let address_id = test_order_address(&mut t).await.id; @@ -477,7 +477,7 @@ mod tests { #[actix::test] async fn non_empty_order_without_cart() { - testx::db_t!(t); + testx::db_t_ref!(t); let buyer_id = test_account(&mut t).await.id; let address_id = test_order_address(&mut t).await.id; @@ -510,7 +510,7 @@ mod tests { #[actix::test] async fn update_by_ext() { - testx::db_t!(t); + testx::db_t_ref!(t); let address_id = test_order_address(&mut t).await.id; let original = test_empty_order_without_cart(&mut t, None, address_id).await; diff --git a/actors/database_manager/src/photos.rs b/actors/database_manager/src/photos.rs index f6aa9ad..3ed739d 100644 --- a/actors/database_manager/src/photos.rs +++ b/actors/database_manager/src/photos.rs @@ -192,7 +192,7 @@ mod tests { #[actix::test] async fn create_photo() { - testx::db_t!(t); + testx::db_t_ref!(t); test_photo(&mut t, None, None, None).await; @@ -201,7 +201,7 @@ mod tests { #[actix::test] async fn all() { - testx::db_t!(t); + testx::db_t_ref!(t); let p1 = test_photo(&mut t, None, None, None).await; let p2 = test_photo(&mut t, None, None, None).await; @@ -215,7 +215,7 @@ mod tests { #[actix::test] async fn products_photos() { - testx::db_t!(t); + testx::db_t_ref!(t); let product_1 = test_product(&mut t).await; let p1 = test_product_photo(&mut t, product_1.id).await; diff --git a/actors/database_manager/src/product_photos.rs b/actors/database_manager/src/product_photos.rs index f4c2241..ed4bf86 100644 --- a/actors/database_manager/src/product_photos.rs +++ b/actors/database_manager/src/product_photos.rs @@ -165,7 +165,7 @@ mod tests { #[actix::test] async fn create_photo() { - testx::db_t!(t); + testx::db_t_ref!(t); test_product_photo(&mut t).await; @@ -174,7 +174,7 @@ mod tests { #[actix::test] async fn delete() { - testx::db_t!(t); + testx::db_t_ref!(t); let p1 = test_product_photo(&mut t).await; let p2 = test_product_photo(&mut t).await; @@ -192,7 +192,7 @@ mod tests { #[actix::test] async fn create() { - testx::db_t!(t); + testx::db_t_ref!(t); test_product_photo(&mut t).await; diff --git a/actors/database_manager/src/products.rs b/actors/database_manager/src/products.rs index bc802ff..f538927 100644 --- a/actors/database_manager/src/products.rs +++ b/actors/database_manager/src/products.rs @@ -360,7 +360,7 @@ mod tests { #[actix::test] async fn create() { - testx::db_t!(t); + testx::db_t_ref!(t); test_product(&mut t, None, None, None, None, None, None).await; @@ -369,7 +369,7 @@ mod tests { #[actix::test] async fn all() { - testx::db_t!(t); + testx::db_t_ref!(t); let p1 = test_product(&mut t, None, None, None, None, None, None).await; let p2 = test_product(&mut t, None, None, None, None, None, None).await; @@ -383,7 +383,7 @@ mod tests { #[actix::test] async fn find() { - testx::db_t!(t); + testx::db_t_ref!(t); let p1 = test_product(&mut t, None, None, None, None, None, None).await; let p2 = test_product(&mut t, None, None, None, None, None, None).await; @@ -401,7 +401,7 @@ mod tests { #[actix::test] async fn update() { - testx::db_t!(t); + testx::db_t_ref!(t); let original = test_product(&mut t, None, None, None, None, None, None).await; let updated = update_product( diff --git a/actors/database_manager/src/shopping_cart_items.rs b/actors/database_manager/src/shopping_cart_items.rs index 634125e..18b393a 100644 --- a/actors/database_manager/src/shopping_cart_items.rs +++ b/actors/database_manager/src/shopping_cart_items.rs @@ -541,7 +541,7 @@ WHERE buyer_id = $1 #[actix::test] async fn create() { - testx::db_t!(t); + testx::db_t_ref!(t); test_shopping_cart_item(&mut t, None, None).await; @@ -550,7 +550,7 @@ WHERE buyer_id = $1 #[actix::test] async fn all() { - testx::db_t!(t); + testx::db_t_ref!(t); let account_id = test_account(&mut t, None, None, None).await.id; @@ -581,7 +581,7 @@ WHERE buyer_id = $1 #[actix::test] async fn account_cart_with_cart_id() { - testx::db_t!(t); + testx::db_t_ref!(t); let account_id = test_account(&mut t, None, None, None).await.id; @@ -618,7 +618,7 @@ WHERE buyer_id = $1 #[actix::test] async fn account_cart_without_cart_id() { - testx::db_t!(t); + testx::db_t_ref!(t); let account_id = test_account(&mut t, None, None, None).await.id; @@ -655,7 +655,7 @@ WHERE buyer_id = $1 #[actix::test] async fn update() { - testx::db_t!(t); + testx::db_t_ref!(t); let account_id = test_account(&mut t, None, None, None).await.id; let cart1 = test_shopping_cart(&mut t, Some(account_id), ShoppingCartState::Closed).await; let item = test_shopping_cart_item(&mut t, Some(cart1.id), None).await; diff --git a/actors/database_manager/src/shopping_carts.rs b/actors/database_manager/src/shopping_carts.rs index 27ea8c8..2895140 100644 --- a/actors/database_manager/src/shopping_carts.rs +++ b/actors/database_manager/src/shopping_carts.rs @@ -345,7 +345,7 @@ mod tests { #[actix::test] async fn create_shopping_cart() { - testx::db_t!(t); + testx::db_t_ref!(t); let account = test_account(&mut t, None, None, None).await; @@ -364,7 +364,7 @@ mod tests { #[actix::test] async fn update_shopping_cart() { - testx::db_t!(t); + testx::db_t_ref!(t); let account = test_account(&mut t, None, None, None).await; @@ -399,7 +399,7 @@ mod tests { #[actix::test] async fn without_cart_ensure_shopping_cart() { - testx::db_t!(t); + testx::db_t_ref!(t); let account = test_account(&mut t, None, None, None).await; @@ -429,7 +429,7 @@ mod tests { #[actix::test] async fn with_inactive_cart_ensure_shopping_cart() { - testx::db_t!(t); + testx::db_t_ref!(t); let account = test_account(&mut t, None, None, None).await; diff --git a/actors/database_manager/src/stocks.rs b/actors/database_manager/src/stocks.rs index 91b293e..12e6a08 100644 --- a/actors/database_manager/src/stocks.rs +++ b/actors/database_manager/src/stocks.rs @@ -267,7 +267,7 @@ mod tests { #[actix::test] async fn create_stock() { - testx::db_t!(t); + testx::db_t_ref!(t); test_stock(&mut t, None, None, None).await; @@ -276,7 +276,7 @@ mod tests { #[actix::test] async fn products_stock() { - testx::db_t!(t); + testx::db_t_ref!(t); let first = test_stock(&mut t, None, None, None).await; let second = test_stock(&mut t, None, None, None).await; @@ -296,7 +296,7 @@ mod tests { #[actix::test] async fn all_stocks() { - testx::db_t!(t); + testx::db_t_ref!(t); let first = test_stock(&mut t, None, None, None).await; let second = test_stock(&mut t, None, None, None).await; @@ -309,7 +309,7 @@ mod tests { #[actix::test] async fn delete_stock() { - testx::db_t!(t); + testx::db_t_ref!(t); let first = test_stock(&mut t, None, None, None).await; let second = test_stock(&mut t, None, None, None).await; @@ -332,7 +332,7 @@ mod tests { #[actix::test] async fn update_stock() { - testx::db_t!(t); + testx::db_t_ref!(t); let first = test_stock(&mut t, None, None, None).await; let second = test_stock(&mut t, None, None, None).await; diff --git a/actors/database_manager/src/tokens.rs b/actors/database_manager/src/tokens.rs index 409030b..e69005d 100644 --- a/actors/database_manager/src/tokens.rs +++ b/actors/database_manager/src/tokens.rs @@ -1,5 +1,5 @@ use actix::Message; -use model::{AccountId, Audience, Token}; +use model::{AccountId, Audience, Token, TokenId}; use crate::{db_async_handler, Result}; @@ -138,6 +138,31 @@ RETURNING id, customer_id, role, issuer, subject, audience, expiration_time, not crate::Error::Token(Error::Create) }) } +#[derive(Message)] +#[rtype(result = "Result>")] +pub struct DeleteToken { + pub token_id: TokenId, +} + +db_async_handler!(DeleteToken, delete_token, Option, inner_delete_token); + +pub(crate) async fn delete_token( + msg: DeleteToken, + t: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result> { + sqlx::query_as(r#" +DELETE FROM tokens +WHERE id = $1 +RETURNING id, customer_id, role, issuer, subject, audience, expiration_time, not_before_time, issued_at_time, jwt_id + "#) + .bind(msg.token_id) + .fetch_optional(t) + .await + .map_err(|e| { + tracing::error!("{e:?}"); + crate::Error::Token(Error::Jti) + }) +} #[cfg(test)] mod tests { @@ -205,7 +230,7 @@ mod tests { #[actix::test] async fn create_token() { - testx::db_t!(t); + testx::db_t_ref!(t); super::create_token( CreateToken { @@ -222,7 +247,7 @@ mod tests { #[actix::test] async fn create_extended_token() { - testx::db_t!(t); + testx::db_t_ref!(t); test_create_account(&mut t).await; @@ -231,7 +256,7 @@ mod tests { #[actix::test] async fn find_by_jti() { - testx::db_t!(t); + testx::db_t_ref!(t); let original = test_create_token_extended(&mut t, None, None, None, None, None).await; @@ -250,7 +275,7 @@ mod tests { #[actix::test] async fn find_by_jti_expired() { - testx::db_t!(t); + testx::db_t_ref!(t); let original = test_create_token_extended( &mut t, diff --git a/actors/payment_manager/src/lib.rs b/actors/payment_manager/src/lib.rs index 11f83a4..0690502 100644 --- a/actors/payment_manager/src/lib.rs +++ b/actors/payment_manager/src/lib.rs @@ -217,7 +217,7 @@ pub(crate) async fn request_payment( }, Error::UnavailableShoppingCart ); - let mut items = + let items = cart_items .iter() .fold(HashMap::with_capacity(cart_items.len()), |mut agg, item| { @@ -233,60 +233,34 @@ pub(crate) async fn request_payment( Error::UnavailableShoppingCart ); - // let payment_required = { - // let l = config.lock(); - // l.payment().optional_payment() != false - // }; - let redirect_uri = { - let pay_u::res::CreateOrder { - status: _, - redirect_uri, - order_id, - ext_order_id: _, - } = { - client - .lock() - .create_order( - pay_u::req::OrderCreate::build( - msg.buyer.into(), - msg.customer_ip, - msg.currency, - format!("Order #{}", db_order.id), - ) - .map_err(|e| { - tracing::error!("{}", e); - Error::InvalidOrder - })? - .with_products(cart_products.into_iter().map(|p| { - pay_u::Product::new( - p.name.to_string(), - **p.price, - items - .remove(&p.id) - .map(|(quantity, _)| **quantity as u32) - .unwrap_or_default(), - ) - })) - .with_ext_order_id(db_order.order_ext_id.to_string()) - .with_notify_url(notify_uri) - .with_continue_url(continue_uri), - ) - .await - .map_err(|e| { - tracing::error!("{}", e); - Error::PaymentFailed - })? - }; + let redirect_uri = match msg.payment_method { + PaymentMethod::PayU => { + let (redirect_uri, ext_order_id) = pay_u_adapter::CreatePayment { + client, + buyer: msg.buyer, + customer_ip: msg.customer_ip, + currency: msg.currency, + description: format!("Order #{}", db_order.id), + cart_products, + items, + order_ext_id: db_order.order_ext_id.to_string(), + notify_uri, + continue_uri, + } + .create_payment() + .await?; - query_db!( - db, - database_manager::SetOrderServiceId { - service_order_id: order_id.0, - id: db_order.id, - }, - Error::CreateOrder - ); - redirect_uri + query_db!( + db, + database_manager::SetOrderServiceId { + service_order_id: ext_order_id.into_inner(), + id: db_order.id, + }, + Error::CreateOrder + ); + redirect_uri + } + PaymentMethod::PaymentOnTheSpot => unreachable!(), }; let order_items = query_db!( diff --git a/actors/payment_manager/src/pay_u_adapter/mod.rs b/actors/payment_manager/src/pay_u_adapter/mod.rs index e69de29..8d266c9 100644 --- a/actors/payment_manager/src/pay_u_adapter/mod.rs +++ b/actors/payment_manager/src/pay_u_adapter/mod.rs @@ -0,0 +1,69 @@ +use std::collections::HashMap; + +use model::*; + +use crate::{Buyer, Error, PayUClient, Result}; + +pub struct CreatePayment { + pub client: PayUClient, + pub buyer: Buyer, + pub customer_ip: String, + pub currency: String, + pub description: String, + pub cart_products: Vec, + pub items: HashMap, + pub order_ext_id: String, + pub notify_uri: String, + pub continue_uri: String, +} + +impl CreatePayment { + pub(crate) async fn create_payment(self) -> Result<(String, ExtOrderId)> { + let CreatePayment { + client, + buyer, + customer_ip, + currency, + description, + cart_products, + mut items, + order_ext_id, + notify_uri, + continue_uri, + } = self; + + let pay_u::res::CreateOrder { + status: _, + redirect_uri, + order_id, + ext_order_id: _, + } = client + .lock() + .create_order( + pay_u::req::OrderCreate::build(buyer.into(), customer_ip, currency, description) + .map_err(|e| { + tracing::error!("{}", e); + Error::InvalidOrder + })? + .with_products(cart_products.into_iter().map(|p| { + pay_u::Product::new( + p.name.to_string(), + **p.price, + items + .remove(&p.id) + .map(|(quantity, _)| **quantity as u32) + .unwrap_or_default(), + ) + })) + .with_ext_order_id(order_ext_id) + .with_notify_url(notify_uri) + .with_continue_url(continue_uri), + ) + .await + .map_err(|e| { + tracing::error!("{}", e); + Error::PaymentFailed + })?; + Ok((redirect_uri, ExtOrderId::new(order_id.0))) + } +} diff --git a/actors/token_manager/Cargo.toml b/actors/token_manager/Cargo.toml index 5b980c8..3e6437f 100644 --- a/actors/token_manager/Cargo.toml +++ b/actors/token_manager/Cargo.toml @@ -35,3 +35,6 @@ sha2 = { version = "0.10", features = [] } tokio = { version = "1.17", features = ["full"] } futures = { version = "0.3", features = [] } futures-util = { version = "0.3", features = [] } + +[dev-dependencies] +testx = { path = "../../shared/testx" } diff --git a/actors/token_manager/src/lib.rs b/actors/token_manager/src/lib.rs index 3802808..df2246c 100644 --- a/actors/token_manager/src/lib.rs +++ b/actors/token_manager/src/lib.rs @@ -1,3 +1,75 @@ +//! Tokens management system. +//! It's responsible for creating and validating all tokens. +//! +//! Application flow goes like this: +//! +//! ```ascii +//! Client API TokenManager Database +//! +//! │ │ │ │ +//! │ │ │ │ +//! │ │ │ ┌───────────────►│ +//! ├────────────────►├──────────────────►├──────►│ │ +//! │ Sign In │ CreatePair │ └───────────────►│ +//! │ │ │ Create │ +//! │ │ │ * AccessToken │ +//! │ │ │ * RefreshToken │ +//! │ │ │ │ +//! │ │ │ │ +//! +//! │ │ │ │ +//! ├────────────────►├──────────────────►├───────────────────────►│ +//! │ Validate token │ ValidateToken │ Load token │ +//! │ │ (string) │◄───────────────────────┤ +//! │ │ │ │ +//! │ │ Is Valid? │ +//! │ │ │ │ +//! │ │◄──────────────── YES │ +//! │ │ AccessToken │ │ +//! │ │ │ │ +//! │ │ │ │ +//! │ │◄──────────────── NO │ +//! │ │ Error │ │ +//! +//! │ │ │ │ +//! │ │ │ │ +//! │ │ │ ┌───────────────►│ +//! ├────────────────►├──────────────────►├──────►│ │ +//! │ Refresh token │ CreatePair │ └───────────────►│ +//! │ │ │ Create │ +//! │ │◄──────────────────┤ * AccessToken │ +//! │ │ Access Token │ * RefreshToken │ +//! │ │ Refresh Token │ │ +//! │ │ │ │ +//! ``` +//! +//! If you need to operate on tokens from API or any other actor you should +//! always use this actor and never touch database directly. +//! +//! # Examples +//! +//! ``` +//! use actix::{Actor, Addr}; +//! use config::SharedAppConfig; +//! use database_manager::Database; +//! use token_manager::*; +//! use model::*; +//! +//! async fn tokens(db: Addr, config: SharedAppConfig) { +//! let manager = TokenManager::new(config, db); +//! +//! let manager_addr = manager.start(); +//! +//! let AuthPair { access_token, access_token_string, refresh_token_string, .. } = manager_addr.send(CreatePair { +//! customer_id: uuid::Uuid::new_v4(), +//! account_id: AccountId::from(0), +//! role: Role::Admin +//! }).await.unwrap().unwrap(); +//! +//! manager_addr.send(Validate { token: access_token_string }).await.unwrap().unwrap(); +//! } +//! ``` + use std::collections::BTreeMap; use std::str::FromStr; @@ -138,6 +210,22 @@ impl TokenManager { } } +/// Creates single token, it's mostly used by [CreatePair] +/// +/// # Examples +/// +/// ``` +/// use actix::Addr; +/// use model::{AccountId, Role}; +/// use token_manager::*; +/// async fn create_pair(token_manager: Addr) { +/// match token_manager.send(CreateToken { customer_id: uuid::Uuid::new_v4(), role: Role::Admin, subject: AccountId::from(1), audience: None, exp: None }).await { +/// Ok(Ok(pair)) => {} +/// Ok(Err(manager_error)) => {} +/// Err(actor_error) => {} +/// } +/// } +/// ``` #[derive(Message)] #[rtype(result = "Result<(Token, AccessTokenString)>")] pub struct CreateToken { @@ -256,12 +344,28 @@ pub struct AuthPair { pub refresh_token_string: model::RefreshTokenString, } +/// Creates access token and refresh token +/// +/// # Examples +/// +/// ``` +/// use actix::Addr; +/// use model::{AccountId, Role}; +/// use token_manager::CreatePair; +/// async fn create_pair(token_manager: Addr) { +/// match token_manager.send(CreatePair { customer_id: uuid::Uuid::new_v4(), account_id: AccountId::from(0), role: Role::Admin }).await { +/// Ok(Ok(pair)) => {} +/// Ok(Err(manager_error)) => {} +/// Err(actor_error) => {} +/// } +/// } +/// ``` #[derive(Message)] #[rtype(result = "Result")] pub struct CreatePair { pub customer_id: uuid::Uuid, pub role: Role, - pub id: AccountId, + pub account_id: AccountId, } token_async_handler!(CreatePair, create_pair, AuthPair); @@ -276,7 +380,7 @@ pub(crate) async fn create_pair( CreateToken { customer_id: msg.customer_id, role: msg.role, - subject: msg.id, + subject: msg.account_id, audience: Some(model::Audience::Web), exp: None }, @@ -287,7 +391,7 @@ pub(crate) async fn create_pair( CreateToken { customer_id: msg.customer_id, role: msg.role, - subject: msg.id, + subject: msg.account_id, audience: Some(model::Audience::Web), exp: Some((chrono::Utc::now() + chrono::Duration::days(31)).naive_utc()) }, @@ -305,6 +409,22 @@ pub(crate) async fn create_pair( }) } +/// Checks if token is still valid +/// +/// # Examples +/// +/// ``` +/// use actix::Addr; +/// use model::{AccessTokenString, AccountId, Role}; +/// use token_manager::{CreatePair, Validate}; +/// async fn create_pair(token_manager: Addr, token: AccessTokenString) { +/// match token_manager.send(Validate { token }).await { +/// Ok(Ok(pair)) => {} +/// Ok(Err(manager_error)) => {} +/// Err(actor_error) => {} +/// } +/// } +/// ``` #[derive(Message)] #[rtype(result = "Result")] pub struct Validate { @@ -349,30 +469,14 @@ pub(crate) async fn validate( return Err(Error::Validate); } - if !validate_pair(&claims, "cti", token.customer_id, validate_uuid) { - return Err(Error::Invalid); - } - if !validate_pair(&claims, "arl", token.role, |left, right| right == left) { - return Err(Error::Invalid); - } - if !validate_pair(&claims, "iss", &token.issuer, |left, right| right == left) { - return Err(Error::Invalid); - } - if !validate_pair(&claims, "sub", token.subject, validate_num) { - return Err(Error::Invalid); - } - if !validate_pair(&claims, "aud", token.audience, |left, right| right == left) { - return Err(Error::Invalid); - } - if !validate_pair(&claims, "exp", &token.expiration_time, validate_time) { - return Err(Error::Invalid); - } - if !validate_pair(&claims, "nbt", &token.not_before_time, validate_time) { - return Err(Error::Invalid); - } - if !validate_pair(&claims, "iat", &token.issued_at_time, validate_time) { - return Err(Error::Invalid); - } + validate_pair(&claims, "cti", token.customer_id, validate_uuid)?; + validate_pair(&claims, "arl", token.role, eq)?; + validate_pair(&claims, "iss", &token.issuer, eq)?; + validate_pair(&claims, "sub", token.subject, validate_num)?; + validate_pair(&claims, "aud", token.audience, eq)?; + validate_pair(&claims, "exp", &token.expiration_time, validate_time)?; + validate_pair(&claims, "nbt", &token.not_before_time, validate_time)?; + validate_pair(&claims, "iat", &token.issued_at_time, validate_time)?; tracing::info!("JWT token valid"); Ok(token) @@ -383,31 +487,155 @@ fn build_key(secret: String) -> Result> { Ok(key) => Ok(key), Err(e) => { tracing::error!("{e:?}"); + dbg!(e); Err(Error::ValidateInternal) } } } -fn validate_pair(claims: &BTreeMap, key: &str, v: V, cmp: F) -> bool +#[inline(always)] +fn validate_pair( + claims: &BTreeMap, + key: &str, + v: V, + cmp: F, +) -> std::result::Result<(), Error> where - F: FnOnce(&str, V) -> bool, + F: for<'s> FnOnce(V, &'s str) -> bool, V: PartialEq, { - claims.get(key).map(|s| cmp(s, v)).unwrap_or_default() + claims + .get(key) + .map(|s| cmp(v, s.as_str())) + .unwrap_or_default() + .then_some(()) + .ok_or(Error::Invalid) } -fn validate_time(left: &str, right: &NaiveDateTime) -> bool { - chrono::DateTime::parse_from_str(left, "%+") - .map(|t| t.naive_utc() == *right) +#[inline(always)] +fn eq(value: V, text: &str) -> bool +where + V: for<'s> PartialEq<&'s str>, +{ + value == text +} + +#[inline(always)] +fn validate_time(left: &NaiveDateTime, right: &str) -> bool { + chrono::DateTime::parse_from_str(right, "%+") + .map(|t| t.naive_utc() == *left) .unwrap_or_default() } -fn validate_num(left: &str, right: i32) -> bool { - left.parse::().map(|n| n == right).unwrap_or_default() +#[inline(always)] +fn validate_num(left: i32, right: &str) -> bool { + right.parse::().map(|n| left == n).unwrap_or_default() } -fn validate_uuid(left: &str, right: uuid::Uuid) -> bool { - uuid::Uuid::from_str(left) - .map(|u| u == right) +#[inline(always)] +fn validate_uuid(left: uuid::Uuid, right: &str) -> bool { + uuid::Uuid::from_str(right) + .map(|u| u == left) .unwrap_or_default() } + +#[cfg(test)] +mod tests { + use actix::Actor; + use config::UpdateConfig; + use database_manager::Database; + use model::*; + + use super::*; + + pub struct NoOpts; + + impl UpdateConfig for NoOpts {} + + #[actix::test] + async fn create_token() { + testx::db!(config, db); + let db = db.start(); + + let (token, _text) = super::create_token( + CreateToken { + customer_id: Default::default(), + role: Role::Admin, + subject: AccountId::from(1), + audience: None, + exp: None, + }, + db.clone(), + config, + ) + .await + .unwrap(); + + db.send(database_manager::DeleteToken { token_id: token.id }) + .await + .ok(); + } + + #[actix::test] + async fn create_pair() { + testx::db!(config, db); + let db = db.start(); + + let AuthPair { + access_token, + access_token_string: _, + refresh_token_string: _, + _refresh_token, + } = super::create_pair( + CreatePair { + customer_id: Default::default(), + role: Role::Admin, + account_id: AccountId::from(0), + }, + db.clone(), + config, + ) + .await + .unwrap(); + + db.send(database_manager::DeleteToken { + token_id: access_token.id, + }) + .await + .ok(); + + db.send(database_manager::DeleteToken { + token_id: _refresh_token.id, + }) + .await + .ok(); + } + + #[actix::test] + async fn validate() { + testx::db!(config, db); + let db = db.start(); + + let (token, text) = super::create_token( + CreateToken { + customer_id: Default::default(), + role: Role::Admin, + subject: AccountId::from(1), + audience: None, + exp: None, + }, + db.clone(), + config.clone(), + ) + .await + .unwrap(); + + super::validate(Validate { token: text }, db.clone(), config.clone()) + .await + .unwrap(); + + db.send(database_manager::DeleteToken { token_id: token.id }) + .await + .ok(); + } +} diff --git a/api/src/routes/admin/api_v1/mod.rs b/api/src/routes/admin/api_v1/mod.rs index 868263b..e19b469 100644 --- a/api/src/routes/admin/api_v1/mod.rs +++ b/api/src/routes/admin/api_v1/mod.rs @@ -55,7 +55,7 @@ async fn sign_in( .send(token_manager::CreatePair { customer_id: account.customer_id, role: account.role, - id: account.id, + account_id: account.id, }) .await .map_err(|_| routes::Error::CriticalFailure)??; diff --git a/api/src/routes/public/api_v1/restricted.rs b/api/src/routes/public/api_v1/restricted.rs index 7235b4d..f50a1e6 100644 --- a/api/src/routes/public/api_v1/restricted.rs +++ b/api/src/routes/public/api_v1/restricted.rs @@ -52,7 +52,7 @@ async fn refresh_token( .send(token_manager::CreatePair { customer_id: account.customer_id, role: account.role, - id: account.id, + account_id: account.id, }) .await .map_err(|_| routes::Error::CriticalFailure)??; diff --git a/api/src/routes/public/api_v1/unrestricted.rs b/api/src/routes/public/api_v1/unrestricted.rs index 9d9ef00..2a28a9e 100644 --- a/api/src/routes/public/api_v1/unrestricted.rs +++ b/api/src/routes/public/api_v1/unrestricted.rs @@ -163,7 +163,7 @@ pub async fn create_account( .send(token_manager::CreatePair { customer_id: account.customer_id, role: account.role, - id: account.id, + account_id: account.id, }) .await .map_err(|_| routes::Error::CriticalFailure)??; @@ -205,7 +205,7 @@ async fn sign_in( .send(token_manager::CreatePair { customer_id: account.customer_id, role: account.role, - id: account.id, + account_id: account.id, }) .await .map_err(|_| routes::Error::CriticalFailure)??; diff --git a/scripts/test.sh b/scripts/test.sh index 4ac4682..db67aba 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -3,4 +3,4 @@ psql postgres postgres -c "DROP DATABASE bazzar_test" psql postgres postgres -c "CREATE DATABASE bazzar_test" sqlx migrate run --database-url='postgres://postgres@localhost/bazzar_test' -cargo test +cargo test --all diff --git a/shared/model/src/lib.rs b/shared/model/src/lib.rs index 2c11b2b..7f4b9cd 100644 --- a/shared/model/src/lib.rs +++ b/shared/model/src/lib.rs @@ -901,24 +901,34 @@ pub struct Stock { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Display, Deref)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Display, Deref)] #[serde(transparent)] pub struct OrderAddressId(RecordId); #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Display, Deref)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Display, Deref)] #[serde(transparent)] pub struct OrderId(RecordId); #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(transparent))] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Display, Deref)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Display, Deref)] #[serde(transparent)] pub struct ExtOrderId(String); +impl ExtOrderId { + pub fn new>(s: S) -> Self { + Self(s.into()) + } + + pub fn into_inner(self) -> String { + self.0 + } +} + #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[derive(Debug, PartialEq, Serialize, Deserialize)] diff --git a/shared/testx/src/lib.rs b/shared/testx/src/lib.rs index cf1545b..e91896b 100644 --- a/shared/testx/src/lib.rs +++ b/shared/testx/src/lib.rs @@ -1,5 +1,5 @@ #[macro_export] -macro_rules! db_t { +macro_rules! db_t_ref { ($t: ident) => { let config = config::default_load(&mut NoOpts); config @@ -13,6 +13,19 @@ macro_rules! db_t { }; } +#[macro_export] +macro_rules! db { + ($config: ident, $db: ident) => { + let $config = config::default_load(&mut NoOpts); + $config + .lock() + .database_mut() + .set_url("postgres://postgres@localhost/bazzar_test"); + + let $db = Database::build($config.clone()).await; + }; +} + #[macro_export] macro_rules! db_rollback { ($t: expr) => {