admin area
This commit is contained in:
95
core/admin/domain/value-objects/Email.test.ts
Normal file
95
core/admin/domain/value-objects/Email.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Email } from './Email';
|
||||
|
||||
describe('Email', () => {
|
||||
describe('TDD - Test First', () => {
|
||||
it('should create a valid email from string', () => {
|
||||
// Arrange & Act
|
||||
const email = Email.fromString('test@example.com');
|
||||
|
||||
// Assert
|
||||
expect(email.value).toBe('test@example.com');
|
||||
});
|
||||
|
||||
it('should trim whitespace', () => {
|
||||
// Arrange & Act
|
||||
const email = Email.fromString(' test@example.com ');
|
||||
|
||||
// Assert
|
||||
expect(email.value).toBe('test@example.com');
|
||||
});
|
||||
|
||||
it('should throw error for empty string', () => {
|
||||
// Arrange & Act & Assert
|
||||
expect(() => Email.fromString('')).toThrow('Email cannot be empty');
|
||||
expect(() => Email.fromString(' ')).toThrow('Email cannot be empty');
|
||||
});
|
||||
|
||||
it('should throw error for null or undefined', () => {
|
||||
// Arrange & Act & Assert
|
||||
expect(() => Email.fromString(null as unknown as string)).toThrow('Email cannot be empty');
|
||||
expect(() => Email.fromString(undefined as unknown as string)).toThrow('Email cannot be empty');
|
||||
});
|
||||
|
||||
it('should handle various email formats', () => {
|
||||
// Arrange & Act
|
||||
const email1 = Email.fromString('user@example.com');
|
||||
const email2 = Email.fromString('user.name@example.com');
|
||||
const email3 = Email.fromString('user+tag@example.co.uk');
|
||||
|
||||
// Assert
|
||||
expect(email1.value).toBe('user@example.com');
|
||||
expect(email2.value).toBe('user.name@example.com');
|
||||
expect(email3.value).toBe('user+tag@example.co.uk');
|
||||
});
|
||||
|
||||
it('should support equals comparison', () => {
|
||||
// Arrange
|
||||
const email1 = Email.fromString('test@example.com');
|
||||
const email2 = Email.fromString('test@example.com');
|
||||
const email3 = Email.fromString('other@example.com');
|
||||
|
||||
// Assert
|
||||
expect(email1.equals(email2)).toBe(true);
|
||||
expect(email1.equals(email3)).toBe(false);
|
||||
});
|
||||
|
||||
it('should support toString', () => {
|
||||
// Arrange
|
||||
const email = Email.fromString('test@example.com');
|
||||
|
||||
// Assert
|
||||
expect(email.toString()).toBe('test@example.com');
|
||||
});
|
||||
|
||||
it('should handle case sensitivity', () => {
|
||||
// Arrange & Act
|
||||
const email1 = Email.fromString('Test@Example.com');
|
||||
const email2 = Email.fromString('test@example.com');
|
||||
|
||||
// Assert - Should preserve case but compare as-is
|
||||
expect(email1.value).toBe('Test@Example.com');
|
||||
expect(email2.value).toBe('test@example.com');
|
||||
});
|
||||
|
||||
it('should handle international characters', () => {
|
||||
// Arrange & Act
|
||||
const email = Email.fromString('tëst@ëxample.com');
|
||||
|
||||
// Assert
|
||||
expect(email.value).toBe('tëst@ëxample.com');
|
||||
});
|
||||
|
||||
it('should handle very long emails', () => {
|
||||
// Arrange
|
||||
const longLocal = 'a'.repeat(100);
|
||||
const longDomain = 'b'.repeat(100);
|
||||
const longEmail = `${longLocal}@${longDomain}.com`;
|
||||
|
||||
// Act
|
||||
const email = Email.fromString(longEmail);
|
||||
|
||||
// Assert
|
||||
expect(email.value).toBe(longEmail);
|
||||
});
|
||||
});
|
||||
});
|
||||
46
core/admin/domain/value-objects/Email.ts
Normal file
46
core/admin/domain/value-objects/Email.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { IValueObject } from '@core/shared/domain';
|
||||
import { AdminDomainValidationError } from '../errors/AdminDomainError';
|
||||
|
||||
export interface EmailProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class Email implements IValueObject<EmailProps> {
|
||||
readonly value: string;
|
||||
|
||||
private constructor(value: string) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
static create(value: string): Email {
|
||||
// Handle null/undefined
|
||||
if (value === null || value === undefined) {
|
||||
throw new AdminDomainValidationError('Email cannot be empty');
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
throw new AdminDomainValidationError('Email cannot be empty');
|
||||
}
|
||||
|
||||
// No format validation - accept any non-empty string
|
||||
return new Email(trimmed);
|
||||
}
|
||||
|
||||
static fromString(value: string): Email {
|
||||
return this.create(value);
|
||||
}
|
||||
|
||||
get props(): EmailProps {
|
||||
return { value: this.value };
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
equals(other: IValueObject<EmailProps>): boolean {
|
||||
return this.value === other.props.value;
|
||||
}
|
||||
}
|
||||
90
core/admin/domain/value-objects/UserId.test.ts
Normal file
90
core/admin/domain/value-objects/UserId.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { UserId } from './UserId';
|
||||
|
||||
describe('UserId', () => {
|
||||
describe('TDD - Test First', () => {
|
||||
it('should create a valid user id from string', () => {
|
||||
// Arrange & Act
|
||||
const userId = UserId.fromString('user-123');
|
||||
|
||||
// Assert
|
||||
expect(userId.value).toBe('user-123');
|
||||
});
|
||||
|
||||
it('should trim whitespace', () => {
|
||||
// Arrange & Act
|
||||
const userId = UserId.fromString(' user-123 ');
|
||||
|
||||
// Assert
|
||||
expect(userId.value).toBe('user-123');
|
||||
});
|
||||
|
||||
it('should throw error for empty string', () => {
|
||||
// Arrange & Act & Assert
|
||||
expect(() => UserId.fromString('')).toThrow('User ID cannot be empty');
|
||||
expect(() => UserId.fromString(' ')).toThrow('User ID cannot be empty');
|
||||
});
|
||||
|
||||
it('should throw error for null or undefined', () => {
|
||||
// Arrange & Act & Assert
|
||||
expect(() => UserId.fromString(null as unknown as string)).toThrow('User ID cannot be empty');
|
||||
expect(() => UserId.fromString(undefined as unknown as string)).toThrow('User ID cannot be empty');
|
||||
});
|
||||
|
||||
it('should handle special characters', () => {
|
||||
// Arrange & Act
|
||||
const userId = UserId.fromString('user-123_test@example');
|
||||
|
||||
// Assert
|
||||
expect(userId.value).toBe('user-123_test@example');
|
||||
});
|
||||
|
||||
it('should support equals comparison', () => {
|
||||
// Arrange
|
||||
const userId1 = UserId.fromString('user-123');
|
||||
const userId2 = UserId.fromString('user-123');
|
||||
const userId3 = UserId.fromString('user-456');
|
||||
|
||||
// Assert
|
||||
expect(userId1.equals(userId2)).toBe(true);
|
||||
expect(userId1.equals(userId3)).toBe(false);
|
||||
});
|
||||
|
||||
it('should support toString', () => {
|
||||
// Arrange
|
||||
const userId = UserId.fromString('user-123');
|
||||
|
||||
// Assert
|
||||
expect(userId.toString()).toBe('user-123');
|
||||
});
|
||||
|
||||
it('should handle very long IDs', () => {
|
||||
// Arrange
|
||||
const longId = 'a'.repeat(1000);
|
||||
|
||||
// Act
|
||||
const userId = UserId.fromString(longId);
|
||||
|
||||
// Assert
|
||||
expect(userId.value).toBe(longId);
|
||||
});
|
||||
|
||||
it('should handle UUID format', () => {
|
||||
// Arrange
|
||||
const uuid = '550e8400-e29b-41d4-a716-446655440000';
|
||||
|
||||
// Act
|
||||
const userId = UserId.fromString(uuid);
|
||||
|
||||
// Assert
|
||||
expect(userId.value).toBe(uuid);
|
||||
});
|
||||
|
||||
it('should handle numeric string IDs', () => {
|
||||
// Arrange & Act
|
||||
const userId = UserId.fromString('123456');
|
||||
|
||||
// Assert
|
||||
expect(userId.value).toBe('123456');
|
||||
});
|
||||
});
|
||||
});
|
||||
38
core/admin/domain/value-objects/UserId.ts
Normal file
38
core/admin/domain/value-objects/UserId.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { IValueObject } from '@core/shared/domain';
|
||||
import { AdminDomainValidationError } from '../errors/AdminDomainError';
|
||||
|
||||
export interface UserIdProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class UserId implements IValueObject<UserIdProps> {
|
||||
readonly value: string;
|
||||
|
||||
private constructor(value: string) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
static create(id: string): UserId {
|
||||
if (!id || id.trim().length === 0) {
|
||||
throw new AdminDomainValidationError('User ID cannot be empty');
|
||||
}
|
||||
|
||||
return new UserId(id.trim());
|
||||
}
|
||||
|
||||
static fromString(id: string): UserId {
|
||||
return this.create(id);
|
||||
}
|
||||
|
||||
get props(): UserIdProps {
|
||||
return { value: this.value };
|
||||
}
|
||||
|
||||
equals(other: IValueObject<UserIdProps>): boolean {
|
||||
return this.value === other.props.value;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
103
core/admin/domain/value-objects/UserRole.test.ts
Normal file
103
core/admin/domain/value-objects/UserRole.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { UserRole } from './UserRole';
|
||||
|
||||
describe('UserRole', () => {
|
||||
describe('TDD - Test First', () => {
|
||||
it('should create a valid role from string', () => {
|
||||
// Arrange & Act
|
||||
const role = UserRole.fromString('owner');
|
||||
|
||||
// Assert
|
||||
expect(role.value).toBe('owner');
|
||||
});
|
||||
|
||||
it('should trim whitespace', () => {
|
||||
// Arrange & Act
|
||||
const role = UserRole.fromString(' admin ');
|
||||
|
||||
// Assert
|
||||
expect(role.value).toBe('admin');
|
||||
});
|
||||
|
||||
it('should throw error for empty string', () => {
|
||||
// Arrange & Act & Assert
|
||||
expect(() => UserRole.fromString('')).toThrow('Role cannot be empty');
|
||||
expect(() => UserRole.fromString(' ')).toThrow('Role cannot be empty');
|
||||
});
|
||||
|
||||
it('should throw error for null or undefined', () => {
|
||||
// Arrange & Act & Assert
|
||||
expect(() => UserRole.fromString(null as unknown as string)).toThrow('Role cannot be empty');
|
||||
expect(() => UserRole.fromString(undefined as unknown as string)).toThrow('Role cannot be empty');
|
||||
});
|
||||
|
||||
it('should handle all valid roles', () => {
|
||||
// Arrange & Act
|
||||
const owner = UserRole.fromString('owner');
|
||||
const admin = UserRole.fromString('admin');
|
||||
const user = UserRole.fromString('user');
|
||||
|
||||
// Assert
|
||||
expect(owner.value).toBe('owner');
|
||||
expect(admin.value).toBe('admin');
|
||||
expect(user.value).toBe('user');
|
||||
});
|
||||
|
||||
it('should detect system admin roles', () => {
|
||||
// Arrange
|
||||
const owner = UserRole.fromString('owner');
|
||||
const admin = UserRole.fromString('admin');
|
||||
const user = UserRole.fromString('user');
|
||||
|
||||
// Assert
|
||||
expect(owner.isSystemAdmin()).toBe(true);
|
||||
expect(admin.isSystemAdmin()).toBe(true);
|
||||
expect(user.isSystemAdmin()).toBe(false);
|
||||
});
|
||||
|
||||
it('should support equals comparison', () => {
|
||||
// Arrange
|
||||
const role1 = UserRole.fromString('owner');
|
||||
const role2 = UserRole.fromString('owner');
|
||||
const role3 = UserRole.fromString('admin');
|
||||
|
||||
// Assert
|
||||
expect(role1.equals(role2)).toBe(true);
|
||||
expect(role1.equals(role3)).toBe(false);
|
||||
});
|
||||
|
||||
it('should support toString', () => {
|
||||
// Arrange
|
||||
const role = UserRole.fromString('owner');
|
||||
|
||||
// Assert
|
||||
expect(role.toString()).toBe('owner');
|
||||
});
|
||||
|
||||
it('should handle custom roles', () => {
|
||||
// Arrange & Act
|
||||
const customRole = UserRole.fromString('steward');
|
||||
|
||||
// Assert
|
||||
expect(customRole.value).toBe('steward');
|
||||
expect(customRole.isSystemAdmin()).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle case sensitivity', () => {
|
||||
// Arrange & Act
|
||||
const role1 = UserRole.fromString('Owner');
|
||||
const role2 = UserRole.fromString('owner');
|
||||
|
||||
// Assert - Should preserve case but compare as-is
|
||||
expect(role1.value).toBe('Owner');
|
||||
expect(role2.value).toBe('owner');
|
||||
});
|
||||
|
||||
it('should handle special characters in role names', () => {
|
||||
// Arrange & Act
|
||||
const role = UserRole.fromString('admin-steward');
|
||||
|
||||
// Assert
|
||||
expect(role.value).toBe('admin-steward');
|
||||
});
|
||||
});
|
||||
});
|
||||
74
core/admin/domain/value-objects/UserRole.ts
Normal file
74
core/admin/domain/value-objects/UserRole.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { IValueObject } from '@core/shared/domain';
|
||||
import { AdminDomainValidationError } from '../errors/AdminDomainError';
|
||||
|
||||
export type UserRoleValue = string;
|
||||
|
||||
export interface UserRoleProps {
|
||||
value: UserRoleValue;
|
||||
}
|
||||
|
||||
export class UserRole implements IValueObject<UserRoleProps> {
|
||||
readonly value: UserRoleValue;
|
||||
|
||||
private constructor(value: UserRoleValue) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
static create(value: UserRoleValue): UserRole {
|
||||
// Handle null/undefined
|
||||
if (value === null || value === undefined) {
|
||||
throw new AdminDomainValidationError('Role cannot be empty');
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
throw new AdminDomainValidationError('Role cannot be empty');
|
||||
}
|
||||
|
||||
return new UserRole(trimmed);
|
||||
}
|
||||
|
||||
static fromString(value: string): UserRole {
|
||||
return this.create(value);
|
||||
}
|
||||
|
||||
get props(): UserRoleProps {
|
||||
return { value: this.value };
|
||||
}
|
||||
|
||||
toString(): UserRoleValue {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
equals(other: IValueObject<UserRoleProps>): boolean {
|
||||
return this.value === other.props.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this role is a system administrator role
|
||||
*/
|
||||
isSystemAdmin(): boolean {
|
||||
const lower = this.value.toLowerCase();
|
||||
return lower === 'owner' || lower === 'admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this role has higher authority than another role
|
||||
*/
|
||||
hasHigherAuthorityThan(other: UserRole): boolean {
|
||||
const hierarchy: Record<string, number> = {
|
||||
user: 0,
|
||||
admin: 1,
|
||||
owner: 2,
|
||||
};
|
||||
|
||||
const myValue = this.value.toLowerCase();
|
||||
const otherValue = other.value.toLowerCase();
|
||||
|
||||
const myRank = hierarchy[myValue] ?? 0;
|
||||
const otherRank = hierarchy[otherValue] ?? 0;
|
||||
|
||||
return myRank > otherRank;
|
||||
}
|
||||
}
|
||||
127
core/admin/domain/value-objects/UserStatus.test.ts
Normal file
127
core/admin/domain/value-objects/UserStatus.test.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { UserStatus } from './UserStatus';
|
||||
|
||||
describe('UserStatus', () => {
|
||||
describe('TDD - Test First', () => {
|
||||
it('should create a valid status from string', () => {
|
||||
// Arrange & Act
|
||||
const status = UserStatus.fromString('active');
|
||||
|
||||
// Assert
|
||||
expect(status.value).toBe('active');
|
||||
});
|
||||
|
||||
it('should trim whitespace', () => {
|
||||
// Arrange & Act
|
||||
const status = UserStatus.fromString(' suspended ');
|
||||
|
||||
// Assert
|
||||
expect(status.value).toBe('suspended');
|
||||
});
|
||||
|
||||
it('should throw error for empty string', () => {
|
||||
// Arrange & Act & Assert
|
||||
expect(() => UserStatus.fromString('')).toThrow('Status cannot be empty');
|
||||
expect(() => UserStatus.fromString(' ')).toThrow('Status cannot be empty');
|
||||
});
|
||||
|
||||
it('should throw error for null or undefined', () => {
|
||||
// Arrange & Act & Assert
|
||||
expect(() => UserStatus.fromString(null as unknown as string)).toThrow('Status cannot be empty');
|
||||
expect(() => UserStatus.fromString(undefined as unknown as string)).toThrow('Status cannot be empty');
|
||||
});
|
||||
|
||||
it('should handle all valid statuses', () => {
|
||||
// Arrange & Act
|
||||
const active = UserStatus.fromString('active');
|
||||
const suspended = UserStatus.fromString('suspended');
|
||||
const deleted = UserStatus.fromString('deleted');
|
||||
|
||||
// Assert
|
||||
expect(active.value).toBe('active');
|
||||
expect(suspended.value).toBe('suspended');
|
||||
expect(deleted.value).toBe('deleted');
|
||||
});
|
||||
|
||||
it('should detect active status', () => {
|
||||
// Arrange
|
||||
const active = UserStatus.fromString('active');
|
||||
const suspended = UserStatus.fromString('suspended');
|
||||
const deleted = UserStatus.fromString('deleted');
|
||||
|
||||
// Assert
|
||||
expect(active.isActive()).toBe(true);
|
||||
expect(suspended.isActive()).toBe(false);
|
||||
expect(deleted.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('should detect suspended status', () => {
|
||||
// Arrange
|
||||
const active = UserStatus.fromString('active');
|
||||
const suspended = UserStatus.fromString('suspended');
|
||||
const deleted = UserStatus.fromString('deleted');
|
||||
|
||||
// Assert
|
||||
expect(active.isSuspended()).toBe(false);
|
||||
expect(suspended.isSuspended()).toBe(true);
|
||||
expect(deleted.isSuspended()).toBe(false);
|
||||
});
|
||||
|
||||
it('should detect deleted status', () => {
|
||||
// Arrange
|
||||
const active = UserStatus.fromString('active');
|
||||
const suspended = UserStatus.fromString('suspended');
|
||||
const deleted = UserStatus.fromString('deleted');
|
||||
|
||||
// Assert
|
||||
expect(active.isDeleted()).toBe(false);
|
||||
expect(suspended.isDeleted()).toBe(false);
|
||||
expect(deleted.isDeleted()).toBe(true);
|
||||
});
|
||||
|
||||
it('should support equals comparison', () => {
|
||||
// Arrange
|
||||
const status1 = UserStatus.fromString('active');
|
||||
const status2 = UserStatus.fromString('active');
|
||||
const status3 = UserStatus.fromString('suspended');
|
||||
|
||||
// Assert
|
||||
expect(status1.equals(status2)).toBe(true);
|
||||
expect(status1.equals(status3)).toBe(false);
|
||||
});
|
||||
|
||||
it('should support toString', () => {
|
||||
// Arrange
|
||||
const status = UserStatus.fromString('active');
|
||||
|
||||
// Assert
|
||||
expect(status.toString()).toBe('active');
|
||||
});
|
||||
|
||||
it('should handle custom statuses', () => {
|
||||
// Arrange & Act
|
||||
const customStatus = UserStatus.fromString('pending');
|
||||
|
||||
// Assert
|
||||
expect(customStatus.value).toBe('pending');
|
||||
expect(customStatus.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle case sensitivity', () => {
|
||||
// Arrange & Act
|
||||
const status1 = UserStatus.fromString('Active');
|
||||
const status2 = UserStatus.fromString('active');
|
||||
|
||||
// Assert - Should preserve case but compare as-is
|
||||
expect(status1.value).toBe('Active');
|
||||
expect(status2.value).toBe('active');
|
||||
});
|
||||
|
||||
it('should handle special characters in status names', () => {
|
||||
// Arrange & Act
|
||||
const status = UserStatus.fromString('under-review');
|
||||
|
||||
// Assert
|
||||
expect(status.value).toBe('under-review');
|
||||
});
|
||||
});
|
||||
});
|
||||
59
core/admin/domain/value-objects/UserStatus.ts
Normal file
59
core/admin/domain/value-objects/UserStatus.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { IValueObject } from '@core/shared/domain';
|
||||
import { AdminDomainValidationError } from '../errors/AdminDomainError';
|
||||
|
||||
export type UserStatusValue = string;
|
||||
|
||||
export interface UserStatusProps {
|
||||
value: UserStatusValue;
|
||||
}
|
||||
|
||||
export class UserStatus implements IValueObject<UserStatusProps> {
|
||||
readonly value: UserStatusValue;
|
||||
|
||||
private constructor(value: UserStatusValue) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
static create(value: UserStatusValue): UserStatus {
|
||||
// Handle null/undefined
|
||||
if (value === null || value === undefined) {
|
||||
throw new AdminDomainValidationError('Status cannot be empty');
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
throw new AdminDomainValidationError('Status cannot be empty');
|
||||
}
|
||||
|
||||
return new UserStatus(trimmed);
|
||||
}
|
||||
|
||||
static fromString(value: string): UserStatus {
|
||||
return this.create(value);
|
||||
}
|
||||
|
||||
get props(): UserStatusProps {
|
||||
return { value: this.value };
|
||||
}
|
||||
|
||||
toString(): UserStatusValue {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
equals(other: IValueObject<UserStatusProps>): boolean {
|
||||
return this.value === other.props.value;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.value === 'active';
|
||||
}
|
||||
|
||||
isSuspended(): boolean {
|
||||
return this.value === 'suspended';
|
||||
}
|
||||
|
||||
isDeleted(): boolean {
|
||||
return this.value === 'deleted';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user