diff --git a/.sqlx/query-ed4bd44b2a0595cd80b36ce70b30ab55af1af12c1f22a2d4a2baf8af4569cf73.json b/.sqlx/query-004d12b7ccb1b21c39ef6de716953bf039bdba5096ae139be7656170ff45613f.json similarity index 74% rename from .sqlx/query-ed4bd44b2a0595cd80b36ce70b30ab55af1af12c1f22a2d4a2baf8af4569cf73.json rename to .sqlx/query-004d12b7ccb1b21c39ef6de716953bf039bdba5096ae139be7656170ff45613f.json index 82b81dc..69ad81b 100644 --- a/.sqlx/query-ed4bd44b2a0595cd80b36ce70b30ab55af1af12c1f22a2d4a2baf8af4569cf73.json +++ b/.sqlx/query-004d12b7ccb1b21c39ef6de716953bf039bdba5096ae139be7656170ff45613f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT EXISTS (\n SELECT 1\n FROM user_query\n WHERE\n username = $1\n );", + "query": "SELECT EXISTS (\n SELECT 1\n FROM user_query\n WHERE\n user_id = $1\n );", "describe": { "columns": [ { @@ -11,12 +11,12 @@ ], "parameters": { "Left": [ - "Text" + "Uuid" ] }, "nullable": [ null ] }, - "hash": "ed4bd44b2a0595cd80b36ce70b30ab55af1af12c1f22a2d4a2baf8af4569cf73" + "hash": "004d12b7ccb1b21c39ef6de716953bf039bdba5096ae139be7656170ff45613f" } diff --git a/.sqlx/query-487deedfaaf10c4ab02fc223b8fec4bf359b082331adfa788096742673168be3.json b/.sqlx/query-0fbaa8084440adce8a6162d67e3e57b6062cc17bbbbeb5ea3e1e58db17ac8240.json similarity index 63% rename from .sqlx/query-487deedfaaf10c4ab02fc223b8fec4bf359b082331adfa788096742673168be3.json rename to .sqlx/query-0fbaa8084440adce8a6162d67e3e57b6062cc17bbbbeb5ea3e1e58db17ac8240.json index d96e1cb..cc15f57 100644 --- a/.sqlx/query-487deedfaaf10c4ab02fc223b8fec4bf359b082331adfa788096742673168be3.json +++ b/.sqlx/query-0fbaa8084440adce8a6162d67e3e57b6062cc17bbbbeb5ea3e1e58db17ac8240.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO verification_otp (secret, created_at, purpose, username)\n VALUES ($1, $2, $3, $4);", + "query": "INSERT INTO verification_otp (secret, created_at, purpose, user_id)\n VALUES ($1, $2, $3, $4);", "describe": { "columns": [], "parameters": { @@ -8,10 +8,10 @@ "Varchar", "Timestamptz", "Text", - "Text" + "Uuid" ] }, "nullable": [] }, - "hash": "487deedfaaf10c4ab02fc223b8fec4bf359b082331adfa788096742673168be3" + "hash": "0fbaa8084440adce8a6162d67e3e57b6062cc17bbbbeb5ea3e1e58db17ac8240" } diff --git a/.sqlx/query-14e8d7a1c8f80701b76b2bac69b1ecd99f7694d620f1945ad5c4ae474a17be1b.json b/.sqlx/query-14e8d7a1c8f80701b76b2bac69b1ecd99f7694d620f1945ad5c4ae474a17be1b.json new file mode 100644 index 0000000..1cb0ae4 --- /dev/null +++ b/.sqlx/query-14e8d7a1c8f80701b76b2bac69b1ecd99f7694d620f1945ad5c4ae474a17be1b.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE\n user_query\n SET\n user_id = $1, version = $2, first_name = $3, email = $4,\n hashed_password = $5, is_admin = $6, is_verified = $7, deleted = $8,\n last_name=$9;", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Int8", + "Text", + "Text", + "Text", + "Bool", + "Bool", + "Bool", + "Text" + ] + }, + "nullable": [] + }, + "hash": "14e8d7a1c8f80701b76b2bac69b1ecd99f7694d620f1945ad5c4ae474a17be1b" +} diff --git a/.sqlx/query-48aa5f3eacaaec4a74ba5d2908875fcac797644d104d3d580715587ecb2e2119.json b/.sqlx/query-48aa5f3eacaaec4a74ba5d2908875fcac797644d104d3d580715587ecb2e2119.json index f4f37cf..37e701b 100644 --- a/.sqlx/query-48aa5f3eacaaec4a74ba5d2908875fcac797644d104d3d580715587ecb2e2119.json +++ b/.sqlx/query-48aa5f3eacaaec4a74ba5d2908875fcac797644d104d3d580715587ecb2e2119.json @@ -9,7 +9,7 @@ "Text", "Text", "Uuid", - "Text" + "Uuid" ] }, "nullable": [] diff --git a/.sqlx/query-5385ba9531992b91670e52b5332af1f09069e1d6d168f9e93b729cf964e97c35.json b/.sqlx/query-5385ba9531992b91670e52b5332af1f09069e1d6d168f9e93b729cf964e97c35.json index c7ff970..2e101fa 100644 --- a/.sqlx/query-5385ba9531992b91670e52b5332af1f09069e1d6d168f9e93b729cf964e97c35.json +++ b/.sqlx/query-5385ba9531992b91670e52b5332af1f09069e1d6d168f9e93b729cf964e97c35.json @@ -21,7 +21,7 @@ { "ordinal": 3, "name": "owner", - "type_info": "Text" + "type_info": "Uuid" } ], "parameters": { diff --git a/.sqlx/query-fa1c65d51e3e0521d6a20998f4b80b615c9e0ffe9ceda82e0bce410c9aca39a0.json b/.sqlx/query-6523c83a859d7ca283d209133a10d4ac74b6b0358bdf1e17f1a54e2cc02e305b.json similarity index 61% rename from .sqlx/query-fa1c65d51e3e0521d6a20998f4b80b615c9e0ffe9ceda82e0bce410c9aca39a0.json rename to .sqlx/query-6523c83a859d7ca283d209133a10d4ac74b6b0358bdf1e17f1a54e2cc02e305b.json index ca1f198..7c06f99 100644 --- a/.sqlx/query-fa1c65d51e3e0521d6a20998f4b80b615c9e0ffe9ceda82e0bce410c9aca39a0.json +++ b/.sqlx/query-6523c83a859d7ca283d209133a10d4ac74b6b0358bdf1e17f1a54e2cc02e305b.json @@ -1,12 +1,12 @@ { "db_name": "PostgreSQL", - "query": "SELECT \n view_id, version\n FROM\n user_query\n WHERE\n view_id = $1;", + "query": "SELECT \n user_id, version\n FROM\n user_query\n WHERE\n user_id = $1;", "describe": { "columns": [ { "ordinal": 0, - "name": "view_id", - "type_info": "Text" + "name": "user_id", + "type_info": "Uuid" }, { "ordinal": 1, @@ -16,7 +16,7 @@ ], "parameters": { "Left": [ - "Text" + "Uuid" ] }, "nullable": [ @@ -24,5 +24,5 @@ false ] }, - "hash": "fa1c65d51e3e0521d6a20998f4b80b615c9e0ffe9ceda82e0bce410c9aca39a0" + "hash": "6523c83a859d7ca283d209133a10d4ac74b6b0358bdf1e17f1a54e2cc02e305b" } diff --git a/.sqlx/query-3edf94a78114819085b573ff51702faee1c444c24bbf6d33ec1b4b245dd9f675.json b/.sqlx/query-70e6216e30f90175d4c3bad51ff51f5fa2b6f965d868b15c53264cb8cd6b4053.json similarity index 62% rename from .sqlx/query-3edf94a78114819085b573ff51702faee1c444c24bbf6d33ec1b4b245dd9f675.json rename to .sqlx/query-70e6216e30f90175d4c3bad51ff51f5fa2b6f965d868b15c53264cb8cd6b4053.json index b8455bd..9db1dab 100644 --- a/.sqlx/query-3edf94a78114819085b573ff51702faee1c444c24bbf6d33ec1b4b245dd9f675.json +++ b/.sqlx/query-70e6216e30f90175d4c3bad51ff51f5fa2b6f965d868b15c53264cb8cd6b4053.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT EXISTS (\n SELECT 1\n FROM verification_otp\n WHERE\n username = $1\n AND\n purpose = $2\n AND\n secret = $3\n );", + "query": "SELECT EXISTS (\n SELECT 1\n FROM verification_otp\n WHERE\n user_id = $1\n AND\n purpose = $2\n AND\n secret = $3\n );", "describe": { "columns": [ { @@ -11,7 +11,7 @@ ], "parameters": { "Left": [ - "Text", + "Uuid", "Text", "Text" ] @@ -20,5 +20,5 @@ null ] }, - "hash": "3edf94a78114819085b573ff51702faee1c444c24bbf6d33ec1b4b245dd9f675" + "hash": "70e6216e30f90175d4c3bad51ff51f5fa2b6f965d868b15c53264cb8cd6b4053" } diff --git a/.sqlx/query-750f96296e6d2c0b6d79c6f21e5b665369d5062a42c2c405f43d56a614a1e0ea.json b/.sqlx/query-750f96296e6d2c0b6d79c6f21e5b665369d5062a42c2c405f43d56a614a1e0ea.json deleted file mode 100644 index 8ce1a68..0000000 --- a/.sqlx/query-750f96296e6d2c0b6d79c6f21e5b665369d5062a42c2c405f43d56a614a1e0ea.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE\n user_query\n SET\n view_id = $1, version = $2, username = $3, email = $4,\n hashed_password = $5, is_admin = $6, is_verified = $7, deleted = $8;", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Int8", - "Text", - "Text", - "Text", - "Bool", - "Bool", - "Bool" - ] - }, - "nullable": [] - }, - "hash": "750f96296e6d2c0b6d79c6f21e5b665369d5062a42c2c405f43d56a614a1e0ea" -} diff --git a/.sqlx/query-7a401e98fff59eb8c0fe1744ccdea68842eaf86ba312c2eb324854041beb9de7.json b/.sqlx/query-7a401e98fff59eb8c0fe1744ccdea68842eaf86ba312c2eb324854041beb9de7.json deleted file mode 100644 index 7990582..0000000 --- a/.sqlx/query-7a401e98fff59eb8c0fe1744ccdea68842eaf86ba312c2eb324854041beb9de7.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO user_query (\n view_id, version, username, email,\n hashed_password, is_admin, is_verified, deleted\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8\n );", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Int8", - "Text", - "Text", - "Text", - "Bool", - "Bool", - "Bool" - ] - }, - "nullable": [] - }, - "hash": "7a401e98fff59eb8c0fe1744ccdea68842eaf86ba312c2eb324854041beb9de7" -} diff --git a/.sqlx/query-803f868ee4a79fefdd85c5d0a034a5925c3bb7de87828095971cb19bc1fc7550.json b/.sqlx/query-7a59c989d043c249cd04fe24544cf9ea55e1329ce4b53889947478c2e766ea1a.json similarity index 58% rename from .sqlx/query-803f868ee4a79fefdd85c5d0a034a5925c3bb7de87828095971cb19bc1fc7550.json rename to .sqlx/query-7a59c989d043c249cd04fe24544cf9ea55e1329ce4b53889947478c2e766ea1a.json index 06c125a..574cb31 100644 --- a/.sqlx/query-803f868ee4a79fefdd85c5d0a034a5925c3bb7de87828095971cb19bc1fc7550.json +++ b/.sqlx/query-7a59c989d043c249cd04fe24544cf9ea55e1329ce4b53889947478c2e766ea1a.json @@ -1,42 +1,52 @@ { "db_name": "PostgreSQL", - "query": "SELECT \n username, email, hashed_password, is_admin, is_verified, deleted\n FROM\n user_query\n WHERE\n view_id = $1;", + "query": "SELECT \n first_name, last_name, user_id, email, hashed_password, is_admin, is_verified, deleted\n FROM\n user_query\n WHERE\n user_id = $1;", "describe": { "columns": [ { "ordinal": 0, - "name": "username", + "name": "first_name", "type_info": "Text" }, { "ordinal": 1, - "name": "email", + "name": "last_name", "type_info": "Text" }, { "ordinal": 2, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "email", + "type_info": "Text" + }, + { + "ordinal": 4, "name": "hashed_password", "type_info": "Text" }, { - "ordinal": 3, + "ordinal": 5, "name": "is_admin", "type_info": "Bool" }, { - "ordinal": 4, + "ordinal": 6, "name": "is_verified", "type_info": "Bool" }, { - "ordinal": 5, + "ordinal": 7, "name": "deleted", "type_info": "Bool" } ], "parameters": { "Left": [ - "Text" + "Uuid" ] }, "nullable": [ @@ -45,8 +55,10 @@ false, false, false, + false, + false, false ] }, - "hash": "803f868ee4a79fefdd85c5d0a034a5925c3bb7de87828095971cb19bc1fc7550" + "hash": "7a59c989d043c249cd04fe24544cf9ea55e1329ce4b53889947478c2e766ea1a" } diff --git a/.sqlx/query-89ebb8b413db2c4da15adff8a09d04d5849c3990f0965de479b76630317f8a6e.json b/.sqlx/query-89ebb8b413db2c4da15adff8a09d04d5849c3990f0965de479b76630317f8a6e.json index 5fe35f2..d24766f 100644 --- a/.sqlx/query-89ebb8b413db2c4da15adff8a09d04d5849c3990f0965de479b76630317f8a6e.json +++ b/.sqlx/query-89ebb8b413db2c4da15adff8a09d04d5849c3990f0965de479b76630317f8a6e.json @@ -9,7 +9,7 @@ "Text", "Text", "Uuid", - "Text" + "Uuid" ] }, "nullable": [] diff --git a/.sqlx/query-c54d1b89ee39ceb11326e82962eef3d8f588f6252ba4bbac99fb73cdbbcd2204.json b/.sqlx/query-c54d1b89ee39ceb11326e82962eef3d8f588f6252ba4bbac99fb73cdbbcd2204.json deleted file mode 100644 index f56645c..0000000 --- a/.sqlx/query-c54d1b89ee39ceb11326e82962eef3d8f588f6252ba4bbac99fb73cdbbcd2204.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM\n verification_otp\n WHERE\n username = $1\n AND\n purpose = $2\n AND\n secret = $3;", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "c54d1b89ee39ceb11326e82962eef3d8f588f6252ba4bbac99fb73cdbbcd2204" -} diff --git a/.sqlx/query-7c48c7569b16e9600aa31fbf14b9ceeb195da7cde0082be861369ef4f997c534.json b/.sqlx/query-ec80b5dc41697e7df7112962aba8185040fc0a101edea4b7e0c23f6468300196.json similarity index 70% rename from .sqlx/query-7c48c7569b16e9600aa31fbf14b9ceeb195da7cde0082be861369ef4f997c534.json rename to .sqlx/query-ec80b5dc41697e7df7112962aba8185040fc0a101edea4b7e0c23f6468300196.json index 075568e..8305206 100644 --- a/.sqlx/query-7c48c7569b16e9600aa31fbf14b9ceeb195da7cde0082be861369ef4f997c534.json +++ b/.sqlx/query-ec80b5dc41697e7df7112962aba8185040fc0a101edea4b7e0c23f6468300196.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n secret\n FROM\n verification_otp\n WHERE\n username = $1\n AND\n purpose = $2;", + "query": "SELECT\n secret\n FROM\n verification_otp\n WHERE\n user_id = $1\n AND\n purpose = $2;", "describe": { "columns": [ { @@ -11,7 +11,7 @@ ], "parameters": { "Left": [ - "Text", + "Uuid", "Text" ] }, @@ -19,5 +19,5 @@ false ] }, - "hash": "7c48c7569b16e9600aa31fbf14b9ceeb195da7cde0082be861369ef4f997c534" + "hash": "ec80b5dc41697e7df7112962aba8185040fc0a101edea4b7e0c23f6468300196" } diff --git a/.sqlx/query-f1b7a6581e20b9f1c0dabeb7a2479d70ab005cf787a0ed13260e65f7ea949136.json b/.sqlx/query-f1b7a6581e20b9f1c0dabeb7a2479d70ab005cf787a0ed13260e65f7ea949136.json new file mode 100644 index 0000000..2d1eab1 --- /dev/null +++ b/.sqlx/query-f1b7a6581e20b9f1c0dabeb7a2479d70ab005cf787a0ed13260e65f7ea949136.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO user_query (\n version, first_name, last_name, email,\n hashed_password, is_admin, is_verified, deleted, user_id\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9\n );", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text", + "Text", + "Text", + "Text", + "Bool", + "Bool", + "Bool", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "f1b7a6581e20b9f1c0dabeb7a2479d70ab005cf787a0ed13260e65f7ea949136" +} diff --git a/.sqlx/query-ffb4315a9b95ed39964c2a0a5a9dc45e4ab7941ced043931568023238d5127f7.json b/.sqlx/query-ffb4315a9b95ed39964c2a0a5a9dc45e4ab7941ced043931568023238d5127f7.json new file mode 100644 index 0000000..88336c5 --- /dev/null +++ b/.sqlx/query-ffb4315a9b95ed39964c2a0a5a9dc45e4ab7941ced043931568023238d5127f7.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM\n verification_otp\n WHERE\n user_id = $1\n AND\n purpose = $2\n AND\n secret = $3;", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "ffb4315a9b95ed39964c2a0a5a9dc45e4ab7941ced043931568023238d5127f7" +} diff --git a/migrations/20240516200802_cqrs_init.sql b/migrations/20240516200802_cqrs_init.sql index edeff0f..a9c50a2 100644 --- a/migrations/20240516200802_cqrs_init.sql +++ b/migrations/20240516200802_cqrs_init.sql @@ -17,17 +17,16 @@ CREATE TABLE IF NOT EXISTS events CREATE TABLE IF NOT EXISTS user_query ( - view_id text NOT NULL, version bigint CHECK (version >= 0) NOT NULL, - username TEXT NOT NULL UNIQUE, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + user_id UUID NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE, hashed_password TEXT NOT NULL, is_admin BOOLEAN NOT NULL DEFAULT FALSE, is_verified BOOLEAN NOT NULL DEFAULT FALSE, deleted BOOLEAN NOT NULL DEFAULT FALSE, - PRIMARY KEY (view_id) + PRIMARY KEY (user_id) ); - -CREATE UNIQUE INDEX IF NOT EXISTS user_username_index ON user_query (username); diff --git a/migrations/20240516203144_verification_otp.sql b/migrations/20240516203144_verification_otp.sql index 2c45b1b..ede1a72 100644 --- a/migrations/20240516203144_verification_otp.sql +++ b/migrations/20240516203144_verification_otp.sql @@ -6,6 +6,6 @@ CREATE TABLE IF NOT EXISTS verification_otp ( secret VARCHAR(32) NOT NULL UNIQUE, created_at timestamp with time zone DEFAULT (CURRENT_TIMESTAMP), purpose TEXT NOT NULL, - username TEXT NOT NULL, + user_id UUID NOT NULL, ID SERIAL PRIMARY KEY NOT NULL ); diff --git a/migrations/20240713073740_cqrs_inventory_category_query.sql b/migrations/20240713073740_cqrs_inventory_category_query.sql index e6d9dd2..ea55dc8 100644 --- a/migrations/20240713073740_cqrs_inventory_category_query.sql +++ b/migrations/20240713073740_cqrs_inventory_category_query.sql @@ -15,8 +15,3 @@ CREATE TABLE IF NOT EXISTS cqrs_inventory_category_query PRIMARY KEY (category_id) ); - -CREATE UNIQUE INDEX IF NOT EXISTS - cqrs_inventory_store_query_category_id_index -ON - cqrs_inventory_category_query (category_id); diff --git a/migrations/20240713080804_cqrs_inventory_store_query.sql b/migrations/20240713080804_cqrs_inventory_store_query.sql index 2701498..bcee88d 100644 --- a/migrations/20240713080804_cqrs_inventory_store_query.sql +++ b/migrations/20240713080804_cqrs_inventory_store_query.sql @@ -8,11 +8,9 @@ CREATE TABLE IF NOT EXISTS cqrs_inventory_store_query name TEXT NOT NULL, address TEXT, - owner TEXT NOT NULL, + owner UUID NOT NULL, store_id UUID NOT NULL UNIQUE, deleted BOOLEAN NOT NULL DEFAULT FALSE, PRIMARY KEY (store_id) ); - -CREATE UNIQUE INDEX IF NOT EXISTS store_store_id_index ON cqrs_inventory_store_query (store_id); diff --git a/src/identity/adapters/output/db/postgres/create_verification_secret.rs b/src/identity/adapters/output/db/postgres/create_verification_secret.rs index b9542ca..da8e103 100644 --- a/src/identity/adapters/output/db/postgres/create_verification_secret.rs +++ b/src/identity/adapters/output/db/postgres/create_verification_secret.rs @@ -11,12 +11,12 @@ use crate::identity::application::port::output::db::{create_verification_secret: impl CreateVerificationSecretOutDBPort for DBOutPostgresAdapter { async fn create_verification_secret(&self, msg: CreateSecretMsg) -> OutDBPortResult<()> { sqlx::query!( - "INSERT INTO verification_otp (secret, created_at, purpose, username) + "INSERT INTO verification_otp (secret, created_at, purpose, user_id) VALUES ($1, $2, $3, $4);", &msg.secret, OffsetDateTime::now_utc(), REGISTRATION_SECRET_PURPOSE, - &msg.username, + &msg.user_id, ) .execute(&self.pool) .await?; @@ -26,6 +26,8 @@ impl CreateVerificationSecretOutDBPort for DBOutPostgresAdapter { #[cfg(test)] mod tests { + use crate::utils::uuid::tests::UUID; + use super::*; #[actix_rt::test] @@ -40,7 +42,7 @@ mod tests { let msg = CreateSecretMsgBuilder::default() .secret("secret".into()) - .username("username".into()) + .user_id(UUID.clone()) .build() .unwrap(); diff --git a/src/identity/adapters/output/db/postgres/delete_verification_secret.rs b/src/identity/adapters/output/db/postgres/delete_verification_secret.rs index a75728e..8d150f0 100644 --- a/src/identity/adapters/output/db/postgres/delete_verification_secret.rs +++ b/src/identity/adapters/output/db/postgres/delete_verification_secret.rs @@ -12,12 +12,12 @@ impl DeleteVerificationSecretOutDBPort for DBOutPostgresAdapter { "DELETE FROM verification_otp WHERE - username = $1 + user_id = $1 AND purpose = $2 AND secret = $3;", - msg.username, + msg.user_id, REGISTRATION_SECRET_PURPOSE, msg.secret, ) @@ -30,13 +30,16 @@ impl DeleteVerificationSecretOutDBPort for DBOutPostgresAdapter { #[cfg(test)] mod tests { use super::*; - use crate::identity::application::port::output::db::{ - create_verification_secret::*, verification_secret_exists::*, + use crate::{ + identity::application::port::output::db::{ + create_verification_secret::*, verification_secret_exists::*, + }, + utils::uuid::tests::UUID, }; #[actix_rt::test] async fn test_postgres_delete_verification_secret() { - let username = "batman"; + let user_id = UUID; let secret = "bsdasdf"; let settings = crate::settings::tests::get_settings().await; settings.create_db().await; @@ -46,7 +49,7 @@ mod tests { .unwrap(), ); let msg = VerifySecretExistsMsgBuilder::default() - .username(username.into()) + .user_id(user_id.clone()) .secret(secret.into()) .build() .unwrap(); @@ -56,7 +59,7 @@ mod tests { let create_msg = CreateSecretMsgBuilder::default() .secret(secret.into()) - .username(username.into()) + .user_id(user_id.clone()) .build() .unwrap(); db.create_verification_secret(create_msg).await.unwrap(); diff --git a/src/identity/adapters/output/db/postgres/email_exists.rs b/src/identity/adapters/output/db/postgres/email_exists.rs index c4cf50f..bac4dd2 100644 --- a/src/identity/adapters/output/db/postgres/email_exists.rs +++ b/src/identity/adapters/output/db/postgres/email_exists.rs @@ -33,6 +33,8 @@ impl EmailExistsOutDBPort for DBOutPostgresAdapter { mod tests { use super::*; + use crate::utils::uuid::tests::*; + #[actix_rt::test] async fn test_postgres_email_exists() { let email = "foo@exmaple.com"; @@ -49,12 +51,13 @@ mod tests { sqlx::query!( "INSERT INTO user_query - (view_id, version, username, email, hashed_password) - VALUES ($1, $2, $3, $4, $5);", - "1", + (user_id, version, first_name, email, hashed_password, last_name) + VALUES ($1, $2, $3, $4, $5, $6);", + &UUID, 1, "foo", email, + "passwd", "passwd" ) .execute(&db.pool) diff --git a/src/identity/adapters/output/db/postgres/get_verification_secret.rs b/src/identity/adapters/output/db/postgres/get_verification_secret.rs index 56dcc77..e97e7aa 100644 --- a/src/identity/adapters/output/db/postgres/get_verification_secret.rs +++ b/src/identity/adapters/output/db/postgres/get_verification_secret.rs @@ -2,13 +2,15 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +use uuid::Uuid; + use super::errors::*; use super::DBOutPostgresAdapter; use crate::identity::application::port::output::db::{errors::*, get_verification_secret::*}; #[async_trait::async_trait] impl GetVerificationSecretOutDBPort for DBOutPostgresAdapter { - async fn get_verification_secret(&self, username: &str) -> OutDBPortResult { + async fn get_verification_secret(&self, user_id: &Uuid) -> OutDBPortResult { struct Secret { secret: String, } @@ -19,10 +21,10 @@ impl GetVerificationSecretOutDBPort for DBOutPostgresAdapter { FROM verification_otp WHERE - username = $1 + user_id = $1 AND purpose = $2;", - username, + user_id, REGISTRATION_SECRET_PURPOSE, ) .fetch_one(&self.pool) @@ -35,11 +37,14 @@ impl GetVerificationSecretOutDBPort for DBOutPostgresAdapter { #[cfg(test)] mod tests { use super::*; - use crate::identity::application::port::output::db::create_verification_secret::*; + use crate::{ + identity::application::port::output::db::create_verification_secret::*, + utils::uuid::tests::UUID, + }; #[actix_rt::test] async fn test_postgres_get_verification_secret() { - let username = "batman"; + let user_id = UUID; let secret = "bsdasdf"; let settings = crate::settings::tests::get_settings().await; settings.create_db().await; @@ -49,19 +54,19 @@ mod tests { .unwrap(), ); assert_eq!( - db.get_verification_secret(username).await.err(), + db.get_verification_secret(&user_id).await.err(), Some(OutDBPortError::VerificationOTPSecretNotFound) ); let create_msg = CreateSecretMsgBuilder::default() .secret(secret.into()) - .username(username.into()) + .user_id(user_id.clone()) .build() .unwrap(); db.create_verification_secret(create_msg).await.unwrap(); - assert_eq!(db.get_verification_secret(username).await.unwrap(), secret); + assert_eq!(db.get_verification_secret(&user_id).await.unwrap(), secret); settings.drop_db().await; } diff --git a/src/identity/adapters/output/db/postgres/mod.rs b/src/identity/adapters/output/db/postgres/mod.rs index d88ec8a..9b57f8c 100644 --- a/src/identity/adapters/output/db/postgres/mod.rs +++ b/src/identity/adapters/output/db/postgres/mod.rs @@ -14,8 +14,8 @@ pub mod delete_verification_secret; pub mod email_exists; mod errors; pub mod get_verification_secret; +pub mod user_id_exists; pub mod user_view; -pub mod username_exists; pub mod verification_secret_exists; #[derive(Clone)] diff --git a/src/identity/adapters/output/db/postgres/username_exists.rs b/src/identity/adapters/output/db/postgres/user_id_exists.rs similarity index 59% rename from src/identity/adapters/output/db/postgres/username_exists.rs rename to src/identity/adapters/output/db/postgres/user_id_exists.rs index 46e8598..5b52b47 100644 --- a/src/identity/adapters/output/db/postgres/username_exists.rs +++ b/src/identity/adapters/output/db/postgres/user_id_exists.rs @@ -2,22 +2,24 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +use uuid::Uuid; + use super::DBOutPostgresAdapter; use crate::identity::application::port::output::db::{ - errors::*, username_exists::UsernameExistsOutDBPort, + errors::*, user_id_exists::UserIDExistsOutDBPort, }; #[async_trait::async_trait] -impl UsernameExistsOutDBPort for DBOutPostgresAdapter { - async fn username_exists(&self, username: &str) -> OutDBPortResult { +impl UserIDExistsOutDBPort for DBOutPostgresAdapter { + async fn user_id_exists(&self, user_id: &Uuid) -> OutDBPortResult { let res = sqlx::query!( "SELECT EXISTS ( SELECT 1 FROM user_query WHERE - username = $1 + user_id = $1 );", - username + user_id, ) .fetch_one(&self.pool) .await?; @@ -33,9 +35,12 @@ impl UsernameExistsOutDBPort for DBOutPostgresAdapter { mod tests { use super::*; + use crate::utils::uuid::tests::UUID; + + use crate::identity::domain::aggregate::*; + #[actix_rt::test] - async fn test_postgres_username_exists() { - let username = "foo@exmaple.com"; + async fn test_postgres_user_id_exists() { let settings = crate::settings::tests::get_settings().await; settings.create_db().await; let db = super::DBOutPostgresAdapter::new( @@ -44,25 +49,28 @@ mod tests { .unwrap(), ); + let user = User::default(); + // state doesn't exist - assert!(!db.username_exists(username).await.unwrap()); + assert!(!db.user_id_exists(&UUID).await.unwrap()); sqlx::query!( "INSERT INTO user_query - (view_id, version, username, email, hashed_password) - VALUES ($1, $2, $3, $4, $5);", - "1", + (version, user_id, email, hashed_password, first_name, last_name) + VALUES ($1, $2, $3, $4, $5, $6);", 1, - username, - "foo", - "passwd" + UUID, + user.email(), + user.hashed_password(), + user.first_name(), + user.last_name(), ) .execute(&db.pool) .await .unwrap(); // state exists - assert!(db.username_exists(username).await.unwrap()); + assert!(db.user_id_exists(&UUID).await.unwrap()); settings.drop_db().await; } diff --git a/src/identity/adapters/output/db/postgres/user_view.rs b/src/identity/adapters/output/db/postgres/user_view.rs index 0c53299..a105f00 100644 --- a/src/identity/adapters/output/db/postgres/user_view.rs +++ b/src/identity/adapters/output/db/postgres/user_view.rs @@ -6,18 +6,24 @@ use async_trait::async_trait; use cqrs_es::persist::GenericQuery; use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository}; use cqrs_es::{EventEnvelope, Query, View}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; use super::errors::*; use super::DBOutPostgresAdapter; use crate::identity::application::services::events::UserEvent; use crate::identity::domain::aggregate::User; -use serde::{Deserialize, Serialize}; +use crate::utils::parse_aggregate_id::parse_aggregate_id; + +pub const NEW_USER_NON_UUID: &str = "new_user_non_uuid-asdfa"; // The view for a User query, for a standard http application this should // be designed to reflect the response dto that will be returned to a user. #[derive(Debug, Default, Serialize, Deserialize)] pub struct UserView { - username: String, + first_name: String, + last_name: String, + user_id: Uuid, email: String, hashed_password: String, is_admin: bool, @@ -32,12 +38,14 @@ impl View for UserView { fn update(&mut self, event: &EventEnvelope) { match &event.payload { UserEvent::UserRegistered(val) => { - self.username = val.username().into(); self.email = val.email().into(); self.hashed_password = val.hashed_password().into(); self.is_admin = val.is_admin().to_owned(); self.is_verified = val.is_verified().to_owned(); self.deleted = false; + self.first_name = val.first_name().into(); + self.last_name = val.last_name().into(); + self.user_id = val.user_id().clone(); } UserEvent::UserDeleted => self.deleted = true, UserEvent::Loggedin(_) => (), @@ -58,16 +66,21 @@ impl View for UserView { #[async_trait] impl ViewRepository for DBOutPostgresAdapter { - async fn load(&self, view_id: &str) -> Result, PersistenceError> { + async fn load(&self, user_id: &str) -> Result, PersistenceError> { + let user_id = match parse_aggregate_id(user_id, NEW_USER_NON_UUID)? { + Some((val, _)) => return Ok(Some(val)), + None => Uuid::parse_str(user_id).unwrap(), + }; + let res = sqlx::query_as!( UserView, "SELECT - username, email, hashed_password, is_admin, is_verified, deleted + first_name, last_name, user_id, email, hashed_password, is_admin, is_verified, deleted FROM user_query WHERE - view_id = $1;", - view_id + user_id = $1;", + user_id ) .fetch_one(&self.pool) .await @@ -77,17 +90,22 @@ impl ViewRepository for DBOutPostgresAdapter { async fn load_with_context( &self, - view_id: &str, + user_id: &str, ) -> Result, PersistenceError> { + let user_id = match parse_aggregate_id(user_id, NEW_USER_NON_UUID)? { + Some(val) => return Ok(Some(val)), + None => Uuid::parse_str(user_id).unwrap(), + }; + let res = sqlx::query_as!( UserView, "SELECT - username, email, hashed_password, is_admin, is_verified, deleted + first_name, last_name, user_id, email, hashed_password, is_admin, is_verified, deleted FROM user_query WHERE - view_id = $1;", - view_id + user_id = $1;", + user_id ) .fetch_one(&self.pool) .await @@ -95,24 +113,24 @@ impl ViewRepository for DBOutPostgresAdapter { struct Context { version: i64, - view_id: String, + user_id: String, } let ctx = sqlx::query_as!( Context, "SELECT - view_id, version + user_id, version FROM user_query WHERE - view_id = $1;", - view_id + user_id = $1;", + user_id ) .fetch_one(&self.pool) .await .map_err(PostgresAggregateError::from)?; - let view_context = ViewContext::new(ctx.view_id, ctx.version); + let view_context = ViewContext::new(ctx.user_id.to_string(), ctx.version); Ok(Some((res, view_context))) } @@ -126,19 +144,20 @@ impl ViewRepository for DBOutPostgresAdapter { let version = context.version + 1; sqlx::query!( "INSERT INTO user_query ( - view_id, version, username, email, - hashed_password, is_admin, is_verified, deleted + version, first_name, last_name, email, + hashed_password, is_admin, is_verified, deleted, user_id ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8 + $1, $2, $3, $4, $5, $6, $7, $8, $9 );", - context.view_instance_id, version, - view.username, + view.first_name, + view.last_name, view.email, view.hashed_password, view.is_admin, view.is_verified, view.deleted, + view.user_id, ) .execute(&self.pool) .await @@ -150,16 +169,18 @@ impl ViewRepository for DBOutPostgresAdapter { "UPDATE user_query SET - view_id = $1, version = $2, username = $3, email = $4, - hashed_password = $5, is_admin = $6, is_verified = $7, deleted = $8;", - context.view_instance_id, + user_id = $1, version = $2, first_name = $3, email = $4, + hashed_password = $5, is_admin = $6, is_verified = $7, deleted = $8, + last_name=$9;", + view.user_id, version, - view.username, + view.first_name, view.email, view.hashed_password, view.is_admin, view.is_verified, view.deleted, + view.last_name, ) .execute(&self.pool) .await diff --git a/src/identity/adapters/output/db/postgres/verification_secret_exists.rs b/src/identity/adapters/output/db/postgres/verification_secret_exists.rs index 052b724..aa1fe87 100644 --- a/src/identity/adapters/output/db/postgres/verification_secret_exists.rs +++ b/src/identity/adapters/output/db/postgres/verification_secret_exists.rs @@ -16,13 +16,13 @@ impl VerificationSecretExistsOutDBPort for DBOutPostgresAdapter { SELECT 1 FROM verification_otp WHERE - username = $1 + user_id = $1 AND purpose = $2 AND secret = $3 );", - msg.username, + msg.user_id, REGISTRATION_SECRET_PURPOSE, msg.secret, ) @@ -39,11 +39,14 @@ impl VerificationSecretExistsOutDBPort for DBOutPostgresAdapter { #[cfg(test)] mod tests { use super::*; - use crate::identity::application::port::output::db::create_verification_secret::*; + use crate::{ + identity::application::port::output::db::create_verification_secret::*, + utils::uuid::tests::UUID, + }; #[actix_rt::test] async fn test_postgres_verification_secret_exists() { - let username = "batman"; + let user_id = UUID; let secret = "bsdasdf"; let settings = crate::settings::tests::get_settings().await; settings.create_db().await; @@ -53,7 +56,7 @@ mod tests { .unwrap(), ); let msg = VerifySecretExistsMsgBuilder::default() - .username(username.into()) + .user_id(user_id.clone()) .secret(secret.into()) .build() .unwrap(); @@ -63,7 +66,7 @@ mod tests { let create_msg = CreateSecretMsgBuilder::default() .secret(secret.into()) - .username(username.into()) + .user_id(user_id.clone()) .build() .unwrap(); db.create_verification_secret(create_msg).await.unwrap(); diff --git a/src/identity/adapters/output/mailer/lettre/account_validation_link.rs b/src/identity/adapters/output/mailer/lettre/account_validation_link.rs index ecdeeec..e926c04 100644 --- a/src/identity/adapters/output/mailer/lettre/account_validation_link.rs +++ b/src/identity/adapters/output/mailer/lettre/account_validation_link.rs @@ -12,17 +12,17 @@ impl AccountValidationLinkOutMailerPort for LettreMailer { async fn account_validation_link( &self, to: &str, - username: &str, + first_name: &str, validation_secret: &str, ) -> OutMailerPortResult<()> { let email = Message::builder() .from(self.from.parse().unwrap()) .reply_to(self.reply_to.parse().unwrap()) - .to(format!("{username} <{to}>").parse().unwrap()) + .to(format!("{first_name} <{to}>").parse().unwrap()) .subject("Please verify your account on Vanikam") // TODO: use better title .header(ContentType::TEXT_PLAIN) .body(format!( - r#"Hello {username}, + r#"Hello {first_name}, Please click here to verify your Vanikam account: {validation_secret} Warm regards, Vanikam Admin diff --git a/src/identity/application/aggregate.rs b/src/identity/application/aggregate.rs deleted file mode 100644 index 3b0470f..0000000 --- a/src/identity/application/aggregate.rs +++ /dev/null @@ -1,325 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Aravinth Manivannan -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -use async_trait::async_trait; -use cqrs_es::Aggregate; - -use crate::identity::application::services::errors::*; -use crate::identity::application::services::events::UserEvent; -use crate::identity::application::services::UserCommand; -use crate::identity::application::services::UserServicesInterface; -use crate::identity::domain::aggregate::User; -use crate::identity::domain::aggregate::UserBuilder; - -#[async_trait] -impl Aggregate for User { - type Command = UserCommand; - type Event = UserEvent; - type Error = IdentityError; - type Services = std::sync::Arc; - - // This identifier should be unique to the system. - fn aggregate_type() -> String { - "account".to_string() - } - - // The aggregate logic goes here. Note that this will be the _bulk_ of a CQRS system - // so expect to use helper functions elsewhere to keep the code clean. - async fn handle( - &self, - command: Self::Command, - services: &Self::Services, - ) -> Result, Self::Error> { - match command { - UserCommand::RegisterUser(cmd) => { - let res = services.register_user().register_user(cmd).await?; - Ok(vec![UserEvent::UserRegistered(res)]) - } - UserCommand::DeleteUser(cmd) => { - services.delete_user().delete_user(cmd).await; - Ok(vec![UserEvent::UserDeleted]) - } - UserCommand::Login(cmd) => { - let res = services.login().login(cmd).await; - Ok(vec![UserEvent::Loggedin(res)]) - } - UserCommand::UpdatePassword(cmd) => { - let res = services.update_password().update_password(cmd).await; - Ok(vec![UserEvent::PasswordUpdated(res)]) - } - UserCommand::UpdateEmail(cmd) => { - let res = services.update_email().update_email(cmd).await?; - Ok(vec![UserEvent::EmailUpdated(res)]) - } - UserCommand::MarkUserVerified(cmd) => { - services - .mark_user_verified() - .mark_user_verified(cmd) - .await?; - Ok(vec![UserEvent::UserVerified]) - } - UserCommand::SetAdmin(cmd) => { - let res = services.set_user_admin().set_user_admin(cmd).await; - Ok(vec![UserEvent::UserPromotedToAdmin(res)]) - } - UserCommand::ResendVerificationEmail(cmd) => { - services - .resend_verification_email() - .resend_verification_email(cmd) - .await?; - Ok(vec![UserEvent::VerificationEmailResent]) - } - } - } - - fn apply(&mut self, event: Self::Event) { - match event { - UserEvent::UserRegistered(e) => { - *self = UserBuilder::default() - .username(e.username().into()) - .email(e.email().into()) - .hashed_password(e.hashed_password().into()) - .is_admin(e.is_admin().to_owned()) - .email_verified(e.email_verified().to_owned()) - .is_verified(e.is_verified().to_owned()) - .deleted(false) - .build() - .unwrap(); - } - UserEvent::UserDeleted => { - self.set_deleted(true); - } - UserEvent::Loggedin(_) => (), - UserEvent::PasswordUpdated(_) => (), - UserEvent::EmailUpdated(e) => { - self.set_email(e.new_email().into()); - self.set_email_verified(false); - } - UserEvent::UserVerified => { - self.set_is_verified(true); - self.set_email_verified(true); - } - UserEvent::UserPromotedToAdmin(_) => { - self.set_is_admin(true); - } - UserEvent::VerificationEmailResent => (), - } - } -} - -//// The aggregate tests are the most important part of a CQRS system. -//// The simplicity and flexibility of these tests are a good part of what -//// makes an event sourced system so friendly to changing business requirements. -//#[cfg(test)] -//mod aggregate_tests { -// use async_trait::async_trait; -// use std::sync::Mutex; -// -// use cqrs_es::test::TestFramework; -// -// use crate::domain::aggregate::User; -// use crate::domain::commands::UserCommand; -// use crate::domain::events::UserEvent; -// use crate::services::{AtmError, UserApi, UserServices, CheckingError}; -// -// // A test framework that will apply our events and command -// // and verify that the logic works as expected. -// type AccountTestFramework = TestFramework; -// -// #[test] -// fn test_deposit_money() { -// let expected = UserEvent::CustomerDepositedMoney { -// amount: 200.0, -// balance: 200.0, -// }; -// let command = UserCommand::DepositMoney { amount: 200.0 }; -// let services = UserServices::new(Box::new(MockUserServices::default())); -// // Obtain a new test framework -// AccountTestFramework::with(services) -// // In a test case with no previous events -// .given_no_previous_events() -// // Wnen we fire this command -// .when(command) -// // then we expect these results -// .then_expect_events(vec![expected]); -// } -// -// #[test] -// fn test_deposit_money_with_balance() { -// let previous = UserEvent::CustomerDepositedMoney { -// amount: 200.0, -// balance: 200.0, -// }; -// let expected = UserEvent::CustomerDepositedMoney { -// amount: 200.0, -// balance: 400.0, -// }; -// let command = UserCommand::DepositMoney { amount: 200.0 }; -// let services = UserServices::new(Box::new(MockUserServices::default())); -// -// AccountTestFramework::with(services) -// // Given this previously applied event -// .given(vec![previous]) -// // When we fire this command -// .when(command) -// // Then we expect this resultant event -// .then_expect_events(vec![expected]); -// } -// -// #[test] -// fn test_withdraw_money() { -// let previous = UserEvent::CustomerDepositedMoney { -// amount: 200.0, -// balance: 200.0, -// }; -// let expected = UserEvent::CustomerWithdrewCash { -// amount: 100.0, -// balance: 100.0, -// }; -// let services = MockUserServices::default(); -// services.set_atm_withdrawal_response(Ok(())); -// let command = UserCommand::WithdrawMoney { -// amount: 100.0, -// atm_id: "ATM34f1ba3c".to_string(), -// }; -// -// AccountTestFramework::with(UserServices::new(Box::new(services))) -// .given(vec![previous]) -// .when(command) -// .then_expect_events(vec![expected]); -// } -// -// #[test] -// fn test_withdraw_money_client_error() { -// let previous = UserEvent::CustomerDepositedMoney { -// amount: 200.0, -// balance: 200.0, -// }; -// let services = MockUserServices::default(); -// services.set_atm_withdrawal_response(Err(AtmError)); -// let command = UserCommand::WithdrawMoney { -// amount: 100.0, -// atm_id: "ATM34f1ba3c".to_string(), -// }; -// -// let services = UserServices::new(Box::new(services)); -// AccountTestFramework::with(services) -// .given(vec![previous]) -// .when(command) -// .then_expect_error_message("atm rule violation"); -// } -// -// #[test] -// fn test_withdraw_money_funds_not_available() { -// let command = UserCommand::WithdrawMoney { -// amount: 200.0, -// atm_id: "ATM34f1ba3c".to_string(), -// }; -// -// let services = UserServices::new(Box::new(MockUserServices::default())); -// AccountTestFramework::with(services) -// .given_no_previous_events() -// .when(command) -// // Here we expect an error rather than any events -// .then_expect_error_message("funds not available") -// } -// -// #[test] -// fn test_wrote_check() { -// let previous = UserEvent::CustomerDepositedMoney { -// amount: 200.0, -// balance: 200.0, -// }; -// let expected = UserEvent::CustomerWroteCheck { -// check_number: "1170".to_string(), -// amount: 100.0, -// balance: 100.0, -// }; -// let services = MockUserServices::default(); -// services.set_validate_check_response(Ok(())); -// let services = UserServices::new(Box::new(services)); -// let command = UserCommand::WriteCheck { -// check_number: "1170".to_string(), -// amount: 100.0, -// }; -// -// AccountTestFramework::with(services) -// .given(vec![previous]) -// .when(command) -// .then_expect_events(vec![expected]); -// } -// -// #[test] -// fn test_wrote_check_bad_check() { -// let previous = UserEvent::CustomerDepositedMoney { -// amount: 200.0, -// balance: 200.0, -// }; -// let services = MockUserServices::default(); -// services.set_validate_check_response(Err(CheckingError)); -// let services = UserServices::new(Box::new(services)); -// let command = UserCommand::WriteCheck { -// check_number: "1170".to_string(), -// amount: 100.0, -// }; -// -// AccountTestFramework::with(services) -// .given(vec![previous]) -// .when(command) -// .then_expect_error_message("check invalid"); -// } -// -// #[test] -// fn test_wrote_check_funds_not_available() { -// let command = UserCommand::WriteCheck { -// check_number: "1170".to_string(), -// amount: 100.0, -// }; -// -// let services = UserServices::new(Box::new(MockUserServices::default())); -// AccountTestFramework::with(services) -// .given_no_previous_events() -// .when(command) -// .then_expect_error_message("funds not available") -// } -// -// pub struct MockUserServices { -// atm_withdrawal_response: Mutex>>, -// validate_check_response: Mutex>>, -// } -// -// impl Default for MockUserServices { -// fn default() -> Self { -// Self { -// atm_withdrawal_response: Mutex::new(None), -// validate_check_response: Mutex::new(None), -// } -// } -// } -// -// impl MockUserServices { -// fn set_atm_withdrawal_response(&self, response: Result<(), AtmError>) { -// *self.atm_withdrawal_response.lock().unwrap() = Some(response); -// } -// fn set_validate_check_response(&self, response: Result<(), CheckingError>) { -// *self.validate_check_response.lock().unwrap() = Some(response); -// } -// } -// -// #[async_trait] -// impl UserApi for MockUserServices { -// async fn atm_withdrawal(&self, _atm_id: &str, _amount: f64) -> Result<(), AtmError> { -// self.atm_withdrawal_response.lock().unwrap().take().unwrap() -// } -// -// async fn validate_check( -// &self, -// _account_id: &str, -// _check_number: &str, -// ) -> Result<(), CheckingError> { -// self.validate_check_response.lock().unwrap().take().unwrap() -// } -// } -//} -// diff --git a/src/identity/application/mod.rs b/src/identity/application/mod.rs index 9d8c62d..2f75b72 100644 --- a/src/identity/application/mod.rs +++ b/src/identity/application/mod.rs @@ -2,6 +2,5 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later -pub mod aggregate; pub mod port; pub mod services; diff --git a/src/identity/application/port/output/db/create_verification_secret.rs b/src/identity/application/port/output/db/create_verification_secret.rs index 612fbad..f266816 100644 --- a/src/identity/application/port/output/db/create_verification_secret.rs +++ b/src/identity/application/port/output/db/create_verification_secret.rs @@ -6,6 +6,7 @@ use derive_builder::Builder; use mockall::predicate::*; use mockall::*; use serde::{Deserialize, Serialize}; +use uuid::Uuid; use super::errors::*; #[cfg(test)] @@ -15,7 +16,7 @@ pub use tests::*; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Builder)] pub struct CreateSecretMsg { pub secret: String, - pub username: String, + pub user_id: Uuid, } pub const REGISTRATION_SECRET_PURPOSE: &str = "account_validation"; diff --git a/src/identity/application/port/output/db/delete_verification_secret.rs b/src/identity/application/port/output/db/delete_verification_secret.rs index 4318fbb..57fe34b 100644 --- a/src/identity/application/port/output/db/delete_verification_secret.rs +++ b/src/identity/application/port/output/db/delete_verification_secret.rs @@ -6,6 +6,7 @@ use derive_builder::Builder; use mockall::predicate::*; use mockall::*; use serde::{Deserialize, Serialize}; +use uuid::Uuid; pub use super::create_verification_secret::REGISTRATION_SECRET_PURPOSE; use super::errors::*; @@ -16,14 +17,14 @@ pub use tests::*; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Builder)] pub struct DeleteSecretMsg { pub secret: String, - pub username: String, + pub user_id: Uuid, } impl From for DeleteSecretMsg { fn from(value: super::verification_secret_exists::VerifySecretExistsMsg) -> Self { Self { secret: value.secret, - username: value.username, + user_id: value.user_id, } } } diff --git a/src/identity/application/port/output/db/get_verification_secret.rs b/src/identity/application/port/output/db/get_verification_secret.rs index 5550423..2128637 100644 --- a/src/identity/application/port/output/db/get_verification_secret.rs +++ b/src/identity/application/port/output/db/get_verification_secret.rs @@ -4,6 +4,7 @@ use mockall::predicate::*; use mockall::*; +use uuid::Uuid; pub use super::create_verification_secret::REGISTRATION_SECRET_PURPOSE; use super::errors::*; @@ -14,7 +15,7 @@ pub use tests::*; #[automock] #[async_trait::async_trait] pub trait GetVerificationSecretOutDBPort: Send + Sync { - async fn get_verification_secret(&self, username: &str) -> OutDBPortResult; + async fn get_verification_secret(&self, user_id: &Uuid) -> OutDBPortResult; } pub type GetVerificationSecretOutDBPortObj = std::sync::Arc; diff --git a/src/identity/application/port/output/db/mod.rs b/src/identity/application/port/output/db/mod.rs index e4a00f7..d3e2ec3 100644 --- a/src/identity/application/port/output/db/mod.rs +++ b/src/identity/application/port/output/db/mod.rs @@ -7,5 +7,5 @@ pub mod delete_verification_secret; pub mod email_exists; pub mod errors; pub mod get_verification_secret; -pub mod username_exists; +pub mod user_id_exists; pub mod verification_secret_exists; diff --git a/src/identity/application/port/output/db/username_exists.rs b/src/identity/application/port/output/db/user_id_exists.rs similarity index 56% rename from src/identity/application/port/output/db/username_exists.rs rename to src/identity/application/port/output/db/user_id_exists.rs index 2bcacbc..e3af83a 100644 --- a/src/identity/application/port/output/db/username_exists.rs +++ b/src/identity/application/port/output/db/user_id_exists.rs @@ -4,6 +4,7 @@ use mockall::predicate::*; use mockall::*; +use uuid::Uuid; use super::errors::*; #[cfg(test)] @@ -12,11 +13,11 @@ pub use tests::*; #[automock] #[async_trait::async_trait] -pub trait UsernameExistsOutDBPort: Send + Sync { - async fn username_exists(&self, username: &str) -> OutDBPortResult; +pub trait UserIDExistsOutDBPort: Send + Sync { + async fn user_id_exists(&self, user_id: &Uuid) -> OutDBPortResult; } -pub type UsernameExistsOutDBPortObj = std::sync::Arc; +pub type UserIDExistsOutDBPortObj = std::sync::Arc; #[cfg(test)] pub mod tests { @@ -24,17 +25,17 @@ pub mod tests { use std::sync::Arc; - pub fn mock_username_exists_db_port( + pub fn mock_user_id_exists_db_port( times: Option, returning: bool, - ) -> UsernameExistsOutDBPortObj { - let mut m = MockUsernameExistsOutDBPort::new(); + ) -> UserIDExistsOutDBPortObj { + let mut m = MockUserIDExistsOutDBPort::new(); if let Some(times) = times { - m.expect_username_exists() + m.expect_user_id_exists() .times(times) .returning(move |_| Ok(returning)); } else { - m.expect_username_exists().returning(move |_| Ok(returning)); + m.expect_user_id_exists().returning(move |_| Ok(returning)); } Arc::new(m) diff --git a/src/identity/application/port/output/db/verification_secret_exists.rs b/src/identity/application/port/output/db/verification_secret_exists.rs index 8feb3bf..bd65faa 100644 --- a/src/identity/application/port/output/db/verification_secret_exists.rs +++ b/src/identity/application/port/output/db/verification_secret_exists.rs @@ -6,6 +6,7 @@ use derive_builder::Builder; use mockall::predicate::*; use mockall::*; use serde::{Deserialize, Serialize}; +use uuid::Uuid; pub use super::create_verification_secret::REGISTRATION_SECRET_PURPOSE; use super::errors::*; @@ -16,7 +17,7 @@ pub use tests::*; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Builder)] pub struct VerifySecretExistsMsg { pub secret: String, - pub username: String, + pub user_id: Uuid, } #[automock] diff --git a/src/identity/application/port/output/mailer/account_validation_link.rs b/src/identity/application/port/output/mailer/account_validation_link.rs index 720796e..533aa5c 100644 --- a/src/identity/application/port/output/mailer/account_validation_link.rs +++ b/src/identity/application/port/output/mailer/account_validation_link.rs @@ -16,7 +16,7 @@ pub trait AccountValidationLinkOutMailerPort: Send + Sync { async fn account_validation_link( &self, to: &str, - username: &str, + first_name: &str, validation_secret: &str, ) -> OutMailerPortResult<()>; } diff --git a/src/identity/application/services/errors.rs b/src/identity/application/services/errors.rs index 38ef522..f03fe92 100644 --- a/src/identity/application/services/errors.rs +++ b/src/identity/application/services/errors.rs @@ -30,13 +30,10 @@ pub enum IdentityCommandError { impl From for IdentityCommandError { fn from(v: CredsError) -> Self { match v { - CredsError::ProfainityError => Self::BadUsername(v.to_string()), - CredsError::UsernameCaseMappedError => Self::BadUsername(v.to_string()), - CredsError::BlacklistError => Self::BadUsername(v.to_string()), CredsError::NotAnEmail => Self::BadEmail, CredsError::PasswordTooShort => Self::BadPassowrd(v.to_string()), CredsError::PasswordTooLong => Self::BadPassowrd(v.to_string()), - CredsError::Argon2Error(e) => Self::BadUsername(e.to_string()), + _ => Self::BadUsername(v.to_string()), } } } diff --git a/src/identity/application/services/mark_user_verified/command.rs b/src/identity/application/services/mark_user_verified/command.rs index be637ad..84138df 100644 --- a/src/identity/application/services/mark_user_verified/command.rs +++ b/src/identity/application/services/mark_user_verified/command.rs @@ -5,29 +5,32 @@ use super::*; use derive_getters::Getters; use serde::{Deserialize, Serialize}; +use uuid::Uuid; #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] pub struct MarkUserVerifiedCommand { - username: String, + user_id: Uuid, secret: String, } impl MarkUserVerifiedCommand { - pub fn new(username: String, secret: String) -> IdentityCommandResult { - Ok(Self { username, secret }) + pub fn new(user_id: Uuid, secret: String) -> IdentityCommandResult { + Ok(Self { user_id, secret }) } } #[cfg(test)] mod tests { + use crate::utils::uuid::tests::UUID; + use super::*; #[test] fn test_cmd() { - let username = "realaravinth"; + let user_id = UUID; let secret = "asdfasdf"; - let cmd = MarkUserVerifiedCommand::new(username.into(), secret.into()).unwrap(); - assert_eq!(cmd.username(), username); + let cmd = MarkUserVerifiedCommand::new(user_id.clone(), secret.into()).unwrap(); + assert_eq!(cmd.user_id(), &user_id); assert_eq!(cmd.secret(), secret); } } diff --git a/src/identity/application/services/mark_user_verified/service.rs b/src/identity/application/services/mark_user_verified/service.rs index 645872f..083392e 100644 --- a/src/identity/application/services/mark_user_verified/service.rs +++ b/src/identity/application/services/mark_user_verified/service.rs @@ -22,7 +22,7 @@ impl MarkUserVerifiedUseCase for MarkUserVerifiedService { cmd: command::MarkUserVerifiedCommand, ) -> IdentityResult<()> { let msg = VerifySecretExistsMsgBuilder::default() - .username(cmd.username().into()) + .user_id(cmd.user_id().clone()) .secret(cmd.secret().into()) .build() .unwrap(); @@ -48,13 +48,13 @@ impl MarkUserVerifiedUseCase for MarkUserVerifiedService { mod tests { use super::*; - use crate::tests::bdd::*; + use crate::{tests::bdd::*, utils::uuid::tests::UUID}; #[actix_rt::test] async fn test_service() { - let username = "realaravinth"; + let user_id = UUID; let secret = "password"; - let cmd = command::MarkUserVerifiedCommand::new(username.into(), secret.into()).unwrap(); + let cmd = command::MarkUserVerifiedCommand::new(user_id.clone(), secret.into()).unwrap(); // happy case { diff --git a/src/identity/application/services/register_user/command.rs b/src/identity/application/services/register_user/command.rs index 5539a66..6508830 100644 --- a/src/identity/application/services/register_user/command.rs +++ b/src/identity/application/services/register_user/command.rs @@ -3,35 +3,45 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use super::*; +use derive_builder::Builder; use derive_getters::Getters; use serde::{Deserialize, Serialize}; +#[derive( + Clone, Debug, Serialize, Deserialize, Builder, Eq, PartialEq, Ord, PartialOrd, Getters, +)] +pub struct UnvalidatedRegisterUserCommand { + first_name: String, + last_name: String, + email: String, + password: String, + confirm_password: String, +} + #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] pub struct RegisterUserCommand { - username: String, + first_name: String, + last_name: String, email: String, hashed_password: String, } -impl RegisterUserCommand { - pub fn new( - username: String, - email: String, - password: String, - confirm_password: String, +impl UnvalidatedRegisterUserCommand { + pub fn validate( + self, config: &argon2_creds::Config, - ) -> IdentityCommandResult { - let username = config.username(&username)?; - config.email(&email)?; + ) -> IdentityCommandResult { + config.email(&self.email)?; - if password != confirm_password { + if self.password != self.confirm_password { return Err(IdentityCommandError::PasswordsDontMatch); } - let hashed_password: String = config.password(&password)?; + let hashed_password: String = config.password(&self.password)?; - Ok(Self { - username, - email, + Ok(RegisterUserCommand { + first_name: self.first_name, + last_name: self.last_name, + email: self.email, hashed_password, }) } @@ -44,49 +54,47 @@ mod tests { #[test] fn test_cmd() { let config = argon2_creds::Config::default(); - RegisterUserCommand::new( - "realaravinth".into(), - "realaravinth@example.com".into(), - "asdfasdfasdfasdf".into(), - "asdfasdfasdfasdf".into(), - &config, - ) - .unwrap(); + let first_name = "John"; + let last_name = "Doe"; + let email = "john@example.com"; + let password = "sadfasdfasdf"; + let wrong_password = "sadfasdfasdf--wrong"; + + UnvalidatedRegisterUserCommandBuilder::default() + .first_name(first_name.into()) + .last_name(last_name.into()) + .email(email.into()) + .password(password.into()) + .confirm_password(password.into()) + .build() + .unwrap() + .validate(&config) + .unwrap(); assert_eq!( - RegisterUserCommand::new( - "realaravinth".into(), - "username".into(), - "password".into(), - "password".into(), - &config, - ) - .err(), - Some(IdentityCommandError::BadEmail) + UnvalidatedRegisterUserCommandBuilder::default() + .first_name(first_name.into()) + .last_name(last_name.into()) + .email(first_name.into()) + .password(password.into()) + .confirm_password(password.into()) + .build() + .unwrap() + .validate(&config), + Err(IdentityCommandError::BadEmail) ); - assert!(matches!( - RegisterUserCommand::new( - "username".into(), - "username@example.com".into(), - "password".into(), - "password".into(), - &config, - ) - .err(), - Some(IdentityCommandError::BadUsername(_)) - )); - assert_eq!( - RegisterUserCommand::new( - "realaravinth".into(), - "realaravinth@example.com".into(), - "password".into(), - "mismatch_password".into(), - &config, - ) - .err(), - Some(IdentityCommandError::PasswordsDontMatch) + UnvalidatedRegisterUserCommandBuilder::default() + .first_name(first_name.into()) + .last_name(last_name.into()) + .email(email.into()) + .password(password.into()) + .confirm_password(wrong_password.into()) + .build() + .unwrap() + .validate(&config), + Err(IdentityCommandError::PasswordsDontMatch) ); } } diff --git a/src/identity/application/services/register_user/events.rs b/src/identity/application/services/register_user/events.rs index 43fd097..947af43 100644 --- a/src/identity/application/services/register_user/events.rs +++ b/src/identity/application/services/register_user/events.rs @@ -5,12 +5,15 @@ use derive_builder::Builder; use derive_getters::Getters; use serde::{Deserialize, Serialize}; +use uuid::Uuid; #[derive( Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, )] pub struct UserRegisteredEvent { - username: String, + first_name: String, + last_name: String, + user_id: Uuid, email: String, hashed_password: String, is_verified: bool, diff --git a/src/identity/application/services/register_user/service.rs b/src/identity/application/services/register_user/service.rs index 593ed8b..d13995e 100644 --- a/src/identity/application/services/register_user/service.rs +++ b/src/identity/application/services/register_user/service.rs @@ -7,10 +7,10 @@ use derive_builder::Builder; use super::*; use crate::identity::application::port::output::{ - db::{create_verification_secret::*, email_exists::*, username_exists::*}, + db::{create_verification_secret::*, email_exists::*, user_id_exists::*}, mailer::account_validation_link::*, }; -use crate::utils::random_string::*; +use crate::utils::{random_string::*, uuid::*}; pub const SECRET_LEN: usize = 20; pub const REGISTRATION_SECRET_PURPOSE: &str = "account_validation"; @@ -18,9 +18,10 @@ pub const REGISTRATION_SECRET_PURPOSE: &str = "account_validation"; #[derive(Builder)] pub struct RegisterUserService { db_email_exists_adapter: EmailExistsOutDBPortObj, - db_username_exists_adapter: UsernameExistsOutDBPortObj, + db_user_id_exists_adapter: UserIDExistsOutDBPortObj, db_create_verification_secret_adapter: CreateVerificationSecretOutDBPortObj, mailer_account_validation_link_adapter: AccountValidationLinkOutMailerPortObj, + get_uuid: GetUUIDInterfaceObj, random_string_adapter: GenerateRandomStringInterfaceObj, } @@ -30,15 +31,6 @@ impl RegisterUserUseCase for RegisterUserService { &self, cmd: command::RegisterUserCommand, ) -> IdentityResult { - if self - .db_username_exists_adapter - .username_exists(cmd.username()) - .await - .unwrap() - { - return Err(IdentityError::DuplicateUsername); - } - if self .db_email_exists_adapter .email_exists(cmd.email()) @@ -48,13 +40,27 @@ impl RegisterUserUseCase for RegisterUserService { return Err(IdentityError::DuplicateEmail); } + let mut user_id = self.get_uuid.get_uuid(); + loop { + if self + .db_user_id_exists_adapter + .user_id_exists(&user_id) + .await + .unwrap() + { + user_id = self.get_uuid.get_uuid(); + } else { + break; + } + } + let secret = self.random_string_adapter.get_random(SECRET_LEN); self.db_create_verification_secret_adapter .create_verification_secret( CreateSecretMsgBuilder::default() .secret(secret.clone()) - .username(cmd.username().into()) + .user_id(user_id.clone()) .build() .unwrap(), ) @@ -62,12 +68,14 @@ impl RegisterUserUseCase for RegisterUserService { .unwrap(); self.mailer_account_validation_link_adapter - .account_validation_link(cmd.email(), cmd.username(), &secret) + .account_validation_link(cmd.email(), cmd.first_name(), &secret) .await .unwrap(); Ok(events::UserRegisteredEventBuilder::default() - .username(cmd.username().into()) + .first_name(cmd.first_name().into()) + .last_name(cmd.last_name().into()) + .user_id(user_id) .email(cmd.email().into()) .hashed_password(cmd.hashed_password().into()) .is_verified(false) @@ -84,6 +92,7 @@ mod tests { use crate::tests::bdd::*; use crate::utils::random_string::tests::*; + use crate::utils::uuid::tests::*; #[actix_rt::test] async fn test_service() { @@ -91,101 +100,88 @@ mod tests { let email = format!("{username}@example.com"); let password = "password"; let config = argon2_creds::Config::default(); - let cmd = command::RegisterUserCommand::new( - username.into(), - email.clone(), - password.into(), - password.into(), - &config, - ) - .unwrap(); + let cmd = command::UnvalidatedRegisterUserCommandBuilder::default() + .first_name(username.into()) + .last_name(username.into()) + .email(email.into()) + .password(password.into()) + .confirm_password(password.into()) + .build() + .unwrap() + .validate(&config) + .unwrap(); - // happy case - { - let s = RegisterUserServiceBuilder::default() - .db_username_exists_adapter(mock_username_exists_db_port( - IS_CALLED_ONLY_ONCE, - RETURNS_FALSE, - )) - .db_create_verification_secret_adapter(mock_create_verification_secret_db_port( - IS_CALLED_ONLY_ONCE, - )) - .db_email_exists_adapter(mock_email_exists_db_port( - IS_CALLED_ONLY_ONCE, - RETURNS_FALSE, - )) - .random_string_adapter(mock_generate_random_string( - IS_CALLED_ONLY_ONCE, - RETURNS_RANDOM_STRING.into(), - )) - .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( - IS_CALLED_ONLY_ONCE, - )) - .build() - .unwrap(); + let s = RegisterUserServiceBuilder::default() + .db_user_id_exists_adapter(mock_user_id_exists_db_port( + IS_CALLED_ONLY_ONCE, + RETURNS_FALSE, + )) + .db_create_verification_secret_adapter(mock_create_verification_secret_db_port( + IS_CALLED_ONLY_ONCE, + )) + .db_email_exists_adapter(mock_email_exists_db_port( + IS_CALLED_ONLY_ONCE, + RETURNS_FALSE, + )) + .random_string_adapter(mock_generate_random_string( + IS_CALLED_ONLY_ONCE, + RETURNS_RANDOM_STRING.into(), + )) + .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( + IS_CALLED_ONLY_ONCE, + )) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) + .build() + .unwrap(); - let res = s.register_user(cmd.clone()).await.unwrap(); - assert_eq!(res.username(), cmd.username()); - assert_eq!(res.email(), cmd.email()); - assert!(!res.is_admin()); - assert!(argon2_creds::Config::verify(res.hashed_password(), password).unwrap()) - } + let res = s.register_user(cmd.clone()).await.unwrap(); + assert_eq!(res.first_name(), cmd.first_name()); + assert_eq!(res.last_name(), cmd.last_name()); + assert_eq!(res.user_id(), &UUID); + assert_eq!(res.email(), cmd.email()); + assert!(!res.is_admin()); + assert!(argon2_creds::Config::verify(res.hashed_password(), password).unwrap()) + } + #[actix_rt::test] + async fn test_service_email_exists() { + let username = "realaravinth"; + let email = format!("{username}@example.com"); + let password = "password"; + let config = argon2_creds::Config::default(); + let cmd = command::UnvalidatedRegisterUserCommandBuilder::default() + .first_name(username.into()) + .last_name(username.into()) + .email(email.into()) + .password(password.into()) + .confirm_password(password.into()) + .build() + .unwrap() + .validate(&config) + .unwrap(); - // username exists - { - let s = RegisterUserServiceBuilder::default() - .db_username_exists_adapter(mock_username_exists_db_port( - IS_CALLED_ONLY_ONCE, - RETURNS_TRUE, - )) - .db_email_exists_adapter(mock_email_exists_db_port(IS_NEVER_CALLED, RETURNS_FALSE)) - .db_create_verification_secret_adapter(mock_create_verification_secret_db_port( - IS_NEVER_CALLED, - )) - .random_string_adapter(mock_generate_random_string( - IS_NEVER_CALLED, - RETURNS_RANDOM_STRING.into(), - )) - .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( - IS_NEVER_CALLED, - )) - .build() - .unwrap(); + let s = RegisterUserServiceBuilder::default() + .db_user_id_exists_adapter(mock_user_id_exists_db_port( + IGNORE_CALL_COUNT, + RETURNS_FALSE, + )) + .db_create_verification_secret_adapter(mock_create_verification_secret_db_port( + IS_NEVER_CALLED, + )) + .db_email_exists_adapter(mock_email_exists_db_port(IS_CALLED_ONLY_ONCE, RETURNS_TRUE)) + .random_string_adapter(mock_generate_random_string( + IS_NEVER_CALLED, + RETURNS_RANDOM_STRING.into(), + )) + .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( + IS_NEVER_CALLED, + )) + .get_uuid(mock_get_uuid(IS_NEVER_CALLED)) + .build() + .unwrap(); - assert_eq!( - s.register_user(cmd.clone()).await.err(), - Some(IdentityError::DuplicateUsername) - ); - } - - // email exists - { - let s = RegisterUserServiceBuilder::default() - .db_username_exists_adapter(mock_username_exists_db_port( - IS_CALLED_ONLY_ONCE, - RETURNS_FALSE, - )) - .db_create_verification_secret_adapter(mock_create_verification_secret_db_port( - IS_NEVER_CALLED, - )) - .db_email_exists_adapter(mock_email_exists_db_port( - IS_CALLED_ONLY_ONCE, - RETURNS_TRUE, - )) - .random_string_adapter(mock_generate_random_string( - IS_NEVER_CALLED, - RETURNS_RANDOM_STRING.into(), - )) - .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( - IS_NEVER_CALLED, - )) - .build() - .unwrap(); - - assert_eq!( - s.register_user(cmd.clone()).await.err(), - Some(IdentityError::DuplicateEmail) - ); - } + assert_eq!( + s.register_user(cmd.clone()).await.err(), + Some(IdentityError::DuplicateEmail) + ); } } diff --git a/src/identity/application/services/resend_verification_email/command.rs b/src/identity/application/services/resend_verification_email/command.rs index 82d91a1..67ee18b 100644 --- a/src/identity/application/services/resend_verification_email/command.rs +++ b/src/identity/application/services/resend_verification_email/command.rs @@ -5,54 +5,58 @@ use super::*; use derive_getters::Getters; use serde::{Deserialize, Serialize}; +use uuid::Uuid; #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] pub struct ResendVerificationEmailCommand { - username: String, + user_id: Uuid, + first_name: String, email: String, } impl ResendVerificationEmailCommand { pub fn new( - username: String, + user_id: Uuid, + first_name: String, email: String, config: &argon2_creds::Config, ) -> IdentityCommandResult { - let username = config.username(&username)?; config.email(&email)?; - Ok(Self { username, email }) + Ok(Self { + user_id, + first_name, + email, + }) } } #[cfg(test)] mod tests { + use crate::utils::uuid::tests::UUID; + use super::*; #[test] fn test_cmd() { let config = argon2_creds::Config::default(); ResendVerificationEmailCommand::new( - "realaravinth".into(), + UUID.clone(), + "john".into(), "realaravinth@example.com".into(), &config, ) .unwrap(); assert_eq!( - ResendVerificationEmailCommand::new("realaravinth".into(), "username".into(), &config,) - .err(), - Some(IdentityCommandError::BadEmail) - ); - - assert!(matches!( ResendVerificationEmailCommand::new( - "username".into(), - "username@example.com".into(), - &config, + UUID.clone(), + "john".into(), + "john".into(), + &config ) .err(), - Some(IdentityCommandError::BadUsername(_)) - )); + Some(IdentityCommandError::BadEmail) + ); } } diff --git a/src/identity/application/services/resend_verification_email/service.rs b/src/identity/application/services/resend_verification_email/service.rs index 57541d9..38b8580 100644 --- a/src/identity/application/services/resend_verification_email/service.rs +++ b/src/identity/application/services/resend_verification_email/service.rs @@ -6,14 +6,13 @@ use derive_builder::Builder; use super::*; use crate::identity::application::port::output::{ - db::{email_exists::*, get_verification_secret::*, username_exists::*}, + db::{email_exists::*, get_verification_secret::*}, mailer::account_validation_link::*, }; #[derive(Builder)] pub struct ResendVerificationEmailService { db_email_exists_adapter: EmailExistsOutDBPortObj, - db_username_exists_adapter: UsernameExistsOutDBPortObj, db_get_verification_secret_adapter: GetVerificationSecretOutDBPortObj, mailer_account_validation_link_adapter: AccountValidationLinkOutMailerPortObj, } @@ -24,15 +23,6 @@ impl ResendVerificationEmailUseCase for ResendVerificationEmailService { &self, cmd: command::ResendVerificationEmailCommand, ) -> IdentityResult<()> { - if self - .db_username_exists_adapter - .username_exists(cmd.username()) - .await - .unwrap() - { - return Err(IdentityError::DuplicateUsername); - } - if self .db_email_exists_adapter .email_exists(cmd.email()) @@ -44,12 +34,12 @@ impl ResendVerificationEmailUseCase for ResendVerificationEmailService { let secret = self .db_get_verification_secret_adapter - .get_verification_secret(cmd.username()) + .get_verification_secret(cmd.user_id()) .await .unwrap(); self.mailer_account_validation_link_adapter - .account_validation_link(cmd.email(), cmd.username(), &secret) + .account_validation_link(cmd.email(), cmd.first_name(), &secret) .await .unwrap(); @@ -62,91 +52,68 @@ mod tests { use super::*; use crate::tests::bdd::*; - use crate::utils::random_string::tests::*; + use crate::utils::uuid::tests::UUID; #[actix_rt::test] async fn test_service() { - let username = "realaravinth"; - let email = format!("{username}@example.com"); + let user_id = UUID; + let email = format!("john@example.com"); let secret = "asdfasdf"; let config = argon2_creds::Config::default(); - let cmd = - command::ResendVerificationEmailCommand::new(username.into(), email.clone(), &config) - .unwrap(); + let cmd = command::ResendVerificationEmailCommand::new( + UUID.clone(), + "john".into(), + email.clone(), + &config, + ) + .unwrap(); - // happy case - { - let s = ResendVerificationEmailServiceBuilder::default() - .db_username_exists_adapter(mock_username_exists_db_port( - IS_CALLED_ONLY_ONCE, - RETURNS_FALSE, - )) - .db_get_verification_secret_adapter(mock_get_verification_secret_db_port( - IS_CALLED_ONLY_ONCE, - secret.into(), - )) - .db_email_exists_adapter(mock_email_exists_db_port( - IS_CALLED_ONLY_ONCE, - RETURNS_FALSE, - )) - .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( - IS_CALLED_ONLY_ONCE, - )) - .build() - .unwrap(); + let s = ResendVerificationEmailServiceBuilder::default() + .db_get_verification_secret_adapter(mock_get_verification_secret_db_port( + IS_CALLED_ONLY_ONCE, + secret.into(), + )) + .db_email_exists_adapter(mock_email_exists_db_port( + IS_CALLED_ONLY_ONCE, + RETURNS_FALSE, + )) + .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( + IS_CALLED_ONLY_ONCE, + )) + .build() + .unwrap(); - s.resend_verification_email(cmd.clone()).await.unwrap(); - } + s.resend_verification_email(cmd.clone()).await.unwrap(); + } + #[actix_rt::test] + async fn test_service_email_exists() { + let user_id = UUID; + let email = format!("john@example.com"); + let secret = "asdfasdf"; + let config = argon2_creds::Config::default(); + let cmd = command::ResendVerificationEmailCommand::new( + UUID.clone(), + "john".into(), + email.clone(), + &config, + ) + .unwrap(); - // username exists - { - let s = ResendVerificationEmailServiceBuilder::default() - .db_username_exists_adapter(mock_username_exists_db_port( - IS_CALLED_ONLY_ONCE, - RETURNS_TRUE, - )) - .db_email_exists_adapter(mock_email_exists_db_port(IS_NEVER_CALLED, RETURNS_FALSE)) - .db_get_verification_secret_adapter(mock_get_verification_secret_db_port( - IS_NEVER_CALLED, - secret.into(), - )) - .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( - IS_NEVER_CALLED, - )) - .build() - .unwrap(); + let s = ResendVerificationEmailServiceBuilder::default() + .db_get_verification_secret_adapter(mock_get_verification_secret_db_port( + IS_NEVER_CALLED, + secret.into(), + )) + .db_email_exists_adapter(mock_email_exists_db_port(IS_CALLED_ONLY_ONCE, RETURNS_TRUE)) + .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( + IS_NEVER_CALLED, + )) + .build() + .unwrap(); - assert_eq!( - s.resend_verification_email(cmd.clone()).await.err(), - Some(IdentityError::DuplicateUsername) - ); - } - - // email exists - { - let s = ResendVerificationEmailServiceBuilder::default() - .db_username_exists_adapter(mock_username_exists_db_port( - IS_CALLED_ONLY_ONCE, - RETURNS_FALSE, - )) - .db_get_verification_secret_adapter(mock_get_verification_secret_db_port( - IS_NEVER_CALLED, - secret.into(), - )) - .db_email_exists_adapter(mock_email_exists_db_port( - IS_CALLED_ONLY_ONCE, - RETURNS_TRUE, - )) - .mailer_account_validation_link_adapter(mock_account_validation_link_db_port( - IS_NEVER_CALLED, - )) - .build() - .unwrap(); - - assert_eq!( - s.resend_verification_email(cmd.clone()).await.err(), - Some(IdentityError::DuplicateEmail) - ); - } + assert_eq!( + s.resend_verification_email(cmd.clone()).await.err(), + Some(IdentityError::DuplicateEmail) + ); } } diff --git a/src/identity/application/services/set_user_admin/command.rs b/src/identity/application/services/set_user_admin/command.rs index 174df06..f2feb19 100644 --- a/src/identity/application/services/set_user_admin/command.rs +++ b/src/identity/application/services/set_user_admin/command.rs @@ -26,6 +26,7 @@ impl SetAdminCommand { #[cfg(test)] mod tests { use crate::identity::domain::aggregate::UserBuilder; + use crate::utils::uuid::tests::UUID; use super::*; @@ -35,13 +36,15 @@ mod tests { SetAdminCommand::new( UserBuilder::default() - .username(username.into()) + .first_name(username.into()) + .last_name(username.into()) .email(username.into()) .hashed_password(username.into()) .is_verified(true) .email_verified(false) .is_admin(true) .deleted(false) + .user_id(UUID.clone()) .build() .unwrap(), ) @@ -50,13 +53,15 @@ mod tests { assert_eq!( SetAdminCommand::new( UserBuilder::default() - .username(username.into()) + .first_name(username.into()) + .last_name(username.into()) .email(username.into()) .hashed_password(username.into()) .is_verified(true) .is_admin(false) .email_verified(false) .deleted(false) + .user_id(UUID.clone()) .build() .unwrap(), ) diff --git a/src/identity/application/services/set_user_admin/service.rs b/src/identity/application/services/set_user_admin/service.rs index 3417b12..da9b8f5 100644 --- a/src/identity/application/services/set_user_admin/service.rs +++ b/src/identity/application/services/set_user_admin/service.rs @@ -20,6 +20,7 @@ impl SetUserAdminUseCase for SetUserAdminService { #[cfg(test)] mod tests { use crate::identity::domain::aggregate::UserBuilder; + use crate::utils::uuid::tests::UUID; use super::*; @@ -29,13 +30,15 @@ mod tests { let s = SetUserAdminService; let u = UserBuilder::default() - .username(username.into()) + .first_name(username.into()) + .last_name(username.into()) .email(username.into()) .hashed_password(username.into()) .is_verified(true) .email_verified(false) .is_admin(true) .deleted(false) + .user_id(UUID.clone()) .build() .unwrap(); diff --git a/src/identity/application/services/update_email/command.rs b/src/identity/application/services/update_email/command.rs index 19e229f..8e56799 100644 --- a/src/identity/application/services/update_email/command.rs +++ b/src/identity/application/services/update_email/command.rs @@ -4,19 +4,22 @@ use derive_getters::Getters; use serde::{Deserialize, Serialize}; +use uuid::Uuid; use super::*; #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] pub struct UpdateEmailCommand { new_email: String, - username: String, + user_id: Uuid, + first_name: String, } impl UpdateEmailCommand { pub fn new( - username: String, new_email: String, + user_id: Uuid, + first_name: String, supplied_password: String, actual_password_hash: &str, config: &argon2_creds::Config, @@ -27,27 +30,32 @@ impl UpdateEmailCommand { config.email(&new_email)?; Ok(Self { - username, + user_id, new_email, + first_name, }) } } #[cfg(test)] mod tests { + use crate::utils::uuid::tests::UUID; + use super::*; #[test] fn test_cmd() { let config = argon2_creds::Config::default(); let password = "adsfasdfasd"; - let username = "realaravinth"; + let first_name = "john"; + let user_id = UUID; let new_email = format!("newemail@example.com"); let hashed_password = config.password(password).unwrap(); assert_eq!( UpdateEmailCommand::new( - username.into(), new_email.clone(), + user_id.clone(), + first_name.into(), password.into(), &hashed_password, &config @@ -60,8 +68,9 @@ mod tests { // email is not valid email assert_eq!( UpdateEmailCommand::new( - username.into(), - username.into(), + user_id.to_string(), + user_id.clone(), + first_name.into(), password.into(), &hashed_password, &config @@ -73,9 +82,10 @@ mod tests { // wrong password assert_eq!( UpdateEmailCommand::new( - username.into(), - username.into(), - username.into(), + new_email.to_string(), + user_id.clone(), + first_name.into(), + first_name.into(), &hashed_password, &config ) diff --git a/src/identity/application/services/update_email/service.rs b/src/identity/application/services/update_email/service.rs index f5090d5..ce7f621 100644 --- a/src/identity/application/services/update_email/service.rs +++ b/src/identity/application/services/update_email/service.rs @@ -44,7 +44,7 @@ impl UpdateEmailUseCase for UpdateEmailService { .create_verification_secret( CreateSecretMsgBuilder::default() .secret(secret.clone()) - .username(cmd.username().into()) + .user_id(cmd.user_id().clone()) .build() .unwrap(), ) @@ -52,7 +52,7 @@ impl UpdateEmailUseCase for UpdateEmailService { .unwrap(); self.mailer_account_validation_link_adapter - .account_validation_link(cmd.new_email(), cmd.username(), &secret) + .account_validation_link(cmd.new_email(), cmd.first_name(), &secret) .await .unwrap(); @@ -66,18 +66,20 @@ mod tests { use crate::utils::random_string::tests::*; use crate::tests::bdd::*; + use crate::utils::uuid::tests::UUID; #[actix_rt::test] async fn test_service() { - let username = "realaravinth"; - let new_email = format!("{username}@example.com"); + let user_id = UUID; + let new_email = format!("john@example.com"); let password = "password"; let config = argon2_creds::Config::default(); let hashed_password = config.password(password).unwrap(); let cmd = command::UpdateEmailCommand::new( - username.into(), new_email.clone(), + user_id.clone(), + "john".into(), password.into(), &hashed_password, &config, diff --git a/src/identity/domain/aggregate.rs b/src/identity/domain/aggregate.rs index 2cd3067..4e77a69 100644 --- a/src/identity/domain/aggregate.rs +++ b/src/identity/domain/aggregate.rs @@ -2,15 +2,25 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +use async_trait::async_trait; +use cqrs_es::Aggregate; use derive_builder::Builder; use derive_getters::Getters; use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::identity::application::services::errors::*; +use crate::identity::application::services::events::UserEvent; +use crate::identity::application::services::UserCommand; +use crate::identity::application::services::UserServicesInterface; #[derive( Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, )] pub struct User { - username: String, + first_name: String, + last_name: String, + user_id: Uuid, email: String, hashed_password: String, is_verified: bool, @@ -22,13 +32,15 @@ pub struct User { impl Default for User { fn default() -> Self { User { - username: "".to_string(), + first_name: "".to_string(), + last_name: "".to_string(), email: "".to_string(), hashed_password: "".to_string(), is_verified: false, is_admin: false, email_verified: false, deleted: false, + user_id: Uuid::new_v4(), } } } @@ -64,6 +76,101 @@ impl User { } } +#[async_trait] +impl Aggregate for User { + type Command = UserCommand; + type Event = UserEvent; + type Error = IdentityError; + type Services = std::sync::Arc; + + // This identifier should be unique to the system. + fn aggregate_type() -> String { + "account".to_string() + } + + // The aggregate logic goes here. Note that this will be the _bulk_ of a CQRS system + // so expect to use helper functions elsewhere to keep the code clean. + async fn handle( + &self, + command: Self::Command, + services: &Self::Services, + ) -> Result, Self::Error> { + match command { + UserCommand::RegisterUser(cmd) => { + let res = services.register_user().register_user(cmd).await?; + Ok(vec![UserEvent::UserRegistered(res)]) + } + UserCommand::DeleteUser(cmd) => { + services.delete_user().delete_user(cmd).await; + Ok(vec![UserEvent::UserDeleted]) + } + UserCommand::Login(cmd) => { + let res = services.login().login(cmd).await; + Ok(vec![UserEvent::Loggedin(res)]) + } + UserCommand::UpdatePassword(cmd) => { + let res = services.update_password().update_password(cmd).await; + Ok(vec![UserEvent::PasswordUpdated(res)]) + } + UserCommand::UpdateEmail(cmd) => { + let res = services.update_email().update_email(cmd).await?; + Ok(vec![UserEvent::EmailUpdated(res)]) + } + UserCommand::MarkUserVerified(cmd) => { + services + .mark_user_verified() + .mark_user_verified(cmd) + .await?; + Ok(vec![UserEvent::UserVerified]) + } + UserCommand::SetAdmin(cmd) => { + let res = services.set_user_admin().set_user_admin(cmd).await; + Ok(vec![UserEvent::UserPromotedToAdmin(res)]) + } + UserCommand::ResendVerificationEmail(cmd) => { + services + .resend_verification_email() + .resend_verification_email(cmd) + .await?; + Ok(vec![UserEvent::VerificationEmailResent]) + } + } + } + + fn apply(&mut self, event: Self::Event) { + match event { + UserEvent::UserRegistered(e) => { + self.first_name = e.first_name().into(); + self.last_name = e.last_name().into(); + self.user_id = e.user_id().clone(); + self.email = e.email().into(); + self.hashed_password = e.hashed_password().into(); + self.is_admin = e.is_admin().clone(); + self.email_verified = e.email_verified().clone(); + self.is_verified = e.is_verified().clone(); + self.deleted = false; + } + UserEvent::UserDeleted => { + self.set_deleted(true); + } + UserEvent::Loggedin(_) => (), + UserEvent::PasswordUpdated(_) => (), + UserEvent::EmailUpdated(e) => { + self.set_email(e.new_email().into()); + self.set_email_verified(false); + } + UserEvent::UserVerified => { + self.set_is_verified(true); + self.set_email_verified(true); + } + UserEvent::UserPromotedToAdmin(_) => { + self.set_is_admin(true); + } + UserEvent::VerificationEmailResent => (), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/inventory/adapters/output/db/postgres/category_view.rs b/src/inventory/adapters/output/db/postgres/category_view.rs index 37f61ed..154fb73 100644 --- a/src/inventory/adapters/output/db/postgres/category_view.rs +++ b/src/inventory/adapters/output/db/postgres/category_view.rs @@ -6,13 +6,14 @@ use async_trait::async_trait; use cqrs_es::persist::GenericQuery; use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository}; use cqrs_es::{EventEnvelope, Query, View}; +use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::errors::*; use super::InventoryDBPostgresAdapter; use crate::inventory::domain::category_aggregate::Category; use crate::inventory::domain::events::InventoryEvent; -use serde::{Deserialize, Serialize}; +use crate::utils::parse_aggregate_id::parse_aggregate_id; pub const NEW_CATEGORY_NON_UUID: &str = "new_category_non_uuid-asdfa"; @@ -46,11 +47,10 @@ impl View for CategoryView { #[async_trait] impl ViewRepository for InventoryDBPostgresAdapter { async fn load(&self, category_id: &str) -> Result, PersistenceError> { - let category_id = - match super::utils::parse_aggregate_id(category_id, NEW_CATEGORY_NON_UUID)? { - Some((val, _)) => return Ok(Some(val)), - None => Uuid::parse_str(category_id).unwrap(), - }; + let category_id = match parse_aggregate_id(category_id, NEW_CATEGORY_NON_UUID)? { + Some((val, _)) => return Ok(Some(val)), + None => Uuid::parse_str(category_id).unwrap(), + }; let res = sqlx::query_as!( CategoryView, @@ -72,11 +72,10 @@ impl ViewRepository for InventoryDBPostgresAdapter { &self, category_id: &str, ) -> Result, PersistenceError> { - let category_id = - match super::utils::parse_aggregate_id(category_id, NEW_CATEGORY_NON_UUID)? { - Some(val) => return Ok(Some(val)), - None => Uuid::parse_str(category_id).unwrap(), - }; + let category_id = match parse_aggregate_id(category_id, NEW_CATEGORY_NON_UUID)? { + Some(val) => return Ok(Some(val)), + None => Uuid::parse_str(category_id).unwrap(), + }; let res = sqlx::query_as!( CategoryView, diff --git a/src/inventory/adapters/output/db/postgres/mod.rs b/src/inventory/adapters/output/db/postgres/mod.rs index 0edfba8..7baaec0 100644 --- a/src/inventory/adapters/output/db/postgres/mod.rs +++ b/src/inventory/adapters/output/db/postgres/mod.rs @@ -15,7 +15,6 @@ mod errors; mod store_id_exists; mod store_name_exists; mod store_view; -mod utils; #[derive(Clone)] pub struct InventoryDBPostgresAdapter { diff --git a/src/inventory/adapters/output/db/postgres/store_id_exists.rs b/src/inventory/adapters/output/db/postgres/store_id_exists.rs index 62f2034..ab64000 100644 --- a/src/inventory/adapters/output/db/postgres/store_id_exists.rs +++ b/src/inventory/adapters/output/db/postgres/store_id_exists.rs @@ -32,6 +32,8 @@ impl StoreIDExistsDBPort for InventoryDBPostgresAdapter { mod tests { use uuid::Uuid; + use crate::utils::uuid::tests::UUID; + use super::*; #[actix_rt::test] @@ -47,7 +49,7 @@ mod tests { let store = StoreBuilder::default() .name("store_name".into()) - .owner("store_owner".into()) + .owner(UUID.clone()) .address(Some("store_address".into())) .store_id(store_id) .build() diff --git a/src/inventory/adapters/output/db/postgres/store_name_exists.rs b/src/inventory/adapters/output/db/postgres/store_name_exists.rs index 0df0a1e..0ac5260 100644 --- a/src/inventory/adapters/output/db/postgres/store_name_exists.rs +++ b/src/inventory/adapters/output/db/postgres/store_name_exists.rs @@ -32,6 +32,8 @@ impl StoreNameExistsDBPort for InventoryDBPostgresAdapter { mod tests { use uuid::Uuid; + use crate::utils::uuid::tests::UUID; + use super::*; #[actix_rt::test] @@ -47,7 +49,7 @@ mod tests { let store = StoreBuilder::default() .name("store_name".into()) - .owner("store_owner".into()) + .owner(UUID.clone()) .address(Some("store_address".into())) .store_id(store_id) .build() diff --git a/src/inventory/adapters/output/db/postgres/store_view.rs b/src/inventory/adapters/output/db/postgres/store_view.rs index 3a8f030..473f24a 100644 --- a/src/inventory/adapters/output/db/postgres/store_view.rs +++ b/src/inventory/adapters/output/db/postgres/store_view.rs @@ -12,6 +12,7 @@ use super::errors::*; use super::InventoryDBPostgresAdapter; use crate::inventory::domain::events::InventoryEvent; use crate::inventory::domain::store_aggregate::Store; +use crate::utils::parse_aggregate_id::parse_aggregate_id; pub const NEW_STORE_NON_UUID: &str = "new_store_non_uuid-asdfa"; @@ -22,7 +23,7 @@ pub struct StoreView { name: String, address: Option, store_id: Uuid, - owner: String, + owner: Uuid, } // This updates the view with events as they are committed. @@ -45,7 +46,7 @@ impl View for StoreView { #[async_trait] impl ViewRepository for InventoryDBPostgresAdapter { async fn load(&self, store_id: &str) -> Result, PersistenceError> { - let store_id = match super::utils::parse_aggregate_id(store_id, NEW_STORE_NON_UUID)? { + let store_id = match parse_aggregate_id(store_id, NEW_STORE_NON_UUID)? { Some((val, _)) => return Ok(Some(val)), None => Uuid::parse_str(store_id).unwrap(), }; @@ -70,7 +71,7 @@ impl ViewRepository for InventoryDBPostgresAdapter { &self, store_id: &str, ) -> Result, PersistenceError> { - let store_id = match super::utils::parse_aggregate_id(store_id, NEW_STORE_NON_UUID)? { + let store_id = match parse_aggregate_id(store_id, NEW_STORE_NON_UUID)? { Some(val) => return Ok(Some(val)), None => Uuid::parse_str(store_id).unwrap(), }; @@ -247,7 +248,7 @@ mod tests { )) .add_category(mock_add_category_service( IS_NEVER_CALLED, - AddCategoryCommand::new("foo".into(), None, UUID.clone(), "bar".into()).unwrap(), + AddCategoryCommand::new("foo".into(), None, UUID.clone(), UUID.clone()).unwrap(), )) .build() .unwrap(); @@ -265,7 +266,7 @@ mod tests { ); let rand = crate::utils::random_string::GenerateRandomString {}; - let cmd = AddStoreCommand::new(rand.get_random(10), None, "me".into()).unwrap(); + let cmd = AddStoreCommand::new(rand.get_random(10), None, UUID.clone()).unwrap(); cqrs.execute("", InventoryCommand::AddStore(cmd.clone())) .await .unwrap(); diff --git a/src/inventory/application/services/add_category_service.rs b/src/inventory/application/services/add_category_service.rs index d9ccde0..2cf7bd0 100644 --- a/src/inventory/application/services/add_category_service.rs +++ b/src/inventory/application/services/add_category_service.rs @@ -78,7 +78,7 @@ impl AddCategoryUseCase for AddCategoryService { Ok(CategoryAddedEventBuilder::default() .name(category.name().into()) .description(category.description().as_ref().map(|s| s.to_string())) - .added_by_user(cmd.adding_by().into()) + .added_by_user(cmd.adding_by().clone()) .store_id(category.store_id().clone()) .category_id(category.category_id().clone()) .build() @@ -104,7 +104,7 @@ pub mod tests { let res = CategoryAddedEventBuilder::default() .name(cmd.name().into()) .description(cmd.description().as_ref().map(|s| s.to_string())) - .added_by_user(cmd.adding_by().into()) + .added_by_user(cmd.adding_by().clone()) .store_id(cmd.store_id().clone()) .category_id(UUID.clone()) .build() @@ -125,7 +125,7 @@ pub mod tests { async fn test_service_category_doesnt_exist() { let name = "foo"; let description = "bar"; - let username = "baz"; + let user_id = UUID; let store_id = Uuid::new_v4(); // description = None @@ -133,7 +133,7 @@ pub mod tests { name.into(), Some(description.into()), store_id.clone(), - username.into(), + user_id.clone(), ) .unwrap(); @@ -158,7 +158,7 @@ pub mod tests { async fn test_service_category_name_exists_for_store() { let name = "foo"; let description = "bar"; - let username = "baz"; + let user_id = UUID; let store_id = Uuid::new_v4(); // description = None @@ -166,7 +166,7 @@ pub mod tests { name.into(), Some(description.into()), store_id.clone(), - username.into(), + user_id.clone(), ) .unwrap(); @@ -175,7 +175,7 @@ pub mod tests { IS_CALLED_ONLY_ONCE, )) .db_category_id_exists(mock_category_id_exists_db_port_false(IS_NEVER_CALLED)) - .get_uuid(mock_get_uuid(IS_NEVER_CALLED)) + .get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE)) .build() .unwrap(); diff --git a/src/inventory/application/services/add_store_service.rs b/src/inventory/application/services/add_store_service.rs index 5181d2b..6c0be72 100644 --- a/src/inventory/application/services/add_store_service.rs +++ b/src/inventory/application/services/add_store_service.rs @@ -43,7 +43,7 @@ impl AddStoreUseCase for AddStoreService { let mut store = StoreBuilder::default() .name(cmd.name().into()) .address(cmd.address().as_ref().map(|s| s.to_string())) - .owner(cmd.owner().into()) + .owner(cmd.owner().clone()) .store_id(store_id.clone()) .build() .unwrap(); @@ -58,6 +58,7 @@ impl AddStoreUseCase for AddStoreService { store = StoreBuilder::default() .name(cmd.name().into()) .address(cmd.address().as_ref().map(|s| s.to_string())) + .owner(cmd.owner().clone()) .store_id(store_id.clone()) .build() .unwrap(); @@ -70,7 +71,7 @@ impl AddStoreUseCase for AddStoreService { Ok(StoreAddedEventBuilder::default() .name(store.name().into()) .address(store.address().as_ref().map(|s| s.to_string())) - .owner(cmd.owner().into()) + .owner(cmd.owner().clone()) .store_id(store_id.clone()) .build() .unwrap()) @@ -93,7 +94,7 @@ pub mod tests { let res = StoreAddedEventBuilder::default() .name(cmd.name().into()) .address(cmd.address().as_ref().map(|s| s.to_string())) - .owner(cmd.owner().into()) + .owner(cmd.owner().clone()) .store_id(UUID.clone()) .build() .unwrap(); @@ -113,10 +114,10 @@ pub mod tests { async fn test_service_store_id_doesnt_exist() { let name = "foo"; let address = "bar"; - let username = "baz"; + let owner = UUID; // address = None - let cmd = AddStoreCommand::new(name.into(), Some(address.into()), username.into()).unwrap(); + let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner.clone()).unwrap(); let s = AddStoreServiceBuilder::default() .db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) @@ -136,10 +137,10 @@ pub mod tests { async fn test_service_store_name_exists() { let name = "foo"; let address = "bar"; - let username = "baz"; + let owner = UUID; // address = None - let cmd = AddStoreCommand::new(name.into(), Some(address.into()), username.into()).unwrap(); + let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner.clone()).unwrap(); let s = AddStoreServiceBuilder::default() .db_store_id_exists(mock_store_id_exists_db_port_false(IS_NEVER_CALLED)) diff --git a/src/inventory/domain/add_category_command.rs b/src/inventory/domain/add_category_command.rs index 0beb41d..e265c54 100644 --- a/src/inventory/domain/add_category_command.rs +++ b/src/inventory/domain/add_category_command.rs @@ -17,7 +17,7 @@ pub struct AddCategoryCommand { name: String, description: Option, store_id: Uuid, - adding_by: String, + adding_by: Uuid, } impl AddCategoryCommand { @@ -25,7 +25,7 @@ impl AddCategoryCommand { name: String, description: Option, store_id: Uuid, - adding_by: String, + adding_by: Uuid, ) -> Result { let description: Option = if let Some(description) = description { let description = description.trim(); @@ -56,19 +56,21 @@ impl AddCategoryCommand { mod tests { use super::*; + use crate::utils::uuid::tests::UUID; + #[test] fn test_cmd() { let name = "foo"; let description = "bar"; - let username = "baz"; + let adding_by = UUID; let store_id = Uuid::new_v4(); // description = None - let cmd = - AddCategoryCommand::new(name.into(), None, store_id.clone(), username.into()).unwrap(); + let cmd = AddCategoryCommand::new(name.into(), None, store_id.clone(), adding_by.clone()) + .unwrap(); assert_eq!(cmd.name(), name); assert_eq!(cmd.description(), &None); - assert_eq!(cmd.adding_by(), username); + assert_eq!(cmd.adding_by(), &adding_by); assert_eq!(cmd.store_id(), &store_id); // description = Some @@ -76,12 +78,12 @@ mod tests { name.into(), Some(description.into()), store_id.clone(), - username.into(), + adding_by.clone(), ) .unwrap(); assert_eq!(cmd.name(), name); assert_eq!(cmd.description(), &Some(description.to_owned())); - assert_eq!(cmd.adding_by(), username); + assert_eq!(cmd.adding_by(), &adding_by); assert_eq!(cmd.store_id(), &store_id); // AddCategoryCommandError::NameIsEmpty @@ -90,7 +92,7 @@ mod tests { "".into(), Some(description.into()), store_id.clone(), - username.into() + adding_by.clone(), ), Err(AddCategoryCommandError::NameIsEmpty) ) diff --git a/src/inventory/domain/add_store_command.rs b/src/inventory/domain/add_store_command.rs index 1a57f60..3277402 100644 --- a/src/inventory/domain/add_store_command.rs +++ b/src/inventory/domain/add_store_command.rs @@ -5,6 +5,7 @@ use derive_getters::Getters; use derive_more::{Display, Error}; use serde::{Deserialize, Serialize}; +use uuid::Uuid; #[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub enum AddStoreCommandError { @@ -15,14 +16,14 @@ pub enum AddStoreCommandError { pub struct AddStoreCommand { name: String, address: Option, - owner: String, + owner: Uuid, } impl AddStoreCommand { pub fn new( name: String, address: Option, - owner: String, + owner: Uuid, ) -> Result { let address: Option = if let Some(address) = address { let address = address.trim(); @@ -50,29 +51,31 @@ impl AddStoreCommand { #[cfg(test)] mod tests { + use crate::utils::uuid::tests::UUID; + use super::*; #[test] fn test_cmd() { let name = "foo"; let address = "bar"; - let username = "baz"; + let owner = UUID.clone(); // address = None - let cmd = AddStoreCommand::new(name.into(), None, username.into()).unwrap(); + let cmd = AddStoreCommand::new(name.into(), None, owner.clone()).unwrap(); assert_eq!(cmd.name(), name); assert_eq!(cmd.address(), &None); - assert_eq!(cmd.owner(), username); + assert_eq!(cmd.owner(), &owner); // address = Some - let cmd = AddStoreCommand::new(name.into(), Some(address.into()), username.into()).unwrap(); + let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner.clone()).unwrap(); assert_eq!(cmd.name(), name); assert_eq!(cmd.address(), &Some(address.to_owned())); - assert_eq!(cmd.owner(), username); + assert_eq!(cmd.owner(), &owner); // AddStoreCommandError::NameIsEmpty assert_eq!( - AddStoreCommand::new("".into(), Some(address.into()), username.into()), + AddStoreCommand::new("".into(), Some(address.into()), owner.clone()), Err(AddStoreCommandError::NameIsEmpty) ) } diff --git a/src/inventory/domain/category_added_event.rs b/src/inventory/domain/category_added_event.rs index e2ead93..32923f0 100644 --- a/src/inventory/domain/category_added_event.rs +++ b/src/inventory/domain/category_added_event.rs @@ -13,7 +13,7 @@ use uuid::Uuid; pub struct CategoryAddedEvent { name: String, description: Option, - added_by_user: String, + added_by_user: Uuid, category_id: Uuid, store_id: Uuid, } diff --git a/src/inventory/domain/category_aggregate.rs b/src/inventory/domain/category_aggregate.rs index a0e32ff..dc3ee20 100644 --- a/src/inventory/domain/category_aggregate.rs +++ b/src/inventory/domain/category_aggregate.rs @@ -92,7 +92,7 @@ mod aggregate_tests { fn test_create_store() { let name = "category_name"; let description = Some("category_description".to_string()); - let adding_by = "store_owner"; + let adding_by = UUID; let store_id = Uuid::new_v4(); let category_id = UUID.clone(); @@ -100,14 +100,14 @@ mod aggregate_tests { name.into(), description.clone(), store_id.clone(), - adding_by.into(), + adding_by.clone(), ) .unwrap(); let expected = CategoryAddedEventBuilder::default() .name(cmd.name().into()) .description(cmd.description().as_ref().map(|s| s.to_string())) - .added_by_user(cmd.adding_by().into()) + .added_by_user(cmd.adding_by().clone()) .store_id(cmd.store_id().clone()) .category_id(category_id.clone()) .build() diff --git a/src/inventory/domain/store_added_event.rs b/src/inventory/domain/store_added_event.rs index 7a67bda..6336da0 100644 --- a/src/inventory/domain/store_added_event.rs +++ b/src/inventory/domain/store_added_event.rs @@ -13,6 +13,6 @@ use uuid::Uuid; pub struct StoreAddedEvent { name: String, address: Option, - owner: String, + owner: Uuid, store_id: Uuid, } diff --git a/src/inventory/domain/store_aggregate.rs b/src/inventory/domain/store_aggregate.rs index 4ea22d2..7542f3b 100644 --- a/src/inventory/domain/store_aggregate.rs +++ b/src/inventory/domain/store_aggregate.rs @@ -20,7 +20,7 @@ use super::{commands::InventoryCommand, events::InventoryEvent}; pub struct Store { name: String, address: Option, - owner: String, + owner: Uuid, store_id: Uuid, } @@ -62,7 +62,7 @@ impl Aggregate for Store { InventoryEvent::StoreAdded(e) => { self.name = e.name().into(); self.address = e.address().as_ref().map(|s| s.to_string()); - self.owner = e.owner().into(); + self.owner = e.owner().clone(); self.store_id = e.store_id().clone(); } _ => (), @@ -95,7 +95,7 @@ mod tests { fn test_create_store() { let name = "store_name"; let address = Some("store_address".to_string()); - let owner = "store_owner"; + let owner = UUID; let store_id = UUID; let expected = StoreAddedEventBuilder::default() @@ -107,7 +107,7 @@ mod tests { .unwrap(); let expected = InventoryEvent::StoreAdded(expected); - let cmd = AddStoreCommand::new(name.into(), address.clone(), owner.into()).unwrap(); + let cmd = AddStoreCommand::new(name.into(), address.clone(), owner.clone()).unwrap(); let mut services = MockInventoryServicesInterface::new(); services diff --git a/src/settings/database.rs b/src/settings/database.rs index 5f449a7..2b49fee 100644 --- a/src/settings/database.rs +++ b/src/settings/database.rs @@ -68,12 +68,19 @@ mod tests { #[test] fn test_db_env_override() { let init_settings = crate::settings::Settings::new().unwrap(); - env_helper!( - init_settings, - "DATABASE_URL", - "postgres://test_db_env_override", - database.url - ); + std::thread::spawn(move || { + env_helper!( + init_settings, + "DATABASE_URL", + "postgres://test_db_env_override", + database.url + ); + + assert!(true); + }) + .join() + .unwrap(); + env_helper!(init_settings, "VANIKAM_database_POOL", 99, database.pool); } } diff --git a/src/settings/mod.rs b/src/settings/mod.rs index a1e7f46..c4d45c1 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -130,6 +130,7 @@ pub mod tests { macro_rules! env_helper { ($init_settings:ident, $env:expr, $val:expr, $val_typed:expr, $($param:ident).+) => { println!("Setting env var {} to {} for test", $env, $val); + let current = env::var($env); env::set_var($env, $val); { let new_settings = $crate::settings::Settings::new().unwrap(); @@ -137,6 +138,9 @@ pub mod tests { assert_ne!(new_settings.$($param).+, $init_settings.$($param).+); } env::remove_var($env); + if let Ok(current) = current { + env::set_var($env, current); + } }; @@ -150,6 +154,7 @@ pub mod tests { let mut db_url = Url::parse(&settings.database.url).unwrap(); db_url.set_path(&GenerateRandomString.get_random(12)); settings.database.url = db_url.to_string(); + settings.database.pool = 1; settings } diff --git a/src/tests/bdd.rs b/src/tests/bdd.rs index ed12203..bf32c87 100644 --- a/src/tests/bdd.rs +++ b/src/tests/bdd.rs @@ -3,9 +3,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pub const IS_CALLED_ONLY_ONCE: Option = Some(1); -pub const IS_NEVER_CALLED: Option = None; +pub const IS_NEVER_CALLED: Option = Some(0); pub const IS_CALLED_ONLY_TWICE: Option = Some(2); pub const RETURNS_RANDOM_STRING: &str = "test_random_string"; +pub const IGNORE_CALL_COUNT: Option = None; pub const RETURNS_TRUE: bool = true; pub const RETURNS_FALSE: bool = false; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index a52985c..fa2f8f5 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -2,5 +2,6 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +pub mod parse_aggregate_id; pub mod random_string; pub mod uuid; diff --git a/src/inventory/adapters/output/db/postgres/utils.rs b/src/utils/parse_aggregate_id.rs similarity index 100% rename from src/inventory/adapters/output/db/postgres/utils.rs rename to src/utils/parse_aggregate_id.rs