/** * Contract Validation Tests for Admin Module * * These tests validate that the admin API DTOs and OpenAPI spec are consistent * and that the generated types will be compatible with the website admin client. */ import * as fs from 'fs/promises'; import * as path from 'path'; import { describe, expect, it } from 'vitest'; interface OpenAPISchema { type?: string; format?: string; $ref?: string; items?: OpenAPISchema; properties?: Record; required?: string[]; enum?: string[]; nullable?: boolean; description?: string; default?: unknown; } interface OpenAPISpec { openapi: string; info: { title: string; description: string; version: string; }; paths: Record; components: { schemas: Record; }; } describe('Admin Module Contract Validation', () => { const apiRoot = path.join(__dirname, '../..'); const openapiPath = path.join(apiRoot, 'apps/api/openapi.json'); const generatedTypesDir = path.join(apiRoot, 'apps/website/lib/types/generated'); const websiteTypesDir = path.join(apiRoot, 'apps/website/lib/types'); describe('OpenAPI Spec Integrity for Admin Endpoints', () => { it('should have admin endpoints defined in OpenAPI spec', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); // Check for admin endpoints expect(spec.paths['/admin/users']).toBeDefined(); expect(spec.paths['/admin/dashboard/stats']).toBeDefined(); // Verify GET methods exist expect(spec.paths['/admin/users'].get).toBeDefined(); expect(spec.paths['/admin/dashboard/stats'].get).toBeDefined(); }); it('should have ListUsersRequestDto schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['ListUsersRequestDto']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify optional query parameters expect(schema.properties?.role).toBeDefined(); expect(schema.properties?.status).toBeDefined(); expect(schema.properties?.email).toBeDefined(); expect(schema.properties?.search).toBeDefined(); expect(schema.properties?.page).toBeDefined(); expect(schema.properties?.limit).toBeDefined(); expect(schema.properties?.sortBy).toBeDefined(); expect(schema.properties?.sortDirection).toBeDefined(); }); it('should have UserResponseDto schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['UserResponseDto']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('id'); expect(schema.required).toContain('email'); expect(schema.required).toContain('displayName'); expect(schema.required).toContain('roles'); expect(schema.required).toContain('status'); expect(schema.required).toContain('isSystemAdmin'); expect(schema.required).toContain('createdAt'); expect(schema.required).toContain('updatedAt'); // Verify field types expect(schema.properties?.id?.type).toBe('string'); expect(schema.properties?.email?.type).toBe('string'); expect(schema.properties?.displayName?.type).toBe('string'); expect(schema.properties?.roles?.type).toBe('array'); expect(schema.properties?.status?.type).toBe('string'); expect(schema.properties?.isSystemAdmin?.type).toBe('boolean'); expect(schema.properties?.createdAt?.type).toBe('string'); expect(schema.properties?.updatedAt?.type).toBe('string'); // Verify optional fields expect(schema.properties?.lastLoginAt).toBeDefined(); expect(schema.properties?.lastLoginAt?.nullable).toBe(true); expect(schema.properties?.primaryDriverId).toBeDefined(); expect(schema.properties?.primaryDriverId?.nullable).toBe(true); }); it('should have UserListResponseDto schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['UserListResponseDto']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('users'); expect(schema.required).toContain('total'); expect(schema.required).toContain('page'); expect(schema.required).toContain('limit'); expect(schema.required).toContain('totalPages'); // Verify field types expect(schema.properties?.users?.type).toBe('array'); expect(schema.properties?.users?.items?.$ref).toBe('#/components/schemas/UserResponseDto'); expect(schema.properties?.total?.type).toBe('number'); expect(schema.properties?.page?.type).toBe('number'); expect(schema.properties?.limit?.type).toBe('number'); expect(schema.properties?.totalPages?.type).toBe('number'); }); it('should have DashboardStatsResponseDto schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardStatsResponseDto']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('totalUsers'); expect(schema.required).toContain('activeUsers'); expect(schema.required).toContain('suspendedUsers'); expect(schema.required).toContain('deletedUsers'); expect(schema.required).toContain('systemAdmins'); expect(schema.required).toContain('recentLogins'); expect(schema.required).toContain('newUsersToday'); expect(schema.required).toContain('userGrowth'); expect(schema.required).toContain('roleDistribution'); expect(schema.required).toContain('statusDistribution'); expect(schema.required).toContain('activityTimeline'); // Verify field types expect(schema.properties?.totalUsers?.type).toBe('number'); expect(schema.properties?.activeUsers?.type).toBe('number'); expect(schema.properties?.suspendedUsers?.type).toBe('number'); expect(schema.properties?.deletedUsers?.type).toBe('number'); expect(schema.properties?.systemAdmins?.type).toBe('number'); expect(schema.properties?.recentLogins?.type).toBe('number'); expect(schema.properties?.newUsersToday?.type).toBe('number'); // Verify nested objects expect(schema.properties?.userGrowth?.type).toBe('array'); expect(schema.properties?.roleDistribution?.type).toBe('array'); expect(schema.properties?.statusDistribution?.type).toBe('object'); expect(schema.properties?.activityTimeline?.type).toBe('array'); }); it('should have proper query parameter validation in OpenAPI', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const listUsersPath = spec.paths['/admin/users']?.get; expect(listUsersPath).toBeDefined(); // Verify query parameters are documented const params = listUsersPath.parameters || []; const paramNames = params.map((p: any) => p.name); // These should be query parameters based on the DTO expect(paramNames).toContain('role'); expect(paramNames).toContain('status'); expect(paramNames).toContain('email'); expect(paramNames).toContain('search'); expect(paramNames).toContain('page'); expect(paramNames).toContain('limit'); expect(paramNames).toContain('sortBy'); expect(paramNames).toContain('sortDirection'); }); }); describe('DTO Consistency', () => { it('should have generated DTO files for admin schemas', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const generatedFiles = await fs.readdir(generatedTypesDir); const generatedDTOs = generatedFiles .filter(f => f.endsWith('.ts')) .map(f => f.replace('.ts', '')); // Check for admin-related DTOs const adminDTOs = [ 'ListUsersRequestDto', 'UserResponseDto', 'UserListResponseDto', 'DashboardStatsResponseDto', ]; for (const dtoName of adminDTOs) { expect(spec.components.schemas[dtoName]).toBeDefined(); expect(generatedDTOs).toContain(dtoName); } }); it('should have consistent property types between DTOs and schemas', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schemas = spec.components.schemas; // Test ListUsersRequestDto const listUsersSchema = schemas['ListUsersRequestDto']; const listUsersDtoPath = path.join(generatedTypesDir, 'ListUsersRequestDto.ts'); const listUsersDtoExists = await fs.access(listUsersDtoPath).then(() => true).catch(() => false); if (listUsersDtoExists) { const listUsersDtoContent = await fs.readFile(listUsersDtoPath, 'utf-8'); // Check that all properties are present if (listUsersSchema.properties) { for (const propName of Object.keys(listUsersSchema.properties)) { expect(listUsersDtoContent).toContain(propName); } } } // Test UserResponseDto const userSchema = schemas['UserResponseDto']; const userDtoPath = path.join(generatedTypesDir, 'UserResponseDto.ts'); const userDtoExists = await fs.access(userDtoPath).then(() => true).catch(() => false); if (userDtoExists) { const userDtoContent = await fs.readFile(userDtoPath, 'utf-8'); // Check that all required properties are present if (userSchema.required) { for (const requiredProp of userSchema.required) { expect(userDtoContent).toContain(requiredProp); } } // Check that all properties are present if (userSchema.properties) { for (const propName of Object.keys(userSchema.properties)) { expect(userDtoContent).toContain(propName); } } } // Test UserListResponseDto const userListSchema = schemas['UserListResponseDto']; const userListDtoPath = path.join(generatedTypesDir, 'UserListResponseDto.ts'); const userListDtoExists = await fs.access(userListDtoPath).then(() => true).catch(() => false); if (userListDtoExists) { const userListDtoContent = await fs.readFile(userListDtoPath, 'utf-8'); // Check that all required properties are present if (userListSchema.required) { for (const requiredProp of userListSchema.required) { expect(userListDtoContent).toContain(requiredProp); } } } // Test DashboardStatsResponseDto const dashboardSchema = schemas['DashboardStatsResponseDto']; const dashboardDtoPath = path.join(generatedTypesDir, 'DashboardStatsResponseDto.ts'); const dashboardDtoExists = await fs.access(dashboardDtoPath).then(() => true).catch(() => false); if (dashboardDtoExists) { const dashboardDtoContent = await fs.readFile(dashboardDtoPath, 'utf-8'); // Check that all required properties are present if (dashboardSchema.required) { for (const requiredProp of dashboardSchema.required) { expect(dashboardDtoContent).toContain(requiredProp); } } } }); it('should have TBD admin types defined', async () => { const adminTypesPath = path.join(websiteTypesDir, 'tbd', 'AdminUserDto.ts'); const adminTypesExists = await fs.access(adminTypesPath).then(() => true).catch(() => false); expect(adminTypesExists).toBe(true); const adminTypesContent = await fs.readFile(adminTypesPath, 'utf-8'); // Verify UserDto interface expect(adminTypesContent).toContain('export interface AdminUserDto'); expect(adminTypesContent).toContain('id: string'); expect(adminTypesContent).toContain('email: string'); expect(adminTypesContent).toContain('displayName: string'); expect(adminTypesContent).toContain('roles: string[]'); expect(adminTypesContent).toContain('status: string'); expect(adminTypesContent).toContain('isSystemAdmin: boolean'); expect(adminTypesContent).toContain('createdAt: string'); expect(adminTypesContent).toContain('updatedAt: string'); expect(adminTypesContent).toContain('lastLoginAt?: string'); expect(adminTypesContent).toContain('primaryDriverId?: string'); // Verify UserListResponse interface expect(adminTypesContent).toContain('export interface AdminUserListResponseDto'); expect(adminTypesContent).toContain('users: AdminUserDto[]'); expect(adminTypesContent).toContain('total: number'); expect(adminTypesContent).toContain('page: number'); expect(adminTypesContent).toContain('limit: number'); expect(adminTypesContent).toContain('totalPages: number'); // Verify DashboardStats interface expect(adminTypesContent).toContain('export interface AdminDashboardStatsDto'); expect(adminTypesContent).toContain('totalUsers: number'); expect(adminTypesContent).toContain('activeUsers: number'); expect(adminTypesContent).toContain('suspendedUsers: number'); expect(adminTypesContent).toContain('deletedUsers: number'); expect(adminTypesContent).toContain('systemAdmins: number'); expect(adminTypesContent).toContain('recentLogins: number'); expect(adminTypesContent).toContain('newUsersToday: number'); // Verify ListUsersQuery interface expect(adminTypesContent).toContain('export interface AdminListUsersQueryDto'); expect(adminTypesContent).toContain('role?: string'); expect(adminTypesContent).toContain('status?: string'); expect(adminTypesContent).toContain('email?: string'); expect(adminTypesContent).toContain('search?: string'); expect(adminTypesContent).toContain('page?: number'); expect(adminTypesContent).toContain('limit?: number'); expect(adminTypesContent).toContain("sortBy?: 'email' | 'displayName' | 'createdAt' | 'lastLoginAt' | 'status'"); expect(adminTypesContent).toContain("sortDirection?: 'asc' | 'desc'"); }); it('should have admin types re-exported from main types file', async () => { const adminTypesPath = path.join(websiteTypesDir, 'admin.ts'); const adminTypesExists = await fs.access(adminTypesPath).then(() => true).catch(() => false); expect(adminTypesExists).toBe(true); const adminTypesContent = await fs.readFile(adminTypesPath, 'utf-8'); // Verify re-exports expect(adminTypesContent).toContain('AdminUserDto as UserDto'); expect(adminTypesContent).toContain('AdminUserListResponseDto as UserListResponse'); expect(adminTypesContent).toContain('AdminListUsersQueryDto as ListUsersQuery'); expect(adminTypesContent).toContain('AdminDashboardStatsDto as DashboardStats'); }); }); describe('Admin API Client Contract', () => { it('should have AdminApiClient defined', async () => { const adminApiClientPath = path.join(apiRoot, 'apps/website/lib/api/admin/AdminApiClient.ts'); const adminApiClientExists = await fs.access(adminApiClientPath).then(() => true).catch(() => false); expect(adminApiClientExists).toBe(true); const adminApiClientContent = await fs.readFile(adminApiClientPath, 'utf-8'); // Verify class definition expect(adminApiClientContent).toContain('export class AdminApiClient'); expect(adminApiClientContent).toContain('extends BaseApiClient'); // Verify methods exist expect(adminApiClientContent).toContain('async listUsers'); expect(adminApiClientContent).toContain('async getUser'); expect(adminApiClientContent).toContain('async updateUserRoles'); expect(adminApiClientContent).toContain('async updateUserStatus'); expect(adminApiClientContent).toContain('async deleteUser'); expect(adminApiClientContent).toContain('async createUser'); expect(adminApiClientContent).toContain('async getDashboardStats'); // Verify method signatures expect(adminApiClientContent).toContain('listUsers(query: ListUsersQuery = {})'); expect(adminApiClientContent).toContain('getUser(userId: string)'); expect(adminApiClientContent).toContain('updateUserRoles(userId: string, roles: string[])'); expect(adminApiClientContent).toContain('updateUserStatus(userId: string, status: string)'); expect(adminApiClientContent).toContain('deleteUser(userId: string)'); expect(adminApiClientContent).toContain('createUser(userData: {'); expect(adminApiClientContent).toContain('getDashboardStats()'); }); it('should have proper request construction in listUsers method', async () => { const adminApiClientPath = path.join(apiRoot, 'apps/website/lib/api/admin/AdminApiClient.ts'); const adminApiClientContent = await fs.readFile(adminApiClientPath, 'utf-8'); // Verify query parameter construction expect(adminApiClientContent).toContain('const params = new URLSearchParams()'); expect(adminApiClientContent).toContain("params.append('role', query.role)"); expect(adminApiClientContent).toContain("params.append('status', query.status)"); expect(adminApiClientContent).toContain("params.append('email', query.email)"); expect(adminApiClientContent).toContain("params.append('search', query.search)"); expect(adminApiClientContent).toContain("params.append('page', query.page.toString())"); expect(adminApiClientContent).toContain("params.append('limit', query.limit.toString())"); expect(adminApiClientContent).toContain("params.append('sortBy', query.sortBy)"); expect(adminApiClientContent).toContain("params.append('sortDirection', query.sortDirection)"); // Verify endpoint construction expect(adminApiClientContent).toContain("return this.get(`/admin/users?${params.toString()}`)"); }); it('should have proper request construction in createUser method', async () => { const adminApiClientPath = path.join(apiRoot, 'apps/website/lib/api/admin/AdminApiClient.ts'); const adminApiClientContent = await fs.readFile(adminApiClientPath, 'utf-8'); // Verify POST request with userData expect(adminApiClientContent).toContain("return this.post(`/admin/users`, userData)"); }); it('should have proper request construction in getDashboardStats method', async () => { const adminApiClientPath = path.join(apiRoot, 'apps/website/lib/api/admin/AdminApiClient.ts'); const adminApiClientContent = await fs.readFile(adminApiClientPath, 'utf-8'); // Verify GET request expect(adminApiClientContent).toContain("return this.get(`/admin/dashboard/stats`)"); }); }); describe('Request Correctness Tests', () => { it('should validate ListUsersRequestDto query parameters', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['ListUsersRequestDto']; // Verify all query parameters are optional (no required fields) expect(schema.required).toBeUndefined(); // Verify enum values for role expect(schema.properties?.role?.enum).toBeUndefined(); // No enum constraint in DTO // Verify enum values for status expect(schema.properties?.status?.enum).toBeUndefined(); // No enum constraint in DTO // Verify enum values for sortBy expect(schema.properties?.sortBy?.enum).toEqual([ 'email', 'displayName', 'createdAt', 'lastLoginAt', 'status' ]); // Verify enum values for sortDirection expect(schema.properties?.sortDirection?.enum).toEqual(['asc', 'desc']); expect(schema.properties?.sortDirection?.default).toBe('asc'); // Verify numeric constraints expect(schema.properties?.page?.minimum).toBe(1); expect(schema.properties?.page?.default).toBe(1); expect(schema.properties?.limit?.minimum).toBe(1); expect(schema.properties?.limit?.maximum).toBe(100); expect(schema.properties?.limit?.default).toBe(10); }); it('should validate UserResponseDto field constraints', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['UserResponseDto']; // Verify required fields expect(schema.required).toContain('id'); expect(schema.required).toContain('email'); expect(schema.required).toContain('displayName'); expect(schema.required).toContain('roles'); expect(schema.required).toContain('status'); expect(schema.required).toContain('isSystemAdmin'); expect(schema.required).toContain('createdAt'); expect(schema.required).toContain('updatedAt'); // Verify optional fields are nullable expect(schema.properties?.lastLoginAt?.nullable).toBe(true); expect(schema.properties?.primaryDriverId?.nullable).toBe(true); // Verify roles is an array expect(schema.properties?.roles?.type).toBe('array'); expect(schema.properties?.roles?.items?.type).toBe('string'); }); it('should validate DashboardStatsResponseDto structure', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['DashboardStatsResponseDto']; // Verify all required fields const requiredFields = [ 'totalUsers', 'activeUsers', 'suspendedUsers', 'deletedUsers', 'systemAdmins', 'recentLogins', 'newUsersToday', 'userGrowth', 'roleDistribution', 'statusDistribution', 'activityTimeline' ]; for (const field of requiredFields) { expect(schema.required).toContain(field); } // Verify nested object structures expect(schema.properties?.userGrowth?.items?.$ref).toBe('#/components/schemas/UserGrowthDto'); expect(schema.properties?.roleDistribution?.items?.$ref).toBe('#/components/schemas/RoleDistributionDto'); expect(schema.properties?.statusDistribution?.$ref).toBe('#/components/schemas/StatusDistributionDto'); expect(schema.properties?.activityTimeline?.items?.$ref).toBe('#/components/schemas/ActivityTimelineDto'); }); }); describe('Response Handling Tests', () => { it('should handle successful user list response', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const userListSchema = spec.components.schemas['UserListResponseDto']; const userSchema = spec.components.schemas['UserResponseDto']; // Verify response structure expect(userListSchema.properties?.users).toBeDefined(); expect(userListSchema.properties?.users?.items?.$ref).toBe('#/components/schemas/UserResponseDto'); // Verify user object has all required fields expect(userSchema.required).toContain('id'); expect(userSchema.required).toContain('email'); expect(userSchema.required).toContain('displayName'); expect(userSchema.required).toContain('roles'); expect(userSchema.required).toContain('status'); expect(userSchema.required).toContain('isSystemAdmin'); expect(userSchema.required).toContain('createdAt'); expect(userSchema.required).toContain('updatedAt'); // Verify optional fields expect(userSchema.properties?.lastLoginAt).toBeDefined(); expect(userSchema.properties?.primaryDriverId).toBeDefined(); }); it('should handle successful dashboard stats response', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const dashboardSchema = spec.components.schemas['DashboardStatsResponseDto']; // Verify all required fields are present expect(dashboardSchema.required).toContain('totalUsers'); expect(dashboardSchema.required).toContain('activeUsers'); expect(dashboardSchema.required).toContain('suspendedUsers'); expect(dashboardSchema.required).toContain('deletedUsers'); expect(dashboardSchema.required).toContain('systemAdmins'); expect(dashboardSchema.required).toContain('recentLogins'); expect(dashboardSchema.required).toContain('newUsersToday'); expect(dashboardSchema.required).toContain('userGrowth'); expect(dashboardSchema.required).toContain('roleDistribution'); expect(dashboardSchema.required).toContain('statusDistribution'); expect(dashboardSchema.required).toContain('activityTimeline'); // Verify nested objects are properly typed expect(dashboardSchema.properties?.userGrowth?.type).toBe('array'); expect(dashboardSchema.properties?.roleDistribution?.type).toBe('array'); expect(dashboardSchema.properties?.statusDistribution?.type).toBe('object'); expect(dashboardSchema.properties?.activityTimeline?.type).toBe('array'); }); it('should handle optional fields in user response', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const userSchema = spec.components.schemas['UserResponseDto']; // Verify optional fields are nullable expect(userSchema.properties?.lastLoginAt?.nullable).toBe(true); expect(userSchema.properties?.primaryDriverId?.nullable).toBe(true); // Verify optional fields are not in required array expect(userSchema.required).not.toContain('lastLoginAt'); expect(userSchema.required).not.toContain('primaryDriverId'); }); it('should handle pagination fields correctly', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const userListSchema = spec.components.schemas['UserListResponseDto']; // Verify pagination fields are required expect(userListSchema.required).toContain('total'); expect(userListSchema.required).toContain('page'); expect(userListSchema.required).toContain('limit'); expect(userListSchema.required).toContain('totalPages'); // Verify pagination field types expect(userListSchema.properties?.total?.type).toBe('number'); expect(userListSchema.properties?.page?.type).toBe('number'); expect(userListSchema.properties?.limit?.type).toBe('number'); expect(userListSchema.properties?.totalPages?.type).toBe('number'); }); }); describe('Error Handling Tests', () => { it('should document 403 Forbidden response for admin endpoints', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const listUsersPath = spec.paths['/admin/users']?.get; const dashboardStatsPath = spec.paths['/admin/dashboard/stats']?.get; // Verify 403 response is documented expect(listUsersPath.responses['403']).toBeDefined(); expect(dashboardStatsPath.responses['403']).toBeDefined(); }); it('should document 401 Unauthorized response for admin endpoints', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const listUsersPath = spec.paths['/admin/users']?.get; const dashboardStatsPath = spec.paths['/admin/dashboard/stats']?.get; // Verify 401 response is documented expect(listUsersPath.responses['401']).toBeDefined(); expect(dashboardStatsPath.responses['401']).toBeDefined(); }); it('should document 400 Bad Request response for invalid query parameters', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const listUsersPath = spec.paths['/admin/users']?.get; // Verify 400 response is documented expect(listUsersPath.responses['400']).toBeDefined(); }); it('should document 500 Internal Server Error response', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const listUsersPath = spec.paths['/admin/users']?.get; const dashboardStatsPath = spec.paths['/admin/dashboard/stats']?.get; // Verify 500 response is documented expect(listUsersPath.responses['500']).toBeDefined(); expect(dashboardStatsPath.responses['500']).toBeDefined(); }); it('should document 404 Not Found response for user operations', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); // Check for user-specific endpoints (if they exist) const getUserPath = spec.paths['/admin/users/{userId}']?.get; if (getUserPath) { expect(getUserPath.responses['404']).toBeDefined(); } }); it('should document 409 Conflict response for duplicate operations', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); // Check for create user endpoint (if it exists) const createUserPath = spec.paths['/admin/users']?.post; if (createUserPath) { expect(createUserPath.responses['409']).toBeDefined(); } }); }); describe('Semantic Guarantee Tests', () => { it('should maintain ordering guarantees for user list', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const listUsersPath = spec.paths['/admin/users']?.get; // Verify sortBy and sortDirection parameters are documented const params = listUsersPath.parameters || []; const sortByParam = params.find((p: any) => p.name === 'sortBy'); const sortDirectionParam = params.find((p: any) => p.name === 'sortDirection'); expect(sortByParam).toBeDefined(); expect(sortDirectionParam).toBeDefined(); // Verify sortDirection has default value expect(sortDirectionParam?.schema?.default).toBe('asc'); }); it('should validate pagination consistency', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const userListSchema = spec.components.schemas['UserListResponseDto']; // Verify pagination fields are all required expect(userListSchema.required).toContain('page'); expect(userListSchema.required).toContain('limit'); expect(userListSchema.required).toContain('total'); expect(userListSchema.required).toContain('totalPages'); // Verify page and limit have constraints const listUsersPath = spec.paths['/admin/users']?.get; const params = listUsersPath.parameters || []; const pageParam = params.find((p: any) => p.name === 'page'); const limitParam = params.find((p: any) => p.name === 'limit'); expect(pageParam?.schema?.minimum).toBe(1); expect(pageParam?.schema?.default).toBe(1); expect(limitParam?.schema?.minimum).toBe(1); expect(limitParam?.schema?.maximum).toBe(100); expect(limitParam?.schema?.default).toBe(10); }); it('should validate idempotency for user status updates', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); // Check for user status update endpoint (if it exists) const updateUserStatusPath = spec.paths['/admin/users/{userId}/status']?.patch; if (updateUserStatusPath) { // Verify it accepts a status parameter const params = updateUserStatusPath.parameters || []; const statusParam = params.find((p: any) => p.name === 'status'); expect(statusParam).toBeDefined(); } }); it('should validate uniqueness constraints for user email', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const userSchema = spec.components.schemas['UserResponseDto']; // Verify email is a required field expect(userSchema.required).toContain('email'); expect(userSchema.properties?.email?.type).toBe('string'); // Check for create user endpoint (if it exists) const createUserPath = spec.paths['/admin/users']?.post; if (createUserPath) { // Verify email is required in request body const requestBody = createUserPath.requestBody; if (requestBody && requestBody.content && requestBody.content['application/json']) { const schema = requestBody.content['application/json'].schema; if (schema && schema.$ref) { // This would reference a CreateUserDto which should have email as required expect(schema.$ref).toContain('CreateUserDto'); } } } }); it('should validate consistency between request and response schemas', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const userSchema = spec.components.schemas['UserResponseDto']; const userListSchema = spec.components.schemas['UserListResponseDto']; // Verify UserListResponse contains array of UserResponse expect(userListSchema.properties?.users?.items?.$ref).toBe('#/components/schemas/UserResponseDto'); // Verify UserResponse has consistent field types expect(userSchema.properties?.id?.type).toBe('string'); expect(userSchema.properties?.email?.type).toBe('string'); expect(userSchema.properties?.displayName?.type).toBe('string'); expect(userSchema.properties?.roles?.type).toBe('array'); expect(userSchema.properties?.status?.type).toBe('string'); expect(userSchema.properties?.isSystemAdmin?.type).toBe('boolean'); }); it('should validate semantic consistency in dashboard stats', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const dashboardSchema = spec.components.schemas['DashboardStatsResponseDto']; // Verify totalUsers >= activeUsers + suspendedUsers + deletedUsers // (This is a semantic guarantee that should be enforced by the backend) expect(dashboardSchema.properties?.totalUsers).toBeDefined(); expect(dashboardSchema.properties?.activeUsers).toBeDefined(); expect(dashboardSchema.properties?.suspendedUsers).toBeDefined(); expect(dashboardSchema.properties?.deletedUsers).toBeDefined(); // Verify systemAdmins is a subset of totalUsers expect(dashboardSchema.properties?.systemAdmins).toBeDefined(); // Verify recentLogins and newUsersToday are non-negative expect(dashboardSchema.properties?.recentLogins).toBeDefined(); expect(dashboardSchema.properties?.newUsersToday).toBeDefined(); }); it('should validate pagination metadata consistency', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const userListSchema = spec.components.schemas['UserListResponseDto']; // Verify pagination metadata is always present expect(userListSchema.required).toContain('page'); expect(userListSchema.required).toContain('limit'); expect(userListSchema.required).toContain('total'); expect(userListSchema.required).toContain('totalPages'); // Verify totalPages calculation is consistent // totalPages should be >= 1 and should be calculated as Math.ceil(total / limit) expect(userListSchema.properties?.totalPages?.type).toBe('number'); }); }); describe('Admin Module Integration Tests', () => { it('should have consistent types between API DTOs and website types', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const adminTypesPath = path.join(websiteTypesDir, 'tbd', 'AdminUserDto.ts'); const adminTypesContent = await fs.readFile(adminTypesPath, 'utf-8'); // Verify UserDto interface matches UserResponseDto schema const userSchema = spec.components.schemas['UserResponseDto']; expect(adminTypesContent).toContain('export interface AdminUserDto'); // Check all required fields from schema are in interface for (const field of userSchema.required || []) { expect(adminTypesContent).toContain(`${field}:`); } // Check all properties from schema are in interface if (userSchema.properties) { for (const propName of Object.keys(userSchema.properties)) { expect(adminTypesContent).toContain(propName); } } }); it('should have consistent query types between API and website', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const adminTypesPath = path.join(websiteTypesDir, 'tbd', 'AdminUserDto.ts'); const adminTypesContent = await fs.readFile(adminTypesPath, 'utf-8'); // Verify ListUsersQuery interface matches ListUsersRequestDto schema const listUsersSchema = spec.components.schemas['ListUsersRequestDto']; expect(adminTypesContent).toContain('export interface AdminListUsersQueryDto'); // Check all properties from schema are in interface if (listUsersSchema.properties) { for (const propName of Object.keys(listUsersSchema.properties)) { expect(adminTypesContent).toContain(propName); } } }); it('should have consistent response types between API and website', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const adminTypesPath = path.join(websiteTypesDir, 'tbd', 'AdminUserDto.ts'); const adminTypesContent = await fs.readFile(adminTypesPath, 'utf-8'); // Verify UserListResponse interface matches UserListResponseDto schema const userListSchema = spec.components.schemas['UserListResponseDto']; expect(adminTypesContent).toContain('export interface AdminUserListResponseDto'); // Check all required fields from schema are in interface for (const field of userListSchema.required || []) { expect(adminTypesContent).toContain(`${field}:`); } // Verify DashboardStats interface matches DashboardStatsResponseDto schema const dashboardSchema = spec.components.schemas['DashboardStatsResponseDto']; expect(adminTypesContent).toContain('export interface AdminDashboardStatsDto'); // Check all required fields from schema are in interface for (const field of dashboardSchema.required || []) { expect(adminTypesContent).toContain(`${field}:`); } }); it('should have AdminApiClient methods matching API endpoints', async () => { const adminApiClientPath = path.join(apiRoot, 'apps/website/lib/api/admin/AdminApiClient.ts'); const adminApiClientContent = await fs.readFile(adminApiClientPath, 'utf-8'); // Verify listUsers method exists and uses correct endpoint expect(adminApiClientContent).toContain('async listUsers'); expect(adminApiClientContent).toContain("return this.get(`/admin/users?${params.toString()}`)"); // Verify getDashboardStats method exists and uses correct endpoint expect(adminApiClientContent).toContain('async getDashboardStats'); expect(adminApiClientContent).toContain("return this.get(`/admin/dashboard/stats`)"); }); it('should have proper error handling in AdminApiClient', async () => { const adminApiClientPath = path.join(apiRoot, 'apps/website/lib/api/admin/AdminApiClient.ts'); const adminApiClientContent = await fs.readFile(adminApiClientPath, 'utf-8'); // Verify BaseApiClient is extended (which provides error handling) expect(adminApiClientContent).toContain('extends BaseApiClient'); // Verify methods use BaseApiClient methods (which handle errors) expect(adminApiClientContent).toContain('this.get<'); expect(adminApiClientContent).toContain('this.post<'); expect(adminApiClientContent).toContain('this.patch<'); expect(adminApiClientContent).toContain('this.delete<'); }); }); });