feat: define Role aggregate and add_role_to_store {cmd,event&service}
This commit is contained in:
parent
d801894b0f
commit
3595560058
29 changed files with 1485 additions and 11 deletions
46
.sqlx/query-3a2efcc806c9a4cf7bd00b3eae4474de188981176005664ad53e39460319d34f.json
generated
Normal file
46
.sqlx/query-3a2efcc806c9a4cf7bd00b3eae4474de188981176005664ad53e39460319d34f.json
generated
Normal 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"
|
||||
}
|
28
.sqlx/query-4df5ca308db869c3488227a26fabe8f9452cf458a000842f5104c7b65e8ed5f2.json
generated
Normal file
28
.sqlx/query-4df5ca308db869c3488227a26fabe8f9452cf458a000842f5104c7b65e8ed5f2.json
generated
Normal 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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
22
.sqlx/query-70211e169bab5fc0f9240e26c14bd183fa8fa607f9b50377b2d1edbd4ec97472.json
generated
Normal file
22
.sqlx/query-70211e169bab5fc0f9240e26c14bd183fa8fa607f9b50377b2d1edbd4ec97472.json
generated
Normal 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"
|
||||
}
|
|
@ -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"
|
||||
}
|
23
.sqlx/query-79e633fecc51e6691730f40831678492191cb240271b6600bc9ab96dcb959035.json
generated
Normal file
23
.sqlx/query-79e633fecc51e6691730f40831678492191cb240271b6600bc9ab96dcb959035.json
generated
Normal 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"
|
||||
}
|
18
.sqlx/query-a93291e26fe69c7945bef30c84066ddefeafed4cb8f2d5f97ec3646f30ca2e78.json
generated
Normal file
18
.sqlx/query-a93291e26fe69c7945bef30c84066ddefeafed4cb8f2d5f97ec3646f30ca2e78.json
generated
Normal 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"
|
||||
}
|
|
@ -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"
|
||||
}
|
19
.sqlx/query-d0bda8d2ac1ede3becdc0aa642671dfb143547e5022b391a411bbb065a29dda0.json
generated
Normal file
19
.sqlx/query-d0bda8d2ac1ede3becdc0aa642671dfb143547e5022b391a411bbb065a29dda0.json
generated
Normal 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"
|
||||
}
|
40
.sqlx/query-e3e3b18f95cf920b423ea3c63db3327d6fdd548ecc0219230d529383297e3c40.json
generated
Normal file
40
.sqlx/query-e3e3b18f95cf920b423ea3c63db3327d6fdd548ecc0219230d529383297e3c40.json
generated
Normal 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"
|
||||
}
|
|
@ -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)
|
||||
|
|
21
migrations/20250112171332_cqrs_identity_role_query.sql
Normal file
21
migrations/20250112171332_cqrs_identity_role_query.sql
Normal 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)
|
||||
);
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
101
src/identity/adapters/output/db/postgres/get_roles_for_store.rs
Normal file
101
src/identity/adapters/output/db/postgres/get_roles_for_store.rs
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
85
src/identity/adapters/output/db/postgres/role_id_exists.rs
Normal file
85
src/identity/adapters/output/db/postgres/role_id_exists.rs
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
390
src/identity/adapters/output/db/postgres/role_view.rs
Normal file
390
src/identity/adapters/output/db/postgres/role_view.rs
Normal 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;
|
||||
// }
|
||||
//}
|
|
@ -21,4 +21,6 @@ pub enum OutDBPortError {
|
|||
DuplicateStoreName,
|
||||
DuplicateStoreID,
|
||||
StoreIDNotFound,
|
||||
RoleIDNotFound,
|
||||
DuplicateRoleName,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
53
src/identity/application/port/output/db/role_id_exists.rs
Normal file
53
src/identity/application/port/output/db/role_id_exists.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
186
src/identity/application/services/add_role_to_store_service.rs
Normal file
186
src/identity/application/services/add_role_to_store_service.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
39
src/identity/domain/add_role_command.rs
Normal file
39
src/identity/domain/add_role_command.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
35
src/identity/domain/role_added_event.rs
Normal file
35
src/identity/domain/role_added_event.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
115
src/identity/domain/role_aggregate.rs
Normal file
115
src/identity/domain/role_aggregate.rs
Normal 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]);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue