fix: replace username with first and last name and use user_id
UUID for primary keys
#32
66 changed files with 719 additions and 873 deletions
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS (\n SELECT 1\n FROM user_query\n WHERE\n username = $1\n );",
|
||||
"query": "SELECT EXISTS (\n SELECT 1\n FROM user_query\n WHERE\n user_id = $1\n );",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
|
@ -11,12 +11,12 @@
|
|||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "ed4bd44b2a0595cd80b36ce70b30ab55af1af12c1f22a2d4a2baf8af4569cf73"
|
||||
"hash": "004d12b7ccb1b21c39ef6de716953bf039bdba5096ae139be7656170ff45613f"
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO verification_otp (secret, created_at, purpose, username)\n VALUES ($1, $2, $3, $4);",
|
||||
"query": "INSERT INTO verification_otp (secret, created_at, purpose, user_id)\n VALUES ($1, $2, $3, $4);",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
|
@ -8,10 +8,10 @@
|
|||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Text",
|
||||
"Text"
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "487deedfaaf10c4ab02fc223b8fec4bf359b082331adfa788096742673168be3"
|
||||
"hash": "0fbaa8084440adce8a6162d67e3e57b6062cc17bbbbeb5ea3e1e58db17ac8240"
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE\n user_query\n SET\n user_id = $1, version = $2, first_name = $3, email = $4,\n hashed_password = $5, is_admin = $6, is_verified = $7, deleted = $8,\n last_name=$9;",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "14e8d7a1c8f80701b76b2bac69b1ecd99f7694d620f1945ad5c4ae474a17be1b"
|
||||
}
|
|
@ -9,7 +9,7 @@
|
|||
"Text",
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Text"
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
{
|
||||
"ordinal": 3,
|
||||
"name": "owner",
|
||||
"type_info": "Text"
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n view_id, version\n FROM\n user_query\n WHERE\n view_id = $1;",
|
||||
"query": "SELECT \n user_id, version\n FROM\n user_query\n WHERE\n user_id = $1;",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "view_id",
|
||||
"type_info": "Text"
|
||||
"name": "user_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
|
@ -16,7 +16,7 @@
|
|||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
|
@ -24,5 +24,5 @@
|
|||
false
|
||||
]
|
||||
},
|
||||
"hash": "fa1c65d51e3e0521d6a20998f4b80b615c9e0ffe9ceda82e0bce410c9aca39a0"
|
||||
"hash": "6523c83a859d7ca283d209133a10d4ac74b6b0358bdf1e17f1a54e2cc02e305b"
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS (\n SELECT 1\n FROM verification_otp\n WHERE\n username = $1\n AND\n purpose = $2\n AND\n secret = $3\n );",
|
||||
"query": "SELECT EXISTS (\n SELECT 1\n FROM verification_otp\n WHERE\n user_id = $1\n AND\n purpose = $2\n AND\n secret = $3\n );",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
|
@ -11,7 +11,7 @@
|
|||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
|
@ -20,5 +20,5 @@
|
|||
null
|
||||
]
|
||||
},
|
||||
"hash": "3edf94a78114819085b573ff51702faee1c444c24bbf6d33ec1b4b245dd9f675"
|
||||
"hash": "70e6216e30f90175d4c3bad51ff51f5fa2b6f965d868b15c53264cb8cd6b4053"
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE\n user_query\n SET\n view_id = $1, version = $2, username = $3, email = $4,\n hashed_password = $5, is_admin = $6, is_verified = $7, deleted = $8;",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "750f96296e6d2c0b6d79c6f21e5b665369d5062a42c2c405f43d56a614a1e0ea"
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO user_query (\n view_id, version, username, email,\n hashed_password, is_admin, is_verified, deleted\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8\n );",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7a401e98fff59eb8c0fe1744ccdea68842eaf86ba312c2eb324854041beb9de7"
|
||||
}
|
|
@ -1,42 +1,52 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n username, email, hashed_password, is_admin, is_verified, deleted\n FROM\n user_query\n WHERE\n view_id = $1;",
|
||||
"query": "SELECT \n first_name, last_name, user_id, email, hashed_password, is_admin, is_verified, deleted\n FROM\n user_query\n WHERE\n user_id = $1;",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "username",
|
||||
"name": "first_name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "email",
|
||||
"name": "last_name",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "user_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "hashed_password",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"ordinal": 5,
|
||||
"name": "is_admin",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 6,
|
||||
"name": "is_verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 7,
|
||||
"name": "deleted",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
|
@ -45,8 +55,10 @@
|
|||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "803f868ee4a79fefdd85c5d0a034a5925c3bb7de87828095971cb19bc1fc7550"
|
||||
"hash": "7a59c989d043c249cd04fe24544cf9ea55e1329ce4b53889947478c2e766ea1a"
|
||||
}
|
|
@ -9,7 +9,7 @@
|
|||
"Text",
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Text"
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM\n verification_otp\n WHERE\n username = $1\n AND\n purpose = $2\n AND\n secret = $3;",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c54d1b89ee39ceb11326e82962eef3d8f588f6252ba4bbac99fb73cdbbcd2204"
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n secret\n FROM\n verification_otp\n WHERE\n username = $1\n AND\n purpose = $2;",
|
||||
"query": "SELECT\n secret\n FROM\n verification_otp\n WHERE\n user_id = $1\n AND\n purpose = $2;",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
|
@ -11,7 +11,7 @@
|
|||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
|
@ -19,5 +19,5 @@
|
|||
false
|
||||
]
|
||||
},
|
||||
"hash": "7c48c7569b16e9600aa31fbf14b9ceeb195da7cde0082be861369ef4f997c534"
|
||||
"hash": "ec80b5dc41697e7df7112962aba8185040fc0a101edea4b7e0c23f6468300196"
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO user_query (\n version, first_name, last_name, email,\n hashed_password, is_admin, is_verified, deleted, user_id\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9\n );",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f1b7a6581e20b9f1c0dabeb7a2479d70ab005cf787a0ed13260e65f7ea949136"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM\n verification_otp\n WHERE\n user_id = $1\n AND\n purpose = $2\n AND\n secret = $3;",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ffb4315a9b95ed39964c2a0a5a9dc45e4ab7941ced043931568023238d5127f7"
|
||||
}
|
|
@ -17,17 +17,16 @@ CREATE TABLE IF NOT EXISTS events
|
|||
|
||||
CREATE TABLE IF NOT EXISTS user_query
|
||||
(
|
||||
view_id text NOT NULL,
|
||||
version bigint CHECK (version >= 0) NOT NULL,
|
||||
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
user_id UUID NOT NULL UNIQUE,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
hashed_password TEXT NOT NULL,
|
||||
is_admin BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
PRIMARY KEY (view_id)
|
||||
PRIMARY KEY (user_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS user_username_index ON user_query (username);
|
||||
|
|
|
@ -6,6 +6,6 @@ CREATE TABLE IF NOT EXISTS verification_otp (
|
|||
secret VARCHAR(32) NOT NULL UNIQUE,
|
||||
created_at timestamp with time zone DEFAULT (CURRENT_TIMESTAMP),
|
||||
purpose TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
user_id UUID NOT NULL,
|
||||
ID SERIAL PRIMARY KEY NOT NULL
|
||||
);
|
||||
|
|
|
@ -15,8 +15,3 @@ CREATE TABLE IF NOT EXISTS cqrs_inventory_category_query
|
|||
|
||||
PRIMARY KEY (category_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS
|
||||
cqrs_inventory_store_query_category_id_index
|
||||
ON
|
||||
cqrs_inventory_category_query (category_id);
|
||||
|
|
|
@ -8,11 +8,9 @@ CREATE TABLE IF NOT EXISTS cqrs_inventory_store_query
|
|||
|
||||
name TEXT NOT NULL,
|
||||
address TEXT,
|
||||
owner TEXT NOT NULL,
|
||||
owner UUID NOT NULL,
|
||||
store_id UUID NOT NULL UNIQUE,
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
PRIMARY KEY (store_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS store_store_id_index ON cqrs_inventory_store_query (store_id);
|
||||
|
|
|
@ -11,12 +11,12 @@ use crate::identity::application::port::output::db::{create_verification_secret:
|
|||
impl CreateVerificationSecretOutDBPort for DBOutPostgresAdapter {
|
||||
async fn create_verification_secret(&self, msg: CreateSecretMsg) -> OutDBPortResult<()> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO verification_otp (secret, created_at, purpose, username)
|
||||
"INSERT INTO verification_otp (secret, created_at, purpose, user_id)
|
||||
VALUES ($1, $2, $3, $4);",
|
||||
&msg.secret,
|
||||
OffsetDateTime::now_utc(),
|
||||
REGISTRATION_SECRET_PURPOSE,
|
||||
&msg.username,
|
||||
&msg.user_id,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
@ -26,6 +26,8 @@ impl CreateVerificationSecretOutDBPort for DBOutPostgresAdapter {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::utils::uuid::tests::UUID;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
|
@ -40,7 +42,7 @@ mod tests {
|
|||
|
||||
let msg = CreateSecretMsgBuilder::default()
|
||||
.secret("secret".into())
|
||||
.username("username".into())
|
||||
.user_id(UUID.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
|
|
|
@ -12,12 +12,12 @@ impl DeleteVerificationSecretOutDBPort for DBOutPostgresAdapter {
|
|||
"DELETE FROM
|
||||
verification_otp
|
||||
WHERE
|
||||
username = $1
|
||||
user_id = $1
|
||||
AND
|
||||
purpose = $2
|
||||
AND
|
||||
secret = $3;",
|
||||
msg.username,
|
||||
msg.user_id,
|
||||
REGISTRATION_SECRET_PURPOSE,
|
||||
msg.secret,
|
||||
)
|
||||
|
@ -30,13 +30,16 @@ impl DeleteVerificationSecretOutDBPort for DBOutPostgresAdapter {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::identity::application::port::output::db::{
|
||||
use crate::{
|
||||
identity::application::port::output::db::{
|
||||
create_verification_secret::*, verification_secret_exists::*,
|
||||
},
|
||||
utils::uuid::tests::UUID,
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_postgres_delete_verification_secret() {
|
||||
let username = "batman";
|
||||
let user_id = UUID;
|
||||
let secret = "bsdasdf";
|
||||
let settings = crate::settings::tests::get_settings().await;
|
||||
settings.create_db().await;
|
||||
|
@ -46,7 +49,7 @@ mod tests {
|
|||
.unwrap(),
|
||||
);
|
||||
let msg = VerifySecretExistsMsgBuilder::default()
|
||||
.username(username.into())
|
||||
.user_id(user_id.clone())
|
||||
.secret(secret.into())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
@ -56,7 +59,7 @@ mod tests {
|
|||
|
||||
let create_msg = CreateSecretMsgBuilder::default()
|
||||
.secret(secret.into())
|
||||
.username(username.into())
|
||||
.user_id(user_id.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
db.create_verification_secret(create_msg).await.unwrap();
|
||||
|
|
|
@ -33,6 +33,8 @@ impl EmailExistsOutDBPort for DBOutPostgresAdapter {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::utils::uuid::tests::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_postgres_email_exists() {
|
||||
let email = "foo@exmaple.com";
|
||||
|
@ -49,12 +51,13 @@ mod tests {
|
|||
|
||||
sqlx::query!(
|
||||
"INSERT INTO user_query
|
||||
(view_id, version, username, email, hashed_password)
|
||||
VALUES ($1, $2, $3, $4, $5);",
|
||||
"1",
|
||||
(user_id, version, first_name, email, hashed_password, last_name)
|
||||
VALUES ($1, $2, $3, $4, $5, $6);",
|
||||
&UUID,
|
||||
1,
|
||||
"foo",
|
||||
email,
|
||||
"passwd",
|
||||
"passwd"
|
||||
)
|
||||
.execute(&db.pool)
|
||||
|
|
|
@ -2,13 +2,15 @@
|
|||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::errors::*;
|
||||
use super::DBOutPostgresAdapter;
|
||||
use crate::identity::application::port::output::db::{errors::*, get_verification_secret::*};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl GetVerificationSecretOutDBPort for DBOutPostgresAdapter {
|
||||
async fn get_verification_secret(&self, username: &str) -> OutDBPortResult<String> {
|
||||
async fn get_verification_secret(&self, user_id: &Uuid) -> OutDBPortResult<String> {
|
||||
struct Secret {
|
||||
secret: String,
|
||||
}
|
||||
|
@ -19,10 +21,10 @@ impl GetVerificationSecretOutDBPort for DBOutPostgresAdapter {
|
|||
FROM
|
||||
verification_otp
|
||||
WHERE
|
||||
username = $1
|
||||
user_id = $1
|
||||
AND
|
||||
purpose = $2;",
|
||||
username,
|
||||
user_id,
|
||||
REGISTRATION_SECRET_PURPOSE,
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
|
@ -35,11 +37,14 @@ impl GetVerificationSecretOutDBPort for DBOutPostgresAdapter {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::identity::application::port::output::db::create_verification_secret::*;
|
||||
use crate::{
|
||||
identity::application::port::output::db::create_verification_secret::*,
|
||||
utils::uuid::tests::UUID,
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_postgres_get_verification_secret() {
|
||||
let username = "batman";
|
||||
let user_id = UUID;
|
||||
let secret = "bsdasdf";
|
||||
let settings = crate::settings::tests::get_settings().await;
|
||||
settings.create_db().await;
|
||||
|
@ -49,19 +54,19 @@ mod tests {
|
|||
.unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_verification_secret(username).await.err(),
|
||||
db.get_verification_secret(&user_id).await.err(),
|
||||
Some(OutDBPortError::VerificationOTPSecretNotFound)
|
||||
);
|
||||
|
||||
let create_msg = CreateSecretMsgBuilder::default()
|
||||
.secret(secret.into())
|
||||
.username(username.into())
|
||||
.user_id(user_id.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
db.create_verification_secret(create_msg).await.unwrap();
|
||||
|
||||
assert_eq!(db.get_verification_secret(username).await.unwrap(), secret);
|
||||
assert_eq!(db.get_verification_secret(&user_id).await.unwrap(), secret);
|
||||
|
||||
settings.drop_db().await;
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ pub mod delete_verification_secret;
|
|||
pub mod email_exists;
|
||||
mod errors;
|
||||
pub mod get_verification_secret;
|
||||
pub mod user_id_exists;
|
||||
pub mod user_view;
|
||||
pub mod username_exists;
|
||||
pub mod verification_secret_exists;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
|
@ -2,22 +2,24 @@
|
|||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::DBOutPostgresAdapter;
|
||||
use crate::identity::application::port::output::db::{
|
||||
errors::*, username_exists::UsernameExistsOutDBPort,
|
||||
errors::*, user_id_exists::UserIDExistsOutDBPort,
|
||||
};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl UsernameExistsOutDBPort for DBOutPostgresAdapter {
|
||||
async fn username_exists(&self, username: &str) -> OutDBPortResult<bool> {
|
||||
impl UserIDExistsOutDBPort for DBOutPostgresAdapter {
|
||||
async fn user_id_exists(&self, user_id: &Uuid) -> OutDBPortResult<bool> {
|
||||
let res = sqlx::query!(
|
||||
"SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM user_query
|
||||
WHERE
|
||||
username = $1
|
||||
user_id = $1
|
||||
);",
|
||||
username
|
||||
user_id,
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
|
@ -33,9 +35,12 @@ impl UsernameExistsOutDBPort for DBOutPostgresAdapter {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::utils::uuid::tests::UUID;
|
||||
|
||||
use crate::identity::domain::aggregate::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_postgres_username_exists() {
|
||||
let username = "foo@exmaple.com";
|
||||
async fn test_postgres_user_id_exists() {
|
||||
let settings = crate::settings::tests::get_settings().await;
|
||||
settings.create_db().await;
|
||||
let db = super::DBOutPostgresAdapter::new(
|
||||
|
@ -44,25 +49,28 @@ mod tests {
|
|||
.unwrap(),
|
||||
);
|
||||
|
||||
let user = User::default();
|
||||
|
||||
// state doesn't exist
|
||||
assert!(!db.username_exists(username).await.unwrap());
|
||||
assert!(!db.user_id_exists(&UUID).await.unwrap());
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO user_query
|
||||
(view_id, version, username, email, hashed_password)
|
||||
VALUES ($1, $2, $3, $4, $5);",
|
||||
"1",
|
||||
(version, user_id, email, hashed_password, first_name, last_name)
|
||||
VALUES ($1, $2, $3, $4, $5, $6);",
|
||||
1,
|
||||
username,
|
||||
"foo",
|
||||
"passwd"
|
||||
UUID,
|
||||
user.email(),
|
||||
user.hashed_password(),
|
||||
user.first_name(),
|
||||
user.last_name(),
|
||||
)
|
||||
.execute(&db.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// state exists
|
||||
assert!(db.username_exists(username).await.unwrap());
|
||||
assert!(db.user_id_exists(&UUID).await.unwrap());
|
||||
|
||||
settings.drop_db().await;
|
||||
}
|
|
@ -6,18 +6,24 @@ use async_trait::async_trait;
|
|||
use cqrs_es::persist::GenericQuery;
|
||||
use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository};
|
||||
use cqrs_es::{EventEnvelope, Query, View};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::errors::*;
|
||||
use super::DBOutPostgresAdapter;
|
||||
use crate::identity::application::services::events::UserEvent;
|
||||
use crate::identity::domain::aggregate::User;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::utils::parse_aggregate_id::parse_aggregate_id;
|
||||
|
||||
pub const NEW_USER_NON_UUID: &str = "new_user_non_uuid-asdfa";
|
||||
|
||||
// The view for a User query, for a standard http application this should
|
||||
// be designed to reflect the response dto that will be returned to a user.
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct UserView {
|
||||
username: String,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
user_id: Uuid,
|
||||
email: String,
|
||||
hashed_password: String,
|
||||
is_admin: bool,
|
||||
|
@ -32,12 +38,14 @@ impl View<User> for UserView {
|
|||
fn update(&mut self, event: &EventEnvelope<User>) {
|
||||
match &event.payload {
|
||||
UserEvent::UserRegistered(val) => {
|
||||
self.username = val.username().into();
|
||||
self.email = val.email().into();
|
||||
self.hashed_password = val.hashed_password().into();
|
||||
self.is_admin = val.is_admin().to_owned();
|
||||
self.is_verified = val.is_verified().to_owned();
|
||||
self.deleted = false;
|
||||
self.first_name = val.first_name().into();
|
||||
self.last_name = val.last_name().into();
|
||||
self.user_id = val.user_id().clone();
|
||||
}
|
||||
UserEvent::UserDeleted => self.deleted = true,
|
||||
UserEvent::Loggedin(_) => (),
|
||||
|
@ -58,16 +66,21 @@ impl View<User> for UserView {
|
|||
|
||||
#[async_trait]
|
||||
impl ViewRepository<UserView, User> for DBOutPostgresAdapter {
|
||||
async fn load(&self, view_id: &str) -> Result<Option<UserView>, PersistenceError> {
|
||||
async fn load(&self, user_id: &str) -> Result<Option<UserView>, PersistenceError> {
|
||||
let user_id = match parse_aggregate_id(user_id, NEW_USER_NON_UUID)? {
|
||||
Some((val, _)) => return Ok(Some(val)),
|
||||
None => Uuid::parse_str(user_id).unwrap(),
|
||||
};
|
||||
|
||||
let res = sqlx::query_as!(
|
||||
UserView,
|
||||
"SELECT
|
||||
username, email, hashed_password, is_admin, is_verified, deleted
|
||||
first_name, last_name, user_id, email, hashed_password, is_admin, is_verified, deleted
|
||||
FROM
|
||||
user_query
|
||||
WHERE
|
||||
view_id = $1;",
|
||||
view_id
|
||||
user_id = $1;",
|
||||
user_id
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
|
@ -77,17 +90,22 @@ impl ViewRepository<UserView, User> for DBOutPostgresAdapter {
|
|||
|
||||
async fn load_with_context(
|
||||
&self,
|
||||
view_id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<Option<(UserView, ViewContext)>, PersistenceError> {
|
||||
let user_id = match parse_aggregate_id(user_id, NEW_USER_NON_UUID)? {
|
||||
Some(val) => return Ok(Some(val)),
|
||||
None => Uuid::parse_str(user_id).unwrap(),
|
||||
};
|
||||
|
||||
let res = sqlx::query_as!(
|
||||
UserView,
|
||||
"SELECT
|
||||
username, email, hashed_password, is_admin, is_verified, deleted
|
||||
first_name, last_name, user_id, email, hashed_password, is_admin, is_verified, deleted
|
||||
FROM
|
||||
user_query
|
||||
WHERE
|
||||
view_id = $1;",
|
||||
view_id
|
||||
user_id = $1;",
|
||||
user_id
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
|
@ -95,24 +113,24 @@ impl ViewRepository<UserView, User> for DBOutPostgresAdapter {
|
|||
|
||||
struct Context {
|
||||
version: i64,
|
||||
view_id: String,
|
||||
user_id: String,
|
||||
}
|
||||
|
||||
let ctx = sqlx::query_as!(
|
||||
Context,
|
||||
"SELECT
|
||||
view_id, version
|
||||
user_id, version
|
||||
FROM
|
||||
user_query
|
||||
WHERE
|
||||
view_id = $1;",
|
||||
view_id
|
||||
user_id = $1;",
|
||||
user_id
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(PostgresAggregateError::from)?;
|
||||
|
||||
let view_context = ViewContext::new(ctx.view_id, ctx.version);
|
||||
let view_context = ViewContext::new(ctx.user_id.to_string(), ctx.version);
|
||||
Ok(Some((res, view_context)))
|
||||
}
|
||||
|
||||
|
@ -126,19 +144,20 @@ impl ViewRepository<UserView, User> for DBOutPostgresAdapter {
|
|||
let version = context.version + 1;
|
||||
sqlx::query!(
|
||||
"INSERT INTO user_query (
|
||||
view_id, version, username, email,
|
||||
hashed_password, is_admin, is_verified, deleted
|
||||
version, first_name, last_name, email,
|
||||
hashed_password, is_admin, is_verified, deleted, user_id
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9
|
||||
);",
|
||||
context.view_instance_id,
|
||||
version,
|
||||
view.username,
|
||||
view.first_name,
|
||||
view.last_name,
|
||||
view.email,
|
||||
view.hashed_password,
|
||||
view.is_admin,
|
||||
view.is_verified,
|
||||
view.deleted,
|
||||
view.user_id,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
|
@ -150,16 +169,18 @@ impl ViewRepository<UserView, User> for DBOutPostgresAdapter {
|
|||
"UPDATE
|
||||
user_query
|
||||
SET
|
||||
view_id = $1, version = $2, username = $3, email = $4,
|
||||
hashed_password = $5, is_admin = $6, is_verified = $7, deleted = $8;",
|
||||
context.view_instance_id,
|
||||
user_id = $1, version = $2, first_name = $3, email = $4,
|
||||
hashed_password = $5, is_admin = $6, is_verified = $7, deleted = $8,
|
||||
last_name=$9;",
|
||||
view.user_id,
|
||||
version,
|
||||
view.username,
|
||||
view.first_name,
|
||||
view.email,
|
||||
view.hashed_password,
|
||||
view.is_admin,
|
||||
view.is_verified,
|
||||
view.deleted,
|
||||
view.last_name,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
|
|
|
@ -16,13 +16,13 @@ impl VerificationSecretExistsOutDBPort for DBOutPostgresAdapter {
|
|||
SELECT 1
|
||||
FROM verification_otp
|
||||
WHERE
|
||||
username = $1
|
||||
user_id = $1
|
||||
AND
|
||||
purpose = $2
|
||||
AND
|
||||
secret = $3
|
||||
);",
|
||||
msg.username,
|
||||
msg.user_id,
|
||||
REGISTRATION_SECRET_PURPOSE,
|
||||
msg.secret,
|
||||
)
|
||||
|
@ -39,11 +39,14 @@ impl VerificationSecretExistsOutDBPort for DBOutPostgresAdapter {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::identity::application::port::output::db::create_verification_secret::*;
|
||||
use crate::{
|
||||
identity::application::port::output::db::create_verification_secret::*,
|
||||
utils::uuid::tests::UUID,
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_postgres_verification_secret_exists() {
|
||||
let username = "batman";
|
||||
let user_id = UUID;
|
||||
let secret = "bsdasdf";
|
||||
let settings = crate::settings::tests::get_settings().await;
|
||||
settings.create_db().await;
|
||||
|
@ -53,7 +56,7 @@ mod tests {
|
|||
.unwrap(),
|
||||
);
|
||||
let msg = VerifySecretExistsMsgBuilder::default()
|
||||
.username(username.into())
|
||||
.user_id(user_id.clone())
|
||||
.secret(secret.into())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
@ -63,7 +66,7 @@ mod tests {
|
|||
|
||||
let create_msg = CreateSecretMsgBuilder::default()
|
||||
.secret(secret.into())
|
||||
.username(username.into())
|
||||
.user_id(user_id.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
db.create_verification_secret(create_msg).await.unwrap();
|
||||
|
|
|
@ -12,17 +12,17 @@ impl AccountValidationLinkOutMailerPort for LettreMailer {
|
|||
async fn account_validation_link(
|
||||
&self,
|
||||
to: &str,
|
||||
username: &str,
|
||||
first_name: &str,
|
||||
validation_secret: &str,
|
||||
) -> OutMailerPortResult<()> {
|
||||
let email = Message::builder()
|
||||
.from(self.from.parse().unwrap())
|
||||
.reply_to(self.reply_to.parse().unwrap())
|
||||
.to(format!("{username} <{to}>").parse().unwrap())
|
||||
.to(format!("{first_name} <{to}>").parse().unwrap())
|
||||
.subject("Please verify your account on Vanikam") // TODO: use better title
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(format!(
|
||||
r#"Hello {username},
|
||||
r#"Hello {first_name},
|
||||
Please click here to verify your Vanikam account: {validation_secret}
|
||||
Warm regards,
|
||||
Vanikam Admin
|
||||
|
|
|
@ -1,325 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use async_trait::async_trait;
|
||||
use cqrs_es::Aggregate;
|
||||
|
||||
use crate::identity::application::services::errors::*;
|
||||
use crate::identity::application::services::events::UserEvent;
|
||||
use crate::identity::application::services::UserCommand;
|
||||
use crate::identity::application::services::UserServicesInterface;
|
||||
use crate::identity::domain::aggregate::User;
|
||||
use crate::identity::domain::aggregate::UserBuilder;
|
||||
|
||||
#[async_trait]
|
||||
impl Aggregate for User {
|
||||
type Command = UserCommand;
|
||||
type Event = UserEvent;
|
||||
type Error = IdentityError;
|
||||
type Services = std::sync::Arc<dyn UserServicesInterface>;
|
||||
|
||||
// This identifier should be unique to the system.
|
||||
fn aggregate_type() -> String {
|
||||
"account".to_string()
|
||||
}
|
||||
|
||||
// The aggregate logic goes here. Note that this will be the _bulk_ of a CQRS system
|
||||
// so expect to use helper functions elsewhere to keep the code clean.
|
||||
async fn handle(
|
||||
&self,
|
||||
command: Self::Command,
|
||||
services: &Self::Services,
|
||||
) -> Result<Vec<Self::Event>, Self::Error> {
|
||||
match command {
|
||||
UserCommand::RegisterUser(cmd) => {
|
||||
let res = services.register_user().register_user(cmd).await?;
|
||||
Ok(vec![UserEvent::UserRegistered(res)])
|
||||
}
|
||||
UserCommand::DeleteUser(cmd) => {
|
||||
services.delete_user().delete_user(cmd).await;
|
||||
Ok(vec![UserEvent::UserDeleted])
|
||||
}
|
||||
UserCommand::Login(cmd) => {
|
||||
let res = services.login().login(cmd).await;
|
||||
Ok(vec![UserEvent::Loggedin(res)])
|
||||
}
|
||||
UserCommand::UpdatePassword(cmd) => {
|
||||
let res = services.update_password().update_password(cmd).await;
|
||||
Ok(vec![UserEvent::PasswordUpdated(res)])
|
||||
}
|
||||
UserCommand::UpdateEmail(cmd) => {
|
||||
let res = services.update_email().update_email(cmd).await?;
|
||||
Ok(vec![UserEvent::EmailUpdated(res)])
|
||||
}
|
||||
UserCommand::MarkUserVerified(cmd) => {
|
||||
services
|
||||
.mark_user_verified()
|
||||
.mark_user_verified(cmd)
|
||||
.await?;
|
||||
Ok(vec![UserEvent::UserVerified])
|
||||
}
|
||||
UserCommand::SetAdmin(cmd) => {
|
||||
let res = services.set_user_admin().set_user_admin(cmd).await;
|
||||
Ok(vec![UserEvent::UserPromotedToAdmin(res)])
|
||||
}
|
||||
UserCommand::ResendVerificationEmail(cmd) => {
|
||||
services
|
||||
.resend_verification_email()
|
||||
.resend_verification_email(cmd)
|
||||
.await?;
|
||||
Ok(vec![UserEvent::VerificationEmailResent])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Self::Event) {
|
||||
match event {
|
||||
UserEvent::UserRegistered(e) => {
|
||||
*self = UserBuilder::default()
|
||||
.username(e.username().into())
|
||||
.email(e.email().into())
|
||||
.hashed_password(e.hashed_password().into())
|
||||
.is_admin(e.is_admin().to_owned())
|
||||
.email_verified(e.email_verified().to_owned())
|
||||
.is_verified(e.is_verified().to_owned())
|
||||
.deleted(false)
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
UserEvent::UserDeleted => {
|
||||
self.set_deleted(true);
|
||||
}
|
||||
UserEvent::Loggedin(_) => (),
|
||||
UserEvent::PasswordUpdated(_) => (),
|
||||
UserEvent::EmailUpdated(e) => {
|
||||
self.set_email(e.new_email().into());
|
||||
self.set_email_verified(false);
|
||||
}
|
||||
UserEvent::UserVerified => {
|
||||
self.set_is_verified(true);
|
||||
self.set_email_verified(true);
|
||||
}
|
||||
UserEvent::UserPromotedToAdmin(_) => {
|
||||
self.set_is_admin(true);
|
||||
}
|
||||
UserEvent::VerificationEmailResent => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//// The aggregate tests are the most important part of a CQRS system.
|
||||
//// The simplicity and flexibility of these tests are a good part of what
|
||||
//// makes an event sourced system so friendly to changing business requirements.
|
||||
//#[cfg(test)]
|
||||
//mod aggregate_tests {
|
||||
// use async_trait::async_trait;
|
||||
// use std::sync::Mutex;
|
||||
//
|
||||
// use cqrs_es::test::TestFramework;
|
||||
//
|
||||
// use crate::domain::aggregate::User;
|
||||
// use crate::domain::commands::UserCommand;
|
||||
// use crate::domain::events::UserEvent;
|
||||
// use crate::services::{AtmError, UserApi, UserServices, CheckingError};
|
||||
//
|
||||
// // A test framework that will apply our events and command
|
||||
// // and verify that the logic works as expected.
|
||||
// type AccountTestFramework = TestFramework<User>;
|
||||
//
|
||||
// #[test]
|
||||
// fn test_deposit_money() {
|
||||
// let expected = UserEvent::CustomerDepositedMoney {
|
||||
// amount: 200.0,
|
||||
// balance: 200.0,
|
||||
// };
|
||||
// let command = UserCommand::DepositMoney { amount: 200.0 };
|
||||
// let services = UserServices::new(Box::new(MockUserServices::default()));
|
||||
// // Obtain a new test framework
|
||||
// AccountTestFramework::with(services)
|
||||
// // In a test case with no previous events
|
||||
// .given_no_previous_events()
|
||||
// // Wnen we fire this command
|
||||
// .when(command)
|
||||
// // then we expect these results
|
||||
// .then_expect_events(vec![expected]);
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn test_deposit_money_with_balance() {
|
||||
// let previous = UserEvent::CustomerDepositedMoney {
|
||||
// amount: 200.0,
|
||||
// balance: 200.0,
|
||||
// };
|
||||
// let expected = UserEvent::CustomerDepositedMoney {
|
||||
// amount: 200.0,
|
||||
// balance: 400.0,
|
||||
// };
|
||||
// let command = UserCommand::DepositMoney { amount: 200.0 };
|
||||
// let services = UserServices::new(Box::new(MockUserServices::default()));
|
||||
//
|
||||
// AccountTestFramework::with(services)
|
||||
// // Given this previously applied event
|
||||
// .given(vec![previous])
|
||||
// // When we fire this command
|
||||
// .when(command)
|
||||
// // Then we expect this resultant event
|
||||
// .then_expect_events(vec![expected]);
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn test_withdraw_money() {
|
||||
// let previous = UserEvent::CustomerDepositedMoney {
|
||||
// amount: 200.0,
|
||||
// balance: 200.0,
|
||||
// };
|
||||
// let expected = UserEvent::CustomerWithdrewCash {
|
||||
// amount: 100.0,
|
||||
// balance: 100.0,
|
||||
// };
|
||||
// let services = MockUserServices::default();
|
||||
// services.set_atm_withdrawal_response(Ok(()));
|
||||
// let command = UserCommand::WithdrawMoney {
|
||||
// amount: 100.0,
|
||||
// atm_id: "ATM34f1ba3c".to_string(),
|
||||
// };
|
||||
//
|
||||
// AccountTestFramework::with(UserServices::new(Box::new(services)))
|
||||
// .given(vec![previous])
|
||||
// .when(command)
|
||||
// .then_expect_events(vec![expected]);
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn test_withdraw_money_client_error() {
|
||||
// let previous = UserEvent::CustomerDepositedMoney {
|
||||
// amount: 200.0,
|
||||
// balance: 200.0,
|
||||
// };
|
||||
// let services = MockUserServices::default();
|
||||
// services.set_atm_withdrawal_response(Err(AtmError));
|
||||
// let command = UserCommand::WithdrawMoney {
|
||||
// amount: 100.0,
|
||||
// atm_id: "ATM34f1ba3c".to_string(),
|
||||
// };
|
||||
//
|
||||
// let services = UserServices::new(Box::new(services));
|
||||
// AccountTestFramework::with(services)
|
||||
// .given(vec![previous])
|
||||
// .when(command)
|
||||
// .then_expect_error_message("atm rule violation");
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn test_withdraw_money_funds_not_available() {
|
||||
// let command = UserCommand::WithdrawMoney {
|
||||
// amount: 200.0,
|
||||
// atm_id: "ATM34f1ba3c".to_string(),
|
||||
// };
|
||||
//
|
||||
// let services = UserServices::new(Box::new(MockUserServices::default()));
|
||||
// AccountTestFramework::with(services)
|
||||
// .given_no_previous_events()
|
||||
// .when(command)
|
||||
// // Here we expect an error rather than any events
|
||||
// .then_expect_error_message("funds not available")
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn test_wrote_check() {
|
||||
// let previous = UserEvent::CustomerDepositedMoney {
|
||||
// amount: 200.0,
|
||||
// balance: 200.0,
|
||||
// };
|
||||
// let expected = UserEvent::CustomerWroteCheck {
|
||||
// check_number: "1170".to_string(),
|
||||
// amount: 100.0,
|
||||
// balance: 100.0,
|
||||
// };
|
||||
// let services = MockUserServices::default();
|
||||
// services.set_validate_check_response(Ok(()));
|
||||
// let services = UserServices::new(Box::new(services));
|
||||
// let command = UserCommand::WriteCheck {
|
||||
// check_number: "1170".to_string(),
|
||||
// amount: 100.0,
|
||||
// };
|
||||
//
|
||||
// AccountTestFramework::with(services)
|
||||
// .given(vec![previous])
|
||||
// .when(command)
|
||||
// .then_expect_events(vec![expected]);
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn test_wrote_check_bad_check() {
|
||||
// let previous = UserEvent::CustomerDepositedMoney {
|
||||
// amount: 200.0,
|
||||
// balance: 200.0,
|
||||
// };
|
||||
// let services = MockUserServices::default();
|
||||
// services.set_validate_check_response(Err(CheckingError));
|
||||
// let services = UserServices::new(Box::new(services));
|
||||
// let command = UserCommand::WriteCheck {
|
||||
// check_number: "1170".to_string(),
|
||||
// amount: 100.0,
|
||||
// };
|
||||
//
|
||||
// AccountTestFramework::with(services)
|
||||
// .given(vec![previous])
|
||||
// .when(command)
|
||||
// .then_expect_error_message("check invalid");
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn test_wrote_check_funds_not_available() {
|
||||
// let command = UserCommand::WriteCheck {
|
||||
// check_number: "1170".to_string(),
|
||||
// amount: 100.0,
|
||||
// };
|
||||
//
|
||||
// let services = UserServices::new(Box::new(MockUserServices::default()));
|
||||
// AccountTestFramework::with(services)
|
||||
// .given_no_previous_events()
|
||||
// .when(command)
|
||||
// .then_expect_error_message("funds not available")
|
||||
// }
|
||||
//
|
||||
// pub struct MockUserServices {
|
||||
// atm_withdrawal_response: Mutex<Option<Result<(), AtmError>>>,
|
||||
// validate_check_response: Mutex<Option<Result<(), CheckingError>>>,
|
||||
// }
|
||||
//
|
||||
// impl Default for MockUserServices {
|
||||
// fn default() -> Self {
|
||||
// Self {
|
||||
// atm_withdrawal_response: Mutex::new(None),
|
||||
// validate_check_response: Mutex::new(None),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl MockUserServices {
|
||||
// fn set_atm_withdrawal_response(&self, response: Result<(), AtmError>) {
|
||||
// *self.atm_withdrawal_response.lock().unwrap() = Some(response);
|
||||
// }
|
||||
// fn set_validate_check_response(&self, response: Result<(), CheckingError>) {
|
||||
// *self.validate_check_response.lock().unwrap() = Some(response);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #[async_trait]
|
||||
// impl UserApi for MockUserServices {
|
||||
// async fn atm_withdrawal(&self, _atm_id: &str, _amount: f64) -> Result<(), AtmError> {
|
||||
// self.atm_withdrawal_response.lock().unwrap().take().unwrap()
|
||||
// }
|
||||
//
|
||||
// async fn validate_check(
|
||||
// &self,
|
||||
// _account_id: &str,
|
||||
// _check_number: &str,
|
||||
// ) -> Result<(), CheckingError> {
|
||||
// self.validate_check_response.lock().unwrap().take().unwrap()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
|
@ -2,6 +2,5 @@
|
|||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub mod aggregate;
|
||||
pub mod port;
|
||||
pub mod services;
|
||||
|
|
|
@ -6,6 +6,7 @@ use derive_builder::Builder;
|
|||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::errors::*;
|
||||
#[cfg(test)]
|
||||
|
@ -15,7 +16,7 @@ pub use tests::*;
|
|||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Builder)]
|
||||
pub struct CreateSecretMsg {
|
||||
pub secret: String,
|
||||
pub username: String,
|
||||
pub user_id: Uuid,
|
||||
}
|
||||
pub const REGISTRATION_SECRET_PURPOSE: &str = "account_validation";
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ use derive_builder::Builder;
|
|||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use super::create_verification_secret::REGISTRATION_SECRET_PURPOSE;
|
||||
use super::errors::*;
|
||||
|
@ -16,14 +17,14 @@ pub use tests::*;
|
|||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Builder)]
|
||||
pub struct DeleteSecretMsg {
|
||||
pub secret: String,
|
||||
pub username: String,
|
||||
pub user_id: Uuid,
|
||||
}
|
||||
|
||||
impl From<super::verification_secret_exists::VerifySecretExistsMsg> for DeleteSecretMsg {
|
||||
fn from(value: super::verification_secret_exists::VerifySecretExistsMsg) -> Self {
|
||||
Self {
|
||||
secret: value.secret,
|
||||
username: value.username,
|
||||
user_id: value.user_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use super::create_verification_secret::REGISTRATION_SECRET_PURPOSE;
|
||||
use super::errors::*;
|
||||
|
@ -14,7 +15,7 @@ pub use tests::*;
|
|||
#[automock]
|
||||
#[async_trait::async_trait]
|
||||
pub trait GetVerificationSecretOutDBPort: Send + Sync {
|
||||
async fn get_verification_secret(&self, username: &str) -> OutDBPortResult<String>;
|
||||
async fn get_verification_secret(&self, user_id: &Uuid) -> OutDBPortResult<String>;
|
||||
}
|
||||
|
||||
pub type GetVerificationSecretOutDBPortObj = std::sync::Arc<dyn GetVerificationSecretOutDBPort>;
|
||||
|
|
|
@ -7,5 +7,5 @@ pub mod delete_verification_secret;
|
|||
pub mod email_exists;
|
||||
pub mod errors;
|
||||
pub mod get_verification_secret;
|
||||
pub mod username_exists;
|
||||
pub mod user_id_exists;
|
||||
pub mod verification_secret_exists;
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::errors::*;
|
||||
#[cfg(test)]
|
||||
|
@ -12,11 +13,11 @@ pub use tests::*;
|
|||
|
||||
#[automock]
|
||||
#[async_trait::async_trait]
|
||||
pub trait UsernameExistsOutDBPort: Send + Sync {
|
||||
async fn username_exists(&self, username: &str) -> OutDBPortResult<bool>;
|
||||
pub trait UserIDExistsOutDBPort: Send + Sync {
|
||||
async fn user_id_exists(&self, user_id: &Uuid) -> OutDBPortResult<bool>;
|
||||
}
|
||||
|
||||
pub type UsernameExistsOutDBPortObj = std::sync::Arc<dyn UsernameExistsOutDBPort>;
|
||||
pub type UserIDExistsOutDBPortObj = std::sync::Arc<dyn UserIDExistsOutDBPort>;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
|
@ -24,17 +25,17 @@ pub mod tests {
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_username_exists_db_port(
|
||||
pub fn mock_user_id_exists_db_port(
|
||||
times: Option<usize>,
|
||||
returning: bool,
|
||||
) -> UsernameExistsOutDBPortObj {
|
||||
let mut m = MockUsernameExistsOutDBPort::new();
|
||||
) -> UserIDExistsOutDBPortObj {
|
||||
let mut m = MockUserIDExistsOutDBPort::new();
|
||||
if let Some(times) = times {
|
||||
m.expect_username_exists()
|
||||
m.expect_user_id_exists()
|
||||
.times(times)
|
||||
.returning(move |_| Ok(returning));
|
||||
} else {
|
||||
m.expect_username_exists().returning(move |_| Ok(returning));
|
||||
m.expect_user_id_exists().returning(move |_| Ok(returning));
|
||||
}
|
||||
|
||||
Arc::new(m)
|
|
@ -6,6 +6,7 @@ use derive_builder::Builder;
|
|||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use super::create_verification_secret::REGISTRATION_SECRET_PURPOSE;
|
||||
use super::errors::*;
|
||||
|
@ -16,7 +17,7 @@ pub use tests::*;
|
|||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Builder)]
|
||||
pub struct VerifySecretExistsMsg {
|
||||
pub secret: String,
|
||||
pub username: String,
|
||||
pub user_id: Uuid,
|
||||
}
|
||||
|
||||
#[automock]
|
||||
|
|
|
@ -16,7 +16,7 @@ pub trait AccountValidationLinkOutMailerPort: Send + Sync {
|
|||
async fn account_validation_link(
|
||||
&self,
|
||||
to: &str,
|
||||
username: &str,
|
||||
first_name: &str,
|
||||
validation_secret: &str,
|
||||
) -> OutMailerPortResult<()>;
|
||||
}
|
||||
|
|
|
@ -30,13 +30,10 @@ pub enum IdentityCommandError {
|
|||
impl From<CredsError> for IdentityCommandError {
|
||||
fn from(v: CredsError) -> Self {
|
||||
match v {
|
||||
CredsError::ProfainityError => Self::BadUsername(v.to_string()),
|
||||
CredsError::UsernameCaseMappedError => Self::BadUsername(v.to_string()),
|
||||
CredsError::BlacklistError => Self::BadUsername(v.to_string()),
|
||||
CredsError::NotAnEmail => Self::BadEmail,
|
||||
CredsError::PasswordTooShort => Self::BadPassowrd(v.to_string()),
|
||||
CredsError::PasswordTooLong => Self::BadPassowrd(v.to_string()),
|
||||
CredsError::Argon2Error(e) => Self::BadUsername(e.to_string()),
|
||||
_ => Self::BadUsername(v.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,29 +5,32 @@
|
|||
use super::*;
|
||||
use derive_getters::Getters;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
|
||||
pub struct MarkUserVerifiedCommand {
|
||||
username: String,
|
||||
user_id: Uuid,
|
||||
secret: String,
|
||||
}
|
||||
|
||||
impl MarkUserVerifiedCommand {
|
||||
pub fn new(username: String, secret: String) -> IdentityCommandResult<Self> {
|
||||
Ok(Self { username, secret })
|
||||
pub fn new(user_id: Uuid, secret: String) -> IdentityCommandResult<Self> {
|
||||
Ok(Self { user_id, secret })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::utils::uuid::tests::UUID;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cmd() {
|
||||
let username = "realaravinth";
|
||||
let user_id = UUID;
|
||||
let secret = "asdfasdf";
|
||||
let cmd = MarkUserVerifiedCommand::new(username.into(), secret.into()).unwrap();
|
||||
assert_eq!(cmd.username(), username);
|
||||
let cmd = MarkUserVerifiedCommand::new(user_id.clone(), secret.into()).unwrap();
|
||||
assert_eq!(cmd.user_id(), &user_id);
|
||||
assert_eq!(cmd.secret(), secret);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ impl MarkUserVerifiedUseCase for MarkUserVerifiedService {
|
|||
cmd: command::MarkUserVerifiedCommand,
|
||||
) -> IdentityResult<()> {
|
||||
let msg = VerifySecretExistsMsgBuilder::default()
|
||||
.username(cmd.username().into())
|
||||
.user_id(cmd.user_id().clone())
|
||||
.secret(cmd.secret().into())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
@ -48,13 +48,13 @@ impl MarkUserVerifiedUseCase for MarkUserVerifiedService {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::tests::bdd::*;
|
||||
use crate::{tests::bdd::*, utils::uuid::tests::UUID};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_service() {
|
||||
let username = "realaravinth";
|
||||
let user_id = UUID;
|
||||
let secret = "password";
|
||||
let cmd = command::MarkUserVerifiedCommand::new(username.into(), secret.into()).unwrap();
|
||||
let cmd = command::MarkUserVerifiedCommand::new(user_id.clone(), secret.into()).unwrap();
|
||||
|
||||
// happy case
|
||||
{
|
||||
|
|
|
@ -3,35 +3,45 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use super::*;
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Serialize, Deserialize, Builder, Eq, PartialEq, Ord, PartialOrd, Getters,
|
||||
)]
|
||||
pub struct UnvalidatedRegisterUserCommand {
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
email: String,
|
||||
password: String,
|
||||
confirm_password: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
|
||||
pub struct RegisterUserCommand {
|
||||
username: String,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
email: String,
|
||||
hashed_password: String,
|
||||
}
|
||||
|
||||
impl RegisterUserCommand {
|
||||
pub fn new(
|
||||
username: String,
|
||||
email: String,
|
||||
password: String,
|
||||
confirm_password: String,
|
||||
impl UnvalidatedRegisterUserCommand {
|
||||
pub fn validate(
|
||||
self,
|
||||
config: &argon2_creds::Config,
|
||||
) -> IdentityCommandResult<Self> {
|
||||
let username = config.username(&username)?;
|
||||
config.email(&email)?;
|
||||
) -> IdentityCommandResult<RegisterUserCommand> {
|
||||
config.email(&self.email)?;
|
||||
|
||||
if password != confirm_password {
|
||||
if self.password != self.confirm_password {
|
||||
return Err(IdentityCommandError::PasswordsDontMatch);
|
||||
}
|
||||
let hashed_password: String = config.password(&password)?;
|
||||
let hashed_password: String = config.password(&self.password)?;
|
||||
|
||||
Ok(Self {
|
||||
username,
|
||||
email,
|
||||
Ok(RegisterUserCommand {
|
||||
first_name: self.first_name,
|
||||
last_name: self.last_name,
|
||||
email: self.email,
|
||||
hashed_password,
|
||||
})
|
||||
}
|
||||
|
@ -44,49 +54,47 @@ mod tests {
|
|||
#[test]
|
||||
fn test_cmd() {
|
||||
let config = argon2_creds::Config::default();
|
||||
RegisterUserCommand::new(
|
||||
"realaravinth".into(),
|
||||
"realaravinth@example.com".into(),
|
||||
"asdfasdfasdfasdf".into(),
|
||||
"asdfasdfasdfasdf".into(),
|
||||
&config,
|
||||
)
|
||||
let first_name = "John";
|
||||
let last_name = "Doe";
|
||||
let email = "john@example.com";
|
||||
let password = "sadfasdfasdf";
|
||||
let wrong_password = "sadfasdfasdf--wrong";
|
||||
|
||||
UnvalidatedRegisterUserCommandBuilder::default()
|
||||
.first_name(first_name.into())
|
||||
.last_name(last_name.into())
|
||||
.email(email.into())
|
||||
.password(password.into())
|
||||
.confirm_password(password.into())
|
||||
.build()
|
||||
.unwrap()
|
||||
.validate(&config)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
RegisterUserCommand::new(
|
||||
"realaravinth".into(),
|
||||
"username".into(),
|
||||
"password".into(),
|
||||
"password".into(),
|
||||
&config,
|
||||
)
|
||||
.err(),
|
||||
Some(IdentityCommandError::BadEmail)
|
||||
UnvalidatedRegisterUserCommandBuilder::default()
|
||||
.first_name(first_name.into())
|
||||
.last_name(last_name.into())
|
||||
.email(first_name.into())
|
||||
.password(password.into())
|
||||
.confirm_password(password.into())
|
||||
.build()
|
||||
.unwrap()
|
||||
.validate(&config),
|
||||
Err(IdentityCommandError::BadEmail)
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
RegisterUserCommand::new(
|
||||
"username".into(),
|
||||
"username@example.com".into(),
|
||||
"password".into(),
|
||||
"password".into(),
|
||||
&config,
|
||||
)
|
||||
.err(),
|
||||
Some(IdentityCommandError::BadUsername(_))
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
RegisterUserCommand::new(
|
||||
"realaravinth".into(),
|
||||
"realaravinth@example.com".into(),
|
||||
"password".into(),
|
||||
"mismatch_password".into(),
|
||||
&config,
|
||||
)
|
||||
.err(),
|
||||
Some(IdentityCommandError::PasswordsDontMatch)
|
||||
UnvalidatedRegisterUserCommandBuilder::default()
|
||||
.first_name(first_name.into())
|
||||
.last_name(last_name.into())
|
||||
.email(email.into())
|
||||
.password(password.into())
|
||||
.confirm_password(wrong_password.into())
|
||||
.build()
|
||||
.unwrap()
|
||||
.validate(&config),
|
||||
Err(IdentityCommandError::PasswordsDontMatch)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,12 +5,15 @@
|
|||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Builder, Serialize, Deserialize, Getters, Eq, PartialEq, Ord, PartialOrd,
|
||||
)]
|
||||
pub struct UserRegisteredEvent {
|
||||
username: String,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
user_id: Uuid,
|
||||
email: String,
|
||||
hashed_password: String,
|
||||
is_verified: bool,
|
||||
|
|
|
@ -7,10 +7,10 @@ use derive_builder::Builder;
|
|||
|
||||
use super::*;
|
||||
use crate::identity::application::port::output::{
|
||||
db::{create_verification_secret::*, email_exists::*, username_exists::*},
|
||||
db::{create_verification_secret::*, email_exists::*, user_id_exists::*},
|
||||
mailer::account_validation_link::*,
|
||||
};
|
||||
use crate::utils::random_string::*;
|
||||
use crate::utils::{random_string::*, uuid::*};
|
||||
|
||||
pub const SECRET_LEN: usize = 20;
|
||||
pub const REGISTRATION_SECRET_PURPOSE: &str = "account_validation";
|
||||
|
@ -18,9 +18,10 @@ pub const REGISTRATION_SECRET_PURPOSE: &str = "account_validation";
|
|||
#[derive(Builder)]
|
||||
pub struct RegisterUserService {
|
||||
db_email_exists_adapter: EmailExistsOutDBPortObj,
|
||||
db_username_exists_adapter: UsernameExistsOutDBPortObj,
|
||||
db_user_id_exists_adapter: UserIDExistsOutDBPortObj,
|
||||
db_create_verification_secret_adapter: CreateVerificationSecretOutDBPortObj,
|
||||
mailer_account_validation_link_adapter: AccountValidationLinkOutMailerPortObj,
|
||||
get_uuid: GetUUIDInterfaceObj,
|
||||
random_string_adapter: GenerateRandomStringInterfaceObj,
|
||||
}
|
||||
|
||||
|
@ -30,15 +31,6 @@ impl RegisterUserUseCase for RegisterUserService {
|
|||
&self,
|
||||
cmd: command::RegisterUserCommand,
|
||||
) -> IdentityResult<events::UserRegisteredEvent> {
|
||||
if self
|
||||
.db_username_exists_adapter
|
||||
.username_exists(cmd.username())
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
return Err(IdentityError::DuplicateUsername);
|
||||
}
|
||||
|
||||
if self
|
||||
.db_email_exists_adapter
|
||||
.email_exists(cmd.email())
|
||||
|
@ -48,13 +40,27 @@ impl RegisterUserUseCase for RegisterUserService {
|
|||
return Err(IdentityError::DuplicateEmail);
|
||||
}
|
||||
|
||||
let mut user_id = self.get_uuid.get_uuid();
|
||||
loop {
|
||||
if self
|
||||
.db_user_id_exists_adapter
|
||||
.user_id_exists(&user_id)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
user_id = self.get_uuid.get_uuid();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let secret = self.random_string_adapter.get_random(SECRET_LEN);
|
||||
|
||||
self.db_create_verification_secret_adapter
|
||||
.create_verification_secret(
|
||||
CreateSecretMsgBuilder::default()
|
||||
.secret(secret.clone())
|
||||
.username(cmd.username().into())
|
||||
.user_id(user_id.clone())
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
|
@ -62,12 +68,14 @@ impl RegisterUserUseCase for RegisterUserService {
|
|||
.unwrap();
|
||||
|
||||
self.mailer_account_validation_link_adapter
|
||||
.account_validation_link(cmd.email(), cmd.username(), &secret)
|
||||
.account_validation_link(cmd.email(), cmd.first_name(), &secret)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(events::UserRegisteredEventBuilder::default()
|
||||
.username(cmd.username().into())
|
||||
.first_name(cmd.first_name().into())
|
||||
.last_name(cmd.last_name().into())
|
||||
.user_id(user_id)
|
||||
.email(cmd.email().into())
|
||||
.hashed_password(cmd.hashed_password().into())
|
||||
.is_verified(false)
|
||||
|
@ -84,6 +92,7 @@ mod tests {
|
|||
|
||||
use crate::tests::bdd::*;
|
||||
use crate::utils::random_string::tests::*;
|
||||
use crate::utils::uuid::tests::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_service() {
|
||||
|
@ -91,19 +100,19 @@ mod tests {
|
|||
let email = format!("{username}@example.com");
|
||||
let password = "password";
|
||||
let config = argon2_creds::Config::default();
|
||||
let cmd = command::RegisterUserCommand::new(
|
||||
username.into(),
|
||||
email.clone(),
|
||||
password.into(),
|
||||
password.into(),
|
||||
&config,
|
||||
)
|
||||
let cmd = command::UnvalidatedRegisterUserCommandBuilder::default()
|
||||
.first_name(username.into())
|
||||
.last_name(username.into())
|
||||
.email(email.into())
|
||||
.password(password.into())
|
||||
.confirm_password(password.into())
|
||||
.build()
|
||||
.unwrap()
|
||||
.validate(&config)
|
||||
.unwrap();
|
||||
|
||||
// happy case
|
||||
{
|
||||
let s = RegisterUserServiceBuilder::default()
|
||||
.db_username_exists_adapter(mock_username_exists_db_port(
|
||||
.db_user_id_exists_adapter(mock_user_id_exists_db_port(
|
||||
IS_CALLED_ONLY_ONCE,
|
||||
RETURNS_FALSE,
|
||||
))
|
||||
|
@ -121,57 +130,44 @@ mod tests {
|
|||
.mailer_account_validation_link_adapter(mock_account_validation_link_db_port(
|
||||
IS_CALLED_ONLY_ONCE,
|
||||
))
|
||||
.get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let res = s.register_user(cmd.clone()).await.unwrap();
|
||||
assert_eq!(res.username(), cmd.username());
|
||||
assert_eq!(res.first_name(), cmd.first_name());
|
||||
assert_eq!(res.last_name(), cmd.last_name());
|
||||
assert_eq!(res.user_id(), &UUID);
|
||||
assert_eq!(res.email(), cmd.email());
|
||||
assert!(!res.is_admin());
|
||||
assert!(argon2_creds::Config::verify(res.hashed_password(), password).unwrap())
|
||||
}
|
||||
|
||||
// username exists
|
||||
{
|
||||
let s = RegisterUserServiceBuilder::default()
|
||||
.db_username_exists_adapter(mock_username_exists_db_port(
|
||||
IS_CALLED_ONLY_ONCE,
|
||||
RETURNS_TRUE,
|
||||
))
|
||||
.db_email_exists_adapter(mock_email_exists_db_port(IS_NEVER_CALLED, RETURNS_FALSE))
|
||||
.db_create_verification_secret_adapter(mock_create_verification_secret_db_port(
|
||||
IS_NEVER_CALLED,
|
||||
))
|
||||
.random_string_adapter(mock_generate_random_string(
|
||||
IS_NEVER_CALLED,
|
||||
RETURNS_RANDOM_STRING.into(),
|
||||
))
|
||||
.mailer_account_validation_link_adapter(mock_account_validation_link_db_port(
|
||||
IS_NEVER_CALLED,
|
||||
))
|
||||
#[actix_rt::test]
|
||||
async fn test_service_email_exists() {
|
||||
let username = "realaravinth";
|
||||
let email = format!("{username}@example.com");
|
||||
let password = "password";
|
||||
let config = argon2_creds::Config::default();
|
||||
let cmd = command::UnvalidatedRegisterUserCommandBuilder::default()
|
||||
.first_name(username.into())
|
||||
.last_name(username.into())
|
||||
.email(email.into())
|
||||
.password(password.into())
|
||||
.confirm_password(password.into())
|
||||
.build()
|
||||
.unwrap()
|
||||
.validate(&config)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
s.register_user(cmd.clone()).await.err(),
|
||||
Some(IdentityError::DuplicateUsername)
|
||||
);
|
||||
}
|
||||
|
||||
// email exists
|
||||
{
|
||||
let s = RegisterUserServiceBuilder::default()
|
||||
.db_username_exists_adapter(mock_username_exists_db_port(
|
||||
IS_CALLED_ONLY_ONCE,
|
||||
.db_user_id_exists_adapter(mock_user_id_exists_db_port(
|
||||
IGNORE_CALL_COUNT,
|
||||
RETURNS_FALSE,
|
||||
))
|
||||
.db_create_verification_secret_adapter(mock_create_verification_secret_db_port(
|
||||
IS_NEVER_CALLED,
|
||||
))
|
||||
.db_email_exists_adapter(mock_email_exists_db_port(
|
||||
IS_CALLED_ONLY_ONCE,
|
||||
RETURNS_TRUE,
|
||||
))
|
||||
.db_email_exists_adapter(mock_email_exists_db_port(IS_CALLED_ONLY_ONCE, RETURNS_TRUE))
|
||||
.random_string_adapter(mock_generate_random_string(
|
||||
IS_NEVER_CALLED,
|
||||
RETURNS_RANDOM_STRING.into(),
|
||||
|
@ -179,6 +175,7 @@ mod tests {
|
|||
.mailer_account_validation_link_adapter(mock_account_validation_link_db_port(
|
||||
IS_NEVER_CALLED,
|
||||
))
|
||||
.get_uuid(mock_get_uuid(IS_NEVER_CALLED))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
|
@ -187,5 +184,4 @@ mod tests {
|
|||
Some(IdentityError::DuplicateEmail)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,54 +5,58 @@
|
|||
use super::*;
|
||||
use derive_getters::Getters;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
|
||||
pub struct ResendVerificationEmailCommand {
|
||||
username: String,
|
||||
user_id: Uuid,
|
||||
first_name: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
impl ResendVerificationEmailCommand {
|
||||
pub fn new(
|
||||
username: String,
|
||||
user_id: Uuid,
|
||||
first_name: String,
|
||||
email: String,
|
||||
config: &argon2_creds::Config,
|
||||
) -> IdentityCommandResult<Self> {
|
||||
let username = config.username(&username)?;
|
||||
config.email(&email)?;
|
||||
|
||||
Ok(Self { username, email })
|
||||
Ok(Self {
|
||||
user_id,
|
||||
first_name,
|
||||
email,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::utils::uuid::tests::UUID;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cmd() {
|
||||
let config = argon2_creds::Config::default();
|
||||
ResendVerificationEmailCommand::new(
|
||||
"realaravinth".into(),
|
||||
UUID.clone(),
|
||||
"john".into(),
|
||||
"realaravinth@example.com".into(),
|
||||
&config,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
ResendVerificationEmailCommand::new("realaravinth".into(), "username".into(), &config,)
|
||||
ResendVerificationEmailCommand::new(
|
||||
UUID.clone(),
|
||||
"john".into(),
|
||||
"john".into(),
|
||||
&config
|
||||
)
|
||||
.err(),
|
||||
Some(IdentityCommandError::BadEmail)
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
ResendVerificationEmailCommand::new(
|
||||
"username".into(),
|
||||
"username@example.com".into(),
|
||||
&config,
|
||||
)
|
||||
.err(),
|
||||
Some(IdentityCommandError::BadUsername(_))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,14 +6,13 @@ use derive_builder::Builder;
|
|||
|
||||
use super::*;
|
||||
use crate::identity::application::port::output::{
|
||||
db::{email_exists::*, get_verification_secret::*, username_exists::*},
|
||||
db::{email_exists::*, get_verification_secret::*},
|
||||
mailer::account_validation_link::*,
|
||||
};
|
||||
|
||||
#[derive(Builder)]
|
||||
pub struct ResendVerificationEmailService {
|
||||
db_email_exists_adapter: EmailExistsOutDBPortObj,
|
||||
db_username_exists_adapter: UsernameExistsOutDBPortObj,
|
||||
db_get_verification_secret_adapter: GetVerificationSecretOutDBPortObj,
|
||||
mailer_account_validation_link_adapter: AccountValidationLinkOutMailerPortObj,
|
||||
}
|
||||
|
@ -24,15 +23,6 @@ impl ResendVerificationEmailUseCase for ResendVerificationEmailService {
|
|||
&self,
|
||||
cmd: command::ResendVerificationEmailCommand,
|
||||
) -> IdentityResult<()> {
|
||||
if self
|
||||
.db_username_exists_adapter
|
||||
.username_exists(cmd.username())
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
return Err(IdentityError::DuplicateUsername);
|
||||
}
|
||||
|
||||
if self
|
||||
.db_email_exists_adapter
|
||||
.email_exists(cmd.email())
|
||||
|
@ -44,12 +34,12 @@ impl ResendVerificationEmailUseCase for ResendVerificationEmailService {
|
|||
|
||||
let secret = self
|
||||
.db_get_verification_secret_adapter
|
||||
.get_verification_secret(cmd.username())
|
||||
.get_verification_secret(cmd.user_id())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.mailer_account_validation_link_adapter
|
||||
.account_validation_link(cmd.email(), cmd.username(), &secret)
|
||||
.account_validation_link(cmd.email(), cmd.first_name(), &secret)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -62,25 +52,23 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
use crate::tests::bdd::*;
|
||||
use crate::utils::random_string::tests::*;
|
||||
use crate::utils::uuid::tests::UUID;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_service() {
|
||||
let username = "realaravinth";
|
||||
let email = format!("{username}@example.com");
|
||||
let user_id = UUID;
|
||||
let email = format!("john@example.com");
|
||||
let secret = "asdfasdf";
|
||||
let config = argon2_creds::Config::default();
|
||||
let cmd =
|
||||
command::ResendVerificationEmailCommand::new(username.into(), email.clone(), &config)
|
||||
let cmd = command::ResendVerificationEmailCommand::new(
|
||||
UUID.clone(),
|
||||
"john".into(),
|
||||
email.clone(),
|
||||
&config,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// happy case
|
||||
{
|
||||
let s = ResendVerificationEmailServiceBuilder::default()
|
||||
.db_username_exists_adapter(mock_username_exists_db_port(
|
||||
IS_CALLED_ONLY_ONCE,
|
||||
RETURNS_FALSE,
|
||||
))
|
||||
.db_get_verification_secret_adapter(mock_get_verification_secret_db_port(
|
||||
IS_CALLED_ONLY_ONCE,
|
||||
secret.into(),
|
||||
|
@ -97,46 +85,26 @@ mod tests {
|
|||
|
||||
s.resend_verification_email(cmd.clone()).await.unwrap();
|
||||
}
|
||||
|
||||
// username exists
|
||||
{
|
||||
let s = ResendVerificationEmailServiceBuilder::default()
|
||||
.db_username_exists_adapter(mock_username_exists_db_port(
|
||||
IS_CALLED_ONLY_ONCE,
|
||||
RETURNS_TRUE,
|
||||
))
|
||||
.db_email_exists_adapter(mock_email_exists_db_port(IS_NEVER_CALLED, RETURNS_FALSE))
|
||||
.db_get_verification_secret_adapter(mock_get_verification_secret_db_port(
|
||||
IS_NEVER_CALLED,
|
||||
secret.into(),
|
||||
))
|
||||
.mailer_account_validation_link_adapter(mock_account_validation_link_db_port(
|
||||
IS_NEVER_CALLED,
|
||||
))
|
||||
.build()
|
||||
#[actix_rt::test]
|
||||
async fn test_service_email_exists() {
|
||||
let user_id = UUID;
|
||||
let email = format!("john@example.com");
|
||||
let secret = "asdfasdf";
|
||||
let config = argon2_creds::Config::default();
|
||||
let cmd = command::ResendVerificationEmailCommand::new(
|
||||
UUID.clone(),
|
||||
"john".into(),
|
||||
email.clone(),
|
||||
&config,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
s.resend_verification_email(cmd.clone()).await.err(),
|
||||
Some(IdentityError::DuplicateUsername)
|
||||
);
|
||||
}
|
||||
|
||||
// email exists
|
||||
{
|
||||
let s = ResendVerificationEmailServiceBuilder::default()
|
||||
.db_username_exists_adapter(mock_username_exists_db_port(
|
||||
IS_CALLED_ONLY_ONCE,
|
||||
RETURNS_FALSE,
|
||||
))
|
||||
.db_get_verification_secret_adapter(mock_get_verification_secret_db_port(
|
||||
IS_NEVER_CALLED,
|
||||
secret.into(),
|
||||
))
|
||||
.db_email_exists_adapter(mock_email_exists_db_port(
|
||||
IS_CALLED_ONLY_ONCE,
|
||||
RETURNS_TRUE,
|
||||
))
|
||||
.db_email_exists_adapter(mock_email_exists_db_port(IS_CALLED_ONLY_ONCE, RETURNS_TRUE))
|
||||
.mailer_account_validation_link_adapter(mock_account_validation_link_db_port(
|
||||
IS_NEVER_CALLED,
|
||||
))
|
||||
|
@ -148,5 +116,4 @@ mod tests {
|
|||
Some(IdentityError::DuplicateEmail)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ impl SetAdminCommand {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::identity::domain::aggregate::UserBuilder;
|
||||
use crate::utils::uuid::tests::UUID;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -35,13 +36,15 @@ mod tests {
|
|||
|
||||
SetAdminCommand::new(
|
||||
UserBuilder::default()
|
||||
.username(username.into())
|
||||
.first_name(username.into())
|
||||
.last_name(username.into())
|
||||
.email(username.into())
|
||||
.hashed_password(username.into())
|
||||
.is_verified(true)
|
||||
.email_verified(false)
|
||||
.is_admin(true)
|
||||
.deleted(false)
|
||||
.user_id(UUID.clone())
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
|
@ -50,13 +53,15 @@ mod tests {
|
|||
assert_eq!(
|
||||
SetAdminCommand::new(
|
||||
UserBuilder::default()
|
||||
.username(username.into())
|
||||
.first_name(username.into())
|
||||
.last_name(username.into())
|
||||
.email(username.into())
|
||||
.hashed_password(username.into())
|
||||
.is_verified(true)
|
||||
.is_admin(false)
|
||||
.email_verified(false)
|
||||
.deleted(false)
|
||||
.user_id(UUID.clone())
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
|
|
|
@ -20,6 +20,7 @@ impl SetUserAdminUseCase for SetUserAdminService {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::identity::domain::aggregate::UserBuilder;
|
||||
use crate::utils::uuid::tests::UUID;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -29,13 +30,15 @@ mod tests {
|
|||
|
||||
let s = SetUserAdminService;
|
||||
let u = UserBuilder::default()
|
||||
.username(username.into())
|
||||
.first_name(username.into())
|
||||
.last_name(username.into())
|
||||
.email(username.into())
|
||||
.hashed_password(username.into())
|
||||
.is_verified(true)
|
||||
.email_verified(false)
|
||||
.is_admin(true)
|
||||
.deleted(false)
|
||||
.user_id(UUID.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
|
|
|
@ -4,19 +4,22 @@
|
|||
|
||||
use derive_getters::Getters;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters)]
|
||||
pub struct UpdateEmailCommand {
|
||||
new_email: String,
|
||||
username: String,
|
||||
user_id: Uuid,
|
||||
first_name: String,
|
||||
}
|
||||
|
||||
impl UpdateEmailCommand {
|
||||
pub fn new(
|
||||
username: String,
|
||||
new_email: String,
|
||||
user_id: Uuid,
|
||||
first_name: String,
|
||||
supplied_password: String,
|
||||
actual_password_hash: &str,
|
||||
config: &argon2_creds::Config,
|
||||
|
@ -27,27 +30,32 @@ impl UpdateEmailCommand {
|
|||
config.email(&new_email)?;
|
||||
|
||||
Ok(Self {
|
||||
username,
|
||||
user_id,
|
||||
new_email,
|
||||
first_name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::utils::uuid::tests::UUID;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cmd() {
|
||||
let config = argon2_creds::Config::default();
|
||||
let password = "adsfasdfasd";
|
||||
let username = "realaravinth";
|
||||
let first_name = "john";
|
||||
let user_id = UUID;
|
||||
let new_email = format!("newemail@example.com");
|
||||
let hashed_password = config.password(password).unwrap();
|
||||
assert_eq!(
|
||||
UpdateEmailCommand::new(
|
||||
username.into(),
|
||||
new_email.clone(),
|
||||
user_id.clone(),
|
||||
first_name.into(),
|
||||
password.into(),
|
||||
&hashed_password,
|
||||
&config
|
||||
|
@ -60,8 +68,9 @@ mod tests {
|
|||
// email is not valid email
|
||||
assert_eq!(
|
||||
UpdateEmailCommand::new(
|
||||
username.into(),
|
||||
username.into(),
|
||||
user_id.to_string(),
|
||||
user_id.clone(),
|
||||
first_name.into(),
|
||||
password.into(),
|
||||
&hashed_password,
|
||||
&config
|
||||
|
@ -73,9 +82,10 @@ mod tests {
|
|||
// wrong password
|
||||
assert_eq!(
|
||||
UpdateEmailCommand::new(
|
||||
username.into(),
|
||||
username.into(),
|
||||
username.into(),
|
||||
new_email.to_string(),
|
||||
user_id.clone(),
|
||||
first_name.into(),
|
||||
first_name.into(),
|
||||
&hashed_password,
|
||||
&config
|
||||
)
|
||||
|
|
|
@ -44,7 +44,7 @@ impl UpdateEmailUseCase for UpdateEmailService {
|
|||
.create_verification_secret(
|
||||
CreateSecretMsgBuilder::default()
|
||||
.secret(secret.clone())
|
||||
.username(cmd.username().into())
|
||||
.user_id(cmd.user_id().clone())
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
|
@ -52,7 +52,7 @@ impl UpdateEmailUseCase for UpdateEmailService {
|
|||
.unwrap();
|
||||
|
||||
self.mailer_account_validation_link_adapter
|
||||
.account_validation_link(cmd.new_email(), cmd.username(), &secret)
|
||||
.account_validation_link(cmd.new_email(), cmd.first_name(), &secret)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -66,18 +66,20 @@ mod tests {
|
|||
use crate::utils::random_string::tests::*;
|
||||
|
||||
use crate::tests::bdd::*;
|
||||
use crate::utils::uuid::tests::UUID;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_service() {
|
||||
let username = "realaravinth";
|
||||
let new_email = format!("{username}@example.com");
|
||||
let user_id = UUID;
|
||||
let new_email = format!("john@example.com");
|
||||
let password = "password";
|
||||
let config = argon2_creds::Config::default();
|
||||
let hashed_password = config.password(password).unwrap();
|
||||
|
||||
let cmd = command::UpdateEmailCommand::new(
|
||||
username.into(),
|
||||
new_email.clone(),
|
||||
user_id.clone(),
|
||||
"john".into(),
|
||||
password.into(),
|
||||
&hashed_password,
|
||||
&config,
|
||||
|
|
|
@ -2,15 +2,25 @@
|
|||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use async_trait::async_trait;
|
||||
use cqrs_es::Aggregate;
|
||||
use derive_builder::Builder;
|
||||
use derive_getters::Getters;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::identity::application::services::errors::*;
|
||||
use crate::identity::application::services::events::UserEvent;
|
||||
use crate::identity::application::services::UserCommand;
|
||||
use crate::identity::application::services::UserServicesInterface;
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder,
|
||||
)]
|
||||
pub struct User {
|
||||
username: String,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
user_id: Uuid,
|
||||
email: String,
|
||||
hashed_password: String,
|
||||
is_verified: bool,
|
||||
|
@ -22,13 +32,15 @@ pub struct User {
|
|||
impl Default for User {
|
||||
fn default() -> Self {
|
||||
User {
|
||||
username: "".to_string(),
|
||||
first_name: "".to_string(),
|
||||
last_name: "".to_string(),
|
||||
email: "".to_string(),
|
||||
hashed_password: "".to_string(),
|
||||
is_verified: false,
|
||||
is_admin: false,
|
||||
email_verified: false,
|
||||
deleted: false,
|
||||
user_id: Uuid::new_v4(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +76,101 @@ impl User {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Aggregate for User {
|
||||
type Command = UserCommand;
|
||||
type Event = UserEvent;
|
||||
type Error = IdentityError;
|
||||
type Services = std::sync::Arc<dyn UserServicesInterface>;
|
||||
|
||||
// This identifier should be unique to the system.
|
||||
fn aggregate_type() -> String {
|
||||
"account".to_string()
|
||||
}
|
||||
|
||||
// The aggregate logic goes here. Note that this will be the _bulk_ of a CQRS system
|
||||
// so expect to use helper functions elsewhere to keep the code clean.
|
||||
async fn handle(
|
||||
&self,
|
||||
command: Self::Command,
|
||||
services: &Self::Services,
|
||||
) -> Result<Vec<Self::Event>, Self::Error> {
|
||||
match command {
|
||||
UserCommand::RegisterUser(cmd) => {
|
||||
let res = services.register_user().register_user(cmd).await?;
|
||||
Ok(vec![UserEvent::UserRegistered(res)])
|
||||
}
|
||||
UserCommand::DeleteUser(cmd) => {
|
||||
services.delete_user().delete_user(cmd).await;
|
||||
Ok(vec![UserEvent::UserDeleted])
|
||||
}
|
||||
UserCommand::Login(cmd) => {
|
||||
let res = services.login().login(cmd).await;
|
||||
Ok(vec![UserEvent::Loggedin(res)])
|
||||
}
|
||||
UserCommand::UpdatePassword(cmd) => {
|
||||
let res = services.update_password().update_password(cmd).await;
|
||||
Ok(vec![UserEvent::PasswordUpdated(res)])
|
||||
}
|
||||
UserCommand::UpdateEmail(cmd) => {
|
||||
let res = services.update_email().update_email(cmd).await?;
|
||||
Ok(vec![UserEvent::EmailUpdated(res)])
|
||||
}
|
||||
UserCommand::MarkUserVerified(cmd) => {
|
||||
services
|
||||
.mark_user_verified()
|
||||
.mark_user_verified(cmd)
|
||||
.await?;
|
||||
Ok(vec![UserEvent::UserVerified])
|
||||
}
|
||||
UserCommand::SetAdmin(cmd) => {
|
||||
let res = services.set_user_admin().set_user_admin(cmd).await;
|
||||
Ok(vec![UserEvent::UserPromotedToAdmin(res)])
|
||||
}
|
||||
UserCommand::ResendVerificationEmail(cmd) => {
|
||||
services
|
||||
.resend_verification_email()
|
||||
.resend_verification_email(cmd)
|
||||
.await?;
|
||||
Ok(vec![UserEvent::VerificationEmailResent])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(&mut self, event: Self::Event) {
|
||||
match event {
|
||||
UserEvent::UserRegistered(e) => {
|
||||
self.first_name = e.first_name().into();
|
||||
self.last_name = e.last_name().into();
|
||||
self.user_id = e.user_id().clone();
|
||||
self.email = e.email().into();
|
||||
self.hashed_password = e.hashed_password().into();
|
||||
self.is_admin = e.is_admin().clone();
|
||||
self.email_verified = e.email_verified().clone();
|
||||
self.is_verified = e.is_verified().clone();
|
||||
self.deleted = false;
|
||||
}
|
||||
UserEvent::UserDeleted => {
|
||||
self.set_deleted(true);
|
||||
}
|
||||
UserEvent::Loggedin(_) => (),
|
||||
UserEvent::PasswordUpdated(_) => (),
|
||||
UserEvent::EmailUpdated(e) => {
|
||||
self.set_email(e.new_email().into());
|
||||
self.set_email_verified(false);
|
||||
}
|
||||
UserEvent::UserVerified => {
|
||||
self.set_is_verified(true);
|
||||
self.set_email_verified(true);
|
||||
}
|
||||
UserEvent::UserPromotedToAdmin(_) => {
|
||||
self.set_is_admin(true);
|
||||
}
|
||||
UserEvent::VerificationEmailResent => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -6,13 +6,14 @@ use async_trait::async_trait;
|
|||
use cqrs_es::persist::GenericQuery;
|
||||
use cqrs_es::persist::{PersistenceError, ViewContext, ViewRepository};
|
||||
use cqrs_es::{EventEnvelope, Query, View};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::errors::*;
|
||||
use super::InventoryDBPostgresAdapter;
|
||||
use crate::inventory::domain::category_aggregate::Category;
|
||||
use crate::inventory::domain::events::InventoryEvent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::utils::parse_aggregate_id::parse_aggregate_id;
|
||||
|
||||
pub const NEW_CATEGORY_NON_UUID: &str = "new_category_non_uuid-asdfa";
|
||||
|
||||
|
@ -46,8 +47,7 @@ impl View<Category> for CategoryView {
|
|||
#[async_trait]
|
||||
impl ViewRepository<CategoryView, Category> for InventoryDBPostgresAdapter {
|
||||
async fn load(&self, category_id: &str) -> Result<Option<CategoryView>, PersistenceError> {
|
||||
let category_id =
|
||||
match super::utils::parse_aggregate_id(category_id, NEW_CATEGORY_NON_UUID)? {
|
||||
let category_id = match parse_aggregate_id(category_id, NEW_CATEGORY_NON_UUID)? {
|
||||
Some((val, _)) => return Ok(Some(val)),
|
||||
None => Uuid::parse_str(category_id).unwrap(),
|
||||
};
|
||||
|
@ -72,8 +72,7 @@ impl ViewRepository<CategoryView, Category> for InventoryDBPostgresAdapter {
|
|||
&self,
|
||||
category_id: &str,
|
||||
) -> Result<Option<(CategoryView, ViewContext)>, PersistenceError> {
|
||||
let category_id =
|
||||
match super::utils::parse_aggregate_id(category_id, NEW_CATEGORY_NON_UUID)? {
|
||||
let category_id = match parse_aggregate_id(category_id, NEW_CATEGORY_NON_UUID)? {
|
||||
Some(val) => return Ok(Some(val)),
|
||||
None => Uuid::parse_str(category_id).unwrap(),
|
||||
};
|
||||
|
|
|
@ -15,7 +15,6 @@ mod errors;
|
|||
mod store_id_exists;
|
||||
mod store_name_exists;
|
||||
mod store_view;
|
||||
mod utils;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InventoryDBPostgresAdapter {
|
||||
|
|
|
@ -32,6 +32,8 @@ impl StoreIDExistsDBPort for InventoryDBPostgresAdapter {
|
|||
mod tests {
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::utils::uuid::tests::UUID;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
|
@ -47,7 +49,7 @@ mod tests {
|
|||
|
||||
let store = StoreBuilder::default()
|
||||
.name("store_name".into())
|
||||
.owner("store_owner".into())
|
||||
.owner(UUID.clone())
|
||||
.address(Some("store_address".into()))
|
||||
.store_id(store_id)
|
||||
.build()
|
||||
|
|
|
@ -32,6 +32,8 @@ impl StoreNameExistsDBPort for InventoryDBPostgresAdapter {
|
|||
mod tests {
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::utils::uuid::tests::UUID;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
|
@ -47,7 +49,7 @@ mod tests {
|
|||
|
||||
let store = StoreBuilder::default()
|
||||
.name("store_name".into())
|
||||
.owner("store_owner".into())
|
||||
.owner(UUID.clone())
|
||||
.address(Some("store_address".into()))
|
||||
.store_id(store_id)
|
||||
.build()
|
||||
|
|
|
@ -12,6 +12,7 @@ use super::errors::*;
|
|||
use super::InventoryDBPostgresAdapter;
|
||||
use crate::inventory::domain::events::InventoryEvent;
|
||||
use crate::inventory::domain::store_aggregate::Store;
|
||||
use crate::utils::parse_aggregate_id::parse_aggregate_id;
|
||||
|
||||
pub const NEW_STORE_NON_UUID: &str = "new_store_non_uuid-asdfa";
|
||||
|
||||
|
@ -22,7 +23,7 @@ pub struct StoreView {
|
|||
name: String,
|
||||
address: Option<String>,
|
||||
store_id: Uuid,
|
||||
owner: String,
|
||||
owner: Uuid,
|
||||
}
|
||||
|
||||
// This updates the view with events as they are committed.
|
||||
|
@ -45,7 +46,7 @@ impl View<Store> for StoreView {
|
|||
#[async_trait]
|
||||
impl ViewRepository<StoreView, Store> for InventoryDBPostgresAdapter {
|
||||
async fn load(&self, store_id: &str) -> Result<Option<StoreView>, PersistenceError> {
|
||||
let store_id = match super::utils::parse_aggregate_id(store_id, NEW_STORE_NON_UUID)? {
|
||||
let store_id = match parse_aggregate_id(store_id, NEW_STORE_NON_UUID)? {
|
||||
Some((val, _)) => return Ok(Some(val)),
|
||||
None => Uuid::parse_str(store_id).unwrap(),
|
||||
};
|
||||
|
@ -70,7 +71,7 @@ impl ViewRepository<StoreView, Store> for InventoryDBPostgresAdapter {
|
|||
&self,
|
||||
store_id: &str,
|
||||
) -> Result<Option<(StoreView, ViewContext)>, PersistenceError> {
|
||||
let store_id = match super::utils::parse_aggregate_id(store_id, NEW_STORE_NON_UUID)? {
|
||||
let store_id = match parse_aggregate_id(store_id, NEW_STORE_NON_UUID)? {
|
||||
Some(val) => return Ok(Some(val)),
|
||||
None => Uuid::parse_str(store_id).unwrap(),
|
||||
};
|
||||
|
@ -247,7 +248,7 @@ mod tests {
|
|||
))
|
||||
.add_category(mock_add_category_service(
|
||||
IS_NEVER_CALLED,
|
||||
AddCategoryCommand::new("foo".into(), None, UUID.clone(), "bar".into()).unwrap(),
|
||||
AddCategoryCommand::new("foo".into(), None, UUID.clone(), UUID.clone()).unwrap(),
|
||||
))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
@ -265,7 +266,7 @@ mod tests {
|
|||
);
|
||||
|
||||
let rand = crate::utils::random_string::GenerateRandomString {};
|
||||
let cmd = AddStoreCommand::new(rand.get_random(10), None, "me".into()).unwrap();
|
||||
let cmd = AddStoreCommand::new(rand.get_random(10), None, UUID.clone()).unwrap();
|
||||
cqrs.execute("", InventoryCommand::AddStore(cmd.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -78,7 +78,7 @@ impl AddCategoryUseCase for AddCategoryService {
|
|||
Ok(CategoryAddedEventBuilder::default()
|
||||
.name(category.name().into())
|
||||
.description(category.description().as_ref().map(|s| s.to_string()))
|
||||
.added_by_user(cmd.adding_by().into())
|
||||
.added_by_user(cmd.adding_by().clone())
|
||||
.store_id(category.store_id().clone())
|
||||
.category_id(category.category_id().clone())
|
||||
.build()
|
||||
|
@ -104,7 +104,7 @@ pub mod tests {
|
|||
let res = CategoryAddedEventBuilder::default()
|
||||
.name(cmd.name().into())
|
||||
.description(cmd.description().as_ref().map(|s| s.to_string()))
|
||||
.added_by_user(cmd.adding_by().into())
|
||||
.added_by_user(cmd.adding_by().clone())
|
||||
.store_id(cmd.store_id().clone())
|
||||
.category_id(UUID.clone())
|
||||
.build()
|
||||
|
@ -125,7 +125,7 @@ pub mod tests {
|
|||
async fn test_service_category_doesnt_exist() {
|
||||
let name = "foo";
|
||||
let description = "bar";
|
||||
let username = "baz";
|
||||
let user_id = UUID;
|
||||
let store_id = Uuid::new_v4();
|
||||
|
||||
// description = None
|
||||
|
@ -133,7 +133,7 @@ pub mod tests {
|
|||
name.into(),
|
||||
Some(description.into()),
|
||||
store_id.clone(),
|
||||
username.into(),
|
||||
user_id.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -158,7 +158,7 @@ pub mod tests {
|
|||
async fn test_service_category_name_exists_for_store() {
|
||||
let name = "foo";
|
||||
let description = "bar";
|
||||
let username = "baz";
|
||||
let user_id = UUID;
|
||||
let store_id = Uuid::new_v4();
|
||||
|
||||
// description = None
|
||||
|
@ -166,7 +166,7 @@ pub mod tests {
|
|||
name.into(),
|
||||
Some(description.into()),
|
||||
store_id.clone(),
|
||||
username.into(),
|
||||
user_id.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -175,7 +175,7 @@ pub mod tests {
|
|||
IS_CALLED_ONLY_ONCE,
|
||||
))
|
||||
.db_category_id_exists(mock_category_id_exists_db_port_false(IS_NEVER_CALLED))
|
||||
.get_uuid(mock_get_uuid(IS_NEVER_CALLED))
|
||||
.get_uuid(mock_get_uuid(IS_CALLED_ONLY_ONCE))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ impl AddStoreUseCase for AddStoreService {
|
|||
let mut store = StoreBuilder::default()
|
||||
.name(cmd.name().into())
|
||||
.address(cmd.address().as_ref().map(|s| s.to_string()))
|
||||
.owner(cmd.owner().into())
|
||||
.owner(cmd.owner().clone())
|
||||
.store_id(store_id.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
@ -58,6 +58,7 @@ impl AddStoreUseCase for AddStoreService {
|
|||
store = StoreBuilder::default()
|
||||
.name(cmd.name().into())
|
||||
.address(cmd.address().as_ref().map(|s| s.to_string()))
|
||||
.owner(cmd.owner().clone())
|
||||
.store_id(store_id.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
@ -70,7 +71,7 @@ impl AddStoreUseCase for AddStoreService {
|
|||
Ok(StoreAddedEventBuilder::default()
|
||||
.name(store.name().into())
|
||||
.address(store.address().as_ref().map(|s| s.to_string()))
|
||||
.owner(cmd.owner().into())
|
||||
.owner(cmd.owner().clone())
|
||||
.store_id(store_id.clone())
|
||||
.build()
|
||||
.unwrap())
|
||||
|
@ -93,7 +94,7 @@ pub mod tests {
|
|||
let res = StoreAddedEventBuilder::default()
|
||||
.name(cmd.name().into())
|
||||
.address(cmd.address().as_ref().map(|s| s.to_string()))
|
||||
.owner(cmd.owner().into())
|
||||
.owner(cmd.owner().clone())
|
||||
.store_id(UUID.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
@ -113,10 +114,10 @@ pub mod tests {
|
|||
async fn test_service_store_id_doesnt_exist() {
|
||||
let name = "foo";
|
||||
let address = "bar";
|
||||
let username = "baz";
|
||||
let owner = UUID;
|
||||
|
||||
// address = None
|
||||
let cmd = AddStoreCommand::new(name.into(), Some(address.into()), username.into()).unwrap();
|
||||
let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner.clone()).unwrap();
|
||||
|
||||
let s = AddStoreServiceBuilder::default()
|
||||
.db_store_id_exists(mock_store_id_exists_db_port_false(IS_CALLED_ONLY_ONCE))
|
||||
|
@ -136,10 +137,10 @@ pub mod tests {
|
|||
async fn test_service_store_name_exists() {
|
||||
let name = "foo";
|
||||
let address = "bar";
|
||||
let username = "baz";
|
||||
let owner = UUID;
|
||||
|
||||
// address = None
|
||||
let cmd = AddStoreCommand::new(name.into(), Some(address.into()), username.into()).unwrap();
|
||||
let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner.clone()).unwrap();
|
||||
|
||||
let s = AddStoreServiceBuilder::default()
|
||||
.db_store_id_exists(mock_store_id_exists_db_port_false(IS_NEVER_CALLED))
|
||||
|
|
|
@ -17,7 +17,7 @@ pub struct AddCategoryCommand {
|
|||
name: String,
|
||||
description: Option<String>,
|
||||
store_id: Uuid,
|
||||
adding_by: String,
|
||||
adding_by: Uuid,
|
||||
}
|
||||
|
||||
impl AddCategoryCommand {
|
||||
|
@ -25,7 +25,7 @@ impl AddCategoryCommand {
|
|||
name: String,
|
||||
description: Option<String>,
|
||||
store_id: Uuid,
|
||||
adding_by: String,
|
||||
adding_by: Uuid,
|
||||
) -> Result<Self, AddCategoryCommandError> {
|
||||
let description: Option<String> = if let Some(description) = description {
|
||||
let description = description.trim();
|
||||
|
@ -56,19 +56,21 @@ impl AddCategoryCommand {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::utils::uuid::tests::UUID;
|
||||
|
||||
#[test]
|
||||
fn test_cmd() {
|
||||
let name = "foo";
|
||||
let description = "bar";
|
||||
let username = "baz";
|
||||
let adding_by = UUID;
|
||||
let store_id = Uuid::new_v4();
|
||||
|
||||
// description = None
|
||||
let cmd =
|
||||
AddCategoryCommand::new(name.into(), None, store_id.clone(), username.into()).unwrap();
|
||||
let cmd = AddCategoryCommand::new(name.into(), None, store_id.clone(), adding_by.clone())
|
||||
.unwrap();
|
||||
assert_eq!(cmd.name(), name);
|
||||
assert_eq!(cmd.description(), &None);
|
||||
assert_eq!(cmd.adding_by(), username);
|
||||
assert_eq!(cmd.adding_by(), &adding_by);
|
||||
assert_eq!(cmd.store_id(), &store_id);
|
||||
|
||||
// description = Some
|
||||
|
@ -76,12 +78,12 @@ mod tests {
|
|||
name.into(),
|
||||
Some(description.into()),
|
||||
store_id.clone(),
|
||||
username.into(),
|
||||
adding_by.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(cmd.name(), name);
|
||||
assert_eq!(cmd.description(), &Some(description.to_owned()));
|
||||
assert_eq!(cmd.adding_by(), username);
|
||||
assert_eq!(cmd.adding_by(), &adding_by);
|
||||
assert_eq!(cmd.store_id(), &store_id);
|
||||
|
||||
// AddCategoryCommandError::NameIsEmpty
|
||||
|
@ -90,7 +92,7 @@ mod tests {
|
|||
"".into(),
|
||||
Some(description.into()),
|
||||
store_id.clone(),
|
||||
username.into()
|
||||
adding_by.clone(),
|
||||
),
|
||||
Err(AddCategoryCommandError::NameIsEmpty)
|
||||
)
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
use derive_getters::Getters;
|
||||
use derive_more::{Display, Error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Error, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum AddStoreCommandError {
|
||||
|
@ -15,14 +16,14 @@ pub enum AddStoreCommandError {
|
|||
pub struct AddStoreCommand {
|
||||
name: String,
|
||||
address: Option<String>,
|
||||
owner: String,
|
||||
owner: Uuid,
|
||||
}
|
||||
|
||||
impl AddStoreCommand {
|
||||
pub fn new(
|
||||
name: String,
|
||||
address: Option<String>,
|
||||
owner: String,
|
||||
owner: Uuid,
|
||||
) -> Result<Self, AddStoreCommandError> {
|
||||
let address: Option<String> = if let Some(address) = address {
|
||||
let address = address.trim();
|
||||
|
@ -50,29 +51,31 @@ impl AddStoreCommand {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::utils::uuid::tests::UUID;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cmd() {
|
||||
let name = "foo";
|
||||
let address = "bar";
|
||||
let username = "baz";
|
||||
let owner = UUID.clone();
|
||||
|
||||
// address = None
|
||||
let cmd = AddStoreCommand::new(name.into(), None, username.into()).unwrap();
|
||||
let cmd = AddStoreCommand::new(name.into(), None, owner.clone()).unwrap();
|
||||
assert_eq!(cmd.name(), name);
|
||||
assert_eq!(cmd.address(), &None);
|
||||
assert_eq!(cmd.owner(), username);
|
||||
assert_eq!(cmd.owner(), &owner);
|
||||
|
||||
// address = Some
|
||||
let cmd = AddStoreCommand::new(name.into(), Some(address.into()), username.into()).unwrap();
|
||||
let cmd = AddStoreCommand::new(name.into(), Some(address.into()), owner.clone()).unwrap();
|
||||
assert_eq!(cmd.name(), name);
|
||||
assert_eq!(cmd.address(), &Some(address.to_owned()));
|
||||
assert_eq!(cmd.owner(), username);
|
||||
assert_eq!(cmd.owner(), &owner);
|
||||
|
||||
// AddStoreCommandError::NameIsEmpty
|
||||
assert_eq!(
|
||||
AddStoreCommand::new("".into(), Some(address.into()), username.into()),
|
||||
AddStoreCommand::new("".into(), Some(address.into()), owner.clone()),
|
||||
Err(AddStoreCommandError::NameIsEmpty)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ use uuid::Uuid;
|
|||
pub struct CategoryAddedEvent {
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
added_by_user: String,
|
||||
added_by_user: Uuid,
|
||||
category_id: Uuid,
|
||||
store_id: Uuid,
|
||||
}
|
||||
|
|
|
@ -92,7 +92,7 @@ mod aggregate_tests {
|
|||
fn test_create_store() {
|
||||
let name = "category_name";
|
||||
let description = Some("category_description".to_string());
|
||||
let adding_by = "store_owner";
|
||||
let adding_by = UUID;
|
||||
let store_id = Uuid::new_v4();
|
||||
let category_id = UUID.clone();
|
||||
|
||||
|
@ -100,14 +100,14 @@ mod aggregate_tests {
|
|||
name.into(),
|
||||
description.clone(),
|
||||
store_id.clone(),
|
||||
adding_by.into(),
|
||||
adding_by.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let expected = CategoryAddedEventBuilder::default()
|
||||
.name(cmd.name().into())
|
||||
.description(cmd.description().as_ref().map(|s| s.to_string()))
|
||||
.added_by_user(cmd.adding_by().into())
|
||||
.added_by_user(cmd.adding_by().clone())
|
||||
.store_id(cmd.store_id().clone())
|
||||
.category_id(category_id.clone())
|
||||
.build()
|
||||
|
|
|
@ -13,6 +13,6 @@ use uuid::Uuid;
|
|||
pub struct StoreAddedEvent {
|
||||
name: String,
|
||||
address: Option<String>,
|
||||
owner: String,
|
||||
owner: Uuid,
|
||||
store_id: Uuid,
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ use super::{commands::InventoryCommand, events::InventoryEvent};
|
|||
pub struct Store {
|
||||
name: String,
|
||||
address: Option<String>,
|
||||
owner: String,
|
||||
owner: Uuid,
|
||||
store_id: Uuid,
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,7 @@ impl Aggregate for Store {
|
|||
InventoryEvent::StoreAdded(e) => {
|
||||
self.name = e.name().into();
|
||||
self.address = e.address().as_ref().map(|s| s.to_string());
|
||||
self.owner = e.owner().into();
|
||||
self.owner = e.owner().clone();
|
||||
self.store_id = e.store_id().clone();
|
||||
}
|
||||
_ => (),
|
||||
|
@ -95,7 +95,7 @@ mod tests {
|
|||
fn test_create_store() {
|
||||
let name = "store_name";
|
||||
let address = Some("store_address".to_string());
|
||||
let owner = "store_owner";
|
||||
let owner = UUID;
|
||||
let store_id = UUID;
|
||||
|
||||
let expected = StoreAddedEventBuilder::default()
|
||||
|
@ -107,7 +107,7 @@ mod tests {
|
|||
.unwrap();
|
||||
let expected = InventoryEvent::StoreAdded(expected);
|
||||
|
||||
let cmd = AddStoreCommand::new(name.into(), address.clone(), owner.into()).unwrap();
|
||||
let cmd = AddStoreCommand::new(name.into(), address.clone(), owner.clone()).unwrap();
|
||||
|
||||
let mut services = MockInventoryServicesInterface::new();
|
||||
services
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub const IS_CALLED_ONLY_ONCE: Option<usize> = Some(1);
|
||||
pub const IS_NEVER_CALLED: Option<usize> = None;
|
||||
pub const IS_NEVER_CALLED: Option<usize> = Some(0);
|
||||
pub const IS_CALLED_ONLY_TWICE: Option<usize> = Some(2);
|
||||
pub const RETURNS_RANDOM_STRING: &str = "test_random_string";
|
||||
pub const IGNORE_CALL_COUNT: Option<usize> = None;
|
||||
|
||||
pub const RETURNS_TRUE: bool = true;
|
||||
pub const RETURNS_FALSE: bool = false;
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub mod parse_aggregate_id;
|
||||
pub mod random_string;
|
||||
pub mod uuid;
|
||||
|
|
Loading…
Reference in a new issue