feat: owner manage employees #138

Open
realaravinth wants to merge 29 commits from owner-manage-employees into master
80 changed files with 3980 additions and 176 deletions

View file

@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n created_time,\n name,\n role_id,\n store_id,\n deleted\n FROM\n cqrs_identity_role_query\n WHERE\n role_id = $1;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "created_time",
"type_info": "Timestamptz"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "role_id",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "store_id",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "deleted",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "3a2efcc806c9a4cf7bd00b3eae4474de188981176005664ad53e39460319d34f"
}

View file

@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n role_id, version\n FROM\n cqrs_identity_role_query\n WHERE\n role_id = $1;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "role_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "version",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false
]
},
"hash": "4df5ca308db869c3488227a26fabe8f9452cf458a000842f5104c7b65e8ed5f2"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n created_time,\n store_id,\n emp_id,\n first_name,\n last_name,\n phone_number_number,\n phone_number_country_code,\n phone_verified,\n deleted\n FROM\n cqrs_identity_employee_query\n WHERE\n emp_id = $1;",
"query": "SELECT \n created_time,\n store_id,\n emp_id,\n first_name,\n last_name,\n phone_number_number,\n phone_number_country_code,\n phone_verified,\n role_id,\n deleted\n FROM\n cqrs_identity_employee_query\n WHERE\n emp_id = $1;",
"describe": {
"columns": [
{
@ -45,6 +45,11 @@
},
{
"ordinal": 8,
"name": "role_id",
"type_info": "Uuid"
},
{
"ordinal": 9,
"name": "deleted",
"type_info": "Bool"
}
@ -63,8 +68,9 @@
false,
false,
false,
true,
false
]
},
"hash": "7c2fd6e897bf18b1f2229eec5fd12932a86d4e88f2fd4ab8ac32246a55303b03"
"hash": "5cf15aaa2223dd2e68681e529a1972a336b8b2346c5dd7a55e445373fff4bf61"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO cqrs_identity_employee_query (\n version,\n created_time,\n store_id,\n emp_id,\n first_name,\n last_name,\n phone_number_number,\n phone_number_country_code,\n phone_verified,\n deleted\n\n\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10\n );",
"query": "INSERT INTO cqrs_identity_employee_query (\n version,\n created_time,\n store_id,\n emp_id,\n first_name,\n last_name,\n phone_number_number,\n phone_number_country_code,\n phone_verified,\n role_id,\n deleted\n\n\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11\n );",
"describe": {
"columns": [],
"parameters": {
@ -14,10 +14,11 @@
"Int8",
"Int4",
"Bool",
"Uuid",
"Bool"
]
},
"nullable": []
},
"hash": "796be4344e585654ea27252b02239158ed4691448b33d4427bf70717aad41263"
"hash": "610f776a0591cba4d7af0bb4d4ef6406fd0dc9c5378146f996ce6d8e8e9b43c8"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_identity_role_query\n WHERE\n role_id = $1\n );",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "70211e169bab5fc0f9240e26c14bd183fa8fa607f9b50377b2d1edbd4ec97472"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n created_time,\n store_id,\n emp_id,\n first_name,\n last_name,\n phone_number_number,\n phone_number_country_code,\n phone_verified,\n deleted\n\n FROM\n cqrs_identity_employee_query\n WHERE\n emp_id = $1;",
"query": "SELECT \n created_time,\n store_id,\n emp_id,\n first_name,\n last_name,\n phone_number_number,\n phone_number_country_code,\n phone_verified,\n role_id,\n deleted\n\n FROM\n cqrs_identity_employee_query\n WHERE\n emp_id = $1;",
"describe": {
"columns": [
{
@ -45,6 +45,11 @@
},
{
"ordinal": 8,
"name": "role_id",
"type_info": "Uuid"
},
{
"ordinal": 9,
"name": "deleted",
"type_info": "Bool"
}
@ -63,8 +68,9 @@
false,
false,
false,
true,
false
]
},
"hash": "848f7c8250f7aba08fcf11491ee1a80c9fd0bfb8e37ca1051604bc2bb25d5356"
"hash": "7137685170920291b99261bb95042a1f8c8ff1ffc5e724c5ceee68e49513246c"
}

View file

@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS (\n SELECT 1\n FROM cqrs_identity_role_query\n WHERE\n name = $1\n AND\n store_id = $2\n AND\n deleted = false\n );",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "79e633fecc51e6691730f40831678492191cb240271b6600bc9ab96dcb959035"
}

View file

@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE\n cqrs_identity_role_query\n SET\n version = $1,\n\n created_time = $2,\n store_id = $3,\n name = $4,\n deleted = $5;",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Timestamptz",
"Uuid",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "a93291e26fe69c7945bef30c84066ddefeafed4cb8f2d5f97ec3646f30ca2e78"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE\n cqrs_identity_employee_query\n SET\n version = $1,\n\n created_time = $2,\n store_id = $3,\n first_name = $4,\n last_name = $5,\n phone_number_number = $6,\n phone_number_country_code = $7,\n phone_verified = $8,\n\n\n deleted = $9;",
"query": "UPDATE\n cqrs_identity_employee_query\n SET\n version = $1,\n\n created_time = $2,\n store_id = $3,\n first_name = $4,\n last_name = $5,\n phone_number_number = $6,\n phone_number_country_code = $7,\n phone_verified = $8,\n role_id = $9,\n\n\n deleted = $10;",
"describe": {
"columns": [],
"parameters": {
@ -13,10 +13,11 @@
"Int8",
"Int4",
"Bool",
"Uuid",
"Bool"
]
},
"nullable": []
},
"hash": "4b8bf25b161a8337bc1ee7bcfba5a065417280cbae1527d3363f6f7561cb50c3"
"hash": "b753a03fcb9f40976b0dd179da0c35730372a03557b703d07bab7ab90e7fe4d1"
}

View file

@ -0,0 +1,19 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO cqrs_identity_role_query (\n version,\n created_time,\n store_id,\n role_id,\n name,\n deleted\n\n\n ) VALUES (\n $1, $2, $3, $4, $5, $6\n );",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Timestamptz",
"Uuid",
"Uuid",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "d0bda8d2ac1ede3becdc0aa642671dfb143547e5022b391a411bbb065a29dda0"
}

View file

@ -0,0 +1,40 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n name, role_id, store_id, deleted\n FROM \n cqrs_identity_role_query\n WHERE\n store_id = $1\n AND\n deleted = false;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "role_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "store_id",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "deleted",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "e3e3b18f95cf920b423ea3c63db3327d6fdd548ecc0219230d529383297e3c40"
}

View file

@ -3,11 +3,11 @@
"devenv": {
"locked": {
"dir": "src/modules",
"lastModified": 1734441494,
"lastModified": 1736426010,
"owner": "cachix",
"repo": "devenv",
"rev": "bdc1a2cefdda8f89e31b1a0f3771786ba9e5d052",
"treeHash": "9f63e582153de59f2326d8efb83d2f8eedd71f58",
"rev": "1c384bc4be3ee571511fbbc6fdc94fe47d60f6cf",
"treeHash": "cd68d11052e7a7fd8f4f2c3ad9f950ce9a23a2c5",
"type": "github"
},
"original": {
@ -25,11 +25,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1734503722,
"lastModified": 1736663419,
"owner": "nix-community",
"repo": "fenix",
"rev": "07f1f47c8f634a5ec52a2ad1d14e7cc7521d9a4f",
"treeHash": "2dbf42e1832bef3cd88faa0c6e8cb8214f605842",
"rev": "89350fe9d9b7f77a17be0541115bfc4f52ba6e40",
"treeHash": "f8992ba6a89adc04f174aced883093a51378f1a3",
"type": "github"
},
"original": {
@ -91,37 +91,20 @@
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1734202038,
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "bcba2fbf6963bf6bed3a749f9f4cf5bff4adb96d",
"treeHash": "ed868e7045ff3d48595deec9ca09f1311c91e749",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
]
},
"locked": {
"lastModified": 1734425854,
"lastModified": 1735882644,
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "0ddd26d0925f618c3a5d85a4fa5eb1e23a09491d",
"treeHash": "7180381e4de59f052b3a3134571af84dc523fd93",
"rev": "a5a961387e75ae44cc20f0a57ae463da5e959656",
"treeHash": "bc4924d0f5cbc6ac438e9652cd6ff1ef7a78bd61",
"type": "github"
},
"original": {
@ -141,11 +124,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1734386068,
"lastModified": 1736576343,
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "0a706f7d2ac093985eae317781200689cfd48b78",
"treeHash": "3f8418c9949a4084758a307478884360952624d2",
"rev": "9923b0085c18c45b1a2340f7ef329ea4828e0734",
"treeHash": "e3e3b73490f3883f769a72a2efd3955378ff4901",
"type": "github"
},
"original": {

View file

@ -22,6 +22,8 @@ CREATE TABLE IF NOT EXISTS cqrs_identity_employee_query
store_id UUID DEFAULT NULL,
role_id UUID DEFAULT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (emp_id)

View file

@ -0,0 +1,21 @@
-- SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later
CREATE TABLE IF NOT EXISTS cqrs_identity_role_query
(
version bigint CHECK (version >= 0) NOT NULL,
created_time timestamp with time zone DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
name TEXT NOT NULL,
role_id UUID NOT NULL UNIQUE,
store_id UUID NOT NULL,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
UNIQUE(name,store_id),
PRIMARY KEY (role_id)
);

View file

@ -47,6 +47,10 @@ pub enum WebError {
DuplicateStoreName,
StoreIDNotFound,
DuplicateStoreID,
DuplicateRoleID,
DuplicateRoleName,
RoleIDNotFound,
RoleNotFound,
}
impl From<IdentityError> for WebError {
@ -71,6 +75,11 @@ impl From<IdentityError> for WebError {
IdentityError::DuplicateStoreName => Self::DuplicateStoreName,
IdentityError::StoreIDNotFound => Self::StoreIDNotFound,
IdentityError::DuplicateStoreID => Self::DuplicateStoreID,
IdentityError::DuplicateRoleID => Self::DuplicateRoleID,
IdentityError::DuplicateUserID => Self::InternalError,
IdentityError::DuplicateRoleName => Self::DuplicateRoleName,
IdentityError::RoleIDNotFound => Self::RoleIDNotFound,
IdentityError::RoleNotFound => Self::RoleNotFound,
}
}
}
@ -99,6 +108,10 @@ impl ResponseError for WebError {
Self::DuplicateStoreName => StatusCode::BAD_REQUEST,
Self::StoreIDNotFound => StatusCode::NOT_FOUND,
Self::DuplicateStoreID => StatusCode::INTERNAL_SERVER_ERROR,
Self::DuplicateRoleID => StatusCode::INTERNAL_SERVER_ERROR,
Self::DuplicateRoleName => StatusCode::BAD_REQUEST,
Self::RoleIDNotFound => StatusCode::BAD_REQUEST,
Self::RoleNotFound => StatusCode::NOT_FOUND,
}
}
@ -127,6 +140,10 @@ impl ResponseError for WebError {
Self::DuplicateStoreName => HttpResponse::BadRequest().json(e),
Self::StoreIDNotFound => HttpResponse::NotFound().json(e),
Self::DuplicateStoreID => HttpResponse::InternalServerError().json(e),
Self::DuplicateRoleID => HttpResponse::InternalServerError().json(e),
Self::DuplicateRoleName => HttpResponse::BadRequest().json(e),
Self::RoleIDNotFound => HttpResponse::BadRequest().json(e),
Self::RoleNotFound => HttpResponse::NotFound().json(e),
}
}
}

View file

@ -11,11 +11,13 @@ use sqlx::postgres::PgPool;
use crate::identity::{
application::services::{IdentityServices, IdentityServicesObj},
domain::{aggregate::User, employee_aggregate::Employee, store_aggregate::Store},
domain::{
aggregate::User, employee_aggregate::Employee, role_aggregate::Role, store_aggregate::Store,
},
};
use crate::settings::Settings;
use output::{
db::postgres::{employee_view, store_view, user_view, DBOutPostgresAdapter},
db::postgres::{employee_view, role_view, store_view, user_view, DBOutPostgresAdapter},
mailer::lettre::LettreMailer,
phone::twilio::Phone,
};
@ -48,6 +50,9 @@ pub fn load_adapters(pool: PgPool, settings: Settings) -> impl FnOnce(&mut web::
Arc::new(db.clone()),
Arc::new(db.clone()),
Arc::new(db.clone()),
Arc::new(db.clone()),
Arc::new(db.clone()),
Arc::new(db.clone()),
Arc::new(mailer.clone()),
Arc::new(phone.clone()),
Arc::new(phone.clone()),
@ -60,11 +65,13 @@ pub fn load_adapters(pool: PgPool, settings: Settings) -> impl FnOnce(&mut web::
let (store_cqrs_exec, store_cqrs_query) = store_view::init_cqrs(db.clone(), services.clone());
let (employee_cqrs_exec, employee_cqrs_query) =
employee_view::init_cqrs(db.clone(), services.clone());
let (role_cqrs_exec, role_cqrs_query) = role_view::init_cqrs(db.clone(), services.clone());
let identity_cqrs_exec = types::WebIdentityCqrsExec::new(Arc::new(
types::IdentityCqrsExecBuilder::default()
.user(user_cqrs_exec)
.employee(employee_cqrs_exec)
.role(role_cqrs_exec)
.store(store_cqrs_exec)
.build()
.unwrap(),
@ -74,6 +81,7 @@ pub fn load_adapters(pool: PgPool, settings: Settings) -> impl FnOnce(&mut web::
cfg.configure(input::web::load_ctx());
cfg.app_data(Data::new(user_cqrs_query.clone()));
cfg.app_data(Data::new(role_cqrs_query.clone()));
cfg.app_data(Data::new(store_cqrs_query.clone()));
cfg.app_data(Data::new(employee_cqrs_query.clone()));
cfg.app_data(identity_cqrs_exec.clone());

View file

@ -33,6 +33,8 @@ pub struct EmployeeView {
phone_number_country_code: i32,
phone_number_number: i64,
role_id: Option<Uuid>,
phone_verified: bool,
store_id: Option<Uuid>,
deleted: bool,
@ -51,6 +53,7 @@ impl From<EmployeeView> for Employee {
.unwrap(),
)
.emp_id(v.emp_id)
.role_id(v.role_id)
.phone_verified(v.phone_verified)
.store_id(v.store_id)
.deleted(v.deleted)
@ -68,6 +71,7 @@ impl Default for EmployeeView {
last_name: e.last_name().clone(),
phone_number_number: *e.phone_number().number() as i64,
phone_number_country_code: *e.phone_number().country_code() as i32,
role_id: e.role_id().clone(),
phone_verified: *e.phone_verified(),
emp_id: *e.emp_id(),
store_id: e.store_id().clone(),
@ -85,6 +89,7 @@ impl EmployeeView {
self.phone_verified = *e.phone_verified();
self.emp_id = *e.emp_id();
self.store_id = e.store_id().clone();
self.role_id = e.role_id().clone();
self.deleted = *e.deleted();
}
}
@ -104,6 +109,21 @@ impl View<Employee> for EmployeeView {
IdentityEvent::PhoneNumberChanged(e) => unimplemented!(),
// IdentityEvent::InviteAccepted(e) => self.store_id = Some(*e.store_id()),
IdentityEvent::OrganizationExited(e) => self.store_id = None,
IdentityEvent::OwnerAddedEmployeeToStore(e) => {
self.store_id = Some(*e.store_id());
self.role_id = None;
}
IdentityEvent::OwnerRemovedEmployeeFromStore(e) => {
self.store_id = None;
self.role_id = None;
}
IdentityEvent::RoleAdded(e) => {
self.store_id = Some(*e.store_id());
self.role_id = Some(*e.role_id());
}
IdentityEvent::EmployeeRemovedFromRole(e) => {
self.role_id = None;
}
_ => (),
}
@ -129,6 +149,7 @@ impl ViewRepository<EmployeeView, Employee> for DBOutPostgresAdapter {
phone_number_number,
phone_number_country_code,
phone_verified,
role_id,
deleted
FROM
cqrs_identity_employee_query
@ -162,6 +183,7 @@ impl ViewRepository<EmployeeView, Employee> for DBOutPostgresAdapter {
phone_number_number,
phone_number_country_code,
phone_verified,
role_id,
deleted
FROM
@ -216,11 +238,12 @@ impl ViewRepository<EmployeeView, Employee> for DBOutPostgresAdapter {
phone_number_number,
phone_number_country_code,
phone_verified,
role_id,
deleted
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
);",
version,
view.created_time,
@ -231,6 +254,7 @@ impl ViewRepository<EmployeeView, Employee> for DBOutPostgresAdapter {
view.phone_number_number,
view.phone_number_country_code,
view.phone_verified,
view.role_id,
view.deleted,
)
.execute(&self.pool)
@ -252,9 +276,10 @@ impl ViewRepository<EmployeeView, Employee> for DBOutPostgresAdapter {
phone_number_number = $6,
phone_number_country_code = $7,
phone_verified = $8,
role_id = $9,
deleted = $9;",
deleted = $10;",
version,
view.created_time,
view.store_id,
@ -263,6 +288,7 @@ impl ViewRepository<EmployeeView, Employee> for DBOutPostgresAdapter {
view.phone_number_number,
view.phone_number_country_code,
view.phone_verified,
view.role_id,
view.deleted,
)
.execute(&self.pool)
@ -441,6 +467,7 @@ mod tests {
let emp = employee_query.load(&emp_id_str).await.unwrap().unwrap();
assert!(emp.phone_verified);
// TODO: test OwnerAddedEmployeeToStore and OwnerRemovedEmployeeFromStore
settings.drop_db().await;
}
}

View file

@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use uuid::Uuid;
use super::DBOutPostgresAdapter;
use crate::identity::application::port::output::db::{errors::*, get_roles_for_store::*};
use crate::identity::domain::role_aggregate::*;
struct DBRole {
name: String,
role_id: Uuid,
store_id: Uuid,
deleted: bool,
}
impl From<DBRole> for Role {
fn from(v: DBRole) -> Self {
RoleBuilder::default()
.role_id(v.role_id)
.store_id(v.store_id)
.name(v.name)
.deleted(v.deleted)
.build()
.unwrap()
}
}
#[async_trait::async_trait]
impl GetRolesForStoreDBPort for DBOutPostgresAdapter {
async fn get_roles_for_store(&self, store_id: Uuid) -> OutDBPortResult<Vec<Role>> {
let mut res = sqlx::query_as!(
DBRole,
"SELECT
name, role_id, store_id, deleted
FROM
cqrs_identity_role_query
WHERE
store_id = $1
AND
deleted = false;",
store_id,
)
.fetch_all(&self.pool)
.await?;
Ok(res.drain(0..).map(|r| r.into()).collect())
}
}
#[cfg(test)]
pub mod tests {
use uuid::Uuid;
use super::*;
use crate::identity::adapters::output::db::postgres::role_id_exists::tests::create_dummy_role_record;
use crate::utils::uuid::tests::UUID;
#[actix_rt::test]
async fn test_postgres_role_exists() {
let role_id = Uuid::new_v4();
let store_id = Uuid::new_v4();
let settings = crate::settings::tests::get_settings().await;
settings.create_db().await;
let db = super::DBOutPostgresAdapter::new(
sqlx::postgres::PgPool::connect(&settings.database.url)
.await
.unwrap(),
);
let role = RoleBuilder::default()
.name("role_name".into())
.store_id(UUID)
.role_id(role_id)
.build()
.unwrap();
// state doesn't exist
assert!(db.get_roles_for_store(UUID).await.unwrap().is_empty());
create_dummy_role_record(&role, &db).await;
// state exists
assert_eq!(
db.get_roles_for_store(*role.store_id()).await.unwrap(),
vec![role.clone()]
);
// Set role.deleted = true; now db.role_name_exists_for_store must return false
sqlx::query!(
"UPDATE cqrs_identity_role_query SET deleted = true WHERE role_id = $1;",
role.role_id(),
)
.execute(&db.pool)
.await
.unwrap();
assert!(db.get_roles_for_store(UUID).await.unwrap().is_empty());
settings.drop_db().await;
}
}

View file

@ -15,6 +15,7 @@ pub mod email_exists;
pub mod employee_view;
mod errors;
pub mod get_verification_secret;
pub mod role_view;
pub mod store_view;
pub mod user_id_exists;
pub mod user_view;
@ -27,8 +28,11 @@ pub mod delete_verification_otp;
pub mod emp_id_exists;
pub mod get_emp_id_from_phone_number;
pub mod get_login_otp;
mod get_roles_for_store;
pub mod get_verification_otp;
pub mod phone_exists;
mod role_id_exists;
mod role_name_exists_for_store;
//pub mod get_invite;
//pub mod invite_id_exists;

View file

@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use uuid::Uuid;
use super::DBOutPostgresAdapter;
use crate::identity::application::port::output::db::{errors::*, role_id_exists::*};
use crate::identity::domain::role_aggregate::*;
#[async_trait::async_trait]
impl RoleIDExistsDBPort for DBOutPostgresAdapter {
async fn role_id_exists(&self, role_id: &Uuid) -> OutDBPortResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (
SELECT 1
FROM cqrs_identity_role_query
WHERE
role_id = $1
);",
role_id
)
.fetch_one(&self.pool)
.await?;
if let Some(x) = res.exists {
Ok(x)
} else {
Ok(false)
}
}
}
#[cfg(test)]
pub mod tests {
use uuid::Uuid;
use crate::utils::uuid::tests::UUID;
use super::*;
pub async fn create_dummy_role_record(s: &Role, db: &DBOutPostgresAdapter) {
sqlx::query!(
"INSERT INTO cqrs_identity_role_query
(version, name, role_id, store_id, deleted)
VALUES ($1, $2, $3, $4, $5);",
1,
s.name(),
s.role_id(),
s.store_id(),
false
)
.execute(&db.pool)
.await
.unwrap();
}
#[actix_rt::test]
async fn test_postgres_role_exists() {
let role_id = Uuid::new_v4();
let settings = crate::settings::tests::get_settings().await;
settings.create_db().await;
let db = super::DBOutPostgresAdapter::new(
sqlx::postgres::PgPool::connect(&settings.database.url)
.await
.unwrap(),
);
let role = RoleBuilder::default()
.name("role_name".into())
.store_id(UUID)
.role_id(role_id)
.build()
.unwrap();
// state doesn't exist
assert!(!db.role_id_exists(role.role_id()).await.unwrap());
create_dummy_role_record(&role, &db).await;
// state exists
assert!(db.role_id_exists(role.role_id()).await.unwrap());
settings.drop_db().await;
}
}

View file

@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::DBOutPostgresAdapter;
use crate::identity::application::port::output::db::{errors::*, role_name_exists_for_store::*};
use crate::identity::domain::role_aggregate::*;
#[async_trait::async_trait]
impl RoleNameExistsForStoreDBPort for DBOutPostgresAdapter {
async fn role_name_exists_for_store(&self, s: &Role) -> OutDBPortResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (
SELECT 1
FROM cqrs_identity_role_query
WHERE
name = $1
AND
store_id = $2
AND
deleted = false
);",
s.name(),
s.store_id(),
)
.fetch_one(&self.pool)
.await?;
if let Some(x) = res.exists {
Ok(x)
} else {
Ok(false)
}
}
}
#[cfg(test)]
pub mod tests {
use uuid::Uuid;
use super::*;
use crate::identity::adapters::output::db::postgres::role_id_exists::tests::create_dummy_role_record;
use crate::utils::uuid::tests::UUID;
#[actix_rt::test]
async fn test_postgres_role_exists() {
let role_id = Uuid::new_v4();
let store_id = Uuid::new_v4();
let settings = crate::settings::tests::get_settings().await;
settings.create_db().await;
let db = super::DBOutPostgresAdapter::new(
sqlx::postgres::PgPool::connect(&settings.database.url)
.await
.unwrap(),
);
let role = RoleBuilder::default()
.name("role_name".into())
.store_id(UUID)
.role_id(role_id)
.build()
.unwrap();
// state doesn't exist
assert!(!db.role_name_exists_for_store(&role).await.unwrap());
create_dummy_role_record(&role, &db).await;
// state exists
assert!(db.role_name_exists_for_store(&role).await.unwrap());
// Set role.deleted = true; now db.role_name_exists_for_store must return false
sqlx::query!(
"UPDATE cqrs_identity_role_query SET deleted = true WHERE role_id = $1;",
role.role_id(),
)
.execute(&db.pool)
.await
.unwrap();
assert!(!db.role_name_exists_for_store(&role).await.unwrap());
settings.drop_db().await;
}
}

View file

@ -0,0 +1,395 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::str::FromStr;
use std::sync::Arc;
use async_trait::async_trait;
use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository};
use cqrs_es::{EventEnvelope, Query, View};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use uuid::Uuid;
use super::errors::*;
use super::DBOutPostgresAdapter;
use crate::identity::adapters::types::{IdentityRoleCqrsExec, IdentityRoleCqrsView};
use crate::identity::application::services::{events::IdentityEvent, IdentityServicesObj};
use crate::identity::domain::role_aggregate::*;
use crate::types::currency::{self, Currency, PriceBuilder};
use crate::utils::parse_aggregate_id::parse_aggregate_id;
pub const NEW_ROLE_NON_UUID: &str = "identity_new_bill_non_uuid-asdfa";
// The view for a Role query, for a standard http application this should
// be designed to reflect the response dto that will be returned to a user.
#[derive(Debug, Serialize, Deserialize)]
pub struct RoleView {
created_time: OffsetDateTime,
name: String,
role_id: Uuid,
store_id: Uuid,
deleted: bool,
}
impl From<RoleView> for Role {
fn from(v: RoleView) -> Self {
RoleBuilder::default()
.name(v.name)
.role_id(v.role_id)
.store_id(v.store_id)
.deleted(v.deleted)
.build()
.unwrap()
}
}
impl Default for RoleView {
fn default() -> Self {
let e = Role::default();
Self {
created_time: OffsetDateTime::now_utc(),
name: e.name().clone(),
role_id: *e.role_id(),
store_id: e.store_id().clone(),
deleted: false,
}
}
}
impl RoleView {
fn merge(&mut self, e: &Role) {
self.name = e.name().clone();
self.role_id = *e.role_id();
self.store_id = e.store_id().clone();
self.deleted = *e.deleted();
}
}
// This updates the view with events as they are committed.
// The logic should be minimal here, e.g., don't calculate the account balance,
// design the events to carry the balance information instead.
impl View<Role> for RoleView {
fn update(&mut self, event: &EventEnvelope<Role>) {
match &event.payload {
IdentityEvent::RoleAdded(e) => {
self.name = e.name().clone();
self.role_id = *e.role_id();
self.store_id = e.store_id().clone();
self.deleted = false;
}
_ => (),
}
}
}
#[async_trait]
impl ViewRepository<RoleView, Role> for DBOutPostgresAdapter {
async fn load(&self, role_id: &str) -> Result<Option<RoleView>, PersistenceError> {
let role_id = match parse_aggregate_id(role_id, NEW_ROLE_NON_UUID)? {
Some((val, _)) => return Ok(Some(val)),
None => Uuid::parse_str(role_id).unwrap(),
};
let res = sqlx::query_as!(
RoleView,
"SELECT
created_time,
name,
role_id,
store_id,
deleted
FROM
cqrs_identity_role_query
WHERE
role_id = $1;",
role_id
)
.fetch_one(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
Ok(Some(res))
}
async fn load_with_context(
&self,
role_id: &str,
) -> Result<Option<(RoleView, ViewContext)>, PersistenceError> {
let role_id = match parse_aggregate_id(role_id, NEW_ROLE_NON_UUID)? {
Some(val) => return Ok(Some(val)),
None => Uuid::parse_str(role_id).unwrap(),
};
let res = sqlx::query_as!(
RoleView,
"SELECT
created_time,
name,
role_id,
store_id,
deleted
FROM
cqrs_identity_role_query
WHERE
role_id = $1;",
&role_id,
)
.fetch_one(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
struct Context {
version: i64,
role_id: Uuid,
}
let ctx = sqlx::query_as!(
Context,
"SELECT
role_id, version
FROM
cqrs_identity_role_query
WHERE
role_id = $1;",
role_id
)
.fetch_one(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
let view_context = ViewContext::new(ctx.role_id.to_string(), ctx.version);
Ok(Some((res, view_context)))
}
async fn update_view(
&self,
view: RoleView,
context: ViewContext,
) -> Result<(), PersistenceError> {
match context.version {
0 => {
let version = context.version + 1;
sqlx::query!(
"INSERT INTO cqrs_identity_role_query (
version,
created_time,
store_id,
role_id,
name,
deleted
) VALUES (
$1, $2, $3, $4, $5, $6
);",
version,
view.created_time,
view.store_id,
view.role_id,
view.name,
view.deleted,
)
.execute(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
}
_ => {
let version = context.version + 1;
sqlx::query!(
"UPDATE
cqrs_identity_role_query
SET
version = $1,
created_time = $2,
store_id = $3,
name = $4,
deleted = $5;",
version,
view.created_time,
view.store_id,
view.name,
view.deleted
)
.execute(&self.pool)
.await
.map_err(PostgresAggregateError::from)?;
}
}
Ok(())
}
}
#[async_trait]
impl Query<Role> for DBOutPostgresAdapter {
async fn dispatch(&self, role_id: &str, events: &[EventEnvelope<Role>]) {
let res = self
.load_with_context(role_id)
.await
.unwrap_or_else(|_| Some((RoleView::default(), ViewContext::new(role_id.into(), 0))));
let (mut view, view_context): (RoleView, ViewContext) = res.unwrap();
for event in events {
view.update(event);
}
self.update_view(view, view_context).await.unwrap();
}
}
pub fn init_cqrs(
db: DBOutPostgresAdapter,
services: IdentityServicesObj,
) -> (IdentityRoleCqrsExec, IdentityRoleCqrsView) {
let queries: Vec<Box<dyn Query<Role>>> = vec![Box::new(db.clone())];
let pool = db.pool.clone();
(
Arc::new(postgres_es::postgres_cqrs(pool.clone(), queries, services)),
Arc::new(db.clone()),
)
}
//
//#[cfg(test)]
//mod tests {
// use super::*;
//
// use postgres_es::PostgresCqrs;
//
// use crate::{
// db::migrate::*,
// identity::{
// application::{
// port::output::{
// db::get_verification_otp::GetVerificationOTPOutDBPort,
// phone::account_validation_otp::mock_account_validation_otp_phone_port,
// },
// services::{
// employee_register_service::*, employee_verify_phone_number_service::*,
// IdentityCommand, MockIdentityServicesInterface,
// },
// },
// domain::{
// employee_aggregate::*, employee_register_command::*, verify_phone_number_command::*,
// },
// },
// tests::bdd::*,
// utils::{random_number::tests::mock_generate_random_number, uuid::tests::*},
// };
// use std::sync::Arc;
//
// #[actix_rt::test]
// async fn pg_query_identity_employee_view() {
// let settings = crate::settings::tests::get_settings().await;
// //let settings = crate::settings::Settings::new().unwrap();
// settings.create_db().await;
//
// let db = crate::db::sqlx_postgres::Postgres::init(&settings.database.url).await;
// db.migrate().await;
// let db = DBOutPostgresAdapter::new(db.pool.clone());
//
// let queries: Vec<Box<dyn Query<Role>>> = vec![Box::new(db.clone())];
//
// let mut mock_services = MockIdentityServicesInterface::new();
//
// //let store = Store::default();
// //crate::identity::adapters::output::db::postgres::store_id_exists::tests::create_dummy_store_record(&store, &db).await;
//
// let db2 = Arc::new(db.clone());
// mock_services
// .expect_employee_register_service()
// .times(IS_CALLED_ONLY_ONCE.unwrap())
// .returning(move || {
// Arc::new(
// RoleRegisterUserServiceBuilder::default()
// .db_role_id_exists_adapter(db2.clone())
// .db_create_verification_otp_adapter(db2.clone())
// .db_phone_exists_adapter(db2.clone())
// .random_number_adapter(mock_generate_random_number(
// IS_CALLED_ONLY_ONCE,
// 999,
// ))
// .phone_account_validation_otp_adapter(
// mock_account_validation_otp_phone_port(IS_CALLED_ONLY_ONCE),
// )
// .build()
// .unwrap(),
// )
// });
//
// let db2 = Arc::new(db.clone());
// mock_services
// .expect_employee_verify_phone_number_service()
// .times(IS_CALLED_ONLY_ONCE.unwrap())
// .returning(move || {
// Arc::new(
// RoleVerifyPhoneNumberServiceBuilder::default()
// .db_get_role_id_from_phone_number_adapter(db2.clone())
// .db_delete_verification_otp(db2.clone())
// .db_get_verification_otp(db2.clone())
// .build()
// .unwrap(),
// )
// });
//
// let (cqrs, employee_query): (
// Arc<PostgresCqrs<Role>>,
// Arc<dyn ViewRepository<RoleView, Role>>,
// ) = (
// Arc::new(postgres_es::postgres_cqrs(
// db.pool.clone(),
// queries,
// Arc::new(mock_services),
// )),
// Arc::new(db.clone()),
// );
//
// let cmd = RoleRegisterCommandBuilder::default()
// .name("foooint".into())
// .last_name("foooint".into())
// .phone_number(PhoneNumber::default())
// .role_id(Uuid::new_v4())
// .build()
// .unwrap();
//
// let role_id_str = cmd.role_id().to_string();
//
// cqrs.execute(&role_id_str, IdentityCommand::RoleRegister(cmd.clone()))
// .await
// .unwrap();
// let emp = employee_query.load(&role_id_str).await.unwrap().unwrap();
// let emp: Role = emp.into();
// assert_eq!(emp.name(), cmd.name());
// assert_eq!(emp.last_name(), cmd.last_name());
// assert_eq!(emp.role_id(), cmd.role_id());
// assert_eq!(emp.phone_number(), cmd.phone_number());
// assert!(!*emp.phone_verified());
// assert!(!*emp.deleted());
// assert!(emp.store_id().is_none());
//
// let otp = db
// .get_verification_otp(emp.phone_number())
// .await
// .unwrap()
// .unwrap();
// cqrs.execute(
// &role_id_str,
// IdentityCommand::RoleVerifyPhoneNumber(
// VerifyPhoneNumberCommandBuilder::default()
// .otp(otp.into())
// .phone_number(emp.phone_number().clone())
// .build()
// .unwrap(),
// ),
// )
// .await
// .unwrap();
//
// let emp = employee_query.load(&role_id_str).await.unwrap().unwrap();
// assert!(emp.phone_verified);
//
// settings.drop_db().await;
// }
//}

View file

@ -32,13 +32,30 @@ impl UserIDExistsOutDBPort for DBOutPostgresAdapter {
}
#[cfg(test)]
mod tests {
pub mod tests {
use super::*;
use crate::utils::uuid::tests::UUID;
use crate::identity::domain::aggregate::*;
pub async fn create_user(user: &User, user_id: Uuid, db: &DBOutPostgresAdapter) {
sqlx::query!(
"INSERT INTO user_query
(version, user_id, email, hashed_password, first_name, last_name)
VALUES ($1, $2, $3, $4, $5, $6);",
1,
user_id,
user.email(),
user.hashed_password(),
user.first_name(),
user.last_name(),
)
.execute(&db.pool)
.await
.unwrap();
}
#[actix_rt::test]
async fn test_postgres_user_id_exists() {
let settings = crate::settings::tests::get_settings().await;
@ -54,20 +71,21 @@ mod tests {
// state doesn't exist
assert!(!db.user_id_exists(&UUID).await.unwrap());
sqlx::query!(
"INSERT INTO user_query
(version, user_id, email, hashed_password, first_name, last_name)
VALUES ($1, $2, $3, $4, $5, $6);",
1,
UUID,
user.email(),
user.hashed_password(),
user.first_name(),
user.last_name(),
)
.execute(&db.pool)
.await
.unwrap();
create_user(&user, UUID, &db).await;
// sqlx::query!(
// "INSERT INTO user_query
// (version, user_id, email, hashed_password, first_name, last_name)
// VALUES ($1, $2, $3, $4, $5, $6);",
// 1,
// UUID,
// user.email(),
// user.hashed_password(),
// user.first_name(),
// user.last_name(),
// )
// .execute(&db.pool)
// .await
// .unwrap();
// state exists
assert!(db.user_id_exists(&UUID).await.unwrap());

View file

@ -14,7 +14,7 @@ use super::errors::*;
use super::DBOutPostgresAdapter;
use crate::identity::adapters::types::{IdentityUserCqrsExec, IdentityUserCqrsView};
use crate::identity::application::services::{events::IdentityEvent, IdentityServicesObj};
use crate::identity::domain::aggregate::User;
use crate::identity::domain::aggregate::{User, UserBuilder};
use crate::utils::parse_aggregate_id::parse_aggregate_id;
pub const NEW_USER_NON_UUID: &str = "new_user_non_uuid-asdfa";
@ -33,6 +33,23 @@ pub struct UserView {
deleted: bool,
}
impl From<UserView> for User {
fn from(v: UserView) -> Self {
UserBuilder::default()
.first_name(v.first_name)
.last_name(v.last_name)
.user_id(v.user_id)
.email(v.email)
.hashed_password(v.hashed_password)
.is_admin(v.is_admin)
.is_verified(v.is_verified)
.deleted(v.deleted)
.email_verified(v.is_verified)
.build()
.unwrap()
}
}
// This updates the view with events as they are committed.
// The logic should be minimal here, e.g., don't calculate the account balance,
// design the events to carry the balance information instead.
@ -241,3 +258,221 @@ pub fn init_cqrs(
Arc::new(db.clone()),
)
}
#[cfg(test)]
mod tests {
use super::*;
use postgres_es::PostgresCqrs;
use crate::{
db::migrate::*,
identity::{
adapters::output::db::postgres::user_id_exists::tests::create_user,
application::{
port::output::mailer::account_validation_link::mock_account_validation_link_mailer_port,
services::{
add_store_service::*,
events::*,
login::{command::*, service::*, *},
register_user::{
command::{tests::PASSWORD, *},
events::*,
service::*,
*,
},
update_store_service::*,
MockIdentityServicesInterface, *,
},
},
domain::{add_store_command::*, update_store_command::*},
},
settings::Settings,
tests::bdd::*,
utils::{random_number::*, random_string::*, uuid::tests::UUID, uuid::*},
};
use std::sync::Arc;
async fn init_test_context() -> (
Settings,
DBOutPostgresAdapter,
Vec<Box<dyn Query<User>>>,
MockIdentityServicesInterface,
GetUUIDInterfaceObj,
GenerateRandomStringInterfaceObj,
GenerateRandomNumberInterfaceObj,
) {
let settings = crate::settings::tests::get_settings().await;
//let settings = crate::settings::Settings::new().unwrap();
settings.create_db().await;
let db = crate::db::sqlx_postgres::Postgres::init(&settings.database.url).await;
db.migrate().await;
let db = DBOutPostgresAdapter::new(db.pool.clone());
let simple_query = SimpleLoggingQuery {};
let queries: Vec<Box<dyn Query<User>>> = vec![Box::new(simple_query), Box::new(db.clone())];
let mut mock_services = MockIdentityServicesInterface::new();
let random_uuid = Arc::new(GenerateUUID);
let random_string = GenerateRandomString::new();
let random_number = GenerateRandomNumber::new();
(
settings,
db,
queries,
mock_services,
random_uuid,
random_string,
random_number,
)
}
#[actix_rt::test]
async fn user_view_login() {
let (settings, db, queries, mut mock_services, random_uuid, random_string, random_number) =
init_test_context().await;
let db2 = db.clone();
mock_services
.expect_login()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.returning(move || Arc::new(LoginService));
let (cqrs_executor, cqrs_view) = init_cqrs(db.clone(), Arc::new(mock_services));
let user = User::default();
create_user(&user, *user.user_id(), &db).await;
let cmd = LoginCommand::get_cmd();
cqrs_executor
.execute(&user.user_id().to_string(), IdentityCommand::Login(cmd))
.await
.unwrap();
settings.drop_db().await;
}
#[actix_rt::test]
async fn user_view_register() {
let (settings, db, queries, mut mock_services, random_uuid, random_string, random_number) =
init_test_context().await;
let service: RegisterUserServiceObj = Arc::new(
RegisterUserServiceBuilder::default()
.db_email_exists_adapter(Arc::new(db.clone()))
.db_user_id_exists_adapter(Arc::new(db.clone()))
.db_create_verification_secret_adapter(Arc::new(db.clone()))
.mailer_account_validation_link_adapter(mock_account_validation_link_mailer_port(
IGNORE_CALL_COUNT,
))
.random_string_adapter(random_string.clone())
.build()
.unwrap(),
);
mock_services
.expect_register_user()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(service); //(move || Arc::new(service.clone()));
let (cqrs_executor, cqrs_view) = init_cqrs(db.clone(), Arc::new(mock_services));
let cmd = RegisterUserCommand::get_command();
let user_id = *cmd.user_id();
cqrs_executor
.execute(
&user_id.to_string(),
IdentityCommand::RegisterUser(cmd.clone()),
)
.await
.unwrap();
let user = cqrs_view.load(&user_id.to_string()).await.unwrap().unwrap();
let user: User = user.into();
assert_eq!(user.first_name(), cmd.first_name());
assert_eq!(user.last_name(), cmd.last_name());
assert_eq!(user.email(), cmd.email());
assert_eq!(user.user_id(), cmd.user_id());
assert_eq!(user.hashed_password(), cmd.hashed_password());
assert!(!user.deleted());
settings.drop_db().await;
}
// let cmd = AddStoreCommandBuilder::default()
// .name(rand.get_random(10))
// .address(None)
// .owner(UUID)
// .store_id(UUID)
// .build()
// .unwrap();
// cqrs.execute(
// &cmd.store_id().to_string(),
// IdentityCommand::AddStore(cmd.clone()),
// )
//
//
// let (cqrs, store_query): (
// Arc<PostgresCqrs<Store>>,
// Arc<dyn ViewRepository<StoreView, Store>>,
// ) = (
// Arc::new(postgres_es::postgres_cqrs(
// db.pool.clone(),
// queries,
// Arc::new(mock_services),
// )),
// Arc::new(db.clone()),
// );
//
// let cmd = AddStoreCommandBuilder::default()
// .name(rand.get_random(10))
// .address(None)
// .owner(UUID)
// .store_id(UUID)
// .build()
// .unwrap();
// cqrs.execute(
// &cmd.store_id().to_string(),
// IdentityCommand::AddStore(cmd.clone()),
// )
// .await
// .unwrap();
//
// let store = store_query
// .load(&(*cmd.store_id()).to_string())
// .await
// .unwrap()
// .unwrap();
// let store: Store = store.into();
// assert_eq!(store.name(), cmd.name());
// assert_eq!(store.address(), cmd.address());
// assert_eq!(store.owner(), cmd.owner());
// assert_eq!(store.store_id(), cmd.store_id());
// assert!(!store.deleted());
//
// let update_store_cmd = UpdateStoreCommand::new(
// rand.get_random(10),
// Some(rand.get_random(10)),
// UUID,
// store,
// UUID,
// )
// .unwrap();
// cqrs.execute(
// &cmd.store_id().to_string(),
// IdentityCommand::UpdateStore(update_store_cmd.clone()),
// )
// .await
// .unwrap();
// let store = store_query
// .load(&(*cmd.store_id()).to_string())
// .await
// .unwrap()
// .unwrap();
// let store: Store = store.into();
// assert_eq!(store.name(), update_store_cmd.name());
// assert_eq!(store.address(), update_store_cmd.address());
// assert_eq!(store.owner(), update_store_cmd.owner());
// assert_eq!(store.store_id(), update_store_cmd.old_store().store_id());
// assert!(!store.deleted());
//
// settings.drop_db().await;
}

View file

@ -17,12 +17,14 @@ use crate::identity::{
adapters::{
input::web::RoutesRepository,
output::db::postgres::{
employee_view::EmployeeView, store_view::StoreView, user_view::UserView,
DBOutPostgresAdapter,
employee_view::EmployeeView, role_view::RoleView, store_view::StoreView,
user_view::UserView, DBOutPostgresAdapter,
},
},
application::services::{errors::IdentityError, IdentityCommand, IdentityServicesObj},
domain::{aggregate::User, employee_aggregate::Employee, store_aggregate::Store},
domain::{
aggregate::User, employee_aggregate::Employee, role_aggregate::Role, store_aggregate::Store,
},
};
pub type WebIdentityRoutesRepository = Data<Arc<RoutesRepository>>;
@ -56,6 +58,7 @@ pub struct IdentityCqrsExec {
user: IdentityUserCqrsExec,
store: IdentityStoreCqrsExec,
employee: IdentityEmployeeCqrsExec,
role: IdentityRoleCqrsExec,
}
#[async_trait]
@ -67,8 +70,13 @@ impl IdentityCqrsExecutor for IdentityCqrsExec {
) -> Result<(), AggregateError<IdentityError>> {
self.user.execute(aggregate_id, command.clone()).await?;
self.store.execute(aggregate_id, command.clone()).await?;
self.role.execute(aggregate_id, command.clone()).await?;
self.employee.execute(aggregate_id, command).await?;
Ok(())
}
}
pub type IdentityRoleCqrsExec = Arc<PostgresCqrs<Role>>;
pub type IdentityRoleCqrsView = Arc<dyn ViewRepository<RoleView, Role>>;
pub type WebidentityRoleCqrsView = Data<IdentityRoleCqrsView>;

View file

@ -21,4 +21,6 @@ pub enum OutDBPortError {
DuplicateStoreName,
DuplicateStoreID,
StoreIDNotFound,
RoleIDNotFound,
DuplicateRoleName,
}

View file

@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
use uuid::Uuid;
use crate::identity::domain::role_aggregate::Role;
use super::errors::*;
#[cfg(test)]
#[allow(unused_imports)]
pub use tests::*;
#[automock]
#[async_trait::async_trait]
pub trait GetRolesForStoreDBPort: Send + Sync {
async fn get_roles_for_store(&self, store_id: Uuid) -> OutDBPortResult<Vec<Role>>;
}
pub type GetRolesForStoreDBPortObj = std::sync::Arc<dyn GetRolesForStoreDBPort>;
#[cfg(test)]
pub mod tests {
use super::*;
use std::sync::Arc;
pub fn mock_get_roles_for_store_db_port_empty(
times: Option<usize>,
) -> GetRolesForStoreDBPortObj {
let mut m = MockGetRolesForStoreDBPort::new();
if let Some(times) = times {
m.expect_get_roles_for_store()
.times(times)
.return_const(Ok(Vec::default()));
} else {
m.expect_get_roles_for_store()
.return_const(Ok(Vec::default()));
}
Arc::new(m)
}
pub fn mock_get_roles_for_store_db_port(times: Option<usize>) -> GetRolesForStoreDBPortObj {
let mut m = MockGetRolesForStoreDBPort::new();
if let Some(times) = times {
m.expect_get_roles_for_store()
.times(times)
.return_const(Ok(vec![Role::get_role()]));
} else {
m.expect_get_roles_for_store()
.return_const(Ok(vec![Role::get_role()]));
}
Arc::new(m)
}
}

View file

@ -22,4 +22,7 @@ pub mod store_id_exists;
pub mod store_name_exists;
pub mod user_id_exists;
//pub mod verification_otp_exists;
pub mod get_roles_for_store;
pub mod role_id_exists;
pub mod role_name_exists_for_store;
pub mod verification_secret_exists;

View file

@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
use uuid::Uuid;
use super::errors::*;
#[cfg(test)]
#[allow(unused_imports)]
pub use tests::*;
#[automock]
#[async_trait::async_trait]
pub trait RoleIDExistsDBPort: Send + Sync {
async fn role_id_exists(&self, role_id: &Uuid) -> OutDBPortResult<bool>;
}
pub type RoleIDExistsDBPortObj = std::sync::Arc<dyn RoleIDExistsDBPort>;
#[cfg(test)]
pub mod tests {
use super::*;
use std::sync::Arc;
pub fn mock_role_id_exists_db_port_false(times: Option<usize>) -> RoleIDExistsDBPortObj {
let mut m = MockRoleIDExistsDBPort::new();
if let Some(times) = times {
m.expect_role_id_exists()
.times(times)
.returning(|_| Ok(false));
} else {
m.expect_role_id_exists().returning(|_| Ok(false));
}
Arc::new(m)
}
pub fn mock_role_id_exists_db_port_true(times: Option<usize>) -> RoleIDExistsDBPortObj {
let mut m = MockRoleIDExistsDBPort::new();
if let Some(times) = times {
m.expect_role_id_exists()
.times(times)
.returning(|_| Ok(true));
} else {
m.expect_role_id_exists().returning(|_| Ok(true));
}
Arc::new(m)
}
}

View file

@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
use crate::identity::domain::role_aggregate::Role;
use super::errors::*;
#[cfg(test)]
#[allow(unused_imports)]
pub use tests::*;
#[automock]
#[async_trait::async_trait]
pub trait RoleNameExistsForStoreDBPort: Send + Sync {
async fn role_name_exists_for_store(&self, n: &Role) -> OutDBPortResult<bool>;
}
pub type RoleNameExistsForStoreDBPortObj = std::sync::Arc<dyn RoleNameExistsForStoreDBPort>;
#[cfg(test)]
pub mod tests {
use super::*;
use std::sync::Arc;
pub fn mock_role_name_exists_for_store_db_port_false(
times: Option<usize>,
) -> RoleNameExistsForStoreDBPortObj {
let mut m = MockRoleNameExistsForStoreDBPort::new();
if let Some(times) = times {
m.expect_role_name_exists_for_store()
.times(times)
.returning(|_| Ok(false));
} else {
m.expect_role_name_exists_for_store()
.returning(|_| Ok(false));
}
Arc::new(m)
}
pub fn mock_role_name_exists_for_store_db_port_true(
times: Option<usize>,
) -> RoleNameExistsForStoreDBPortObj {
let mut m = MockRoleNameExistsForStoreDBPort::new();
if let Some(times) = times {
m.expect_role_name_exists_for_store()
.times(times)
.returning(|_| Ok(true));
} else {
m.expect_role_name_exists_for_store()
.returning(|_| Ok(true));
}
Arc::new(m)
}
}

View file

@ -0,0 +1,186 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_builder::Builder;
use mockall::predicate::*;
use mockall::*;
use crate::identity::application::port::output::db::{
role_id_exists::*, role_name_exists_for_store::*, store_id_exists::*,
};
use crate::identity::domain::role_aggregate::RoleBuilder;
use crate::identity::domain::{add_role_command::*, role_added_event::*};
use super::errors::*;
#[automock]
#[async_trait::async_trait]
pub trait AddRoleToStoreUseCase: Send + Sync {
async fn add_role_to_store(&self, cmd: AddRoleCommand) -> IdentityResult<RoleAddedEvent>;
}
pub type AddRoleToStoreServiceObj = std::sync::Arc<dyn AddRoleToStoreUseCase>;
#[derive(Clone, Builder)]
pub struct AddRoleToStoreService {
db_store_id_exists_adapter: StoreIDExistsDBPortObj,
db_role_id_exists_adapter: RoleIDExistsDBPortObj,
db_role_name_exists_for_store_adapter: RoleNameExistsForStoreDBPortObj,
}
#[async_trait::async_trait]
impl AddRoleToStoreUseCase for AddRoleToStoreService {
async fn add_role_to_store(&self, cmd: AddRoleCommand) -> IdentityResult<RoleAddedEvent> {
if !self
.db_store_id_exists_adapter
.store_id_exists(cmd.store_id())
.await?
{
return Err(IdentityError::StoreNotFound);
}
if self
.db_role_id_exists_adapter
.role_id_exists(cmd.role_id())
.await?
{
return Err(IdentityError::DuplicateRoleID);
}
let role = RoleBuilder::default()
.name(cmd.name().trim().to_lowercase())
.role_id(*cmd.role_id())
.store_id(*cmd.store_id())
.build()
.unwrap();
if self
.db_role_name_exists_for_store_adapter
.role_name_exists_for_store(&role)
.await?
{
return Err(IdentityError::DuplicateRoleName);
}
Ok(RoleAddedEventBuilder::default()
.name(role.name().clone())
.store_id(*role.store_id())
.role_id(*role.role_id())
.build()
.unwrap())
}
}
#[cfg(test)]
mod tests {
use crate::{tests::bdd::*, utils::uuid::tests::*};
use super::*;
impl AddRoleToStoreService {
pub fn mock_service(times: Option<usize>, cmd: AddRoleCommand) -> AddRoleToStoreServiceObj {
let res = RoleAddedEvent::get_event(&cmd);
let mut m = MockAddRoleToStoreUseCase::default();
if let Some(times) = times {
m.expect_add_role_to_store()
.times(times)
.returning(move |_| Ok(res.clone()));
} else {
m.expect_add_role_to_store()
.returning(move |_| Ok(res.clone()));
}
std::sync::Arc::new(m)
}
}
#[actix_rt::test]
async fn test_service() {
let s = AddRoleToStoreServiceBuilder::default()
.db_store_id_exists_adapter(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_false(
IS_CALLED_ONLY_ONCE,
))
.build()
.unwrap();
{
let cmd = AddRoleCommandBuilder::default()
.role_id(UUID)
.store_id(UUID)
.name("foo".into())
.build()
.unwrap();
let res = s.add_role_to_store(cmd.clone()).await.unwrap();
assert_eq!(*res.role_id(), *cmd.role_id());
assert_eq!(res.name(), cmd.name());
assert_eq!(*res.store_id(), *cmd.store_id());
}
}
#[actix_rt::test]
async fn test_service_store_no_exist() {
let s = AddRoleToStoreServiceBuilder::default()
.db_store_id_exists_adapter(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_false(IS_NEVER_CALLED))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_false(
IS_NEVER_CALLED,
))
.build()
.unwrap();
{
let cmd = AddRoleCommand::get_cmd();
assert_eq!(
s.add_role_to_store(cmd.clone()).await.err(),
Some(IdentityError::StoreNotFound)
);
}
}
#[actix_rt::test]
async fn test_service_role_id_exist() {
let s = AddRoleToStoreServiceBuilder::default()
.db_store_id_exists_adapter(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_false(
IS_NEVER_CALLED,
))
.build()
.unwrap();
{
let cmd = AddRoleCommand::get_cmd();
assert_eq!(
s.add_role_to_store(cmd.clone()).await.err(),
Some(IdentityError::DuplicateRoleID)
);
}
}
#[actix_rt::test]
async fn test_service_role_exists_for_store() {
let s = AddRoleToStoreServiceBuilder::default()
.db_store_id_exists_adapter(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_true(
IS_CALLED_ONLY_ONCE,
))
.build()
.unwrap();
{
let cmd = AddRoleCommand::get_cmd();
assert_eq!(
s.add_role_to_store(cmd.clone()).await.err(),
Some(IdentityError::DuplicateRoleName)
);
}
}
}

View file

@ -25,6 +25,12 @@ impl DeleteUserCommand {
mod tests {
use super::*;
impl DeleteUserCommand {
pub fn get_cmd() -> Self {
DeleteUserCommand
}
}
#[test]
fn test_cmd() {
let config = argon2_creds::Config::default();

View file

@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
pub mod command;
pub mod events;
@ -8,6 +10,7 @@ pub mod service;
use super::errors::*;
#[automock]
#[async_trait::async_trait]
pub trait DeleteUserUseCase: Send + Sync {
async fn delete_user(

View file

@ -20,6 +20,23 @@ impl DeleteUserUseCase for DeleteUserService {
mod tests {
use super::*;
impl DeleteUserService {
pub fn mock_service(
times: Option<usize>,
cmd: command::DeleteUserCommand,
) -> DeleteUserServiceObj {
let mut m = MockDeleteUserUseCase::default();
if let Some(times) = times {
m.expect_delete_user().times(times).return_const(());
} else {
m.expect_delete_user().return_const(());
}
std::sync::Arc::new(m)
}
}
#[actix_rt::test]
async fn test_service() {
let config = argon2_creds::Config::default();

View file

@ -32,6 +32,11 @@ pub enum IdentityError {
DuplicateStoreName,
StoreIDNotFound,
DuplicateStoreID,
DuplicateUserID,
DuplicateRoleID,
DuplicateRoleName,
RoleIDNotFound,
RoleNotFound,
}
pub type IdentityCommandResult<V> = Result<V, IdentityCommandError>;
@ -80,6 +85,8 @@ impl From<OutDBPortError> for IdentityError {
OutDBPortError::DuplicateStoreName => Self::DuplicateStoreName,
OutDBPortError::DuplicateStoreID => Self::DuplicateStoreID,
OutDBPortError::StoreIDNotFound => Self::StoreIDNotFound,
OutDBPortError::RoleIDNotFound => Self::RoleIDNotFound,
OutDBPortError::DuplicateRoleName => Self::DuplicateRoleName,
}
}
}

View file

@ -12,17 +12,13 @@ use super::update_email::events::*;
use super::update_password::events::*;
use crate::identity::domain::{
employee_logged_in_event::*,
employee_registered_event::*, //invite_accepted_event::*,
login_otp_sent_event::*,
organization_exited_event::*,
phone_number_changed_event::*,
phone_number_verified_event::*,
resend_login_otp_event::*,
store_added_event::*,
store_updated_event::*,
verification_otp_resent_event::*,
verification_otp_sent_event::*,
employee_logged_in_event::*, employee_registered_event::*,
employee_removed_from_role::EmployeeRemovedFromRoleEvent, login_otp_sent_event::*,
organization_exited_event::*, owner_added_employee_to_store_event::*,
owner_removed_employee_from_store_event::*, phone_number_changed_event::*,
phone_number_verified_event::*, resend_login_otp_event::*, role_added_event::*,
role_set_to_employee_event::RoleSetToEmployeeEvent, store_added_event::*,
store_updated_event::*, verification_otp_resent_event::*, verification_otp_sent_event::*,
};
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
@ -35,6 +31,11 @@ pub enum IdentityEvent {
UserVerified,
VerificationEmailResent,
UserPromotedToAdmin(UserPromotedToAdminEvent),
OwnerAddedEmployeeToStore(OwnerAddedEmployeeToStoreEvent),
OwnerRemovedEmployeeFromStore(OwnerRemovedEmployeeFromStoreEvent),
RoleAdded(RoleAddedEvent),
RoleSetToEmployee(RoleSetToEmployeeEvent),
EmployeeRemovedFromRole(EmployeeRemovedFromRoleEvent),
// employee
EmployeeRegistered(EmployeeRegisteredEvent),
@ -70,6 +71,13 @@ impl DomainEvent for IdentityEvent {
IdentityEvent::UserVerified => "IdentityUserIsVerified",
IdentityEvent::UserPromotedToAdmin { .. } => "IdentityUserPromotedToAdmin",
IdentityEvent::VerificationEmailResent => "IdentityVerficationEmailResent",
IdentityEvent::OwnerAddedEmployeeToStore { .. } => "IdentityOwnerAddedEmployeeToStore",
IdentityEvent::OwnerRemovedEmployeeFromStore { .. } => {
"IdentityOwnerRemovedEmployeeFromStore"
}
IdentityEvent::RoleAdded { .. } => "IdentityRoleAddedEvent",
IdentityEvent::RoleSetToEmployee { .. } => "IdentityRoleSetToEmployee",
IdentityEvent::EmployeeRemovedFromRole { .. } => "IdentityEmployeeRemovedFromRole",
// employee
IdentityEvent::EmployeeRegistered { .. } => "EmployeeRegistered",
IdentityEvent::EmployeeLoggedIn { .. } => "EmployeeLoggedIn",

View file

@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::sync::Arc;
use derive_builder::Builder;
use mockall::predicate::*;
use mockall::*;
use super::errors::*;
use crate::identity::{
application::port::output::db::{get_roles_for_store::*, store_id_exists::*},
domain::{role_aggregate::*, store_aggregate::*},
};
#[automock]
#[async_trait::async_trait]
pub trait GetRolesForStoreUseCase: Send + Sync {
async fn get_roles_for_store(&self, store: &Store) -> IdentityResult<Vec<Role>>;
}
pub type GetRolesForStoreServiceObj = Arc<dyn GetRolesForStoreUseCase>;
#[derive(Clone, Builder)]
pub struct GetRolesForStoreService {
db_store_id_exists: StoreIDExistsDBPortObj,
db_get_roles_for_store: GetRolesForStoreDBPortObj,
}
#[async_trait::async_trait]
impl GetRolesForStoreUseCase for GetRolesForStoreService {
async fn get_roles_for_store(&self, store: &Store) -> IdentityResult<Vec<Role>> {
if !self
.db_store_id_exists
.store_id_exists(store.store_id())
.await?
{
return Err(IdentityError::StoreNotFound);
}
Ok(self
.db_get_roles_for_store
.get_roles_for_store(*store.store_id())
.await?)
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::tests::bdd::*;
use crate::utils::uuid::tests::*;
pub fn mock_get_roles_for_store_service(
times: Option<usize>,
store: &Store,
) -> GetRolesForStoreServiceObj {
let mut m = MockGetRolesForStoreUseCase::new();
let res = vec![Role::default()];
if let Some(times) = times {
m.expect_get_roles_for_store()
.times(times)
.return_const(Ok(res));
} else {
m.expect_get_roles_for_store().return_const(Ok(res));
}
Arc::new(m)
}
#[actix_rt::test]
// with mock that returns Vec<Role>.len() = 1
async fn test_service() {
let store = Store::default();
let s = GetRolesForStoreServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_get_roles_for_store(mock_get_roles_for_store_db_port(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
let roles = s.get_roles_for_store(&store).await.unwrap();
assert_eq!(roles.len(), 1);
assert_eq!(roles.first().unwrap().to_owned(), Role::get_role());
}
#[actix_rt::test]
// with mock that returns empty Vec<Role>
async fn test_service_empty_res() {
let store = Store::default();
{
let s = GetRolesForStoreServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_get_roles_for_store(mock_get_roles_for_store_db_port_empty(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
let roles = s.get_roles_for_store(&store).await.unwrap();
assert!(roles.is_empty());
}
}
#[actix_rt::test]
async fn test_service_store_id_no_exist() {
let store = Store::default();
let s = GetRolesForStoreServiceBuilder::default()
.db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_get_roles_for_store(mock_get_roles_for_store_db_port_empty(IS_NEVER_CALLED))
.build()
.unwrap();
assert_eq!(
s.get_roles_for_store(&store).await.err(),
Some(IdentityError::StoreNotFound)
);
}
}

View file

@ -27,6 +27,12 @@ impl LoginCommand {
mod tests {
use super::*;
impl LoginCommand {
pub fn get_cmd() -> Self {
LoginCommand { success: true }
}
}
#[test]
fn test_cmd() {
let config = argon2_creds::Config::default();

View file

@ -15,3 +15,17 @@ impl LoginEvent {
Self { success }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::application::services::login::command::*;
impl LoginEvent {
pub fn get_event(cmd: &LoginCommand) -> Self {
Self {
success: *cmd.success(),
}
}
}
}

View file

@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
pub mod command;
pub mod events;
@ -8,6 +10,7 @@ pub mod service;
use super::errors::*;
#[automock]
#[async_trait::async_trait]
pub trait LoginUseCase: Send + Sync {
async fn login(

View file

@ -21,6 +21,21 @@ impl LoginUseCase for LoginService {
mod tests {
use super::*;
impl LoginService {
pub fn mock_service(times: Option<usize>, cmd: command::LoginCommand) -> LoginServiceObj {
let mut m = MockLoginUseCase::default();
let res = events::LoginEvent::get_event(&cmd);
if let Some(times) = times {
m.expect_login().times(times).return_const(res);
} else {
m.expect_login().return_const(res);
}
std::sync::Arc::new(m)
}
}
#[actix_rt::test]
async fn test_service() {
let config = argon2_creds::Config::default();
@ -31,9 +46,7 @@ mod tests {
let s = LoginService;
{
let cmd =
command::LoginCommand::new(username.into(), password.into(), &hashed_password)
.unwrap();
let cmd = command::LoginCommand::get_cmd();
let res = s.login(cmd.clone()).await;
assert_eq!(res.success(), cmd.success());
}

View file

@ -33,4 +33,12 @@ mod tests {
assert_eq!(cmd.user_id(), &user_id);
assert_eq!(cmd.secret(), secret);
}
impl MarkUserVerifiedCommand {
pub fn get_cmd() -> Self {
let user_id = UUID;
let secret = "asdfasdf";
MarkUserVerifiedCommand::new(user_id, secret.into()).unwrap()
}
}
}

View file

@ -1,12 +1,15 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
pub mod command;
pub mod service;
use super::errors::*;
#[automock]
#[async_trait::async_trait]
pub trait MarkUserVerifiedUseCase: Send + Sync {
async fn mark_user_verified(&self, cmd: command::MarkUserVerifiedCommand)

View file

@ -52,9 +52,7 @@ mod tests {
#[actix_rt::test]
async fn test_service() {
let user_id = UUID;
let secret = "password";
let cmd = command::MarkUserVerifiedCommand::new(user_id, secret.into()).unwrap();
let cmd = command::MarkUserVerifiedCommand::get_cmd();
// happy case
{
@ -91,4 +89,24 @@ mod tests {
);
}
}
impl MarkUserVerifiedService {
pub fn mock_service(
times: Option<usize>,
cmd: command::MarkUserVerifiedCommand,
) -> MarkUserVerifiedServiceObj {
let mut m = MockMarkUserVerifiedUseCase::default();
let res = ();
if let Some(times) = times {
m.expect_mark_user_verified()
.times(times)
.return_const(Ok(res));
} else {
m.expect_mark_user_verified().return_const(Ok(res));
}
std::sync::Arc::new(m)
}
}
}

View file

@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};
pub mod delete_user;
//pub mod employee_accept_invite_service;
pub mod add_role_to_store_service;
pub mod employee_exit_organization_service;
pub mod employee_login_service;
pub mod employee_register_service;
@ -21,18 +22,24 @@ pub mod errors;
pub mod events;
pub mod login;
pub mod mark_user_verified;
pub mod owner_manage_store_employee_service;
pub mod register_user;
pub mod resend_verification_email;
pub mod set_user_admin;
pub mod update_email;
pub mod update_password;
//pub mod owner_set_role_to_employee_service;
//pub mod owner_remove_employee_from_role_service;
pub mod add_store_service;
pub mod get_roles_for_store_service;
pub mod update_store_service;
use add_store_service::*;
use delete_user::{service::*, *};
//use employee_accept_invite_service::*;
use add_role_to_store_service::*;
use add_role_to_store_service::*;
use employee_exit_organization_service::*;
use employee_login_service::*;
use employee_register_service::*;
@ -52,14 +59,19 @@ use errors::*;
use events::*;
use crate::identity::domain::{
add_role_command::*,
// accept_invite_command::*,
add_store_command::*,
change_phone_number_command::*,
employee_login_command::*,
employee_register_command::*,
exit_organization_command::*,
owner_add_employee_to_store_command::*,
owner_remove_employee_from_store_command::*,
remove_employee_from_role_command::*,
resend_login_otp_command::*,
resend_verification_otp_command::*,
set_role_to_employee_command::*,
update_store_command::*,
verify_phone_number_command::*,
};
@ -69,8 +81,10 @@ use crate::utils::{
uuid::*,
};
use delete_user::command::*;
use get_roles_for_store_service::*;
use login::command::*;
use mark_user_verified::command::*;
use owner_manage_store_employee_service::*;
use register_user::command::*;
use resend_verification_email::command::*;
use set_user_admin::command::*;
@ -82,7 +96,8 @@ use crate::identity::application::port::output::{
create_login_otp::*, create_verification_otp::*, create_verification_secret::*,
delete_login_otp::*, delete_verification_otp::*, delete_verification_secret::*,
email_exists::*, emp_id_exists::*, get_emp_id_from_phone_number::*, get_login_otp::*,
get_verification_otp::*, get_verification_secret::*, phone_exists::*, store_id_exists::*,
get_roles_for_store::*, get_verification_otp::*, get_verification_secret::*,
phone_exists::*, role_id_exists::*, role_name_exists_for_store::*, store_id_exists::*,
store_name_exists::*, user_id_exists::*, verification_secret_exists::*,
},
mailer::account_validation_link::*,
@ -99,6 +114,12 @@ pub enum IdentityCommand {
MarkUserVerified(MarkUserVerifiedCommand),
SetAdmin(SetAdminCommand),
ResendVerificationEmail(ResendVerificationEmailCommand),
OwnerAddEmployeeToStore(OwnerAddEmployeeToStoreCommand),
OwnerRemoveEmployeeFromStore(OwnerRemoveEmployeeFromStoreCommand),
AddRole(AddRoleCommand),
SetRoleToEmployee(SetRoleToEmployeeCommand),
RemoveEmployeeFromRole(RemoveEmployeeFromRoleCommand),
// employee
EmployeeRegister(EmployeeRegisterCommand),
EmployeeInitLogin(EmployeeInitLoginCommand),
@ -124,6 +145,9 @@ pub trait IdentityServicesInterface: Send + Sync {
fn set_user_admin(&self) -> SetUserAdminServiceObj;
fn update_email(&self) -> UpdateEmailServiceObj;
fn update_password(&self) -> UpdatePasswordServiceObj;
fn owner_manage_employee(&self) -> OwnerManageStoreEmployeesServiceObj;
fn add_role_to_store(&self) -> AddRoleToStoreServiceObj;
fn get_roles_for_store(&self) -> GetRolesForStoreServiceObj;
// employee
// fn employee_accept_invite_service(&self) -> EmployeeAcceptInviteServiceObj;
@ -151,6 +175,9 @@ pub struct IdentityServices {
set_user_admin: SetUserAdminServiceObj,
update_email: UpdateEmailServiceObj,
update_password: UpdatePasswordServiceObj,
owner_manage_store_employee: OwnerManageStoreEmployeesServiceObj,
add_role_to_store: AddRoleToStoreServiceObj,
get_roles_for_store: GetRolesForStoreServiceObj,
// employee_accept_invite_service: EmployeeAcceptInviteServiceObj,
employee_exit_organization_service: EmployeeExitOrganizationServiceObj,
@ -190,6 +217,17 @@ impl IdentityServicesInterface for IdentityServices {
self.update_password.clone()
}
fn owner_manage_employee(&self) -> OwnerManageStoreEmployeesServiceObj {
self.owner_manage_store_employee.clone()
}
fn add_role_to_store(&self) -> AddRoleToStoreServiceObj {
self.add_role_to_store.clone()
}
fn get_roles_for_store(&self) -> GetRolesForStoreServiceObj {
self.get_roles_for_store.clone()
}
// employee
// fn employee_accept_invite_service(&self) -> EmployeeAcceptInviteServiceObj {
// self.employee_accept_invite_service.clone()
@ -240,6 +278,9 @@ impl IdentityServices {
out_db_store_name_exists: StoreNameExistsDBPortObj,
out_db_user_id_exists: UserIDExistsOutDBPortObj,
out_db_verification_secret_exists: VerificationSecretExistsOutDBPortObj,
out_db_role_id_exists: RoleIDExistsDBPortObj,
out_db_role_name_exists_for_store: RoleNameExistsForStoreDBPortObj,
out_db_get_roles_for_store: GetRolesForStoreDBPortObj,
out_mailer_account_validating_link: AccountValidationLinkOutMailerPortObj,
@ -266,7 +307,6 @@ impl IdentityServices {
.db_user_id_exists_adapter(out_db_user_id_exists.clone())
.db_create_verification_secret_adapter(out_db_create_verification_secret.clone())
.mailer_account_validation_link_adapter(out_mailer_account_validating_link.clone())
.get_uuid(get_uuid.clone())
.random_string_adapter(random_string.clone())
.build()
.unwrap(),
@ -295,6 +335,33 @@ impl IdentityServices {
let update_password: UpdatePasswordServiceObj = Arc::new(UpdatePasswordService);
let owner_manage_store_employee: OwnerManageStoreEmployeesServiceObj = Arc::new(
OwnerManageStoreEmployeesServiceBuilder::default()
.db_store_id_exists_adapter(out_db_store_id_exists.clone())
.db_emp_id_exists_adapter(out_db_emp_id_exists.clone())
.db_role_id_exists_adapter(out_db_role_id_exists.clone())
.db_role_name_exists_for_store_adapter(out_db_role_name_exists_for_store.clone())
.build()
.unwrap(),
);
let add_role_to_store: AddRoleToStoreServiceObj = Arc::new(
AddRoleToStoreServiceBuilder::default()
.db_store_id_exists_adapter(out_db_store_id_exists.clone())
.db_role_id_exists_adapter(out_db_role_id_exists.clone())
.db_role_name_exists_for_store_adapter(out_db_role_name_exists_for_store.clone())
.build()
.unwrap(),
);
let get_roles_for_store: GetRolesForStoreServiceObj = Arc::new(
GetRolesForStoreServiceBuilder::default()
.db_store_id_exists(out_db_store_id_exists.clone())
.db_get_roles_for_store(out_db_get_roles_for_store.clone())
.build()
.unwrap(),
);
// let employee_accept_invite_service: EmployeeAcceptInviteServiceObj = Arc::new(
// EmployeeAcceptInviteServiceBuilder::default()
// .db_get_invite_adapter(unimplemented!())
@ -397,6 +464,9 @@ impl IdentityServices {
set_user_admin,
update_email,
update_password,
owner_manage_store_employee,
add_role_to_store,
get_roles_for_store,
// employee_accept_invite_service,
employee_exit_organization_service,
@ -417,7 +487,8 @@ mod tests {
use random_number::tests::mock_generate_random_number;
use crate::{
tests::bdd::IS_NEVER_CALLED,
identity::adapters::output::db::postgres::DBOutPostgresAdapter,
tests::bdd::{IGNORE_CALL_COUNT, IS_NEVER_CALLED},
utils::{random_string::tests::mock_generate_random_string, uuid::tests::mock_get_uuid},
};
@ -443,6 +514,9 @@ mod tests {
mock_store_name_exists_db_port_true(IS_NEVER_CALLED),
mock_user_id_exists_db_port(IS_NEVER_CALLED, false),
mock_verification_secret_exists_db_port(IS_NEVER_CALLED, false),
mock_role_id_exists_db_port_true(IS_NEVER_CALLED),
mock_role_name_exists_for_store_db_port_true(IS_NEVER_CALLED),
mock_get_roles_for_store_db_port(IS_NEVER_CALLED),
mock_account_validation_link_mailer_port(IS_NEVER_CALLED),
mock_account_validation_otp_phone_port(IS_NEVER_CALLED),
mock_account_login_otp_phone_port(IS_NEVER_CALLED),

View file

@ -0,0 +1,654 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_builder::Builder;
use mockall::predicate::*;
use mockall::*;
use crate::identity::application::port::output::db::{
emp_id_exists::*, role_id_exists::*, role_name_exists_for_store::*, store_id_exists::*,
};
use crate::identity::domain::{
employee_removed_from_role::*, owner_add_employee_to_store_command::*,
owner_added_employee_to_store_event::*, owner_remove_employee_from_store_command::*,
owner_removed_employee_from_store_event::*, remove_employee_from_role_command::*,
role_set_to_employee_event::*, set_role_to_employee_command::*,
};
use super::errors::*;
#[automock]
#[async_trait::async_trait]
pub trait OwnerManageStoreEmployeesUseCase: Send + Sync {
async fn add_employee_to_store(
&self,
cmd: OwnerAddEmployeeToStoreCommand,
) -> IdentityResult<OwnerAddedEmployeeToStoreEvent>;
async fn remove_employee_from_store(
&self,
cmd: OwnerRemoveEmployeeFromStoreCommand,
) -> IdentityResult<OwnerRemovedEmployeeFromStoreEvent>;
async fn set_role_to_employee(
&self,
cmd: SetRoleToEmployeeCommand,
) -> IdentityResult<RoleSetToEmployeeEvent>;
async fn remove_employee_from_role(
&self,
cmd: RemoveEmployeeFromRoleCommand,
) -> IdentityResult<EmployeeRemovedFromRoleEvent>;
}
pub type OwnerManageStoreEmployeesServiceObj = std::sync::Arc<dyn OwnerManageStoreEmployeesUseCase>;
#[derive(Clone, Builder)]
pub struct OwnerManageStoreEmployeesService {
db_emp_id_exists_adapter: EmpIDExistsOutDBPortObj,
db_store_id_exists_adapter: StoreIDExistsDBPortObj,
db_role_id_exists_adapter: RoleIDExistsDBPortObj,
db_role_name_exists_for_store_adapter: RoleNameExistsForStoreDBPortObj,
}
#[async_trait::async_trait]
impl OwnerManageStoreEmployeesUseCase for OwnerManageStoreEmployeesService {
async fn remove_employee_from_store(
&self,
cmd: OwnerRemoveEmployeeFromStoreCommand,
) -> IdentityResult<OwnerRemovedEmployeeFromStoreEvent> {
if !self
.db_store_id_exists_adapter
.store_id_exists(cmd.store_id())
.await?
{
return Err(IdentityError::StoreNotFound);
}
if !self
.db_emp_id_exists_adapter
.emp_id_exists(cmd.emp_id())
.await?
{
return Err(IdentityError::EmployeeNotFound);
}
Ok(OwnerRemovedEmployeeFromStoreEventBuilder::default()
.emp_id(*cmd.emp_id())
.store_id(*cmd.store_id())
.added_by(*cmd.adding_by())
.build()
.unwrap())
}
async fn add_employee_to_store(
&self,
cmd: OwnerAddEmployeeToStoreCommand,
) -> IdentityResult<OwnerAddedEmployeeToStoreEvent> {
if !self
.db_store_id_exists_adapter
.store_id_exists(cmd.store_id())
.await?
{
return Err(IdentityError::StoreNotFound);
}
if !self
.db_emp_id_exists_adapter
.emp_id_exists(cmd.emp_id())
.await?
{
return Err(IdentityError::EmployeeNotFound);
}
Ok(OwnerAddedEmployeeToStoreEventBuilder::default()
.emp_id(*cmd.emp_id())
.store_id(*cmd.store_id())
.added_by(*cmd.adding_by())
.build()
.unwrap())
}
async fn set_role_to_employee(
&self,
cmd: SetRoleToEmployeeCommand,
) -> IdentityResult<RoleSetToEmployeeEvent> {
if !self
.db_store_id_exists_adapter
.store_id_exists(cmd.store_id())
.await?
{
return Err(IdentityError::StoreNotFound);
}
if !self
.db_emp_id_exists_adapter
.emp_id_exists(cmd.emp_id())
.await?
{
return Err(IdentityError::EmployeeNotFound);
}
if !self
.db_role_id_exists_adapter
.role_id_exists(cmd.role().role_id())
.await?
{
return Err(IdentityError::RoleIDNotFound);
}
if !self
.db_role_name_exists_for_store_adapter
.role_name_exists_for_store(cmd.role())
.await?
{
return Err(IdentityError::RoleNotFound);
}
Ok(RoleSetToEmployeeEventBuilder::default()
.emp_id(*cmd.emp_id())
.store_id(*cmd.store_id())
.added_by(*cmd.adding_by())
.role(cmd.role().clone())
.build()
.unwrap())
}
async fn remove_employee_from_role(
&self,
cmd: RemoveEmployeeFromRoleCommand,
) -> IdentityResult<EmployeeRemovedFromRoleEvent> {
if !self
.db_store_id_exists_adapter
.store_id_exists(cmd.store_id())
.await?
{
return Err(IdentityError::StoreNotFound);
}
if !self
.db_emp_id_exists_adapter
.emp_id_exists(cmd.employee().emp_id())
.await?
{
return Err(IdentityError::EmployeeNotFound);
}
if cmd.employee().role_id() != &Some(*cmd.role().role_id()) {
return Err(IdentityError::RoleNotFound);
}
if !self
.db_role_id_exists_adapter
.role_id_exists(cmd.role().role_id())
.await?
{
return Err(IdentityError::RoleIDNotFound);
}
if !self
.db_role_name_exists_for_store_adapter
.role_name_exists_for_store(cmd.role())
.await?
{
return Err(IdentityError::RoleNotFound);
}
Ok(EmployeeRemovedFromRoleEventBuilder::default()
.emp_id(*cmd.employee().emp_id())
.store_id(*cmd.store_id())
.added_by(*cmd.adding_by())
.role(cmd.role().clone())
.build()
.unwrap())
}
}
#[cfg(test)]
mod tests {
use crate::{
identity::domain::employee_aggregate::Employee, tests::bdd::*, utils::uuid::tests::*,
};
use super::*;
impl OwnerManageStoreEmployeesService {
pub fn mock_service_add_employee_to_store(
times: Option<usize>,
cmd: OwnerAddEmployeeToStoreCommand,
) -> OwnerManageStoreEmployeesServiceObj {
let res = OwnerAddedEmployeeToStoreEvent::get_event(&cmd);
let mut m = MockOwnerManageStoreEmployeesUseCase::default();
if let Some(times) = times {
m.expect_add_employee_to_store()
.times(times)
.returning(move |_| Ok(res.clone()));
} else {
m.expect_add_employee_to_store()
.returning(move |_| Ok(res.clone()));
}
std::sync::Arc::new(m)
}
pub fn mock_service_remove_employee_from_store(
times: Option<usize>,
cmd: OwnerRemoveEmployeeFromStoreCommand,
) -> OwnerManageStoreEmployeesServiceObj {
let res = OwnerRemovedEmployeeFromStoreEvent::get_event(&cmd);
let mut m = MockOwnerManageStoreEmployeesUseCase::default();
if let Some(times) = times {
m.expect_remove_employee_from_store()
.times(times)
.returning(move |_| Ok(res.clone()));
} else {
m.expect_remove_employee_from_store()
.returning(move |_| Ok(res.clone()));
}
std::sync::Arc::new(m)
}
pub fn mock_service_remove_employee_from_role(
times: Option<usize>,
cmd: RemoveEmployeeFromRoleCommand,
) -> OwnerManageStoreEmployeesServiceObj {
let res = EmployeeRemovedFromRoleEvent::get_event(&cmd);
let mut m = MockOwnerManageStoreEmployeesUseCase::default();
if let Some(times) = times {
m.expect_remove_employee_from_role()
.times(times)
.returning(move |_| Ok(res.clone()));
} else {
m.expect_remove_employee_from_role()
.returning(move |_| Ok(res.clone()));
}
std::sync::Arc::new(m)
}
pub fn mock_service_set_role_to_employee(
times: Option<usize>,
cmd: SetRoleToEmployeeCommand,
) -> OwnerManageStoreEmployeesServiceObj {
let res = RoleSetToEmployeeEvent::get_event(&cmd);
let mut m = MockOwnerManageStoreEmployeesUseCase::default();
if let Some(times) = times {
m.expect_set_role_to_employee()
.times(times)
.returning(move |_| Ok(res.clone()));
} else {
m.expect_set_role_to_employee()
.returning(move |_| Ok(res.clone()));
}
std::sync::Arc::new(m)
}
}
#[actix_rt::test]
async fn test_service_add_employee() {
let s = OwnerManageStoreEmployeesServiceBuilder::default()
.db_emp_id_exists_adapter(mock_emp_id_exists_db_port(IS_CALLED_ONLY_ONCE, true))
.db_store_id_exists_adapter(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_true(IS_NEVER_CALLED))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_true(
IS_NEVER_CALLED,
))
.build()
.unwrap();
{
let cmd = OwnerAddEmployeeToStoreCommand::get_cmd();
let res = s.add_employee_to_store(cmd.clone()).await.unwrap();
assert_eq!(*res.emp_id(), *cmd.emp_id());
assert_eq!(*res.added_by(), *cmd.adding_by());
assert_eq!(*res.store_id(), *cmd.store_id());
}
}
#[actix_rt::test]
async fn test_service_add_employee_emp_no_exist() {
let s = OwnerManageStoreEmployeesServiceBuilder::default()
.db_emp_id_exists_adapter(mock_emp_id_exists_db_port(IS_CALLED_ONLY_ONCE, false))
.db_store_id_exists_adapter(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_false(IS_NEVER_CALLED))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_true(
IS_NEVER_CALLED,
))
.build()
.unwrap();
{
let cmd = OwnerAddEmployeeToStoreCommand::get_cmd();
assert_eq!(
s.add_employee_to_store(cmd.clone()).await.err(),
Some(IdentityError::EmployeeNotFound)
);
}
}
#[actix_rt::test]
async fn test_service_add_employee_store_no_exist() {
let s = OwnerManageStoreEmployeesServiceBuilder::default()
.db_emp_id_exists_adapter(mock_emp_id_exists_db_port(IS_NEVER_CALLED, false))
.db_store_id_exists_adapter(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_false(IS_NEVER_CALLED))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_true(
IS_NEVER_CALLED,
))
.build()
.unwrap();
{
let cmd = OwnerAddEmployeeToStoreCommand::get_cmd();
assert_eq!(
s.add_employee_to_store(cmd.clone()).await.err(),
Some(IdentityError::StoreNotFound)
);
}
}
// remove employee
#[actix_rt::test]
async fn test_service_remove_employee() {
let s = OwnerManageStoreEmployeesServiceBuilder::default()
.db_emp_id_exists_adapter(mock_emp_id_exists_db_port(IS_CALLED_ONLY_ONCE, true))
.db_store_id_exists_adapter(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_false(IS_NEVER_CALLED))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_true(
IS_NEVER_CALLED,
))
.build()
.unwrap();
{
let cmd = OwnerRemoveEmployeeFromStoreCommand::get_cmd();
let res = s.remove_employee_from_store(cmd.clone()).await.unwrap();
assert_eq!(*res.emp_id(), *cmd.emp_id());
assert_eq!(*res.added_by(), *cmd.adding_by());
assert_eq!(*res.store_id(), *cmd.store_id());
}
}
#[actix_rt::test]
async fn test_service_remove_employee_emp_no_exist() {
let s = OwnerManageStoreEmployeesServiceBuilder::default()
.db_emp_id_exists_adapter(mock_emp_id_exists_db_port(IS_CALLED_ONLY_ONCE, false))
.db_store_id_exists_adapter(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_false(IS_NEVER_CALLED))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_true(
IS_NEVER_CALLED,
))
.build()
.unwrap();
{
let cmd = OwnerRemoveEmployeeFromStoreCommand::get_cmd();
assert_eq!(
s.remove_employee_from_store(cmd.clone()).await.err(),
Some(IdentityError::EmployeeNotFound)
);
}
}
#[actix_rt::test]
async fn test_service_remove_employee_store_no_exist() {
let s = OwnerManageStoreEmployeesServiceBuilder::default()
.db_emp_id_exists_adapter(mock_emp_id_exists_db_port(IS_NEVER_CALLED, true))
.db_store_id_exists_adapter(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_false(IS_NEVER_CALLED))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_true(
IS_NEVER_CALLED,
))
.build()
.unwrap();
{
let cmd = OwnerRemoveEmployeeFromStoreCommand::get_cmd();
assert_eq!(
s.remove_employee_from_store(cmd.clone()).await.err(),
Some(IdentityError::StoreNotFound)
);
}
}
// set role to employee
#[actix_rt::test]
async fn test_service_set_role_to_employee() {
let s = OwnerManageStoreEmployeesServiceBuilder::default()
.db_emp_id_exists_adapter(mock_emp_id_exists_db_port(IS_CALLED_ONLY_ONCE, true))
.db_store_id_exists_adapter(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_true(
IS_CALLED_ONLY_ONCE,
))
.build()
.unwrap();
{
let cmd = SetRoleToEmployeeCommand::get_cmd();
let res = s.set_role_to_employee(cmd.clone()).await.unwrap();
assert_eq!(cmd.emp_id(), res.emp_id());
assert_eq!(cmd.adding_by(), res.added_by());
assert_eq!(cmd.store_id(), res.store_id());
assert_eq!(cmd.role(), res.role());
}
}
#[actix_rt::test]
async fn test_service_set_role_to_employee_store_no_exist() {
let s = OwnerManageStoreEmployeesServiceBuilder::default()
.db_emp_id_exists_adapter(mock_emp_id_exists_db_port(IS_NEVER_CALLED, true))
.db_store_id_exists_adapter(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_true(IS_NEVER_CALLED))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_true(
IS_NEVER_CALLED,
))
.build()
.unwrap();
{
let cmd = SetRoleToEmployeeCommand::get_cmd();
assert_eq!(
s.set_role_to_employee(cmd.clone()).await.err(),
Some(IdentityError::StoreNotFound)
);
}
}
#[actix_rt::test]
async fn test_service_set_role_to_employee_role_id_no_exist() {
let s = OwnerManageStoreEmployeesServiceBuilder::default()
.db_emp_id_exists_adapter(mock_emp_id_exists_db_port(IS_CALLED_ONLY_ONCE, true))
.db_store_id_exists_adapter(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_true(
IS_NEVER_CALLED,
))
.build()
.unwrap();
{
let cmd = SetRoleToEmployeeCommand::get_cmd();
assert_eq!(
s.set_role_to_employee(cmd.clone()).await.err(),
Some(IdentityError::RoleIDNotFound)
);
}
}
#[actix_rt::test]
async fn test_service_set_role_to_employee_role_name_no_exist_for_store() {
let s = OwnerManageStoreEmployeesServiceBuilder::default()
.db_emp_id_exists_adapter(mock_emp_id_exists_db_port(IS_CALLED_ONLY_ONCE, true))
.db_store_id_exists_adapter(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_false(
IS_CALLED_ONLY_ONCE,
))
.build()
.unwrap();
{
let cmd = SetRoleToEmployeeCommand::get_cmd();
assert_eq!(
s.set_role_to_employee(cmd.clone()).await.err(),
Some(IdentityError::RoleNotFound)
);
}
}
// remove employee from role
#[actix_rt::test]
async fn test_service_remove_employee_from_role() {
let s = OwnerManageStoreEmployeesServiceBuilder::default()
.db_emp_id_exists_adapter(mock_emp_id_exists_db_port(IS_CALLED_ONLY_ONCE, true))
.db_store_id_exists_adapter(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_true(
IS_CALLED_ONLY_ONCE,
))
.build()
.unwrap();
{
let cmd = RemoveEmployeeFromRoleCommand::get_cmd();
let res = s.remove_employee_from_role(cmd.clone()).await.unwrap();
assert_eq!(cmd.employee().emp_id(), res.emp_id());
assert_eq!(cmd.adding_by(), res.added_by());
assert_eq!(cmd.store_id(), res.store_id());
assert_eq!(cmd.role(), res.role());
}
}
#[actix_rt::test]
async fn test_service_remove_employee_from_role_no_store() {
let s = OwnerManageStoreEmployeesServiceBuilder::default()
.db_emp_id_exists_adapter(mock_emp_id_exists_db_port(IS_NEVER_CALLED, true))
.db_store_id_exists_adapter(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_true(IS_NEVER_CALLED))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_true(
IS_NEVER_CALLED,
))
.build()
.unwrap();
{
let cmd = RemoveEmployeeFromRoleCommand::get_cmd();
assert_eq!(
s.remove_employee_from_role(cmd.clone()).await.err(),
Some(IdentityError::StoreNotFound)
);
}
}
#[actix_rt::test]
async fn test_service_remove_employee_from_role_no_employee() {
let s = OwnerManageStoreEmployeesServiceBuilder::default()
.db_emp_id_exists_adapter(mock_emp_id_exists_db_port(IS_CALLED_ONLY_ONCE, false))
.db_store_id_exists_adapter(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_true(IS_NEVER_CALLED))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_true(
IS_NEVER_CALLED,
))
.build()
.unwrap();
{
let cmd = RemoveEmployeeFromRoleCommand::get_cmd();
assert_eq!(
s.remove_employee_from_role(cmd.clone()).await.err(),
Some(IdentityError::EmployeeNotFound)
);
}
}
#[actix_rt::test]
async fn test_service_remove_employee_from_role_unremovable_role() {
let s = OwnerManageStoreEmployeesServiceBuilder::default()
.db_emp_id_exists_adapter(mock_emp_id_exists_db_port(IS_CALLED_ONLY_ONCE, true))
.db_store_id_exists_adapter(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_false(IS_NEVER_CALLED))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_true(
IS_NEVER_CALLED,
))
.build()
.unwrap();
{
let cmd = RemoveEmployeeFromRoleCommand::get_cmd();
let cmd = RemoveEmployeeFromRoleCommandBuilder::default()
.role(cmd.role().clone())
.store_id(*cmd.store_id())
.adding_by(*cmd.adding_by())
.employee(Employee::default())
.build()
.unwrap();
assert_eq!(
s.remove_employee_from_role(cmd.clone()).await.err(),
Some(IdentityError::RoleNotFound)
);
}
}
#[actix_rt::test]
async fn test_service_remove_employee_from_role_no_role() {
let s = OwnerManageStoreEmployeesServiceBuilder::default()
.db_emp_id_exists_adapter(mock_emp_id_exists_db_port(IS_CALLED_ONLY_ONCE, true))
.db_store_id_exists_adapter(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_true(
IS_NEVER_CALLED,
))
.build()
.unwrap();
{
let cmd = RemoveEmployeeFromRoleCommand::get_cmd();
assert_eq!(
s.remove_employee_from_role(cmd.clone()).await.err(),
Some(IdentityError::RoleIDNotFound)
);
}
}
#[actix_rt::test]
async fn test_service_remove_employee_from_role_no_role_for_store() {
let s = OwnerManageStoreEmployeesServiceBuilder::default()
.db_emp_id_exists_adapter(mock_emp_id_exists_db_port(IS_CALLED_ONLY_ONCE, true))
.db_store_id_exists_adapter(mock_store_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_id_exists_adapter(mock_role_id_exists_db_port_true(IS_CALLED_ONLY_ONCE))
.db_role_name_exists_for_store_adapter(mock_role_name_exists_for_store_db_port_false(
IS_CALLED_ONLY_ONCE,
))
.build()
.unwrap();
{
let cmd = RemoveEmployeeFromRoleCommand::get_cmd();
assert_eq!(
s.remove_employee_from_role(cmd.clone()).await.err(),
Some(IdentityError::RoleNotFound)
);
}
}
}

View file

@ -6,6 +6,7 @@ use super::*;
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(
Clone, Debug, Serialize, Deserialize, Builder, Eq, PartialEq, Ord, PartialOrd, Getters,
@ -16,6 +17,7 @@ pub struct UnvalidatedRegisterUserCommand {
email: String,
password: String,
confirm_password: String,
user_id: Uuid,
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
@ -24,6 +26,7 @@ pub struct RegisterUserCommand {
last_name: String,
email: String,
hashed_password: String,
user_id: Uuid,
}
impl UnvalidatedRegisterUserCommand {
@ -43,13 +46,38 @@ impl UnvalidatedRegisterUserCommand {
last_name: self.last_name,
email: self.email,
hashed_password,
user_id: self.user_id,
})
}
}
#[cfg(test)]
mod tests {
pub mod tests {
use super::*;
use crate::utils::uuid::tests::UUID;
pub const PASSWORD: &str = "sadfasdfasdf";
impl RegisterUserCommand {
pub fn get_command() -> Self {
let config = argon2_creds::Config::default();
let first_name = "John";
let last_name = "Doe";
let email = "john@example.com";
UnvalidatedRegisterUserCommandBuilder::default()
.user_id(UUID)
.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()
}
}
#[test]
fn test_cmd() {
@ -61,6 +89,7 @@ mod tests {
let wrong_password = "sadfasdfasdf--wrong";
UnvalidatedRegisterUserCommandBuilder::default()
.user_id(UUID)
.first_name(first_name.into())
.last_name(last_name.into())
.email(email.into())
@ -73,6 +102,7 @@ mod tests {
assert_eq!(
UnvalidatedRegisterUserCommandBuilder::default()
.user_id(UUID)
.first_name(first_name.into())
.last_name(last_name.into())
.email(first_name.into())

View file

@ -20,3 +20,25 @@ pub struct UserRegisteredEvent {
is_admin: bool,
email_verified: bool,
}
mod test {
use super::*;
use crate::{
identity::application::services::register_user::command::*, utils::uuid::tests::*,
};
impl UserRegisteredEvent {
pub fn get_event(cmd: &RegisterUserCommand) -> Self {
Self {
first_name: cmd.first_name().clone(),
last_name: cmd.last_name().clone(),
user_id: UUID,
email: cmd.email().clone(),
hashed_password: cmd.hashed_password().clone(),
is_verified: false,
is_admin: false,
email_verified: false,
}
}
}
}

View file

@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
pub mod command;
pub mod events;
@ -8,6 +10,7 @@ pub mod service;
use super::errors::*;
#[automock]
#[async_trait::async_trait]
pub trait RegisterUserUseCase: Send + Sync {
async fn register_user(

View file

@ -15,13 +15,12 @@ use crate::utils::{random_string::*, uuid::*};
pub const SECRET_LEN: usize = 20;
pub const REGISTRATION_SECRET_PURPOSE: &str = "account_validation";
#[derive(Builder)]
#[derive(Builder, Clone)]
pub struct RegisterUserService {
db_email_exists_adapter: EmailExistsOutDBPortObj,
db_user_id_exists_adapter: UserIDExistsOutDBPortObj,
db_create_verification_secret_adapter: CreateVerificationSecretOutDBPortObj,
mailer_account_validation_link_adapter: AccountValidationLinkOutMailerPortObj,
get_uuid: GetUUIDInterfaceObj,
random_string_adapter: GenerateRandomStringInterfaceObj,
}
@ -40,18 +39,13 @@ impl RegisterUserUseCase for RegisterUserService {
return Err(IdentityError::DuplicateEmail);
}
let mut user_id = self.get_uuid.get_uuid();
loop {
if self
.db_user_id_exists_adapter
.user_id_exists(&user_id)
.await
.unwrap()
{
user_id = self.get_uuid.get_uuid();
} else {
break;
}
if self
.db_user_id_exists_adapter
.user_id_exists(cmd.user_id())
.await
.unwrap()
{
return Err(IdentityError::DuplicateUserID);
}
let secret = self.random_string_adapter.get_random(SECRET_LEN);
@ -60,7 +54,7 @@ impl RegisterUserUseCase for RegisterUserService {
.create_verification_secret(
CreateSecretMsgBuilder::default()
.secret(secret.clone())
.user_id(user_id)
.user_id(*cmd.user_id())
.build()
.unwrap(),
)
@ -75,7 +69,7 @@ impl RegisterUserUseCase for RegisterUserService {
Ok(events::UserRegisteredEventBuilder::default()
.first_name(cmd.first_name().into())
.last_name(cmd.last_name().into())
.user_id(user_id)
.user_id(*cmd.user_id())
.email(cmd.email().into())
.hashed_password(cmd.hashed_password().into())
.is_verified(false)
@ -94,22 +88,29 @@ mod tests {
use crate::utils::random_string::tests::*;
use crate::utils::uuid::tests::*;
impl RegisterUserService {
pub fn mock_service(
times: Option<usize>,
cmd: command::RegisterUserCommand,
) -> RegisterUserServiceObj {
let res = events::UserRegisteredEvent::get_event(&cmd);
let mut m = MockRegisterUserUseCase::default();
if let Some(times) = times {
m.expect_register_user()
.times(times)
.return_const(Ok(res.clone()));
} else {
m.expect_register_user().return_const(Ok(res.clone()));
}
std::sync::Arc::new(m)
}
}
#[actix_rt::test]
async fn test_service() {
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)
.password(password.into())
.confirm_password(password.into())
.build()
.unwrap()
.validate(&config)
.unwrap();
let cmd = command::RegisterUserCommand::get_command();
let s = RegisterUserServiceBuilder::default()
.db_user_id_exists_adapter(mock_user_id_exists_db_port(
@ -130,7 +131,6 @@ mod tests {
.mailer_account_validation_link_adapter(mock_account_validation_link_mailer_port(
IS_CALLED_ONLY_ONCE,
))
.get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE))
.build()
.unwrap();
@ -140,28 +140,17 @@ mod tests {
assert_eq!(res.user_id(), &UUID);
assert_eq!(res.email(), cmd.email());
assert!(!res.is_admin());
assert!(argon2_creds::Config::verify(res.hashed_password(), password).unwrap())
assert!(
argon2_creds::Config::verify(res.hashed_password(), command::tests::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)
.password(password.into())
.confirm_password(password.into())
.build()
.unwrap()
.validate(&config)
.unwrap();
let cmd = command::RegisterUserCommand::get_command();
let s = RegisterUserServiceBuilder::default()
.db_user_id_exists_adapter(mock_user_id_exists_db_port(
IGNORE_CALL_COUNT,
IS_CALLED_ONLY_ONCE,
RETURNS_FALSE,
))
.db_create_verification_secret_adapter(mock_create_verification_secret_db_port(
@ -175,7 +164,6 @@ mod tests {
.mailer_account_validation_link_adapter(mock_account_validation_link_mailer_port(
IS_NEVER_CALLED,
))
.get_uuid(mock_get_uuid(IS_NEVER_CALLED))
.build()
.unwrap();
@ -184,4 +172,36 @@ mod tests {
Some(IdentityError::DuplicateEmail)
);
}
#[actix_rt::test]
async fn test_register_user_service_user_id_exists() {
let cmd = command::RegisterUserCommand::get_command();
let s = RegisterUserServiceBuilder::default()
.db_user_id_exists_adapter(mock_user_id_exists_db_port(
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_CALLED_ONLY_ONCE,
RETURNS_FALSE,
))
.random_string_adapter(mock_generate_random_string(
IS_NEVER_CALLED,
RETURNS_RANDOM_STRING.into(),
))
.mailer_account_validation_link_adapter(mock_account_validation_link_mailer_port(
IS_NEVER_CALLED,
))
.build()
.unwrap();
assert_eq!(
s.register_user(cmd.clone()).await.err(),
Some(IdentityError::DuplicateUserID)
);
}
}

View file

@ -53,4 +53,15 @@ mod tests {
Some(IdentityCommandError::BadEmail)
);
}
impl ResendVerificationEmailCommand {
pub fn get_cmd() -> Self {
let u = crate::identity::domain::aggregate::User::default();
Self {
user_id: *u.user_id(),
first_name: u.first_name().clone(),
email: u.email().clone(),
}
}
}
}

View file

@ -1,12 +1,15 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
pub mod command;
pub mod service;
use super::errors::*;
#[automock]
#[async_trait::async_trait]
pub trait ResendVerificationEmailUseCase: Send + Sync {
async fn resend_verification_email(

View file

@ -116,4 +116,24 @@ mod tests {
Some(IdentityError::DuplicateEmail)
);
}
impl ResendVerificationEmailService {
pub fn mock_service(
times: Option<usize>,
cmd: command::ResendVerificationEmailCommand,
) -> ResendVerificationEmailServiceObj {
let mut m = MockResendVerificationEmailUseCase::default();
let res = ();
if let Some(times) = times {
m.expect_resend_verification_email()
.times(times)
.return_const(Ok(res));
} else {
m.expect_resend_verification_email().return_const(Ok(res));
}
std::sync::Arc::new(m)
}
}
}

View file

@ -34,21 +34,7 @@ mod tests {
async fn test_cmd() {
let username = "realaravinth";
SetAdminCommand::new(
UserBuilder::default()
.first_name(username.into())
.last_name(username.into())
.email(username.into())
.hashed_password(username.into())
.is_verified(true)
.email_verified(false)
.is_admin(true)
.deleted(false)
.user_id(UUID)
.build()
.unwrap(),
)
.unwrap();
SetAdminCommand::get_cmd();
assert_eq!(
SetAdminCommand::new(
@ -69,4 +55,25 @@ mod tests {
Some(IdentityCommandError::PermissionDenied)
);
}
impl SetAdminCommand {
pub fn get_cmd() -> Self {
let u = User::default();
Self::new(
UserBuilder::default()
.first_name(u.first_name().clone())
.last_name(u.last_name().clone())
.email(u.email().clone())
.hashed_password(u.hashed_password().clone())
.is_verified(true)
.email_verified(false)
.is_admin(true)
.deleted(false)
.user_id(*u.user_id())
.build()
.unwrap(),
)
.unwrap()
}
}
}

View file

@ -17,3 +17,18 @@ impl UserPromotedToAdminEvent {
Self { promoted_by_user }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::application::services::set_user_admin::command::SetAdminCommand;
impl UserPromotedToAdminEvent {
pub fn get_event(cmd: &SetAdminCommand) -> Self {
Self {
promoted_by_user: cmd.promoted_by_user().clone(),
}
}
}
}

View file

@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
pub mod command;
pub mod events;
@ -8,6 +10,7 @@ pub mod service;
use super::errors::*;
#[automock]
#[async_trait::async_trait]
pub trait SetUserAdminUseCase: Send + Sync {
async fn set_user_admin(

View file

@ -26,26 +26,30 @@ mod tests {
#[actix_rt::test]
async fn test_service() {
let username = "realaravinth";
let s = SetUserAdminService;
let u = UserBuilder::default()
.first_name(username.into())
.last_name(username.into())
.email(username.into())
.hashed_password(username.into())
.is_verified(true)
.email_verified(false)
.is_admin(true)
.deleted(false)
.user_id(UUID)
.build()
.unwrap();
let cmd = command::SetAdminCommand::new(u).unwrap();
let cmd = command::SetAdminCommand::get_cmd();
assert_eq!(
s.set_user_admin(cmd.clone()).await.promoted_by_user(),
cmd.promoted_by_user()
)
}
impl SetUserAdminService {
pub fn mock_service(
times: Option<usize>,
cmd: command::SetAdminCommand,
) -> SetUserAdminServiceObj {
let mut m = MockSetUserAdminUseCase::default();
let res = events::UserPromotedToAdminEvent::get_event(&cmd);
if let Some(times) = times {
m.expect_set_user_admin().times(times).return_const(res);
} else {
m.expect_set_user_admin().return_const(res);
}
std::sync::Arc::new(m)
}
}
}

View file

@ -93,4 +93,27 @@ mod tests {
Some(IdentityCommandError::WrongPassword)
);
}
// command
impl UpdateEmailCommand {
pub fn get_cmd() -> Self {
let config = argon2_creds::Config::default();
let password = "adsfasdfasd";
let first_name = "john";
let user_id = UUID;
let new_email = "newemail@example.com".to_string();
let hashed_password = config.password(password).unwrap();
UpdateEmailCommand::new(
new_email.clone(),
user_id,
first_name.into(),
password.into(),
&hashed_password,
&config,
)
.unwrap()
}
}
}

View file

@ -15,3 +15,18 @@ impl EmailUpdatedEvent {
Self { new_email }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::application::services::update_email::command::UpdateEmailCommand;
impl EmailUpdatedEvent {
pub fn get_event(cmd: &UpdateEmailCommand) -> Self {
Self {
new_email: cmd.new_email().clone(),
}
}
}
}

View file

@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
pub mod command;
pub mod events;
@ -8,6 +10,7 @@ pub mod service;
use super::errors::*;
#[automock]
#[async_trait::async_trait]
pub trait UpdateEmailUseCase: Send + Sync {
async fn update_email(

View file

@ -68,23 +68,27 @@ mod tests {
use crate::tests::bdd::*;
use crate::utils::uuid::tests::UUID;
impl UpdateEmailService {
pub fn mock_service(
times: Option<usize>,
cmd: command::UpdateEmailCommand,
) -> UpdateEmailServiceObj {
let mut m = MockUpdateEmailUseCase::default();
let res = events::EmailUpdatedEvent::get_event(&cmd);
if let Some(times) = times {
m.expect_update_email().times(times).return_const(Ok(res));
} else {
m.expect_update_email().return_const(Ok(res));
}
std::sync::Arc::new(m)
}
}
#[actix_rt::test]
async fn test_service() {
let user_id = UUID;
let new_email = "john@example.com".to_string();
let password = "password";
let config = argon2_creds::Config::default();
let hashed_password = config.password(password).unwrap();
let cmd = command::UpdateEmailCommand::new(
new_email.clone(),
user_id,
"john".into(),
password.into(),
&hashed_password,
&config,
)
.unwrap();
let cmd = command::UpdateEmailCommand::get_cmd();
// happy case
{

View file

@ -37,6 +37,14 @@ impl UpdatePasswordCommand {
mod tests {
use super::*;
impl UpdatePasswordCommand {
pub fn get_cmd() -> Self {
Self {
hashed_new_passowrd: "foo".into(),
}
}
}
#[test]
fn test_cmd() {
let config = argon2_creds::Config::default();

View file

@ -15,3 +15,19 @@ impl PasswordUpdatedEvent {
Self { hashed_password }
}
}
// events
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::application::services::update_password::command::UpdatePasswordCommand;
impl PasswordUpdatedEvent {
pub fn get_event(cmd: &UpdatePasswordCommand) -> Self {
Self {
hashed_password: cmd.hashed_new_passowrd().clone(),
}
}
}
}

View file

@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use mockall::predicate::*;
use mockall::*;
pub mod command;
pub mod events;
@ -8,6 +10,7 @@ pub mod service;
use super::errors::*;
#[automock]
#[async_trait::async_trait]
pub trait UpdatePasswordUseCase: Send + Sync {
async fn update_password(

View file

@ -21,6 +21,24 @@ impl UpdatePasswordUseCase for UpdatePasswordService {
mod tests {
use super::*;
impl UpdatePasswordService {
pub fn mock_service(
times: Option<usize>,
cmd: command::UpdatePasswordCommand,
) -> UpdatePasswordServiceObj {
let mut m = MockUpdatePasswordUseCase::default();
let res = events::PasswordUpdatedEvent::get_event(&cmd);
if let Some(times) = times {
m.expect_update_password().times(times).return_const(res);
} else {
m.expect_update_password().return_const(res);
}
std::sync::Arc::new(m)
}
}
#[actix_rt::test]
async fn test_service() {
let username = "realaravinth";

View file

@ -0,0 +1,39 @@
// 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 derive_builder::Builder;
use derive_getters::Getters;
use events::IdentityEvent;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::identity::application::services::{errors::*, *};
#[derive(
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct AddRoleCommand {
name: String,
role_id: Uuid,
store_id: Uuid,
}
#[cfg(test)]
pub mod tests {
use crate::utils::uuid::tests::UUID;
use super::*;
impl AddRoleCommand {
pub fn get_cmd() -> Self {
Self {
name: "some role".into(),
role_id: UUID,
store_id: UUID,
}
}
}
}

View file

@ -134,6 +134,38 @@ impl Aggregate for User {
.await?;
Ok(vec![IdentityEvent::VerificationEmailResent])
}
IdentityCommand::OwnerAddEmployeeToStore(cmd) => {
let res = services
.owner_manage_employee()
.add_employee_to_store(cmd)
.await?;
Ok(vec![IdentityEvent::OwnerAddedEmployeeToStore(res)])
}
IdentityCommand::OwnerRemoveEmployeeFromStore(cmd) => {
let res = services
.owner_manage_employee()
.remove_employee_from_store(cmd)
.await?;
Ok(vec![IdentityEvent::OwnerRemovedEmployeeFromStore(res)])
}
IdentityCommand::AddRole(cmd) => {
let res = services.add_role_to_store().add_role_to_store(cmd).await?;
Ok(vec![IdentityEvent::RoleAdded(res)])
}
IdentityCommand::SetRoleToEmployee(cmd) => {
let res = services
.owner_manage_employee()
.set_role_to_employee(cmd)
.await?;
Ok(vec![IdentityEvent::RoleSetToEmployee(res)])
}
IdentityCommand::RemoveEmployeeFromRole(cmd) => {
let res = services
.owner_manage_employee()
.remove_employee_from_role(cmd)
.await?;
Ok(vec![IdentityEvent::EmployeeRemovedFromRole(res)])
}
_ => Ok(Vec::new()),
}
}
@ -195,4 +227,331 @@ mod tests {
assert!(!u.email_verified());
assert!(u.set_email_verified(true).deleted());
}
use std::sync::Arc;
use cqrs_es::test::TestFramework;
use super::*;
use crate::identity::{
application::services::{
add_role_to_store_service::AddRoleToStoreService,
delete_user::{command::DeleteUserCommand, service::DeleteUserService},
events::IdentityEvent,
login::{command::LoginCommand, events::LoginEvent, service::LoginService},
owner_manage_store_employee_service::*,
register_user::{
command::RegisterUserCommand, events::UserRegisteredEvent,
service::RegisterUserService,
},
IdentityCommand, MockIdentityServicesInterface,
},
domain::{
add_role_command::AddRoleCommand,
employee_removed_from_role::EmployeeRemovedFromRoleEvent,
owner_add_employee_to_store_command::*, owner_added_employee_to_store_event::*,
owner_remove_employee_from_store_command::OwnerRemoveEmployeeFromStoreCommand,
owner_removed_employee_from_store_event::OwnerRemovedEmployeeFromStoreEvent,
remove_employee_from_role_command::RemoveEmployeeFromRoleCommand,
role_added_event::RoleAddedEvent, role_set_to_employee_event::RoleSetToEmployeeEvent,
set_role_to_employee_command::SetRoleToEmployeeCommand,
},
};
use crate::tests::bdd::*;
type UserTestFramework = TestFramework<User>;
#[test]
fn test_user_aggregate_register_user() {
let cmd = RegisterUserCommand::get_command();
let expected = UserRegisteredEvent::get_event(&cmd);
let expected = IdentityEvent::UserRegistered(expected);
let mut services = MockIdentityServicesInterface::new();
services
.expect_register_user()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(RegisterUserService::mock_service(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
));
UserTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(IdentityCommand::RegisterUser(cmd))
.then_expect_events(vec![expected]);
}
#[test]
fn test_user_aggregate_delete_user() {
let cmd = DeleteUserCommand::get_cmd();
let expected = IdentityEvent::UserDeleted;
let mut services = MockIdentityServicesInterface::new();
services
.expect_delete_user()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(DeleteUserService::mock_service(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
));
UserTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(IdentityCommand::DeleteUser(cmd))
.then_expect_events(vec![expected]);
}
#[test]
fn test_user_aggregate_login_user() {
let cmd = LoginCommand::get_cmd();
let expected = IdentityEvent::Loggedin(LoginEvent::get_event(&cmd));
let mut services = MockIdentityServicesInterface::new();
services
.expect_login()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(LoginService::mock_service(IS_CALLED_ONLY_ONCE, cmd.clone()));
UserTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(IdentityCommand::Login(cmd))
.then_expect_events(vec![expected]);
}
#[test]
fn test_user_aggregate_update_password() {
use crate::identity::application::services::update_password::{
command::*, events::*, service::*, *,
};
let cmd = UpdatePasswordCommand::get_cmd();
let expected = PasswordUpdatedEvent::get_event(&cmd);
let expected = IdentityEvent::PasswordUpdated(expected);
let mut services = MockIdentityServicesInterface::new();
services
.expect_update_password()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(UpdatePasswordService::mock_service(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
));
UserTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(IdentityCommand::UpdatePassword(cmd))
.then_expect_events(vec![expected]);
}
#[test]
fn test_user_aggregate_update_email() {
use crate::identity::application::services::update_email::{
command::*, events::*, service::*, *,
};
let cmd = UpdateEmailCommand::get_cmd();
let expected = EmailUpdatedEvent::get_event(&cmd);
let expected = IdentityEvent::EmailUpdated(expected);
let mut services = MockIdentityServicesInterface::new();
services
.expect_update_email()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(UpdateEmailService::mock_service(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
));
UserTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(IdentityCommand::UpdateEmail(cmd))
.then_expect_events(vec![expected]);
}
#[test]
fn test_user_aggregate_mark_user_verified() {
use crate::identity::application::services::mark_user_verified::{
command::*, service::*, *,
};
let cmd = MarkUserVerifiedCommand::get_cmd();
let expected = IdentityEvent::UserVerified;
let mut services = MockIdentityServicesInterface::new();
services
.expect_mark_user_verified()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(MarkUserVerifiedService::mock_service(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
));
UserTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(IdentityCommand::MarkUserVerified(cmd))
.then_expect_events(vec![expected]);
}
#[test]
fn test_user_aggregate_set_admin() {
use crate::identity::application::services::set_user_admin::{
command::*, events::*, service::*, *,
};
let cmd = SetAdminCommand::get_cmd();
let expected = UserPromotedToAdminEvent::get_event(&cmd);
let expected = IdentityEvent::UserPromotedToAdmin(expected);
let mut services = MockIdentityServicesInterface::new();
services
.expect_set_user_admin()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(SetUserAdminService::mock_service(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
));
UserTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(IdentityCommand::SetAdmin(cmd))
.then_expect_events(vec![expected]);
}
#[test]
fn test_user_aggregate_resend_verification_email() {
use crate::identity::application::services::resend_verification_email::{
command::*, service::*, *,
};
let cmd = ResendVerificationEmailCommand::get_cmd();
let expected = IdentityEvent::VerificationEmailResent;
let mut services = MockIdentityServicesInterface::new();
services
.expect_resend_verification_email()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(ResendVerificationEmailService::mock_service(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
));
UserTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(IdentityCommand::ResendVerificationEmail(cmd))
.then_expect_events(vec![expected]);
}
#[test]
fn test_user_aggregate_owner_added_employee_to_store() {
let cmd = OwnerAddEmployeeToStoreCommand::get_cmd();
let expected = OwnerAddedEmployeeToStoreEvent::get_event(&cmd);
let expected = IdentityEvent::OwnerAddedEmployeeToStore(expected);
let mut services = MockIdentityServicesInterface::new();
services
.expect_owner_manage_employee()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(
OwnerManageStoreEmployeesService::mock_service_add_employee_to_store(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
),
);
UserTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(IdentityCommand::OwnerAddEmployeeToStore(cmd))
.then_expect_events(vec![expected]);
}
#[test]
fn test_user_aggregate_owner_remove_employee_to_store() {
let cmd = OwnerRemoveEmployeeFromStoreCommand::get_cmd();
let expected = OwnerRemovedEmployeeFromStoreEvent::get_event(&cmd);
let expected = IdentityEvent::OwnerRemovedEmployeeFromStore(expected);
let mut services = MockIdentityServicesInterface::new();
services
.expect_owner_manage_employee()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(
OwnerManageStoreEmployeesService::mock_service_remove_employee_from_store(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
),
);
UserTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(IdentityCommand::OwnerRemoveEmployeeFromStore(cmd))
.then_expect_events(vec![expected]);
}
#[test]
fn test_user_aggregate_owner_set_role_to_employee() {
let cmd = SetRoleToEmployeeCommand::get_cmd();
let expected = RoleSetToEmployeeEvent::get_event(&cmd);
let expected = IdentityEvent::RoleSetToEmployee(expected);
let mut services = MockIdentityServicesInterface::new();
services
.expect_owner_manage_employee()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(
OwnerManageStoreEmployeesService::mock_service_set_role_to_employee(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
),
);
UserTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(IdentityCommand::SetRoleToEmployee(cmd))
.then_expect_events(vec![expected]);
}
#[test]
fn test_user_aggregate_owner_remove_employee_from_role() {
let cmd = RemoveEmployeeFromRoleCommand::get_cmd();
let expected = EmployeeRemovedFromRoleEvent::get_event(&cmd);
let expected = IdentityEvent::EmployeeRemovedFromRole(expected);
let mut services = MockIdentityServicesInterface::new();
services
.expect_owner_manage_employee()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(
OwnerManageStoreEmployeesService::mock_service_remove_employee_from_role(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
),
);
UserTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(IdentityCommand::RemoveEmployeeFromRole(cmd))
.then_expect_events(vec![expected]);
}
#[test]
fn test_user_aggregate_owner_add_roles_to_store() {
let cmd = AddRoleCommand::get_cmd();
let expected = RoleAddedEvent::get_event(&cmd);
let expected = IdentityEvent::RoleAdded(expected);
let mut services = MockIdentityServicesInterface::new();
services
.expect_add_role_to_store()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(AddRoleToStoreService::mock_service(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
));
UserTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(IdentityCommand::AddRole(cmd))
.then_expect_events(vec![expected]);
}
}

View file

@ -26,6 +26,8 @@ pub struct Employee {
#[builder(default = "None")]
store_id: Option<Uuid>,
deleted: bool,
#[builder(default = "None")]
role_id: Option<Uuid>,
}
#[derive(
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
@ -62,6 +64,7 @@ impl Default for Employee {
first_name: "".to_string(),
last_name: "".to_string(),
phone_number: Default::default(),
role_id: None,
phone_verified: false,
deleted: false,
emp_id: Uuid::new_v4(),
@ -160,6 +163,21 @@ impl Aggregate for Employee {
IdentityEvent::PhoneNumberChanged(e) => unimplemented!(),
// IdentityEvent::InviteAccepted(e) => self.store_id = Some(*e.store_id()),
IdentityEvent::OrganizationExited(e) => self.store_id = None,
IdentityEvent::OwnerAddedEmployeeToStore(e) => {
self.store_id = Some(*e.store_id());
self.role_id = None;
}
IdentityEvent::OwnerRemovedEmployeeFromStore(e) => {
self.store_id = None;
self.role_id = None;
}
IdentityEvent::RoleAdded(e) => {
self.store_id = Some(*e.store_id());
self.role_id = Some(*e.role_id());
}
IdentityEvent::EmployeeRemovedFromRole(e) => {
self.role_id = None;
}
_ => (),
}
@ -185,8 +203,9 @@ mod tests {
employee_register_command::EmployeeRegisterCommand,
employee_registered_event::EmployeeRegisteredEvent,
exit_organization_command::ExitOrganizationCommand,
// invite_accepted_event::InviteAcceptedEvent,
organization_exited_event::OrganizationExitedEvent,
owner_add_employee_to_store_command::OwnerAddEmployeeToStoreCommand,
owner_added_employee_to_store_event::OwnerAddedEmployeeToStoreEvent,
phone_number_verified_event::PhoneNumberVerifiedEvent,
resend_login_otp_command::ResendLoginOTPCommand,
resend_login_otp_event::ResentLoginOTPEvent,

View file

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::role_aggregate::*;
#[derive(
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct EmployeeRemovedFromRoleEvent {
emp_id: Uuid,
added_by: Uuid,
store_id: Uuid,
role: Role,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
identity::domain::remove_employee_from_role_command::RemoveEmployeeFromRoleCommand,
utils::uuid::tests::UUID,
};
impl EmployeeRemovedFromRoleEvent {
pub fn get_event(cmd: &RemoveEmployeeFromRoleCommand) -> Self {
Self {
emp_id: *cmd.employee().emp_id(),
added_by: *cmd.adding_by(),
store_id: *cmd.store_id(),
role: cmd.role().clone(),
}
}
}
}

View file

@ -4,6 +4,7 @@
pub mod aggregate;
pub mod employee_aggregate;
pub mod role_aggregate;
//pub mod employee_commands;
pub mod store_aggregate;
// pub mod invite;
@ -12,11 +13,16 @@ pub mod store_aggregate;
pub mod employee_logged_in_event;
pub mod employee_registered_event;
//pub mod invite_accepted_event;
pub mod employee_removed_from_role;
pub mod login_otp_sent_event;
pub mod organization_exited_event;
pub mod owner_added_employee_to_store_event;
pub mod owner_removed_employee_from_store_event;
pub mod phone_number_changed_event;
pub mod phone_number_verified_event;
pub mod resend_login_otp_event;
pub mod role_added_event;
pub mod role_set_to_employee_event;
pub mod store_added_event;
pub mod store_updated_event;
pub mod verification_otp_resent_event;
@ -24,12 +30,17 @@ pub mod verification_otp_sent_event;
// commands
//pub mod accept_invite_command;
pub mod add_role_command;
pub mod add_store_command;
pub mod change_phone_number_command;
pub mod employee_login_command;
pub mod employee_register_command;
pub mod exit_organization_command;
pub mod owner_add_employee_to_store_command;
pub mod owner_remove_employee_from_store_command;
pub mod remove_employee_from_role_command;
pub mod resend_login_otp_command;
pub mod resend_verification_otp_command;
pub mod set_role_to_employee_command;
pub mod update_store_command;
pub mod verify_phone_number_command;

View file

@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::employee_aggregate::*;
#[derive(
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct OwnerAddEmployeeToStoreCommand {
adding_by: Uuid,
emp_id: Uuid,
store_id: Uuid,
}
#[cfg(test)]
pub mod tests {
use crate::utils::uuid::tests::UUID;
use super::*;
impl OwnerAddEmployeeToStoreCommand {
pub fn get_cmd() -> Self {
Self {
emp_id: UUID,
adding_by: UUID,
store_id: UUID,
}
}
}
}

View file

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::role_aggregate::*;
#[derive(
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct OwnerAddedEmployeeToStoreEvent {
emp_id: Uuid,
added_by: Uuid,
store_id: Uuid,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
identity::domain::owner_add_employee_to_store_command::OwnerAddEmployeeToStoreCommand,
utils::uuid::tests::UUID,
};
impl OwnerAddedEmployeeToStoreEvent {
pub fn get_event(cmd: &OwnerAddEmployeeToStoreCommand) -> Self {
Self {
emp_id: *cmd.emp_id(),
added_by: *cmd.adding_by(),
store_id: *cmd.store_id(),
}
}
}
}

View file

@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::employee_aggregate::*;
#[derive(
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct OwnerRemoveEmployeeFromStoreCommand {
adding_by: Uuid,
emp_id: Uuid,
store_id: Uuid,
}
#[cfg(test)]
pub mod tests {
use crate::utils::uuid::tests::UUID;
use super::*;
impl OwnerRemoveEmployeeFromStoreCommand {
pub fn get_cmd() -> Self {
Self {
emp_id: UUID,
adding_by: UUID,
store_id: UUID,
}
}
}
}

View file

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::employee_aggregate::*;
#[derive(
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct OwnerRemovedEmployeeFromStoreEvent {
emp_id: Uuid,
added_by: Uuid,
store_id: Uuid,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
identity::domain::owner_remove_employee_from_store_command::OwnerRemoveEmployeeFromStoreCommand,
utils::uuid::tests::UUID,
};
impl OwnerRemovedEmployeeFromStoreEvent {
pub fn get_event(cmd: &OwnerRemoveEmployeeFromStoreCommand) -> Self {
Self {
emp_id: *cmd.emp_id(),
added_by: *cmd.adding_by(),
store_id: *cmd.store_id(),
}
}
}
}

View file

@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::employee_aggregate::*;
use super::role_aggregate::Role;
#[derive(
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct RemoveEmployeeFromRoleCommand {
adding_by: Uuid,
store_id: Uuid,
role: Role,
employee: Employee,
}
#[cfg(test)]
pub mod tests {
use crate::utils::uuid::tests::UUID;
use super::*;
impl RemoveEmployeeFromRoleCommand {
pub fn get_cmd() -> Self {
let role = Role::default();
let employee = Employee::default();
let employee = EmployeeBuilder::default()
.first_name(employee.first_name().clone())
.last_name(employee.last_name().clone())
.emp_id(*employee.emp_id())
.phone_number(employee.phone_number().clone())
.phone_verified(employee.phone_verified().clone())
.store_id(Some(UUID))
.deleted(false)
.role_id(Some(*role.role_id()))
.build()
.unwrap();
Self {
adding_by: UUID,
store_id: UUID,
role,
employee,
}
}
}
}

View file

@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::role_aggregate::*;
#[derive(
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct RoleAddedEvent {
name: String,
role_id: Uuid,
store_id: Uuid,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{identity::domain::add_role_command::AddRoleCommand, utils::uuid::tests::UUID};
impl RoleAddedEvent {
pub fn get_event(cmd: &AddRoleCommand) -> Self {
Self {
name: cmd.name().clone(),
role_id: *cmd.role_id(),
store_id: *cmd.store_id(),
}
}
}
}

View file

@ -0,0 +1,129 @@
// 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 derive_builder::Builder;
use derive_getters::Getters;
use events::IdentityEvent;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::identity::application::services::{errors::*, *};
#[derive(
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct Role {
name: String,
role_id: Uuid,
store_id: Uuid,
#[builder(default = "false")]
deleted: bool,
}
impl Default for Role {
fn default() -> Self {
Role {
name: "".to_string(),
store_id: Uuid::new_v4(),
role_id: Uuid::new_v4(),
deleted: false,
}
}
}
#[async_trait]
impl Aggregate for Role {
type Command = IdentityCommand;
type Event = IdentityEvent;
type Error = IdentityError;
type Services = std::sync::Arc<dyn IdentityServicesInterface>;
// This identifier should be unique to the system.
fn aggregate_type() -> String {
"identity.role".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 {
IdentityCommand::AddRole(cmd) => Ok(vec![IdentityEvent::RoleAdded(
services.add_role_to_store().add_role_to_store(cmd).await?,
)]),
_ => Ok(Vec::new()),
}
}
fn apply(&mut self, event: Self::Event) {
match event {
IdentityEvent::RoleAdded(event) => {
self.name = event.name().clone();
self.role_id = *self.role_id();
self.store_id = *self.store_id();
self.deleted = false;
}
_ => (),
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use cqrs_es::test::TestFramework;
use crate::{
identity::domain::{
add_role_command::*,
// accept_invite_command::AcceptInviteCommand,
role_added_event::*,
},
utils::uuid::tests::UUID,
};
use add_role_to_store_service::*;
use super::*;
use crate::tests::bdd::*;
type RoleTestFramework = TestFramework<Role>;
impl Role {
pub fn get_role() -> Self {
Role {
name: "test_role_should_never_exist_in_prod".to_string(),
store_id: UUID,
role_id: UUID,
deleted: false,
}
}
}
#[test]
fn test_role_aggregate_add_role() {
let cmd = AddRoleCommand::get_cmd();
let expected = RoleAddedEvent::get_event(&cmd);
let expected = IdentityEvent::RoleAdded(expected);
let mut services = MockIdentityServicesInterface::new();
services
.expect_add_role_to_store()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(AddRoleToStoreService::mock_service(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
));
RoleTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(IdentityCommand::AddRole(cmd))
.then_expect_events(vec![expected]);
}
}

View file

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::role_aggregate::*;
#[derive(
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct RoleSetToEmployeeEvent {
emp_id: Uuid,
added_by: Uuid,
store_id: Uuid,
role: Role,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
identity::domain::set_role_to_employee_command::SetRoleToEmployeeCommand,
utils::uuid::tests::UUID,
};
impl RoleSetToEmployeeEvent {
pub fn get_event(cmd: &SetRoleToEmployeeCommand) -> Self {
Self {
emp_id: *cmd.emp_id(),
added_by: *cmd.adding_by(),
store_id: *cmd.store_id(),
role: cmd.role().clone(),
}
}
}
}

View file

@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use derive_builder::Builder;
use derive_getters::Getters;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::employee_aggregate::*;
use super::role_aggregate::Role;
#[derive(
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
)]
pub struct SetRoleToEmployeeCommand {
adding_by: Uuid,
emp_id: Uuid,
store_id: Uuid,
role: Role,
}
#[cfg(test)]
pub mod tests {
use crate::utils::uuid::tests::UUID;
use super::*;
impl SetRoleToEmployeeCommand {
pub fn get_cmd() -> Self {
Self {
emp_id: UUID,
adding_by: UUID,
store_id: UUID,
role: Role::default(),
}
}
}
}

149
utils/gen_cmd_event_inits.sh Executable file
View file

@ -0,0 +1,149 @@
#!/bin/bash
help() {
echo "Usage: gen_cmd_event_inits.sh
<domain name>
<service name>
<cmd struct name>
<event struct name>
<service struct name>
<service obj>
<service method>
<service use case>
"
}
run() {
echo "//service
use mockall::predicate::*;
use mockall::*;
#[automock]
// command
#[cfg(test)]
mod tests {
use super::*;
impl $3 {
pub fn get_cmd() -> Self {
Self {
}
}
}
}
// events
#[cfg(test)]
mod tests {
use super::*;
use crate::$1::application::services::$2::command::$3;
impl $4 {
pub fn get_event(cmd: &${3}) -> Self {
Self {
}
}
}
}
// service
impl $5 {
pub fn mock_service(
times: Option<usize>,
cmd: command::$3,
) -> $6 {
let mut m = Mock${8}::default();
let res = events::$4::get_event(&cmd);
if let Some(times) = times {
m.expect_${7}().times(times).return_const(Ok(res));
} else {
m.expect_${7}().return_const(Ok(res));
}
std::sync::Arc::new(m)
}
}
// aggregate test
#[test]
fn test_user_aggregate_${2}() {
use crate::${1}::application::services::${2}::{*, service::*, command::*, events::*};
let cmd = ${3}::get_cmd();
let expected = ${4}::get_event(&cmd);
let expected = IdentityEvent::$(echo $4 | sed 's/Event//')(expected);
let mut services = MockIdentityServicesInterface::new();
services
.expect_${2}()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.return_const(${5}::mock_service(
IS_CALLED_ONLY_ONCE,
cmd.clone(),
));
UserTestFramework::with(Arc::new(services))
.given_no_previous_events()
.when(IdentityCommand::$(echo $3 | sed 's/Command//')(cmd))
.then_expect_events(vec![expected]);
}
"
}
# echo "Usage: gen_cmd_event_inits.sh
# <domain name>
# <service name>
# <cmd struct name>
# <event struct name>
#
# <service struct name>
# <service obj>
# <service method>
if [ -z $1 ]
then
help
elif [ -z $2 ]
then
help
elif [ -z $3 ]
then
help
elif [ -z $4 ]
then
help
elif [ -z $5 ]
then
help
elif [ -z $6 ]
then
help
elif [ -z $7 ]
then
help
elif [ -z $8 ]
then
help
else
run "${@}"
run "${@}" | wl-copy
fi