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

Skip to content

Commit b8fbeb3

Browse files
Test: Add req and res streaming e2e tests
1 parent 51ddc5c commit b8fbeb3

15 files changed

Lines changed: 1703 additions & 2 deletions

eslint.config.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import globals from 'globals';
99
const sharedRules = {
1010
'@typescript-eslint/explicit-function-return-type': 'off',
1111
'@typescript-eslint/explicit-module-boundary-types': 'off',
12-
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
12+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }],
1313
'prettier/prettier': 'error',
1414
};
1515

@@ -38,7 +38,7 @@ export default [
3838
rules: {
3939
...prettierConfig.rules,
4040
'prettier/prettier': 'error',
41-
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
41+
'no-unused-vars': ['error', { argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }],
4242
'no-console': 'off',
4343
},
4444
},
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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+
7+
// Shared state for event tracking across requests
8+
const eventState = {
9+
finishCount: 0,
10+
closeCount: 0,
11+
};
12+
13+
@Controller('events')
14+
class EventsController {
15+
@Get('finish')
16+
finishEvent(@Res() res: UwsResponse) {
17+
res.on('finish', () => {
18+
eventState.finishCount++;
19+
});
20+
res.send('done');
21+
}
22+
23+
@Get('close')
24+
closeEvent(@Res() res: UwsResponse) {
25+
res.on('close', () => {
26+
eventState.closeCount++;
27+
});
28+
// Intentionally do not send — wait for client abort
29+
}
30+
31+
@Get('status')
32+
getStatus() {
33+
return { ...eventState };
34+
}
35+
36+
@Get('reset')
37+
resetStatus() {
38+
eventState.finishCount = 0;
39+
eventState.closeCount = 0;
40+
return { reset: true };
41+
}
42+
}
43+
44+
@Module({
45+
controllers: [EventsController],
46+
})
47+
class TestModule {}
48+
49+
describe('Response Events E2E', () => {
50+
let app: INestApplication;
51+
let baseUrl: string;
52+
const port = 13353;
53+
54+
beforeAll(async () => {
55+
const adapter = new UwsPlatformAdapter({ port, maxBodySize: 10 * 1024 * 1024 });
56+
app = await NestFactory.create(TestModule, adapter);
57+
await app.init();
58+
59+
await new Promise<void>((resolve, reject) => {
60+
adapter.listen(port, (error) => {
61+
if (error) reject(error);
62+
else resolve();
63+
});
64+
});
65+
66+
baseUrl = `http://localhost:${port}`;
67+
}, 10000);
68+
69+
afterAll(async () => {
70+
if (app) {
71+
await app.close();
72+
}
73+
await new Promise((resolve) => setTimeout(resolve, 500));
74+
});
75+
76+
beforeEach(async () => {
77+
await fetch(`${baseUrl}/events/reset`);
78+
});
79+
80+
it('should emit finish event when response is sent', async () => {
81+
const response = await fetch(`${baseUrl}/events/finish`);
82+
83+
expect(response.status).toBe(200);
84+
expect(await response.text()).toBe('done');
85+
86+
// Query status endpoint to verify finish event fired
87+
const statusResponse = await fetch(`${baseUrl}/events/status`);
88+
const status = await statusResponse.json();
89+
90+
expect(status.finishCount).toBe(1);
91+
});
92+
93+
it('should emit close event when connection is aborted', async () => {
94+
const controller = new AbortController();
95+
96+
// Start request and abort quickly
97+
setTimeout(() => controller.abort(), 50);
98+
99+
try {
100+
await fetch(`${baseUrl}/events/close`, {
101+
signal: controller.signal,
102+
});
103+
} catch {
104+
// Expected abort error
105+
}
106+
107+
// Wait for server to process abort
108+
await new Promise((resolve) => setTimeout(resolve, 300));
109+
110+
// Query status endpoint to verify close event fired
111+
const statusResponse = await fetch(`${baseUrl}/events/status`);
112+
const status = await statusResponse.json();
113+
114+
expect(status.closeCount).toBe(1);
115+
});
116+
117+
it('should emit finish event for each completed request', async () => {
118+
// Send 3 requests
119+
for (let i = 0; i < 3; i++) {
120+
const response = await fetch(`${baseUrl}/events/finish`);
121+
expect(response.status).toBe(200);
122+
}
123+
124+
const statusResponse = await fetch(`${baseUrl}/events/status`);
125+
const status = await statusResponse.json();
126+
127+
expect(status.finishCount).toBe(3);
128+
});
129+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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+
7+
@Controller('write-test')
8+
class WriteTestController {
9+
@Get('multiple-writes')
10+
multipleWrites(@Res() res: UwsResponse) {
11+
res.setHeader('x-multi-write', 'true');
12+
res.write('Hello ');
13+
res.write('World');
14+
res.send('!');
15+
}
16+
17+
@Get('many-writes')
18+
manyWrites(@Res() res: UwsResponse) {
19+
res.setHeader('x-many-writes', 'true');
20+
for (let i = 0; i < 10; i++) {
21+
res.write(`chunk-${i}-`);
22+
}
23+
res.send('end');
24+
}
25+
}
26+
27+
@Module({
28+
controllers: [WriteTestController],
29+
})
30+
class TestModule {}
31+
32+
describe('Response Multiple Writes + Send E2E', () => {
33+
let app: INestApplication;
34+
let baseUrl: string;
35+
const port = 13352;
36+
37+
beforeAll(async () => {
38+
const adapter = new UwsPlatformAdapter({ port, maxBodySize: 10 * 1024 * 1024 });
39+
app = await NestFactory.create(TestModule, adapter);
40+
await app.init();
41+
42+
await new Promise<void>((resolve, reject) => {
43+
adapter.listen(port, (error) => {
44+
if (error) reject(error);
45+
else resolve();
46+
});
47+
});
48+
49+
baseUrl = `http://localhost:${port}`;
50+
}, 10000);
51+
52+
afterAll(async () => {
53+
if (app) {
54+
await app.close();
55+
}
56+
await new Promise((resolve) => setTimeout(resolve, 500));
57+
});
58+
59+
it('should combine multiple res.write() calls with res.send()', async () => {
60+
const response = await fetch(`${baseUrl}/write-test/multiple-writes`);
61+
62+
expect(response.status).toBe(200);
63+
expect(response.headers.get('x-multi-write')).toBe('true');
64+
65+
const body = await response.text();
66+
expect(body).toBe('Hello World!');
67+
});
68+
69+
it('should handle many res.write() calls batched correctly', async () => {
70+
const response = await fetch(`${baseUrl}/write-test/many-writes`);
71+
72+
expect(response.status).toBe(200);
73+
expect(response.headers.get('x-many-writes')).toBe('true');
74+
75+
const body = await response.text();
76+
const expected = Array.from({ length: 10 }, (_, i) => `chunk-${i}-`).join('') + 'end';
77+
expect(body).toBe(expected);
78+
});
79+
});
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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 crypto from 'crypto';
7+
import * as fs from 'fs';
8+
import * as os from 'os';
9+
import * as path from 'path';
10+
11+
const TEST_TEMP_DIR = path.join(os.tmpdir(), 'uwestjs-response-stream-chunked');
12+
13+
@Controller('stream-test')
14+
class StreamTestController {
15+
@Get('chunked')
16+
async chunkedStream(@Res() res: UwsResponse) {
17+
const testFilePath = path.join(TEST_TEMP_DIR, 'test-file.bin');
18+
const fileStream = fs.createReadStream(testFilePath);
19+
20+
res.setHeader('x-is-streamed', 'true');
21+
await res.stream(fileStream);
22+
}
23+
}
24+
25+
@Module({
26+
controllers: [StreamTestController],
27+
})
28+
class TestModule {}
29+
30+
describe('Response Streaming - Chunked Encoding Mode E2E', () => {
31+
let app: INestApplication;
32+
let baseUrl: string;
33+
const port = 13349;
34+
35+
beforeAll(async () => {
36+
if (!fs.existsSync(TEST_TEMP_DIR)) {
37+
fs.mkdirSync(TEST_TEMP_DIR, { recursive: true });
38+
}
39+
40+
// Create test file with random data
41+
const testFilePath = path.join(TEST_TEMP_DIR, 'test-file.bin');
42+
const testData = crypto.randomBytes(512 * 1024); // 512KB
43+
fs.writeFileSync(testFilePath, testData);
44+
45+
const adapter = new UwsPlatformAdapter({ port, maxBodySize: 10 * 1024 * 1024 });
46+
app = await NestFactory.create(TestModule, adapter);
47+
await app.init();
48+
49+
await new Promise<void>((resolve, reject) => {
50+
adapter.listen(port, (error) => {
51+
if (error) reject(error);
52+
else resolve();
53+
});
54+
});
55+
56+
baseUrl = `http://localhost:${port}`;
57+
}, 10000);
58+
59+
afterAll(async () => {
60+
if (app) {
61+
await app.close();
62+
}
63+
await new Promise((resolve) => setTimeout(resolve, 500));
64+
65+
if (fs.existsSync(TEST_TEMP_DIR)) {
66+
fs.rmSync(TEST_TEMP_DIR, { recursive: true, force: true });
67+
}
68+
});
69+
70+
it('should stream response with chunked transfer encoding (no Content-Length)', async () => {
71+
const testFilePath = path.join(TEST_TEMP_DIR, 'test-file.bin');
72+
const expectedBuffer = fs.readFileSync(testFilePath);
73+
const expectedHash = crypto.createHash('md5').update(expectedBuffer).digest('hex');
74+
75+
const response = await fetch(`${baseUrl}/stream-test/chunked`);
76+
77+
expect(response.status).toBe(200);
78+
expect(response.headers.get('x-is-streamed')).toBe('true');
79+
80+
// Chunked encoding should not have a Content-Length header
81+
// (the response uses transfer-encoding: chunked)
82+
expect(response.headers.get('content-length')).toBeNull();
83+
84+
const receivedBuffer = Buffer.from(await response.arrayBuffer());
85+
const receivedHash = crypto.createHash('md5').update(receivedBuffer).digest('hex');
86+
87+
expect(receivedHash).toBe(expectedHash);
88+
expect(receivedBuffer.byteLength).toBe(expectedBuffer.byteLength);
89+
});
90+
91+
it('should stream large file with chunked encoding (2MB)', async () => {
92+
// Overwrite the same file the controller reads
93+
const testFilePath = path.join(TEST_TEMP_DIR, 'test-file.bin');
94+
const testData = crypto.randomBytes(2 * 1024 * 1024); // 2MB
95+
fs.writeFileSync(testFilePath, testData);
96+
97+
const expectedHash = crypto.createHash('md5').update(testData).digest('hex');
98+
99+
const response = await fetch(`${baseUrl}/stream-test/chunked`);
100+
101+
expect(response.status).toBe(200);
102+
expect(response.headers.get('content-length')).toBeNull();
103+
104+
const receivedBuffer = Buffer.from(await response.arrayBuffer());
105+
const receivedHash = crypto.createHash('md5').update(receivedBuffer).digest('hex');
106+
107+
expect(receivedHash).toBe(expectedHash);
108+
expect(receivedBuffer.byteLength).toBe(testData.byteLength);
109+
}, 15000);
110+
});

0 commit comments

Comments
 (0)