Skip to content

Commit 81f27d5

Browse files
committed
feat: add in tests for file uploadn and download
1 parent d2e0297 commit 81f27d5

17 files changed

+242
-82
lines changed

apps/complex-sample/README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,3 @@ We created our own ParseIntPipe even though Nest provides one just to show how t
2121
## Future Additions
2222

2323
Filters can be added in the future to make for even better example of tests.
24-
25-
## Closing Remarks
26-
27-
Do note that while the non-mocked E2E test looks cleaner and easier to deal with, it is also a lot more prone to breaking as it very closely depends on the logic implemented in the CatsService. If that logic changes, the test will fail, while for the most part, the mocked variant will pass. Just as in the unit tests, it is possible to get each provider individually so that you can mock in each function as your heart desires. _(Note to self: I should add that in)_
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<p align="center">
2+
<img src="./testCoverage.png"/>
3+
</p>
4+
5+
# file-up-and-down-sample
Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,5 @@
1+
const baseConfig = require('../../jest.config');
2+
13
module.exports = {
2-
displayName: 'file-up-and-down-sample',
3-
preset: '../../jest.preset.js',
4-
globals: {
5-
'ts-jest': {
6-
tsconfig: '<rootDir>/tsconfig.spec.json',
7-
},
8-
},
9-
testEnvironment: 'node',
10-
transform: {
11-
'^.+\\.[tj]s$': 'ts-jest',
12-
},
13-
moduleFileExtensions: ['ts', 'js', 'html'],
14-
coverageDirectory: '../../coverage/apps/file-up-and-down-sample',
4+
...baseConfig,
155
};
Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,95 @@
1+
import { StreamableFile } from '@nestjs/common';
12
import { Test, TestingModule } from '@nestjs/testing';
3+
import { Readable } from 'stream';
24

35
import { AppController } from './app.controller';
46
import { AppService } from './app.service';
57

68
describe('AppController', () => {
79
let app: TestingModule;
10+
let appController: AppController;
811

912
beforeAll(async () => {
1013
app = await Test.createTestingModule({
1114
controllers: [AppController],
12-
providers: [AppService],
15+
providers: [
16+
{
17+
provide: AppService,
18+
useValue: {
19+
getData: jest
20+
.fn()
21+
.mockReturnValue({ message: 'AppService#getData' }),
22+
getFileStream: jest.fn().mockImplementation(() => {
23+
const readable = new Readable();
24+
readable.push(Buffer.from('Hello World!'));
25+
readable.push(null);
26+
return readable;
27+
}),
28+
},
29+
},
30+
],
1331
}).compile();
32+
appController = app.get<AppController>(AppController);
1433
});
1534

1635
describe('getData', () => {
1736
it('should return "Welcome to file-up-and-down-sample!"', () => {
18-
const appController = app.get<AppController>(AppController);
1937
expect(appController.getData()).toEqual({
20-
message: 'Welcome to file-up-and-down-sample!',
38+
message: 'AppService#getData',
2139
});
2240
});
2341
});
42+
describe('postFile', () => {
43+
it('should return the file.originalname property', () => {
44+
expect(
45+
appController.postFile({
46+
originalname: 'testfile.json',
47+
buffer: Buffer.from('Hello'),
48+
destination: '',
49+
fieldname: 'file',
50+
filename: 'new-file.json',
51+
mimetype: 'text/plain',
52+
encoding: '7bit',
53+
path: '',
54+
size: 5091,
55+
stream: new Readable(),
56+
}),
57+
).toEqual('testfile.json');
58+
});
59+
});
60+
describe('getStreamableFile', () => {
61+
it('should return a streamable file', () => {
62+
expect(appController.getStreamableFile()).toEqual(
63+
new StreamableFile(Buffer.from('Hello World!')),
64+
);
65+
});
66+
});
67+
describe('getFileViaResStream', () => {
68+
it('should pipe the response object through the readStream', (done) => {
69+
// mocking all of the writable methods
70+
const resMock: NodeJS.WritableStream = {
71+
end: done,
72+
write: jest.fn(),
73+
addListener: jest.fn(),
74+
emit: jest.fn(),
75+
eventNames: jest.fn(),
76+
getMaxListeners: jest.fn(),
77+
listenerCount: jest.fn(),
78+
listeners: jest.fn(),
79+
off: jest.fn(),
80+
on: jest.fn(),
81+
once: jest.fn(),
82+
prependListener: jest.fn(),
83+
prependOnceListener: jest.fn(),
84+
rawListeners: jest.fn(),
85+
removeAllListeners: jest.fn(),
86+
removeListener: jest.fn(),
87+
setMaxListeners: jest.fn(),
88+
writable: true,
89+
};
90+
// keep in mind this is the absolute **minimum** for testing, and should be expanded upon with better tests later
91+
appController.getFileViaResStream(resMock);
92+
expect(resMock.on).toBeCalled();
93+
});
94+
});
2495
});

apps/file-up-and-down-sample/src/app/app.controller.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1-
import { Controller, Get } from '@nestjs/common';
1+
import {
2+
Controller,
3+
Get,
4+
UploadedFile,
5+
Post,
6+
StreamableFile,
7+
Res,
8+
UseInterceptors,
9+
} from '@nestjs/common';
10+
import { FileInterceptor } from '@nestjs/platform-express';
11+
import { Express } from 'express';
12+
import 'multer';
213

314
import { AppService } from './app.service';
415

@@ -10,4 +21,20 @@ export class AppController {
1021
getData() {
1122
return this.appService.getData();
1223
}
24+
25+
@UseInterceptors(FileInterceptor('file'))
26+
@Post('post-file')
27+
postFile(@UploadedFile() file: Express.Multer.File) {
28+
return file.originalname;
29+
}
30+
31+
@Get('streamable-file')
32+
getStreamableFile() {
33+
return new StreamableFile(this.appService.getFileStream());
34+
}
35+
36+
// normally, `@Res()` would be a `Response` type, but I'm only interested in piping it here, so this makes the mock in the spec smaller
37+
@Get('res-stream') getFileViaResStream(@Res() res: NodeJS.WritableStream) {
38+
this.appService.getFileStream().pipe(res);
39+
}
1340
}

apps/file-up-and-down-sample/src/app/app.service.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { Test } from '@nestjs/testing';
2+
import * as fs from 'fs';
3+
import { join } from 'path';
24

35
import { AppService } from './app.service';
46

@@ -19,5 +21,17 @@ describe('AppService', () => {
1921
message: 'Welcome to file-up-and-down-sample!',
2022
});
2123
});
24+
describe('getFileStream', () => {
25+
it('should get the file information', () => {
26+
// this is passing a value to an internal constructor that isn't usually exposed. Only do this if you understand the implications
27+
// ref: https://github.com/nodejs/node/blob/master/lib/internal/fs/streams.js#L145
28+
const stream = new fs.ReadStream('path' as unknown);
29+
const crsMock = jest
30+
.spyOn(fs, 'createReadStream')
31+
.mockReturnValue(stream);
32+
expect(service.getFileStream()).toEqual(stream);
33+
expect(crsMock).toBeCalledWith(join(process.cwd(), 'package.json'));
34+
});
35+
});
2236
});
2337
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { Injectable } from '@nestjs/common';
2+
import { createReadStream } from 'fs';
3+
import { join } from 'path';
24

35
@Injectable()
46
export class AppService {
57
getData(): { message: string } {
68
return { message: 'Welcome to file-up-and-down-sample!' };
79
}
10+
11+
getFileStream() {
12+
return createReadStream(join(process.cwd(), 'package.json'));
13+
}
814
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { INestApplication } from '@nestjs/common';
2+
import { Test } from '@nestjs/testing';
3+
import { readFile } from 'fs/promises';
4+
import { request, spec } from 'pactum';
5+
import { join } from 'path/posix';
6+
7+
import { AppModule } from '../src/app/app.module';
8+
9+
describe('File Upload nad Download E2E', () => {
10+
let app: INestApplication;
11+
beforeAll(async () => {
12+
const modRef = await Test.createTestingModule({
13+
imports: [AppModule],
14+
}).compile();
15+
app = modRef.createNestApplication();
16+
app.setGlobalPrefix('api');
17+
await app.listen(0);
18+
const url = await app.getUrl();
19+
request.setBaseUrl(`${url.replace('[::1]', 'localhost')}/api`);
20+
});
21+
22+
afterAll(async () => {
23+
await app.close();
24+
});
25+
26+
describe('AppController', () => {
27+
let fileContents: string;
28+
beforeAll(async () => {
29+
fileContents = (
30+
await readFile(join(process.cwd(), 'package.json'))
31+
).toString();
32+
});
33+
it('GET /', async () => {
34+
return spec()
35+
.get('/')
36+
.expectBody({ message: 'Welcome to file-up-and-down-sample!' });
37+
});
38+
it('POST /post-file', async () => {
39+
return spec()
40+
.post('/post-file')
41+
.withFile('file', join(process.cwd(), 'package.json'))
42+
.expectBody('package.json');
43+
});
44+
it('GET /streamable-file', async () => {
45+
return spec().get('/streamable-file').expectBody(fileContents);
46+
});
47+
it('GET /res-stream', async () => {
48+
return spec().get('/res-stream').expectBody(fileContents);
49+
});
50+
});
51+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
const e2eBaseConfig = require('../../../jest.e2e');
3+
4+
module.exports = {
5+
...e2eBaseConfig,
6+
};
26.4 KB
Loading

0 commit comments

Comments
 (0)