health
This commit is contained in:
@@ -1,14 +1,23 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import type { Response } from 'express';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
import { PrismaService } from './database/prisma.service';
|
||||||
|
|
||||||
describe('AppController', () => {
|
describe('AppController', () => {
|
||||||
let appController: AppController;
|
let appController: AppController;
|
||||||
|
const prismaMock = {
|
||||||
|
$queryRaw: jest.fn().mockResolvedValue([1]),
|
||||||
|
} as unknown as PrismaService;
|
||||||
|
|
||||||
|
const prismaDownMock = {
|
||||||
|
$queryRaw: jest.fn().mockRejectedValue(new Error('db down')),
|
||||||
|
} as unknown as PrismaService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const app: TestingModule = await Test.createTestingModule({
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService, { provide: PrismaService, useValue: prismaMock }],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
appController = app.get<AppController>(AppController);
|
appController = app.get<AppController>(AppController);
|
||||||
@@ -19,4 +28,36 @@ describe('AppController', () => {
|
|||||||
expect(appController.getHello()).toBe('Hello World!');
|
expect(appController.getHello()).toBe('Hello World!');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('health', () => {
|
||||||
|
it('should return health payload', async () => {
|
||||||
|
const res = { status: jest.fn() } as unknown as Response;
|
||||||
|
const response = await appController.getHealth(res);
|
||||||
|
|
||||||
|
expect(res.status).not.toHaveBeenCalled();
|
||||||
|
expect(['OK', 'DOWN']).toContain(response.status);
|
||||||
|
expect(response.version).toBeDefined();
|
||||||
|
expect(response.uptimeSecs).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(['OK', 'DOWN']).toContain(response.db);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark down and set 503 when db fails', async () => {
|
||||||
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [
|
||||||
|
AppService,
|
||||||
|
{ provide: PrismaService, useValue: prismaDownMock },
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
const controller = app.get<AppController>(AppController);
|
||||||
|
const res = { status: jest.fn() } as unknown as Response;
|
||||||
|
|
||||||
|
const response = await controller.getHealth(res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(503 as const);
|
||||||
|
expect(response.status).toBe('DOWN');
|
||||||
|
expect(response.db).toBe('DOWN');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get, Header, HttpStatus, Res } from '@nestjs/common';
|
||||||
import { AppService } from './app.service';
|
import type { Response } from 'express';
|
||||||
|
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { AppService, HealthResponse } from './app.service';
|
||||||
|
import { HealthResponseDto } from './app.health-response.dto';
|
||||||
|
|
||||||
|
@ApiTags('system')
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private readonly appService: AppService) {}
|
constructor(private readonly appService: AppService) {}
|
||||||
@@ -9,4 +13,20 @@ export class AppController {
|
|||||||
getHello(): string {
|
getHello(): string {
|
||||||
return this.appService.getHello();
|
return this.appService.getHello();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/health')
|
||||||
|
@Header('Cache-Control', 'no-store')
|
||||||
|
@ApiOperation({ summary: 'Health check' })
|
||||||
|
@ApiOkResponse({ description: 'Service health', type: HealthResponseDto })
|
||||||
|
async getHealth(
|
||||||
|
@Res({ passthrough: true }) res: Response,
|
||||||
|
): Promise<HealthResponse> {
|
||||||
|
const health = await this.appService.getHealth();
|
||||||
|
|
||||||
|
if (health.status === 'DOWN') {
|
||||||
|
res.status(HttpStatus.SERVICE_UNAVAILABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return health;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/app.health-response.dto.ts
Normal file
20
src/app.health-response.dto.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import type { DatabaseHealth } from './app.service';
|
||||||
|
|
||||||
|
export class HealthResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
enum: ['OK', 'DOWN'],
|
||||||
|
example: 'OK',
|
||||||
|
description: 'Overall service status',
|
||||||
|
})
|
||||||
|
status!: DatabaseHealth;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Server build version', example: '0.0.1' })
|
||||||
|
version!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Process uptime in seconds', example: 123 })
|
||||||
|
uptimeSecs!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ enum: ['OK', 'DOWN'], example: 'OK' })
|
||||||
|
db!: DatabaseHealth;
|
||||||
|
}
|
||||||
@@ -1,8 +1,43 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from './database/prisma.service';
|
||||||
|
|
||||||
|
const appVersion =
|
||||||
|
process.env.APP_VERSION ?? process.env.npm_package_version ?? 'unknown';
|
||||||
|
|
||||||
|
export type DatabaseHealth = 'OK' | 'DOWN';
|
||||||
|
|
||||||
|
export interface HealthResponse {
|
||||||
|
status: DatabaseHealth;
|
||||||
|
version: string;
|
||||||
|
uptimeSecs: number;
|
||||||
|
db: DatabaseHealth;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppService {
|
export class AppService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
getHello(): string {
|
getHello(): string {
|
||||||
return 'Hello World!';
|
return 'Hello World!';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getHealth(): Promise<HealthResponse> {
|
||||||
|
const uptimeSecs = Math.floor(process.uptime());
|
||||||
|
let db: DatabaseHealth = 'OK';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.prisma.$queryRaw`SELECT 1`;
|
||||||
|
} catch {
|
||||||
|
db = 'DOWN';
|
||||||
|
}
|
||||||
|
|
||||||
|
const status: DatabaseHealth = db === 'OK' ? 'OK' : 'DOWN';
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
version: appVersion,
|
||||||
|
uptimeSecs,
|
||||||
|
db,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,15 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { URLSearchParams } from 'url';
|
||||||
|
import axios from 'axios';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { UsersService } from '../users/users.service';
|
import { UsersService } from '../users/users.service';
|
||||||
import type { AuthenticatedUser } from './decorators/current-user.decorator';
|
import type { AuthenticatedUser } from './decorators/current-user.decorator';
|
||||||
import { User } from '../users/users.entity';
|
import { User } from '../users/users.entity';
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { URLSearchParams } from 'url';
|
|
||||||
|
|
||||||
describe('AuthService', () => {
|
describe('AuthService', () => {
|
||||||
let service: AuthService;
|
let service: AuthService;
|
||||||
|
|
||||||
const mockCreateFromToken = jest.fn();
|
|
||||||
const mockFindByKeycloakSub = jest.fn();
|
|
||||||
const mockFindOrCreate = jest.fn();
|
|
||||||
|
|
||||||
const mockUsersService = {
|
|
||||||
createFromToken: mockCreateFromToken,
|
|
||||||
findByKeycloakSub: mockFindByKeycloakSub,
|
|
||||||
findOrCreate: mockFindOrCreate,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockAuthUser: AuthenticatedUser = {
|
|
||||||
keycloakSub: 'f:realm:user123',
|
|
||||||
email: 'test@example.com',
|
|
||||||
name: 'Test User',
|
|
||||||
username: 'testuser',
|
|
||||||
picture: 'https://example.com/avatar.jpg',
|
|
||||||
roles: ['user', 'premium'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockUser: User = {
|
const mockUser: User = {
|
||||||
id: 'uuid-123',
|
id: 'uuid-123',
|
||||||
keycloakSub: 'f:realm:user123',
|
keycloakSub: 'f:realm:user123',
|
||||||
@@ -43,6 +24,23 @@ describe('AuthService', () => {
|
|||||||
activeDollId: null,
|
activeDollId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockUsersService: jest.Mocked<
|
||||||
|
Pick<UsersService, 'createFromToken' | 'findByKeycloakSub' | 'findOrCreate'>
|
||||||
|
> = {
|
||||||
|
createFromToken: jest.fn().mockResolvedValue(mockUser),
|
||||||
|
findByKeycloakSub: jest.fn().mockResolvedValue(null),
|
||||||
|
findOrCreate: jest.fn().mockResolvedValue(mockUser),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockAuthUser: AuthenticatedUser = {
|
||||||
|
keycloakSub: 'f:realm:user123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
name: 'Test User',
|
||||||
|
username: 'testuser',
|
||||||
|
picture: 'https://example.com/avatar.jpg',
|
||||||
|
roles: ['user', 'premium'],
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -82,9 +80,10 @@ describe('AuthService', () => {
|
|||||||
it('should skip when config missing', async () => {
|
it('should skip when config missing', async () => {
|
||||||
const missingConfigService = new ConfigService({});
|
const missingConfigService = new ConfigService({});
|
||||||
const localService = new AuthService(
|
const localService = new AuthService(
|
||||||
mockUsersService as any,
|
mockUsersService as unknown as UsersService,
|
||||||
missingConfigService,
|
missingConfigService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const warnSpy = jest.spyOn<any, any>(localService['logger'], 'warn');
|
const warnSpy = jest.spyOn<any, any>(localService['logger'], 'warn');
|
||||||
const result = await localService.revokeToken('rt');
|
const result = await localService.revokeToken('rt');
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
@@ -127,12 +126,12 @@ describe('AuthService', () => {
|
|||||||
|
|
||||||
describe('syncUserFromToken', () => {
|
describe('syncUserFromToken', () => {
|
||||||
it('should create a new user if user does not exist', async () => {
|
it('should create a new user if user does not exist', async () => {
|
||||||
mockCreateFromToken.mockReturnValue(mockUser);
|
mockUsersService.createFromToken.mockResolvedValue(mockUser);
|
||||||
|
|
||||||
const result = await service.syncUserFromToken(mockAuthUser);
|
const result = await service.syncUserFromToken(mockAuthUser);
|
||||||
|
|
||||||
expect(result).toEqual(mockUser);
|
expect(result).toEqual(mockUser);
|
||||||
expect(mockCreateFromToken).toHaveBeenCalledWith({
|
expect(mockUsersService.createFromToken).toHaveBeenCalledWith({
|
||||||
keycloakSub: 'f:realm:user123',
|
keycloakSub: 'f:realm:user123',
|
||||||
email: 'test@example.com',
|
email: 'test@example.com',
|
||||||
name: 'Test User',
|
name: 'Test User',
|
||||||
@@ -144,12 +143,12 @@ describe('AuthService', () => {
|
|||||||
|
|
||||||
it('should handle existing user via upsert', async () => {
|
it('should handle existing user via upsert', async () => {
|
||||||
const updatedUser = { ...mockUser, lastLoginAt: new Date('2024-02-01') };
|
const updatedUser = { ...mockUser, lastLoginAt: new Date('2024-02-01') };
|
||||||
mockCreateFromToken.mockReturnValue(updatedUser);
|
mockUsersService.createFromToken.mockResolvedValue(updatedUser);
|
||||||
|
|
||||||
const result = await service.syncUserFromToken(mockAuthUser);
|
const result = await service.syncUserFromToken(mockAuthUser);
|
||||||
|
|
||||||
expect(result).toEqual(updatedUser);
|
expect(result).toEqual(updatedUser);
|
||||||
expect(mockCreateFromToken).toHaveBeenCalledWith({
|
expect(mockUsersService.createFromToken).toHaveBeenCalledWith({
|
||||||
keycloakSub: 'f:realm:user123',
|
keycloakSub: 'f:realm:user123',
|
||||||
email: 'test@example.com',
|
email: 'test@example.com',
|
||||||
name: 'Test User',
|
name: 'Test User',
|
||||||
@@ -165,7 +164,7 @@ describe('AuthService', () => {
|
|||||||
name: 'No Email User',
|
name: 'No Email User',
|
||||||
};
|
};
|
||||||
|
|
||||||
mockCreateFromToken.mockReturnValue({
|
mockUsersService.createFromToken.mockResolvedValue({
|
||||||
...mockUser,
|
...mockUser,
|
||||||
email: '',
|
email: '',
|
||||||
name: 'No Email User',
|
name: 'No Email User',
|
||||||
@@ -173,7 +172,7 @@ describe('AuthService', () => {
|
|||||||
|
|
||||||
await service.syncUserFromToken(authUserNoEmail);
|
await service.syncUserFromToken(authUserNoEmail);
|
||||||
|
|
||||||
expect(mockCreateFromToken).toHaveBeenCalledWith(
|
expect(mockUsersService.createFromToken).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
email: '',
|
email: '',
|
||||||
name: 'No Email User',
|
name: 'No Email User',
|
||||||
@@ -187,14 +186,14 @@ describe('AuthService', () => {
|
|||||||
username: 'fallbackuser',
|
username: 'fallbackuser',
|
||||||
};
|
};
|
||||||
|
|
||||||
mockCreateFromToken.mockReturnValue({
|
mockUsersService.createFromToken.mockResolvedValue({
|
||||||
...mockUser,
|
...mockUser,
|
||||||
name: 'fallbackuser',
|
name: 'fallbackuser',
|
||||||
});
|
});
|
||||||
|
|
||||||
await service.syncUserFromToken(authUserNoName);
|
await service.syncUserFromToken(authUserNoName);
|
||||||
|
|
||||||
expect(mockCreateFromToken).toHaveBeenCalledWith(
|
expect(mockUsersService.createFromToken).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
name: 'fallbackuser',
|
name: 'fallbackuser',
|
||||||
}),
|
}),
|
||||||
@@ -206,14 +205,14 @@ describe('AuthService', () => {
|
|||||||
keycloakSub: 'f:realm:minimal',
|
keycloakSub: 'f:realm:minimal',
|
||||||
};
|
};
|
||||||
|
|
||||||
mockCreateFromToken.mockReturnValue({
|
mockUsersService.createFromToken.mockResolvedValue({
|
||||||
...mockUser,
|
...mockUser,
|
||||||
name: 'Unknown User',
|
name: 'Unknown User',
|
||||||
});
|
});
|
||||||
|
|
||||||
await service.syncUserFromToken(authUserMinimal);
|
await service.syncUserFromToken(authUserMinimal);
|
||||||
|
|
||||||
expect(mockCreateFromToken).toHaveBeenCalledWith(
|
expect(mockUsersService.createFromToken).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
name: 'Unknown User',
|
name: 'Unknown User',
|
||||||
}),
|
}),
|
||||||
@@ -227,7 +226,7 @@ describe('AuthService', () => {
|
|||||||
name: 'Empty Sub User',
|
name: 'Empty Sub User',
|
||||||
};
|
};
|
||||||
|
|
||||||
mockCreateFromToken.mockReturnValue({
|
mockUsersService.createFromToken.mockResolvedValue({
|
||||||
...mockUser,
|
...mockUser,
|
||||||
keycloakSub: '',
|
keycloakSub: '',
|
||||||
email: 'empty@example.com',
|
email: 'empty@example.com',
|
||||||
@@ -236,7 +235,7 @@ describe('AuthService', () => {
|
|||||||
|
|
||||||
await service.syncUserFromToken(authUserEmptySub);
|
await service.syncUserFromToken(authUserEmptySub);
|
||||||
|
|
||||||
expect(mockCreateFromToken).toHaveBeenCalledWith(
|
expect(mockUsersService.createFromToken).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
keycloakSub: '',
|
keycloakSub: '',
|
||||||
email: 'empty@example.com',
|
email: 'empty@example.com',
|
||||||
@@ -252,7 +251,7 @@ describe('AuthService', () => {
|
|||||||
name: 'Malformed User',
|
name: 'Malformed User',
|
||||||
};
|
};
|
||||||
|
|
||||||
mockCreateFromToken.mockReturnValue({
|
mockUsersService.createFromToken.mockResolvedValue({
|
||||||
...mockUser,
|
...mockUser,
|
||||||
keycloakSub: 'invalid-format',
|
keycloakSub: 'invalid-format',
|
||||||
email: 'malformed@example.com',
|
email: 'malformed@example.com',
|
||||||
@@ -261,24 +260,25 @@ describe('AuthService', () => {
|
|||||||
|
|
||||||
const result = await service.syncUserFromToken(authUserMalformed);
|
const result = await service.syncUserFromToken(authUserMalformed);
|
||||||
|
|
||||||
expect(mockCreateFromToken).toHaveBeenCalledWith(
|
expect(mockUsersService.createFromToken).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
keycloakSub: 'invalid-format',
|
keycloakSub: 'invalid-format',
|
||||||
email: 'malformed@example.com',
|
email: 'malformed@example.com',
|
||||||
name: 'Malformed User',
|
name: 'Malformed User',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
expect(result.keycloakSub).toBe('invalid-format');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ensureUserExists', () => {
|
describe('ensureUserExists', () => {
|
||||||
it('should call findOrCreate with correct params', async () => {
|
it('should call findOrCreate with correct params', async () => {
|
||||||
mockFindOrCreate.mockResolvedValue(mockUser);
|
mockUsersService.findOrCreate.mockResolvedValue(mockUser);
|
||||||
|
|
||||||
const result = await service.ensureUserExists(mockAuthUser);
|
const result = await service.ensureUserExists(mockAuthUser);
|
||||||
|
|
||||||
expect(result).toEqual(mockUser);
|
expect(result).toEqual(mockUser);
|
||||||
expect(mockFindOrCreate).toHaveBeenCalledWith({
|
expect(mockUsersService.findOrCreate).toHaveBeenCalledWith({
|
||||||
keycloakSub: 'f:realm:user123',
|
keycloakSub: 'f:realm:user123',
|
||||||
email: 'test@example.com',
|
email: 'test@example.com',
|
||||||
name: 'Test User',
|
name: 'Test User',
|
||||||
@@ -288,45 +288,23 @@ describe('AuthService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle user with no email', async () => {
|
it('should handle missing username gracefully', async () => {
|
||||||
const authUserNoEmail: AuthenticatedUser = {
|
mockUsersService.findOrCreate.mockResolvedValue(mockUser);
|
||||||
keycloakSub: 'f:realm:user456',
|
|
||||||
name: 'No Email User',
|
|
||||||
};
|
|
||||||
|
|
||||||
mockFindOrCreate.mockResolvedValue({
|
const result = await service.ensureUserExists({
|
||||||
...mockUser,
|
...mockAuthUser,
|
||||||
email: '',
|
username: undefined,
|
||||||
name: 'No Email User',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await service.ensureUserExists(authUserNoEmail);
|
expect(result).toEqual(mockUser);
|
||||||
|
expect(mockUsersService.findOrCreate).toHaveBeenCalledWith({
|
||||||
expect(mockFindOrCreate).toHaveBeenCalledWith(
|
keycloakSub: 'f:realm:user123',
|
||||||
expect.objectContaining({
|
email: 'test@example.com',
|
||||||
email: '',
|
name: 'Test User',
|
||||||
name: 'No Email User',
|
username: undefined,
|
||||||
}),
|
picture: 'https://example.com/avatar.jpg',
|
||||||
);
|
roles: ['user', 'premium'],
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use "Unknown User" when creating user with no name or username', async () => {
|
|
||||||
const authUserMinimal: AuthenticatedUser = {
|
|
||||||
keycloakSub: 'f:realm:minimal',
|
|
||||||
};
|
|
||||||
|
|
||||||
mockFindOrCreate.mockResolvedValue({
|
|
||||||
...mockUser,
|
|
||||||
name: 'Unknown User',
|
|
||||||
});
|
|
||||||
|
|
||||||
await service.ensureUserExists(authUserMinimal);
|
|
||||||
|
|
||||||
expect(mockFindOrCreate).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
name: 'Unknown User',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { App } from 'supertest/types';
|
import { App } from 'supertest/types';
|
||||||
|
import { PrismaService } from '../src/database/prisma.service';
|
||||||
import { AppModule } from './../src/app.module';
|
import { AppModule } from './../src/app.module';
|
||||||
|
|
||||||
|
const prismaMock = {
|
||||||
|
$queryRaw: jest.fn().mockResolvedValue([1]),
|
||||||
|
};
|
||||||
|
|
||||||
describe('AppController (e2e)', () => {
|
describe('AppController (e2e)', () => {
|
||||||
let app: INestApplication<App>;
|
let app: INestApplication<App>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
imports: [AppModule],
|
imports: [AppModule],
|
||||||
}).compile();
|
})
|
||||||
|
.overrideProvider(PrismaService)
|
||||||
|
.useValue(prismaMock)
|
||||||
|
.compile();
|
||||||
|
|
||||||
app = moduleFixture.createNestApplication();
|
app = moduleFixture.createNestApplication();
|
||||||
await app.init();
|
await app.init();
|
||||||
@@ -22,4 +30,50 @@ describe('AppController (e2e)', () => {
|
|||||||
.expect(200)
|
.expect(200)
|
||||||
.expect('Hello World!');
|
.expect('Hello World!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('/health (GET) returns ok with db up', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/health')
|
||||||
|
.expect('Cache-Control', 'no-store')
|
||||||
|
.expect(200)
|
||||||
|
.expect(
|
||||||
|
({
|
||||||
|
body,
|
||||||
|
}: {
|
||||||
|
body: {
|
||||||
|
status: string;
|
||||||
|
version: string;
|
||||||
|
uptimeSecs: number;
|
||||||
|
db: string;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
expect(body.status).toBe('ok');
|
||||||
|
expect(body.db).toBe('ok');
|
||||||
|
expect(body.version).toBeDefined();
|
||||||
|
expect(body.uptimeSecs).toBeGreaterThanOrEqual(0);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/health (GET) returns 503 with db down', async () => {
|
||||||
|
prismaMock.$queryRaw.mockRejectedValueOnce(new Error('db down'));
|
||||||
|
|
||||||
|
await request(app.getHttpServer())
|
||||||
|
.get('/health')
|
||||||
|
.expect('Cache-Control', 'no-store')
|
||||||
|
.expect(503)
|
||||||
|
.expect(
|
||||||
|
({
|
||||||
|
body,
|
||||||
|
}: {
|
||||||
|
body: {
|
||||||
|
status: string;
|
||||||
|
db: string;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
expect(body.status).toBe('DOWN');
|
||||||
|
expect(body.db).toBe('DOWN');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user