If you are using NestJS, it can be easier than you think.
File Upload Module:
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { S3 } from 'aws-sdk';
import { FileUploadController } from './file-upload.controller';
import { FileUploadRepository } from './file-upload.repository';
import { FileUploadService } from './file-upload.service';
@Module({
imports: [TypeOrmModule.forFeature([FileUploadRepository])],
controllers: [FileUploadController],
providers: [
FileUploadService,
{
provide: S3,
useFactory: (configService: ConfigService) =>
new S3({
accessKeyId: configService.get('AWS_ACCESS_KEY'),
secretAccessKey: configService.get('AWS_ACCESS_SECRET'),
region: configService.get('AWS_REGION'),
}),
},
],
})
export class FileUploadModule {}
The service itself:
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { S3 } from 'aws-sdk';
import { v4 as uuid } from 'uuid';
import { FileUpload } from './file-upload.entity';
import { FileUploadRepository } from './file-upload.repository';
@Injectable()
export class FileUploadService {
constructor(
@InjectRepository(FileUploadRepository)
private readonly publicFileRepository: FileUploadRepository,
private readonly configService: ConfigService,
private readonly s3: S3,
) {}
async uploadPublicFile(
dataBuffer: Buffer,
filename: string,
): Promise<FileUpload> {
const uploadResult = await this.s3
.upload({
Bucket: this.configService.get('AWS_BUCKET_NAME'),
Body: dataBuffer,
Key: `${uuid()}-${filename}`,
})
.promise();
const createdFile = this.publicFileRepository.create({
key: uploadResult.Key,
url: uploadResult.Location,
});
await this.publicFileRepository.save(createdFile);
return createdFile;
}
async deletePublicFile(
publicFileId: string,
publicFileKey: string,
): Promise<FileUpload> {
const response = await this.s3
.deleteObject({
Bucket: this.configService.get('AWS_BUCKET_NAME'),
Key: publicFileKey,
})
.promise();
if (!response) {
throw new InternalServerErrorException(
`Could not delete file ${publicFileKey}`,
);
}
const { raw: deletedItem } = await this.publicFileRepository.delete(
publicFileId,
);
return deletedItem;
}
}
and then finally the test:
import { InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { S3 } from 'aws-sdk';
import { mockFileUpload } from './file-upload.mock';
import { FileUploadRepository } from './file-upload.repository';
import { FileUploadService } from './file-upload.service';
export const mockFileUploadRepository = () => ({
create: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
});
const mS3Instance = {
upload: jest.fn().mockReturnThis(),
promise: jest.fn(),
deleteObject: jest.fn().mockReturnThis(),
};
describe('FileUploadService', () => {
let service: FileUploadService;
let repository;
let s3Service;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
FileUploadService,
{
provide: FileUploadRepository,
useFactory: mockFileUploadRepository,
},
{
provide: ConfigService,
useValue: {
get: jest.fn(),
},
},
{
provide: S3,
useFactory: () => mS3Instance,
},
],
}).compile();
service = module.get<FileUploadService>(FileUploadService);
repository = module.get<FileUploadRepository>(FileUploadRepository);
repository = module.get<FileUploadRepository>(FileUploadRepository);
s3Service = module.get<S3>(S3);
});
describe('FileUploadService.uploadPublicFile', () => {
it('should create public file and throw no error', async () => {
repository.create.mockResolvedValue(mockFileUpload);
repository.save.mockResolvedValue(mockFileUpload);
s3Service.promise = jest.fn().mockResolvedValue({
Key: 'some-key',
Location: 'some-location',
});
const file = Buffer.alloc(513, '0');
const response = await service.uploadPublicFile(file, 'somefilename');
expect(response).toBeDefined();
});
});
describe('FileUploadService.deletePublicFile', () => {
it('should delete public file and throw no error', async () => {
repository.delete.mockResolvedValue({ raw: mockFileUpload });
repository.save.mockResolvedValue(mockFileUpload);
s3Service.promise = jest.fn().mockResolvedValue({
Key: 'some-key',
Location: 'some-location',
});
const response = await service.deletePublicFile('someid', 'somefilename');
expect(response).toBeDefined();
});
it('should not delete public file and throw InternalServerErrorException', async () => {
repository.delete.mockResolvedValue({ raw: mockFileUpload });
repository.save.mockResolvedValue(mockFileUpload);
s3Service.promise = jest.fn().mockResolvedValue(null);
const promise = service.deletePublicFile('someid', 'somefilename');
expect(promise).rejects.toThrow(InternalServerErrorException);
});
});
});