feat(compression): Improve compression implementation (buffering and threshold), add Deno brotli support, add compression tests and dynamic route API
This commit is contained in:
234
test/test.ts
234
test/test.ts
@@ -7,9 +7,53 @@ import {
|
||||
Guard,
|
||||
Transform,
|
||||
Intercept,
|
||||
Compress,
|
||||
NoCompress,
|
||||
HttpError,
|
||||
type IRequestContext,
|
||||
} from '../ts/index.js';
|
||||
import * as zlib from 'zlib';
|
||||
import * as http from 'http';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const gunzip = promisify(zlib.gunzip);
|
||||
const brotliDecompress = promisify(zlib.brotliDecompress);
|
||||
|
||||
/**
|
||||
* Make a raw HTTP request without automatic decompression
|
||||
* (Node.js fetch auto-decompresses, which breaks our tests)
|
||||
*/
|
||||
function rawRequest(options: {
|
||||
port: number;
|
||||
path: string;
|
||||
headers?: Record<string, string>;
|
||||
}): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: Buffer }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request(
|
||||
{
|
||||
hostname: 'localhost',
|
||||
port: options.port,
|
||||
path: options.path,
|
||||
method: 'GET',
|
||||
headers: options.headers,
|
||||
},
|
||||
(res) => {
|
||||
const chunks: Buffer[] = [];
|
||||
res.on('data', (chunk) => chunks.push(chunk));
|
||||
res.on('end', () => {
|
||||
resolve({
|
||||
status: res.statusCode ?? 200,
|
||||
headers: res.headers,
|
||||
body: Buffer.concat(chunks),
|
||||
});
|
||||
});
|
||||
res.on('error', reject);
|
||||
}
|
||||
);
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Test controller
|
||||
@Route('/api')
|
||||
@@ -56,6 +100,48 @@ class WrappedController {
|
||||
}
|
||||
}
|
||||
|
||||
// Controller with compression decorators
|
||||
@Route('/compression')
|
||||
class CompressionController {
|
||||
@Get('/large')
|
||||
getLargeData() {
|
||||
// Return data larger than default threshold (1024 bytes)
|
||||
return {
|
||||
data: 'x'.repeat(2000),
|
||||
items: Array.from({ length: 50 }, (_, i) => ({
|
||||
id: i,
|
||||
name: `Item ${i}`,
|
||||
description: `This is a description for item ${i} with some extra text to make it longer`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/small')
|
||||
getSmallData() {
|
||||
// Return data smaller than threshold
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@Get('/no-compress')
|
||||
@NoCompress()
|
||||
getNoCompress() {
|
||||
// Should never be compressed
|
||||
return {
|
||||
data: 'x'.repeat(2000),
|
||||
compressed: false,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/force-compress')
|
||||
@Compress({ level: 9 })
|
||||
getForceCompress() {
|
||||
return {
|
||||
data: 'y'.repeat(2000),
|
||||
compressed: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('SmartServe should create server instance', async () => {
|
||||
const server = new SmartServe({ port: 3456 });
|
||||
expect(server).toBeInstanceOf(SmartServe);
|
||||
@@ -205,4 +291,152 @@ tap.test('HttpError should create proper responses', async () => {
|
||||
expect(body.details.id).toEqual('123');
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Compression Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('Compression should apply gzip when Accept-Encoding is sent', async () => {
|
||||
const server = new SmartServe({ port: 3470 });
|
||||
server.register(CompressionController);
|
||||
await server.start();
|
||||
|
||||
try {
|
||||
const response = await rawRequest({
|
||||
port: 3470,
|
||||
path: '/compression/large',
|
||||
headers: { 'Accept-Encoding': 'gzip' },
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.headers['content-encoding']).toEqual('gzip');
|
||||
expect(response.headers['vary']).toInclude('Accept-Encoding');
|
||||
|
||||
// Decompress and verify content
|
||||
const decompressed = await gunzip(response.body);
|
||||
const data = JSON.parse(decompressed.toString());
|
||||
|
||||
expect(data.data).toStartWith('xxx');
|
||||
expect(data.items.length).toEqual(50);
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Compression should apply brotli when preferred', async () => {
|
||||
const server = new SmartServe({ port: 3471 });
|
||||
server.register(CompressionController);
|
||||
await server.start();
|
||||
|
||||
try {
|
||||
const response = await rawRequest({
|
||||
port: 3471,
|
||||
path: '/compression/large',
|
||||
headers: { 'Accept-Encoding': 'br, gzip' },
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.headers['content-encoding']).toEqual('br');
|
||||
|
||||
// Decompress and verify content
|
||||
const decompressed = await brotliDecompress(response.body);
|
||||
const data = JSON.parse(decompressed.toString());
|
||||
|
||||
expect(data.data).toStartWith('xxx');
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Compression should skip small responses', async () => {
|
||||
const server = new SmartServe({ port: 3472 });
|
||||
server.register(CompressionController);
|
||||
await server.start();
|
||||
|
||||
try {
|
||||
const response = await rawRequest({
|
||||
port: 3472,
|
||||
path: '/compression/small',
|
||||
headers: { 'Accept-Encoding': 'gzip, br' },
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
// Small response should NOT be compressed
|
||||
expect(response.headers['content-encoding']).toBeUndefined();
|
||||
|
||||
const data = JSON.parse(response.body.toString());
|
||||
expect(data.ok).toBeTrue();
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Compression should respect @NoCompress decorator', async () => {
|
||||
const server = new SmartServe({ port: 3473 });
|
||||
server.register(CompressionController);
|
||||
await server.start();
|
||||
|
||||
try {
|
||||
const response = await rawRequest({
|
||||
port: 3473,
|
||||
path: '/compression/no-compress',
|
||||
headers: { 'Accept-Encoding': 'gzip, br' },
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
// Should NOT be compressed due to @NoCompress
|
||||
expect(response.headers['content-encoding']).toBeUndefined();
|
||||
|
||||
const data = JSON.parse(response.body.toString());
|
||||
expect(data.compressed).toBeFalse();
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Compression should skip when Accept-Encoding not sent', async () => {
|
||||
const server = new SmartServe({ port: 3474 });
|
||||
server.register(CompressionController);
|
||||
await server.start();
|
||||
|
||||
try {
|
||||
const response = await rawRequest({
|
||||
port: 3474,
|
||||
path: '/compression/large',
|
||||
// No Accept-Encoding header
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
// Should NOT be compressed when client doesn't accept it
|
||||
expect(response.headers['content-encoding']).toBeUndefined();
|
||||
|
||||
const data = JSON.parse(response.body.toString());
|
||||
expect(data.data).toStartWith('xxx');
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Compression can be disabled globally', async () => {
|
||||
const server = new SmartServe({
|
||||
port: 3475,
|
||||
compression: false,
|
||||
});
|
||||
server.register(CompressionController);
|
||||
await server.start();
|
||||
|
||||
try {
|
||||
const response = await rawRequest({
|
||||
port: 3475,
|
||||
path: '/compression/large',
|
||||
headers: { 'Accept-Encoding': 'gzip, br' },
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
// Should NOT be compressed when globally disabled
|
||||
expect(response.headers['content-encoding']).toBeUndefined();
|
||||
} finally {
|
||||
await server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
Reference in New Issue
Block a user