Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 791cc90

Browse files
Feat: Add HTTP compression E2E test
1 parent f614dab commit 791cc90

1 file changed

Lines changed: 247 additions & 0 deletions

File tree

test/http/compression.e2e.spec.ts

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
// @ts-nocheck - NestJS decorators in test files cause false positive TypeScript errors
2+
import { NestFactory } from '@nestjs/core';
3+
import { Controller, Get, Res, Module, INestApplication } from '@nestjs/common';
4+
import { UwsPlatformAdapter } from '../../src/http/platform/uws-platform.adapter';
5+
import { UwsResponse } from '../../src/http/core/response';
6+
import * as zlib from 'zlib';
7+
import * as http from 'http';
8+
import { promisify } from 'util';
9+
import { Readable } from 'stream';
10+
11+
const gunzip = promisify(zlib.gunzip);
12+
const brotliDecompress = promisify(zlib.brotliDecompress);
13+
const inflate = promisify(zlib.inflate);
14+
15+
const LARGE_TEXT = 'x'.repeat(2048); // 2KB — above default 1KB threshold
16+
const SMALL_TEXT = 'hello'; // Below threshold
17+
18+
@Controller('compress-test')
19+
class CompressTestController {
20+
@Get('text')
21+
text(@Res() res: UwsResponse) {
22+
res.type('text/plain');
23+
res.send(LARGE_TEXT);
24+
}
25+
26+
@Get('json')
27+
json(@Res() res: UwsResponse) {
28+
res.json({ message: LARGE_TEXT });
29+
}
30+
31+
@Get('small')
32+
small(@Res() res: UwsResponse) {
33+
res.type('text/plain');
34+
res.send(SMALL_TEXT);
35+
}
36+
37+
@Get('image')
38+
image(@Res() res: UwsResponse) {
39+
res.type('image/png');
40+
res.send(Buffer.from('fake-png-data'));
41+
}
42+
43+
@Get('write-then-stream')
44+
writeThenStream(@Res() res: UwsResponse) {
45+
res.type('text/plain');
46+
res.write('preamble-' + 'x'.repeat(512));
47+
const stream = Readable.from(['stream-' + 'y'.repeat(1024)]);
48+
res.stream(stream);
49+
}
50+
}
51+
52+
@Module({
53+
controllers: [CompressTestController],
54+
})
55+
class TestModule {}
56+
57+
describe('Response Compression E2E', () => {
58+
let app: INestApplication;
59+
let baseUrl: string;
60+
const port = 13358;
61+
62+
beforeAll(async () => {
63+
const adapter = new UwsPlatformAdapter({
64+
port,
65+
compress: {
66+
threshold: 1024,
67+
level: 6,
68+
brotli: true,
69+
},
70+
});
71+
app = await NestFactory.create(TestModule, adapter);
72+
await app.init();
73+
74+
await new Promise<void>((resolve, reject) => {
75+
adapter.listen(port, (error) => {
76+
if (error) reject(error);
77+
else resolve();
78+
});
79+
});
80+
81+
baseUrl = `http://localhost:${port}`;
82+
}, 10000);
83+
84+
afterAll(async () => {
85+
if (app) {
86+
await app.close();
87+
}
88+
await new Promise((resolve) => setTimeout(resolve, 500));
89+
});
90+
91+
function httpGet(
92+
path: string,
93+
encoding?: string
94+
): Promise<{
95+
status: number;
96+
headers: Record<string, string | string[]>;
97+
body: Buffer;
98+
}> {
99+
return new Promise((resolve, reject) => {
100+
const headers: Record<string, string> = {};
101+
if (encoding) {
102+
headers['Accept-Encoding'] = encoding;
103+
}
104+
105+
const req = http.get(`${baseUrl}${path}`, { agent: false, headers }, (res) => {
106+
const chunks: Buffer[] = [];
107+
res.on('data', (chunk) => chunks.push(chunk));
108+
res.on('end', () => {
109+
resolve({
110+
status: res.statusCode || 0,
111+
headers: res.headers as Record<string, string | string[]>,
112+
body: Buffer.concat(chunks),
113+
});
114+
});
115+
});
116+
req.on('error', reject);
117+
});
118+
}
119+
120+
// ============================================================================
121+
// Gzip
122+
// ============================================================================
123+
124+
describe('gzip compression', () => {
125+
it('should compress text response with gzip', async () => {
126+
const res = await httpGet('/compress-test/text', 'gzip');
127+
128+
expect(res.status).toBe(200);
129+
expect(res.headers['content-encoding']).toBe('gzip');
130+
expect(res.headers['vary']).toMatch(/accept-encoding/i);
131+
132+
const decompressed = await gunzip(res.body);
133+
expect(decompressed.toString()).toBe(LARGE_TEXT);
134+
});
135+
136+
it('should compress json response with gzip', async () => {
137+
const res = await httpGet('/compress-test/json', 'gzip');
138+
139+
expect(res.status).toBe(200);
140+
expect(res.headers['content-encoding']).toBe('gzip');
141+
142+
const decompressed = await gunzip(res.body);
143+
const parsed = JSON.parse(decompressed.toString());
144+
expect(parsed.message).toBe(LARGE_TEXT);
145+
});
146+
});
147+
148+
// ============================================================================
149+
// Brotli
150+
// ============================================================================
151+
152+
describe('brotli compression', () => {
153+
it('should compress with brotli when client accepts br', async () => {
154+
const res = await httpGet('/compress-test/text', 'br');
155+
156+
expect(res.status).toBe(200);
157+
expect(res.headers['content-encoding']).toBe('br');
158+
159+
const decompressed = await brotliDecompress(res.body);
160+
expect(decompressed.toString()).toBe(LARGE_TEXT);
161+
});
162+
163+
it('should prefer brotli over gzip when both accepted', async () => {
164+
const res = await httpGet('/compress-test/text', 'gzip, deflate, br');
165+
166+
expect(res.status).toBe(200);
167+
expect(res.headers['content-encoding']).toBe('br');
168+
});
169+
});
170+
171+
// ============================================================================
172+
// Deflate
173+
// ============================================================================
174+
175+
describe('deflate compression', () => {
176+
it('should compress with deflate when requested', async () => {
177+
const res = await httpGet('/compress-test/text', 'deflate');
178+
179+
expect(res.status).toBe(200);
180+
expect(res.headers['content-encoding']).toBe('deflate');
181+
182+
const decompressed = await inflate(res.body);
183+
expect(decompressed.toString()).toBe(LARGE_TEXT);
184+
});
185+
});
186+
187+
// ============================================================================
188+
// No compression
189+
// ============================================================================
190+
191+
describe('no compression', () => {
192+
it('should not compress when Accept-Encoding is missing', async () => {
193+
const res = await httpGet('/compress-test/text');
194+
195+
expect(res.status).toBe(200);
196+
expect(res.headers['content-encoding']).toBeUndefined();
197+
expect(res.body.toString()).toBe(LARGE_TEXT);
198+
});
199+
200+
it('should not compress responses below threshold', async () => {
201+
const res = await httpGet('/compress-test/small', 'gzip');
202+
203+
expect(res.status).toBe(200);
204+
expect(res.headers['content-encoding']).toBeUndefined();
205+
expect(res.body.toString()).toBe(SMALL_TEXT);
206+
});
207+
208+
it('should not compress non-compressible content types', async () => {
209+
const res = await httpGet('/compress-test/image', 'gzip');
210+
211+
expect(res.status).toBe(200);
212+
expect(res.headers['content-encoding']).toBeUndefined();
213+
});
214+
});
215+
216+
// ============================================================================
217+
// Streaming with prior write() — regression for mixed-encoding bug
218+
// ============================================================================
219+
220+
describe('streaming compression', () => {
221+
it('should skip compression when write() was already called', async () => {
222+
const res = await httpGet('/compress-test/write-then-stream', 'gzip');
223+
224+
expect(res.status).toBe(200);
225+
// Headers were already sent by write(), so compression must be skipped
226+
expect(res.headers['content-encoding']).toBeUndefined();
227+
228+
const text = res.body.toString();
229+
expect(text.startsWith('preamble-')).toBe(true);
230+
expect(text.includes('stream-')).toBe(true);
231+
});
232+
});
233+
234+
// ============================================================================
235+
// Quality values (Accept-Encoding negotiation)
236+
// ============================================================================
237+
238+
describe('accept-encoding negotiation', () => {
239+
it('should respect quality values', async () => {
240+
// deflate;q=1.0 should win over gzip;q=0.8
241+
const res = await httpGet('/compress-test/text', 'gzip;q=0.8, deflate;q=1.0');
242+
243+
expect(res.status).toBe(200);
244+
expect(res.headers['content-encoding']).toBe('deflate');
245+
});
246+
});
247+
});

0 commit comments

Comments
 (0)