From d71291630528885341f73a35e659114286104261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Jacobo=20Vi=C3=B1a=20Rebolledo?= Date: Thu, 14 Dec 2023 01:57:31 +0100 Subject: [PATCH] [GH-38] feat(User): add value objects --- package-lock.json | 18 +++++ package.json | 1 + .../Auth/application/LoginUserRequest.ts | 4 + .../Auth/application/RegisterUserRequest.ts | 5 ++ src/Contexts/apiApp/Auth/domain/User.ts | 75 +++++++++++++++++++ src/Contexts/apiApp/Auth/domain/UserPatch.ts | 57 ++++++++++++++ .../apiApp/Auth/domain/UserRepository.ts | 11 +++ src/Contexts/apiApp/Auth/domain/UserRoles.ts | 21 ++++++ src/Contexts/apiApp/Auth/domain/Username.ts | 25 +++++++ src/Contexts/apiApp/Auth/domain/index.ts | 7 ++ .../domain/errors/InvalidArgumentError.ts | 7 +- .../shared/domain/value-object/Email.ts | 61 +++++++++++++++ .../domain/value-object/StringValueObject.ts | 12 ++- .../shared/domain/value-object/Uuid.ts | 6 ++ .../Contexts/apiApp/Auth/domain/User.test.ts | 49 ++++++++++++ .../apiApp/Auth/domain/UserName.test.ts | 18 +++++ .../apiApp/Auth/domain/UserRoles.test.ts | 17 +++++ .../apiApp/Auth/domain/mothers/UserMother.ts | 63 ++++++++++++++++ .../Auth/domain/mothers/UserRolesMother.ts | 12 +++ .../apiApp/Auth/domain/mothers/index.ts | 2 + tests/Contexts/fixtures/shared/random.ts | 6 +- tests/Contexts/shared/domain/Email.test.ts | 42 +++++++++++ .../shared/domain/mothers/EmailMother.ts | 12 +++ 23 files changed, 528 insertions(+), 3 deletions(-) create mode 100644 src/Contexts/apiApp/Auth/application/LoginUserRequest.ts create mode 100644 src/Contexts/apiApp/Auth/application/RegisterUserRequest.ts create mode 100644 src/Contexts/apiApp/Auth/domain/User.ts create mode 100644 src/Contexts/apiApp/Auth/domain/UserPatch.ts create mode 100644 src/Contexts/apiApp/Auth/domain/UserRepository.ts create mode 100644 src/Contexts/apiApp/Auth/domain/UserRoles.ts create mode 100644 src/Contexts/apiApp/Auth/domain/Username.ts create mode 100644 src/Contexts/apiApp/Auth/domain/index.ts create mode 100644 src/Contexts/shared/domain/value-object/Email.ts create mode 100644 tests/Contexts/apiApp/Auth/domain/User.test.ts create mode 100644 tests/Contexts/apiApp/Auth/domain/UserName.test.ts create mode 100644 tests/Contexts/apiApp/Auth/domain/UserRoles.test.ts create mode 100644 tests/Contexts/apiApp/Auth/domain/mothers/UserMother.ts create mode 100644 tests/Contexts/apiApp/Auth/domain/mothers/UserRolesMother.ts create mode 100644 tests/Contexts/apiApp/Auth/domain/mothers/index.ts create mode 100644 tests/Contexts/shared/domain/Email.test.ts create mode 100644 tests/Contexts/shared/domain/mothers/EmailMother.ts diff --git a/package-lock.json b/package-lock.json index fab256c..a6031db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "swagger-ui-express": "^5.0.0", "ts-node": "^10.9.1", "typescript": "^5.2.2", + "uuid": "^9.0.1", "uuid-validate": "^0.0.3", "winston": "^3.11.0", "winston-mongodb": "^5.1.1", @@ -12783,6 +12784,18 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/uuid-validate": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/uuid-validate/-/uuid-validate-0.0.3.tgz", @@ -23066,6 +23079,11 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + }, "uuid-validate": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/uuid-validate/-/uuid-validate-0.0.3.tgz", diff --git a/package.json b/package.json index 09fe696..a004714 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "swagger-ui-express": "^5.0.0", "ts-node": "^10.9.1", "typescript": "^5.2.2", + "uuid": "^9.0.1", "uuid-validate": "^0.0.3", "winston": "^3.11.0", "winston-mongodb": "^5.1.1", diff --git a/src/Contexts/apiApp/Auth/application/LoginUserRequest.ts b/src/Contexts/apiApp/Auth/application/LoginUserRequest.ts new file mode 100644 index 0000000..8574e0e --- /dev/null +++ b/src/Contexts/apiApp/Auth/application/LoginUserRequest.ts @@ -0,0 +1,4 @@ +export interface LoginUserRequest { + email: string; + password: string; +} diff --git a/src/Contexts/apiApp/Auth/application/RegisterUserRequest.ts b/src/Contexts/apiApp/Auth/application/RegisterUserRequest.ts new file mode 100644 index 0000000..211ef82 --- /dev/null +++ b/src/Contexts/apiApp/Auth/application/RegisterUserRequest.ts @@ -0,0 +1,5 @@ +import { LoginUserRequest } from './LoginUserRequest'; + +export interface RegisterUserRequest extends LoginUserRequest { + username: string; +} diff --git a/src/Contexts/apiApp/Auth/domain/User.ts b/src/Contexts/apiApp/Auth/domain/User.ts new file mode 100644 index 0000000..8fd3c9e --- /dev/null +++ b/src/Contexts/apiApp/Auth/domain/User.ts @@ -0,0 +1,75 @@ +import { AggregateRoot } from '../../../shared/domain/AggregateRoot'; +import { StringValueObject } from '../../../shared/domain/value-object/StringValueObject'; +import { Uuid } from '../../../shared/domain/value-object/Uuid'; +import { Email } from '../../../shared/domain/value-object/Email'; +import { UserRoles } from './UserRoles'; +import { Username } from './Username'; + +export class User extends AggregateRoot { + readonly id: Uuid; + readonly email: Email; + readonly username: Username; + readonly password: StringValueObject; + readonly emailValidated: boolean; + readonly roles: UserRoles; + + constructor({ + id, + email, + username, + password, + emailValidated, + roles + }: { + id: Uuid; + email: Email; + username: Username; + password: StringValueObject; + emailValidated: boolean; + roles: UserRoles; + }) { + super(); + this.id = id; + this.email = email; + this.username = username; + this.password = password; + this.emailValidated = emailValidated; + this.roles = roles; + } + + toPrimitives(): Record { + return { + id: this.id.value, + email: this.email.value, + username: this.username.value, + password: this.password.value, + emailValidated: this.emailValidated, + roles: this.roles.value + }; + } + + static fromPrimitives({ + id, + email, + username, + password, + emailValidated, + roles + }: { + id: string; + email: string; + username: string; + password: string; + emailValidated: boolean; + roles: string[]; + }): User { + return new User({ + id: new Uuid(id), + email: new Email(email), + username: new Username(username), + password: new StringValueObject(password), + emailValidated, + roles: new UserRoles(roles) + }); + } +} diff --git a/src/Contexts/apiApp/Auth/domain/UserPatch.ts b/src/Contexts/apiApp/Auth/domain/UserPatch.ts new file mode 100644 index 0000000..71ca0f1 --- /dev/null +++ b/src/Contexts/apiApp/Auth/domain/UserPatch.ts @@ -0,0 +1,57 @@ +import { AggregateRoot } from '../../../shared/domain/AggregateRoot'; +import { StringValueObject } from '../../../shared/domain/value-object/StringValueObject'; +import { Uuid } from '../../../shared/domain/value-object/Uuid'; +import { UserRoles } from './UserRoles'; + +export class UserPatch extends AggregateRoot { + readonly id: Uuid; + readonly password?: StringValueObject; + readonly emailValidated?: boolean; + readonly roles?: UserRoles; + + constructor({ + id, + password, + emailValidated, + roles + }: { + id: Uuid; + password?: StringValueObject; + emailValidated?: boolean; + roles?: UserRoles; + }) { + super(); + this.id = id; + password && (this.password = password); + emailValidated && (this.emailValidated = emailValidated); + roles && (this.roles = roles); + } + + toPrimitives() { + return { + id: this.id.value, + ...(this.password?.value && { password: this.password.value }), + ...(this.emailValidated && { emailValidated: this.emailValidated }), + ...(this.roles?.value && { roles: this.roles.value }) + }; + } + + static fromPrimitives({ + id, + password, + emailValidated, + roles + }: { + id: string; + password?: string; + emailValidated?: boolean; + roles?: string[]; + }) { + return new UserPatch({ + id: new Uuid(id), + ...(password && { password: new StringValueObject(password) }), + ...(emailValidated && { emailValidated }), + ...(roles && { roles: new UserRoles(roles) }) + }); + } +} diff --git a/src/Contexts/apiApp/Auth/domain/UserRepository.ts b/src/Contexts/apiApp/Auth/domain/UserRepository.ts new file mode 100644 index 0000000..b718c9e --- /dev/null +++ b/src/Contexts/apiApp/Auth/domain/UserRepository.ts @@ -0,0 +1,11 @@ +import { Nullable } from '../../../shared/domain/Nullable'; +import { User } from './User'; +import { UserPatch } from './UserPatch'; + +export interface UserRepository { + save(user: User): Promise; + + update(user: UserPatch): Promise; + + search(email: string): Promise>; +} diff --git a/src/Contexts/apiApp/Auth/domain/UserRoles.ts b/src/Contexts/apiApp/Auth/domain/UserRoles.ts new file mode 100644 index 0000000..d2c1628 --- /dev/null +++ b/src/Contexts/apiApp/Auth/domain/UserRoles.ts @@ -0,0 +1,21 @@ +import { InvalidArgumentError } from '../../../shared/domain/errors/InvalidArgumentError'; + +export class UserRoles { + private static validRoles = ['admin', 'user']; + readonly value: string[]; + + constructor(value: string[]) { + this.ensureRoles(value); + this.value = value; + } + + private ensureRoles(value: string[]): void { + value.forEach((role) => { + if (!UserRoles.validRoles.includes(role)) { + throw new InvalidArgumentError( + `<${this.constructor.name}> does not allow the value <${role}>` + ); + } + }); + } +} diff --git a/src/Contexts/apiApp/Auth/domain/Username.ts b/src/Contexts/apiApp/Auth/domain/Username.ts new file mode 100644 index 0000000..bf45013 --- /dev/null +++ b/src/Contexts/apiApp/Auth/domain/Username.ts @@ -0,0 +1,25 @@ +import { InvalidArgumentError } from '../../../shared/domain/errors/InvalidArgumentError'; +import { StringValueObject } from '../../../shared/domain/value-object/StringValueObject'; + +export class Username extends StringValueObject { + constructor(value: string) { + super(value); + this.ensureLength(value); + } + + private ensureLength(value: string): string { + if (value.length < 4) { + throw new InvalidArgumentError( + ' must be at least 4 characters long' + ); + } + + if (value.length > 20) { + throw new InvalidArgumentError( + ' must be less than 20 characters long' + ); + } + + return value; + } +} diff --git a/src/Contexts/apiApp/Auth/domain/index.ts b/src/Contexts/apiApp/Auth/domain/index.ts new file mode 100644 index 0000000..13b8409 --- /dev/null +++ b/src/Contexts/apiApp/Auth/domain/index.ts @@ -0,0 +1,7 @@ +export * from './User'; +export * from '../../../shared/domain/value-object/Email'; +export * from './UserRoles'; +export * from './Username'; +export * from './UserPatch'; + +export * from './UserRepository'; diff --git a/src/Contexts/shared/domain/errors/InvalidArgumentError.ts b/src/Contexts/shared/domain/errors/InvalidArgumentError.ts index 1b49f05..7e991fa 100644 --- a/src/Contexts/shared/domain/errors/InvalidArgumentError.ts +++ b/src/Contexts/shared/domain/errors/InvalidArgumentError.ts @@ -1 +1,6 @@ -export class InvalidArgumentError extends Error {} +export class InvalidArgumentError extends Error { + constructor(message: string) { + super(message); + this.name = 'InvalidArgumentError'; + } +} diff --git a/src/Contexts/shared/domain/value-object/Email.ts b/src/Contexts/shared/domain/value-object/Email.ts new file mode 100644 index 0000000..dae5fbd --- /dev/null +++ b/src/Contexts/shared/domain/value-object/Email.ts @@ -0,0 +1,61 @@ +import { InvalidArgumentError } from '../errors/InvalidArgumentError'; + +export class Email { + private readonly emailRegex = /^[a-zA-Z0-9._-]+@[a-z0-9.-]+\.[a-z]{2,6}$/; + private readonly minLength = 6; + private readonly maxLength = 255; + private readonly domainsBlacklist = [ + 'mailinator.com', + 'guerrillamail.com', + 'sharklasers.com' + ]; + readonly value: string; + + constructor(value: string) { + this.value = this.ensureIsValidEmail(value); + } + + private ensureLength(value: string): string { + if (value.length < this.minLength) { + throw new InvalidArgumentError( + `<${this.constructor.name}> must be at least ${this.minLength} characters long` + ); + } + + if (value.length > this.maxLength) { + throw new InvalidArgumentError( + `<${this.constructor.name}> must be less than ${this.maxLength} characters long` + ); + } + + return value; + } + + private ensureDomainsBlacklist(value: string): string { + const domain = value.split('@')[1]; + if (this.domainsBlacklist.includes(domain)) { + throw new InvalidArgumentError( + `<${this.constructor.name}> does not allow the domain <${value}>` + ); + } + return value; + } + + private ensureIsEmailAddress(value: string): string { + if (!this.emailRegex.test(value)) { + throw new InvalidArgumentError( + `<${this.constructor.name}> does not allow the value <${value}>` + ); + } + return value; + } + + private ensureIsValidEmail(value: string): string { + const trimmedValue = value.trim(); + this.ensureLength(trimmedValue); + this.ensureIsEmailAddress(trimmedValue); + this.ensureDomainsBlacklist(trimmedValue); + + return trimmedValue; + } +} diff --git a/src/Contexts/shared/domain/value-object/StringValueObject.ts b/src/Contexts/shared/domain/value-object/StringValueObject.ts index 7edb726..44f8589 100644 --- a/src/Contexts/shared/domain/value-object/StringValueObject.ts +++ b/src/Contexts/shared/domain/value-object/StringValueObject.ts @@ -1,7 +1,17 @@ +import { InvalidArgumentError } from '../errors/InvalidArgumentError'; + export class StringValueObject { readonly value: string; constructor(value: string) { - this.value = value; + this.value = this.ensureIsValidValue(value); + } + + private ensureIsValidValue(value: string): string { + if (typeof value !== 'string') { + throw new InvalidArgumentError('Invalid value'); + } + + return value.trim(); } } diff --git a/src/Contexts/shared/domain/value-object/Uuid.ts b/src/Contexts/shared/domain/value-object/Uuid.ts index f0eb222..9d4fd63 100644 --- a/src/Contexts/shared/domain/value-object/Uuid.ts +++ b/src/Contexts/shared/domain/value-object/Uuid.ts @@ -1,4 +1,6 @@ import validate from 'uuid-validate'; +import { v4 as uuidv4 } from 'uuid'; + import { InvalidArgumentError } from '../errors/InvalidArgumentError'; export class Uuid { @@ -13,6 +15,10 @@ export class Uuid { return this.value; } + static random(): Uuid { + return new Uuid(uuidv4()); + } + private ensureIsValidUuid(id: string): void { if (!validate(id)) { throw new InvalidArgumentError( diff --git a/tests/Contexts/apiApp/Auth/domain/User.test.ts b/tests/Contexts/apiApp/Auth/domain/User.test.ts new file mode 100644 index 0000000..dc2d2b9 --- /dev/null +++ b/tests/Contexts/apiApp/Auth/domain/User.test.ts @@ -0,0 +1,49 @@ +import { User } from '../../../../../src/Contexts/apiApp/Auth/domain'; +import { UserMother } from './mothers/UserMother'; + +describe('User', () => { + it('should create a valid user', () => { + const user = UserMother.create(); + + expect(user).toBeInstanceOf(User); + expect(user.id).toBeDefined(); + expect(user.email).toBeDefined(); + expect(user.username).toBeDefined(); + expect(user.password).toBeDefined(); + expect(user.emailValidated).toBeDefined(); + expect(user.roles).toBeDefined(); + }); + + it('should return primitives from user', () => { + const user = UserMother.random(); + + const primitives = user.toPrimitives(); + + expect(primitives).toMatchObject({ + id: expect.any(String), + email: expect.any(String), + username: expect.any(String), + password: expect.any(String), + emailValidated: expect.any(Boolean), + roles: expect.any(Array) + }); + }); + + it('should create a valid user from primitives', () => { + const user = UserMother.random(); + + const userFromPrimitives = User.fromPrimitives( + user.toPrimitives() as { + id: string; + email: string; + username: string; + password: string; + emailValidated: boolean; + roles: string[]; + } + ); + + expect(userFromPrimitives).toBeInstanceOf(User); + expect(userFromPrimitives).toEqual(user); + }); +}); diff --git a/tests/Contexts/apiApp/Auth/domain/UserName.test.ts b/tests/Contexts/apiApp/Auth/domain/UserName.test.ts new file mode 100644 index 0000000..3f2702e --- /dev/null +++ b/tests/Contexts/apiApp/Auth/domain/UserName.test.ts @@ -0,0 +1,18 @@ +import { Username } from '../../../../../src/Contexts/apiApp/Auth/domain/Username'; +import { random } from '../../../fixtures/shared'; + +describe('UserName', () => { + it('should throw an error if user username is more than 20 chars long', () => { + const invalidUsername = random.word({ min: 21, max: 255 }); + expect(() => new Username(invalidUsername)).toThrowError( + ' must be less than 20 characters long' + ); + }); + + it('should throw an error if user username is less than 4 chars long', () => { + const invalidUsername = random.word({ min: 1, max: 3 }); + expect(() => new Username(invalidUsername)).toThrowError( + ' must be at least 4 characters long' + ); + }); +}); diff --git a/tests/Contexts/apiApp/Auth/domain/UserRoles.test.ts b/tests/Contexts/apiApp/Auth/domain/UserRoles.test.ts new file mode 100644 index 0000000..7cd04de --- /dev/null +++ b/tests/Contexts/apiApp/Auth/domain/UserRoles.test.ts @@ -0,0 +1,17 @@ +import { UserRoles } from '../../../../../src/Contexts/apiApp/Auth/domain'; +import { random } from '../../../fixtures/shared'; +import { UserRolesMother } from './mothers/UserRolesMother'; + +describe('UserRoles', () => { + it('should create a valid user roles', () => { + const userRoles = UserRolesMother.random(); + expect(userRoles).toBeInstanceOf(UserRoles); + }); + + it('should throw an error if user roles are invalid', () => { + const roles = [random.word()]; + expect(() => UserRolesMother.create(roles)).toThrowError( + ` does not allow the value <${roles}>` + ); + }); +}); diff --git a/tests/Contexts/apiApp/Auth/domain/mothers/UserMother.ts b/tests/Contexts/apiApp/Auth/domain/mothers/UserMother.ts new file mode 100644 index 0000000..6a24798 --- /dev/null +++ b/tests/Contexts/apiApp/Auth/domain/mothers/UserMother.ts @@ -0,0 +1,63 @@ +import { RegisterUserRequest } from '../../../../../../src/Contexts/apiApp/Auth/application/RegisterUserRequest'; +import { + User, + Email, + UserRoles, + Username, + UserPatch +} from '../../../../../../src/Contexts/apiApp/Auth/domain'; +import { StringValueObject } from '../../../../../../src/Contexts/shared/domain/value-object/StringValueObject'; +import { Uuid } from '../../../../../../src/Contexts/shared/domain/value-object/Uuid'; +import { random } from '../../../../fixtures/shared'; +import { EmailMother } from '../../../../shared/domain/mothers/EmailMother'; +import { UserRolesMother } from './UserRolesMother'; + +export class UserMother { + static create({ + id, + email, + username, + password, + emailValidated, + roles + }: { + id?: Uuid; + email?: Email; + username?: Username; + password?: StringValueObject; + emailValidated?: boolean; + roles?: UserRoles; + } = {}): User { + return new User({ + id: id ?? Uuid.random(), + email: email ?? EmailMother.random(), + username: username ?? new Username(random.word({ min: 4, max: 20 })), + password: password ?? new StringValueObject(random.word()), + emailValidated: emailValidated ?? random.boolean(), + roles: + roles ?? + UserRolesMother.create([`${random.arrayElement(['admin', 'user'])}`]) + }); + } + + static from(command: RegisterUserRequest): User { + return this.create({ + email: new Email(command.email), + username: new Username(command.username), + password: new StringValueObject(command.password) + }); + } + + static random(): User { + return this.create(); + } + + static randomPatch(id: string): UserPatch { + return new UserPatch({ + id: new Uuid(id), + password: new StringValueObject(random.word()), + emailValidated: random.boolean(), + roles: UserRolesMother.random() + }); + } +} diff --git a/tests/Contexts/apiApp/Auth/domain/mothers/UserRolesMother.ts b/tests/Contexts/apiApp/Auth/domain/mothers/UserRolesMother.ts new file mode 100644 index 0000000..9069a78 --- /dev/null +++ b/tests/Contexts/apiApp/Auth/domain/mothers/UserRolesMother.ts @@ -0,0 +1,12 @@ +import { UserRoles } from '../../../../../../src/Contexts/apiApp/Auth/domain'; +import { random } from '../../../../fixtures/shared'; + +export class UserRolesMother { + static create(value: string[]) { + return new UserRoles(value); + } + + static random() { + return this.create([`${random.arrayElement(['admin', 'user'])}`]); + } +} diff --git a/tests/Contexts/apiApp/Auth/domain/mothers/index.ts b/tests/Contexts/apiApp/Auth/domain/mothers/index.ts new file mode 100644 index 0000000..c56e656 --- /dev/null +++ b/tests/Contexts/apiApp/Auth/domain/mothers/index.ts @@ -0,0 +1,2 @@ +export * from './UserMother'; +export * from './UserRolesMother'; diff --git a/tests/Contexts/fixtures/shared/random.ts b/tests/Contexts/fixtures/shared/random.ts index f7874a4..688ca36 100644 --- a/tests/Contexts/fixtures/shared/random.ts +++ b/tests/Contexts/fixtures/shared/random.ts @@ -43,7 +43,7 @@ export class Random { max = 256 }: { min?: number; max?: number } = {}): string { return this.chance.word({ - length: Math.floor(Math.random() * (max - min + 1)) + min + length: Math.floor(Math.random() * (max - min + 1) + min) }); } @@ -66,4 +66,8 @@ export class Random { return `${prefix}-${group1}-${group2}-${group3}-${group4}`; } + + public email(): string { + return this.chance.email(); + } } diff --git a/tests/Contexts/shared/domain/Email.test.ts b/tests/Contexts/shared/domain/Email.test.ts new file mode 100644 index 0000000..09a526f --- /dev/null +++ b/tests/Contexts/shared/domain/Email.test.ts @@ -0,0 +1,42 @@ +import { Email } from '../../../../src/Contexts/apiApp/Auth/domain'; +import { random } from '../../fixtures/shared'; +import { EmailMother } from './mothers/EmailMother'; + +describe('Email', () => { + it('should create a valid email', () => { + const email = EmailMother.random(); + expect(email).toBeInstanceOf(Email); + }); + + it('should throw an error if email is more than 255 chars long', () => { + const invalidEmail = random.word({ min: 256, max: 512 }); + expect(() => EmailMother.create(invalidEmail)).toThrowError( + ' must be less than 255 characters long' + ); + }); + + it('should throw an error if email is less than 6 chars long', () => { + const invalidEmail = random.word({ min: 1, max: 3 }); + expect(() => EmailMother.create(invalidEmail)).toThrowError( + ' must be at least 6 characters long' + ); + }); + + it('should throw an error if email is not an email address', () => { + const invalidEmail = random.word({ min: 6, max: 255 }); + expect(() => EmailMother.create(invalidEmail)).toThrowError( + ` does not allow the value <${invalidEmail}>` + ); + }); + + it('should throw an error if email domain is in the blackList', () => { + const invalidEmail = `test@${random.arrayElement([ + 'mailinator.com', + 'guerrillamail.com', + 'sharklasers.com' + ])}`; + expect(() => EmailMother.create(invalidEmail)).toThrowError( + ` does not allow the domain <${invalidEmail}>` + ); + }); +}); diff --git a/tests/Contexts/shared/domain/mothers/EmailMother.ts b/tests/Contexts/shared/domain/mothers/EmailMother.ts new file mode 100644 index 0000000..03a75ec --- /dev/null +++ b/tests/Contexts/shared/domain/mothers/EmailMother.ts @@ -0,0 +1,12 @@ +import { Email } from '../../../../../src/Contexts/apiApp/Auth/domain'; +import { random } from '../../../fixtures/shared'; + +export class EmailMother { + static create(value: string) { + return new Email(value); + } + + static random() { + return this.create(random.email()); + } +}