Merge pull request 'fix: replace username with first and last name and use user_id UUID for primary keys' (#32) from user-di into master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

Reviewed-on: #32
This commit is contained in:
Aravinth Manivannan 2024-07-14 22:53:49 +05:30
commit caf580c6df
68 changed files with 737 additions and 879 deletions

View file

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "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": { "describe": {
"columns": [ "columns": [
{ {
@ -11,12 +11,12 @@
], ],
"parameters": { "parameters": {
"Left": [ "Left": [
"Text" "Uuid"
] ]
}, },
"nullable": [ "nullable": [
null null
] ]
}, },
"hash": "ed4bd44b2a0595cd80b36ce70b30ab55af1af12c1f22a2d4a2baf8af4569cf73" "hash": "004d12b7ccb1b21c39ef6de716953bf039bdba5096ae139be7656170ff45613f"
} }

View file

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "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": { "describe": {
"columns": [], "columns": [],
"parameters": { "parameters": {
@ -8,10 +8,10 @@
"Varchar", "Varchar",
"Timestamptz", "Timestamptz",
"Text", "Text",
"Text" "Uuid"
] ]
}, },
"nullable": [] "nullable": []
}, },
"hash": "487deedfaaf10c4ab02fc223b8fec4bf359b082331adfa788096742673168be3" "hash": "0fbaa8084440adce8a6162d67e3e57b6062cc17bbbbeb5ea3e1e58db17ac8240"
} }

View file

@ -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"
}

View file

@ -9,7 +9,7 @@
"Text", "Text",
"Text", "Text",
"Uuid", "Uuid",
"Text" "Uuid"
] ]
}, },
"nullable": [] "nullable": []

View file

@ -21,7 +21,7 @@
{ {
"ordinal": 3, "ordinal": 3,
"name": "owner", "name": "owner",
"type_info": "Text" "type_info": "Uuid"
} }
], ],
"parameters": { "parameters": {

View file

@ -1,12 +1,12 @@
{ {
"db_name": "PostgreSQL", "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": { "describe": {
"columns": [ "columns": [
{ {
"ordinal": 0, "ordinal": 0,
"name": "view_id", "name": "user_id",
"type_info": "Text" "type_info": "Uuid"
}, },
{ {
"ordinal": 1, "ordinal": 1,
@ -16,7 +16,7 @@
], ],
"parameters": { "parameters": {
"Left": [ "Left": [
"Text" "Uuid"
] ]
}, },
"nullable": [ "nullable": [
@ -24,5 +24,5 @@
false false
] ]
}, },
"hash": "fa1c65d51e3e0521d6a20998f4b80b615c9e0ffe9ceda82e0bce410c9aca39a0" "hash": "6523c83a859d7ca283d209133a10d4ac74b6b0358bdf1e17f1a54e2cc02e305b"
} }

View file

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "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": { "describe": {
"columns": [ "columns": [
{ {
@ -11,7 +11,7 @@
], ],
"parameters": { "parameters": {
"Left": [ "Left": [
"Text", "Uuid",
"Text", "Text",
"Text" "Text"
] ]
@ -20,5 +20,5 @@
null null
] ]
}, },
"hash": "3edf94a78114819085b573ff51702faee1c444c24bbf6d33ec1b4b245dd9f675" "hash": "70e6216e30f90175d4c3bad51ff51f5fa2b6f965d868b15c53264cb8cd6b4053"
} }

View file

@ -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"
}

View file

@ -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"
}

View file

@ -1,42 +1,52 @@
{ {
"db_name": "PostgreSQL", "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": { "describe": {
"columns": [ "columns": [
{ {
"ordinal": 0, "ordinal": 0,
"name": "username", "name": "first_name",
"type_info": "Text" "type_info": "Text"
}, },
{ {
"ordinal": 1, "ordinal": 1,
"name": "email", "name": "last_name",
"type_info": "Text" "type_info": "Text"
}, },
{ {
"ordinal": 2, "ordinal": 2,
"name": "user_id",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "email",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "hashed_password", "name": "hashed_password",
"type_info": "Text" "type_info": "Text"
}, },
{ {
"ordinal": 3, "ordinal": 5,
"name": "is_admin", "name": "is_admin",
"type_info": "Bool" "type_info": "Bool"
}, },
{ {
"ordinal": 4, "ordinal": 6,
"name": "is_verified", "name": "is_verified",
"type_info": "Bool" "type_info": "Bool"
}, },
{ {
"ordinal": 5, "ordinal": 7,
"name": "deleted", "name": "deleted",
"type_info": "Bool" "type_info": "Bool"
} }
], ],
"parameters": { "parameters": {
"Left": [ "Left": [
"Text" "Uuid"
] ]
}, },
"nullable": [ "nullable": [
@ -45,8 +55,10 @@
false, false,
false, false,
false, false,
false,
false,
false false
] ]
}, },
"hash": "803f868ee4a79fefdd85c5d0a034a5925c3bb7de87828095971cb19bc1fc7550" "hash": "7a59c989d043c249cd04fe24544cf9ea55e1329ce4b53889947478c2e766ea1a"
} }

View file

@ -9,7 +9,7 @@
"Text", "Text",
"Text", "Text",
"Uuid", "Uuid",
"Text" "Uuid"
] ]
}, },
"nullable": [] "nullable": []

View file

@ -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"
}

View file

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "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": { "describe": {
"columns": [ "columns": [
{ {
@ -11,7 +11,7 @@
], ],
"parameters": { "parameters": {
"Left": [ "Left": [
"Text", "Uuid",
"Text" "Text"
] ]
}, },
@ -19,5 +19,5 @@
false false
] ]
}, },
"hash": "7c48c7569b16e9600aa31fbf14b9ceeb195da7cde0082be861369ef4f997c534" "hash": "ec80b5dc41697e7df7112962aba8185040fc0a101edea4b7e0c23f6468300196"
} }

View file

@ -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"
}

View file

@ -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"
}

View file

@ -17,17 +17,16 @@ CREATE TABLE IF NOT EXISTS events
CREATE TABLE IF NOT EXISTS user_query CREATE TABLE IF NOT EXISTS user_query
( (
view_id text NOT NULL,
version bigint CHECK (version >= 0) 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, email TEXT NOT NULL UNIQUE,
hashed_password TEXT NOT NULL, hashed_password TEXT NOT NULL,
is_admin BOOLEAN NOT NULL DEFAULT FALSE, is_admin BOOLEAN NOT NULL DEFAULT FALSE,
is_verified BOOLEAN NOT NULL DEFAULT FALSE, is_verified BOOLEAN NOT NULL DEFAULT FALSE,
deleted 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);

View file

@ -6,6 +6,6 @@ CREATE TABLE IF NOT EXISTS verification_otp (
secret VARCHAR(32) NOT NULL UNIQUE, secret VARCHAR(32) NOT NULL UNIQUE,
created_at timestamp with time zone DEFAULT (CURRENT_TIMESTAMP), created_at timestamp with time zone DEFAULT (CURRENT_TIMESTAMP),
purpose TEXT NOT NULL, purpose TEXT NOT NULL,
username TEXT NOT NULL, user_id UUID NOT NULL,
ID SERIAL PRIMARY KEY NOT NULL ID SERIAL PRIMARY KEY NOT NULL
); );

View file

@ -15,8 +15,3 @@ CREATE TABLE IF NOT EXISTS cqrs_inventory_category_query
PRIMARY KEY (category_id) PRIMARY KEY (category_id)
); );
CREATE UNIQUE INDEX IF NOT EXISTS
cqrs_inventory_store_query_category_id_index
ON
cqrs_inventory_category_query (category_id);

View file

@ -8,11 +8,9 @@ CREATE TABLE IF NOT EXISTS cqrs_inventory_store_query
name TEXT NOT NULL, name TEXT NOT NULL,
address TEXT, address TEXT,
owner TEXT NOT NULL, owner UUID NOT NULL,
store_id UUID NOT NULL UNIQUE, store_id UUID NOT NULL UNIQUE,
deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (store_id) PRIMARY KEY (store_id)
); );
CREATE UNIQUE INDEX IF NOT EXISTS store_store_id_index ON cqrs_inventory_store_query (store_id);

View file

@ -11,12 +11,12 @@ use crate::identity::application::port::output::db::{create_verification_secret:
impl CreateVerificationSecretOutDBPort for DBOutPostgresAdapter { impl CreateVerificationSecretOutDBPort for DBOutPostgresAdapter {
async fn create_verification_secret(&self, msg: CreateSecretMsg) -> OutDBPortResult<()> { async fn create_verification_secret(&self, msg: CreateSecretMsg) -> OutDBPortResult<()> {
sqlx::query!( 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);", VALUES ($1, $2, $3, $4);",
&msg.secret, &msg.secret,
OffsetDateTime::now_utc(), OffsetDateTime::now_utc(),
REGISTRATION_SECRET_PURPOSE, REGISTRATION_SECRET_PURPOSE,
&msg.username, &msg.user_id,
) )
.execute(&self.pool) .execute(&self.pool)
.await?; .await?;
@ -26,6 +26,8 @@ impl CreateVerificationSecretOutDBPort for DBOutPostgresAdapter {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::utils::uuid::tests::UUID;
use super::*; use super::*;
#[actix_rt::test] #[actix_rt::test]
@ -40,7 +42,7 @@ mod tests {
let msg = CreateSecretMsgBuilder::default() let msg = CreateSecretMsgBuilder::default()
.secret("secret".into()) .secret("secret".into())
.username("username".into()) .user_id(UUID.clone())
.build() .build()
.unwrap(); .unwrap();

View file

@ -12,12 +12,12 @@ impl DeleteVerificationSecretOutDBPort for DBOutPostgresAdapter {
"DELETE FROM "DELETE FROM
verification_otp verification_otp
WHERE WHERE
username = $1 user_id = $1
AND AND
purpose = $2 purpose = $2
AND AND
secret = $3;", secret = $3;",
msg.username, msg.user_id,
REGISTRATION_SECRET_PURPOSE, REGISTRATION_SECRET_PURPOSE,
msg.secret, msg.secret,
) )
@ -30,13 +30,16 @@ impl DeleteVerificationSecretOutDBPort for DBOutPostgresAdapter {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::identity::application::port::output::db::{ use crate::{
create_verification_secret::*, verification_secret_exists::*, identity::application::port::output::db::{
create_verification_secret::*, verification_secret_exists::*,
},
utils::uuid::tests::UUID,
}; };
#[actix_rt::test] #[actix_rt::test]
async fn test_postgres_delete_verification_secret() { async fn test_postgres_delete_verification_secret() {
let username = "batman"; let user_id = UUID;
let secret = "bsdasdf"; let secret = "bsdasdf";
let settings = crate::settings::tests::get_settings().await; let settings = crate::settings::tests::get_settings().await;
settings.create_db().await; settings.create_db().await;
@ -46,7 +49,7 @@ mod tests {
.unwrap(), .unwrap(),
); );
let msg = VerifySecretExistsMsgBuilder::default() let msg = VerifySecretExistsMsgBuilder::default()
.username(username.into()) .user_id(user_id.clone())
.secret(secret.into()) .secret(secret.into())
.build() .build()
.unwrap(); .unwrap();
@ -56,7 +59,7 @@ mod tests {
let create_msg = CreateSecretMsgBuilder::default() let create_msg = CreateSecretMsgBuilder::default()
.secret(secret.into()) .secret(secret.into())
.username(username.into()) .user_id(user_id.clone())
.build() .build()
.unwrap(); .unwrap();
db.create_verification_secret(create_msg).await.unwrap(); db.create_verification_secret(create_msg).await.unwrap();

View file

@ -33,6 +33,8 @@ impl EmailExistsOutDBPort for DBOutPostgresAdapter {
mod tests { mod tests {
use super::*; use super::*;
use crate::utils::uuid::tests::*;
#[actix_rt::test] #[actix_rt::test]
async fn test_postgres_email_exists() { async fn test_postgres_email_exists() {
let email = "foo@exmaple.com"; let email = "foo@exmaple.com";
@ -49,12 +51,13 @@ mod tests {
sqlx::query!( sqlx::query!(
"INSERT INTO user_query "INSERT INTO user_query
(view_id, version, username, email, hashed_password) (user_id, version, first_name, email, hashed_password, last_name)
VALUES ($1, $2, $3, $4, $5);", VALUES ($1, $2, $3, $4, $5, $6);",
"1", &UUID,
1, 1,
"foo", "foo",
email, email,
"passwd",
"passwd" "passwd"
) )
.execute(&db.pool) .execute(&db.pool)

View file

@ -2,13 +2,15 @@
// //
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
use uuid::Uuid;
use super::errors::*; use super::errors::*;
use super::DBOutPostgresAdapter; use super::DBOutPostgresAdapter;
use crate::identity::application::port::output::db::{errors::*, get_verification_secret::*}; use crate::identity::application::port::output::db::{errors::*, get_verification_secret::*};
#[async_trait::async_trait] #[async_trait::async_trait]
impl GetVerificationSecretOutDBPort for DBOutPostgresAdapter { impl GetVerificationSecretOutDBPort for DBOutPostgresAdapter {
async fn get_verification_secret(&self, username: &str) -> OutDBPortResult<String> { async fn get_verification_secret(&self, user_id: &Uuid) -> OutDBPortResult<String> {
struct Secret { struct Secret {
secret: String, secret: String,
} }
@ -19,10 +21,10 @@ impl GetVerificationSecretOutDBPort for DBOutPostgresAdapter {
FROM FROM
verification_otp verification_otp
WHERE WHERE
username = $1 user_id = $1
AND AND
purpose = $2;", purpose = $2;",
username, user_id,
REGISTRATION_SECRET_PURPOSE, REGISTRATION_SECRET_PURPOSE,
) )
.fetch_one(&self.pool) .fetch_one(&self.pool)
@ -35,11 +37,14 @@ impl GetVerificationSecretOutDBPort for DBOutPostgresAdapter {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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] #[actix_rt::test]
async fn test_postgres_get_verification_secret() { async fn test_postgres_get_verification_secret() {
let username = "batman"; let user_id = UUID;
let secret = "bsdasdf"; let secret = "bsdasdf";
let settings = crate::settings::tests::get_settings().await; let settings = crate::settings::tests::get_settings().await;
settings.create_db().await; settings.create_db().await;
@ -49,19 +54,19 @@ mod tests {
.unwrap(), .unwrap(),
); );
assert_eq!( assert_eq!(
db.get_verification_secret(username).await.err(), db.get_verification_secret(&user_id).await.err(),
Some(OutDBPortError::VerificationOTPSecretNotFound) Some(OutDBPortError::VerificationOTPSecretNotFound)
); );
let create_msg = CreateSecretMsgBuilder::default() let create_msg = CreateSecretMsgBuilder::default()
.secret(secret.into()) .secret(secret.into())
.username(username.into()) .user_id(user_id.clone())
.build() .build()
.unwrap(); .unwrap();
db.create_verification_secret(create_msg).await.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; settings.drop_db().await;
} }

View file

@ -14,8 +14,8 @@ pub mod delete_verification_secret;
pub mod email_exists; pub mod email_exists;
mod errors; mod errors;
pub mod get_verification_secret; pub mod get_verification_secret;
pub mod user_id_exists;
pub mod user_view; pub mod user_view;
pub mod username_exists;
pub mod verification_secret_exists; pub mod verification_secret_exists;
#[derive(Clone)] #[derive(Clone)]

View file

@ -2,22 +2,24 @@
// //
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
use uuid::Uuid;
use super::DBOutPostgresAdapter; use super::DBOutPostgresAdapter;
use crate::identity::application::port::output::db::{ use crate::identity::application::port::output::db::{
errors::*, username_exists::UsernameExistsOutDBPort, errors::*, user_id_exists::UserIDExistsOutDBPort,
}; };
#[async_trait::async_trait] #[async_trait::async_trait]
impl UsernameExistsOutDBPort for DBOutPostgresAdapter { impl UserIDExistsOutDBPort for DBOutPostgresAdapter {
async fn username_exists(&self, username: &str) -> OutDBPortResult<bool> { async fn user_id_exists(&self, user_id: &Uuid) -> OutDBPortResult<bool> {
let res = sqlx::query!( let res = sqlx::query!(
"SELECT EXISTS ( "SELECT EXISTS (
SELECT 1 SELECT 1
FROM user_query FROM user_query
WHERE WHERE
username = $1 user_id = $1
);", );",
username user_id,
) )
.fetch_one(&self.pool) .fetch_one(&self.pool)
.await?; .await?;
@ -33,9 +35,12 @@ impl UsernameExistsOutDBPort for DBOutPostgresAdapter {
mod tests { mod tests {
use super::*; use super::*;
use crate::utils::uuid::tests::UUID;
use crate::identity::domain::aggregate::*;
#[actix_rt::test] #[actix_rt::test]
async fn test_postgres_username_exists() { async fn test_postgres_user_id_exists() {
let username = "foo@exmaple.com";
let settings = crate::settings::tests::get_settings().await; let settings = crate::settings::tests::get_settings().await;
settings.create_db().await; settings.create_db().await;
let db = super::DBOutPostgresAdapter::new( let db = super::DBOutPostgresAdapter::new(
@ -44,25 +49,28 @@ mod tests {
.unwrap(), .unwrap(),
); );
let user = User::default();
// state doesn't exist // state doesn't exist
assert!(!db.username_exists(username).await.unwrap()); assert!(!db.user_id_exists(&UUID).await.unwrap());
sqlx::query!( sqlx::query!(
"INSERT INTO user_query "INSERT INTO user_query
(view_id, version, username, email, hashed_password) (version, user_id, email, hashed_password, first_name, last_name)
VALUES ($1, $2, $3, $4, $5);", VALUES ($1, $2, $3, $4, $5, $6);",
"1",
1, 1,
username, UUID,
"foo", user.email(),
"passwd" user.hashed_password(),
user.first_name(),
user.last_name(),
) )
.execute(&db.pool) .execute(&db.pool)
.await .await
.unwrap(); .unwrap();
// state exists // state exists
assert!(db.username_exists(username).await.unwrap()); assert!(db.user_id_exists(&UUID).await.unwrap());
settings.drop_db().await; settings.drop_db().await;
} }

View file

@ -6,18 +6,24 @@ use async_trait::async_trait;
use cqrs_es::persist::GenericQuery; use cqrs_es::persist::GenericQuery;
use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository}; use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository};
use cqrs_es::{EventEnvelope, Query, View}; use cqrs_es::{EventEnvelope, Query, View};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::errors::*; use super::errors::*;
use super::DBOutPostgresAdapter; use super::DBOutPostgresAdapter;
use crate::identity::application::services::events::UserEvent; use crate::identity::application::services::events::UserEvent;
use crate::identity::domain::aggregate::User; 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 // 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. // be designed to reflect the response dto that will be returned to a user.
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
pub struct UserView { pub struct UserView {
username: String, first_name: String,
last_name: String,
user_id: Uuid,
email: String, email: String,
hashed_password: String, hashed_password: String,
is_admin: bool, is_admin: bool,
@ -32,12 +38,14 @@ impl View<User> for UserView {
fn update(&mut self, event: &EventEnvelope<User>) { fn update(&mut self, event: &EventEnvelope<User>) {
match &event.payload { match &event.payload {
UserEvent::UserRegistered(val) => { UserEvent::UserRegistered(val) => {
self.username = val.username().into();
self.email = val.email().into(); self.email = val.email().into();
self.hashed_password = val.hashed_password().into(); self.hashed_password = val.hashed_password().into();
self.is_admin = val.is_admin().to_owned(); self.is_admin = val.is_admin().to_owned();
self.is_verified = val.is_verified().to_owned(); self.is_verified = val.is_verified().to_owned();
self.deleted = false; 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::UserDeleted => self.deleted = true,
UserEvent::Loggedin(_) => (), UserEvent::Loggedin(_) => (),
@ -58,16 +66,21 @@ impl View<User> for UserView {
#[async_trait] #[async_trait]
impl ViewRepository<UserView, User> for DBOutPostgresAdapter { impl ViewRepository<UserView, User> for DBOutPostgresAdapter {
async fn load(&self, view_id: &str) -> Result<Option<UserView>, PersistenceError> { async fn load(&self, user_id: &str) -> Result<Option<UserView>, 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!( let res = sqlx::query_as!(
UserView, UserView,
"SELECT "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 FROM
user_query user_query
WHERE WHERE
view_id = $1;", user_id = $1;",
view_id user_id
) )
.fetch_one(&self.pool) .fetch_one(&self.pool)
.await .await
@ -77,17 +90,22 @@ impl ViewRepository<UserView, User> for DBOutPostgresAdapter {
async fn load_with_context( async fn load_with_context(
&self, &self,
view_id: &str, user_id: &str,
) -> Result<Option<(UserView, ViewContext)>, PersistenceError> { ) -> Result<Option<(UserView, ViewContext)>, 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!( let res = sqlx::query_as!(
UserView, UserView,
"SELECT "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 FROM
user_query user_query
WHERE WHERE
view_id = $1;", user_id = $1;",
view_id user_id
) )
.fetch_one(&self.pool) .fetch_one(&self.pool)
.await .await
@ -95,24 +113,24 @@ impl ViewRepository<UserView, User> for DBOutPostgresAdapter {
struct Context { struct Context {
version: i64, version: i64,
view_id: String, user_id: String,
} }
let ctx = sqlx::query_as!( let ctx = sqlx::query_as!(
Context, Context,
"SELECT "SELECT
view_id, version user_id, version
FROM FROM
user_query user_query
WHERE WHERE
view_id = $1;", user_id = $1;",
view_id user_id
) )
.fetch_one(&self.pool) .fetch_one(&self.pool)
.await .await
.map_err(PostgresAggregateError::from)?; .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))) Ok(Some((res, view_context)))
} }
@ -126,19 +144,20 @@ impl ViewRepository<UserView, User> for DBOutPostgresAdapter {
let version = context.version + 1; let version = context.version + 1;
sqlx::query!( sqlx::query!(
"INSERT INTO user_query ( "INSERT INTO user_query (
view_id, version, username, email, version, first_name, last_name, email,
hashed_password, is_admin, is_verified, deleted hashed_password, is_admin, is_verified, deleted, user_id
) VALUES ( ) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8 $1, $2, $3, $4, $5, $6, $7, $8, $9
);", );",
context.view_instance_id,
version, version,
view.username, view.first_name,
view.last_name,
view.email, view.email,
view.hashed_password, view.hashed_password,
view.is_admin, view.is_admin,
view.is_verified, view.is_verified,
view.deleted, view.deleted,
view.user_id,
) )
.execute(&self.pool) .execute(&self.pool)
.await .await
@ -150,16 +169,18 @@ impl ViewRepository<UserView, User> for DBOutPostgresAdapter {
"UPDATE "UPDATE
user_query user_query
SET SET
view_id = $1, version = $2, username = $3, email = $4, user_id = $1, version = $2, first_name = $3, email = $4,
hashed_password = $5, is_admin = $6, is_verified = $7, deleted = $8;", hashed_password = $5, is_admin = $6, is_verified = $7, deleted = $8,
context.view_instance_id, last_name=$9;",
view.user_id,
version, version,
view.username, view.first_name,
view.email, view.email,
view.hashed_password, view.hashed_password,
view.is_admin, view.is_admin,
view.is_verified, view.is_verified,
view.deleted, view.deleted,
view.last_name,
) )
.execute(&self.pool) .execute(&self.pool)
.await .await

View file

@ -16,13 +16,13 @@ impl VerificationSecretExistsOutDBPort for DBOutPostgresAdapter {
SELECT 1 SELECT 1
FROM verification_otp FROM verification_otp
WHERE WHERE
username = $1 user_id = $1
AND AND
purpose = $2 purpose = $2
AND AND
secret = $3 secret = $3
);", );",
msg.username, msg.user_id,
REGISTRATION_SECRET_PURPOSE, REGISTRATION_SECRET_PURPOSE,
msg.secret, msg.secret,
) )
@ -39,11 +39,14 @@ impl VerificationSecretExistsOutDBPort for DBOutPostgresAdapter {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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] #[actix_rt::test]
async fn test_postgres_verification_secret_exists() { async fn test_postgres_verification_secret_exists() {
let username = "batman"; let user_id = UUID;
let secret = "bsdasdf"; let secret = "bsdasdf";
let settings = crate::settings::tests::get_settings().await; let settings = crate::settings::tests::get_settings().await;
settings.create_db().await; settings.create_db().await;
@ -53,7 +56,7 @@ mod tests {
.unwrap(), .unwrap(),
); );
let msg = VerifySecretExistsMsgBuilder::default() let msg = VerifySecretExistsMsgBuilder::default()
.username(username.into()) .user_id(user_id.clone())
.secret(secret.into()) .secret(secret.into())
.build() .build()
.unwrap(); .unwrap();
@ -63,7 +66,7 @@ mod tests {
let create_msg = CreateSecretMsgBuilder::default() let create_msg = CreateSecretMsgBuilder::default()
.secret(secret.into()) .secret(secret.into())
.username(username.into()) .user_id(user_id.clone())
.build() .build()
.unwrap(); .unwrap();
db.create_verification_secret(create_msg).await.unwrap(); db.create_verification_secret(create_msg).await.unwrap();

View file

@ -12,17 +12,17 @@ impl AccountValidationLinkOutMailerPort for LettreMailer {
async fn account_validation_link( async fn account_validation_link(
&self, &self,
to: &str, to: &str,
username: &str, first_name: &str,
validation_secret: &str, validation_secret: &str,
) -> OutMailerPortResult<()> { ) -> OutMailerPortResult<()> {
let email = Message::builder() let email = Message::builder()
.from(self.from.parse().unwrap()) .from(self.from.parse().unwrap())
.reply_to(self.reply_to.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 .subject("Please verify your account on Vanikam") // TODO: use better title
.header(ContentType::TEXT_PLAIN) .header(ContentType::TEXT_PLAIN)
.body(format!( .body(format!(
r#"Hello {username}, r#"Hello {first_name},
Please click here to verify your Vanikam account: {validation_secret} Please click here to verify your Vanikam account: {validation_secret}
Warm regards, Warm regards,
Vanikam Admin Vanikam Admin

View file

@ -1,325 +0,0 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// 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<dyn UserServicesInterface>;
// 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<Vec<Self::Event>, 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<User>;
//
// #[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<Option<Result<(), AtmError>>>,
// validate_check_response: Mutex<Option<Result<(), CheckingError>>>,
// }
//
// 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()
// }
// }
//}
//

View file

@ -2,6 +2,5 @@
// //
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
pub mod aggregate;
pub mod port; pub mod port;
pub mod services; pub mod services;

View file

@ -6,6 +6,7 @@ use derive_builder::Builder;
use mockall::predicate::*; use mockall::predicate::*;
use mockall::*; use mockall::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::errors::*; use super::errors::*;
#[cfg(test)] #[cfg(test)]
@ -15,7 +16,7 @@ pub use tests::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Builder)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Builder)]
pub struct CreateSecretMsg { pub struct CreateSecretMsg {
pub secret: String, pub secret: String,
pub username: String, pub user_id: Uuid,
} }
pub const REGISTRATION_SECRET_PURPOSE: &str = "account_validation"; pub const REGISTRATION_SECRET_PURPOSE: &str = "account_validation";

View file

@ -6,6 +6,7 @@ use derive_builder::Builder;
use mockall::predicate::*; use mockall::predicate::*;
use mockall::*; use mockall::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub use super::create_verification_secret::REGISTRATION_SECRET_PURPOSE; pub use super::create_verification_secret::REGISTRATION_SECRET_PURPOSE;
use super::errors::*; use super::errors::*;
@ -16,14 +17,14 @@ pub use tests::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Builder)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Builder)]
pub struct DeleteSecretMsg { pub struct DeleteSecretMsg {
pub secret: String, pub secret: String,
pub username: String, pub user_id: Uuid,
} }
impl From<super::verification_secret_exists::VerifySecretExistsMsg> for DeleteSecretMsg { impl From<super::verification_secret_exists::VerifySecretExistsMsg> for DeleteSecretMsg {
fn from(value: super::verification_secret_exists::VerifySecretExistsMsg) -> Self { fn from(value: super::verification_secret_exists::VerifySecretExistsMsg) -> Self {
Self { Self {
secret: value.secret, secret: value.secret,
username: value.username, user_id: value.user_id,
} }
} }
} }

View file

@ -4,6 +4,7 @@
use mockall::predicate::*; use mockall::predicate::*;
use mockall::*; use mockall::*;
use uuid::Uuid;
pub use super::create_verification_secret::REGISTRATION_SECRET_PURPOSE; pub use super::create_verification_secret::REGISTRATION_SECRET_PURPOSE;
use super::errors::*; use super::errors::*;
@ -14,7 +15,7 @@ pub use tests::*;
#[automock] #[automock]
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait GetVerificationSecretOutDBPort: Send + Sync { pub trait GetVerificationSecretOutDBPort: Send + Sync {
async fn get_verification_secret(&self, username: &str) -> OutDBPortResult<String>; async fn get_verification_secret(&self, user_id: &Uuid) -> OutDBPortResult<String>;
} }
pub type GetVerificationSecretOutDBPortObj = std::sync::Arc<dyn GetVerificationSecretOutDBPort>; pub type GetVerificationSecretOutDBPortObj = std::sync::Arc<dyn GetVerificationSecretOutDBPort>;

View file

@ -7,5 +7,5 @@ pub mod delete_verification_secret;
pub mod email_exists; pub mod email_exists;
pub mod errors; pub mod errors;
pub mod get_verification_secret; pub mod get_verification_secret;
pub mod username_exists; pub mod user_id_exists;
pub mod verification_secret_exists; pub mod verification_secret_exists;

View file

@ -4,6 +4,7 @@
use mockall::predicate::*; use mockall::predicate::*;
use mockall::*; use mockall::*;
use uuid::Uuid;
use super::errors::*; use super::errors::*;
#[cfg(test)] #[cfg(test)]
@ -12,11 +13,11 @@ pub use tests::*;
#[automock] #[automock]
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait UsernameExistsOutDBPort: Send + Sync { pub trait UserIDExistsOutDBPort: Send + Sync {
async fn username_exists(&self, username: &str) -> OutDBPortResult<bool>; async fn user_id_exists(&self, user_id: &Uuid) -> OutDBPortResult<bool>;
} }
pub type UsernameExistsOutDBPortObj = std::sync::Arc<dyn UsernameExistsOutDBPort>; pub type UserIDExistsOutDBPortObj = std::sync::Arc<dyn UserIDExistsOutDBPort>;
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
@ -24,17 +25,17 @@ pub mod tests {
use std::sync::Arc; use std::sync::Arc;
pub fn mock_username_exists_db_port( pub fn mock_user_id_exists_db_port(
times: Option<usize>, times: Option<usize>,
returning: bool, returning: bool,
) -> UsernameExistsOutDBPortObj { ) -> UserIDExistsOutDBPortObj {
let mut m = MockUsernameExistsOutDBPort::new(); let mut m = MockUserIDExistsOutDBPort::new();
if let Some(times) = times { if let Some(times) = times {
m.expect_username_exists() m.expect_user_id_exists()
.times(times) .times(times)
.returning(move |_| Ok(returning)); .returning(move |_| Ok(returning));
} else { } else {
m.expect_username_exists().returning(move |_| Ok(returning)); m.expect_user_id_exists().returning(move |_| Ok(returning));
} }
Arc::new(m) Arc::new(m)

View file

@ -6,6 +6,7 @@ use derive_builder::Builder;
use mockall::predicate::*; use mockall::predicate::*;
use mockall::*; use mockall::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub use super::create_verification_secret::REGISTRATION_SECRET_PURPOSE; pub use super::create_verification_secret::REGISTRATION_SECRET_PURPOSE;
use super::errors::*; use super::errors::*;
@ -16,7 +17,7 @@ pub use tests::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Builder)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Builder)]
pub struct VerifySecretExistsMsg { pub struct VerifySecretExistsMsg {
pub secret: String, pub secret: String,
pub username: String, pub user_id: Uuid,
} }
#[automock] #[automock]

View file

@ -16,7 +16,7 @@ pub trait AccountValidationLinkOutMailerPort: Send + Sync {
async fn account_validation_link( async fn account_validation_link(
&self, &self,
to: &str, to: &str,
username: &str, first_name: &str,
validation_secret: &str, validation_secret: &str,
) -> OutMailerPortResult<()>; ) -> OutMailerPortResult<()>;
} }

View file

@ -30,13 +30,10 @@ pub enum IdentityCommandError {
impl From<CredsError> for IdentityCommandError { impl From<CredsError> for IdentityCommandError {
fn from(v: CredsError) -> Self { fn from(v: CredsError) -> Self {
match v { 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::NotAnEmail => Self::BadEmail,
CredsError::PasswordTooShort => Self::BadPassowrd(v.to_string()), CredsError::PasswordTooShort => Self::BadPassowrd(v.to_string()),
CredsError::PasswordTooLong => 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()),
} }
} }
} }

View file

@ -5,29 +5,32 @@
use super::*; use super::*;
use derive_getters::Getters; use derive_getters::Getters;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
pub struct MarkUserVerifiedCommand { pub struct MarkUserVerifiedCommand {
username: String, user_id: Uuid,
secret: String, secret: String,
} }
impl MarkUserVerifiedCommand { impl MarkUserVerifiedCommand {
pub fn new(username: String, secret: String) -> IdentityCommandResult<Self> { pub fn new(user_id: Uuid, secret: String) -> IdentityCommandResult<Self> {
Ok(Self { username, secret }) Ok(Self { user_id, secret })
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::utils::uuid::tests::UUID;
use super::*; use super::*;
#[test] #[test]
fn test_cmd() { fn test_cmd() {
let username = "realaravinth"; let user_id = UUID;
let secret = "asdfasdf"; let secret = "asdfasdf";
let cmd = MarkUserVerifiedCommand::new(username.into(), secret.into()).unwrap(); let cmd = MarkUserVerifiedCommand::new(user_id.clone(), secret.into()).unwrap();
assert_eq!(cmd.username(), username); assert_eq!(cmd.user_id(), &user_id);
assert_eq!(cmd.secret(), secret); assert_eq!(cmd.secret(), secret);
} }
} }

View file

@ -22,7 +22,7 @@ impl MarkUserVerifiedUseCase for MarkUserVerifiedService {
cmd: command::MarkUserVerifiedCommand, cmd: command::MarkUserVerifiedCommand,
) -> IdentityResult<()> { ) -> IdentityResult<()> {
let msg = VerifySecretExistsMsgBuilder::default() let msg = VerifySecretExistsMsgBuilder::default()
.username(cmd.username().into()) .user_id(cmd.user_id().clone())
.secret(cmd.secret().into()) .secret(cmd.secret().into())
.build() .build()
.unwrap(); .unwrap();
@ -48,13 +48,13 @@ impl MarkUserVerifiedUseCase for MarkUserVerifiedService {
mod tests { mod tests {
use super::*; use super::*;
use crate::tests::bdd::*; use crate::{tests::bdd::*, utils::uuid::tests::UUID};
#[actix_rt::test] #[actix_rt::test]
async fn test_service() { async fn test_service() {
let username = "realaravinth"; let user_id = UUID;
let secret = "password"; 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 // happy case
{ {

View file

@ -3,35 +3,45 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
use super::*; use super::*;
use derive_builder::Builder;
use derive_getters::Getters; use derive_getters::Getters;
use serde::{Deserialize, Serialize}; 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)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
pub struct RegisterUserCommand { pub struct RegisterUserCommand {
username: String, first_name: String,
last_name: String,
email: String, email: String,
hashed_password: String, hashed_password: String,
} }
impl RegisterUserCommand { impl UnvalidatedRegisterUserCommand {
pub fn new( pub fn validate(
username: String, self,
email: String,
password: String,
confirm_password: String,
config: &argon2_creds::Config, config: &argon2_creds::Config,
) -> IdentityCommandResult<Self> { ) -> IdentityCommandResult<RegisterUserCommand> {
let username = config.username(&username)?; config.email(&self.email)?;
config.email(&email)?;
if password != confirm_password { if self.password != self.confirm_password {
return Err(IdentityCommandError::PasswordsDontMatch); return Err(IdentityCommandError::PasswordsDontMatch);
} }
let hashed_password: String = config.password(&password)?; let hashed_password: String = config.password(&self.password)?;
Ok(Self { Ok(RegisterUserCommand {
username, first_name: self.first_name,
email, last_name: self.last_name,
email: self.email,
hashed_password, hashed_password,
}) })
} }
@ -44,49 +54,47 @@ mod tests {
#[test] #[test]
fn test_cmd() { fn test_cmd() {
let config = argon2_creds::Config::default(); let config = argon2_creds::Config::default();
RegisterUserCommand::new( let first_name = "John";
"realaravinth".into(), let last_name = "Doe";
"realaravinth@example.com".into(), let email = "john@example.com";
"asdfasdfasdfasdf".into(), let password = "sadfasdfasdf";
"asdfasdfasdfasdf".into(), let wrong_password = "sadfasdfasdf--wrong";
&config,
) UnvalidatedRegisterUserCommandBuilder::default()
.unwrap(); .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!( assert_eq!(
RegisterUserCommand::new( UnvalidatedRegisterUserCommandBuilder::default()
"realaravinth".into(), .first_name(first_name.into())
"username".into(), .last_name(last_name.into())
"password".into(), .email(first_name.into())
"password".into(), .password(password.into())
&config, .confirm_password(password.into())
) .build()
.err(), .unwrap()
Some(IdentityCommandError::BadEmail) .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!( assert_eq!(
RegisterUserCommand::new( UnvalidatedRegisterUserCommandBuilder::default()
"realaravinth".into(), .first_name(first_name.into())
"realaravinth@example.com".into(), .last_name(last_name.into())
"password".into(), .email(email.into())
"mismatch_password".into(), .password(password.into())
&config, .confirm_password(wrong_password.into())
) .build()
.err(), .unwrap()
Some(IdentityCommandError::PasswordsDontMatch) .validate(&config),
Err(IdentityCommandError::PasswordsDontMatch)
); );
} }
} }

View file

@ -5,12 +5,15 @@
use derive_builder::Builder; use derive_builder::Builder;
use derive_getters::Getters; use derive_getters::Getters;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive( #[derive(
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd, Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
)] )]
pub struct UserRegisteredEvent { pub struct UserRegisteredEvent {
username: String, first_name: String,
last_name: String,
user_id: Uuid,
email: String, email: String,
hashed_password: String, hashed_password: String,
is_verified: bool, is_verified: bool,

View file

@ -7,10 +7,10 @@ use derive_builder::Builder;
use super::*; use super::*;
use crate::identity::application::port::output::{ 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::*, mailer::account_validation_link::*,
}; };
use crate::utils::random_string::*; use crate::utils::{random_string::*, uuid::*};
pub const SECRET_LEN: usize = 20; pub const SECRET_LEN: usize = 20;
pub const REGISTRATION_SECRET_PURPOSE: &str = "account_validation"; pub const REGISTRATION_SECRET_PURPOSE: &str = "account_validation";
@ -18,9 +18,10 @@ pub const REGISTRATION_SECRET_PURPOSE: &str = "account_validation";
#[derive(Builder)] #[derive(Builder)]
pub struct RegisterUserService { pub struct RegisterUserService {
db_email_exists_adapter: EmailExistsOutDBPortObj, db_email_exists_adapter: EmailExistsOutDBPortObj,
db_username_exists_adapter: UsernameExistsOutDBPortObj, db_user_id_exists_adapter: UserIDExistsOutDBPortObj,
db_create_verification_secret_adapter: CreateVerificationSecretOutDBPortObj, db_create_verification_secret_adapter: CreateVerificationSecretOutDBPortObj,
mailer_account_validation_link_adapter: AccountValidationLinkOutMailerPortObj, mailer_account_validation_link_adapter: AccountValidationLinkOutMailerPortObj,
get_uuid: GetUUIDInterfaceObj,
random_string_adapter: GenerateRandomStringInterfaceObj, random_string_adapter: GenerateRandomStringInterfaceObj,
} }
@ -30,15 +31,6 @@ impl RegisterUserUseCase for RegisterUserService {
&self, &self,
cmd: command::RegisterUserCommand, cmd: command::RegisterUserCommand,
) -> IdentityResult<events::UserRegisteredEvent> { ) -> IdentityResult<events::UserRegisteredEvent> {
if self
.db_username_exists_adapter
.username_exists(cmd.username())
.await
.unwrap()
{
return Err(IdentityError::DuplicateUsername);
}
if self if self
.db_email_exists_adapter .db_email_exists_adapter
.email_exists(cmd.email()) .email_exists(cmd.email())
@ -48,13 +40,27 @@ impl RegisterUserUseCase for RegisterUserService {
return Err(IdentityError::DuplicateEmail); 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); let secret = self.random_string_adapter.get_random(SECRET_LEN);
self.db_create_verification_secret_adapter self.db_create_verification_secret_adapter
.create_verification_secret( .create_verification_secret(
CreateSecretMsgBuilder::default() CreateSecretMsgBuilder::default()
.secret(secret.clone()) .secret(secret.clone())
.username(cmd.username().into()) .user_id(user_id.clone())
.build() .build()
.unwrap(), .unwrap(),
) )
@ -62,12 +68,14 @@ impl RegisterUserUseCase for RegisterUserService {
.unwrap(); .unwrap();
self.mailer_account_validation_link_adapter self.mailer_account_validation_link_adapter
.account_validation_link(cmd.email(), cmd.username(), &secret) .account_validation_link(cmd.email(), cmd.first_name(), &secret)
.await .await
.unwrap(); .unwrap();
Ok(events::UserRegisteredEventBuilder::default() 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()) .email(cmd.email().into())
.hashed_password(cmd.hashed_password().into()) .hashed_password(cmd.hashed_password().into())
.is_verified(false) .is_verified(false)
@ -84,6 +92,7 @@ mod tests {
use crate::tests::bdd::*; use crate::tests::bdd::*;
use crate::utils::random_string::tests::*; use crate::utils::random_string::tests::*;
use crate::utils::uuid::tests::*;
#[actix_rt::test] #[actix_rt::test]
async fn test_service() { async fn test_service() {
@ -91,101 +100,88 @@ mod tests {
let email = format!("{username}@example.com"); let email = format!("{username}@example.com");
let password = "password"; let password = "password";
let config = argon2_creds::Config::default(); let config = argon2_creds::Config::default();
let cmd = command::RegisterUserCommand::new( let cmd = command::UnvalidatedRegisterUserCommandBuilder::default()
username.into(), .first_name(username.into())
email.clone(), .last_name(username.into())
password.into(), .email(email.into())
password.into(), .password(password.into())
&config, .confirm_password(password.into())
) .build()
.unwrap(); .unwrap()
.validate(&config)
.unwrap();
// happy case let s = RegisterUserServiceBuilder::default()
{ .db_user_id_exists_adapter(mock_user_id_exists_db_port(
let s = RegisterUserServiceBuilder::default() IS_CALLED_ONLY_ONCE,
.db_username_exists_adapter(mock_username_exists_db_port( RETURNS_FALSE,
IS_CALLED_ONLY_ONCE, ))
RETURNS_FALSE, .db_create_verification_secret_adapter(mock_create_verification_secret_db_port(
)) IS_CALLED_ONLY_ONCE,
.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,
.db_email_exists_adapter(mock_email_exists_db_port( RETURNS_FALSE,
IS_CALLED_ONLY_ONCE, ))
RETURNS_FALSE, .random_string_adapter(mock_generate_random_string(
)) IS_CALLED_ONLY_ONCE,
.random_string_adapter(mock_generate_random_string( RETURNS_RANDOM_STRING.into(),
IS_CALLED_ONLY_ONCE, ))
RETURNS_RANDOM_STRING.into(), .mailer_account_validation_link_adapter(mock_account_validation_link_db_port(
)) IS_CALLED_ONLY_ONCE,
.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()
.build() .unwrap();
.unwrap();
let res = s.register_user(cmd.clone()).await.unwrap(); let res = s.register_user(cmd.clone()).await.unwrap();
assert_eq!(res.username(), cmd.username()); assert_eq!(res.first_name(), cmd.first_name());
assert_eq!(res.email(), cmd.email()); assert_eq!(res.last_name(), cmd.last_name());
assert!(!res.is_admin()); assert_eq!(res.user_id(), &UUID);
assert!(argon2_creds::Config::verify(res.hashed_password(), password).unwrap()) 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_user_id_exists_adapter(mock_user_id_exists_db_port(
let s = RegisterUserServiceBuilder::default() IGNORE_CALL_COUNT,
.db_username_exists_adapter(mock_username_exists_db_port( RETURNS_FALSE,
IS_CALLED_ONLY_ONCE, ))
RETURNS_TRUE, .db_create_verification_secret_adapter(mock_create_verification_secret_db_port(
)) IS_NEVER_CALLED,
.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( .db_email_exists_adapter(mock_email_exists_db_port(IS_CALLED_ONLY_ONCE, RETURNS_TRUE))
IS_NEVER_CALLED, .random_string_adapter(mock_generate_random_string(
)) IS_NEVER_CALLED,
.random_string_adapter(mock_generate_random_string( RETURNS_RANDOM_STRING.into(),
IS_NEVER_CALLED, ))
RETURNS_RANDOM_STRING.into(), .mailer_account_validation_link_adapter(mock_account_validation_link_db_port(
)) IS_NEVER_CALLED,
.mailer_account_validation_link_adapter(mock_account_validation_link_db_port( ))
IS_NEVER_CALLED, .get_uuid(mock_get_uuid(IS_NEVER_CALLED))
)) .build()
.build() .unwrap();
.unwrap();
assert_eq!( assert_eq!(
s.register_user(cmd.clone()).await.err(), s.register_user(cmd.clone()).await.err(),
Some(IdentityError::DuplicateUsername) Some(IdentityError::DuplicateEmail)
); );
}
// 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)
);
}
} }
} }

View file

@ -5,54 +5,58 @@
use super::*; use super::*;
use derive_getters::Getters; use derive_getters::Getters;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
pub struct ResendVerificationEmailCommand { pub struct ResendVerificationEmailCommand {
username: String, user_id: Uuid,
first_name: String,
email: String, email: String,
} }
impl ResendVerificationEmailCommand { impl ResendVerificationEmailCommand {
pub fn new( pub fn new(
username: String, user_id: Uuid,
first_name: String,
email: String, email: String,
config: &argon2_creds::Config, config: &argon2_creds::Config,
) -> IdentityCommandResult<Self> { ) -> IdentityCommandResult<Self> {
let username = config.username(&username)?;
config.email(&email)?; config.email(&email)?;
Ok(Self { username, email }) Ok(Self {
user_id,
first_name,
email,
})
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::utils::uuid::tests::UUID;
use super::*; use super::*;
#[test] #[test]
fn test_cmd() { fn test_cmd() {
let config = argon2_creds::Config::default(); let config = argon2_creds::Config::default();
ResendVerificationEmailCommand::new( ResendVerificationEmailCommand::new(
"realaravinth".into(), UUID.clone(),
"john".into(),
"realaravinth@example.com".into(), "realaravinth@example.com".into(),
&config, &config,
) )
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
ResendVerificationEmailCommand::new("realaravinth".into(), "username".into(), &config,)
.err(),
Some(IdentityCommandError::BadEmail)
);
assert!(matches!(
ResendVerificationEmailCommand::new( ResendVerificationEmailCommand::new(
"username".into(), UUID.clone(),
"username@example.com".into(), "john".into(),
&config, "john".into(),
&config
) )
.err(), .err(),
Some(IdentityCommandError::BadUsername(_)) Some(IdentityCommandError::BadEmail)
)); );
} }
} }

View file

@ -6,14 +6,13 @@ use derive_builder::Builder;
use super::*; use super::*;
use crate::identity::application::port::output::{ use crate::identity::application::port::output::{
db::{email_exists::*, get_verification_secret::*, username_exists::*}, db::{email_exists::*, get_verification_secret::*},
mailer::account_validation_link::*, mailer::account_validation_link::*,
}; };
#[derive(Builder)] #[derive(Builder)]
pub struct ResendVerificationEmailService { pub struct ResendVerificationEmailService {
db_email_exists_adapter: EmailExistsOutDBPortObj, db_email_exists_adapter: EmailExistsOutDBPortObj,
db_username_exists_adapter: UsernameExistsOutDBPortObj,
db_get_verification_secret_adapter: GetVerificationSecretOutDBPortObj, db_get_verification_secret_adapter: GetVerificationSecretOutDBPortObj,
mailer_account_validation_link_adapter: AccountValidationLinkOutMailerPortObj, mailer_account_validation_link_adapter: AccountValidationLinkOutMailerPortObj,
} }
@ -24,15 +23,6 @@ impl ResendVerificationEmailUseCase for ResendVerificationEmailService {
&self, &self,
cmd: command::ResendVerificationEmailCommand, cmd: command::ResendVerificationEmailCommand,
) -> IdentityResult<()> { ) -> IdentityResult<()> {
if self
.db_username_exists_adapter
.username_exists(cmd.username())
.await
.unwrap()
{
return Err(IdentityError::DuplicateUsername);
}
if self if self
.db_email_exists_adapter .db_email_exists_adapter
.email_exists(cmd.email()) .email_exists(cmd.email())
@ -44,12 +34,12 @@ impl ResendVerificationEmailUseCase for ResendVerificationEmailService {
let secret = self let secret = self
.db_get_verification_secret_adapter .db_get_verification_secret_adapter
.get_verification_secret(cmd.username()) .get_verification_secret(cmd.user_id())
.await .await
.unwrap(); .unwrap();
self.mailer_account_validation_link_adapter self.mailer_account_validation_link_adapter
.account_validation_link(cmd.email(), cmd.username(), &secret) .account_validation_link(cmd.email(), cmd.first_name(), &secret)
.await .await
.unwrap(); .unwrap();
@ -62,91 +52,68 @@ mod tests {
use super::*; use super::*;
use crate::tests::bdd::*; use crate::tests::bdd::*;
use crate::utils::random_string::tests::*; use crate::utils::uuid::tests::UUID;
#[actix_rt::test] #[actix_rt::test]
async fn test_service() { async fn test_service() {
let username = "realaravinth"; let user_id = UUID;
let email = format!("{username}@example.com"); let email = format!("john@example.com");
let secret = "asdfasdf"; let secret = "asdfasdf";
let config = argon2_creds::Config::default(); let config = argon2_creds::Config::default();
let cmd = let cmd = command::ResendVerificationEmailCommand::new(
command::ResendVerificationEmailCommand::new(username.into(), email.clone(), &config) UUID.clone(),
.unwrap(); "john".into(),
email.clone(),
&config,
)
.unwrap();
// happy case let s = ResendVerificationEmailServiceBuilder::default()
{ .db_get_verification_secret_adapter(mock_get_verification_secret_db_port(
let s = ResendVerificationEmailServiceBuilder::default() IS_CALLED_ONLY_ONCE,
.db_username_exists_adapter(mock_username_exists_db_port( secret.into(),
IS_CALLED_ONLY_ONCE, ))
RETURNS_FALSE, .db_email_exists_adapter(mock_email_exists_db_port(
)) IS_CALLED_ONLY_ONCE,
.db_get_verification_secret_adapter(mock_get_verification_secret_db_port( RETURNS_FALSE,
IS_CALLED_ONLY_ONCE, ))
secret.into(), .mailer_account_validation_link_adapter(mock_account_validation_link_db_port(
)) IS_CALLED_ONLY_ONCE,
.db_email_exists_adapter(mock_email_exists_db_port( ))
IS_CALLED_ONLY_ONCE, .build()
RETURNS_FALSE, .unwrap();
))
.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_get_verification_secret_adapter(mock_get_verification_secret_db_port(
let s = ResendVerificationEmailServiceBuilder::default() IS_NEVER_CALLED,
.db_username_exists_adapter(mock_username_exists_db_port( secret.into(),
IS_CALLED_ONLY_ONCE, ))
RETURNS_TRUE, .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(
.db_email_exists_adapter(mock_email_exists_db_port(IS_NEVER_CALLED, RETURNS_FALSE)) IS_NEVER_CALLED,
.db_get_verification_secret_adapter(mock_get_verification_secret_db_port( ))
IS_NEVER_CALLED, .build()
secret.into(), .unwrap();
))
.mailer_account_validation_link_adapter(mock_account_validation_link_db_port(
IS_NEVER_CALLED,
))
.build()
.unwrap();
assert_eq!( assert_eq!(
s.resend_verification_email(cmd.clone()).await.err(), s.resend_verification_email(cmd.clone()).await.err(),
Some(IdentityError::DuplicateUsername) Some(IdentityError::DuplicateEmail)
); );
}
// 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)
);
}
} }
} }

View file

@ -26,6 +26,7 @@ impl SetAdminCommand {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::identity::domain::aggregate::UserBuilder; use crate::identity::domain::aggregate::UserBuilder;
use crate::utils::uuid::tests::UUID;
use super::*; use super::*;
@ -35,13 +36,15 @@ mod tests {
SetAdminCommand::new( SetAdminCommand::new(
UserBuilder::default() UserBuilder::default()
.username(username.into()) .first_name(username.into())
.last_name(username.into())
.email(username.into()) .email(username.into())
.hashed_password(username.into()) .hashed_password(username.into())
.is_verified(true) .is_verified(true)
.email_verified(false) .email_verified(false)
.is_admin(true) .is_admin(true)
.deleted(false) .deleted(false)
.user_id(UUID.clone())
.build() .build()
.unwrap(), .unwrap(),
) )
@ -50,13 +53,15 @@ mod tests {
assert_eq!( assert_eq!(
SetAdminCommand::new( SetAdminCommand::new(
UserBuilder::default() UserBuilder::default()
.username(username.into()) .first_name(username.into())
.last_name(username.into())
.email(username.into()) .email(username.into())
.hashed_password(username.into()) .hashed_password(username.into())
.is_verified(true) .is_verified(true)
.is_admin(false) .is_admin(false)
.email_verified(false) .email_verified(false)
.deleted(false) .deleted(false)
.user_id(UUID.clone())
.build() .build()
.unwrap(), .unwrap(),
) )

View file

@ -20,6 +20,7 @@ impl SetUserAdminUseCase for SetUserAdminService {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::identity::domain::aggregate::UserBuilder; use crate::identity::domain::aggregate::UserBuilder;
use crate::utils::uuid::tests::UUID;
use super::*; use super::*;
@ -29,13 +30,15 @@ mod tests {
let s = SetUserAdminService; let s = SetUserAdminService;
let u = UserBuilder::default() let u = UserBuilder::default()
.username(username.into()) .first_name(username.into())
.last_name(username.into())
.email(username.into()) .email(username.into())
.hashed_password(username.into()) .hashed_password(username.into())
.is_verified(true) .is_verified(true)
.email_verified(false) .email_verified(false)
.is_admin(true) .is_admin(true)
.deleted(false) .deleted(false)
.user_id(UUID.clone())
.build() .build()
.unwrap(); .unwrap();

View file

@ -4,19 +4,22 @@
use derive_getters::Getters; use derive_getters::Getters;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::*; use super::*;
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
pub struct UpdateEmailCommand { pub struct UpdateEmailCommand {
new_email: String, new_email: String,
username: String, user_id: Uuid,
first_name: String,
} }
impl UpdateEmailCommand { impl UpdateEmailCommand {
pub fn new( pub fn new(
username: String,
new_email: String, new_email: String,
user_id: Uuid,
first_name: String,
supplied_password: String, supplied_password: String,
actual_password_hash: &str, actual_password_hash: &str,
config: &argon2_creds::Config, config: &argon2_creds::Config,
@ -27,27 +30,32 @@ impl UpdateEmailCommand {
config.email(&new_email)?; config.email(&new_email)?;
Ok(Self { Ok(Self {
username, user_id,
new_email, new_email,
first_name,
}) })
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::utils::uuid::tests::UUID;
use super::*; use super::*;
#[test] #[test]
fn test_cmd() { fn test_cmd() {
let config = argon2_creds::Config::default(); let config = argon2_creds::Config::default();
let password = "adsfasdfasd"; let password = "adsfasdfasd";
let username = "realaravinth"; let first_name = "john";
let user_id = UUID;
let new_email = format!("newemail@example.com"); let new_email = format!("newemail@example.com");
let hashed_password = config.password(password).unwrap(); let hashed_password = config.password(password).unwrap();
assert_eq!( assert_eq!(
UpdateEmailCommand::new( UpdateEmailCommand::new(
username.into(),
new_email.clone(), new_email.clone(),
user_id.clone(),
first_name.into(),
password.into(), password.into(),
&hashed_password, &hashed_password,
&config &config
@ -60,8 +68,9 @@ mod tests {
// email is not valid email // email is not valid email
assert_eq!( assert_eq!(
UpdateEmailCommand::new( UpdateEmailCommand::new(
username.into(), user_id.to_string(),
username.into(), user_id.clone(),
first_name.into(),
password.into(), password.into(),
&hashed_password, &hashed_password,
&config &config
@ -73,9 +82,10 @@ mod tests {
// wrong password // wrong password
assert_eq!( assert_eq!(
UpdateEmailCommand::new( UpdateEmailCommand::new(
username.into(), new_email.to_string(),
username.into(), user_id.clone(),
username.into(), first_name.into(),
first_name.into(),
&hashed_password, &hashed_password,
&config &config
) )

View file

@ -44,7 +44,7 @@ impl UpdateEmailUseCase for UpdateEmailService {
.create_verification_secret( .create_verification_secret(
CreateSecretMsgBuilder::default() CreateSecretMsgBuilder::default()
.secret(secret.clone()) .secret(secret.clone())
.username(cmd.username().into()) .user_id(cmd.user_id().clone())
.build() .build()
.unwrap(), .unwrap(),
) )
@ -52,7 +52,7 @@ impl UpdateEmailUseCase for UpdateEmailService {
.unwrap(); .unwrap();
self.mailer_account_validation_link_adapter 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 .await
.unwrap(); .unwrap();
@ -66,18 +66,20 @@ mod tests {
use crate::utils::random_string::tests::*; use crate::utils::random_string::tests::*;
use crate::tests::bdd::*; use crate::tests::bdd::*;
use crate::utils::uuid::tests::UUID;
#[actix_rt::test] #[actix_rt::test]
async fn test_service() { async fn test_service() {
let username = "realaravinth"; let user_id = UUID;
let new_email = format!("{username}@example.com"); let new_email = format!("john@example.com");
let password = "password"; let password = "password";
let config = argon2_creds::Config::default(); let config = argon2_creds::Config::default();
let hashed_password = config.password(password).unwrap(); let hashed_password = config.password(password).unwrap();
let cmd = command::UpdateEmailCommand::new( let cmd = command::UpdateEmailCommand::new(
username.into(),
new_email.clone(), new_email.clone(),
user_id.clone(),
"john".into(),
password.into(), password.into(),
&hashed_password, &hashed_password,
&config, &config,

View file

@ -2,15 +2,25 @@
// //
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
use async_trait::async_trait;
use cqrs_es::Aggregate;
use derive_builder::Builder; use derive_builder::Builder;
use derive_getters::Getters; use derive_getters::Getters;
use serde::{Deserialize, Serialize}; 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( #[derive(
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)] )]
pub struct User { pub struct User {
username: String, first_name: String,
last_name: String,
user_id: Uuid,
email: String, email: String,
hashed_password: String, hashed_password: String,
is_verified: bool, is_verified: bool,
@ -22,13 +32,15 @@ pub struct User {
impl Default for User { impl Default for User {
fn default() -> Self { fn default() -> Self {
User { User {
username: "".to_string(), first_name: "".to_string(),
last_name: "".to_string(),
email: "".to_string(), email: "".to_string(),
hashed_password: "".to_string(), hashed_password: "".to_string(),
is_verified: false, is_verified: false,
is_admin: false, is_admin: false,
email_verified: false, email_verified: false,
deleted: 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<dyn UserServicesInterface>;
// 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<Vec<Self::Event>, 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -6,13 +6,14 @@ use async_trait::async_trait;
use cqrs_es::persist::GenericQuery; use cqrs_es::persist::GenericQuery;
use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository}; use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository};
use cqrs_es::{EventEnvelope, Query, View}; use cqrs_es::{EventEnvelope, Query, View};
use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use super::errors::*; use super::errors::*;
use super::InventoryDBPostgresAdapter; use super::InventoryDBPostgresAdapter;
use crate::inventory::domain::category_aggregate::Category; use crate::inventory::domain::category_aggregate::Category;
use crate::inventory::domain::events::InventoryEvent; 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"; pub const NEW_CATEGORY_NON_UUID: &str = "new_category_non_uuid-asdfa";
@ -46,11 +47,10 @@ impl View<Category> for CategoryView {
#[async_trait] #[async_trait]
impl ViewRepository<CategoryView, Category> for InventoryDBPostgresAdapter { impl ViewRepository<CategoryView, Category> for InventoryDBPostgresAdapter {
async fn load(&self, category_id: &str) -> Result<Option<CategoryView>, PersistenceError> { async fn load(&self, category_id: &str) -> Result<Option<CategoryView>, PersistenceError> {
let category_id = let category_id = match parse_aggregate_id(category_id, NEW_CATEGORY_NON_UUID)? {
match super::utils::parse_aggregate_id(category_id, NEW_CATEGORY_NON_UUID)? { Some((val, _)) => return Ok(Some(val)),
Some((val, _)) => return Ok(Some(val)), None => Uuid::parse_str(category_id).unwrap(),
None => Uuid::parse_str(category_id).unwrap(), };
};
let res = sqlx::query_as!( let res = sqlx::query_as!(
CategoryView, CategoryView,
@ -72,11 +72,10 @@ impl ViewRepository<CategoryView, Category> for InventoryDBPostgresAdapter {
&self, &self,
category_id: &str, category_id: &str,
) -> Result<Option<(CategoryView, ViewContext)>, PersistenceError> { ) -> Result<Option<(CategoryView, ViewContext)>, PersistenceError> {
let category_id = let category_id = match parse_aggregate_id(category_id, NEW_CATEGORY_NON_UUID)? {
match super::utils::parse_aggregate_id(category_id, NEW_CATEGORY_NON_UUID)? { Some(val) => return Ok(Some(val)),
Some(val) => return Ok(Some(val)), None => Uuid::parse_str(category_id).unwrap(),
None => Uuid::parse_str(category_id).unwrap(), };
};
let res = sqlx::query_as!( let res = sqlx::query_as!(
CategoryView, CategoryView,

View file

@ -15,7 +15,6 @@ mod errors;
mod store_id_exists; mod store_id_exists;
mod store_name_exists; mod store_name_exists;
mod store_view; mod store_view;
mod utils;
#[derive(Clone)] #[derive(Clone)]
pub struct InventoryDBPostgresAdapter { pub struct InventoryDBPostgresAdapter {

View file

@ -32,6 +32,8 @@ impl StoreIDExistsDBPort for InventoryDBPostgresAdapter {
mod tests { mod tests {
use uuid::Uuid; use uuid::Uuid;
use crate::utils::uuid::tests::UUID;
use super::*; use super::*;
#[actix_rt::test] #[actix_rt::test]
@ -47,7 +49,7 @@ mod tests {
let store = StoreBuilder::default() let store = StoreBuilder::default()
.name("store_name".into()) .name("store_name".into())
.owner("store_owner".into()) .owner(UUID.clone())
.address(Some("store_address".into())) .address(Some("store_address".into()))
.store_id(store_id) .store_id(store_id)
.build() .build()

View file

@ -32,6 +32,8 @@ impl StoreNameExistsDBPort for InventoryDBPostgresAdapter {
mod tests { mod tests {
use uuid::Uuid; use uuid::Uuid;
use crate::utils::uuid::tests::UUID;
use super::*; use super::*;
#[actix_rt::test] #[actix_rt::test]
@ -47,7 +49,7 @@ mod tests {
let store = StoreBuilder::default() let store = StoreBuilder::default()
.name("store_name".into()) .name("store_name".into())
.owner("store_owner".into()) .owner(UUID.clone())
.address(Some("store_address".into())) .address(Some("store_address".into()))
.store_id(store_id) .store_id(store_id)
.build() .build()

View file

@ -12,6 +12,7 @@ use super::errors::*;
use super::InventoryDBPostgresAdapter; use super::InventoryDBPostgresAdapter;
use crate::inventory::domain::events::InventoryEvent; use crate::inventory::domain::events::InventoryEvent;
use crate::inventory::domain::store_aggregate::Store; 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"; pub const NEW_STORE_NON_UUID: &str = "new_store_non_uuid-asdfa";
@ -22,7 +23,7 @@ pub struct StoreView {
name: String, name: String,
address: Option<String>, address: Option<String>,
store_id: Uuid, store_id: Uuid,
owner: String, owner: Uuid,
} }
// This updates the view with events as they are committed. // This updates the view with events as they are committed.
@ -45,7 +46,7 @@ impl View<Store> for StoreView {
#[async_trait] #[async_trait]
impl ViewRepository<StoreView, Store> for InventoryDBPostgresAdapter { impl ViewRepository<StoreView, Store> for InventoryDBPostgresAdapter {
async fn load(&self, store_id: &str) -> Result<Option<StoreView>, PersistenceError> { async fn load(&self, store_id: &str) -> Result<Option<StoreView>, 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)), Some((val, _)) => return Ok(Some(val)),
None => Uuid::parse_str(store_id).unwrap(), None => Uuid::parse_str(store_id).unwrap(),
}; };
@ -70,7 +71,7 @@ impl ViewRepository<StoreView, Store> for InventoryDBPostgresAdapter {
&self, &self,
store_id: &str, store_id: &str,
) -> Result<Option<(StoreView, ViewContext)>, PersistenceError> { ) -> Result<Option<(StoreView, ViewContext)>, 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)), Some(val) => return Ok(Some(val)),
None => Uuid::parse_str(store_id).unwrap(), None => Uuid::parse_str(store_id).unwrap(),
}; };
@ -247,7 +248,7 @@ mod tests {
)) ))
.add_category(mock_add_category_service( .add_category(mock_add_category_service(
IS_NEVER_CALLED, IS_NEVER_CALLED,
AddCategoryCommand::new("foo".into(), None, UUID.clone(), "bar".into()).unwrap(), AddCategoryCommand::new("foo".into(), None, UUID.clone(), UUID.clone()).unwrap(),
)) ))
.build() .build()
.unwrap(); .unwrap();
@ -265,7 +266,7 @@ mod tests {
); );
let rand = crate::utils::random_string::GenerateRandomString {}; 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())) cqrs.execute("", InventoryCommand::AddStore(cmd.clone()))
.await .await
.unwrap(); .unwrap();

View file

@ -78,7 +78,7 @@ impl AddCategoryUseCase for AddCategoryService {
Ok(CategoryAddedEventBuilder::default() Ok(CategoryAddedEventBuilder::default()
.name(category.name().into()) .name(category.name().into())
.description(category.description().as_ref().map(|s| s.to_string())) .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()) .store_id(category.store_id().clone())
.category_id(category.category_id().clone()) .category_id(category.category_id().clone())
.build() .build()
@ -104,7 +104,7 @@ pub mod tests {
let res = CategoryAddedEventBuilder::default() let res = CategoryAddedEventBuilder::default()
.name(cmd.name().into()) .name(cmd.name().into())
.description(cmd.description().as_ref().map(|s| s.to_string())) .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()) .store_id(cmd.store_id().clone())
.category_id(UUID.clone()) .category_id(UUID.clone())
.build() .build()
@ -125,7 +125,7 @@ pub mod tests {
async fn test_service_category_doesnt_exist() { async fn test_service_category_doesnt_exist() {
let name = "foo"; let name = "foo";
let description = "bar"; let description = "bar";
let username = "baz"; let user_id = UUID;
let store_id = Uuid::new_v4(); let store_id = Uuid::new_v4();
// description = None // description = None
@ -133,7 +133,7 @@ pub mod tests {
name.into(), name.into(),
Some(description.into()), Some(description.into()),
store_id.clone(), store_id.clone(),
username.into(), user_id.clone(),
) )
.unwrap(); .unwrap();
@ -158,7 +158,7 @@ pub mod tests {
async fn test_service_category_name_exists_for_store() { async fn test_service_category_name_exists_for_store() {
let name = "foo"; let name = "foo";
let description = "bar"; let description = "bar";
let username = "baz"; let user_id = UUID;
let store_id = Uuid::new_v4(); let store_id = Uuid::new_v4();
// description = None // description = None
@ -166,7 +166,7 @@ pub mod tests {
name.into(), name.into(),
Some(description.into()), Some(description.into()),
store_id.clone(), store_id.clone(),
username.into(), user_id.clone(),
) )
.unwrap(); .unwrap();
@ -175,7 +175,7 @@ pub mod tests {
IS_CALLED_ONLY_ONCE, IS_CALLED_ONLY_ONCE,
)) ))
.db_category_id_exists(mock_category_id_exists_db_port_false(IS_NEVER_CALLED)) .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() .build()
.unwrap(); .unwrap();

View file

@ -43,7 +43,7 @@ impl AddStoreUseCase for AddStoreService {
let mut store = StoreBuilder::default() let mut store = StoreBuilder::default()
.name(cmd.name().into()) .name(cmd.name().into())
.address(cmd.address().as_ref().map(|s| s.to_string())) .address(cmd.address().as_ref().map(|s| s.to_string()))
.owner(cmd.owner().into()) .owner(cmd.owner().clone())
.store_id(store_id.clone()) .store_id(store_id.clone())
.build() .build()
.unwrap(); .unwrap();
@ -58,6 +58,7 @@ impl AddStoreUseCase for AddStoreService {
store = StoreBuilder::default() store = StoreBuilder::default()
.name(cmd.name().into()) .name(cmd.name().into())
.address(cmd.address().as_ref().map(|s| s.to_string())) .address(cmd.address().as_ref().map(|s| s.to_string()))
.owner(cmd.owner().clone())
.store_id(store_id.clone()) .store_id(store_id.clone())
.build() .build()
.unwrap(); .unwrap();
@ -70,7 +71,7 @@ impl AddStoreUseCase for AddStoreService {
Ok(StoreAddedEventBuilder::default() Ok(StoreAddedEventBuilder::default()
.name(store.name().into()) .name(store.name().into())
.address(store.address().as_ref().map(|s| s.to_string())) .address(store.address().as_ref().map(|s| s.to_string()))
.owner(cmd.owner().into()) .owner(cmd.owner().clone())
.store_id(store_id.clone()) .store_id(store_id.clone())
.build() .build()
.unwrap()) .unwrap())
@ -93,7 +94,7 @@ pub mod tests {
let res = StoreAddedEventBuilder::default() let res = StoreAddedEventBuilder::default()
.name(cmd.name().into()) .name(cmd.name().into())
.address(cmd.address().as_ref().map(|s| s.to_string())) .address(cmd.address().as_ref().map(|s| s.to_string()))
.owner(cmd.owner().into()) .owner(cmd.owner().clone())
.store_id(UUID.clone()) .store_id(UUID.clone())
.build() .build()
.unwrap(); .unwrap();
@ -113,10 +114,10 @@ pub mod tests {
async fn test_service_store_id_doesnt_exist() { async fn test_service_store_id_doesnt_exist() {
let name = "foo"; let name = "foo";
let address = "bar"; let address = "bar";
let username = "baz"; let owner = UUID;
// address = None // 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() let s = AddStoreServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE)) .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() { async fn test_service_store_name_exists() {
let name = "foo"; let name = "foo";
let address = "bar"; let address = "bar";
let username = "baz"; let owner = UUID;
// address = None // 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() let s = AddStoreServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_false(IS_NEVER_CALLED)) .db_store_id_exists(mock_store_id_exists_db_port_false(IS_NEVER_CALLED))

View file

@ -17,7 +17,7 @@ pub struct AddCategoryCommand {
name: String, name: String,
description: Option<String>, description: Option<String>,
store_id: Uuid, store_id: Uuid,
adding_by: String, adding_by: Uuid,
} }
impl AddCategoryCommand { impl AddCategoryCommand {
@ -25,7 +25,7 @@ impl AddCategoryCommand {
name: String, name: String,
description: Option<String>, description: Option<String>,
store_id: Uuid, store_id: Uuid,
adding_by: String, adding_by: Uuid,
) -> Result<Self, AddCategoryCommandError> { ) -> Result<Self, AddCategoryCommandError> {
let description: Option<String> = if let Some(description) = description { let description: Option<String> = if let Some(description) = description {
let description = description.trim(); let description = description.trim();
@ -56,19 +56,21 @@ impl AddCategoryCommand {
mod tests { mod tests {
use super::*; use super::*;
use crate::utils::uuid::tests::UUID;
#[test] #[test]
fn test_cmd() { fn test_cmd() {
let name = "foo"; let name = "foo";
let description = "bar"; let description = "bar";
let username = "baz"; let adding_by = UUID;
let store_id = Uuid::new_v4(); let store_id = Uuid::new_v4();
// description = None // description = None
let cmd = let cmd = AddCategoryCommand::new(name.into(), None, store_id.clone(), adding_by.clone())
AddCategoryCommand::new(name.into(), None, store_id.clone(), username.into()).unwrap(); .unwrap();
assert_eq!(cmd.name(), name); assert_eq!(cmd.name(), name);
assert_eq!(cmd.description(), &None); 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); assert_eq!(cmd.store_id(), &store_id);
// description = Some // description = Some
@ -76,12 +78,12 @@ mod tests {
name.into(), name.into(),
Some(description.into()), Some(description.into()),
store_id.clone(), store_id.clone(),
username.into(), adding_by.clone(),
) )
.unwrap(); .unwrap();
assert_eq!(cmd.name(), name); assert_eq!(cmd.name(), name);
assert_eq!(cmd.description(), &Some(description.to_owned())); 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); assert_eq!(cmd.store_id(), &store_id);
// AddCategoryCommandError::NameIsEmpty // AddCategoryCommandError::NameIsEmpty
@ -90,7 +92,7 @@ mod tests {
"".into(), "".into(),
Some(description.into()), Some(description.into()),
store_id.clone(), store_id.clone(),
username.into() adding_by.clone(),
), ),
Err(AddCategoryCommandError::NameIsEmpty) Err(AddCategoryCommandError::NameIsEmpty)
) )

View file

@ -5,6 +5,7 @@
use derive_getters::Getters; use derive_getters::Getters;
use derive_more::{Display, Error}; use derive_more::{Display, Error};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum AddStoreCommandError { pub enum AddStoreCommandError {
@ -15,14 +16,14 @@ pub enum AddStoreCommandError {
pub struct AddStoreCommand { pub struct AddStoreCommand {
name: String, name: String,
address: Option<String>, address: Option<String>,
owner: String, owner: Uuid,
} }
impl AddStoreCommand { impl AddStoreCommand {
pub fn new( pub fn new(
name: String, name: String,
address: Option<String>, address: Option<String>,
owner: String, owner: Uuid,
) -> Result<Self, AddStoreCommandError> { ) -> Result<Self, AddStoreCommandError> {
let address: Option<String> = if let Some(address) = address { let address: Option<String> = if let Some(address) = address {
let address = address.trim(); let address = address.trim();
@ -50,29 +51,31 @@ impl AddStoreCommand {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::utils::uuid::tests::UUID;
use super::*; use super::*;
#[test] #[test]
fn test_cmd() { fn test_cmd() {
let name = "foo"; let name = "foo";
let address = "bar"; let address = "bar";
let username = "baz"; let owner = UUID.clone();
// address = None // 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.name(), name);
assert_eq!(cmd.address(), &None); assert_eq!(cmd.address(), &None);
assert_eq!(cmd.owner(), username); assert_eq!(cmd.owner(), &owner);
// address = Some // 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.name(), name);
assert_eq!(cmd.address(), &Some(address.to_owned())); assert_eq!(cmd.address(), &Some(address.to_owned()));
assert_eq!(cmd.owner(), username); assert_eq!(cmd.owner(), &owner);
// AddStoreCommandError::NameIsEmpty // AddStoreCommandError::NameIsEmpty
assert_eq!( assert_eq!(
AddStoreCommand::new("".into(), Some(address.into()), username.into()), AddStoreCommand::new("".into(), Some(address.into()), owner.clone()),
Err(AddStoreCommandError::NameIsEmpty) Err(AddStoreCommandError::NameIsEmpty)
) )
} }

View file

@ -13,7 +13,7 @@ use uuid::Uuid;
pub struct CategoryAddedEvent { pub struct CategoryAddedEvent {
name: String, name: String,
description: Option<String>, description: Option<String>,
added_by_user: String, added_by_user: Uuid,
category_id: Uuid, category_id: Uuid,
store_id: Uuid, store_id: Uuid,
} }

View file

@ -92,7 +92,7 @@ mod aggregate_tests {
fn test_create_store() { fn test_create_store() {
let name = "category_name"; let name = "category_name";
let description = Some("category_description".to_string()); let description = Some("category_description".to_string());
let adding_by = "store_owner"; let adding_by = UUID;
let store_id = Uuid::new_v4(); let store_id = Uuid::new_v4();
let category_id = UUID.clone(); let category_id = UUID.clone();
@ -100,14 +100,14 @@ mod aggregate_tests {
name.into(), name.into(),
description.clone(), description.clone(),
store_id.clone(), store_id.clone(),
adding_by.into(), adding_by.clone(),
) )
.unwrap(); .unwrap();
let expected = CategoryAddedEventBuilder::default() let expected = CategoryAddedEventBuilder::default()
.name(cmd.name().into()) .name(cmd.name().into())
.description(cmd.description().as_ref().map(|s| s.to_string())) .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()) .store_id(cmd.store_id().clone())
.category_id(category_id.clone()) .category_id(category_id.clone())
.build() .build()

View file

@ -13,6 +13,6 @@ use uuid::Uuid;
pub struct StoreAddedEvent { pub struct StoreAddedEvent {
name: String, name: String,
address: Option<String>, address: Option<String>,
owner: String, owner: Uuid,
store_id: Uuid, store_id: Uuid,
} }

View file

@ -20,7 +20,7 @@ use super::{commands::InventoryCommand, events::InventoryEvent};
pub struct Store { pub struct Store {
name: String, name: String,
address: Option<String>, address: Option<String>,
owner: String, owner: Uuid,
store_id: Uuid, store_id: Uuid,
} }
@ -62,7 +62,7 @@ impl Aggregate for Store {
InventoryEvent::StoreAdded(e) => { InventoryEvent::StoreAdded(e) => {
self.name = e.name().into(); self.name = e.name().into();
self.address = e.address().as_ref().map(|s| s.to_string()); 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(); self.store_id = e.store_id().clone();
} }
_ => (), _ => (),
@ -95,7 +95,7 @@ mod tests {
fn test_create_store() { fn test_create_store() {
let name = "store_name"; let name = "store_name";
let address = Some("store_address".to_string()); let address = Some("store_address".to_string());
let owner = "store_owner"; let owner = UUID;
let store_id = UUID; let store_id = UUID;
let expected = StoreAddedEventBuilder::default() let expected = StoreAddedEventBuilder::default()
@ -107,7 +107,7 @@ mod tests {
.unwrap(); .unwrap();
let expected = InventoryEvent::StoreAdded(expected); 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(); let mut services = MockInventoryServicesInterface::new();
services services

View file

@ -68,12 +68,19 @@ mod tests {
#[test] #[test]
fn test_db_env_override() { fn test_db_env_override() {
let init_settings = crate::settings::Settings::new().unwrap(); let init_settings = crate::settings::Settings::new().unwrap();
env_helper!( std::thread::spawn(move || {
init_settings, env_helper!(
"DATABASE_URL", init_settings,
"postgres://test_db_env_override", "DATABASE_URL",
database.url "postgres://test_db_env_override",
); database.url
);
assert!(true);
})
.join()
.unwrap();
env_helper!(init_settings, "VANIKAM_database_POOL", 99, database.pool); env_helper!(init_settings, "VANIKAM_database_POOL", 99, database.pool);
} }
} }

View file

@ -130,6 +130,7 @@ pub mod tests {
macro_rules! env_helper { macro_rules! env_helper {
($init_settings:ident, $env:expr, $val:expr, $val_typed:expr, $($param:ident).+) => { ($init_settings:ident, $env:expr, $val:expr, $val_typed:expr, $($param:ident).+) => {
println!("Setting env var {} to {} for test", $env, $val); println!("Setting env var {} to {} for test", $env, $val);
let current = env::var($env);
env::set_var($env, $val); env::set_var($env, $val);
{ {
let new_settings = $crate::settings::Settings::new().unwrap(); let new_settings = $crate::settings::Settings::new().unwrap();
@ -137,6 +138,9 @@ pub mod tests {
assert_ne!(new_settings.$($param).+, $init_settings.$($param).+); assert_ne!(new_settings.$($param).+, $init_settings.$($param).+);
} }
env::remove_var($env); 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(); let mut db_url = Url::parse(&settings.database.url).unwrap();
db_url.set_path(&GenerateRandomString.get_random(12)); db_url.set_path(&GenerateRandomString.get_random(12));
settings.database.url = db_url.to_string(); settings.database.url = db_url.to_string();
settings.database.pool = 1;
settings settings
} }

View file

@ -3,9 +3,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
pub const IS_CALLED_ONLY_ONCE: Option<usize> = Some(1); pub const IS_CALLED_ONLY_ONCE: Option<usize> = Some(1);
pub const IS_NEVER_CALLED: Option<usize> = None; pub const IS_NEVER_CALLED: Option<usize> = Some(0);
pub const IS_CALLED_ONLY_TWICE: Option<usize> = Some(2); pub const IS_CALLED_ONLY_TWICE: Option<usize> = Some(2);
pub const RETURNS_RANDOM_STRING: &str = "test_random_string"; pub const RETURNS_RANDOM_STRING: &str = "test_random_string";
pub const IGNORE_CALL_COUNT: Option<usize> = None;
pub const RETURNS_TRUE: bool = true; pub const RETURNS_TRUE: bool = true;
pub const RETURNS_FALSE: bool = false; pub const RETURNS_FALSE: bool = false;

View file

@ -2,5 +2,6 @@
// //
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
pub mod parse_aggregate_id;
pub mod random_string; pub mod random_string;
pub mod uuid; pub mod uuid;