diff --git a/frontends/nextjs/src/lib/db/users/add-user.test.ts b/frontends/nextjs/src/lib/db/users/add-user.test.ts new file mode 100644 index 000000000..0b22544af --- /dev/null +++ b/frontends/nextjs/src/lib/db/users/add-user.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockCreate = vi.fn() +const mockAdapter = { create: mockCreate } + +vi.mock('../dbal-client', () => ({ + getAdapter: () => mockAdapter, +})) + +import { addUser } from './add-user' + +describe('addUser', () => { + beforeEach(() => { + mockCreate.mockReset() + }) + + it.each([ + { + name: 'basic user', + user: { + id: 'user_1', + username: 'testuser', + email: 'test@example.com', + role: 'user' as const, + createdAt: 1000000, + }, + expectedData: { + id: 'user_1', + username: 'testuser', + email: 'test@example.com', + role: 'user', + profilePicture: undefined, + bio: undefined, + createdAt: BigInt(1000000), + tenantId: undefined, + isInstanceOwner: false, + }, + }, + { + name: 'user with all fields', + user: { + id: 'user_2', + username: 'fulluser', + email: 'full@example.com', + role: 'admin' as const, + profilePicture: 'https://example.com/pic.jpg', + bio: 'Test bio', + createdAt: 2000000, + tenantId: 'tenant_1', + isInstanceOwner: true, + }, + expectedData: { + id: 'user_2', + username: 'fulluser', + email: 'full@example.com', + role: 'admin', + profilePicture: 'https://example.com/pic.jpg', + bio: 'Test bio', + createdAt: BigInt(2000000), + tenantId: 'tenant_1', + isInstanceOwner: true, + }, + }, + ])('should add $name', async ({ user, expectedData }) => { + mockCreate.mockResolvedValue(undefined) + + await addUser(user) + + expect(mockCreate).toHaveBeenCalledWith('User', expectedData) + }) + + it('should propagate adapter errors', async () => { + mockCreate.mockRejectedValue(new Error('DB error')) + + await expect(addUser({ + id: 'user_1', + username: 'test', + email: 'test@example.com', + role: 'user', + createdAt: 1000, + })).rejects.toThrow('DB error') + }) +}) diff --git a/frontends/nextjs/src/lib/db/users/delete-user.test.ts b/frontends/nextjs/src/lib/db/users/delete-user.test.ts new file mode 100644 index 000000000..9626abf4a --- /dev/null +++ b/frontends/nextjs/src/lib/db/users/delete-user.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockDelete = vi.fn() +const mockAdapter = { delete: mockDelete } + +vi.mock('../dbal-client', () => ({ + getAdapter: () => mockAdapter, +})) + +import { deleteUser } from './delete-user' + +describe('deleteUser', () => { + beforeEach(() => { + mockDelete.mockReset() + }) + + it.each([ + { userId: 'user_1' }, + { userId: 'user_abc123' }, + { userId: 'some-uuid-format' }, + ])('should delete user with id $userId', async ({ userId }) => { + mockDelete.mockResolvedValue(undefined) + + await deleteUser(userId) + + expect(mockDelete).toHaveBeenCalledWith('User', userId) + }) + + it('should propagate adapter errors', async () => { + mockDelete.mockRejectedValue(new Error('User not found')) + + await expect(deleteUser('nonexistent')).rejects.toThrow('User not found') + }) +}) diff --git a/frontends/nextjs/src/lib/db/users/get-super-god.test.ts b/frontends/nextjs/src/lib/db/users/get-super-god.test.ts new file mode 100644 index 000000000..8636eb88c --- /dev/null +++ b/frontends/nextjs/src/lib/db/users/get-super-god.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockList = vi.fn() +const mockAdapter = { list: mockList } + +vi.mock('../dbal-client', () => ({ + getAdapter: () => mockAdapter, +})) + +import { getSuperGod } from './get-super-god' + +describe('getSuperGod', () => { + beforeEach(() => { + mockList.mockReset() + }) + + it.each([ + { + name: 'null when no instance owner', + dbData: [], + expected: null, + }, + { + name: 'supergod user when found', + dbData: [ + { + id: 'user_sg', + username: 'supergod', + email: 'sg@example.com', + role: 'supergod', + profilePicture: null, + bio: 'Supreme admin', + createdAt: BigInt(1000), + tenantId: null, + isInstanceOwner: true, + }, + ], + expected: { + id: 'user_sg', + username: 'supergod', + email: 'sg@example.com', + role: 'supergod', + profilePicture: undefined, + bio: 'Supreme admin', + createdAt: 1000, + tenantId: undefined, + isInstanceOwner: true, + }, + }, + ])('should return $name', async ({ dbData, expected }) => { + mockList.mockResolvedValue({ data: dbData }) + + const result = await getSuperGod() + + expect(mockList).toHaveBeenCalledWith('User', { filter: { isInstanceOwner: true } }) + expect(result).toEqual(expected) + }) +}) diff --git a/frontends/nextjs/src/lib/db/users/get-users.test.ts b/frontends/nextjs/src/lib/db/users/get-users.test.ts new file mode 100644 index 000000000..2efbfcca3 --- /dev/null +++ b/frontends/nextjs/src/lib/db/users/get-users.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockList = vi.fn() +const mockAdapter = { list: mockList } + +vi.mock('../dbal-client', () => ({ + getAdapter: () => mockAdapter, +})) + +import { getUsers } from './get-users' + +describe('getUsers', () => { + beforeEach(() => { + mockList.mockReset() + }) + + it.each([ + { + name: 'empty array when no users', + dbData: [], + expected: [], + }, + { + name: 'single user', + dbData: [ + { + id: 'user_1', + username: 'test', + email: 'test@example.com', + role: 'user', + profilePicture: null, + bio: null, + createdAt: BigInt(1000), + tenantId: null, + isInstanceOwner: false, + }, + ], + expected: [ + { + id: 'user_1', + username: 'test', + email: 'test@example.com', + role: 'user', + profilePicture: undefined, + bio: undefined, + createdAt: 1000, + tenantId: undefined, + isInstanceOwner: false, + }, + ], + }, + { + name: 'multiple users with all fields', + dbData: [ + { + id: 'user_1', + username: 'admin', + email: 'admin@example.com', + role: 'admin', + profilePicture: 'pic.jpg', + bio: 'Admin bio', + createdAt: BigInt(1000), + tenantId: 'tenant_1', + isInstanceOwner: false, + }, + { + id: 'user_2', + username: 'supergod', + email: 'sg@example.com', + role: 'supergod', + profilePicture: null, + bio: null, + createdAt: BigInt(2000), + tenantId: null, + isInstanceOwner: true, + }, + ], + expected: [ + { + id: 'user_1', + username: 'admin', + email: 'admin@example.com', + role: 'admin', + profilePicture: 'pic.jpg', + bio: 'Admin bio', + createdAt: 1000, + tenantId: 'tenant_1', + isInstanceOwner: false, + }, + { + id: 'user_2', + username: 'supergod', + email: 'sg@example.com', + role: 'supergod', + profilePicture: undefined, + bio: undefined, + createdAt: 2000, + tenantId: undefined, + isInstanceOwner: true, + }, + ], + }, + ])('should return $name', async ({ dbData, expected }) => { + mockList.mockResolvedValue({ data: dbData }) + + const result = await getUsers() + + expect(mockList).toHaveBeenCalledWith('User') + expect(result).toEqual(expected) + }) +}) diff --git a/frontends/nextjs/src/lib/db/users/set-users.test.ts b/frontends/nextjs/src/lib/db/users/set-users.test.ts new file mode 100644 index 000000000..3b99b9a61 --- /dev/null +++ b/frontends/nextjs/src/lib/db/users/set-users.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockList = vi.fn() +const mockDelete = vi.fn() +const mockCreate = vi.fn() +const mockAdapter = { list: mockList, delete: mockDelete, create: mockCreate } + +vi.mock('../dbal-client', () => ({ + getAdapter: () => mockAdapter, +})) + +import { setUsers } from './set-users' + +describe('setUsers', () => { + beforeEach(() => { + mockList.mockReset() + mockDelete.mockReset() + mockCreate.mockReset() + }) + + it.each([ + { + name: 'empty users replacing empty', + existingUsers: [], + newUsers: [], + expectedDeletes: 0, + expectedCreates: 0, + }, + { + name: 'new users replacing empty', + existingUsers: [], + newUsers: [ + { id: 'u1', username: 'user1', email: 'u1@test.com', role: 'user' as const, createdAt: 1000 }, + ], + expectedDeletes: 0, + expectedCreates: 1, + }, + { + name: 'empty users replacing existing', + existingUsers: [{ id: 'old1' }, { id: 'old2' }], + newUsers: [], + expectedDeletes: 2, + expectedCreates: 0, + }, + { + name: 'new users replacing existing', + existingUsers: [{ id: 'old1' }], + newUsers: [ + { id: 'new1', username: 'newuser', email: 'new@test.com', role: 'admin' as const, createdAt: 2000 }, + ], + expectedDeletes: 1, + expectedCreates: 1, + }, + ])('should handle $name', async ({ existingUsers, newUsers, expectedDeletes, expectedCreates }) => { + mockList.mockResolvedValue({ data: existingUsers }) + mockDelete.mockResolvedValue(undefined) + mockCreate.mockResolvedValue(undefined) + + await setUsers(newUsers) + + expect(mockList).toHaveBeenCalledWith('User') + expect(mockDelete).toHaveBeenCalledTimes(expectedDeletes) + expect(mockCreate).toHaveBeenCalledTimes(expectedCreates) + }) +}) diff --git a/frontends/nextjs/src/lib/db/users/transfer-super-god-power.test.ts b/frontends/nextjs/src/lib/db/users/transfer-super-god-power.test.ts new file mode 100644 index 000000000..283ac127e --- /dev/null +++ b/frontends/nextjs/src/lib/db/users/transfer-super-god-power.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockUpdate = vi.fn() +const mockAdapter = { update: mockUpdate } + +vi.mock('../dbal-client', () => ({ + getAdapter: () => mockAdapter, +})) + +import { transferSuperGodPower } from './transfer-super-god-power' + +describe('transferSuperGodPower', () => { + beforeEach(() => { + mockUpdate.mockReset() + }) + + it.each([ + { fromUserId: 'user_sg', toUserId: 'user_god' }, + { fromUserId: 'owner_1', toUserId: 'admin_2' }, + ])('should transfer from $fromUserId to $toUserId', async ({ fromUserId, toUserId }) => { + mockUpdate.mockResolvedValue(undefined) + + await transferSuperGodPower(fromUserId, toUserId) + + expect(mockUpdate).toHaveBeenCalledTimes(2) + expect(mockUpdate).toHaveBeenNthCalledWith(1, 'User', fromUserId, { + isInstanceOwner: false, + role: 'god', + }) + expect(mockUpdate).toHaveBeenNthCalledWith(2, 'User', toUserId, { + isInstanceOwner: true, + role: 'supergod', + }) + }) + + it('should propagate adapter errors', async () => { + mockUpdate.mockRejectedValue(new Error('Transfer failed')) + + await expect(transferSuperGodPower('u1', 'u2')).rejects.toThrow('Transfer failed') + }) +}) diff --git a/frontends/nextjs/src/lib/db/users/update-user.test.ts b/frontends/nextjs/src/lib/db/users/update-user.test.ts new file mode 100644 index 000000000..cc2855fed --- /dev/null +++ b/frontends/nextjs/src/lib/db/users/update-user.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const mockUpdate = vi.fn() +const mockAdapter = { update: mockUpdate } + +vi.mock('../dbal-client', () => ({ + getAdapter: () => mockAdapter, +})) + +import { updateUser } from './update-user' + +describe('updateUser', () => { + beforeEach(() => { + mockUpdate.mockReset() + }) + + it.each([ + { + name: 'username only', + userId: 'user_1', + updates: { username: 'newname' }, + expectedData: { + username: 'newname', + email: undefined, + role: undefined, + profilePicture: undefined, + bio: undefined, + tenantId: undefined, + isInstanceOwner: undefined, + }, + }, + { + name: 'multiple fields', + userId: 'user_2', + updates: { email: 'new@example.com', role: 'admin' as const, bio: 'New bio' }, + expectedData: { + username: undefined, + email: 'new@example.com', + role: 'admin', + profilePicture: undefined, + bio: 'New bio', + tenantId: undefined, + isInstanceOwner: undefined, + }, + }, + { + name: 'all fields', + userId: 'user_3', + updates: { + username: 'full', + email: 'full@example.com', + role: 'god' as const, + profilePicture: 'pic.jpg', + bio: 'Bio', + tenantId: 'tenant_1', + isInstanceOwner: true, + }, + expectedData: { + username: 'full', + email: 'full@example.com', + role: 'god', + profilePicture: 'pic.jpg', + bio: 'Bio', + tenantId: 'tenant_1', + isInstanceOwner: true, + }, + }, + ])('should update $name', async ({ userId, updates, expectedData }) => { + mockUpdate.mockResolvedValue(undefined) + + await updateUser(userId, updates) + + expect(mockUpdate).toHaveBeenCalledWith('User', userId, expectedData) + }) + + it('should propagate adapter errors', async () => { + mockUpdate.mockRejectedValue(new Error('Update failed')) + + await expect(updateUser('user_1', { username: 'test' })).rejects.toThrow('Update failed') + }) +}) diff --git a/frontends/nextjs/src/seed-data/pages.test.ts b/frontends/nextjs/src/seed-data/pages.test.ts index d822bb23c..16b064670 100644 --- a/frontends/nextjs/src/seed-data/pages.test.ts +++ b/frontends/nextjs/src/seed-data/pages.test.ts @@ -18,7 +18,7 @@ vi.mock('@/lib/database', () => ({ }, })) -vi.mock('@/lib/page-definition-builder', () => ({ +vi.mock('@/lib/rendering/page-definition-builder', () => ({ getPageDefinitionBuilder, }))