import { User } from '../../src/modules/auth/domain/entities/User'; import { Organization } from '../../src/modules/auth/domain/entities/Organization'; import { Email } from '../../src/modules/auth/domain/value-objects/Email'; import { Role } from '../../src/modules/auth/domain/value-objects/Role'; import { RegisterCommand } from '../../src/modules/auth/application/commands/RegisterCommand'; import { LoginCommand } from '../../src/modules/auth/application/commands/LoginCommand'; import { IUserRepository } from '../../src/modules/auth/domain/ports/IUserRepository'; import { ISessionRepository, AuthSession } from '../../src/modules/auth/domain/ports/ISessionRepository'; import { EventBus } from '../../src/shared/application/EventBus'; import { DomainEvent } from '../../src/shared/domain/DomainEvent'; import { EventHandler } from '../../src/shared/application/EventHandler'; import { defineAbilityFor } from '../../src/modules/auth/infrastructure/casl/AbilityFactory'; // ─── Mock EventBus ───────────────────────────────────────────────────────────── class MockEventBus implements EventBus { published: DomainEvent[] = []; async publish(event: DomainEvent): Promise { this.published.push(event); } subscribe(_name: string, _handler: EventHandler): void {} } // ─── Mock User Repository ────────────────────────────────────────────────────── class InMemoryUserRepository implements IUserRepository { private store = new Map(); async save(user: User): Promise { this.store.set(user.id.toString(), user); } async findById(id: string): Promise { return this.store.get(id); } async findByEmail(email: string): Promise { return Array.from(this.store.values()).find(u => u.email.value === email); } async findAll(): Promise { return Array.from(this.store.values()); } async count(): Promise { return this.store.size; } } // ─── Mock Session Repository ─────────────────────────────────────────────────── class InMemorySessionRepository implements ISessionRepository { private store = new Map(); async save(session: AuthSession): Promise { this.store.set(session.token, session); } async findByToken(token: string): Promise { return this.store.get(token); } async deleteByToken(token: string): Promise { this.store.delete(token); } async deleteExpired(): Promise { const now = new Date(); for (const [token, session] of this.store) { if (session.expiresAt < now) this.store.delete(token); } } } // ─── Tests: Email Value Object ───────────────────────────────────────────────── describe('Email value object', () => { it('creates valid email', () => { const email = Email.create('Test@Example.COM'); expect(email.value).toBe('test@example.com'); }); it('throws for invalid email', () => { expect(() => Email.create('not-an-email')).toThrow('Invalid email address'); }); it('equals by value', () => { const a = Email.create('user@example.com'); const b = Email.create('user@example.com'); expect(a.equals(b)).toBe(true); }); }); // ─── Tests: Role Value Object ────────────────────────────────────────────────── describe('Role value object', () => { it('creates valid roles', () => { expect(Role.owner().value).toBe('owner'); expect(Role.admin().value).toBe('admin'); expect(Role.member().value).toBe('member'); expect(Role.viewer().value).toBe('viewer'); }); it('throws for invalid role', () => { expect(() => Role.create('superadmin')).toThrow('Invalid role'); }); it('checks role type', () => { expect(Role.owner().isOwner()).toBe(true); expect(Role.admin().isAdmin()).toBe(true); expect(Role.member().isMember()).toBe(true); expect(Role.viewer().isViewer()).toBe(true); }); }); // ─── Tests: User Aggregate ───────────────────────────────────────────────────── describe('User aggregate', () => { it('creates user and emits UserCreated event', () => { const user = User.create({ email: Email.create('alice@example.com'), name: 'Alice', passwordHash: 'hash', role: Role.owner(), }); expect(user.email.value).toBe('alice@example.com'); expect(user.name).toBe('Alice'); expect(user.role.isOwner()).toBe(true); expect(user.domainEvents).toHaveLength(1); expect(user.domainEvents[0]!.eventName).toBe('auth.user.created'); }); it('assigns user to org', () => { const user = User.create({ email: Email.create('bob@example.com'), name: 'Bob', passwordHash: 'hash', role: Role.member(), }); user.assignToOrg('org-123'); expect(user.orgId).toBe('org-123'); }); }); // ─── Tests: Organization Aggregate ──────────────────────────────────────────── describe('Organization aggregate', () => { it('creates org with slug and emits OrgCreated', () => { const org = Organization.create({ name: 'Acme Corp', slug: 'acme-corp' }); expect(org.name).toBe('Acme Corp'); expect(org.slug).toBe('acme-corp'); expect(org.domainEvents[0]!.eventName).toBe('auth.org.created'); }); it('slugifies name correctly', () => { expect(Organization.slugify('My Awesome Org!')).toBe('my-awesome-org'); expect(Organization.slugify(' hello world ')).toBe('hello-world'); }); }); // ─── Tests: RegisterCommand ──────────────────────────────────────────────────── describe('RegisterCommand', () => { let userRepo: InMemoryUserRepository; let eventBus: MockEventBus; let registerCommand: RegisterCommand; beforeEach(() => { userRepo = new InMemoryUserRepository(); eventBus = new MockEventBus(); registerCommand = new RegisterCommand(userRepo, eventBus, async (p) => `hash:${p}`); }); it('registers a new user successfully', async () => { const result = await registerCommand.execute({ email: 'alice@example.com', password: 'password123', name: 'Alice', }); expect(result.ok).toBe(true); if (result.ok) { expect(result.value.email).toBe('alice@example.com'); expect(result.value.role).toBe('member'); } expect(await userRepo.count()).toBe(1); expect(eventBus.published).toHaveLength(1); expect(eventBus.published[0]!.eventName).toBe('auth.user.created'); }); it('fails if email already registered', async () => { await registerCommand.execute({ email: 'alice@example.com', password: 'pass1234', name: 'Alice' }); const result = await registerCommand.execute({ email: 'alice@example.com', password: 'pass5678', name: 'Alice2' }); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toContain('already registered'); }); it('fails if password too short', async () => { const result = await registerCommand.execute({ email: 'bob@test.com', password: 'short', name: 'Bob' }); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toContain('8 characters'); }); it('fails with invalid email', async () => { const result = await registerCommand.execute({ email: 'not-an-email', password: 'password123', name: 'Bob' }); expect(result.ok).toBe(false); }); it('registers with custom role', async () => { const result = await registerCommand.execute({ email: 'admin@example.com', password: 'password123', name: 'Admin', role: 'owner', }); expect(result.ok).toBe(true); if (result.ok) expect(result.value.role).toBe('owner'); }); }); // ─── Tests: LoginCommand ─────────────────────────────────────────────────────── describe('LoginCommand', () => { let userRepo: InMemoryUserRepository; let sessionRepo: InMemorySessionRepository; let eventBus: MockEventBus; let registerCommand: RegisterCommand; let loginCommand: LoginCommand; beforeEach(async () => { userRepo = new InMemoryUserRepository(); sessionRepo = new InMemorySessionRepository(); eventBus = new MockEventBus(); registerCommand = new RegisterCommand(userRepo, eventBus, async (p) => `hash:${p}`); loginCommand = new LoginCommand( userRepo, sessionRepo, eventBus, async (password, hash) => hash === `hash:${password}` ); await registerCommand.execute({ email: 'alice@example.com', password: 'password123', name: 'Alice' }); eventBus.published = []; }); it('logs in with correct credentials', async () => { const result = await loginCommand.execute({ email: 'alice@example.com', password: 'password123' }); expect(result.ok).toBe(true); if (result.ok) { expect(result.value.sessionToken).toBeTruthy(); expect(result.value.role).toBe('member'); } expect(eventBus.published[0]!.eventName).toBe('auth.user.logged_in'); }); it('fails with wrong password', async () => { const result = await loginCommand.execute({ email: 'alice@example.com', password: 'wrongpass' }); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toBe('Invalid credentials'); }); it('fails with unknown email', async () => { const result = await loginCommand.execute({ email: 'nobody@example.com', password: 'password123' }); expect(result.ok).toBe(false); }); it('creates session in repository', async () => { const result = await loginCommand.execute({ email: 'alice@example.com', password: 'password123' }); expect(result.ok).toBe(true); if (result.ok) { const session = await sessionRepo.findByToken(result.value.sessionToken); expect(session).toBeDefined(); expect(session!.userId).toBeTruthy(); } }); }); // ─── Tests: CASL AbilityFactory ──────────────────────────────────────────────── describe('CASL AbilityFactory', () => { it('owner can manage all', () => { const ability = defineAbilityFor('owner'); expect(ability.can('manage', 'all')).toBe(true); }); it('admin can manage sessions but not delete org or manage license', () => { const ability = defineAbilityFor('admin'); expect(ability.can('create', 'Session')).toBe(true); expect(ability.can('delete', 'Organization')).toBe(false); expect(ability.can('manage', 'License')).toBe(false); expect(ability.can('read', 'License')).toBe(true); }); it('member can create sessions/findings/reports and read all', () => { const ability = defineAbilityFor('member'); expect(ability.can('create', 'Session')).toBe(true); expect(ability.can('read', 'Session')).toBe(true); expect(ability.can('delete', 'Session')).toBe(false); expect(ability.can('update', 'Finding')).toBe(true); }); it('viewer can only read', () => { const ability = defineAbilityFor('viewer'); expect(ability.can('read', 'Session')).toBe(true); expect(ability.can('create', 'Session')).toBe(false); expect(ability.can('delete', 'Finding')).toBe(false); }); });