Files
Autonomous-Bug-Explorer/tests/modules/auth.test.ts
debian 08011d22d5 fase(25-26): keyboard shortcuts, mobile responsive, enterprise SSO/audit
- Phase 25.4: N shortcut for new exploration on dashboard (react-hotkeys-hook)
- Phase 25.5: overflow-x-auto on tables, responsive padding (p-4 md:p-6)
- Phase 26: SAML/OIDC/LDAP providers (build-fixed), TOTP/MFA service
- Phase 26: KyselySSOConfigRepository + KyselyTOTPRepository
- Phase 26: SSO HTTP controller (config CRUD + MFA setup/verify/disable)
- Phase 26: Audit module index.ts + SSO module index.ts
- Phase 26: Session management endpoints (findByUserId, deleteById, list/revoke)
- Phase 26: SSO and audit routes feature-gated (auth:sso, audit:logs)
- Phase 26: Frontend SSOSection (SAML/OIDC/LDAP config + TOTP setup)
- Phase 26: Frontend SessionsSection (list/revoke active sessions)
- Phase 26: Settings navigation updated with SSO & Sessions sections

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 13:38:25 -04:00

307 lines
12 KiB
TypeScript

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<void> { this.published.push(event); }
subscribe(_name: string, _handler: EventHandler): void {}
}
// ─── Mock User Repository ──────────────────────────────────────────────────────
class InMemoryUserRepository implements IUserRepository {
private store = new Map<string, User>();
async save(user: User): Promise<void> {
this.store.set(user.id.toString(), user);
}
async findById(id: string): Promise<User | undefined> {
return this.store.get(id);
}
async findByEmail(email: string): Promise<User | undefined> {
return Array.from(this.store.values()).find(u => u.email.value === email);
}
async findAll(): Promise<User[]> {
return Array.from(this.store.values());
}
async count(): Promise<number> {
return this.store.size;
}
}
// ─── Mock Session Repository ───────────────────────────────────────────────────
class InMemorySessionRepository implements ISessionRepository {
private store = new Map<string, AuthSession>();
async save(session: AuthSession): Promise<void> {
this.store.set(session.token, session);
}
async findByToken(token: string): Promise<AuthSession | undefined> {
return this.store.get(token);
}
async deleteByToken(token: string): Promise<void> {
this.store.delete(token);
}
async deleteExpired(): Promise<void> {
const now = new Date();
for (const [token, session] of this.store) {
if (session.expiresAt < now) this.store.delete(token);
}
}
async findByUserId(userId: string): Promise<AuthSession[]> {
return Array.from(this.store.values()).filter(s => s.userId === userId);
}
async deleteById(id: string): Promise<void> {
for (const [token, s] of this.store) { if (s.id === id) { this.store.delete(token); break; } }
}
}
// ─── 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);
});
});