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,531 @@
import { AdminUser } from './AdminUser';
import { UserRole } from '../value-objects/UserRole';
describe('AdminUser', () => {
describe('TDD - Test First', () => {
it('should create a valid admin user', () => {
// Arrange & Act
const user = AdminUser.create({
id: 'user-123',
email: 'admin@example.com',
displayName: 'Admin User',
roles: ['owner'],
status: 'active',
});
// Assert
expect(user.id.value).toBe('user-123');
expect(user.email.value).toBe('admin@example.com');
expect(user.displayName).toBe('Admin User');
expect(user.roles).toHaveLength(1);
expect(user.roles[0]!.value).toBe('owner');
expect(user.status.value).toBe('active');
});
it('should validate email format', () => {
// Arrange & Act
const user = AdminUser.create({
id: 'user-123',
email: 'invalid-email',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
// Assert - Email should be created but validation happens in value object
expect(user.email.value).toBe('invalid-email');
});
it('should detect system admin (owner)', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Assert
expect(owner.isSystemAdmin()).toBe(true);
expect(admin.isSystemAdmin()).toBe(true);
expect(user.isSystemAdmin()).toBe(false);
});
it('should handle multiple roles', () => {
// Arrange & Act
const user = AdminUser.create({
id: 'user-123',
email: 'multi@example.com',
displayName: 'Multi Role',
roles: ['owner', 'admin'],
status: 'active',
});
// Assert
expect(user.roles).toHaveLength(2);
expect(user.roles.map(r => r.value)).toContain('owner');
expect(user.roles.map(r => r.value)).toContain('admin');
});
it('should handle suspended status', () => {
// Arrange & Act
const user = AdminUser.create({
id: 'user-123',
email: 'suspended@example.com',
displayName: 'Suspended User',
roles: ['user'],
status: 'suspended',
});
// Assert
expect(user.status.value).toBe('suspended');
expect(user.isActive()).toBe(false);
});
it('should handle optional fields', () => {
// Arrange & Act
const user = AdminUser.create({
id: 'user-123',
email: 'minimal@example.com',
displayName: 'Minimal User',
roles: ['user'],
status: 'active',
});
// Assert
expect(user.primaryDriverId).toBeUndefined();
expect(user.lastLoginAt).toBeUndefined();
});
it('should handle all optional fields', () => {
// Arrange
const now = new Date();
// Act
const user = AdminUser.create({
id: 'user-123',
email: 'full@example.com',
displayName: 'Full User',
roles: ['user'],
status: 'active',
primaryDriverId: 'driver-456',
lastLoginAt: now,
});
// Assert
expect(user.primaryDriverId).toBe('driver-456');
expect(user.lastLoginAt).toEqual(now);
});
it('should handle createdAt and updatedAt', () => {
// Arrange & Act
const user = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
// Assert
expect(user.createdAt).toBeInstanceOf(Date);
expect(user.updatedAt).toBeInstanceOf(Date);
});
it('should handle role assignment with validation', () => {
// Arrange & Act
const user = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
// Assert - Should accept any role string (validation happens in value object)
expect(user.roles).toHaveLength(1);
expect(user.roles[0]!.value).toBe('user');
});
it('should handle status changes', () => {
// Arrange
const user = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
// Act - Use domain method to change status
user.suspend();
// Assert
expect(user.status.value).toBe('suspended');
});
it('should check if user is active', () => {
// Arrange
const activeUser = AdminUser.create({
id: 'user-123',
email: 'active@example.com',
displayName: 'Active User',
roles: ['user'],
status: 'active',
});
const suspendedUser = AdminUser.create({
id: 'user-456',
email: 'suspended@example.com',
displayName: 'Suspended User',
roles: ['user'],
status: 'suspended',
});
// Assert
expect(activeUser.isActive()).toBe(true);
expect(suspendedUser.isActive()).toBe(false);
});
it('should handle role management', () => {
// Arrange
const user = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
// Act
user.addRole(UserRole.fromString('admin'));
// Assert
expect(user.roles).toHaveLength(2);
expect(user.hasRole('admin')).toBe(true);
});
it('should handle display name updates', () => {
// Arrange
const user = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Old Name',
roles: ['user'],
status: 'active',
});
// Act
user.updateDisplayName('New Name');
// Assert
expect(user.displayName).toBe('New Name');
});
it('should handle login recording', () => {
// Arrange
const user = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
const beforeLogin = user.lastLoginAt;
// Act
user.recordLogin();
// Assert
expect(user.lastLoginAt).toBeDefined();
expect(user.lastLoginAt).not.toEqual(beforeLogin);
});
it('should handle summary generation', () => {
// Arrange
const user = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['owner'],
status: 'active',
lastLoginAt: new Date(),
});
// Act
const summary = user.toSummary();
// Assert
expect(summary.id).toBe('user-123');
expect(summary.email).toBe('test@example.com');
expect(summary.displayName).toBe('Test User');
expect(summary.roles).toEqual(['owner']);
expect(summary.status).toBe('active');
expect(summary.isSystemAdmin).toBe(true);
expect(summary.lastLoginAt).toBeDefined();
});
it('should handle equality comparison', () => {
// Arrange
const user1 = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
const user2 = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['user'],
status: 'active',
});
const user3 = AdminUser.create({
id: 'user-456',
email: 'other@example.com',
displayName: 'Other User',
roles: ['user'],
status: 'active',
});
// Assert
expect(user1.equals(user2)).toBe(true);
expect(user1.equals(user3)).toBe(false);
expect(user1.equals(undefined)).toBe(false);
});
it('should handle management permissions', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Assert - Owner can manage everyone
expect(owner.canManage(admin)).toBe(true);
expect(owner.canManage(user)).toBe(true);
expect(owner.canManage(owner)).toBe(true); // Can manage self
// Admin can manage users but not admins/owners
expect(admin.canManage(user)).toBe(true);
expect(admin.canManage(admin)).toBe(false);
expect(admin.canManage(owner)).toBe(false);
// User cannot manage anyone except self
expect(user.canManage(user)).toBe(true);
expect(user.canManage(admin)).toBe(false);
expect(user.canManage(owner)).toBe(false);
});
it('should handle role modification permissions', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Assert - Only owner can modify roles
expect(owner.canModifyRoles(user)).toBe(true);
expect(owner.canModifyRoles(admin)).toBe(true);
expect(owner.canModifyRoles(owner)).toBe(false); // Cannot modify own roles
expect(admin.canModifyRoles(user)).toBe(false);
expect(user.canModifyRoles(user)).toBe(false);
});
it('should handle status change permissions', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Assert - Owner can change anyone's status
expect(owner.canChangeStatus(user)).toBe(true);
expect(owner.canChangeStatus(admin)).toBe(true);
expect(owner.canChangeStatus(owner)).toBe(false); // Cannot change own status
// Admin can change user status but not admin/owner
expect(admin.canChangeStatus(user)).toBe(true);
expect(admin.canChangeStatus(admin)).toBe(false);
expect(admin.canChangeStatus(owner)).toBe(false);
// User cannot change status
expect(user.canChangeStatus(user)).toBe(false);
expect(user.canChangeStatus(admin)).toBe(false);
expect(user.canChangeStatus(owner)).toBe(false);
});
it('should handle deletion permissions', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Assert - Owner can delete anyone except self
expect(owner.canDelete(user)).toBe(true);
expect(owner.canDelete(admin)).toBe(true);
expect(owner.canDelete(owner)).toBe(false); // Cannot delete self
// Admin can delete users but not admins/owners
expect(admin.canDelete(user)).toBe(true);
expect(admin.canDelete(admin)).toBe(false);
expect(admin.canDelete(owner)).toBe(false);
// User cannot delete anyone
expect(user.canDelete(user)).toBe(false);
expect(user.canDelete(admin)).toBe(false);
expect(user.canDelete(owner)).toBe(false);
});
it('should handle authority comparison', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Assert
expect(owner.hasHigherAuthorityThan(admin)).toBe(true);
expect(owner.hasHigherAuthorityThan(user)).toBe(true);
expect(admin.hasHigherAuthorityThan(user)).toBe(true);
expect(admin.hasHigherAuthorityThan(owner)).toBe(false);
expect(user.hasHigherAuthorityThan(admin)).toBe(false);
});
it('should handle role display names', () => {
// Arrange
const user = AdminUser.create({
id: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
roles: ['owner', 'admin'],
status: 'active',
});
// Act
const displayNames = user.getRoleDisplayNames();
// Assert
expect(displayNames).toContain('Owner');
expect(displayNames).toContain('Admin');
});
});
});

View File

@@ -0,0 +1,485 @@
import type { IEntity } from '@core/shared/domain';
import { UserId } from '../value-objects/UserId';
import { Email } from '../value-objects/Email';
import { UserRole } from '../value-objects/UserRole';
import { UserStatus } from '../value-objects/UserStatus';
import { AdminDomainValidationError, AdminDomainInvariantError } from '../errors/AdminDomainError';
export interface AdminUserProps {
id: UserId;
email: Email;
roles: UserRole[];
status: UserStatus;
displayName: string;
createdAt: Date;
updatedAt: Date;
lastLoginAt: Date | undefined;
primaryDriverId: string | undefined;
}
export class AdminUser implements IEntity<UserId> {
readonly id: UserId;
private _email: Email;
private _roles: UserRole[];
private _status: UserStatus;
private _displayName: string;
private _createdAt: Date;
private _updatedAt: Date;
private _lastLoginAt: Date | undefined;
private _primaryDriverId: string | undefined;
private constructor(props: AdminUserProps) {
this.id = props.id;
this._email = props.email;
this._roles = props.roles;
this._status = props.status;
this._displayName = props.displayName;
this._createdAt = props.createdAt;
this._updatedAt = props.updatedAt;
this._lastLoginAt = props.lastLoginAt;
this._primaryDriverId = props.primaryDriverId;
}
/**
* Factory method to create a new AdminUser
* Validates all business rules and invariants
*/
static create(props: {
id: string;
email: string;
roles: string[];
status: string;
displayName: string;
createdAt?: Date;
updatedAt?: Date;
lastLoginAt?: Date;
primaryDriverId?: string;
}): AdminUser {
// Validate required fields
if (!props.id || props.id.trim().length === 0) {
throw new AdminDomainValidationError('User ID is required');
}
if (!props.email || props.email.trim().length === 0) {
throw new AdminDomainValidationError('Email is required');
}
if (!props.roles || props.roles.length === 0) {
throw new AdminDomainValidationError('At least one role is required');
}
if (!props.status || props.status.trim().length === 0) {
throw new AdminDomainValidationError('Status is required');
}
if (!props.displayName || props.displayName.trim().length === 0) {
throw new AdminDomainValidationError('Display name is required');
}
// Validate display name length
const trimmedName = props.displayName.trim();
if (trimmedName.length < 2 || trimmedName.length > 100) {
throw new AdminDomainValidationError('Display name must be between 2 and 100 characters');
}
// Create value objects
const id = UserId.fromString(props.id);
const email = Email.fromString(props.email);
const roles = props.roles.map(role => UserRole.fromString(role));
const status = UserStatus.fromString(props.status);
// Validate role hierarchy - ensure no duplicate roles
const uniqueRoles = new Set(roles.map(r => r.toString()));
if (uniqueRoles.size !== roles.length) {
throw new AdminDomainValidationError('Duplicate roles are not allowed');
}
const now = props.createdAt ?? new Date();
return new AdminUser({
id,
email,
roles,
status,
displayName: trimmedName,
createdAt: now,
updatedAt: props.updatedAt ?? now,
lastLoginAt: props.lastLoginAt ?? undefined,
primaryDriverId: props.primaryDriverId ?? undefined,
});
}
/**
* Rehydrate from storage
*/
static rehydrate(props: {
id: string;
email: string;
roles: string[];
status: string;
displayName: string;
createdAt: Date;
updatedAt: Date;
lastLoginAt?: Date;
primaryDriverId?: string;
}): AdminUser {
return this.create(props);
}
// Getters
get email(): Email {
return this._email;
}
get roles(): UserRole[] {
return [...this._roles];
}
get status(): UserStatus {
return this._status;
}
get displayName(): string {
return this._displayName;
}
get createdAt(): Date {
return new Date(this._createdAt.getTime());
}
get updatedAt(): Date {
return new Date(this._updatedAt.getTime());
}
get lastLoginAt(): Date | undefined {
return this._lastLoginAt ? new Date(this._lastLoginAt.getTime()) : undefined;
}
get primaryDriverId(): string | undefined {
return this._primaryDriverId;
}
// Domain methods
/**
* Add a role to the user
* Cannot add duplicate roles
* Cannot add owner role if user already has other roles
*/
addRole(role: UserRole): void {
if (this._roles.some(r => r.equals(role))) {
throw new AdminDomainInvariantError(`Role ${role.value} is already assigned`);
}
// If adding owner role, user must have no other roles
if (role.value === 'owner' && this._roles.length > 0) {
throw new AdminDomainInvariantError('Cannot add owner role to user with existing roles');
}
// If user has owner role, cannot add other roles
if (this._roles.some(r => r.value === 'owner')) {
throw new AdminDomainInvariantError('Owner cannot have additional roles');
}
this._roles.push(role);
this._updatedAt = new Date();
}
/**
* Remove a role from the user
* Cannot remove the last role
* Cannot remove owner role (must be transferred first)
*/
removeRole(role: UserRole): void {
const roleIndex = this._roles.findIndex(r => r.equals(role));
if (roleIndex === -1) {
throw new AdminDomainInvariantError(`Role ${role.value} not found`);
}
if (this._roles.length === 1) {
throw new AdminDomainInvariantError('Cannot remove the last role from user');
}
if (role.value === 'owner') {
throw new AdminDomainInvariantError('Cannot remove owner role. Transfer ownership first.');
}
this._roles.splice(roleIndex, 1);
this._updatedAt = new Date();
}
/**
* Update user status
*/
updateStatus(newStatus: UserStatus): void {
if (this._status.equals(newStatus)) {
throw new AdminDomainInvariantError(`User already has status ${newStatus.value}`);
}
this._status = newStatus;
this._updatedAt = new Date();
}
/**
* Check if user has a specific role
*/
hasRole(roleValue: string): boolean {
return this._roles.some(r => r.value === roleValue);
}
/**
* Check if user is a system administrator
*/
isSystemAdmin(): boolean {
return this._roles.some(r => r.isSystemAdmin());
}
/**
* Check if user has higher authority than another user
*/
hasHigherAuthorityThan(other: AdminUser): boolean {
// Get highest role for each user
const hierarchy: Record<string, number> = {
user: 0,
admin: 1,
owner: 2,
};
const myHighest = Math.max(...this._roles.map(r => hierarchy[r.value] ?? 0));
const otherHighest = Math.max(...other._roles.map(r => hierarchy[r.value] ?? 0));
return myHighest > otherHighest;
}
/**
* Update last login timestamp
*/
recordLogin(): void {
this._lastLoginAt = new Date();
this._updatedAt = new Date();
}
/**
* Update display name (only for admin operations)
*/
updateDisplayName(newName: string): void {
const trimmed = newName.trim();
if (trimmed.length < 2 || trimmed.length > 100) {
throw new AdminDomainValidationError('Display name must be between 2 and 100 characters');
}
this._displayName = trimmed;
this._updatedAt = new Date();
}
/**
* Update email
*/
updateEmail(newEmail: Email): void {
if (this._email.equals(newEmail)) {
throw new AdminDomainInvariantError('Email is already the same');
}
this._email = newEmail;
this._updatedAt = new Date();
}
/**
* Check if user is active
*/
isActive(): boolean {
return this._status.isActive();
}
/**
* Suspend user
*/
suspend(): void {
if (this._status.isSuspended()) {
throw new AdminDomainInvariantError('User is already suspended');
}
if (this._status.isDeleted()) {
throw new AdminDomainInvariantError('Cannot suspend a deleted user');
}
this._status = UserStatus.create('suspended');
this._updatedAt = new Date();
}
/**
* Activate user
*/
activate(): void {
if (this._status.isActive()) {
throw new AdminDomainInvariantError('User is already active');
}
if (this._status.isDeleted()) {
throw new AdminDomainInvariantError('Cannot activate a deleted user');
}
this._status = UserStatus.create('active');
this._updatedAt = new Date();
}
/**
* Soft delete user
*/
delete(): void {
if (this._status.isDeleted()) {
throw new AdminDomainInvariantError('User is already deleted');
}
this._status = UserStatus.create('deleted');
this._updatedAt = new Date();
}
/**
* Get role display names
*/
getRoleDisplayNames(): string[] {
return this._roles.map(r => {
switch (r.value) {
case 'owner': return 'Owner';
case 'admin': return 'Admin';
case 'user': return 'User';
default: return r.value;
}
});
}
/**
* Check if this user can manage another user
* Owner can manage everyone (including self)
* Admin can manage users but not admins/owners (including self)
* User can manage self only
*/
canManage(target: AdminUser): boolean {
// Owner can manage everyone
if (this.hasRole('owner')) {
return true;
}
// Admin can manage non-admin users
if (this.hasRole('admin')) {
// Cannot manage admins/owners (including self)
if (target.isSystemAdmin()) {
return false;
}
// Can manage non-admin users
return true;
}
// User can only manage self
return this.id.equals(target.id);
}
/**
* Check if this user can modify roles of target user
* Only owner can modify roles
*/
canModifyRoles(target: AdminUser): boolean {
// Only owner can modify roles
if (!this.hasRole('owner')) {
return false;
}
// Cannot modify own roles (prevents accidental lockout)
if (this.id.equals(target.id)) {
return false;
}
return true;
}
/**
* Check if this user can change status of target user
* Owner can change anyone's status
* Admin can change user status but not other admins/owners
*/
canChangeStatus(target: AdminUser): boolean {
if (this.id.equals(target.id)) {
return false; // Cannot change own status
}
if (this.hasRole('owner')) {
return true;
}
if (this.hasRole('admin')) {
return !target.isSystemAdmin();
}
return false;
}
/**
* Check if this user can delete target user
* Owner can delete anyone except self
* Admin can delete users but not admins/owners
*/
canDelete(target: AdminUser): boolean {
if (this.id.equals(target.id)) {
return false; // Cannot delete self
}
if (this.hasRole('owner')) {
return true;
}
if (this.hasRole('admin')) {
return !target.isSystemAdmin();
}
return false;
}
/**
* Get summary for display
*/
toSummary(): {
id: string;
email: string;
displayName: string;
roles: string[];
status: string;
isSystemAdmin: boolean;
lastLoginAt?: Date;
} {
const summary: {
id: string;
email: string;
displayName: string;
roles: string[];
status: string;
isSystemAdmin: boolean;
lastLoginAt?: Date;
} = {
id: this.id.value,
email: this._email.value,
displayName: this._displayName,
roles: this._roles.map(r => r.value),
status: this._status.value,
isSystemAdmin: this.isSystemAdmin(),
};
if (this._lastLoginAt) {
summary.lastLoginAt = this._lastLoginAt;
}
return summary;
}
/**
* Equals comparison
*/
equals(other?: AdminUser): boolean {
if (!other) {
return false;
}
return this.id.equals(other.id);
}
}

View File

@@ -0,0 +1,45 @@
import type { IDomainError, CommonDomainErrorKind } from '@core/shared/errors';
export abstract class AdminDomainError extends Error implements IDomainError<CommonDomainErrorKind> {
readonly type = 'domain' as const;
readonly context = 'admin-domain';
abstract readonly kind: CommonDomainErrorKind;
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}
export class AdminDomainValidationError
extends AdminDomainError
implements IDomainError<'validation'>
{
readonly kind = 'validation' as const;
constructor(message: string) {
super(message);
}
}
export class AdminDomainInvariantError
extends AdminDomainError
implements IDomainError<'invariant'>
{
readonly kind = 'invariant' as const;
constructor(message: string) {
super(message);
}
}
export class AuthorizationError
extends AdminDomainError
implements IDomainError<'authorization'>
{
readonly kind = 'authorization' as const;
constructor(message: string) {
super(message);
}
}

View File

@@ -0,0 +1,114 @@
import { AdminUser } from '../entities/AdminUser';
import { UserId } from '../value-objects/UserId';
import { Email } from '../value-objects/Email';
import { UserRole } from '../value-objects/UserRole';
import { UserStatus } from '../value-objects/UserStatus';
export interface UserFilter {
role?: UserRole;
status?: UserStatus;
email?: Email;
search?: string;
}
export interface UserSort {
field: 'email' | 'displayName' | 'createdAt' | 'lastLoginAt' | 'status';
direction: 'asc' | 'desc';
}
export interface UserPagination {
page: number;
limit: number;
}
export interface UserListQuery {
filter?: UserFilter;
sort?: UserSort | undefined;
pagination?: UserPagination | undefined;
}
export interface UserListResult {
users: AdminUser[];
total: number;
page: number;
limit: number;
totalPages: number;
}
export interface StoredAdminUser {
id: string;
email: string;
roles: string[];
status: string;
displayName: string;
createdAt: Date;
updatedAt: Date;
lastLoginAt?: Date;
primaryDriverId?: string;
}
/**
* Repository interface for AdminUser entity
* Follows clean architecture - this is an output port from application layer
*/
export interface IAdminUserRepository {
/**
* Find user by ID
*/
findById(id: UserId): Promise<AdminUser | null>;
/**
* Find user by email
*/
findByEmail(email: Email): Promise<AdminUser | null>;
/**
* Check if email exists
*/
emailExists(email: Email): Promise<boolean>;
/**
* Check if user exists by ID
*/
existsById(id: UserId): Promise<boolean>;
/**
* Check if user exists by email
*/
existsByEmail(email: Email): Promise<boolean>;
/**
* List users with filtering, sorting, and pagination
*/
list(query?: UserListQuery): Promise<UserListResult>;
/**
* Count users matching filter
*/
count(filter?: UserFilter): Promise<number>;
/**
* Create a new user
*/
create(user: AdminUser): Promise<AdminUser>;
/**
* Update existing user
*/
update(user: AdminUser): Promise<AdminUser>;
/**
* Delete user (soft delete)
*/
delete(id: UserId): Promise<void>;
/**
* Get user for storage
*/
toStored(user: AdminUser): StoredAdminUser;
/**
* Rehydrate user from storage
*/
fromStored(stored: StoredAdminUser): AdminUser;
}

View File

@@ -0,0 +1,747 @@
import { AuthorizationService } from './AuthorizationService';
import { AdminUser } from '../entities/AdminUser';
describe('AuthorizationService', () => {
describe('TDD - Test First', () => {
describe('canListUsers', () => {
it('should allow owner to list users', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
// Act
const canList = AuthorizationService.canListUsers(owner);
// Assert
expect(canList).toBe(true);
});
it('should allow admin to list users', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
// Act
const canList = AuthorizationService.canListUsers(admin);
// Assert
expect(canList).toBe(true);
});
it('should deny regular user from listing users', () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canList = AuthorizationService.canListUsers(user);
// Assert
expect(canList).toBe(false);
});
it('should deny suspended admin from listing users', () => {
// Arrange
const suspendedAdmin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'suspended',
});
// Act
const canList = AuthorizationService.canListUsers(suspendedAdmin);
// Assert
expect(canList).toBe(false);
});
it('should allow owner with multiple roles to list users', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
// Act
const canList = AuthorizationService.canListUsers(owner);
// Assert
expect(canList).toBe(true);
});
});
describe('canPerformAction with manage', () => {
it('should allow owner to manage any user', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canManage = AuthorizationService.canPerformAction(owner, 'manage', targetUser);
// Assert
expect(canManage).toBe(true);
});
it('should allow admin to manage non-admin users', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canManage = AuthorizationService.canPerformAction(admin, 'manage', targetUser);
// Assert
expect(canManage).toBe(true);
});
it('should deny admin from managing other admins', () => {
// Arrange
const admin1 = AdminUser.create({
id: 'admin-1',
email: 'admin1@example.com',
displayName: 'Admin 1',
roles: ['admin'],
status: 'active',
});
const admin2 = AdminUser.create({
id: 'admin-2',
email: 'admin2@example.com',
displayName: 'Admin 2',
roles: ['admin'],
status: 'active',
});
// Act
const canManage = AuthorizationService.canPerformAction(admin1, 'manage', admin2);
// Assert
expect(canManage).toBe(false);
});
it('should deny regular user from managing anyone', () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
});
// Act
const canManage = AuthorizationService.canPerformAction(user, 'manage', targetUser);
// Assert
expect(canManage).toBe(false);
});
it('should allow admin to manage suspended users', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
const suspendedUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'suspended',
});
// Act
const canManage = AuthorizationService.canPerformAction(admin, 'manage', suspendedUser);
// Assert
expect(canManage).toBe(true);
});
});
describe('canPerformAction with modify_roles', () => {
it('should allow owner to modify roles', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canModify = AuthorizationService.canPerformAction(owner, 'modify_roles', targetUser);
// Assert
expect(canModify).toBe(true);
});
it('should deny admin from modifying roles', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canModify = AuthorizationService.canPerformAction(admin, 'modify_roles', targetUser);
// Assert
expect(canModify).toBe(false);
});
it('should deny regular user from modifying roles', () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
});
// Act
const canModify = AuthorizationService.canPerformAction(user, 'modify_roles', targetUser);
// Assert
expect(canModify).toBe(false);
});
});
describe('canPerformAction with change_status', () => {
it('should allow owner to change any status', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canChange = AuthorizationService.canPerformAction(owner, 'change_status', targetUser);
// Assert
expect(canChange).toBe(true);
});
it('should allow admin to change non-admin status', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canChange = AuthorizationService.canPerformAction(admin, 'change_status', targetUser);
// Assert
expect(canChange).toBe(true);
});
it('should deny admin from changing admin status', () => {
// Arrange
const admin1 = AdminUser.create({
id: 'admin-1',
email: 'admin1@example.com',
displayName: 'Admin 1',
roles: ['admin'],
status: 'active',
});
const admin2 = AdminUser.create({
id: 'admin-2',
email: 'admin2@example.com',
displayName: 'Admin 2',
roles: ['admin'],
status: 'active',
});
// Act
const canChange = AuthorizationService.canPerformAction(admin1, 'change_status', admin2);
// Assert
expect(canChange).toBe(false);
});
it('should deny regular user from changing status', () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
});
// Act
const canChange = AuthorizationService.canPerformAction(user, 'change_status', targetUser);
// Assert
expect(canChange).toBe(false);
});
});
describe('canPerformAction with delete', () => {
it('should allow owner to delete any user', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canDelete = AuthorizationService.canPerformAction(owner, 'delete', targetUser);
// Assert
expect(canDelete).toBe(true);
});
it('should allow admin to delete non-admin users', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const canDelete = AuthorizationService.canPerformAction(admin, 'delete', targetUser);
// Assert
expect(canDelete).toBe(true);
});
it('should deny admin from deleting other admins', () => {
// Arrange
const admin1 = AdminUser.create({
id: 'admin-1',
email: 'admin1@example.com',
displayName: 'Admin 1',
roles: ['admin'],
status: 'active',
});
const admin2 = AdminUser.create({
id: 'admin-2',
email: 'admin2@example.com',
displayName: 'Admin 2',
roles: ['admin'],
status: 'active',
});
// Act
const canDelete = AuthorizationService.canPerformAction(admin1, 'delete', admin2);
// Assert
expect(canDelete).toBe(false);
});
it('should deny regular user from deleting anyone', () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
});
// Act
const canDelete = AuthorizationService.canPerformAction(user, 'delete', targetUser);
// Assert
expect(canDelete).toBe(false);
});
it('should allow owner to delete suspended users', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const suspendedUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'suspended',
});
// Act
const canDelete = AuthorizationService.canPerformAction(owner, 'delete', suspendedUser);
// Assert
expect(canDelete).toBe(true);
});
});
describe('getPermissions', () => {
it('should return correct permissions for owner', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
// Act
const permissions = AuthorizationService.getPermissions(owner);
// Assert
expect(permissions).toContain('users.view');
expect(permissions).toContain('users.list');
expect(permissions).toContain('users.manage');
expect(permissions).toContain('users.roles.modify');
expect(permissions).toContain('users.status.change');
expect(permissions).toContain('users.create');
expect(permissions).toContain('users.delete');
expect(permissions).toContain('users.export');
});
it('should return correct permissions for admin', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
// Act
const permissions = AuthorizationService.getPermissions(admin);
// Assert
expect(permissions).toContain('users.view');
expect(permissions).toContain('users.list');
expect(permissions).toContain('users.manage');
expect(permissions).toContain('users.status.change');
expect(permissions).toContain('users.create');
expect(permissions).toContain('users.delete');
expect(permissions).not.toContain('users.roles.modify');
expect(permissions).not.toContain('users.export');
});
it('should return empty permissions for regular user', () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const permissions = AuthorizationService.getPermissions(user);
// Assert
expect(permissions).toEqual([]);
});
it('should return empty permissions for suspended admin', () => {
// Arrange
const suspendedAdmin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'suspended',
});
// Act
const permissions = AuthorizationService.getPermissions(suspendedAdmin);
// Assert
expect(permissions).toEqual([]);
});
});
describe('hasPermission', () => {
it('should return true for owner with users.view permission', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
// Act
const hasPermission = AuthorizationService.hasPermission(owner, 'users.view');
// Assert
expect(hasPermission).toBe(true);
});
it('should return false for admin with users.roles.modify permission', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
// Act
const hasPermission = AuthorizationService.hasPermission(admin, 'users.roles.modify');
// Assert
expect(hasPermission).toBe(false);
});
it('should return false for regular user with any permission', () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act
const hasPermission = AuthorizationService.hasPermission(user, 'users.view');
// Assert
expect(hasPermission).toBe(false);
});
});
describe('enforce', () => {
it('should not throw for authorized action', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
// Act & Assert - Should not throw
expect(() => {
AuthorizationService.enforce(owner, 'manage', targetUser);
}).not.toThrow();
});
it('should throw for unauthorized action', () => {
// Arrange
const user = AdminUser.create({
id: 'user-1',
email: 'user@example.com',
displayName: 'User',
roles: ['user'],
status: 'active',
});
const targetUser = AdminUser.create({
id: 'user-2',
email: 'user2@example.com',
displayName: 'User 2',
roles: ['user'],
status: 'active',
});
// Act & Assert - Should throw
expect(() => {
AuthorizationService.enforce(user, 'manage', targetUser);
}).toThrow();
});
});
describe('enforcePermission', () => {
it('should not throw for authorized permission', () => {
// Arrange
const owner = AdminUser.create({
id: 'owner-1',
email: 'owner@example.com',
displayName: 'Owner',
roles: ['owner'],
status: 'active',
});
// Act & Assert - Should not throw
expect(() => {
AuthorizationService.enforcePermission(owner, 'users.view');
}).not.toThrow();
});
it('should throw for unauthorized permission', () => {
// Arrange
const admin = AdminUser.create({
id: 'admin-1',
email: 'admin@example.com',
displayName: 'Admin',
roles: ['admin'],
status: 'active',
});
// Act & Assert - Should throw
expect(() => {
AuthorizationService.enforcePermission(admin, 'users.roles.modify');
}).toThrow();
});
});
});
});

View File

@@ -0,0 +1,283 @@
import { AdminUser } from '../entities/AdminUser';
import { AuthorizationError } from '../errors/AdminDomainError';
/**
* Domain service for authorization checks
* Stateless service that enforces access control rules
*/
export class AuthorizationService {
/**
* Check if an actor can perform an action on a target user
*/
static canPerformAction(
actor: AdminUser,
action: 'view' | 'manage' | 'modify_roles' | 'change_status' | 'delete',
target?: AdminUser
): boolean {
// Actors must be system admins and active
if (!actor.isSystemAdmin() || !actor.isActive()) {
return false;
}
switch (action) {
case 'view':
return this.canView(actor, target);
case 'manage':
return this.canManage(actor, target);
case 'modify_roles':
return this.canModifyRoles(actor, target);
case 'change_status':
return this.canChangeStatus(actor, target);
case 'delete':
return this.canDelete(actor, target);
default:
return false;
}
}
/**
* Check if actor can view target user
*/
private static canView(actor: AdminUser, target?: AdminUser): boolean {
if (!target) {
// Viewing list - only admins can view
return actor.isSystemAdmin();
}
// Can always view self
if (actor.id.equals(target.id)) {
return true;
}
// Owner can view everyone
if (actor.hasRole('owner')) {
return true;
}
// Admin can view non-admin users
if (actor.hasRole('admin')) {
return !target.isSystemAdmin();
}
return false;
}
/**
* Check if actor can manage target user
*/
private static canManage(actor: AdminUser, target?: AdminUser): boolean {
if (!target) {
return false;
}
// Can always manage self
if (actor.id.equals(target.id)) {
return true;
}
// Owner can manage everyone
if (actor.hasRole('owner')) {
return true;
}
// Admin can manage non-admin users
if (actor.hasRole('admin')) {
return !target.isSystemAdmin();
}
return false;
}
/**
* Check if actor can modify roles of target user
*/
private static canModifyRoles(actor: AdminUser, target?: AdminUser): boolean {
if (!target) {
return false;
}
// Only owner can modify roles
if (!actor.hasRole('owner')) {
return false;
}
// Cannot modify own roles (prevents accidental lockout)
if (actor.id.equals(target.id)) {
return false;
}
return true;
}
/**
* Check if actor can change status of target user
*/
private static canChangeStatus(actor: AdminUser, target?: AdminUser): boolean {
if (!target) {
return false;
}
// Cannot change own status
if (actor.id.equals(target.id)) {
return false;
}
// Owner can change anyone's status
if (actor.hasRole('owner')) {
return true;
}
// Admin can change user status but not other admins/owners
if (actor.hasRole('admin')) {
return !target.isSystemAdmin();
}
return false;
}
/**
* Check if actor can delete target user
*/
private static canDelete(actor: AdminUser, target?: AdminUser): boolean {
if (!target) {
return false;
}
// Cannot delete self
if (actor.id.equals(target.id)) {
return false;
}
// Owner can delete anyone
if (actor.hasRole('owner')) {
return true;
}
// Admin can delete users but not admins/owners
if (actor.hasRole('admin')) {
return !target.isSystemAdmin();
}
return false;
}
/**
* Enforce authorization - throws if not authorized
*/
static enforce(
actor: AdminUser,
action: 'view' | 'manage' | 'modify_roles' | 'change_status' | 'delete',
target?: AdminUser
): void {
if (!this.canPerformAction(actor, action, target)) {
const actionLabel = action.replace('_', ' ');
const targetLabel = target ? `user ${target.email.value}` : 'user list';
throw new AuthorizationError(
`User ${actor.email.value} is not authorized to ${actionLabel} ${targetLabel}`
);
}
}
/**
* Check if actor can list users
*/
static canListUsers(actor: AdminUser): boolean {
return actor.isSystemAdmin() && actor.isActive();
}
/**
* Check if actor can create users
*/
static canCreateUsers(actor: AdminUser): boolean {
// Only owner can create users with admin roles
// Admin can create regular users
return actor.isSystemAdmin() && actor.isActive();
}
/**
* Check if actor can assign a specific role
*/
static canAssignRole(actor: AdminUser, roleToAssign: string, target?: AdminUser): boolean {
// Only owner can assign owner or admin roles
if (roleToAssign === 'owner' || roleToAssign === 'admin') {
return actor.hasRole('owner');
}
// Admin can assign user role
if (roleToAssign === 'user') {
// Admin can assign user role to non-admin users
// Owner can assign user role to anyone
if (actor.hasRole('owner')) {
return true;
}
if (actor.hasRole('admin')) {
if (!target) {
return true;
}
return !target.isSystemAdmin();
}
}
return false;
}
/**
* Get permissions for actor
*/
static getPermissions(actor: AdminUser): string[] {
const permissions: string[] = [];
if (!actor.isSystemAdmin() || !actor.isActive()) {
return permissions;
}
// Base permissions for all admins
permissions.push(
'users.view',
'users.list'
);
// Admin permissions
if (actor.hasRole('admin')) {
permissions.push(
'users.manage',
'users.status.change',
'users.create',
'users.delete'
);
}
// Owner permissions
if (actor.hasRole('owner')) {
permissions.push(
'users.manage',
'users.roles.modify',
'users.status.change',
'users.create',
'users.delete',
'users.export'
);
}
return permissions;
}
/**
* Check if actor has specific permission
*/
static hasPermission(actor: AdminUser, permission: string): boolean {
const permissions = this.getPermissions(actor);
return permissions.includes(permission);
}
/**
* Enforce permission - throws if not authorized
*/
static enforcePermission(actor: AdminUser, permission: string): void {
if (!this.hasPermission(actor, permission)) {
throw new AuthorizationError(
`User ${actor.email.value} does not have permission: ${permission}`
);
}
}
}

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