test(backend): remove duplicate and useless tests (#2820)

This commit is contained in:
Hampus
2026-09-17 15:32:25 +02:00
committed by GitHub
parent deac653a9e
commit 56e04e7b53
102 changed files with 10 additions and 15831 deletions
@@ -13,7 +13,7 @@ import {requestId} from '@fluxer/hono/src/middleware/RequestId';
import type {LogFunction, RequestLoggerOptions} from '@fluxer/hono/src/middleware/RequestLogger';
import {requestLogger} from '@fluxer/hono/src/middleware/RequestLogger';
import {fluxerVersionHeader} from '@fluxer/hono/src/middleware/VersionHeader';
import type {Context, Env, Hono, MiddlewareHandler} from 'hono';
import type {Env, Hono, MiddlewareHandler} from 'hono';
interface MiddlewareStackOptions {
requestId?: RequestIdOptions;
@@ -38,7 +38,7 @@ interface ApplyMiddlewareStackOptions extends MiddlewareStackOptions {
skipErrorHandler?: boolean;
}
export function createStandardMiddlewareStack(options: MiddlewareStackOptions = {}): Array<MiddlewareHandler> {
function createStandardMiddlewareStack(options: MiddlewareStackOptions = {}): Array<MiddlewareHandler> {
const stack: Array<MiddlewareHandler> = [fluxerVersionHeader()];
if (options.requestId) {
stack.push(requestId(options.requestId));
@@ -90,36 +90,3 @@ export function applyMiddlewareStack<E extends Env = Env>(
app.onError(errorHandler);
}
}
export function createDefaultLogger(options: {serviceName: string; skip?: Array<string>}): LogFunction {
return (data) => {
if (options.skip?.includes(data.path)) {
return;
}
console.log(
JSON.stringify({
service: options.serviceName,
method: data.method,
path: data.path,
status: data.status,
durationMs: data.durationMs,
timestamp: new Date().toISOString(),
}),
);
};
}
export function createDefaultErrorLogger(options: {serviceName: string}): (error: Error, context: Context) => void {
return (error: Error, context: Context) => {
console.error(
JSON.stringify({
service: options.serviceName,
error: error.message,
stack: error.stack,
path: context.req.path,
method: context.req.method,
timestamp: new Date().toISOString(),
}),
);
};
}
+1 -1
View File
@@ -12,7 +12,7 @@ export interface RequestIdOptions {
setResponseHeader?: boolean;
}
export const REQUEST_ID_KEY = 'requestId';
const REQUEST_ID_KEY = 'requestId';
export function requestId(options: RequestIdOptions = {}): MiddlewareHandler {
const {headerName = Headers.X_REQUEST_ID, generator = randomUUID, setResponseHeader = true} = options;
@@ -1,220 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {cacheHeaders} from '@fluxer/hono/src/middleware/CacheHeaders';
import {Hono} from 'hono';
import {describe, expect, test} from 'vitest';
describe('CacheHeaders Middleware', () => {
describe('static content caching', () => {
test('sets long-lived cache for CSS files', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/styles.css', () => {
return new Response('body { color: red; }', {
headers: {'Content-Type': 'text/css'},
});
});
const response = await app.request('/styles.css');
expect(response.headers.get('Cache-Control')).toBe('public, max-age=31536000');
});
test('sets long-lived cache for JavaScript files', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/app.js', () => {
return new Response('console.log("hello");', {
headers: {'Content-Type': 'application/javascript'},
});
});
const response = await app.request('/app.js');
expect(response.headers.get('Cache-Control')).toBe('public, max-age=31536000');
});
test('sets long-lived cache for images', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/logo.png', (c) => {
c.header('Content-Type', 'image/png');
return c.body(new Uint8Array([1, 2, 3]));
});
const response = await app.request('/logo.png');
expect(response.headers.get('Cache-Control')).toBe('public, max-age=31536000');
});
test('sets long-lived cache for fonts', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/font.woff2', (c) => {
c.header('Content-Type', 'font/woff2');
return c.body(new Uint8Array([1, 2, 3]));
});
const response = await app.request('/font.woff2');
expect(response.headers.get('Cache-Control')).toBe('public, max-age=31536000');
});
test('sets long-lived cache for application/font-woff2', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/font.woff2', (c) => {
c.header('Content-Type', 'application/font-woff2');
return c.body(new Uint8Array([1, 2, 3]));
});
const response = await app.request('/font.woff2');
expect(response.headers.get('Cache-Control')).toBe('public, max-age=31536000');
});
test('sets long-lived cache for video files', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/video.mp4', (c) => {
c.header('Content-Type', 'video/mp4');
return c.body(new Uint8Array([1, 2, 3]));
});
const response = await app.request('/video.mp4');
expect(response.headers.get('Cache-Control')).toBe('public, max-age=31536000');
});
test('sets long-lived cache for audio files', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/audio.mp3', (c) => {
c.header('Content-Type', 'audio/mpeg');
return c.body(new Uint8Array([1, 2, 3]));
});
const response = await app.request('/audio.mp3');
expect(response.headers.get('Cache-Control')).toBe('public, max-age=31536000');
});
});
describe('default caching', () => {
test('sets no-cache for JSON responses', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/api/data', (c) => c.json({ok: true}));
const response = await app.request('/api/data');
expect(response.headers.get('Cache-Control')).toBe('no-cache');
});
test('sets no-cache for HTML responses', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/', (c) => {
c.header('Content-Type', 'text/html');
return c.html('<html><body>Hello</body></html>');
});
const response = await app.request('/');
expect(response.headers.get('Cache-Control')).toBe('no-cache');
});
test('sets no-cache for text/plain responses', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/readme.txt', (c) => c.text('Hello World'));
const response = await app.request('/readme.txt');
expect(response.headers.get('Cache-Control')).toBe('no-cache');
});
test('sets no-cache when Content-Type is not set', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/test', (c) => c.body('data'));
const response = await app.request('/test');
expect(response.headers.get('Cache-Control')).toBe('no-cache');
});
});
describe('existing Cache-Control header', () => {
test('does not override existing Cache-Control header', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/test', (c) => {
c.header('Cache-Control', 'private, max-age=3600');
return c.json({ok: true});
});
const response = await app.request('/test');
expect(response.headers.get('Cache-Control')).toBe('private, max-age=3600');
});
test('respects existing no-store header', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/sensitive', (c) => {
c.header('Cache-Control', 'no-store');
return c.json({secret: 'data'});
});
const response = await app.request('/sensitive');
expect(response.headers.get('Cache-Control')).toBe('no-store');
});
});
describe('custom options', () => {
test('uses custom staticCacheControl', async () => {
const app = new Hono();
app.use('*', cacheHeaders({staticCacheControl: 'public, max-age=86400'}));
app.get('/image.png', () => {
return new Response(new Uint8Array([1, 2, 3]), {
headers: {'Content-Type': 'image/png'},
});
});
const response = await app.request('/image.png');
expect(response.headers.get('Cache-Control')).toBe('public, max-age=86400');
});
test('uses custom defaultCacheControl', async () => {
const app = new Hono();
app.use('*', cacheHeaders({defaultCacheControl: 'no-store'}));
app.get('/api/data', (c) => c.json({ok: true}));
const response = await app.request('/api/data');
expect(response.headers.get('Cache-Control')).toBe('no-store');
});
test('uses both custom options together', async () => {
const app = new Hono();
app.use(
'*',
cacheHeaders({
staticCacheControl: 'public, max-age=7200',
defaultCacheControl: 'private, no-cache',
}),
);
app.get('/style.css', () => {
return new Response('body {}', {
headers: {'Content-Type': 'text/css'},
});
});
app.get('/api/data', (c) => c.json({ok: true}));
const cssResponse = await app.request('/style.css');
expect(cssResponse.headers.get('Cache-Control')).toBe('public, max-age=7200');
const jsonResponse = await app.request('/api/data');
expect(jsonResponse.headers.get('Cache-Control')).toBe('private, no-cache');
});
});
describe('content type variations', () => {
test('handles image/jpeg', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/photo.jpg', (c) => {
c.header('Content-Type', 'image/jpeg');
return c.body(new Uint8Array([1, 2, 3]));
});
const response = await app.request('/photo.jpg');
expect(response.headers.get('Cache-Control')).toBe('public, max-age=31536000');
});
test('handles image/gif', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/animation.gif', (c) => {
c.header('Content-Type', 'image/gif');
return c.body(new Uint8Array([1, 2, 3]));
});
const response = await app.request('/animation.gif');
expect(response.headers.get('Cache-Control')).toBe('public, max-age=31536000');
});
test('handles image/svg+xml', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/icon.svg', () => {
return new Response('<svg></svg>', {
headers: {'Content-Type': 'image/svg+xml'},
});
});
const response = await app.request('/icon.svg');
expect(response.headers.get('Cache-Control')).toBe('public, max-age=31536000');
});
test('handles video/webm', async () => {
const app = new Hono();
app.use('*', cacheHeaders());
app.get('/video.webm', (c) => {
c.header('Content-Type', 'video/webm');
return c.body(new Uint8Array([1, 2, 3]));
});
const response = await app.request('/video.webm');
expect(response.headers.get('Cache-Control')).toBe('public, max-age=31536000');
});
});
});
@@ -1,195 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createErrorHandler} from '@fluxer/hono/src/middleware/ErrorHandler';
import {Hono} from 'hono';
import {HTTPException} from 'hono/http-exception';
import {describe, expect, test, vi} from 'vitest';
interface ErrorResponse {
code: string;
message: string;
stack?: string;
}
describe('ErrorHandler Middleware', () => {
describe('generic errors', () => {
test('handles generic Error with 500 status', async () => {
const app = new Hono();
app.onError(createErrorHandler());
app.get('/test', () => {
throw new Error('Something went wrong');
});
const response = await app.request('/test');
expect(response.status).toBe(500);
const body = (await response.json()) as ErrorResponse;
expect(body.code).toBe('INTERNAL_SERVER_ERROR');
expect(body.message).toBe('Something went wrong. Please try again later.');
});
test('includes stack trace when includeStack is true', async () => {
const app = new Hono();
app.onError(createErrorHandler({includeStack: true}));
app.get('/test', () => {
throw new Error('Something went wrong');
});
const response = await app.request('/test');
const body = (await response.json()) as ErrorResponse;
expect(body.message).toBe('Something went wrong');
expect(body.stack).toBeTruthy();
});
test('excludes stack trace by default', async () => {
const app = new Hono();
app.onError(createErrorHandler());
app.get('/test', () => {
throw new Error('Something went wrong');
});
const response = await app.request('/test');
const body = (await response.json()) as ErrorResponse;
expect(body.stack).toBeUndefined();
});
});
describe('HTTPException handling', () => {
test('handles HTTPException with correct status', async () => {
const app = new Hono();
app.onError(createErrorHandler());
app.get('/test', () => {
throw new HTTPException(404, {message: 'Resource not found'});
});
const response = await app.request('/test');
expect(response.status).toBe(404);
const body = (await response.json()) as ErrorResponse;
expect(body.code).toBe('NOT_FOUND');
expect(body.message).toBe('Resource not found');
});
test('handles HTTPException with 403 Forbidden', async () => {
const app = new Hono();
app.onError(createErrorHandler());
app.get('/test', () => {
throw new HTTPException(403, {message: 'Access denied'});
});
const response = await app.request('/test');
expect(response.status).toBe(403);
const body = (await response.json()) as ErrorResponse;
expect(body.code).toBe('FORBIDDEN');
expect(body.message).toBe('Access denied');
});
test('handles HTTPException without message', async () => {
const app = new Hono();
app.onError(createErrorHandler());
app.get('/test', () => {
throw new HTTPException(400);
});
const response = await app.request('/test');
expect(response.status).toBe(400);
const body = (await response.json()) as ErrorResponse;
expect(body.message).toBe('An error occurred');
});
});
describe('logger option', () => {
test('calls logger with error and context', async () => {
const logger = vi.fn();
const app = new Hono();
app.onError(createErrorHandler({logger}));
app.get('/test', () => {
throw new Error('Test error');
});
await app.request('/test');
expect(logger).toHaveBeenCalledTimes(1);
expect(logger).toHaveBeenCalledWith(expect.any(Error), expect.objectContaining({req: expect.anything()}));
});
test('does not call logger when not provided', async () => {
const app = new Hono();
app.onError(createErrorHandler());
app.get('/test', () => {
throw new Error('Test error');
});
const response = await app.request('/test');
expect(response.status).toBe(500);
});
});
describe('captureException option', () => {
test('calls captureException with error and context info', async () => {
const captureException = vi.fn();
const app = new Hono();
app.onError(createErrorHandler({captureException}));
app.get('/test', () => {
throw new Error('Test error');
});
await app.request('/test');
expect(captureException).toHaveBeenCalledTimes(1);
expect(captureException).toHaveBeenCalledWith(
expect.any(Error),
expect.objectContaining({
path: '/test',
method: 'GET',
}),
);
});
test('calls both logger and captureException when both provided', async () => {
const logger = vi.fn();
const captureException = vi.fn();
const app = new Hono();
app.onError(createErrorHandler({logger, captureException}));
app.get('/test', () => {
throw new Error('Test error');
});
await app.request('/test');
expect(logger).toHaveBeenCalledTimes(1);
expect(captureException).toHaveBeenCalledTimes(1);
});
});
describe('async errors', () => {
test('handles async errors', async () => {
const app = new Hono();
app.onError(createErrorHandler());
app.get('/test', async () => {
await Promise.resolve();
throw new Error('Async error');
});
const response = await app.request('/test');
expect(response.status).toBe(500);
});
test('handles rejected promises', async () => {
const app = new Hono();
app.onError(createErrorHandler());
app.get('/test', async () => {
return Promise.reject(new Error('Rejected promise'));
});
const response = await app.request('/test');
expect(response.status).toBe(500);
});
});
describe('multiple routes', () => {
test('handles errors from different routes', async () => {
const app = new Hono();
app.onError(createErrorHandler());
app.get('/route1', () => {
throw new HTTPException(404, {message: 'Route 1 not found'});
});
app.get('/route2', () => {
throw new Error('Route 2 error');
});
const response1 = await app.request('/route1');
expect(response1.status).toBe(404);
const response2 = await app.request('/route2');
expect(response2.status).toBe(500);
});
});
describe('error recovery', () => {
test('does not affect subsequent successful requests', async () => {
const app = new Hono();
app.onError(createErrorHandler());
app.get('/error', () => {
throw new Error('Error route');
});
app.get('/success', (c) => c.json({ok: true}));
const errorResponse = await app.request('/error');
expect(errorResponse.status).toBe(500);
const successResponse = await app.request('/success');
expect(successResponse.status).toBe(200);
const body = (await successResponse.json()) as {
ok: boolean;
};
expect(body.ok).toBe(true);
});
});
});
@@ -1,346 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {Headers} from '@fluxer/constants/src/Headers';
import {
applyMiddlewareStack,
createDefaultErrorLogger,
createDefaultLogger,
createStandardMiddlewareStack,
} from '@fluxer/hono/src/middleware/MiddlewareStack';
import type {RateLimitResult, RateLimitService} from '@fluxer/hono/src/middleware/RateLimit';
import {REQUEST_ID_KEY} from '@fluxer/hono/src/middleware/RequestId';
import type {Context} from 'hono';
import {Hono} from 'hono';
import {describe, expect, test, vi} from 'vitest';
function createMockRateLimitService(result: Partial<RateLimitResult> = {}): RateLimitService {
const defaultResult: RateLimitResult = {
allowed: true,
limit: 100,
remaining: 99,
resetTime: new Date(Date.now() + 60000),
...result,
};
return {
checkLimit: vi.fn().mockResolvedValue(defaultResult),
};
}
describe('createStandardMiddlewareStack', () => {
test('includes cacheHeaders by default with no options', () => {
const stack = createStandardMiddlewareStack();
expect(stack).toHaveLength(2);
});
test('includes requestId middleware when configured', () => {
const stack = createStandardMiddlewareStack({requestId: {}});
expect(stack).toHaveLength(3);
});
test('includes cors middleware when enabled', () => {
const stack = createStandardMiddlewareStack({cors: {enabled: true}});
expect(stack).toHaveLength(3);
});
test('excludes cors middleware when disabled', () => {
const stack = createStandardMiddlewareStack({cors: {enabled: false}});
expect(stack).toHaveLength(2);
});
test('excludes cacheHeaders when set to false', () => {
const stack = createStandardMiddlewareStack({cacheHeaders: false});
expect(stack).toHaveLength(1);
});
test('includes logger middleware when log function provided', () => {
const stack = createStandardMiddlewareStack({logger: {log: vi.fn()}});
expect(stack).toHaveLength(3);
});
test('excludes logger middleware when no log function', () => {
const stack = createStandardMiddlewareStack({logger: {}});
expect(stack).toHaveLength(2);
});
test('includes rateLimit middleware when enabled with service', () => {
const stack = createStandardMiddlewareStack({
rateLimit: {enabled: true, service: createMockRateLimitService()},
});
expect(stack).toHaveLength(3);
});
test('excludes rateLimit middleware when no service provided', () => {
const stack = createStandardMiddlewareStack({rateLimit: {enabled: true}});
expect(stack).toHaveLength(2);
});
test('includes custom middleware', () => {
const customMiddleware = vi.fn();
const stack = createStandardMiddlewareStack({
customMiddleware: [customMiddleware, customMiddleware],
});
expect(stack).toHaveLength(4);
});
test('combines all middleware in correct order', () => {
const stack = createStandardMiddlewareStack({
requestId: {},
cors: {enabled: true},
logger: {log: vi.fn()},
rateLimit: {enabled: true, service: createMockRateLimitService()},
customMiddleware: [vi.fn()],
});
expect(stack).toHaveLength(7);
});
});
describe('applyMiddlewareStack', () => {
test('applies version header middleware', async () => {
const app = new Hono();
applyMiddlewareStack(app, {});
app.get('/test', (c) => c.json({ok: true}));
const response = await app.request('/test');
expect(response.headers.get(Headers.X_FLUXER_VERSION)).toBe('dev');
});
test('applies requestId middleware', async () => {
const app = new Hono<{
Variables: {
[REQUEST_ID_KEY]: string;
};
}>();
applyMiddlewareStack(app, {requestId: {}});
app.get('/test', (c) => c.json({id: c.get(REQUEST_ID_KEY)}));
const response = await app.request('/test');
expect(response.headers.get(Headers.X_REQUEST_ID)).toBeTruthy();
});
test('applies cors middleware', async () => {
const app = new Hono();
applyMiddlewareStack(app, {cors: {enabled: true, origins: '*'}});
app.get('/test', (c) => c.json({ok: true}));
const response = await app.request('/test', {
headers: {origin: 'https://example.com'},
});
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*');
});
test('applies error handler by default', async () => {
const app = new Hono();
applyMiddlewareStack(app, {});
app.get('/test', () => {
throw new Error('Test error');
});
const response = await app.request('/test');
expect(response.status).toBe(500);
const body = (await response.json()) as {
code: string;
};
expect(body.code).toBe('INTERNAL_SERVER_ERROR');
});
test('skips requestId when skipRequestId is true', async () => {
const app = new Hono<{
Variables: {
[REQUEST_ID_KEY]: string;
};
}>();
applyMiddlewareStack(app, {
requestId: {},
skipRequestId: true,
});
app.get('/test', (c) => c.json({id: c.get(REQUEST_ID_KEY)}));
const response = await app.request('/test');
expect(response.headers.get(Headers.X_REQUEST_ID)).toBeNull();
});
test('skips cors when skipCors is true', async () => {
const app = new Hono();
applyMiddlewareStack(app, {
cors: {enabled: true, origins: '*'},
skipCors: true,
});
app.get('/test', (c) => c.json({ok: true}));
const response = await app.request('/test', {
headers: {origin: 'https://example.com'},
});
expect(response.headers.get('Access-Control-Allow-Origin')).toBeNull();
});
test('skips logger when skipLogger is true', async () => {
const log = vi.fn();
const app = new Hono();
applyMiddlewareStack(app, {
logger: {log},
skipLogger: true,
});
app.get('/test', (c) => c.json({ok: true}));
await app.request('/test');
expect(log).not.toHaveBeenCalled();
});
test('skips rateLimit when skipRateLimit is true', async () => {
const service = createMockRateLimitService();
const app = new Hono();
applyMiddlewareStack(app, {
rateLimit: {enabled: true, service},
skipRateLimit: true,
});
app.get('/test', (c) => c.json({ok: true}));
await app.request('/test');
expect(service.checkLimit).not.toHaveBeenCalled();
});
test('applies cache headers by default', async () => {
const app = new Hono();
applyMiddlewareStack(app, {});
app.get('/test', (c) => c.json({ok: true}));
const response = await app.request('/test');
expect(response.headers.get('Cache-Control')).toBe('no-cache');
});
test('skips cache headers when skipCacheHeaders is true', async () => {
const app = new Hono();
applyMiddlewareStack(app, {skipCacheHeaders: true});
app.get('/test', (c) => c.json({ok: true}));
const response = await app.request('/test');
expect(response.headers.get('Cache-Control')).toBeNull();
});
test('skips errorHandler when skipErrorHandler is true', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const error = new Error('Test error');
const app = new Hono();
applyMiddlewareStack(app, {
skipErrorHandler: true,
});
app.get('/test', () => {
throw error;
});
try {
const response = await app.request('/test');
expect(response.status).toBe(500);
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
expect(consoleErrorSpy).toHaveBeenCalledWith(error);
} finally {
consoleErrorSpy.mockRestore();
}
});
test('applies custom middleware', async () => {
const customMiddleware = vi.fn().mockImplementation(async (_c, next) => {
await next();
});
const app = new Hono();
applyMiddlewareStack(app, {
customMiddleware: [customMiddleware],
});
app.get('/test', (c) => c.json({ok: true}));
await app.request('/test');
expect(customMiddleware).toHaveBeenCalled();
});
test('applies logger with skip paths', async () => {
const log = vi.fn();
const app = new Hono();
applyMiddlewareStack(app, {
logger: {log, skip: ['/_health']},
});
app.get('/_health', (c) => c.json({ok: true}));
app.get('/api', (c) => c.json({ok: true}));
await app.request('/_health');
expect(log).not.toHaveBeenCalled();
await app.request('/api');
expect(log).toHaveBeenCalled();
});
});
describe('createDefaultLogger', () => {
test('logs request data as JSON', async () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const logger = createDefaultLogger({serviceName: 'test-service'});
logger({method: 'GET', path: '/api/test', status: 200, durationMs: 50});
expect(consoleSpy).toHaveBeenCalledTimes(1);
const loggedData = JSON.parse(consoleSpy.mock.calls[0][0] as string);
expect(loggedData.service).toBe('test-service');
expect(loggedData.method).toBe('GET');
expect(loggedData.path).toBe('/api/test');
expect(loggedData.status).toBe(200);
expect(loggedData.durationMs).toBe(50);
expect(loggedData.timestamp).toBeTruthy();
consoleSpy.mockRestore();
});
test('skips paths in skip array', () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const logger = createDefaultLogger({serviceName: 'test-service', skip: ['/_health']});
logger({method: 'GET', path: '/_health', status: 200, durationMs: 1});
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();
});
test('logs paths not in skip array', () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const logger = createDefaultLogger({serviceName: 'test-service', skip: ['/_health']});
logger({method: 'GET', path: '/api/users', status: 200, durationMs: 1});
expect(consoleSpy).toHaveBeenCalledTimes(1);
consoleSpy.mockRestore();
});
});
describe('createDefaultErrorLogger', () => {
test('logs error data as JSON', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const errorLogger = createDefaultErrorLogger({serviceName: 'test-service'});
const mockContext = {
req: {
path: '/api/test',
method: 'POST',
},
} as Context;
const error = new Error('Test error');
errorLogger(error, mockContext);
expect(consoleSpy).toHaveBeenCalledTimes(1);
const loggedData = JSON.parse(consoleSpy.mock.calls[0][0] as string);
expect(loggedData.service).toBe('test-service');
expect(loggedData.error).toBe('Test error');
expect(loggedData.stack).toBeTruthy();
expect(loggedData.path).toBe('/api/test');
expect(loggedData.method).toBe('POST');
expect(loggedData.timestamp).toBeTruthy();
consoleSpy.mockRestore();
});
});
describe('integration tests', () => {
test('full middleware stack works together', async () => {
const log = vi.fn();
const rateLimitService = createMockRateLimitService();
const keyGenerator = vi.fn().mockResolvedValue('test-client');
const app = new Hono();
applyMiddlewareStack(app, {
requestId: {},
cors: {enabled: true, origins: '*'},
logger: {log},
rateLimit: {enabled: true, service: rateLimitService, keyGenerator},
});
app.get('/test', (c) => c.json({ok: true}));
const response = await app.request('/test');
expect(response.status).toBe(200);
expect(response.headers.get(Headers.X_REQUEST_ID)).toBeTruthy();
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*');
expect(response.headers.get('Cache-Control')).toBe('no-cache');
expect(response.headers.get('X-RateLimit-Limit')).toBe('100');
expect(log).toHaveBeenCalled();
expect(keyGenerator).toHaveBeenCalled();
expect(rateLimitService.checkLimit).toHaveBeenCalled();
});
test('error handler catches errors from routes', async () => {
const app = new Hono();
applyMiddlewareStack(app, {requestId: {}});
app.get('/error', () => {
throw new Error('Route error');
});
const response = await app.request('/error');
expect(response.status).toBe(500);
expect(response.headers.get(Headers.X_REQUEST_ID)).toBeTruthy();
});
test('rate limiter blocks requests when limit exceeded', async () => {
const rateLimitService = createMockRateLimitService({allowed: false, remaining: 0});
const keyGenerator = vi.fn().mockResolvedValue('blocked-client');
const app = new Hono();
applyMiddlewareStack(app, {
rateLimit: {enabled: true, service: rateLimitService, keyGenerator},
});
app.get('/test', (c) => c.json({ok: true}));
const response = await app.request('/test');
expect(response.status).toBe(429);
expect(keyGenerator).toHaveBeenCalled();
});
test('health endpoints are skipped by default for rate limiting', async () => {
const rateLimitService = createMockRateLimitService();
const app = new Hono();
applyMiddlewareStack(app, {
rateLimit: {enabled: true, service: rateLimitService},
});
app.get('/_health', (c) => c.json({ok: true}));
await app.request('/_health');
expect(rateLimitService.checkLimit).not.toHaveBeenCalled();
});
});
@@ -1,136 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {Headers} from '@fluxer/constants/src/Headers';
import {REQUEST_ID_KEY, requestId} from '@fluxer/hono/src/middleware/RequestId';
import {Hono} from 'hono';
import {describe, expect, test, vi} from 'vitest';
type AppEnv = {
Variables: {
requestId: string;
};
};
describe('RequestId Middleware', () => {
test('generates a request ID when none provided', async () => {
const app = new Hono<AppEnv>();
app.use('*', requestId());
app.get('/test', (c) => {
const id = c.get(REQUEST_ID_KEY);
return c.json({requestId: id});
});
const response = await app.request('/test');
expect(response.status).toBe(200);
const responseId = response.headers.get(Headers.X_REQUEST_ID);
expect(responseId).toBeTruthy();
expect(responseId).toMatch(/^[0-9a-f-]{36}$/);
const body = (await response.json()) as {
requestId: string;
};
expect(body.requestId).toBe(responseId);
});
test('uses existing request ID from header', async () => {
const app = new Hono<AppEnv>();
app.use('*', requestId());
app.get('/test', (c) => {
const id = c.get(REQUEST_ID_KEY);
return c.json({requestId: id});
});
const existingId = 'existing-request-id-12345';
const response = await app.request('/test', {
headers: {
[Headers.X_REQUEST_ID]: existingId,
},
});
expect(response.status).toBe(200);
expect(response.headers.get(Headers.X_REQUEST_ID)).toBe(existingId);
const body = (await response.json()) as {
requestId: string;
};
expect(body.requestId).toBe(existingId);
});
test('uses custom header name', async () => {
const customHeader = 'X-Custom-Request-ID';
const app = new Hono<AppEnv>();
app.use('*', requestId({headerName: customHeader}));
app.get('/test', (c) => {
const id = c.get(REQUEST_ID_KEY);
return c.json({requestId: id});
});
const existingId = 'custom-header-id';
const response = await app.request('/test', {
headers: {
[customHeader]: existingId,
},
});
expect(response.status).toBe(200);
expect(response.headers.get(customHeader)).toBe(existingId);
});
test('uses custom generator function', async () => {
const customGenerator = vi.fn().mockReturnValue('custom-generated-id');
const app = new Hono<AppEnv>();
app.use('*', requestId({generator: customGenerator}));
app.get('/test', (c) => {
const id = c.get(REQUEST_ID_KEY);
return c.json({requestId: id});
});
const response = await app.request('/test');
expect(response.status).toBe(200);
expect(customGenerator).toHaveBeenCalled();
expect(response.headers.get(Headers.X_REQUEST_ID)).toBe('custom-generated-id');
});
test('does not call generator when request ID exists', async () => {
const customGenerator = vi.fn().mockReturnValue('custom-generated-id');
const app = new Hono<AppEnv>();
app.use('*', requestId({generator: customGenerator}));
app.get('/test', (c) => c.json({ok: true}));
await app.request('/test', {
headers: {
[Headers.X_REQUEST_ID]: 'existing-id',
},
});
expect(customGenerator).not.toHaveBeenCalled();
});
test('does not set response header when setResponseHeader is false', async () => {
const app = new Hono<AppEnv>();
app.use('*', requestId({setResponseHeader: false}));
app.get('/test', (c) => c.json({ok: true}));
const response = await app.request('/test');
expect(response.status).toBe(200);
expect(response.headers.get(Headers.X_REQUEST_ID)).toBeNull();
});
test('sets response header by default', async () => {
const app = new Hono<AppEnv>();
app.use('*', requestId());
app.get('/test', (c) => c.json({ok: true}));
const response = await app.request('/test');
expect(response.status).toBe(200);
expect(response.headers.get(Headers.X_REQUEST_ID)).toBeTruthy();
});
test('request ID is available in context after middleware runs', async () => {
const app = new Hono<AppEnv>();
let capturedId: string | undefined;
app.use('*', requestId());
app.get('/test', (c) => {
capturedId = c.get(REQUEST_ID_KEY);
return c.json({ok: true});
});
await app.request('/test');
expect(capturedId).toBeTruthy();
expect(capturedId).toMatch(/^[0-9a-f-]{36}$/);
});
test('different requests get different IDs', async () => {
const app = new Hono<AppEnv>();
const capturedIds: Array<string> = [];
app.use('*', requestId());
app.get('/test', (c) => {
capturedIds.push(c.get(REQUEST_ID_KEY));
return c.json({ok: true});
});
await app.request('/test');
await app.request('/test');
await app.request('/test');
expect(capturedIds).toHaveLength(3);
expect(new Set(capturedIds).size).toBe(3);
});
});
@@ -1,202 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {requestLogger} from '@fluxer/hono/src/middleware/RequestLogger';
import {Hono} from 'hono';
import {describe, expect, test, vi} from 'vitest';
describe('RequestLogger Middleware', () => {
describe('logging', () => {
test('calls log function with request data', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log}));
app.get('/test', (c) => c.json({ok: true}));
await app.request('/test');
expect(log).toHaveBeenCalledTimes(1);
expect(log).toHaveBeenCalledWith(
expect.objectContaining({
method: 'GET',
path: '/test',
status: 200,
}),
);
});
test('includes method in log data', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log}));
app.post('/users', (c) => c.json({created: true}));
await app.request('/users', {method: 'POST'});
expect(log).toHaveBeenCalledWith(
expect.objectContaining({
method: 'POST',
}),
);
});
test('includes path in log data', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log}));
app.get('/api/users/123', (c) => c.json({ok: true}));
await app.request('/api/users/123');
expect(log).toHaveBeenCalledWith(
expect.objectContaining({
path: '/api/users/123',
}),
);
});
test('includes status code in log data', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log}));
app.get('/error', (c) => c.json({error: 'Not found'}, 404));
await app.request('/error');
expect(log).toHaveBeenCalledWith(
expect.objectContaining({
status: 404,
}),
);
});
test('includes durationMs in log data', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log}));
app.get('/test', (c) => c.json({ok: true}));
await app.request('/test');
expect(log).toHaveBeenCalledWith(
expect.objectContaining({
durationMs: expect.any(Number),
}),
);
expect(log.mock.calls[0][0].durationMs).toBeGreaterThanOrEqual(0);
});
});
describe('skip option', () => {
test('skips logging for paths in skip array', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log, skip: ['/_health']}));
app.get('/_health', (c) => c.json({ok: true}));
await app.request('/_health');
expect(log).not.toHaveBeenCalled();
});
test('logs paths not in skip array', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log, skip: ['/_health']}));
app.get('/api/users', (c) => c.json({ok: true}));
await app.request('/api/users');
expect(log).toHaveBeenCalled();
});
test('skips multiple paths', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log, skip: ['/_health', '/metrics', '/_health']}));
app.get('/_health', (c) => c.json({ok: true}));
app.get('/metrics', (c) => c.json({ok: true}));
app.get('/_health', (c) => c.json({ok: true}));
await app.request('/_health');
await app.request('/metrics');
await app.request('/_health');
expect(log).not.toHaveBeenCalled();
});
test('uses empty skip array by default', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log}));
app.get('/_health', (c) => c.json({ok: true}));
await app.request('/_health');
expect(log).toHaveBeenCalled();
});
});
describe('different HTTP methods', () => {
test('logs GET requests', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log}));
app.get('/test', (c) => c.json({ok: true}));
await app.request('/test');
expect(log).toHaveBeenCalledWith(expect.objectContaining({method: 'GET'}));
});
test('logs POST requests', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log}));
app.post('/test', (c) => c.json({ok: true}));
await app.request('/test', {method: 'POST'});
expect(log).toHaveBeenCalledWith(expect.objectContaining({method: 'POST'}));
});
test('logs PUT requests', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log}));
app.put('/test', (c) => c.json({ok: true}));
await app.request('/test', {method: 'PUT'});
expect(log).toHaveBeenCalledWith(expect.objectContaining({method: 'PUT'}));
});
test('logs DELETE requests', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log}));
app.delete('/test', (c) => c.json({ok: true}));
await app.request('/test', {method: 'DELETE'});
expect(log).toHaveBeenCalledWith(expect.objectContaining({method: 'DELETE'}));
});
test('logs PATCH requests', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log}));
app.patch('/test', (c) => c.json({ok: true}));
await app.request('/test', {method: 'PATCH'});
expect(log).toHaveBeenCalledWith(expect.objectContaining({method: 'PATCH'}));
});
});
describe('different status codes', () => {
test('logs 200 OK responses', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log}));
app.get('/test', (c) => c.json({ok: true}));
await app.request('/test');
expect(log).toHaveBeenCalledWith(expect.objectContaining({status: 200}));
});
test('logs 201 Created responses', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log}));
app.post('/test', (c) => c.json({created: true}, 201));
await app.request('/test', {method: 'POST'});
expect(log).toHaveBeenCalledWith(expect.objectContaining({status: 201}));
});
test('logs 400 Bad Request responses', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log}));
app.get('/test', (c) => c.json({error: 'Bad Request'}, 400));
await app.request('/test');
expect(log).toHaveBeenCalledWith(expect.objectContaining({status: 400}));
});
test('logs 500 Internal Server Error responses', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log}));
app.get('/test', (c) => c.json({error: 'Server Error'}, 500));
await app.request('/test');
expect(log).toHaveBeenCalledWith(expect.objectContaining({status: 500}));
});
});
describe('multiple requests', () => {
test('logs each request separately', async () => {
const log = vi.fn();
const app = new Hono();
app.use('*', requestLogger({log}));
app.get('/first', (c) => c.json({ok: true}));
app.get('/second', (c) => c.json({ok: true}));
await app.request('/first');
await app.request('/second');
expect(log).toHaveBeenCalledTimes(2);
expect(log).toHaveBeenNthCalledWith(1, expect.objectContaining({path: '/first'}));
expect(log).toHaveBeenNthCalledWith(2, expect.objectContaining({path: '/second'}));
});
});
});