feat: define Role aggregate and add_role_to_store {cmd,event&service}

This commit is contained in:
Aravinth Manivannan 2025-01-13 00:30:16 +05:30
parent d801894b0f
commit 3595560058
Signed by: realaravinth
GPG key ID: F8F50389936984FF
29 changed files with 1485 additions and 11 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

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

@ -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,7 +109,14 @@ 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;
}
_ => (),
}
}
@ -129,6 +141,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 +175,7 @@ impl ViewRepository<EmployeeView, Employee> for DBOutPostgresAdapter {
phone_number_number,
phone_number_country_code,
phone_verified,
role_id,
deleted
FROM
@ -216,11 +230,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 +246,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 +268,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 +280,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 +459,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,390 @@
// 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::OrganizationExited(e) => self.store_id = None,
_ => (),
}
}
}
#[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

@ -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::default()]));
} else {
m.expect_get_roles_for_store()
.return_const(Ok(vec![Role::default()]));
}
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

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

@ -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;
@ -14,9 +15,12 @@ pub mod employee_registered_event;
//pub mod invite_accepted_event;
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 store_added_event;
pub mod store_updated_event;
pub mod verification_otp_resent_event;
@ -24,11 +28,14 @@ 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 resend_login_otp_command;
pub mod resend_verification_otp_command;
pub mod update_store_command;

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,115 @@
// 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::*,
};
use add_role_to_store_service::*;
use super::*;
use crate::tests::bdd::*;
type RoleTestFramework = TestFramework<Role>;
#[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]);
}
}