admin area

This commit is contained in:
2026-01-01 12:10:35 +01:00
parent 02c0cc44e1
commit f001df3744
68 changed files with 10324 additions and 32 deletions

View 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);
});
});
});

View 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;
}
}

View 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');
});
});
});

View 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;
}
}

View 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');
});
});
});

View 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;
}
}

View 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');
});
});
});

View 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';
}
}