Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 0 additions & 3 deletions backend/src/api/controllers/sep12.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ type UploadedFiles = { [fieldname: string]: Array<{ path: string }> };
const ALLOWED_CONTENT_TYPES = (process.env.UPLOAD_ALLOWED_CONTENT_TYPES ?? 'image/jpeg,image/png,application/pdf').split(',');
const UPLOAD_URL_EXPIRY_SECONDS = parseInt(process.env.UPLOAD_URL_EXPIRY_SECONDS ?? '900', 10);
const KEY_PREFIX = process.env.STORAGE_KEY_PREFIX ?? 'kyc';

type UploadedFiles = { [fieldname: string]: Array<{ path: string }> };

const pack = (enc?: { encryptedData: string; iv: string } | null) =>
enc ? `${enc.iv}|${enc.encryptedData}` : null;

Expand Down
3 changes: 2 additions & 1 deletion backend/src/api/routes/queue-dashboard.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const queues = Object.values(QUEUE_NAMES).map(
(name) => new BullMQAdapter(new Queue(name, { connection: queueConnection }))
);

createBullBoard({ queues, serverAdapter });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
createBullBoard({ queues: queues as any, serverAdapter });

const router = Router();
router.use('/', serverAdapter.getRouter());
Expand Down
44 changes: 40 additions & 4 deletions backend/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import request from 'supertest';
import prisma from './lib/prisma';
import { redis } from './lib/redis';

jest.mock('./lib/prisma', () => ({
transaction: {
findMany: jest.fn(),
count: jest.fn()
__esModule: true,
default: {
transaction: {
findMany: jest.fn(),
count: jest.fn()
},
$queryRaw: jest.fn()
}
}));

Expand All @@ -20,10 +26,40 @@ const app = require('./index').default;


describe('Backend API', () => {
it('should return UP on health check', async () => {
beforeEach(() => {
jest.clearAllMocks();
(prisma.$queryRaw as jest.Mock).mockResolvedValue([{ '?column?': 1 }]);
if (typeof redis.ping === 'function') {
jest.spyOn(redis, 'ping').mockResolvedValue('PONG');
}
});

it('should return UP on health check when all services are healthy', async () => {
const res = await request(app).get('/health');
expect(res.statusCode).toEqual(200);
expect(res.body.status).toEqual('UP');
expect(res.body.services.database).toEqual('UP');
expect(res.body.services.redis).toEqual('UP');
});

it('should return DOWN on health check when database is down', async () => {
(prisma.$queryRaw as jest.Mock).mockRejectedValue(new Error('DB Connection Refused'));

const res = await request(app).get('/health');
expect(res.statusCode).toEqual(503);
expect(res.body.status).toEqual('DOWN');
expect(res.body.services.database).toEqual('DOWN');
expect(res.body.services.redis).toEqual('UP');
});

it('should return DOWN on health check when Redis is down', async () => {
jest.spyOn(redis, 'ping').mockRejectedValue(new Error('Redis Timeout'));

const res = await request(app).get('/health');
expect(res.statusCode).toEqual(503);
expect(res.body.status).toEqual('DOWN');
expect(res.body.services.database).toEqual('UP');
expect(res.body.services.redis).toEqual('DOWN');
});

it('should return 200 on root access', async () => {
Expand Down
78 changes: 74 additions & 4 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import { createEmailProvider, ConsoleSmsProvider, ConsolePushProvider } from './
import { NotificationType } from './services/notification.service';
import { validateKmsConfigOnStartup } from './lib/key-management.service';
import queueDashboardRouter from './api/routes/queue-dashboard.route';
import prisma from './lib/prisma';
import { redis } from './lib/redis';

// Initialize Notification Engine
notificationService.registerProvider(NotificationType.EMAIL, createEmailProvider());
Expand Down Expand Up @@ -75,11 +77,11 @@ app.get('/', (req: Request, res: Response) => {
* /health:
* get:
* summary: Health check
* description: Check if the API server is running
* description: Check if the API server and its backend dependencies (database, Redis) are running
* tags: [Health]
* responses:
* 200:
* description: Server is healthy
* description: Server and all dependencies are healthy
* content:
* application/json:
* schema:
Expand All @@ -91,9 +93,77 @@ app.get('/', (req: Request, res: Response) => {
* timestamp:
* type: string
* format: date-time
* services:
* type: object
* properties:
* database:
* type: string
* example: UP
* redis:
* type: string
* example: UP
* 503:
* description: One or more backend dependencies are down
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* example: DOWN
* timestamp:
* type: string
* format: date-time
* services:
* type: object
* properties:
* database:
* type: string
* example: DOWN
* redis:
* type: string
* example: UP
*/
app.get('/health', (req: Request, res: Response) => {
res.json({ status: 'UP', timestamp: new Date().toISOString() });
app.get('/health', async (req: Request, res: Response) => {
let dbStatus = 'UP';
let redisStatus = 'UP';
let isHealthy = true;

try {
await prisma.$queryRaw`SELECT 1`;
} catch (err) {
dbStatus = 'DOWN';
isHealthy = false;
logger.error('Health Check - Database connection failed:', err);
}

try {
const pong = await redis.ping();
if (pong !== 'PONG') {
redisStatus = 'DOWN';
isHealthy = false;
}
} catch (err) {
redisStatus = 'DOWN';
isHealthy = false;
logger.error('Health Check - Redis connection failed:', err);
}

const responsePayload = {
status: isHealthy ? 'UP' : 'DOWN',
timestamp: new Date().toISOString(),
services: {
database: dbStatus,
redis: redisStatus,
},
};

if (!isHealthy) {
return res.status(503).json(responsePayload);
}

return res.status(200).json(responsePayload);
});

// Swagger API Documentation
Expand Down
1 change: 1 addition & 0 deletions backend/src/lib/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const redis = isTest
set: createNoop<(key: string, value: string) => Promise<'OK'>>(Promise.resolve('OK')),
del: createNoop<(key: string) => Promise<number>>(Promise.resolve(1)),
publish: createNoop<(channel: string, message: string) => Promise<number>>(Promise.resolve(1)),
ping: createNoop<() => Promise<string>>(Promise.resolve('PONG')),
} as any)


Expand Down
33 changes: 33 additions & 0 deletions backend/src/services/storage-provider.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { MockStorageProvider, storageProvider } from './storage-provider.service';

describe('storage-provider.service', () => {
describe('MockStorageProvider', () => {
let mockProvider: MockStorageProvider;

beforeEach(() => {
mockProvider = new MockStorageProvider('test-mock-bucket');
});

it('should generate mock presigned URL', async () => {
const url = await mockProvider.generatePresignedPutUrl('test-key', 'image/png', 3600);
expect(url).toBe('https://test-mock-bucket.mock.storage/test-key?X-Mock-Signed=1');
});

it('should correctly check if object exists', async () => {
expect(await mockProvider.objectExists('test-key')).toBe(false);
mockProvider._markUploaded('test-key');
expect(await mockProvider.objectExists('test-key')).toBe(true);
});

it('should use default mock bucket name if not specified', () => {
const defaultProvider = new MockStorageProvider();
expect(defaultProvider).toBeDefined();
});
});

describe('default storageProvider instance', () => {
it('should be defined', () => {
expect(storageProvider).toBeDefined();
});
});
});
58 changes: 58 additions & 0 deletions backend/src/services/upload-store.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { uploadStore } from './upload-store.service';

describe('uploadStore', () => {
beforeEach(() => {
// Clear in-memory maps or stale records if any
const recordsMap = (uploadStore as any).records;
if (recordsMap) {
recordsMap.clear();
}
});

it('should create and retrieve an upload record', () => {
const expiresAt = new Date(Date.now() + 60000);
const record = uploadStore.create(
'GD3...123',
'id_front',
'kyc/GD3...123/id_front/1',
'image/png',
expiresAt
);

expect(record.uploadId).toBeDefined();
expect(record.status).toBe('PENDING');
expect(record.account).toBe('GD3...123');

const fetched = uploadStore.get(record.uploadId);
expect(fetched).toEqual(record);
});

it('should set the status of a record', () => {
const expiresAt = new Date(Date.now() + 60000);
const record = uploadStore.create(
'GD3...123',
'id_front',
'kyc/GD3...123/id_front/1',
'image/png',
expiresAt
);

uploadStore.setStatus(record.uploadId, 'COMPLETED');
expect(uploadStore.get(record.uploadId)?.status).toBe('COMPLETED');
});

it('should expire stale records', () => {
const pastDate = new Date(Date.now() - 10000);
const record = uploadStore.create(
'GD3...123',
'id_front',
'kyc/GD3...123/id_front/1',
'image/png',
pastDate
);

const expiredCount = uploadStore.expireStale();
expect(expiredCount).toBe(1);
expect(uploadStore.get(record.uploadId)?.status).toBe('EXPIRED');
});
});
Loading