/** * Contract Validation Tests for Auth Module * * These tests validate that the auth API DTOs and OpenAPI spec are consistent * and that the generated types will be compatible with the website auth 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('Auth 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 Auth Endpoints', () => { it('should have auth endpoints defined in OpenAPI spec', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); // Check for auth endpoints expect(spec.paths['/auth/signup']).toBeDefined(); expect(spec.paths['/auth/login']).toBeDefined(); expect(spec.paths['/auth/session']).toBeDefined(); expect(spec.paths['/auth/logout']).toBeDefined(); expect(spec.paths['/auth/forgot-password']).toBeDefined(); expect(spec.paths['/auth/reset-password']).toBeDefined(); // Verify POST methods exist for signup, login, forgot-password, reset-password expect(spec.paths['/auth/signup'].post).toBeDefined(); expect(spec.paths['/auth/login'].post).toBeDefined(); expect(spec.paths['/auth/forgot-password'].post).toBeDefined(); expect(spec.paths['/auth/reset-password'].post).toBeDefined(); // Verify GET methods exist for session expect(spec.paths['/auth/session'].get).toBeDefined(); }); it('should have AuthSessionDTO schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['AuthSessionDTO']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('token'); expect(schema.required).toContain('user'); // Verify field types expect(schema.properties?.token?.type).toBe('string'); expect(schema.properties?.user?.$ref).toBe('#/components/schemas/AuthenticatedUserDTO'); }); it('should have AuthenticatedUserDTO schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['AuthenticatedUserDTO']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('userId'); expect(schema.required).toContain('email'); expect(schema.required).toContain('displayName'); // Verify field types expect(schema.properties?.userId?.type).toBe('string'); expect(schema.properties?.email?.type).toBe('string'); expect(schema.properties?.displayName?.type).toBe('string'); // Verify optional fields expect(schema.properties?.primaryDriverId).toBeDefined(); expect(schema.properties?.avatarUrl).toBeDefined(); expect(schema.properties?.avatarUrl?.nullable).toBe(true); expect(schema.properties?.companyId).toBeDefined(); expect(schema.properties?.role).toBeDefined(); }); it('should have SignupParamsDTO schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['SignupParamsDTO']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('email'); expect(schema.required).toContain('password'); expect(schema.required).toContain('displayName'); // Verify field types expect(schema.properties?.email?.type).toBe('string'); expect(schema.properties?.password?.type).toBe('string'); expect(schema.properties?.displayName?.type).toBe('string'); // Verify optional fields expect(schema.properties?.username).toBeDefined(); expect(schema.properties?.iracingCustomerId).toBeDefined(); expect(schema.properties?.primaryDriverId).toBeDefined(); expect(schema.properties?.avatarUrl).toBeDefined(); expect(schema.properties?.avatarUrl?.nullable).toBe(true); }); it('should have LoginParamsDTO schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['LoginParamsDTO']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('email'); expect(schema.required).toContain('password'); // Verify field types expect(schema.properties?.email?.type).toBe('string'); expect(schema.properties?.password?.type).toBe('string'); // Verify optional fields expect(schema.properties?.rememberMe).toBeDefined(); expect(schema.properties?.rememberMe?.type).toBe('boolean'); expect(schema.properties?.rememberMe?.default).toBe(false); }); it('should have ForgotPasswordDTO schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['ForgotPasswordDTO']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('email'); // Verify field types expect(schema.properties?.email?.type).toBe('string'); }); it('should have ResetPasswordDTO schema defined', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['ResetPasswordDTO']; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // Verify required fields expect(schema.required).toContain('token'); expect(schema.required).toContain('newPassword'); // Verify field types expect(schema.properties?.token?.type).toBe('string'); expect(schema.properties?.newPassword?.type).toBe('string'); }); it('should have proper request/response structure for signup endpoint', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const signupPath = spec.paths['/auth/signup']?.post; expect(signupPath).toBeDefined(); // Verify request body const requestBody = signupPath.requestBody; expect(requestBody).toBeDefined(); expect(requestBody.content['application/json']).toBeDefined(); expect(requestBody.content['application/json'].schema.$ref).toBe('#/components/schemas/SignupParamsDTO'); // Verify response const response200 = signupPath.responses['200']; expect(response200).toBeDefined(); expect(response200.content['application/json'].schema.$ref).toBe('#/components/schemas/AuthSessionDTO'); }); it('should have proper request/response structure for login endpoint', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const loginPath = spec.paths['/auth/login']?.post; expect(loginPath).toBeDefined(); // Verify request body const requestBody = loginPath.requestBody; expect(requestBody).toBeDefined(); expect(requestBody.content['application/json']).toBeDefined(); expect(requestBody.content['application/json'].schema.$ref).toBe('#/components/schemas/LoginParamsDTO'); // Verify response const response200 = loginPath.responses['200']; expect(response200).toBeDefined(); expect(response200.content['application/json'].schema.$ref).toBe('#/components/schemas/AuthSessionDTO'); }); it('should have proper response structure for session endpoint', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const sessionPath = spec.paths['/auth/session']?.get; expect(sessionPath).toBeDefined(); // Verify response const response200 = sessionPath.responses['200']; expect(response200).toBeDefined(); expect(response200.content['application/json'].schema.$ref).toBe('#/components/schemas/AuthSessionDTO'); }); it('should have proper request/response structure for forgot-password endpoint', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const forgotPasswordPath = spec.paths['/auth/forgot-password']?.post; expect(forgotPasswordPath).toBeDefined(); // Verify request body const requestBody = forgotPasswordPath.requestBody; expect(requestBody).toBeDefined(); expect(requestBody.content['application/json']).toBeDefined(); expect(requestBody.content['application/json'].schema.$ref).toBe('#/components/schemas/ForgotPasswordDTO'); // Verify response const response200 = forgotPasswordPath.responses['200']; expect(response200).toBeDefined(); }); it('should have proper request/response structure for reset-password endpoint', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const resetPasswordPath = spec.paths['/auth/reset-password']?.post; expect(resetPasswordPath).toBeDefined(); // Verify request body const requestBody = resetPasswordPath.requestBody; expect(requestBody).toBeDefined(); expect(requestBody.content['application/json']).toBeDefined(); expect(requestBody.content['application/json'].schema.$ref).toBe('#/components/schemas/ResetPasswordDTO'); // Verify response const response200 = resetPasswordPath.responses['200']; expect(response200).toBeDefined(); }); }); describe('DTO Consistency', () => { it('should have generated DTO files for auth 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 auth-related DTOs const authDTOs = [ 'AuthSessionDTO', 'AuthenticatedUserDTO', 'SignupParamsDTO', 'LoginParamsDTO', 'ForgotPasswordDTO', 'ResetPasswordDTO', ]; for (const dtoName of authDTOs) { 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 AuthSessionDTO const authSessionSchema = schemas['AuthSessionDTO']; const authSessionDtoPath = path.join(generatedTypesDir, 'AuthSessionDTO.ts'); const authSessionDtoExists = await fs.access(authSessionDtoPath).then(() => true).catch(() => false); if (authSessionDtoExists) { const authSessionDtoContent = await fs.readFile(authSessionDtoPath, 'utf-8'); // Check that all required properties are present if (authSessionSchema.required) { for (const requiredProp of authSessionSchema.required) { expect(authSessionDtoContent).toContain(requiredProp); } } // Check that all properties are present if (authSessionSchema.properties) { for (const propName of Object.keys(authSessionSchema.properties)) { expect(authSessionDtoContent).toContain(propName); } } } // Test AuthenticatedUserDTO const authenticatedUserSchema = schemas['AuthenticatedUserDTO']; const authenticatedUserDtoPath = path.join(generatedTypesDir, 'AuthenticatedUserDTO.ts'); const authenticatedUserDtoExists = await fs.access(authenticatedUserDtoPath).then(() => true).catch(() => false); if (authenticatedUserDtoExists) { const authenticatedUserDtoContent = await fs.readFile(authenticatedUserDtoPath, 'utf-8'); // Check that all required properties are present if (authenticatedUserSchema.required) { for (const requiredProp of authenticatedUserSchema.required) { expect(authenticatedUserDtoContent).toContain(requiredProp); } } // Check that all properties are present if (authenticatedUserSchema.properties) { for (const propName of Object.keys(authenticatedUserSchema.properties)) { expect(authenticatedUserDtoContent).toContain(propName); } } } // Test SignupParamsDTO const signupParamsSchema = schemas['SignupParamsDTO']; const signupParamsDtoPath = path.join(generatedTypesDir, 'SignupParamsDTO.ts'); const signupParamsDtoExists = await fs.access(signupParamsDtoPath).then(() => true).catch(() => false); if (signupParamsDtoExists) { const signupParamsDtoContent = await fs.readFile(signupParamsDtoPath, 'utf-8'); // Check that all required properties are present if (signupParamsSchema.required) { for (const requiredProp of signupParamsSchema.required) { expect(signupParamsDtoContent).toContain(requiredProp); } } // Check that all properties are present if (signupParamsSchema.properties) { for (const propName of Object.keys(signupParamsSchema.properties)) { expect(signupParamsDtoContent).toContain(propName); } } } // Test LoginParamsDTO const loginParamsSchema = schemas['LoginParamsDTO']; const loginParamsDtoPath = path.join(generatedTypesDir, 'LoginParamsDTO.ts'); const loginParamsDtoExists = await fs.access(loginParamsDtoPath).then(() => true).catch(() => false); if (loginParamsDtoExists) { const loginParamsDtoContent = await fs.readFile(loginParamsDtoPath, 'utf-8'); // Check that all required properties are present if (loginParamsSchema.required) { for (const requiredProp of loginParamsSchema.required) { expect(loginParamsDtoContent).toContain(requiredProp); } } // Check that all properties are present if (loginParamsSchema.properties) { for (const propName of Object.keys(loginParamsSchema.properties)) { expect(loginParamsDtoContent).toContain(propName); } } } // Test ForgotPasswordDTO const forgotPasswordSchema = schemas['ForgotPasswordDTO']; const forgotPasswordDtoPath = path.join(generatedTypesDir, 'ForgotPasswordDTO.ts'); const forgotPasswordDtoExists = await fs.access(forgotPasswordDtoPath).then(() => true).catch(() => false); if (forgotPasswordDtoExists) { const forgotPasswordDtoContent = await fs.readFile(forgotPasswordDtoPath, 'utf-8'); // Check that all required properties are present if (forgotPasswordSchema.required) { for (const requiredProp of forgotPasswordSchema.required) { expect(forgotPasswordDtoContent).toContain(requiredProp); } } // Check that all properties are present if (forgotPasswordSchema.properties) { for (const propName of Object.keys(forgotPasswordSchema.properties)) { expect(forgotPasswordDtoContent).toContain(propName); } } } // Test ResetPasswordDTO const resetPasswordSchema = schemas['ResetPasswordDTO']; const resetPasswordDtoPath = path.join(generatedTypesDir, 'ResetPasswordDTO.ts'); const resetPasswordDtoExists = await fs.access(resetPasswordDtoPath).then(() => true).catch(() => false); if (resetPasswordDtoExists) { const resetPasswordDtoContent = await fs.readFile(resetPasswordDtoPath, 'utf-8'); // Check that all required properties are present if (resetPasswordSchema.required) { for (const requiredProp of resetPasswordSchema.required) { expect(resetPasswordDtoContent).toContain(requiredProp); } } // Check that all properties are present if (resetPasswordSchema.properties) { for (const propName of Object.keys(resetPasswordSchema.properties)) { expect(resetPasswordDtoContent).toContain(propName); } } } }); it('should have auth types defined in tbd folder', async () => { // Check if auth types exist in tbd folder (similar to admin types) const tbdDir = path.join(websiteTypesDir, 'tbd'); const tbdFiles = await fs.readdir(tbdDir).catch(() => []); // Auth types might be in a separate file or combined with existing types // For now, we'll check if the generated types are properly available const generatedFiles = await fs.readdir(generatedTypesDir); const authGenerated = generatedFiles.filter(f => f.includes('Auth') || f.includes('Login') || f.includes('Signup') || f.includes('Session') || f.includes('Forgot') || f.includes('Reset') ); expect(authGenerated.length).toBeGreaterThanOrEqual(6); }); it('should have auth types re-exported from main types file', async () => { // Check if there's an auth.ts file or if types are exported elsewhere const authTypesPath = path.join(websiteTypesDir, 'auth.ts'); const authTypesExists = await fs.access(authTypesPath).then(() => true).catch(() => false); if (authTypesExists) { const authTypesContent = await fs.readFile(authTypesPath, 'utf-8'); // Verify re-exports expect(authTypesContent).toContain('AuthSessionDTO'); expect(authTypesContent).toContain('AuthenticatedUserDTO'); expect(authTypesContent).toContain('SignupParamsDTO'); expect(authTypesContent).toContain('LoginParamsDTO'); expect(authTypesContent).toContain('ForgotPasswordDTO'); expect(authTypesContent).toContain('ResetPasswordDTO'); } }); }); describe('Auth API Client Contract', () => { it('should have AuthApiClient defined', async () => { const authApiClientPath = path.join(apiRoot, 'apps/website/lib/api/auth/AuthApiClient.ts'); const authApiClientExists = await fs.access(authApiClientPath).then(() => true).catch(() => false); expect(authApiClientExists).toBe(true); const authApiClientContent = await fs.readFile(authApiClientPath, 'utf-8'); // Verify class definition expect(authApiClientContent).toContain('export class AuthApiClient'); expect(authApiClientContent).toContain('extends BaseApiClient'); // Verify methods exist expect(authApiClientContent).toContain('signup'); expect(authApiClientContent).toContain('login'); expect(authApiClientContent).toContain('getSession'); expect(authApiClientContent).toContain('logout'); expect(authApiClientContent).toContain('forgotPassword'); expect(authApiClientContent).toContain('resetPassword'); // Verify method signatures expect(authApiClientContent).toContain('signup(params: SignupParamsDTO)'); expect(authApiClientContent).toContain('login(params: LoginParamsDTO)'); expect(authApiClientContent).toContain('getSession()'); expect(authApiClientContent).toContain('logout()'); expect(authApiClientContent).toContain('forgotPassword(params: ForgotPasswordDTO)'); expect(authApiClientContent).toContain('resetPassword(params: ResetPasswordDTO)'); }); it('should have proper request construction in signup method', async () => { const authApiClientPath = path.join(apiRoot, 'apps/website/lib/api/auth/AuthApiClient.ts'); const authApiClientContent = await fs.readFile(authApiClientPath, 'utf-8'); // Verify POST request with params expect(authApiClientContent).toContain("return this.post('/auth/signup', params)"); }); it('should have proper request construction in login method', async () => { const authApiClientPath = path.join(apiRoot, 'apps/website/lib/api/auth/AuthApiClient.ts'); const authApiClientContent = await fs.readFile(authApiClientPath, 'utf-8'); // Verify POST request with params expect(authApiClientContent).toContain("return this.post('/auth/login', params)"); }); it('should have proper request construction in getSession method', async () => { const authApiClientPath = path.join(apiRoot, 'apps/website/lib/api/auth/AuthApiClient.ts'); const authApiClientContent = await fs.readFile(authApiClientPath, 'utf-8'); // Verify GET request with allowUnauthenticated option expect(authApiClientContent).toContain("return this.request('GET', '/auth/session', undefined, {"); expect(authApiClientContent).toContain('allowUnauthenticated: true'); }); it('should have proper request construction in logout method', async () => { const authApiClientPath = path.join(apiRoot, 'apps/website/lib/api/auth/AuthApiClient.ts'); const authApiClientContent = await fs.readFile(authApiClientPath, 'utf-8'); // Verify POST request with empty body expect(authApiClientContent).toContain("return this.post('/auth/logout', {})"); }); it('should have proper request construction in forgotPassword method', async () => { const authApiClientPath = path.join(apiRoot, 'apps/website/lib/api/auth/AuthApiClient.ts'); const authApiClientContent = await fs.readFile(authApiClientPath, 'utf-8'); // Verify POST request with params expect(authApiClientContent).toContain("return this.post<{ message: string; magicLink?: string }>('/auth/forgot-password', params)"); }); it('should have proper request construction in resetPassword method', async () => { const authApiClientPath = path.join(apiRoot, 'apps/website/lib/api/auth/AuthApiClient.ts'); const authApiClientContent = await fs.readFile(authApiClientPath, 'utf-8'); // Verify POST request with params expect(authApiClientContent).toContain("return this.post<{ message: string }>('/auth/reset-password', params)"); }); }); describe('Request Correctness Tests', () => { it('should validate SignupParamsDTO required fields', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['SignupParamsDTO']; // Verify all required fields are present expect(schema.required).toContain('email'); expect(schema.required).toContain('password'); expect(schema.required).toContain('displayName'); // Verify no extra required fields expect(schema.required.length).toBe(3); }); it('should validate SignupParamsDTO optional fields', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['SignupParamsDTO']; // Verify optional fields are not required expect(schema.required).not.toContain('username'); expect(schema.required).not.toContain('iracingCustomerId'); expect(schema.required).not.toContain('primaryDriverId'); expect(schema.required).not.toContain('avatarUrl'); // Verify optional fields exist expect(schema.properties?.username).toBeDefined(); expect(schema.properties?.iracingCustomerId).toBeDefined(); expect(schema.properties?.primaryDriverId).toBeDefined(); expect(schema.properties?.avatarUrl).toBeDefined(); }); it('should validate LoginParamsDTO required fields', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['LoginParamsDTO']; // Verify all required fields are present expect(schema.required).toContain('email'); expect(schema.required).toContain('password'); // Verify no extra required fields expect(schema.required.length).toBe(2); }); it('should validate LoginParamsDTO optional fields', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['LoginParamsDTO']; // Verify optional fields are not required expect(schema.required).not.toContain('rememberMe'); // Verify optional fields exist expect(schema.properties?.rememberMe).toBeDefined(); expect(schema.properties?.rememberMe?.type).toBe('boolean'); expect(schema.properties?.rememberMe?.default).toBe(false); }); it('should validate ForgotPasswordDTO structure', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['ForgotPasswordDTO']; // Verify all required fields expect(schema.required).toContain('email'); // Verify field types expect(schema.properties?.email?.type).toBe('string'); }); it('should validate ResetPasswordDTO structure', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['ResetPasswordDTO']; // Verify all required fields expect(schema.required).toContain('token'); expect(schema.required).toContain('newPassword'); // Verify field types expect(schema.properties?.token?.type).toBe('string'); expect(schema.properties?.newPassword?.type).toBe('string'); }); it('should validate AuthSessionDTO structure', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['AuthSessionDTO']; // Verify all required fields expect(schema.required).toContain('token'); expect(schema.required).toContain('user'); // Verify field types expect(schema.properties?.token?.type).toBe('string'); expect(schema.properties?.user?.$ref).toBe('#/components/schemas/AuthenticatedUserDTO'); }); it('should validate AuthenticatedUserDTO structure', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const schema = spec.components.schemas['AuthenticatedUserDTO']; // Verify all required fields const requiredFields = ['userId', 'email', 'displayName']; for (const field of requiredFields) { expect(schema.required).toContain(field); } // Verify field types expect(schema.properties?.userId?.type).toBe('string'); expect(schema.properties?.email?.type).toBe('string'); expect(schema.properties?.displayName?.type).toBe('string'); expect(schema.properties?.role?.type).toBe('string'); // Verify optional fields are nullable expect(schema.properties?.avatarUrl?.nullable).toBe(true); }); }); describe('Response Handling Tests', () => { it('should handle successful signup response', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const authSessionSchema = spec.components.schemas['AuthSessionDTO']; // Verify response structure expect(authSessionSchema.properties?.token).toBeDefined(); expect(authSessionSchema.properties?.user).toBeDefined(); expect(authSessionSchema.properties?.token?.type).toBe('string'); expect(authSessionSchema.properties?.user?.$ref).toBe('#/components/schemas/AuthenticatedUserDTO'); expect(authSessionSchema.required).toContain('token'); expect(authSessionSchema.required).toContain('user'); }); it('should handle successful login response', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const authSessionSchema = spec.components.schemas['AuthSessionDTO']; // Verify response structure expect(authSessionSchema.properties?.token).toBeDefined(); expect(authSessionSchema.properties?.user).toBeDefined(); expect(authSessionSchema.properties?.token?.type).toBe('string'); expect(authSessionSchema.properties?.user?.$ref).toBe('#/components/schemas/AuthenticatedUserDTO'); expect(authSessionSchema.required).toContain('token'); expect(authSessionSchema.required).toContain('user'); }); it('should handle session response with all required fields', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const authSessionSchema = spec.components.schemas['AuthSessionDTO']; // Verify all required fields are present for (const field of ['token', 'user']) { expect(authSessionSchema.required).toContain(field); expect(authSessionSchema.properties?.[field]).toBeDefined(); } }); it('should handle optional fields in user response correctly', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const userSchema = spec.components.schemas['AuthenticatedUserDTO']; // Verify optional fields are nullable expect(userSchema.properties?.avatarUrl?.nullable).toBe(true); // Verify optional fields are not in required array expect(userSchema.required).not.toContain('avatarUrl'); expect(userSchema.required).not.toContain('primaryDriverId'); expect(userSchema.required).not.toContain('companyId'); expect(userSchema.required).not.toContain('role'); }); it('should handle forgot-password response correctly', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const forgotPasswordPath = spec.paths['/auth/forgot-password']?.post; expect(forgotPasswordPath).toBeDefined(); // Verify response structure const response200 = forgotPasswordPath.responses['200']; expect(response200).toBeDefined(); }); it('should handle reset-password response correctly', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const resetPasswordPath = spec.paths['/auth/reset-password']?.post; expect(resetPasswordPath).toBeDefined(); // Verify response structure const response200 = resetPasswordPath.responses['200']; expect(response200).toBeDefined(); }); }); describe('Error Handling Tests', () => { it('should document 400 Bad Request response for invalid signup input', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const signupPath = spec.paths['/auth/signup']?.post; // Check if 400 response is documented if (signupPath.responses['400']) { expect(signupPath.responses['400']).toBeDefined(); } }); it('should document 400 Bad Request response for invalid login input', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const loginPath = spec.paths['/auth/login']?.post; // Check if 400 response is documented if (loginPath.responses['400']) { expect(loginPath.responses['400']).toBeDefined(); } }); it('should document 401 Unauthorized response for login endpoint', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const loginPath = spec.paths['/auth/login']?.post; // Check if 401 response is documented if (loginPath.responses['401']) { expect(loginPath.responses['401']).toBeDefined(); } }); it('should document 400 Bad Request response for invalid forgot-password input', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const forgotPasswordPath = spec.paths['/auth/forgot-password']?.post; // Check if 400 response is documented if (forgotPasswordPath.responses['400']) { expect(forgotPasswordPath.responses['400']).toBeDefined(); } }); it('should document 400 Bad Request response for invalid reset-password input', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const resetPasswordPath = spec.paths['/auth/reset-password']?.post; // Check if 400 response is documented if (resetPasswordPath.responses['400']) { expect(resetPasswordPath.responses['400']).toBeDefined(); } }); it('should document 401 Unauthorized response for protected endpoints', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); // Session endpoint should require authentication const sessionPath = spec.paths['/auth/session']?.get; // Check if 401 responses are documented if (sessionPath.responses['401']) { expect(sessionPath.responses['401']).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 signupPath = spec.paths['/auth/signup']?.post; const loginPath = spec.paths['/auth/login']?.post; // Check if 500 response is documented for auth endpoints if (signupPath.responses['500']) { expect(signupPath.responses['500']).toBeDefined(); } if (loginPath.responses['500']) { expect(loginPath.responses['500']).toBeDefined(); } }); it('should have proper error handling in AuthApiClient', async () => { const authApiClientPath = path.join(apiRoot, 'apps/website/lib/api/auth/AuthApiClient.ts'); const authApiClientContent = await fs.readFile(authApiClientPath, 'utf-8'); // Verify BaseApiClient is extended (which provides error handling) expect(authApiClientContent).toContain('extends BaseApiClient'); // Verify methods use BaseApiClient methods (which handle errors) expect(authApiClientContent).toContain('this.post<'); expect(authApiClientContent).toContain('this.get<'); expect(authApiClientContent).toContain('this.request<'); }); }); describe('Semantic Guarantee Tests', () => { it('should maintain consistency between request and response schemas', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); // Verify signup request/response consistency const signupInputSchema = spec.components.schemas['SignupParamsDTO']; const signupOutputSchema = spec.components.schemas['AuthSessionDTO']; // Output should contain token and user expect(signupOutputSchema.properties?.token).toBeDefined(); expect(signupOutputSchema.properties?.user).toBeDefined(); // Verify login request/response consistency const loginInputSchema = spec.components.schemas['LoginParamsDTO']; const loginOutputSchema = spec.components.schemas['AuthSessionDTO']; // Output should contain token and user expect(loginOutputSchema.properties?.token).toBeDefined(); expect(loginOutputSchema.properties?.user).toBeDefined(); }); it('should validate semantic consistency in auth session', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const authSessionSchema = spec.components.schemas['AuthSessionDTO']; const userSchema = spec.components.schemas['AuthenticatedUserDTO']; // Verify session has token and user expect(authSessionSchema.properties?.token?.type).toBe('string'); expect(authSessionSchema.properties?.user?.$ref).toBe('#/components/schemas/AuthenticatedUserDTO'); // Verify user has required fields expect(userSchema.required).toContain('userId'); expect(userSchema.required).toContain('email'); expect(userSchema.required).toContain('displayName'); }); it('should validate idempotency for logout', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); // Check if logout endpoint exists const logoutPath = spec.paths['/auth/logout']?.post; if (logoutPath) { // Verify it accepts an empty body (idempotent operation) const requestBody = logoutPath.requestBody; if (requestBody && requestBody.content && requestBody.content['application/json']) { const schema = requestBody.content['application/json'].schema; // Empty body or minimal body expect(schema).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['AuthenticatedUserDTO']; // Verify email is a required field expect(userSchema.required).toContain('email'); expect(userSchema.properties?.email?.type).toBe('string'); // Check for signup endpoint const signupPath = spec.paths['/auth/signup']?.post; if (signupPath) { // Verify email is required in request body const requestBody = signupPath.requestBody; if (requestBody && requestBody.content && requestBody.content['application/json']) { const schema = requestBody.content['application/json'].schema; if (schema && schema.$ref) { // This would reference SignupParamsDTO which should have email as required expect(schema.$ref).toContain('SignupParamsDTO'); } } } }); it('should validate consistency between request and response types', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); // Verify all DTOs have consistent type definitions const dtos = [ 'AuthSessionDTO', 'AuthenticatedUserDTO', 'SignupParamsDTO', 'LoginParamsDTO', 'ForgotPasswordDTO', 'ResetPasswordDTO', ]; for (const dtoName of dtos) { const schema = spec.components.schemas[dtoName]; expect(schema).toBeDefined(); expect(schema.type).toBe('object'); // All should have properties defined expect(schema.properties).toBeDefined(); // All should have required fields (even if empty array) expect(schema.required).toBeDefined(); } }); it('should validate semantic consistency in auth session token', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); const authSessionSchema = spec.components.schemas['AuthSessionDTO']; // Verify token is a string (JWT format) expect(authSessionSchema.properties?.token?.type).toBe('string'); // Verify user is a reference to AuthenticatedUserDTO expect(authSessionSchema.properties?.user?.$ref).toBe('#/components/schemas/AuthenticatedUserDTO'); }); it('should validate pagination is not applicable for auth endpoints', async () => { const content = await fs.readFile(openapiPath, 'utf-8'); const spec: OpenAPISpec = JSON.parse(content); // Auth endpoints should not have pagination const authEndpoints = [ '/auth/signup', '/auth/login', '/auth/session', '/auth/logout', '/auth/forgot-password', '/auth/reset-password', ]; for (const endpoint of authEndpoints) { const path = spec.paths[endpoint]; if (path) { // Check if there are any query parameters for pagination const methods = Object.keys(path); for (const method of methods) { const operation = path[method]; if (operation.parameters) { const paramNames = operation.parameters.map((p: any) => p.name); // Auth endpoints should not have page/limit parameters expect(paramNames).not.toContain('page'); expect(paramNames).not.toContain('limit'); } } } } }); }); describe('Auth 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 generatedFiles = await fs.readdir(generatedTypesDir); const generatedDTOs = generatedFiles .filter(f => f.endsWith('.ts')) .map(f => f.replace('.ts', '')); // Check all auth DTOs exist in generated types const authDTOs = [ 'AuthSessionDTO', 'AuthenticatedUserDTO', 'SignupParamsDTO', 'LoginParamsDTO', 'ForgotPasswordDTO', 'ResetPasswordDTO', ]; for (const dtoName of authDTOs) { expect(spec.components.schemas[dtoName]).toBeDefined(); expect(generatedDTOs).toContain(dtoName); } }); it('should have AuthApiClient methods matching API endpoints', async () => { const authApiClientPath = path.join(apiRoot, 'apps/website/lib/api/auth/AuthApiClient.ts'); const authApiClientContent = await fs.readFile(authApiClientPath, 'utf-8'); // Verify signup method exists and uses correct endpoint expect(authApiClientContent).toContain('async signup'); expect(authApiClientContent).toContain("return this.post('/auth/signup', params)"); // Verify login method exists and uses correct endpoint expect(authApiClientContent).toContain('async login'); expect(authApiClientContent).toContain("return this.post('/auth/login', params)"); // Verify getSession method exists and uses correct endpoint expect(authApiClientContent).toContain('async getSession'); expect(authApiClientContent).toContain("return this.request('GET', '/auth/session', undefined, {"); // Verify logout method exists and uses correct endpoint expect(authApiClientContent).toContain('async logout'); expect(authApiClientContent).toContain("return this.post('/auth/logout', {})"); }); it('should have proper error handling in AuthApiClient', async () => { const authApiClientPath = path.join(apiRoot, 'apps/website/lib/api/auth/AuthApiClient.ts'); const authApiClientContent = await fs.readFile(authApiClientPath, 'utf-8'); // Verify BaseApiClient is extended (which provides error handling) expect(authApiClientContent).toContain('extends BaseApiClient'); // Verify methods use BaseApiClient methods (which handle errors) expect(authApiClientContent).toContain('this.post<'); expect(authApiClientContent).toContain('this.get<'); expect(authApiClientContent).toContain('this.request<'); }); it('should have consistent type imports in AuthApiClient', async () => { const authApiClientPath = path.join(apiRoot, 'apps/website/lib/api/auth/AuthApiClient.ts'); const authApiClientContent = await fs.readFile(authApiClientPath, 'utf-8'); // Verify all required types are imported expect(authApiClientContent).toContain('AuthSessionDTO'); expect(authApiClientContent).toContain('LoginParamsDTO'); expect(authApiClientContent).toContain('SignupParamsDTO'); expect(authApiClientContent).toContain('ForgotPasswordDTO'); expect(authApiClientContent).toContain('ResetPasswordDTO'); }); }); });