remove demo code
This commit is contained in:
439
adapters/bootstrap/SeedDemoUsers.test.ts
Normal file
439
adapters/bootstrap/SeedDemoUsers.test.ts
Normal file
@@ -0,0 +1,439 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { IAuthRepository } from '@core/identity/domain/repositories/IAuthRepository';
|
||||
import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
|
||||
import type { IAdminUserRepository } from '@core/admin/domain/repositories/IAdminUserRepository';
|
||||
import { User } from '@core/identity/domain/entities/User';
|
||||
import { AdminUser } from '@core/admin/domain/entities/AdminUser';
|
||||
import { EmailAddress } from '@core/identity/domain/value-objects/EmailAddress';
|
||||
import { UserId } from '@core/identity/domain/value-objects/UserId';
|
||||
import { Email } from '@core/admin/domain/value-objects/Email';
|
||||
import { UserRole } from '@core/admin/domain/value-objects/UserRole';
|
||||
import { UserStatus } from '@core/admin/domain/value-objects/UserStatus';
|
||||
import { PasswordHash } from '@core/identity/domain/value-objects/PasswordHash';
|
||||
|
||||
// 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')
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
265
adapters/bootstrap/SeedDemoUsers.ts
Normal file
265
adapters/bootstrap/SeedDemoUsers.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import type { Logger } from '@core/shared/application';
|
||||
import type { IAuthRepository } from '@core/identity/domain/repositories/IAuthRepository';
|
||||
import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
|
||||
import type { IAdminUserRepository } from '@core/admin/domain/repositories/IAdminUserRepository';
|
||||
import { User } from '@core/identity/domain/entities/User';
|
||||
import { AdminUser } from '@core/admin/domain/entities/AdminUser';
|
||||
import { EmailAddress } from '@core/identity/domain/value-objects/EmailAddress';
|
||||
import { UserId } from '@core/identity/domain/value-objects/UserId';
|
||||
import { Email } from '@core/admin/domain/value-objects/Email';
|
||||
import { PasswordHash } from '@core/identity/domain/value-objects/PasswordHash';
|
||||
import { stableUuidFromSeedKey } from './racing/SeedIdHelper';
|
||||
|
||||
interface DemoUserSpec {
|
||||
email: string;
|
||||
password: string;
|
||||
needsAdminUser: boolean;
|
||||
needsPrimaryDriverId: boolean;
|
||||
roles: string[];
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* SeedDemoUsers - Creates predefined demo users for testing and development
|
||||
*
|
||||
* This class creates a canonical set of demo users with fixed emails and passwords.
|
||||
* It is idempotent and supports force reseeding via environment variables.
|
||||
*/
|
||||
export class SeedDemoUsers {
|
||||
private readonly demoUserSpecs: DemoUserSpec[] = [
|
||||
{
|
||||
email: 'demo.driver@example.com',
|
||||
password: 'Demo1234!',
|
||||
needsAdminUser: false,
|
||||
needsPrimaryDriverId: true,
|
||||
roles: ['user'],
|
||||
displayName: 'John Driver',
|
||||
},
|
||||
{
|
||||
email: 'demo.sponsor@example.com',
|
||||
password: 'Demo1234!',
|
||||
needsAdminUser: false,
|
||||
needsPrimaryDriverId: false,
|
||||
roles: ['user'],
|
||||
displayName: 'Jane Sponsor',
|
||||
},
|
||||
{
|
||||
email: 'demo.owner@example.com',
|
||||
password: 'Demo1234!',
|
||||
needsAdminUser: true,
|
||||
needsPrimaryDriverId: true,
|
||||
roles: ['owner'],
|
||||
displayName: 'Alice Owner',
|
||||
},
|
||||
{
|
||||
email: 'demo.steward@example.com',
|
||||
password: 'Demo1234!',
|
||||
needsAdminUser: true,
|
||||
needsPrimaryDriverId: true,
|
||||
roles: ['user'],
|
||||
displayName: 'Bob Steward',
|
||||
},
|
||||
{
|
||||
email: 'demo.admin@example.com',
|
||||
password: 'Demo1234!',
|
||||
needsAdminUser: true,
|
||||
needsPrimaryDriverId: true,
|
||||
roles: ['admin'],
|
||||
displayName: 'Charlie Admin',
|
||||
},
|
||||
{
|
||||
email: 'demo.systemowner@example.com',
|
||||
password: 'Demo1234!',
|
||||
needsAdminUser: true,
|
||||
needsPrimaryDriverId: true,
|
||||
roles: ['admin'],
|
||||
displayName: 'Diana SystemOwner',
|
||||
},
|
||||
{
|
||||
email: 'demo.superadmin@example.com',
|
||||
password: 'Demo1234!',
|
||||
needsAdminUser: true,
|
||||
needsPrimaryDriverId: true,
|
||||
roles: ['admin'],
|
||||
displayName: 'Edward SuperAdmin',
|
||||
},
|
||||
];
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly authRepository: IAuthRepository,
|
||||
private readonly passwordHashingService: IPasswordHashingService,
|
||||
private readonly adminUserRepository: IAdminUserRepository,
|
||||
) {}
|
||||
|
||||
private getApiPersistence(): 'postgres' | 'inmemory' {
|
||||
const configured = process.env.GRIDPILOT_API_PERSISTENCE?.toLowerCase();
|
||||
if (configured === 'postgres' || configured === 'inmemory') {
|
||||
return configured;
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return 'inmemory';
|
||||
}
|
||||
|
||||
return process.env.DATABASE_URL ? 'postgres' : 'inmemory';
|
||||
}
|
||||
|
||||
private generateDeterministicId(seedKey: string, persistence: 'postgres' | 'inmemory'): string {
|
||||
if (persistence === 'postgres') {
|
||||
return stableUuidFromSeedKey(seedKey);
|
||||
}
|
||||
return seedKey;
|
||||
}
|
||||
|
||||
private generatePrimaryDriverId(email: string, persistence: 'postgres' | 'inmemory'): string {
|
||||
// Use the email as the seed for the primary driver ID
|
||||
const seedKey = `primary-driver-${email}`;
|
||||
return this.generateDeterministicId(seedKey, persistence);
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const persistence = this.getApiPersistence();
|
||||
|
||||
// Check for force reseed via environment variable
|
||||
const forceReseedRaw = process.env.GRIDPILOT_API_FORCE_RESEED;
|
||||
const forceReseed = forceReseedRaw !== undefined && forceReseedRaw !== '0' && forceReseedRaw.toLowerCase() !== 'false';
|
||||
|
||||
this.logger.info(
|
||||
`[Bootstrap] Demo users seed precheck: forceReseed=${forceReseed}, persistence=${persistence}`,
|
||||
);
|
||||
|
||||
// Check if all demo users already exist
|
||||
let allUsersExist = true;
|
||||
for (const spec of this.demoUserSpecs) {
|
||||
const existingUser = await this.authRepository.findByEmail(EmailAddress.create(spec.email));
|
||||
if (!existingUser) {
|
||||
allUsersExist = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Also check for admin users if needed
|
||||
if (spec.needsAdminUser) {
|
||||
const existingAdmin = await this.adminUserRepository.findByEmail(Email.create(spec.email));
|
||||
if (!existingAdmin) {
|
||||
allUsersExist = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allUsersExist && !forceReseed) {
|
||||
this.logger.info('[Bootstrap] Demo users already exist, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
if (forceReseed) {
|
||||
this.logger.info('[Bootstrap] Force reseed enabled - updating existing demo users');
|
||||
} else {
|
||||
this.logger.info('[Bootstrap] Starting demo users seed');
|
||||
}
|
||||
|
||||
// Create or update each demo user
|
||||
for (const spec of this.demoUserSpecs) {
|
||||
await this.createOrUpdateDemoUser(spec, persistence);
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`[Bootstrap] Demo users seed completed: ${this.demoUserSpecs.length} users processed`,
|
||||
);
|
||||
}
|
||||
|
||||
private async createOrUpdateDemoUser(spec: DemoUserSpec, persistence: 'postgres' | 'inmemory'): Promise<void> {
|
||||
const userId = this.generateDeterministicId(`demo-user-${spec.email}`, persistence);
|
||||
|
||||
// Check if user exists
|
||||
const existingUser = await this.authRepository.findByEmail(EmailAddress.create(spec.email));
|
||||
|
||||
// Hash the password
|
||||
const passwordHash = await this.passwordHashingService.hash(spec.password);
|
||||
|
||||
if (existingUser) {
|
||||
// Update existing user
|
||||
const rehydrateProps: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
email?: string;
|
||||
passwordHash?: PasswordHash;
|
||||
primaryDriverId?: string;
|
||||
} = {
|
||||
id: userId,
|
||||
displayName: spec.displayName,
|
||||
email: spec.email,
|
||||
passwordHash: PasswordHash.fromHash(passwordHash),
|
||||
};
|
||||
|
||||
if (spec.needsPrimaryDriverId) {
|
||||
rehydrateProps.primaryDriverId = this.generatePrimaryDriverId(spec.email, persistence);
|
||||
}
|
||||
|
||||
const updatedUser = User.rehydrate(rehydrateProps);
|
||||
|
||||
await this.authRepository.save(updatedUser);
|
||||
this.logger.debug(`[Bootstrap] Updated demo user: ${spec.email}`);
|
||||
} else {
|
||||
// Create new user
|
||||
const createProps: {
|
||||
id: UserId;
|
||||
displayName: string;
|
||||
email?: string;
|
||||
passwordHash?: PasswordHash;
|
||||
primaryDriverId?: string;
|
||||
} = {
|
||||
id: UserId.fromString(userId),
|
||||
displayName: spec.displayName,
|
||||
email: spec.email,
|
||||
passwordHash: PasswordHash.fromHash(passwordHash),
|
||||
};
|
||||
|
||||
if (spec.needsPrimaryDriverId) {
|
||||
createProps.primaryDriverId = this.generatePrimaryDriverId(spec.email, persistence);
|
||||
}
|
||||
|
||||
const user = User.create(createProps);
|
||||
|
||||
await this.authRepository.save(user);
|
||||
this.logger.debug(`[Bootstrap] Created demo user: ${spec.email}`);
|
||||
}
|
||||
|
||||
// Handle admin user if needed
|
||||
if (spec.needsAdminUser) {
|
||||
const adminUserId = this.generateDeterministicId(`demo-admin-${spec.email}`, persistence);
|
||||
const existingAdmin = await this.adminUserRepository.findByEmail(Email.create(spec.email));
|
||||
|
||||
if (existingAdmin) {
|
||||
// Admin user exists, no update needed for now
|
||||
this.logger.debug(`[Bootstrap] Admin user already exists: ${spec.email}`);
|
||||
} else {
|
||||
// Create admin user
|
||||
const adminCreateProps: {
|
||||
id: string;
|
||||
email: string;
|
||||
roles: string[];
|
||||
status: string;
|
||||
displayName: string;
|
||||
primaryDriverId?: string;
|
||||
} = {
|
||||
id: adminUserId,
|
||||
email: spec.email,
|
||||
roles: spec.roles,
|
||||
status: 'active',
|
||||
displayName: spec.displayName,
|
||||
};
|
||||
|
||||
if (spec.needsPrimaryDriverId) {
|
||||
adminCreateProps.primaryDriverId = this.generatePrimaryDriverId(spec.email, persistence);
|
||||
}
|
||||
|
||||
const adminUser = AdminUser.create(adminCreateProps);
|
||||
|
||||
await this.adminUserRepository.create(adminUser);
|
||||
this.logger.debug(`[Bootstrap] Created admin user: ${spec.email} with roles: ${spec.roles.join(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,9 @@ import type { ISocialGraphRepository } from '@core/social/domain/repositories/IS
|
||||
import type { IDriverStatsRepository } from '@core/racing/domain/repositories/IDriverStatsRepository';
|
||||
import type { ITeamStatsRepository, TeamStats } from '@core/racing/domain/repositories/ITeamStatsRepository';
|
||||
import type { IMediaRepository } from '@core/racing/domain/repositories/IMediaRepository';
|
||||
import type { IAuthRepository } from '@core/identity/domain/repositories/IAuthRepository';
|
||||
import type { IPasswordHashingService } from '@core/identity/domain/services/PasswordHashingService';
|
||||
import type { IAdminUserRepository } from '@core/admin/domain/repositories/IAdminUserRepository';
|
||||
import { createRacingSeed } from './racing/RacingSeed';
|
||||
import { seedId } from './racing/SeedIdHelper';
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
@@ -56,6 +59,10 @@ export type RacingSeedDependencies = {
|
||||
driverStatsRepository: IDriverStatsRepository;
|
||||
teamStatsRepository: ITeamStatsRepository;
|
||||
mediaRepository: IMediaRepository;
|
||||
// Identity dependencies for demo user seed
|
||||
authRepository: IAuthRepository;
|
||||
passwordHashingService: IPasswordHashingService;
|
||||
adminUserRepository: IAdminUserRepository;
|
||||
};
|
||||
|
||||
export class SeedRacingData {
|
||||
@@ -766,4 +773,4 @@ export class SeedRacingData {
|
||||
|
||||
return 'club-default';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user