Files
gridpilot.gg/adapters/bootstrap/SeedDemoUsers.test.ts
2026-01-16 15:20:25 +01:00

435 lines
18 KiB
TypeScript

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 driver, owner, steward, admin, systemowner, superadmin have primaryDriverId
const usersWithPrimaryDriverId = saveCalls.filter((call: any) => {
const user: User = call[0];
return user.getPrimaryDriverId() !== undefined;
});
expect(usersWithPrimaryDriverId.length).toBe(6); // All except sponsor
});
});
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')
);
});
});
});