Skip to content

Commit

Permalink
Merge pull request #589 from boostcampwm2023/BE-SeparateImageService-…
Browse files Browse the repository at this point in the history
…#579

[BE/#579]이미지 서비스 분리
  • Loading branch information
namewhat99 authored Dec 30, 2023
2 parents 70a4bc9 + 240debe commit 6bbe708
Show file tree
Hide file tree
Showing 13 changed files with 401 additions and 111 deletions.
54 changes: 16 additions & 38 deletions BE/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion BE/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"typeorm": "^0.3.17",
"uuidv4": "^6.2.13",
"uuid": "^9.0.1",
"winston": "^3.11.0",
"winston-daily-rotate-file": "^4.7.1",
"ws": "^8.14.2"
Expand Down
2 changes: 2 additions & 0 deletions BE/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ChatModule } from './chat/chat.module';
import { CacheModule } from '@nestjs/cache-manager';
import { RedisConfigProvider } from './config/redis.config';
import { ReportModule } from './report/report.module';
import { ImageModule } from './image/image.module';

@Module({
imports: [
Expand All @@ -40,6 +41,7 @@ import { ReportModule } from './report/report.module';
LoginModule,
ChatModule,
ReportModule,
ImageModule,
],
controllers: [AppController],
providers: [
Expand Down
2 changes: 1 addition & 1 deletion BE/src/common/S3Handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
PutObjectCommand,
S3Client,
} from '@aws-sdk/client-s3';
import { uuid } from 'uuidv4';
import { v4 as uuid } from 'uuid';
import { HttpException, Injectable } from '@nestjs/common';

@Injectable()
Expand Down
2 changes: 1 addition & 1 deletion BE/src/common/greenEyeHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios from 'axios';
import { uuid } from 'uuidv4';
import { v4 as uuid } from 'uuid';
import { ConfigService } from '@nestjs/config';
import { Injectable } from '@nestjs/common';

Expand Down
20 changes: 20 additions & 0 deletions BE/src/config/s3.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ConfigModule, ConfigService } from '@nestjs/config';
import { S3Client } from '@aws-sdk/client-s3';

export const S3Provider = [
{
provide: 'S3_CLIENT',
import: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
return new S3Client({
endpoint: configService.get('S3_ENDPOINT'),
region: configService.get('S3_REGION'),
credentials: {
accessKeyId: configService.get('S3_ACCESS_KEY'),
secretAccessKey: configService.get('S3_SECRET_KEY'),
},
});
},
},
];
10 changes: 10 additions & 0 deletions BE/src/image/image.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ImageService } from './image.service';
import { S3Provider } from '../config/s3.config';
import { PostImageRepository } from './postImage.repository';

@Module({
providers: [ImageService, ...S3Provider, PostImageRepository],
exports: [ImageService],
})
export class ImageModule {}
214 changes: 214 additions & 0 deletions BE/src/image/image.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ImageService } from './image.service';
import { PostImageRepository } from './postImage.repository';
import { ConfigService } from '@nestjs/config';
import { HttpException } from '@nestjs/common';
import { PostImageEntity } from '../entities/postImage.entity';
jest.mock('uuid', () => ({
v4: jest.fn(() => 'fixed-uuid-value'),
}));

const mockRepository = {
save: jest.fn(),
softDelete: jest.fn(),
findOne: jest.fn(),
};

const mockPostImageRepository = {
getRepository: jest.fn().mockReturnValue(mockRepository),
};

describe('ImageService', () => {
let service: ImageService;
let postImageRepository;
let s3ClientMock;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ImageService,
{
provide: PostImageRepository,
useValue: mockPostImageRepository,
},
{
provide: ConfigService,
useValue: { get: jest.fn((key: string) => 'mocked-value') },
},
{
provide: 'S3_CLIENT',
useValue: { send: jest.fn() },
},
],
}).compile();

service = module.get<ImageService>(ImageService);
postImageRepository = module.get<jest.Mock>(PostImageRepository);
s3ClientMock = module.get('S3_CLIENT');
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('uploadImage', function () {
const file: Express.Multer.File = {
buffer: Buffer.from('test-content'),
originalname: 'test.jpg',
mimetype: 'image/jpeg',
size: 1024,
fieldname: 'image',
encoding: '7bit',
destination: '',
filename: 'test.jpg',
path: '',
stream: {} as any,
};
it('should upload file', async function () {
s3ClientMock.send.mockReturnValue(undefined);
const res = await service.uploadImage(file);
expect(res).toEqual('mocked-value/mocked-value/fixed-uuid-value');
});

it('should fail to upload', function () {
s3ClientMock.send.mockRejectedValue(new Error('test'));
expect(async () => {
await service.uploadImage(file);
}).rejects.toThrowError(
new HttpException('업로드에 실패하였습니다.', 500),
);
});
});

describe('createPostImages', function () {
const files = [];
for (let i = 0; i < 7; i++) {
const file: Express.Multer.File = {
buffer: Buffer.from(`test-content ${i}`),
originalname: 'test.jpg',
mimetype: 'image/jpeg',
size: 1024,
fieldname: 'image',
encoding: '7bit',
destination: '',
filename: 'test.jpg',
path: '',
stream: {} as any,
};
files.push(file);
}

it('should create images', async function () {
s3ClientMock.send.mockReturnValue(undefined);
postImageRepository.getRepository().save.mockResolvedValue('test');
const res = await service.createPostImages(files, 3);
expect(res).toEqual('mocked-value/mocked-value/fixed-uuid-value');
expect(s3ClientMock.send).toHaveBeenCalledTimes(7);
expect(postImageRepository.getRepository().save).toHaveBeenCalledTimes(1);
});

it('should fail to upload images', async function () {
s3ClientMock.send.mockRejectedValue(new Error('fail to upload image'));
await expect(async () => {
await service.createPostImages(files, 3);
}).rejects.toThrowError(
new HttpException('업로드에 실패하였습니다.', 500),
);
});

it('should fail to create images', async function () {
postImageRepository.getRepository().save.mockResolvedValue('test');
postImageRepository
.getRepository()
.save.mockRejectedValue(new Error('fail to create images'));
await expect(async () => {
await service.createPostImages(files, 3);
}).rejects.toThrowError();
expect(s3ClientMock.send).toHaveBeenCalledTimes(7);
});
});

describe('removePostImages', function () {
const imageLocations = ['test1', 'test2', 'test3'];
it('should remove post images', async function () {
await service.removePostImages(imageLocations);
expect(
postImageRepository.getRepository().softDelete,
).toHaveBeenCalledTimes(1);
});

it('should fail to remove images', async function () {
postImageRepository
.getRepository()
.softDelete.mockRejectedValue(new Error('fail to remove images'));
await expect(async () => {
await service.removePostImages(imageLocations);
}).rejects.toThrowError();
});
});

describe('', function () {
const files = [];
for (let i = 0; i < 2; i++) {
const file: Express.Multer.File = {
buffer: Buffer.from(`test-content ${i}`),
originalname: 'test.jpg',
mimetype: 'image/jpeg',
size: 1024,
fieldname: 'image',
encoding: '7bit',
destination: '',
filename: 'test.jpg',
path: '',
stream: {} as any,
};
files.push(file);
}
it('should update Post Image', async function () {
const postImageEntity: PostImageEntity = {
id: 1,
post_id: 1,
image_url: 'updatedImageUrl',
delete_date: null,
post: null,
};
postImageRepository
.getRepository()
.findOne.mockResolvedValue(postImageEntity);
jest.spyOn(service, 'createPostImages').mockResolvedValue(undefined);
jest.spyOn(service, 'removePostImages').mockResolvedValue(undefined);
const deletedImages = ['image1', 'image2'];
const postId = 1;

const result = await service.updatePostImage(
files,
deletedImages,
postId,
);

expect(service.createPostImages).toHaveBeenCalledWith(files, postId);
expect(service.removePostImages).toHaveBeenCalledWith(deletedImages);
expect(postImageRepository.getRepository().findOne).toHaveBeenCalledWith({
where: { post_id: postId },
order: { id: 'ASC' },
});
expect(result).toBe('updatedImageUrl');
});

it('should return null', async function () {
jest.spyOn(service, 'createPostImages').mockResolvedValue(undefined);
jest.spyOn(service, 'removePostImages').mockResolvedValue(undefined);
postImageRepository.getRepository().findOne.mockResolvedValue(null);
const deletedImages = ['image1', 'image2']; // Replace with your actual deleted image data
const postId = 1;

const result = await service.updatePostImage(
files,
deletedImages,
postId,
);

expect(result).toBeNull();
});
});
});
Loading

0 comments on commit 6bbe708

Please sign in to comment.