Files
gridpilot.gg/tests/contracts/auth-contract.test.ts
Marc Mintel 12027793b1
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m47s
Contract Testing / contract-snapshot (pull_request) Has been skipped
contract tests
2026-01-22 17:31:54 +01:00

1113 lines
45 KiB
TypeScript

/**
* 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<string, OpenAPISchema>;
required?: string[];
enum?: string[];
nullable?: boolean;
description?: string;
default?: unknown;
}
interface OpenAPISpec {
openapi: string;
info: {
title: string;
description: string;
version: string;
};
paths: Record<string, any>;
components: {
schemas: Record<string, OpenAPISchema>;
};
}
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<AuthSessionDTO>('/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<AuthSessionDTO>('/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<AuthSessionDTO | null>('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<void>('/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<AuthSessionDTO>('/auth/signup', params)");
// Verify login method exists and uses correct endpoint
expect(authApiClientContent).toContain('async login');
expect(authApiClientContent).toContain("return this.post<AuthSessionDTO>('/auth/login', params)");
// Verify getSession method exists and uses correct endpoint
expect(authApiClientContent).toContain('async getSession');
expect(authApiClientContent).toContain("return this.request<AuthSessionDTO | null>('GET', '/auth/session', undefined, {");
// Verify logout method exists and uses correct endpoint
expect(authApiClientContent).toContain('async logout');
expect(authApiClientContent).toContain("return this.post<void>('/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');
});
});
});