diff --git a/frontends/nextjs/src/seed-data/index.test.ts b/frontends/nextjs/src/seed-data/index.test.ts new file mode 100644 index 000000000..011079604 --- /dev/null +++ b/frontends/nextjs/src/seed-data/index.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' + +const { + initializeUsers, + initializeComponents, + initializeScripts, + initializeWorkflows, + initializePages, + initializePackages, +} = vi.hoisted(() => ({ + initializeUsers: vi.fn(), + initializeComponents: vi.fn(), + initializeScripts: vi.fn(), + initializeWorkflows: vi.fn(), + initializePages: vi.fn(), + initializePackages: vi.fn(), +})) + +vi.mock('./users', () => ({ initializeUsers })) +vi.mock('./components', () => ({ initializeComponents })) +vi.mock('./scripts', () => ({ initializeScripts })) +vi.mock('./workflows', () => ({ initializeWorkflows })) +vi.mock('./pages', () => ({ initializePages })) +vi.mock('./packages', () => ({ initializePackages })) + +import { initializeAllSeedData } from './index' + +describe('initializeAllSeedData', () => { + beforeEach(() => { + initializeUsers.mockReset() + initializeComponents.mockReset() + initializeScripts.mockReset() + initializeWorkflows.mockReset() + initializePages.mockReset() + initializePackages.mockReset() + }) + + it.each([ + { + name: 'run seeders in order', + errorStep: null as string | null, + expectedCalls: ['users', 'components', 'scripts', 'workflows', 'pages', 'packages'], + }, + { + name: 'stop when a seeder fails', + errorStep: 'scripts', + expectedCalls: ['users', 'components', 'scripts'], + }, + ])('should $name', async ({ errorStep, expectedCalls }) => { + const callOrder: string[] = [] + initializeUsers.mockImplementation(async () => callOrder.push('users')) + initializeComponents.mockImplementation(async () => callOrder.push('components')) + initializeScripts.mockImplementation(async () => { + callOrder.push('scripts') + if (errorStep === 'scripts') { + throw new Error('seed failed') + } + }) + initializeWorkflows.mockImplementation(async () => callOrder.push('workflows')) + initializePages.mockImplementation(async () => callOrder.push('pages')) + initializePackages.mockImplementation(async () => callOrder.push('packages')) + + if (errorStep) { + await expect(initializeAllSeedData()).rejects.toThrow('seed failed') + } else { + await expect(initializeAllSeedData()).resolves.toBeUndefined() + } + + expect(callOrder).toEqual(expectedCalls) + }) +}) diff --git a/frontends/nextjs/src/seed-data/packages.test.ts b/frontends/nextjs/src/seed-data/packages.test.ts new file mode 100644 index 000000000..b2c152cac --- /dev/null +++ b/frontends/nextjs/src/seed-data/packages.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' + +const { initializePackageSystem } = vi.hoisted(() => ({ + initializePackageSystem: vi.fn(), +})) + +vi.mock('@/lib/package-loader', () => ({ + initializePackageSystem, +})) + +import { initializePackages } from '@/seed-data/packages' + +describe('initializePackages', () => { + beforeEach(() => { + initializePackageSystem.mockReset() + }) + + it.each([ + { name: 'resolve when initializer succeeds', error: null as Error | null }, + { name: 'propagate errors from initializer', error: new Error('init failed') }, + ])('should $name', async ({ error }) => { + if (error) { + initializePackageSystem.mockRejectedValueOnce(error) + await expect(initializePackages()).rejects.toThrow('init failed') + } else { + initializePackageSystem.mockResolvedValueOnce(undefined) + await expect(initializePackages()).resolves.toBeUndefined() + } + + expect(initializePackageSystem).toHaveBeenCalledTimes(1) + }) +}) diff --git a/frontends/nextjs/src/seed-data/pages.test.ts b/frontends/nextjs/src/seed-data/pages.test.ts new file mode 100644 index 000000000..d822bb23c --- /dev/null +++ b/frontends/nextjs/src/seed-data/pages.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' + +const { getPages } = vi.hoisted(() => ({ + getPages: vi.fn(), +})) + +const { initializeDefaultPages } = vi.hoisted(() => ({ + initializeDefaultPages: vi.fn(), +})) + +const { getPageDefinitionBuilder } = vi.hoisted(() => ({ + getPageDefinitionBuilder: vi.fn(), +})) + +vi.mock('@/lib/database', () => ({ + Database: { + getPages, + }, +})) + +vi.mock('@/lib/page-definition-builder', () => ({ + getPageDefinitionBuilder, +})) + +import { initializePages } from '@/seed-data/pages' + +describe('initializePages', () => { + beforeEach(() => { + getPages.mockReset() + getPageDefinitionBuilder.mockReset() + initializeDefaultPages.mockReset() + }) + + it.each([ + { + name: 'skip initialization when pages exist', + existingPages: [{ id: 'page1' }], + shouldInitialize: false, + }, + { + name: 'initialize defaults when no pages exist', + existingPages: [], + shouldInitialize: true, + }, + ])('should $name', async ({ existingPages, shouldInitialize }) => { + getPages.mockResolvedValue(existingPages) + getPageDefinitionBuilder.mockReturnValue({ initializeDefaultPages }) + + await initializePages() + + expect(getPages).toHaveBeenCalledTimes(1) + if (shouldInitialize) { + expect(getPageDefinitionBuilder).toHaveBeenCalledTimes(1) + expect(initializeDefaultPages).toHaveBeenCalledTimes(1) + } else { + expect(getPageDefinitionBuilder).not.toHaveBeenCalled() + expect(initializeDefaultPages).not.toHaveBeenCalled() + } + }) +}) diff --git a/frontends/nextjs/src/seed-data/scripts.test.ts b/frontends/nextjs/src/seed-data/scripts.test.ts new file mode 100644 index 000000000..1aefa57e6 --- /dev/null +++ b/frontends/nextjs/src/seed-data/scripts.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' + +const { getLuaScripts, addLuaScript } = vi.hoisted(() => ({ + getLuaScripts: vi.fn(), + addLuaScript: vi.fn(), +})) + +vi.mock('@/lib/database', () => ({ + Database: { + getLuaScripts, + addLuaScript, + }, +})) + +import { initializeScripts } from '@/seed-data/scripts' + +const expectedScriptIds = [ + 'script_welcome_message', + 'script_format_date', + 'script_validate_email', + 'script_permission_check', + 'script_page_load_analytics', +] + +describe('initializeScripts', () => { + beforeEach(() => { + getLuaScripts.mockReset() + addLuaScript.mockReset() + }) + + it.each([ + { + name: 'skip seeding when scripts exist', + existingScripts: [{ id: 'existing' }], + expectedAdds: 0, + }, + { + name: 'seed default scripts when none exist', + existingScripts: [], + expectedAdds: expectedScriptIds.length, + }, + ])('should $name', async ({ existingScripts, expectedAdds }) => { + getLuaScripts.mockResolvedValue(existingScripts) + + await initializeScripts() + + expect(getLuaScripts).toHaveBeenCalledTimes(1) + expect(addLuaScript).toHaveBeenCalledTimes(expectedAdds) + + if (expectedAdds > 0) { + expectedScriptIds.forEach(id => { + expect(addLuaScript).toHaveBeenCalledWith(expect.objectContaining({ id })) + }) + } + }) +}) diff --git a/frontends/nextjs/src/seed-data/users.test.ts b/frontends/nextjs/src/seed-data/users.test.ts new file mode 100644 index 000000000..253d6365e --- /dev/null +++ b/frontends/nextjs/src/seed-data/users.test.ts @@ -0,0 +1,98 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' + +const { + getUsers, + setCredential, + setFirstLoginFlag, + addUser, + setGodCredentialsExpiry, + hashPassword, +} = vi.hoisted(() => ({ + getUsers: vi.fn(), + setCredential: vi.fn(), + setFirstLoginFlag: vi.fn(), + addUser: vi.fn(), + setGodCredentialsExpiry: vi.fn(), + hashPassword: vi.fn(), +})) + +const { getScrambledPassword } = vi.hoisted(() => ({ + getScrambledPassword: vi.fn(), +})) + +vi.mock('@/lib/database', () => ({ + Database: { + getUsers, + setCredential, + setFirstLoginFlag, + addUser, + setGodCredentialsExpiry, + }, + hashPassword, +})) + +vi.mock('@/lib/auth', () => ({ + getScrambledPassword, +})) + +import { initializeUsers } from '@/seed-data/users' + +const expectedUsernames = ['supergod', 'god', 'admin', 'alice', 'bob'] + +describe('initializeUsers', () => { + beforeEach(() => { + getUsers.mockReset() + setCredential.mockReset() + setFirstLoginFlag.mockReset() + addUser.mockReset() + setGodCredentialsExpiry.mockReset() + hashPassword.mockReset() + getScrambledPassword.mockReset() + }) + + it.each([ + { + name: 'skip seeding when users exist', + existingUsers: [{ id: 'existing' }], + expectedAdds: 0, + }, + { + name: 'seed users when none exist', + existingUsers: [], + expectedAdds: expectedUsernames.length, + }, + ])('should $name', async ({ existingUsers, expectedAdds }) => { + const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(1000) + getUsers.mockResolvedValue(existingUsers) + getScrambledPassword.mockImplementation((username: string) => `scrambled-${username}`) + hashPassword.mockImplementation(async (value: string) => `hash-${value}`) + + if (expectedAdds === 0) { + await initializeUsers() + } else { + await expect(initializeUsers()).resolves.toBeUndefined() + } + + expect(getUsers).toHaveBeenCalledTimes(1) + expect(addUser).toHaveBeenCalledTimes(expectedAdds) + expect(setCredential).toHaveBeenCalledTimes(expectedAdds) + expect(setFirstLoginFlag).toHaveBeenCalledTimes(expectedAdds) + + if (expectedAdds === 0) { + expect(getScrambledPassword).not.toHaveBeenCalled() + expect(hashPassword).not.toHaveBeenCalled() + expect(setGodCredentialsExpiry).not.toHaveBeenCalled() + } else { + expectedUsernames.forEach(username => { + expect(getScrambledPassword).toHaveBeenCalledWith(username) + expect(hashPassword).toHaveBeenCalledWith(`scrambled-${username}`) + expect(setCredential).toHaveBeenCalledWith(username, `hash-scrambled-${username}`) + expect(setFirstLoginFlag).toHaveBeenCalledWith(username, true) + expect(addUser).toHaveBeenCalledWith(expect.objectContaining({ username })) + }) + expect(setGodCredentialsExpiry).toHaveBeenCalledWith(1000 + 60 * 60 * 1000) + } + + nowSpy.mockRestore() + }) +}) diff --git a/frontends/nextjs/src/seed-data/workflows.test.ts b/frontends/nextjs/src/seed-data/workflows.test.ts new file mode 100644 index 000000000..c4adbebaa --- /dev/null +++ b/frontends/nextjs/src/seed-data/workflows.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' + +const { getWorkflows, addWorkflow } = vi.hoisted(() => ({ + getWorkflows: vi.fn(), + addWorkflow: vi.fn(), +})) + +vi.mock('@/lib/database', () => ({ + Database: { + getWorkflows, + addWorkflow, + }, +})) + +import { initializeWorkflows } from '@/seed-data/workflows' + +const expectedWorkflowIds = [ + 'workflow_user_registration', + 'workflow_page_access', + 'workflow_comment_submission', +] + +describe('initializeWorkflows', () => { + beforeEach(() => { + getWorkflows.mockReset() + addWorkflow.mockReset() + }) + + it.each([ + { + name: 'skip seeding when workflows exist', + existingWorkflows: [{ id: 'existing' }], + expectedAdds: 0, + }, + { + name: 'seed default workflows when none exist', + existingWorkflows: [], + expectedAdds: expectedWorkflowIds.length, + }, + ])('should $name', async ({ existingWorkflows, expectedAdds }) => { + getWorkflows.mockResolvedValue(existingWorkflows) + + await initializeWorkflows() + + expect(getWorkflows).toHaveBeenCalledTimes(1) + expect(addWorkflow).toHaveBeenCalledTimes(expectedAdds) + + if (expectedAdds > 0) { + expectedWorkflowIds.forEach(id => { + expect(addWorkflow).toHaveBeenCalledWith(expect.objectContaining({ id })) + }) + } + }) +})