231 أسطر
9.5 KiB
TypeScript
231 أسطر
9.5 KiB
TypeScript
import { ConfigService } from '@nestjs/config';
|
|
import { NestFactory } from '@nestjs/core';
|
|
import { SwaggerModule } from '@nestjs/swagger';
|
|
import * as express from 'express';
|
|
import { existsSync, mkdirSync } from 'fs';
|
|
import { networkInterfaces } from 'os';
|
|
import { AppLoggerService } from './infrastructure/logging/app-logger.service';
|
|
import { RedisService } from './infrastructure/redis/redis.service';
|
|
import { configureHttpServer } from './infrastructure/reliability/http-server.config';
|
|
import { RedisIoAdapter } from './infrastructure/socket/redis-io.adapter';
|
|
import { bootstrap, getLocalIpv4Addresses, getStaticMediaHeaders, isPrivateIpv4Host } from './main';
|
|
|
|
jest.mock('./app.module', () => ({ AppModule: class AppModule {} }));
|
|
jest.mock('@nestjs/core', () => ({ NestFactory: { create: jest.fn() } }));
|
|
jest.mock('@nestjs/swagger', () => {
|
|
class DocumentBuilder {
|
|
setTitle() { return this; }
|
|
setDescription() { return this; }
|
|
setVersion() { return this; }
|
|
addBearerAuth() { return this; }
|
|
build() { return { openapi: '3.0.0' }; }
|
|
}
|
|
return {
|
|
DocumentBuilder,
|
|
SwaggerModule: { createDocument: jest.fn(() => ({ paths: {} })), setup: jest.fn() },
|
|
};
|
|
});
|
|
jest.mock('compression', () => jest.fn(() => 'compression-middleware'));
|
|
jest.mock('express', () => ({
|
|
json: jest.fn(() => 'json-middleware'),
|
|
urlencoded: jest.fn(() => 'urlencoded-middleware'),
|
|
static: jest.fn(() => 'static-middleware'),
|
|
}));
|
|
jest.mock('fs', () => ({
|
|
...jest.requireActual<typeof import('fs')>('fs'),
|
|
existsSync: jest.fn(),
|
|
mkdirSync: jest.fn(),
|
|
}));
|
|
jest.mock('os', () => ({
|
|
...jest.requireActual<typeof import('os')>('os'),
|
|
networkInterfaces: jest.fn(),
|
|
}));
|
|
jest.mock('./infrastructure/reliability/http-server.config', () => ({ configureHttpServer: jest.fn() }));
|
|
jest.mock('./infrastructure/socket/redis-io.adapter', () => ({
|
|
RedisIoAdapter: jest.fn().mockImplementation(() => ({
|
|
connectToRedis: jest.fn().mockResolvedValue(undefined),
|
|
})),
|
|
}));
|
|
|
|
describe('main bootstrap', () => {
|
|
const build = (overrides: Record<string, unknown> = {}) => {
|
|
const values: Record<string, unknown> = {
|
|
'cors.origins': ['https://app.example.com'],
|
|
nodeEnv: 'production',
|
|
'security.bodyLimit': '2mb',
|
|
'performance.compressionEnabled': true,
|
|
'performance.compressionThresholdBytes': 2048,
|
|
'storage.provider': 'local',
|
|
'storage.basePath': '/uploads/',
|
|
publicBaseUrl: 'http://localhost:4000/',
|
|
'email.enabled': true,
|
|
'email.smtpHost': 'smtp.example.com',
|
|
'email.smtpUser': 'smtp-user',
|
|
'email.fromEmail': 'noreply@example.com',
|
|
globalPrefix: 'api/v1',
|
|
responseEnvelopeEnabled: true,
|
|
'redis.enabled': true,
|
|
'redis.socketAdapterEnabled': true,
|
|
'swagger.enabled': true,
|
|
'swagger.path': 'docs',
|
|
'swagger.title': 'Oudelaa API',
|
|
'swagger.description': 'API',
|
|
'swagger.version': '1.0.0',
|
|
port: 4000,
|
|
host: '0.0.0.0',
|
|
...overrides,
|
|
};
|
|
const config = {
|
|
get: jest.fn((key: string, defaultValue?: unknown) =>
|
|
Object.prototype.hasOwnProperty.call(values, key) ? values[key] : defaultValue,
|
|
),
|
|
};
|
|
const logger = {
|
|
log: jest.fn(),
|
|
warn: jest.fn(),
|
|
logHttp: jest.fn(),
|
|
};
|
|
const redis = {};
|
|
const httpServer = { keepAliveTimeout: 0, headersTimeout: 0, requestTimeout: 0 };
|
|
const app = {
|
|
get: jest.fn((token: unknown) => {
|
|
if (token === ConfigService) return config;
|
|
if (token === AppLoggerService) return logger;
|
|
if (token === RedisService) return redis;
|
|
return undefined;
|
|
}),
|
|
useLogger: jest.fn(),
|
|
enableShutdownHooks: jest.fn(),
|
|
use: jest.fn(),
|
|
enableCors: jest.fn(),
|
|
setGlobalPrefix: jest.fn(),
|
|
useGlobalPipes: jest.fn(),
|
|
useGlobalInterceptors: jest.fn(),
|
|
useWebSocketAdapter: jest.fn(),
|
|
listen: jest.fn().mockResolvedValue(undefined),
|
|
getHttpServer: jest.fn(() => httpServer),
|
|
};
|
|
(NestFactory.create as jest.Mock).mockResolvedValue(app);
|
|
return { app, config, logger, redis, httpServer };
|
|
};
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
(existsSync as jest.Mock).mockReturnValue(false);
|
|
(networkInterfaces as jest.Mock).mockReturnValue({
|
|
Ethernet: [
|
|
{ family: 'IPv4', internal: false, address: '192.168.1.20' },
|
|
{ family: 'IPv6', internal: false, address: '::1' },
|
|
null,
|
|
],
|
|
Loopback: [{ family: 'IPv4', internal: true, address: '127.0.0.1' }],
|
|
});
|
|
});
|
|
|
|
it('classifies local addresses and static-media response headers', () => {
|
|
expect(getLocalIpv4Addresses()).toEqual(['192.168.1.20']);
|
|
expect(isPrivateIpv4Host('10.0.0.1')).toBe(true);
|
|
expect(isPrivateIpv4Host('192.168.1.1')).toBe(true);
|
|
expect(isPrivateIpv4Host('172.16.0.1')).toBe(true);
|
|
expect(isPrivateIpv4Host('172.31.0.1')).toBe(true);
|
|
expect(isPrivateIpv4Host('172.15.0.1')).toBe(false);
|
|
expect(getStaticMediaHeaders('.mp3', '/audio/song.mp3')).toEqual(
|
|
expect.objectContaining({ contentType: 'audio/mpeg', acceptRanges: true }),
|
|
);
|
|
expect(getStaticMediaHeaders('.gif')).toEqual(expect.objectContaining({ contentType: 'image/gif' }));
|
|
expect(getStaticMediaHeaders('.m3u8')).toEqual(
|
|
expect.objectContaining({ contentType: 'application/vnd.apple.mpegurl', acceptRanges: true }),
|
|
);
|
|
expect(getStaticMediaHeaders('.m4s')).toEqual(expect.objectContaining({ contentType: 'video/iso.segment' }));
|
|
expect(getStaticMediaHeaders('.ts')).toEqual(expect.objectContaining({ contentType: 'video/mp2t' }));
|
|
expect(getStaticMediaHeaders('.unknown')).toEqual({});
|
|
});
|
|
|
|
it('boots the production stack with security, local media, Redis sockets, and Swagger', async () => {
|
|
const { app, logger, httpServer } = build();
|
|
|
|
await bootstrap();
|
|
|
|
expect(NestFactory.create).toHaveBeenCalledWith(expect.any(Function), {
|
|
bufferLogs: true,
|
|
bodyParser: false,
|
|
});
|
|
expect(mkdirSync).toHaveBeenCalledWith(expect.stringMatching(/uploads$/), { recursive: true });
|
|
expect(app.enableCors).toHaveBeenCalledWith(expect.objectContaining({ credentials: true }));
|
|
expect(app.setGlobalPrefix).toHaveBeenCalledWith('api/v1');
|
|
expect(app.useGlobalPipes).toHaveBeenCalled();
|
|
expect(app.useGlobalInterceptors).toHaveBeenCalled();
|
|
expect(app.listen).toHaveBeenCalledWith(4000, '0.0.0.0');
|
|
expect(configureHttpServer).toHaveBeenCalledWith(httpServer, expect.anything());
|
|
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Mobile devices on the LAN'), 'Bootstrap');
|
|
expect(RedisIoAdapter).toHaveBeenCalled();
|
|
const adapter = (RedisIoAdapter as unknown as jest.Mock).mock.results[0].value;
|
|
expect(adapter.connectToRedis).toHaveBeenCalled();
|
|
expect(app.useWebSocketAdapter).toHaveBeenCalledWith(adapter);
|
|
expect(SwaggerModule.createDocument).toHaveBeenCalled();
|
|
expect(SwaggerModule.setup).toHaveBeenCalledWith('docs', app, expect.anything());
|
|
|
|
const middleware = app.use.mock.calls
|
|
.filter((call) => typeof call[0] === 'function')
|
|
.map((call) => call[0]);
|
|
expect(middleware).toHaveLength(2);
|
|
const setHeader = jest.fn();
|
|
const next = jest.fn();
|
|
middleware[0]({}, { setHeader } as any, next);
|
|
expect(setHeader).toHaveBeenCalledWith('Strict-Transport-Security', expect.any(String));
|
|
|
|
let finish: (() => void) | undefined;
|
|
const request = { headers: { 'x-request-id': 'invalid id' }, method: 'GET', originalUrl: '/health' } as any;
|
|
const response = {
|
|
statusCode: 200,
|
|
setHeader,
|
|
on: jest.fn((_event: string, listener: () => void) => { finish = listener; }),
|
|
} as any;
|
|
middleware[1](request, response, next);
|
|
finish?.();
|
|
expect(request.headers['x-request-id']).toEqual(expect.any(String));
|
|
expect(logger.logHttp).toHaveBeenCalledWith(expect.objectContaining({ method: 'GET', statusCode: 200 }));
|
|
|
|
const suppliedIdRequest = {
|
|
headers: { 'x-request-id': 'mobile-client:request-1' },
|
|
method: 'GET',
|
|
originalUrl: '/health',
|
|
} as any;
|
|
middleware[1](suppliedIdRequest, response, next);
|
|
expect(suppliedIdRequest.headers['x-request-id']).toBe('mobile-client:request-1');
|
|
|
|
const staticOptions = (express.static as unknown as jest.Mock).mock.calls[0][1];
|
|
const staticHeader = jest.fn();
|
|
staticOptions.setHeaders({ setHeader: staticHeader }, '/uploads/stream.m3u8');
|
|
expect(staticHeader).toHaveBeenCalledWith('Content-Type', 'application/vnd.apple.mpegurl');
|
|
expect(staticHeader).toHaveBeenCalledWith('Accept-Ranges', 'bytes');
|
|
});
|
|
|
|
it('boots without optional integrations and warns about a mismatched private public URL', async () => {
|
|
const { app, logger } = build({
|
|
'cors.origins': [],
|
|
nodeEnv: 'development',
|
|
'performance.compressionEnabled': false,
|
|
'storage.provider': 's3',
|
|
publicBaseUrl: 'http://192.168.1.99:4000',
|
|
responseEnvelopeEnabled: false,
|
|
'redis.enabled': false,
|
|
'redis.socketAdapterEnabled': false,
|
|
'swagger.enabled': false,
|
|
'email.enabled': false,
|
|
'email.smtpHost': '',
|
|
'email.smtpUser': '',
|
|
'email.fromEmail': '',
|
|
});
|
|
|
|
await bootstrap();
|
|
|
|
expect(mkdirSync).not.toHaveBeenCalled();
|
|
expect(app.enableCors).toHaveBeenCalledWith(expect.objectContaining({ origin: true, credentials: true }));
|
|
expect(app.useGlobalInterceptors).not.toHaveBeenCalled();
|
|
expect(app.useWebSocketAdapter).not.toHaveBeenCalled();
|
|
expect(SwaggerModule.setup).not.toHaveBeenCalled();
|
|
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('not assigned to the current machine'), 'Bootstrap');
|
|
});
|
|
});
|