Skip to content

Commit a1314b8

Browse files
committed
feat: add upload related test code
- add esm compatible code in jest with swc
1 parent 4d66062 commit a1314b8

4 files changed

Lines changed: 367 additions & 2 deletions

File tree

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@
125125
"^.+\\.(t|j)s$": [
126126
"ts-jest",
127127
"@swc/jest"
128-
]
128+
],
129+
"^.+\\.mjs$": "@swc/jest"
129130
},
130131
"moduleDirectories": [
131132
"node_modules",
@@ -141,7 +142,7 @@
141142
"coverageDirectory": "coverage",
142143
"testEnvironment": "node",
143144
"transformIgnorePatterns": [
144-
"node_modules/(?!(uuid)/)"
145+
"node_modules/(?!(uuid|graphql-upload)/)"
145146
]
146147
}
147148
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { HttpModule } from '@nestjs/axios';
2+
import { HttpStatus, INestApplication } from '@nestjs/common';
3+
import { ConfigModule } from '@nestjs/config';
4+
import { Query, Resolver } from '@nestjs/graphql';
5+
import { Test, TestingModule } from '@nestjs/testing';
6+
7+
import { S3 } from '@aws-sdk/client-s3';
8+
import { Upload } from '@aws-sdk/lib-storage';
9+
import * as request from 'supertest';
10+
11+
import { getEnvPath } from 'src/common/helper/env.helper';
12+
13+
import { UploadService } from './upload.service';
14+
15+
jest.mock('@aws-sdk/client-s3');
16+
jest.mock('@aws-sdk/lib-storage');
17+
18+
@Resolver()
19+
class DummyResolver {
20+
@Query(() => String)
21+
_dummy(): string {
22+
return 'dummy';
23+
}
24+
}
25+
26+
describe('UploadModule', () => {
27+
let app: INestApplication;
28+
let mockS3Send: jest.Mock;
29+
30+
beforeAll(async () => {
31+
mockS3Send = jest.fn();
32+
(S3 as jest.Mock).mockImplementation(() => ({
33+
send: mockS3Send,
34+
}));
35+
36+
(Upload as unknown as jest.Mock).mockImplementation(() => ({
37+
done: jest.fn().mockResolvedValue({}),
38+
}));
39+
40+
const { ApolloDriver } = await import('@nestjs/apollo');
41+
const { GraphQLModule } = await import('@nestjs/graphql');
42+
const { UploadResolver } = await import('./upload.resolver');
43+
44+
const module: TestingModule = await Test.createTestingModule({
45+
imports: [
46+
ConfigModule.forRoot({
47+
isGlobal: true,
48+
envFilePath: getEnvPath(process.cwd()),
49+
}),
50+
GraphQLModule.forRoot({
51+
driver: ApolloDriver,
52+
autoSchemaFile: true,
53+
}),
54+
HttpModule.register({}),
55+
],
56+
providers: [UploadService, UploadResolver, DummyResolver],
57+
}).compile();
58+
59+
app = module.createNestApplication();
60+
await app.init();
61+
});
62+
63+
afterAll(async () => {
64+
await app.close();
65+
});
66+
67+
afterEach(() => {
68+
jest.clearAllMocks();
69+
});
70+
71+
it('deleteFiles', async () => {
72+
const keyName = 'deleteFiles';
73+
74+
mockS3Send
75+
.mockResolvedValueOnce({ Contents: [{ Key: 'folder/file1.png' }] })
76+
.mockResolvedValueOnce({})
77+
.mockResolvedValueOnce({ Contents: [{ Key: 'folder/file2.png' }] })
78+
.mockResolvedValueOnce({});
79+
80+
const gqlQuery = {
81+
query: `
82+
mutation ($keys: [String!]!) {
83+
${keyName}(keys: $keys)
84+
}
85+
`,
86+
variables: {
87+
keys: [
88+
'https://test-bucket.s3.amazonaws.com/folder/file1.png',
89+
'https://test-bucket.s3.amazonaws.com/folder/file2.png',
90+
],
91+
},
92+
};
93+
94+
await request(app.getHttpServer())
95+
.post('/graphql')
96+
.send(gqlQuery)
97+
.set('Content-Type', 'application/json')
98+
.expect(HttpStatus.OK)
99+
.expect(({ body: { data } }) => {
100+
expect(data[keyName]).toBe(true);
101+
});
102+
});
103+
});

src/upload/upload.resolver.spec.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
3+
import type { FileUpload } from 'graphql-upload/processRequest.mjs';
4+
import { Readable } from 'stream';
5+
6+
import {
7+
MockService,
8+
MockServiceFactory,
9+
} from 'src/common/factory/mockFactory';
10+
11+
import { UploadResolver } from './upload.resolver';
12+
import { UploadService } from './upload.service';
13+
14+
describe('UploadResolver', () => {
15+
let resolver: UploadResolver;
16+
let mockedService: MockService<UploadService>;
17+
18+
const mockFileUpload: FileUpload = {
19+
filename: 'test-file.png',
20+
mimetype: 'image/png',
21+
encoding: '7bit',
22+
createReadStream: () => Readable.from(Buffer.from('test')),
23+
};
24+
25+
beforeAll(async () => {
26+
const { UploadResolver } = await import('./upload.resolver');
27+
28+
const module: TestingModule = await Test.createTestingModule({
29+
providers: [
30+
UploadResolver,
31+
{
32+
provide: UploadService,
33+
useFactory: MockServiceFactory.getMockService(UploadService),
34+
},
35+
],
36+
}).compile();
37+
38+
resolver = module.get(UploadResolver);
39+
mockedService = module.get<MockService<UploadService>>(UploadService);
40+
});
41+
42+
afterEach(() => {
43+
jest.resetAllMocks();
44+
});
45+
46+
it('Calling "uploadFile" method', async () => {
47+
const key = 'someFolderName/2024-01-01T00:00:00.000Z_test-file.png';
48+
const expectedUrl = `https://test-bucket.s3.amazonaws.com/${key}`;
49+
50+
mockedService.uploadFileToS3.mockResolvedValue({ key });
51+
mockedService.getLinkByKey.mockReturnValue(expectedUrl);
52+
53+
const result = await resolver.uploadFile(mockFileUpload);
54+
55+
expect(result).toEqual(expectedUrl);
56+
expect(mockedService.uploadFileToS3).toHaveBeenCalledWith({
57+
folderName: 'someFolderName',
58+
file: mockFileUpload,
59+
});
60+
expect(mockedService.getLinkByKey).toHaveBeenCalledWith(key);
61+
});
62+
63+
it('Calling "uploadFiles" method', async () => {
64+
const files: FileUpload[] = [mockFileUpload, mockFileUpload];
65+
const key1 = 'someFolderName/2024-01-01T00:00:00.000Z_test-file-1.png';
66+
const key2 = 'someFolderName/2024-01-01T00:00:00.000Z_test-file-2.png';
67+
const expectedUrl1 = `https://test-bucket.s3.amazonaws.com/${key1}`;
68+
const expectedUrl2 = `https://test-bucket.s3.amazonaws.com/${key2}`;
69+
70+
mockedService.uploadFileToS3
71+
.mockResolvedValueOnce({ key: key1 })
72+
.mockResolvedValueOnce({ key: key2 });
73+
mockedService.getLinkByKey
74+
.mockReturnValueOnce(expectedUrl1)
75+
.mockReturnValueOnce(expectedUrl2);
76+
77+
const result = await resolver.uploadFiles(files);
78+
79+
expect(result).toEqual([expectedUrl1, expectedUrl2]);
80+
expect(mockedService.uploadFileToS3).toHaveBeenCalledTimes(2);
81+
});
82+
83+
it('Calling "deleteFiles" method', async () => {
84+
const keys = [
85+
'https://test-bucket.s3.amazonaws.com/folder/file1.png',
86+
'https://test-bucket.s3.amazonaws.com/folder/file2.png',
87+
];
88+
89+
mockedService.deleteS3Object.mockResolvedValue({ success: true });
90+
91+
const result = await resolver.deleteFiles(keys);
92+
93+
expect(result).toBe(true);
94+
expect(mockedService.deleteS3Object).toHaveBeenCalledTimes(2);
95+
});
96+
});

src/upload/upload.service.spec.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { HttpService } from '@nestjs/axios';
2+
import { ConfigService } from '@nestjs/config';
3+
import { Test, TestingModule } from '@nestjs/testing';
4+
5+
import { S3 } from '@aws-sdk/client-s3';
6+
import { Upload } from '@aws-sdk/lib-storage';
7+
import type { FileUpload } from 'graphql-upload/processRequest.mjs';
8+
import { of } from 'rxjs';
9+
import { Readable } from 'stream';
10+
11+
import { CustomBadRequestException } from 'src/common/exceptions';
12+
13+
import { UploadService } from './upload.service';
14+
15+
jest.mock('@aws-sdk/client-s3');
16+
jest.mock('@aws-sdk/lib-storage');
17+
18+
describe('UploadService', () => {
19+
let service: UploadService;
20+
let mockS3Send: jest.Mock;
21+
let mockHttpService: { get: jest.Mock };
22+
23+
const mockConfigService = {
24+
get: jest.fn((key: string) => {
25+
const config = {
26+
AWS_S3_REGION: 'ap-northeast-2',
27+
AWS_S3_ACCESS_KEY: 'test-access-key',
28+
AWS_S3_SECRET_KEY: 'test-secret-key',
29+
AWS_S3_BUCKET_NAME: 'test-bucket',
30+
};
31+
return config[key];
32+
}),
33+
};
34+
35+
const mockFileUpload: FileUpload = {
36+
filename: 'test-file.png',
37+
mimetype: 'image/png',
38+
encoding: '7bit',
39+
createReadStream: () => Readable.from(Buffer.from('test')),
40+
};
41+
42+
beforeAll(async () => {
43+
mockS3Send = jest.fn();
44+
(S3 as jest.Mock).mockImplementation(() => ({
45+
send: mockS3Send,
46+
}));
47+
48+
mockHttpService = {
49+
get: jest.fn(),
50+
};
51+
52+
const module: TestingModule = await Test.createTestingModule({
53+
providers: [
54+
UploadService,
55+
{
56+
provide: ConfigService,
57+
useValue: mockConfigService,
58+
},
59+
{
60+
provide: HttpService,
61+
useValue: mockHttpService,
62+
},
63+
],
64+
}).compile();
65+
66+
service = module.get<UploadService>(UploadService);
67+
});
68+
69+
afterEach(() => {
70+
jest.clearAllMocks();
71+
});
72+
73+
describe('getLinkByKey', () => {
74+
it('should return correct S3 URL', () => {
75+
const key = 'folder/test-file.png';
76+
const result = service.getLinkByKey(key);
77+
78+
expect(result).toBe(
79+
'https://test-bucket.s3.amazonaws.com/folder/test-file.png',
80+
);
81+
});
82+
});
83+
84+
describe('uploadFileToS3', () => {
85+
it('should upload file successfully', async () => {
86+
const mockUploadDone = jest.fn().mockResolvedValue({});
87+
(Upload as unknown as jest.Mock).mockImplementation(() => ({
88+
done: mockUploadDone,
89+
}));
90+
91+
const result = await service.uploadFileToS3({
92+
folderName: 'testFolder',
93+
file: mockFileUpload,
94+
});
95+
96+
expect(result).toHaveProperty('key');
97+
expect(result.key).toContain('testFolder/');
98+
expect(result.key).toContain('test-file.png');
99+
expect(mockUploadDone).toHaveBeenCalled();
100+
});
101+
102+
it('should throw CustomBadRequestException on upload failure', async () => {
103+
(Upload as unknown as jest.Mock).mockImplementation(() => ({
104+
done: jest.fn().mockRejectedValue(new Error('Upload failed')),
105+
}));
106+
107+
await expect(
108+
service.uploadFileToS3({
109+
folderName: 'testFolder',
110+
file: mockFileUpload,
111+
}),
112+
).rejects.toThrow(CustomBadRequestException);
113+
});
114+
});
115+
116+
describe('deleteS3Object', () => {
117+
it('should delete file successfully', async () => {
118+
mockS3Send
119+
.mockResolvedValueOnce({ Contents: [{ Key: 'folder/file.png' }] })
120+
.mockResolvedValueOnce({});
121+
122+
const result = await service.deleteS3Object('folder/file.png');
123+
124+
expect(result).toEqual({ success: true });
125+
expect(mockS3Send).toHaveBeenCalledTimes(2);
126+
});
127+
128+
it('should throw CustomBadRequestException when file does not exist', async () => {
129+
mockS3Send.mockResolvedValueOnce({ Contents: [] });
130+
131+
await expect(
132+
service.deleteS3Object('nonexistent/file.png'),
133+
).rejects.toThrow(CustomBadRequestException);
134+
});
135+
136+
it('should throw CustomBadRequestException on delete failure', async () => {
137+
mockS3Send
138+
.mockResolvedValueOnce({ Contents: [{ Key: 'folder/file.png' }] })
139+
.mockRejectedValueOnce(new Error('Delete failed'));
140+
141+
await expect(service.deleteS3Object('folder/file.png')).rejects.toThrow(
142+
CustomBadRequestException,
143+
);
144+
});
145+
});
146+
147+
describe('listS3Object', () => {
148+
it('should list and fetch S3 objects', async () => {
149+
const mockContents = [
150+
{ Key: 'folder/file1.png' },
151+
{ Key: 'folder/file2.png' },
152+
];
153+
154+
mockS3Send.mockResolvedValueOnce({ Contents: mockContents });
155+
mockHttpService.get
156+
.mockReturnValueOnce(of({ data: 'file1-content' }))
157+
.mockReturnValueOnce(of({ data: 'file2-content' }));
158+
159+
const result = await service.listS3Object('folder');
160+
161+
expect(result).toEqual(['file1-content', 'file2-content']);
162+
expect(mockHttpService.get).toHaveBeenCalledTimes(2);
163+
});
164+
});
165+
});

0 commit comments

Comments
 (0)