import { AdminUser } from '@core/admin/domain/entities/AdminUser'; import { Email } from '@core/admin/domain/value-objects/Email'; import { UserRole } from '@core/admin/domain/value-objects/UserRole'; import { User } from '@core/identity/domain/entities/User'; import { EmailAddress } from '@core/identity/domain/value-objects/EmailAddress'; import { PasswordHash } from '@core/identity/domain/value-objects/PasswordHash'; import { UserId } from '@core/identity/domain/value-objects/UserId'; import type { Logger } from '@core/shared/domain/Logger'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; // Import the class we're testing (will be created) import { SeedDemoUsers } from './SeedDemoUsers'; describe('SeedDemoUsers', () => { const originalEnv = { ...process.env }; let logger: Logger; let authRepository: IAuthRepository; let passwordHashingService: IPasswordHashingService; let adminUserRepository: IAdminUserRepository; beforeEach(() => { vi.clearAllMocks(); process.env = { ...originalEnv }; logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }; // Mock password hashing service passwordHashingService = { hash: vi.fn().mockImplementation(async (plain: string) => { return `hashed_${plain}`; }), verify: vi.fn(), }; // Mock auth repository authRepository = { findByEmail: vi.fn(), save: vi.fn(), }; // Mock admin user repository adminUserRepository = { findById: vi.fn(), findByEmail: vi.fn(), emailExists: vi.fn(), existsById: vi.fn(), existsByEmail: vi.fn(), list: vi.fn(), count: vi.fn(), create: vi.fn(), update: vi.fn(), delete: vi.fn(), toStored: vi.fn(), fromStored: vi.fn(), }; }); afterEach(() => { process.env = originalEnv; }); describe('Demo user specification', () => { it('should define the correct demo users with fixed emails', async () => { const seed = new SeedDemoUsers(logger, authRepository, passwordHashingService, adminUserRepository); const expectedEmails = [ 'demo.driver@example.com', 'demo.sponsor@example.com', 'demo.owner@example.com', 'demo.steward@example.com', 'demo.admin@example.com', 'demo.systemowner@example.com', 'demo.superadmin@example.com', ]; // Mock repositories to return null (users don't exist) (authRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.create as any).mockImplementation((user: AdminUser) => user); (authRepository.save as any).mockResolvedValue(undefined); await seed.execute(); // Verify that findByEmail was called for each expected email const calls = (authRepository.findByEmail as any).mock.calls; const emailsCalled = calls.map((call: any) => call[0].value); expect(emailsCalled).toEqual(expect.arrayContaining(expectedEmails)); expect(emailsCalled.length).toBeGreaterThanOrEqual(7); }); it('should use the fixed password "Demo1234!"', async () => { const seed = new SeedDemoUsers(logger, authRepository, passwordHashingService, adminUserRepository); // Mock repositories to return null (users don't exist) (authRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.create as any).mockImplementation((user: AdminUser) => user); (authRepository.save as any).mockResolvedValue(undefined); await seed.execute(); // Verify password hashing was called with the correct password expect(passwordHashingService.hash).toHaveBeenCalledWith('Demo1234!'); }); it('should generate deterministic IDs using SeedIdHelper', async () => { // Set postgres mode for this test process.env.DATABASE_URL = 'postgresql://localhost/test'; delete process.env.GRIDPILOT_API_PERSISTENCE; delete process.env.NODE_ENV; const seed = new SeedDemoUsers(logger, authRepository, passwordHashingService, adminUserRepository); // Mock repositories to return null (users don't exist) (authRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.create as any).mockImplementation((user: AdminUser) => user); (authRepository.save as any).mockResolvedValue(undefined); await seed.execute(); // Verify that users were saved with UUIDs const saveCalls = (authRepository.save as any).mock.calls; expect(saveCalls.length).toBeGreaterThanOrEqual(7); // Check that IDs are UUIDs (deterministic from seed keys) for (const call of saveCalls) { const user: User = call[0]; const id = user.getId().value; // Should be a valid UUID format expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); } }); it('should create primaryDriverId for roles that need it', async () => { const seed = new SeedDemoUsers(logger, authRepository, passwordHashingService, adminUserRepository); // Mock repositories to return null (users don't exist) (authRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.create as any).mockImplementation((user: AdminUser) => user); (authRepository.save as any).mockResolvedValue(undefined); await seed.execute(); const saveCalls = (authRepository.save as any).mock.calls; // Check that all users have primaryDriverId const usersWithPrimaryDriverId = saveCalls.filter((call: any) => { const user: User = call[0]; return user.getPrimaryDriverId() !== undefined; }); expect(usersWithPrimaryDriverId.length).toBe(7); // All users }); }); describe('Idempotency', () => { it('should not create duplicates when execute() is called twice', async () => { const seed = new SeedDemoUsers(logger, authRepository, passwordHashingService, adminUserRepository); // First execution - users don't exist (authRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.create as any).mockImplementation((user: AdminUser) => user); (authRepository.save as any).mockResolvedValue(undefined); await seed.execute(); const firstSaveCount = (authRepository.save as any).mock.calls.length; const firstAdminCreateCount = (adminUserRepository.create as any).mock.calls.length; // Reset mocks vi.clearAllMocks(); // Second execution - users now exist (authRepository.findByEmail as any).mockImplementation(async (email: EmailAddress) => { // Return existing user for all demo emails if (email.value.startsWith('demo.')) { const displayName = email.value === 'demo.driver@example.com' ? 'John Driver' : email.value === 'demo.sponsor@example.com' ? 'Jane Sponsor' : email.value === 'demo.owner@example.com' ? 'Alice Owner' : email.value === 'demo.steward@example.com' ? 'Bob Steward' : email.value === 'demo.admin@example.com' ? 'Charlie Admin' : email.value === 'demo.systemowner@example.com' ? 'Diana SystemOwner' : 'Edward SuperAdmin'; return User.create({ id: UserId.create(), displayName: displayName, email: email.value, passwordHash: PasswordHash.fromHash('hashed_Demo1234!'), }); } return null; }); (adminUserRepository.findByEmail as any).mockImplementation(async (email: Email) => { // Return existing admin user for admin roles if (email.value.startsWith('demo.owner') || email.value.startsWith('demo.steward') || email.value.startsWith('demo.admin') || email.value.startsWith('demo.systemowner') || email.value.startsWith('demo.superadmin')) { const displayName = email.value === 'demo.owner@example.com' ? 'Alice Owner' : email.value === 'demo.steward@example.com' ? 'Bob Steward' : email.value === 'demo.admin@example.com' ? 'Charlie Admin' : email.value === 'demo.systemowner@example.com' ? 'Diana SystemOwner' : 'Edward SuperAdmin'; return AdminUser.create({ id: UserId.create().value, email: email.value, roles: ['user'], status: 'active', displayName: displayName, }); } return null; }); await seed.execute(); const secondSaveCount = (authRepository.save as any).mock.calls.length; const secondAdminCreateCount = (adminUserRepository.create as any).mock.calls.length; // Second execution should not create any new users expect(secondSaveCount).toBe(0); expect(secondAdminCreateCount).toBe(0); }); }); describe('Admin user creation', () => { it('should create AdminUser entities for admin roles', async () => { const seed = new SeedDemoUsers(logger, authRepository, passwordHashingService, adminUserRepository); // Mock repositories (authRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.create as any).mockImplementation((user: AdminUser) => user); (authRepository.save as any).mockResolvedValue(undefined); await seed.execute(); // Verify admin users were created const adminCreateCalls = (adminUserRepository.create as any).mock.calls; // Should create admin users for: owner, steward, admin, systemowner, superadmin expect(adminCreateCalls.length).toBe(5); // Verify the roles const createdRoles = adminCreateCalls.map((call: any) => { const adminUser: AdminUser = call[0]; return adminUser.roles.map((r: UserRole) => r.value); }); // Each should have appropriate roles expect(createdRoles).toContainEqual(['owner']); expect(createdRoles).toContainEqual(['user']); // steward expect(createdRoles).toContainEqual(['admin']); expect(createdRoles).toContainEqual(['admin']); // systemowner expect(createdRoles).toContainEqual(['admin']); // superadmin }); it('should not create AdminUser for driver and sponsor roles', async () => { const seed = new SeedDemoUsers(logger, authRepository, passwordHashingService, adminUserRepository); // Mock repositories (authRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.create as any).mockImplementation((user: AdminUser) => user); (authRepository.save as any).mockResolvedValue(undefined); await seed.execute(); const adminCreateCalls = (adminUserRepository.create as any).mock.calls; // Verify no admin users were created for driver and sponsor const adminEmails = adminCreateCalls.map((call: any) => call[0].email.value); expect(adminEmails).not.toContain('demo.driver@example.com'); expect(adminEmails).not.toContain('demo.sponsor@example.com'); }); }); describe('Persistence detection', () => { it('should detect postgres persistence from DATABASE_URL', async () => { process.env.DATABASE_URL = 'postgresql://localhost/test'; delete process.env.GRIDPILOT_API_PERSISTENCE; delete process.env.NODE_ENV; const seed = new SeedDemoUsers(logger, authRepository, passwordHashingService, adminUserRepository); // Mock repositories (authRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.create as any).mockImplementation((user: AdminUser) => user); (authRepository.save as any).mockResolvedValue(undefined); await seed.execute(); // Should use UUIDs for IDs (verified by checking ID format) const saveCalls = (authRepository.save as any).mock.calls; const user: User = saveCalls[0][0]; const id = user.getId().value; // UUID format expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); }); it('should detect inmemory persistence in test environment', async () => { process.env.NODE_ENV = 'test'; delete process.env.DATABASE_URL; delete process.env.GRIDPILOT_API_PERSISTENCE; const seed = new SeedDemoUsers(logger, authRepository, passwordHashingService, adminUserRepository); // Mock repositories (authRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.create as any).mockImplementation((user: AdminUser) => user); (authRepository.save as any).mockResolvedValue(undefined); await seed.execute(); // Should use deterministic string IDs const saveCalls = (authRepository.save as any).mock.calls; const user: User = saveCalls[0][0]; const id = user.getId().value; // Should be a deterministic string (not UUID) expect(id).toContain('demo-'); }); }); describe('Force reseed support', () => { it('should support force reseed via environment variable', async () => { process.env.GRIDPILOT_API_FORCE_RESEED = '1'; process.env.GRIDPILOT_API_PERSISTENCE = 'inmemory'; const seed = new SeedDemoUsers(logger, authRepository, passwordHashingService, adminUserRepository); // Mock existing users const existingUser = User.create({ id: UserId.create(), displayName: 'John Driver', email: 'demo.driver@example.com', passwordHash: PasswordHash.fromHash('old_hash'), }); (authRepository.findByEmail as any).mockResolvedValue(existingUser); (adminUserRepository.findByEmail as any).mockResolvedValue(null); (authRepository.save as any).mockResolvedValue(undefined); (adminUserRepository.create as any).mockImplementation((user: AdminUser) => user); await seed.execute(); // Should still save users (force reseed means update existing) expect(authRepository.save).toHaveBeenCalled(); }); }); describe('Logging', () => { it('should log progress and results', async () => { const seed = new SeedDemoUsers(logger, authRepository, passwordHashingService, adminUserRepository); // Mock repositories (authRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.findByEmail as any).mockResolvedValue(null); (adminUserRepository.create as any).mockImplementation((user: AdminUser) => user); (authRepository.save as any).mockResolvedValue(undefined); await seed.execute(); // Should log start expect(logger.info).toHaveBeenCalledWith( expect.stringContaining('[Bootstrap] Starting demo users seed') ); // Should log completion expect(logger.info).toHaveBeenCalledWith( expect.stringContaining('[Bootstrap] Demo users seed completed') ); }); it('should log when skipping due to existing users', async () => { const seed = new SeedDemoUsers(logger, authRepository, passwordHashingService, adminUserRepository); // Mock existing users (authRepository.findByEmail as any).mockImplementation(async (email: EmailAddress) => { const displayName = email.value === 'demo.driver@example.com' ? 'John Driver' : email.value === 'demo.sponsor@example.com' ? 'Jane Sponsor' : email.value === 'demo.owner@example.com' ? 'Alice Owner' : email.value === 'demo.steward@example.com' ? 'Bob Steward' : email.value === 'demo.admin@example.com' ? 'Charlie Admin' : email.value === 'demo.systemowner@example.com' ? 'Diana SystemOwner' : 'Edward SuperAdmin'; return User.create({ id: UserId.create(), displayName: displayName, email: email.value, passwordHash: PasswordHash.fromHash('hashed_Demo1234!'), }); }); (adminUserRepository.findByEmail as any).mockImplementation(async (email: Email) => { if (email.value.startsWith('demo.owner') || email.value.startsWith('demo.steward') || email.value.startsWith('demo.admin') || email.value.startsWith('demo.systemowner') || email.value.startsWith('demo.superadmin')) { const displayName = email.value === 'demo.owner@example.com' ? 'Alice Owner' : email.value === 'demo.steward@example.com' ? 'Bob Steward' : email.value === 'demo.admin@example.com' ? 'Charlie Admin' : email.value === 'demo.systemowner@example.com' ? 'Diana SystemOwner' : 'Edward SuperAdmin'; return AdminUser.create({ id: UserId.create().value, email: email.value, roles: ['user'], status: 'active', displayName: displayName, }); } return null; }); await seed.execute(); expect(logger.info).toHaveBeenCalledWith( expect.stringContaining('[Bootstrap] Demo users already exist, skipping') ); }); }); });