/** * View Data Layer Tests - Admin Functionality * * This test file covers the view data layer for admin functionality. * * The view data layer is responsible for: * - DTO → UI model mapping * - Formatting, sorting, and grouping * - Derived fields and defaults * - UI-specific semantics * * This layer isolates the UI from API churn by providing a stable interface * between the API layer and the presentation layer. * * Test coverage includes: * - Admin dashboard data transformation * - User management view models * - Admin-specific formatting and validation * - Derived fields for admin UI components * - Default values and fallbacks for admin views */ import { AdminDashboardViewDataBuilder } from '@/lib/builders/view-data/AdminDashboardViewDataBuilder'; import { AdminUsersViewDataBuilder } from '@/lib/builders/view-data/AdminUsersViewDataBuilder'; import type { DashboardStats } from '@/lib/types/admin'; import type { UserListResponse } from '@/lib/types/admin'; describe('AdminDashboardViewDataBuilder', () => { describe('happy paths', () => { it('should transform DashboardStats DTO to AdminDashboardViewData correctly', () => { const dashboardStats: DashboardStats = { totalUsers: 1000, activeUsers: 800, suspendedUsers: 50, deletedUsers: 150, systemAdmins: 5, recentLogins: 120, newUsersToday: 15, }; const result = AdminDashboardViewDataBuilder.build(dashboardStats); expect(result).toEqual({ stats: { totalUsers: 1000, activeUsers: 800, suspendedUsers: 50, deletedUsers: 150, systemAdmins: 5, recentLogins: 120, newUsersToday: 15, }, }); }); it('should handle zero values correctly', () => { const dashboardStats: DashboardStats = { totalUsers: 0, activeUsers: 0, suspendedUsers: 0, deletedUsers: 0, systemAdmins: 0, recentLogins: 0, newUsersToday: 0, }; const result = AdminDashboardViewDataBuilder.build(dashboardStats); expect(result).toEqual({ stats: { totalUsers: 0, activeUsers: 0, suspendedUsers: 0, deletedUsers: 0, systemAdmins: 0, recentLogins: 0, newUsersToday: 0, }, }); }); it('should handle large numbers correctly', () => { const dashboardStats: DashboardStats = { totalUsers: 1000000, activeUsers: 750000, suspendedUsers: 25000, deletedUsers: 225000, systemAdmins: 50, recentLogins: 50000, newUsersToday: 1000, }; const result = AdminDashboardViewDataBuilder.build(dashboardStats); expect(result.stats.totalUsers).toBe(1000000); expect(result.stats.activeUsers).toBe(750000); expect(result.stats.systemAdmins).toBe(50); }); }); describe('data transformation', () => { it('should preserve all DTO fields in the output', () => { const dashboardStats: DashboardStats = { totalUsers: 500, activeUsers: 400, suspendedUsers: 25, deletedUsers: 75, systemAdmins: 3, recentLogins: 80, newUsersToday: 10, }; const result = AdminDashboardViewDataBuilder.build(dashboardStats); expect(result.stats.totalUsers).toBe(dashboardStats.totalUsers); expect(result.stats.activeUsers).toBe(dashboardStats.activeUsers); expect(result.stats.suspendedUsers).toBe(dashboardStats.suspendedUsers); expect(result.stats.deletedUsers).toBe(dashboardStats.deletedUsers); expect(result.stats.systemAdmins).toBe(dashboardStats.systemAdmins); expect(result.stats.recentLogins).toBe(dashboardStats.recentLogins); expect(result.stats.newUsersToday).toBe(dashboardStats.newUsersToday); }); it('should not modify the input DTO', () => { const dashboardStats: DashboardStats = { totalUsers: 100, activeUsers: 80, suspendedUsers: 5, deletedUsers: 15, systemAdmins: 2, recentLogins: 20, newUsersToday: 5, }; const originalStats = { ...dashboardStats }; AdminDashboardViewDataBuilder.build(dashboardStats); expect(dashboardStats).toEqual(originalStats); }); }); describe('edge cases', () => { it('should handle negative values (if API returns them)', () => { const dashboardStats: DashboardStats = { totalUsers: -1, activeUsers: -1, suspendedUsers: -1, deletedUsers: -1, systemAdmins: -1, recentLogins: -1, newUsersToday: -1, }; const result = AdminDashboardViewDataBuilder.build(dashboardStats); expect(result.stats.totalUsers).toBe(-1); expect(result.stats.activeUsers).toBe(-1); }); it('should handle very large numbers', () => { const dashboardStats: DashboardStats = { totalUsers: Number.MAX_SAFE_INTEGER, activeUsers: Number.MAX_SAFE_INTEGER - 1000, suspendedUsers: 100, deletedUsers: 100, systemAdmins: 10, recentLogins: 1000, newUsersToday: 100, }; const result = AdminDashboardViewDataBuilder.build(dashboardStats); expect(result.stats.totalUsers).toBe(Number.MAX_SAFE_INTEGER); expect(result.stats.activeUsers).toBe(Number.MAX_SAFE_INTEGER - 1000); }); }); }); describe('AdminUsersViewDataBuilder', () => { describe('happy paths', () => { it('should transform UserListResponse DTO to AdminUsersViewData correctly', () => { const userListResponse: UserListResponse = { users: [ { id: 'user-1', email: 'admin@example.com', displayName: 'Admin User', roles: ['admin', 'owner'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', lastLoginAt: '2024-01-20T10:00:00.000Z', primaryDriverId: 'driver-123', }, { id: 'user-2', email: 'user@example.com', displayName: 'Regular User', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-05T00:00:00.000Z', updatedAt: '2024-01-10T08:00:00.000Z', lastLoginAt: '2024-01-18T14:00:00.000Z', primaryDriverId: 'driver-456', }, ], total: 2, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.users).toHaveLength(2); expect(result.users[0]).toEqual({ id: 'user-1', email: 'admin@example.com', displayName: 'Admin User', roles: ['admin', 'owner'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', lastLoginAt: '2024-01-20T10:00:00.000Z', primaryDriverId: 'driver-123', }); expect(result.users[1]).toEqual({ id: 'user-2', email: 'user@example.com', displayName: 'Regular User', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-05T00:00:00.000Z', updatedAt: '2024-01-10T08:00:00.000Z', lastLoginAt: '2024-01-18T14:00:00.000Z', primaryDriverId: 'driver-456', }); expect(result.total).toBe(2); expect(result.page).toBe(1); expect(result.limit).toBe(10); expect(result.totalPages).toBe(1); }); it('should calculate derived fields correctly', () => { const userListResponse: UserListResponse = { users: [ { id: 'user-1', email: 'user1@example.com', displayName: 'User 1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', }, { id: 'user-2', email: 'user2@example.com', displayName: 'User 2', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-02T00:00:00.000Z', updatedAt: '2024-01-16T12:00:00.000Z', }, { id: 'user-3', email: 'user3@example.com', displayName: 'User 3', roles: ['admin'], status: 'suspended', isSystemAdmin: true, createdAt: '2024-01-03T00:00:00.000Z', updatedAt: '2024-01-17T12:00:00.000Z', }, { id: 'user-4', email: 'user4@example.com', displayName: 'User 4', roles: ['member'], status: 'deleted', isSystemAdmin: false, createdAt: '2024-01-04T00:00:00.000Z', updatedAt: '2024-01-18T12:00:00.000Z', }, ], total: 4, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); // activeUserCount should count only users with status 'active' expect(result.activeUserCount).toBe(2); // adminCount should count only system admins expect(result.adminCount).toBe(1); }); it('should handle empty users list', () => { const userListResponse: UserListResponse = { users: [], total: 0, page: 1, limit: 10, totalPages: 0, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.users).toHaveLength(0); expect(result.total).toBe(0); expect(result.activeUserCount).toBe(0); expect(result.adminCount).toBe(0); }); it('should handle users without optional fields', () => { const userListResponse: UserListResponse = { users: [ { id: 'user-1', email: 'user1@example.com', displayName: 'User 1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', // lastLoginAt and primaryDriverId are optional }, ], total: 1, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.users[0].lastLoginAt).toBeUndefined(); expect(result.users[0].primaryDriverId).toBeUndefined(); }); }); describe('date formatting', () => { it('should handle ISO date strings correctly', () => { const userListResponse: UserListResponse = { users: [ { id: 'user-1', email: 'user1@example.com', displayName: 'User 1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', lastLoginAt: '2024-01-20T10:00:00.000Z', }, ], total: 1, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.users[0].createdAt).toBe('2024-01-01T00:00:00.000Z'); expect(result.users[0].updatedAt).toBe('2024-01-15T12:00:00.000Z'); expect(result.users[0].lastLoginAt).toBe('2024-01-20T10:00:00.000Z'); }); it('should handle Date objects and convert to ISO strings', () => { const userListResponse: UserListResponse = { users: [ { id: 'user-1', email: 'user1@example.com', displayName: 'User 1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: new Date('2024-01-01T00:00:00.000Z'), updatedAt: new Date('2024-01-15T12:00:00.000Z'), lastLoginAt: new Date('2024-01-20T10:00:00.000Z'), }, ], total: 1, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.users[0].createdAt).toBe('2024-01-01T00:00:00.000Z'); expect(result.users[0].updatedAt).toBe('2024-01-15T12:00:00.000Z'); expect(result.users[0].lastLoginAt).toBe('2024-01-20T10:00:00.000Z'); }); it('should handle Date objects for lastLoginAt when present', () => { const userListResponse: UserListResponse = { users: [ { id: 'user-1', email: 'user1@example.com', displayName: 'User 1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', lastLoginAt: new Date('2024-01-20T10:00:00.000Z'), }, ], total: 1, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.users[0].lastLoginAt).toBe('2024-01-20T10:00:00.000Z'); }); }); describe('data transformation', () => { it('should preserve all DTO fields in the output', () => { const userListResponse: UserListResponse = { users: [ { id: 'user-1', email: 'user1@example.com', displayName: 'User 1', roles: ['admin', 'owner'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', lastLoginAt: '2024-01-20T10:00:00.000Z', primaryDriverId: 'driver-123', }, ], total: 1, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.users[0].id).toBe(userListResponse.users[0].id); expect(result.users[0].email).toBe(userListResponse.users[0].email); expect(result.users[0].displayName).toBe(userListResponse.users[0].displayName); expect(result.users[0].roles).toEqual(userListResponse.users[0].roles); expect(result.users[0].status).toBe(userListResponse.users[0].status); expect(result.users[0].isSystemAdmin).toBe(userListResponse.users[0].isSystemAdmin); expect(result.users[0].createdAt).toBe(userListResponse.users[0].createdAt); expect(result.users[0].updatedAt).toBe(userListResponse.users[0].updatedAt); expect(result.users[0].lastLoginAt).toBe(userListResponse.users[0].lastLoginAt); expect(result.users[0].primaryDriverId).toBe(userListResponse.users[0].primaryDriverId); expect(result.total).toBe(userListResponse.total); expect(result.page).toBe(userListResponse.page); expect(result.limit).toBe(userListResponse.limit); expect(result.totalPages).toBe(userListResponse.totalPages); }); it('should not modify the input DTO', () => { const userListResponse: UserListResponse = { users: [ { id: 'user-1', email: 'user1@example.com', displayName: 'User 1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', }, ], total: 1, page: 1, limit: 10, totalPages: 1, }; const originalResponse = { ...userListResponse }; AdminUsersViewDataBuilder.build(userListResponse); expect(userListResponse).toEqual(originalResponse); }); }); describe('edge cases', () => { it('should handle users with multiple roles', () => { const userListResponse: UserListResponse = { users: [ { id: 'user-1', email: 'user1@example.com', displayName: 'User 1', roles: ['admin', 'owner', 'steward', 'member'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', }, ], total: 1, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.users[0].roles).toEqual(['admin', 'owner', 'steward', 'member']); }); it('should handle users with different statuses', () => { const userListResponse: UserListResponse = { users: [ { id: 'user-1', email: 'user1@example.com', displayName: 'User 1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', }, { id: 'user-2', email: 'user2@example.com', displayName: 'User 2', roles: ['member'], status: 'suspended', isSystemAdmin: false, createdAt: '2024-01-02T00:00:00.000Z', updatedAt: '2024-01-16T12:00:00.000Z', }, { id: 'user-3', email: 'user3@example.com', displayName: 'User 3', roles: ['member'], status: 'deleted', isSystemAdmin: false, createdAt: '2024-01-03T00:00:00.000Z', updatedAt: '2024-01-17T12:00:00.000Z', }, ], total: 3, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.users[0].status).toBe('active'); expect(result.users[1].status).toBe('suspended'); expect(result.users[2].status).toBe('deleted'); expect(result.activeUserCount).toBe(1); }); it('should handle pagination metadata correctly', () => { const userListResponse: UserListResponse = { users: [ { id: 'user-1', email: 'user1@example.com', displayName: 'User 1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', }, ], total: 100, page: 5, limit: 20, totalPages: 5, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.total).toBe(100); expect(result.page).toBe(5); expect(result.limit).toBe(20); expect(result.totalPages).toBe(5); }); it('should handle users with empty roles array', () => { const userListResponse: UserListResponse = { users: [ { id: 'user-1', email: 'user1@example.com', displayName: 'User 1', roles: [], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', }, ], total: 1, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.users[0].roles).toEqual([]); }); it('should handle users with special characters in display name', () => { const userListResponse: UserListResponse = { users: [ { id: 'user-1', email: 'user1@example.com', displayName: 'User 1 & 2 (Admin)', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', }, ], total: 1, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.users[0].displayName).toBe('User 1 & 2 (Admin)'); }); it('should handle users with very long email addresses', () => { const longEmail = 'verylongemailaddresswithmanycharacters@example.com'; const userListResponse: UserListResponse = { users: [ { id: 'user-1', email: longEmail, displayName: 'User 1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z', }, ], total: 1, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.users[0].email).toBe(longEmail); }); }); describe('derived fields calculation', () => { it('should calculate activeUserCount correctly with mixed statuses', () => { const userListResponse: UserListResponse = { users: [ { id: '1', email: '1@e.com', displayName: '1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, { id: '2', email: '2@e.com', displayName: '2', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, { id: '3', email: '3@e.com', displayName: '3', roles: ['member'], status: 'suspended', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, { id: '4', email: '4@e.com', displayName: '4', roles: ['member'], status: 'deleted', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, ], total: 4, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.activeUserCount).toBe(2); }); it('should calculate adminCount correctly with mixed roles', () => { const userListResponse: UserListResponse = { users: [ { id: '1', email: '1@e.com', displayName: '1', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, { id: '2', email: '2@e.com', displayName: '2', roles: ['admin', 'owner'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, { id: '3', email: '3@e.com', displayName: '3', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, { id: '4', email: '4@e.com', displayName: '4', roles: ['owner'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, ], total: 4, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.adminCount).toBe(2); }); it('should handle all active users', () => { const userListResponse: UserListResponse = { users: [ { id: '1', email: '1@e.com', displayName: '1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, { id: '2', email: '2@e.com', displayName: '2', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, { id: '3', email: '3@e.com', displayName: '3', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, ], total: 3, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.activeUserCount).toBe(3); }); it('should handle no active users', () => { const userListResponse: UserListResponse = { users: [ { id: '1', email: '1@e.com', displayName: '1', roles: ['member'], status: 'suspended', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, { id: '2', email: '2@e.com', displayName: '2', roles: ['member'], status: 'deleted', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, ], total: 2, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.activeUserCount).toBe(0); }); it('should handle all system admins', () => { const userListResponse: UserListResponse = { users: [ { id: '1', email: '1@e.com', displayName: '1', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, { id: '2', email: '2@e.com', displayName: '2', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, { id: '3', email: '3@e.com', displayName: '3', roles: ['admin'], status: 'active', isSystemAdmin: true, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, ], total: 3, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.adminCount).toBe(3); }); it('should handle no system admins', () => { const userListResponse: UserListResponse = { users: [ { id: '1', email: '1@e.com', displayName: '1', roles: ['member'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, { id: '2', email: '2@e.com', displayName: '2', roles: ['owner'], status: 'active', isSystemAdmin: false, createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-15T12:00:00.000Z' }, ], total: 2, page: 1, limit: 10, totalPages: 1, }; const result = AdminUsersViewDataBuilder.build(userListResponse); expect(result.adminCount).toBe(0); }); }); });