Skip to content
Merged

Dev #62

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
6063342
feat: add comprehensive database tests for connection, error handling…
AdityaDRathore May 22, 2025
84fd8f8
chore: remove obsolete database connection and Prisma types tests
AdityaDRathore May 22, 2025
afc7cff
feat: add database error handling and performance monitoring utilities
AdityaDRathore May 22, 2025
54b9227
feat: Add unified development plan for Time-Booking Application
AdityaDRathore May 22, 2025
bfd3a44
Merge pull request #58 from AdityaDRathore/feature/db_test
AdityaDRathore May 22, 2025
18147f4
feat: Enhance environment variable validation for test mode and updat…
AdityaDRathore May 22, 2025
c6bfb27
chore: remove isolatedModules option from Jest configuration
AdityaDRathore May 22, 2025
ee76d33
chore: add isolatedModules option to TypeScript configuration
AdityaDRathore May 22, 2025
0058fc8
feat: add jest-mock-extended dependency for enhanced testing capabili…
AdityaDRathore May 22, 2025
943e235
fix: update fallback assignment to use nullish coalescing operator fo…
AdityaDRathore May 22, 2025
d543d54
feat: add prisma-mock for enhanced testing with Prisma client
AdityaDRathore May 22, 2025
f01efaf
refactor: replace PrismaClient with createMockPrismaClient in tests f…
AdityaDRathore May 22, 2025
01dbdd5
Merge pull request #60 from AdityaDRathore/feature/db_test
AdityaDRathore May 22, 2025
1188000
refactor: enhance logger configuration for better environment handlin…
AdityaDRathore May 27, 2025
c0bcd44
Merge pull request #61 from AdityaDRathore/feature/db_test
AdityaDRathore May 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions backend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@ module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/src'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.tsx?$': ['ts-jest', {
tsconfig: 'tsconfig.json',
}],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'],
globals: {
'ts-jest': {
tsconfig: 'tsconfig.json',
},
},
};
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.7.0",
"jest-mock-extended": "^4.0.0-beta1",
"prettier": "^3.2.5",
"prisma": "^6.8.2",
"ts-jest": "^29.1.4",
Expand Down
44 changes: 38 additions & 6 deletions backend/src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,22 @@ dotenv.config();
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
PORT: z.string().default('4000'),
DATABASE_URL: z.string(),
JWT_SECRET: z.string(),
DATABASE_URL:
process.env.NODE_ENV === 'test'
? z
.string()
.optional()
.default('postgresql://postgres:password@localhost:5432/test_db_schema_default')
: z.string(),
JWT_SECRET:
process.env.NODE_ENV === 'test'
? z.string().optional().default('test_jwt_secret_schema_default')
: z.string(),
JWT_EXPIRES_IN: z.string().default('15m'),
REFRESH_TOKEN_SECRET: z.string(),
REFRESH_TOKEN_SECRET:
process.env.NODE_ENV === 'test'
? z.string().optional().default('test_refresh_secret_schema_default')
: z.string(),
REFRESH_TOKEN_EXPIRES_IN: z.string().default('7d'),
REDIS_URL: z.string().optional(),
CORS_ORIGIN: z.string().default('*'),
Expand All @@ -24,8 +36,28 @@ const envSchema = z.object({
const env = envSchema.safeParse(process.env);

if (!env.success) {
console.error('❌ Invalid environment variables:', JSON.stringify(env.error.format(), null, 4));
process.exit(1);
if (process.env.NODE_ENV === 'test') {
console.warn('⚠️ Running with incomplete environment in test mode, using fallbacks.');
} else {
console.error('❌ Invalid environment variables:', JSON.stringify(env.error.format(), null, 4));
process.exit(1);
}
}

export const config = env.data;
// Export validated config or fallbacks for test environment
export const config = env.success
? env.data
: {
NODE_ENV: process.env.NODE_ENV ?? 'test',
PORT: process.env.PORT ?? '4000',
LOG_LEVEL: (process.env.LOG_LEVEL as z.infer<typeof envSchema.shape.LOG_LEVEL>) ?? 'error',
DATABASE_URL:
process.env.DATABASE_URL ??
'postgresql://postgres:password@localhost:5432/test_db_fallback',
JWT_SECRET: process.env.JWT_SECRET ?? 'test_jwt_secret_fallback',
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN ?? '15m',
REFRESH_TOKEN_SECRET: process.env.REFRESH_TOKEN_SECRET ?? 'test_refresh_secret_fallback',
REFRESH_TOKEN_EXPIRES_IN: process.env.REFRESH_TOKEN_EXPIRES_IN ?? '7d',
REDIS_URL: process.env.REDIS_URL ?? undefined, // Ensure REDIS_URL is in the fallback
CORS_ORIGIN: process.env.CORS_ORIGIN ?? '*',
};
114 changes: 114 additions & 0 deletions backend/src/tests/database/db-connection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Prisma } from '@prisma/client';
import logger from '../../utils/logger';
import { createMockPrismaClient } from '../prisma-mock';
import { mockDeep } from 'jest-mock-extended';

// Replace jest-fail-fast which can't be found
const fail = (message: string): never => {
throw new Error(message);
};

describe('Database Connection', () => {
let prisma: ReturnType<typeof createMockPrismaClient>;

beforeEach(() => {
prisma = createMockPrismaClient();
});

it('should connect to the database successfully', async () => {
// Mock the query response
prisma.$queryRaw.mockResolvedValue([{ result: 1 }]);

const result = await prisma.$queryRaw`SELECT 1 as result`;
expect(result).toBeDefined();
expect(result).toEqual([{ result: 1 }]);
});

it('should handle connection pool correctly', async () => {
// Mock multiple parallel queries
for (let i = 0; i < 5; i++) {
prisma.$queryRaw.mockResolvedValueOnce([{ value: Math.random() }]);
}

const promises = Array(5)
.fill(0)
.map(() => prisma.$queryRaw`SELECT random() as value`);

const results = await Promise.all(promises);
expect(results.length).toBe(5);
expect(results.every(r => Array.isArray(r) && r.length === 1)).toBe(true);
});

it('should handle transaction rollback correctly', async () => {
// Mock successful transaction
const mockTx = mockDeep<Prisma.TransactionClient>();

// First mock a successful query
mockTx.$executeRaw.mockResolvedValueOnce(1);

// Then mock a failing query
const duplicateKeyError = new Error('Duplicate key value');
mockTx.$executeRaw.mockRejectedValueOnce(duplicateKeyError);

// Fix the callback type to match Prisma's $transaction overloads
// Define transaction related types
type TransactionCallback<T> = (tx: Prisma.TransactionClient) => Promise<T>;
type TransactionQueries<T> = Array<Promise<T>>;

prisma.$transaction.mockImplementation(
<T>(
callbackOrQueries: TransactionCallback<T> | TransactionQueries<T>,
): Promise<T | Array<T>> => {
if (typeof callbackOrQueries === 'function') {
return callbackOrQueries(mockTx).catch(error => {
throw error instanceof Error ? error : new Error(String(error));
});
}
// Handle the array of queries case
return Promise.all(callbackOrQueries);
},
);

try {
await prisma.$transaction(async tx => {
// This should succeed
await tx.$executeRaw`CREATE TEMPORARY TABLE test_table (id SERIAL PRIMARY KEY)`;
// This will deliberately fail
await tx.$executeRaw`INSERT INTO test_table (id) VALUES (1), (1)`;
});
fail('Transaction should have failed');
} catch (error: unknown) {
expect(error).toBeDefined();
expect(error).toEqual(duplicateKeyError);
}
});

it('should handle connection errors gracefully', async () => {
// Instead of creating a real bad client, we'll mock a connection error
// Fix constructor arguments to match expected signature
const mockError = new Prisma.PrismaClientInitializationError(
"Can't reach database server",
'4.5.0',
);

// No need to set clientVersion property as it's now provided in the constructor

prisma.$queryRaw.mockRejectedValueOnce(mockError);

try {
await prisma.$queryRaw`SELECT 1`;
fail('Query should have failed with database connection error');
} catch (error: unknown) {
// Add proper error handling logic to satisfy SonarQube
expect(error).toBeDefined();
expect(error).toBe(mockError);

if (error instanceof Prisma.PrismaClientInitializationError) {
expect(error.message).toContain("Can't reach database server");
logger.error(`Database connection error: ${error.message}`);
} else {
fail('Error should be a PrismaClientInitializationError');
}
}
});
});
58 changes: 58 additions & 0 deletions backend/src/tests/database/db-error-handling.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Prisma } from '@prisma/client';
import { withErrorHandling, handlePrismaError } from '../../utils/db-errors';
import { AppError } from '../../utils/errors';
import { createMockPrismaClient } from '../prisma-mock';

describe('Database Error Handling', () => {
let prisma: ReturnType<typeof createMockPrismaClient>;

beforeEach(() => {
prisma = createMockPrismaClient();
});

it('should handle record not found errors', async () => {
// Mock a record not found error
const notFoundError = new Prisma.PrismaClientKnownRequestError('Record not found', {
code: 'P2001',
clientVersion: '4.5.0',
});

prisma.user.findUniqueOrThrow.mockRejectedValueOnce(notFoundError);

try {
await withErrorHandling(async () => {
return await prisma.user.findUniqueOrThrow({
where: { id: 'non-existent-id' },
});
});
fail('Should have thrown an error');
} catch (error) {
expect(error).toBeInstanceOf(AppError);
expect((error as AppError).statusCode).toBe(404);
}
});

it('should handle unique constraint errors', async () => {
// Create a proper PrismaClientKnownRequestError for unique constraint
const uniqueError = new Prisma.PrismaClientKnownRequestError(
'Unique constraint failed on the fields: (`user_email`)',
{
code: 'P2002',
clientVersion: '4.5.0',
meta: { target: ['user_email'] },
},
);

const error = handlePrismaError(uniqueError);
expect(error.statusCode).toBe(409); // Conflict
expect(error.message).toContain('already exists');
});

it('should pass through successful operations', async () => {
const result = await withErrorHandling(async () => {
return 'success';
});

expect(result).toBe('success');
});
});
76 changes: 76 additions & 0 deletions backend/src/tests/database/db-performance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { createPerformanceTest, DatabasePerformance } from '../../utils/db-performance';
import fs from 'fs';
import { createMockPrismaClient } from '../prisma-mock';

// Mock logger to avoid file system operations
jest.mock('../../utils/logger', () => ({
info: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
}));

// More complete fs mock including stat function needed by Winston
jest.mock('fs', () => ({
existsSync: jest.fn().mockReturnValue(true),
mkdirSync: jest.fn(),
writeFileSync: jest.fn(),
stat: jest.fn().mockImplementation((path, callback) => {
callback(null, { isDirectory: () => true });
}),
createWriteStream: jest.fn().mockReturnValue({
on: jest.fn(),
write: jest.fn(),
end: jest.fn(),
once: jest.fn(),
}),
}));

describe('Database Performance Baseline', () => {
let prisma: ReturnType<typeof createMockPrismaClient>;
let performanceMonitor: DatabasePerformance;

beforeEach(() => {
prisma = createMockPrismaClient();
performanceMonitor = new DatabasePerformance(prisma, 'test');
});

it('should establish performance baseline for key queries', async () => {
// Mock necessary Prisma methods
prisma.user.findMany.mockResolvedValue([]);
prisma.lab.findFirst.mockResolvedValue(null);

// Mock fs methods
(fs.existsSync as jest.Mock).mockReturnValue(false);

const report = await createPerformanceTest(prisma);

// Basic assertions to ensure the test ran
expect(report.queries.length).toBeGreaterThan(0);
expect(report.summary.totalQueries).toBeGreaterThan(0);
});

// Add tests for specific high-impact queries
it('should measure lab booking query performance', async () => {
prisma.lab.findMany.mockResolvedValue([]);

const result = await performanceMonitor.measureQuery('Find available labs', async () => {
return prisma.lab.findMany({
where: { status: 'ACTIVE' },
include: {
timeSlots: {
where: {
bookings: {
some: {
booking_status: 'CONFIRMED',
},
},
},
},
},
});
});

expect(result).toBeDefined();
});
});
28 changes: 0 additions & 28 deletions backend/src/tests/db-connection.test.ts

This file was deleted.

11 changes: 11 additions & 0 deletions backend/src/tests/prisma-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { PrismaClient } from '@prisma/client';
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended';

// Create a mock instance of the Prisma client
export const prisma = mockDeep<PrismaClient>();

// Factory for creating a fresh mock for each test
export const createMockPrismaClient = (): DeepMockProxy<PrismaClient> => {
mockReset(prisma);
return prisma;
};
Loading