From ca0f2416f9a6347c302c8f00163fe2aac93af79a Mon Sep 17 00:00:00 2001 From: Jjoobob123 <273hur4747@gmail.com> Date: Fri, 31 Mar 2023 11:00:22 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20user=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=B4=88=EA=B8=B0=ED=99=94=20API=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 초기화 API 기능 추가 --- src/apis/auth/auth.service.ts | 15 ++++++++------- src/apis/auth/strategies/jwt-refresh.strategy.ts | 3 +-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/apis/auth/auth.service.ts b/src/apis/auth/auth.service.ts index 731efd7..4eff5da 100644 --- a/src/apis/auth/auth.service.ts +++ b/src/apis/auth/auth.service.ts @@ -101,7 +101,7 @@ export class AuthService { getAccessToken({ user }: IAuthServiceGetAccessToken): string { return this.jwtService.sign( { sub: user.id, email: user.email }, //ƒ - { secret: process.env.JWT_ACCESS_KEY, expiresIn: '2w' }, + { secret: process.env.JWT_ACCESS_KEY, expiresIn: '10h' }, ); } @@ -110,9 +110,9 @@ export class AuthService { { sub: user.id, email: user.email }, // { secret: process.env.JWT_REFRESH_KEY, expiresIn: '2w' }, ); - + console.log('🐳🐳🐳🐳🐳', refreshToken); // 개발 환경 - // res.setHeader('set-Cookie', `refreshToken=${refreshToken}; path=/;`); + // res.setHeader('Set-Cookie', `refreshToken=${refreshToken}; path=/;`); // 배포 환경 ============== 배포 하기 전까지 잠시 주석 ============= @@ -120,7 +120,7 @@ export class AuthService { 'http://localhost:3000', 'http://groomeong.store', // 프론트 도메인 주소?? 'https://groomeong.store', // 프론트 도메인 주소?? - 'https://www.groomeong.shop/graphql', + 'https://groomeong.shop', // ssl 된 주소 https:// ..... ]; const origin = req.headers.origin; @@ -129,6 +129,7 @@ export class AuthService { res.setHeader('Access-Control-Allow-Origin', origin); } + // res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000'); // 프런트엔드 js 코드에 대한 응답을 노출할지 여부를 브라우저에 알려준다. res.setHeader('Access-Control-Allow-Credentials', 'true'); // 리소스에 엑세스할 때 허용되는 하나 이상의 메서드를 지정해준다. @@ -140,13 +141,13 @@ export class AuthService { // X-Custom-Header => 서버에 대한 cors 요청에 의해 지원 // Upgrade-Insecure-Requests => 여러 헤더에 대한 지원을 지정 res.setHeader( - 'Access-Control-Allow-Headers', // - 'Access-Control-Allow-Headers, Origin, Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers', + 'Access-Control-Allow-Headers', + 'Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers', ); res.setHeader( 'Set-Cookie', - `refreshToken=${refreshToken}; path=/; domain=www.groomeong.shop ; Secure; httpOnly; SameSite=None;`, + `refreshToken=${refreshToken}; path=/; domain=.groomeong.shop; Secure; httpOnly; SameSite=None;`, ); } diff --git a/src/apis/auth/strategies/jwt-refresh.strategy.ts b/src/apis/auth/strategies/jwt-refresh.strategy.ts index 50f5a00..331ec56 100644 --- a/src/apis/auth/strategies/jwt-refresh.strategy.ts +++ b/src/apis/auth/strategies/jwt-refresh.strategy.ts @@ -11,8 +11,7 @@ export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'refresh') { super({ jwtFromRequest: (req) => { const cookie = req.headers.cookie; - const refreshToken = cookie.replace('refreshToken=', ''); - return refreshToken; + if (cookie) return cookie.replace('refreshToken=', ''); }, secretOrKey: process.env.JWT_REFRESH_KEY, passReqToCallback: true, From 07cc15f833b87e5631e4f24cd1a4e2fb96eb655f Mon Sep 17 00:00:00 2001 From: cabbage556 Date: Fri, 31 Mar 2023 11:58:30 +0900 Subject: [PATCH 2/7] =?UTF-8?q?test:=20fetchUserDogs=20API=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit context 생성 후 mocking 유저 id 할당 feature-#137 --- src/apis/dogs/__test__/dogs.resolver.spec.ts | 64 ++++++++++---------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/src/apis/dogs/__test__/dogs.resolver.spec.ts b/src/apis/dogs/__test__/dogs.resolver.spec.ts index 94f2909..6a3fea4 100644 --- a/src/apis/dogs/__test__/dogs.resolver.spec.ts +++ b/src/apis/dogs/__test__/dogs.resolver.spec.ts @@ -1,13 +1,12 @@ -import { - CanActivate, - ExecutionContext, - NotFoundException, -} from '@nestjs/common'; +import { CanActivate, NotFoundException } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { DogsResolver } from '../dogs.resolver'; import { DogsService } from '../dogs.service'; import { DOG_TYPE } from '../enum/dog-type.enum'; import { GqlAuthGuard } from '../../auth/guards/gql-auth.guard'; +import * as httpMocks from 'node-mocks-http'; +import { IAuthUser, IContext } from 'src/commons/interface/context'; +import { User } from 'src/apis/users/entities/user.entity'; const MOCK_USER = { id: 'c84fa63e-7a05-4cd5-b015-d4db9a262b18', @@ -36,28 +35,25 @@ const MOCK_DOG = { userId: 'c84fa63e-7a05-4cd5-b015-d4db9a262b18', }; -// mock 서비스 만들기 -const mockDogsService = { - // mock 서비스 로직 만들기 - findOneById: jest.fn().mockImplementation((id: string) => { - return MOCK_DOG; - }), -}; - -const mockAuthGuard: CanActivate = { - canActivate(context: ExecutionContext) { - const request = context.switchToHttp().getRequest(); - request.user = { - email: 'liberty556@gmail.com', - id: 'c84fa63e-7a05-4cd5-b015-d4db9a262b18', - }; - return request.user; - }, -}; - describe('DogsResolver', () => { let dogsResolver: DogsResolver; + const mockAuthGuard: CanActivate = { + canActivate: jest.fn().mockImplementation(() => true), + }; + + const context: IContext = { + req: httpMocks.createRequest(), + res: httpMocks.createResponse(), + }; + context.req.user = new User(); + context.req.user.id = MOCK_USER.id; + + const mockDogsService = { + findOneById: jest.fn().mockImplementation((id: string) => MOCK_DOG), + findByUserId: jest.fn().mockImplementation((userId: string) => [MOCK_DOG]), + }; + beforeEach(async () => { const dogsModule = await Test.createTestingModule({ providers: [ @@ -66,11 +62,12 @@ describe('DogsResolver', () => { provide: DogsService, useValue: mockDogsService, }, + { + provide: GqlAuthGuard, + useValue: mockAuthGuard, + }, ], - }) - .overrideGuard(GqlAuthGuard) - .useValue(mockAuthGuard) - .compile(); + }).compile(); dogsResolver = dogsModule.get(DogsResolver); }); @@ -79,10 +76,9 @@ describe('DogsResolver', () => { expect(dogsResolver).toBeDefined(); }); - describe('fetchDog API', () => { + describe('fetchDog', () => { it('강아지 정보를 리턴해야 함', () => { - const validMockId = MOCK_DOG.id; - expect(dogsResolver.fetchDog(validMockId)).toEqual({ + expect(dogsResolver.fetchDog(MOCK_DOG.id)).toEqual({ ...MOCK_DOG, }); }); @@ -96,4 +92,10 @@ describe('DogsResolver', () => { } }); }); + + describe('fetchUserDogs', () => { + it('유저의 강아지 정보를 배열로 리턴해야 함', () => { + expect(dogsResolver.fetchUserDogs(context)).toEqual([{ ...MOCK_DOG }]); + }); + }); }); From 83f2f590b44b1c3ec6f0afa3ddc4a9a426c6adf0 Mon Sep 17 00:00:00 2001 From: cabbage556 Date: Fri, 31 Mar 2023 12:55:46 +0900 Subject: [PATCH 3/7] =?UTF-8?q?test:=20createDog,=20updateDog,=20deleteDog?= =?UTF-8?q?=20API=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test-#145 --- src/apis/dogs/__test__/dogs.resolver.spec.ts | 102 ++++++++++++++++++- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/src/apis/dogs/__test__/dogs.resolver.spec.ts b/src/apis/dogs/__test__/dogs.resolver.spec.ts index 6a3fea4..f38bf54 100644 --- a/src/apis/dogs/__test__/dogs.resolver.spec.ts +++ b/src/apis/dogs/__test__/dogs.resolver.spec.ts @@ -5,8 +5,10 @@ import { DogsService } from '../dogs.service'; import { DOG_TYPE } from '../enum/dog-type.enum'; import { GqlAuthGuard } from '../../auth/guards/gql-auth.guard'; import * as httpMocks from 'node-mocks-http'; -import { IAuthUser, IContext } from 'src/commons/interface/context'; +import { IContext } from 'src/commons/interface/context'; import { User } from 'src/apis/users/entities/user.entity'; +import { CreateDogInput } from '../dto/create-dog.input'; +import { UpdateDogInput } from '../dto/update-dog.input'; const MOCK_USER = { id: 'c84fa63e-7a05-4cd5-b015-d4db9a262b18', @@ -35,9 +37,24 @@ const MOCK_DOG = { userId: 'c84fa63e-7a05-4cd5-b015-d4db9a262b18', }; +const UPDATED_MOCK_DOG = { + id: '3ce6246c-f37a-426e-b95a-b38ec6d55f4e', + name: '댕댕이', + age: 5, + weight: 10.5, + breed: DOG_TYPE.LARGE, + specifics: '성격이 착해요', + image: + 'https://storage.cloud.google.com/groomeong-storage/origin/dog/a6c16f50-2946-4dfb-9785-a782cea6c570/%03b%EF%BF%BD2.jpeg', + createdAt: '2023-03-21 12:13:02.011088', + deletedAt: null, + userId: 'c84fa63e-7a05-4cd5-b015-d4db9a262b18', +}; + describe('DogsResolver', () => { let dogsResolver: DogsResolver; + // Guard 통과 가정 const mockAuthGuard: CanActivate = { canActivate: jest.fn().mockImplementation(() => true), }; @@ -50,8 +67,25 @@ describe('DogsResolver', () => { context.req.user.id = MOCK_USER.id; const mockDogsService = { - findOneById: jest.fn().mockImplementation((id: string) => MOCK_DOG), - findByUserId: jest.fn().mockImplementation((userId: string) => [MOCK_DOG]), + findOneById: jest + .fn() // + .mockImplementation((id: string) => MOCK_DOG), + findByUserId: jest + .fn() // + .mockImplementation((userId: string) => [MOCK_DOG]), + create: jest + .fn() // + .mockImplementation( + (createDogInput: CreateDogInput, userId: string) => MOCK_DOG, + ), + updateOneById: jest + .fn() // + .mockImplementation( + (id: string, updateDogInput: UpdateDogInput) => UPDATED_MOCK_DOG, + ), + deleteOneById: jest + .fn() // + .mockImplementation((id: string, userId: string) => true), }; beforeEach(async () => { @@ -84,9 +118,9 @@ describe('DogsResolver', () => { }); it('NotFoundException을 던져야 함', () => { - const nonValidMockId = '3ce6246c-f37a-426e-b95a-b38ec6d55f4f'; + const inValidMockId = '3ce6246c-f37a-426e-b95a-b38ec6d55f4f'; try { - dogsResolver.fetchDog(nonValidMockId); + dogsResolver.fetchDog(inValidMockId); } catch (error) { expect(error).toBeInstanceOf(NotFoundException); } @@ -98,4 +132,62 @@ describe('DogsResolver', () => { expect(dogsResolver.fetchUserDogs(context)).toEqual([{ ...MOCK_DOG }]); }); }); + + describe('createDog', () => { + it('생성한 강아지 정보를 리턴해야 함', () => { + const createDogInput: CreateDogInput = { + name: MOCK_DOG.name, + age: MOCK_DOG.age, + weight: MOCK_DOG.weight, + breed: MOCK_DOG.breed, + specifics: MOCK_DOG.specifics, + image: MOCK_DOG.image, + }; + + expect(dogsResolver.createDog(createDogInput, context)).toEqual({ + ...MOCK_DOG, + }); + }); + }); + + describe('updateDog', () => { + it('업데이트한 강아지 정보를 리턴해야 함', () => { + const updateDogInput: UpdateDogInput = { + weight: UPDATED_MOCK_DOG.weight, + breed: UPDATED_MOCK_DOG.breed, + }; + + expect(dogsResolver.updateDog(MOCK_DOG.id, updateDogInput)).toEqual({ + ...UPDATED_MOCK_DOG, + }); + }); + + it('NotFoundException을 던져야 함', () => { + const inValidMockId = '3ce6246c-f37a-426e-b95a-b38ec6d55f4f'; + const updateDogInput: UpdateDogInput = { + weight: UPDATED_MOCK_DOG.weight, + breed: UPDATED_MOCK_DOG.breed, + }; + try { + dogsResolver.updateDog(inValidMockId, updateDogInput); + } catch (error) { + expect(error).toBeInstanceOf(NotFoundException); + } + }); + }); + + describe('deleteDog', () => { + it('삭제 여부 true를 반환해야 함', () => { + expect(dogsResolver.deleteDog(MOCK_DOG.id, context)).toBe(true); + }); + + it('NotFoundException을 던져야 함', () => { + const inValidMockId = '3ce6246c-f37a-426e-b95a-b38ec6d55f4f'; + try { + dogsResolver.deleteDog(inValidMockId, context); + } catch (error) { + expect(error).toBeInstanceOf(NotFoundException); + } + }); + }); }); From dfc69f10bf5b37275de910589292da72ff4311a4 Mon Sep 17 00:00:00 2001 From: YR8002 <120006167+YR8002@users.noreply.github.com> Date: Fri, 31 Mar 2023 13:42:37 +0900 Subject: [PATCH 4/7] =?UTF-8?q?test:=20auth.service=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=ED=8C=8C=EC=9D=BC=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/auth/__test__/auth.service.spec.ts | 140 ++++++++++++------ .../reviews/__test__/reviews.moking.dummy.ts | 4 + .../reviews/__test__/reviews.service.spec.ts | 87 +++++++++++ 3 files changed, 184 insertions(+), 47 deletions(-) create mode 100644 src/apis/reviews/__test__/reviews.moking.dummy.ts create mode 100644 src/apis/reviews/__test__/reviews.service.spec.ts diff --git a/src/apis/auth/__test__/auth.service.spec.ts b/src/apis/auth/__test__/auth.service.spec.ts index 95d1a91..217ab80 100644 --- a/src/apis/auth/__test__/auth.service.spec.ts +++ b/src/apis/auth/__test__/auth.service.spec.ts @@ -1,6 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { CACHE_MANAGER, UnprocessableEntityException } from '@nestjs/common'; +import { + CACHE_MANAGER, + UnauthorizedException, + UnprocessableEntityException, +} from '@nestjs/common'; import { Cache } from 'cache-manager'; import { UsersService } from 'src/apis/users/user.service'; import { User } from 'src/apis/users/entities/user.entity'; @@ -8,8 +11,6 @@ import { AuthService } from '../auth.service'; import { IContext } from 'src/commons/interface/context'; import { JwtService } from '@nestjs/jwt'; import * as httpMocks from 'node-mocks-http'; -import { MockUsersRepository } from './auth.mocking.dummy'; -import { MailerModule } from '@nestjs-modules/mailer'; jest.mock('../auth.service'); @@ -20,9 +21,9 @@ const EXAMPLE_USER: User = { password: 'exampleUserPassword', phone: 'exampleUserPhone', image: 'exampleUserImage', - createAt: new Date(), - deleteAt: new Date(), - updateAt: new Date(), + createdAt: new Date(), + deletedAt: new Date(), + updatedAt: new Date(), dogs: [null], reservation: [null], }; @@ -30,35 +31,41 @@ const EXAMPLE_USER: User = { describe('AuthResolver', () => { let authService: AuthService; let usersService: UsersService; - let jwtService: JwtService; - let cache: Cache; - let context: IContext; + let jwt: JwtService; // Fix here + let cacheManager: Cache; + + const context: IContext = { + req: httpMocks.createRequest(), + res: httpMocks.createResponse(), + }; + context.req.user = new User(); + context.req.user.id = EXAMPLE_USER.id; + const email = EXAMPLE_USER.email; + const password = EXAMPLE_USER.password; + const process = { + env: { + JWT_ACCESS_KEY: 'exampleAccess', + JWT_REFRESH_KEY: 'exampleRefresh', + }, + }; beforeEach(async () => { jest.clearAllMocks(); - const usersModule: TestingModule = await Test.createTestingModule({ - imports: [ - MailerModule.forRootAsync({ - useFactory: () => ({ - transport: { - service: 'Gmail', - host: process.env.EMAIL_HOST, - port: Number(process.env.DATABASE_PORT), - secure: false, - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASS, - }, - }, - }), - }), - ], + const modules: TestingModule = await Test.createTestingModule({ providers: [ AuthService, - UsersService, { - provide: getRepositoryToken(User), - useClass: MockUsersRepository, + provide: UsersService, + useValue: { + findOneByEmail: jest.fn(() => EXAMPLE_USER), + create: jest.fn(() => EXAMPLE_USER), + }, + }, + { + provide: JwtService, + useValue: { + verify: jest.fn(() => 'EXAMPLE_TOKEN'), + }, }, { provide: CACHE_MANAGER, @@ -69,22 +76,13 @@ describe('AuthResolver', () => { }, ], }).compile(); - const authModule: TestingModule = await Test.createTestingModule({ - providers: [AuthService], - }).compile(); - usersService = usersModule.get(UsersService); - authService = authModule.get(AuthService); - cache = usersModule.get(CACHE_MANAGER); - context = { - req: httpMocks.createRequest(), - res: httpMocks.createResponse(), - }; + authService = modules.get(AuthService); + usersService = modules.get(UsersService); + jwt = modules.get(JwtService); + cacheManager = modules.get(CACHE_MANAGER); }); - const email = EXAMPLE_USER.email; - const password = EXAMPLE_USER.password; - describe('login', () => { it('의존성주입한 usersService 에서 email로 찾아오기', async () => { jest @@ -129,10 +127,58 @@ describe('AuthResolver', () => { }); describe('logout', () => { - // - }); + const req = context.req; + it('토큰 증명해서 로그아웃 인가', async () => { + const req = { + headers: { + authorization: 'Bearer ACCESS_TOKEN', + cookie: 'refreshToken=REFRESH_TOKEN', + }, + }; - describe('restoreAccessToken', () => { - // + try { + const accessToken = await req.headers['authorization'].replace( + 'Bearer ', + '', + ); + const refreshToken = await req.headers['cookie'].split( + 'refreshToken=', + )[1]; + + // accessToken 토큰 + const jwtAccessKey = jwt.verify(accessToken); + + // refresh 토큰 + const jwtRefreshKey = jwt.verify(refreshToken); + + await cacheManager.set(`accessToken:${accessToken}`, 'accessToken', { + ttl: jwtAccessKey['exp'] - jwtAccessKey['iat'], + }); + + await cacheManager.set(`refreshToken:${refreshToken}`, 'refreshToken', { + ttl: jwtRefreshKey['exp'] - jwtRefreshKey['iat'], + }); + + expect(jwt.verify).toBeCalled(); + + return '로그아웃에 성공했습니다.'; + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error.message).toBe('로그아웃을 실패했습니다.'); + throw new UnauthorizedException('로그아웃을 실패했습니다.'); + } + }); + + describe('restoreAccessToken', () => { + const user = EXAMPLE_USER; + + beforeEach(async () => { + await authService.getAccessToken({ user }); + }); + + it('getAccessToken should be called', () => { + expect(authService.getAccessToken).toBeCalled(); + }); + }); }); }); diff --git a/src/apis/reviews/__test__/reviews.moking.dummy.ts b/src/apis/reviews/__test__/reviews.moking.dummy.ts new file mode 100644 index 0000000..713bf9d --- /dev/null +++ b/src/apis/reviews/__test__/reviews.moking.dummy.ts @@ -0,0 +1,4 @@ +import { Repository } from 'typeorm'; +import { Review } from '../entities/review.entity'; + +export class diff --git a/src/apis/reviews/__test__/reviews.service.spec.ts b/src/apis/reviews/__test__/reviews.service.spec.ts new file mode 100644 index 0000000..1ae83ca --- /dev/null +++ b/src/apis/reviews/__test__/reviews.service.spec.ts @@ -0,0 +1,87 @@ +import { UnprocessableEntityException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Review } from '../entities/review.entity'; +import { ReviewsService } from '../reviews.service'; + +jest.mock('../reviews.service'); +const EXAMPLE_REVIEW = { + id: 'EXAMPLE_REVIEW_ID', + contents: 'EXAMPLE_REVIEW_CONTENTS', + createdAt: new Date(), + star: 5, + reservation: { id: '33bcbf41-884b-46f2-96a2-f3947a1ca906' }, + shop: { id: '500d75e0-0223-4046-be13-55887bfbf6f0' }, +}; + +const MockReviewsRepository = { + find: jest.fn((where, skip, take, order, relations) => { + EXAMPLE_REVIEW; + }), + findOne: jest.fn((where, relations) => { + EXAMPLE_REVIEW; + }), + save: jest.fn(({ contents, star, reservation, shop }) => { + EXAMPLE_REVIEW; + }), +}; + +describe('ReviewsService', () => { + let reviewsService: ReviewsService; + let mockReviewsRepository: Repository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ReviewsService, + { + provide: getRepositoryToken(Review), + useClass: Repository, + }, + ], + }).compile(); + + reviewsService = module.get(ReviewsService); + mockReviewsRepository = module.get>( + getRepositoryToken(Review), + ); + }); + + const reviewId = EXAMPLE_REVIEW.id; + + describe('find', () => { + it('reviewsRepository의 findOne을 실행하고 값이 없으면 error 반환해야함', async () => { + jest.spyOn(mockReviewsRepository, 'findOne').mockResolvedValue(undefined); + expect(mockReviewsRepository.findOne).not.toBeCalled(); + + try { + await reviewsService.find({ reviewId }); + } catch (error) { + expect(error).toBeInstanceOf(UnprocessableEntityException); + expect(error.message).toEqual('아이디를 찾을 수 없습니다'); + } + + expect(mockReviewsRepository.findOne).toBeCalledWith({ + where: { id: reviewId }, + relations: ['shop', 'reservation'], + }); + }); + }); + + describe('findByShopIdWithPage', () => { + it('', () => {}); + }); + + describe('create', () => { + it('', async () => {}); + }); + + describe('averageStar', () => { + it('', async () => {}); + }); + + describe('checkReviewAuth', () => { + it('', async () => {}); + }); +}); From 1421106806522d650b3e881be0c2529a311123b5 Mon Sep 17 00:00:00 2001 From: Jjoobob123 <273hur4747@gmail.com> Date: Fri, 31 Mar 2023 13:55:28 +0900 Subject: [PATCH 5/7] =?UTF-8?q?build:=20access/refresh=20=ED=94=84?= =?UTF-8?q?=EB=A1=A0=ED=8A=B8=20=EB=B0=8F=20=EB=B0=B1=EC=97=94=EB=93=9C=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 배포환경 수정 및 로그아웃시 refresh 토큰 삭제 기능 추가 --- src/apis/auth/auth.service.ts | 49 +++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/apis/auth/auth.service.ts b/src/apis/auth/auth.service.ts index a9bd531..c4f4c11 100644 --- a/src/apis/auth/auth.service.ts +++ b/src/apis/auth/auth.service.ts @@ -79,6 +79,48 @@ export class AuthService { }, ); + // 개발 환경 + // res.setHeader('Authorization', ''); // Authorization 헤더 값을 빈 문자열로 설정합니다. + // res.clearCookie('refreshToken'); // refreshToken 쿠키를 삭제합니다. + + // 배포 환경 + const originList = [ + 'http://localhost:3000', + 'http://127.0.0.1:3000', + 'http://34.64.53.80:3000', + 'https://groomeong.shop', + 'https://groomeong.store', + ]; + const origin = req.headers.origin; + if (originList.includes(origin)) { + // 리소스에 엑세스하기 위해 코드 요청을 허용하도록 브라우저에 알리는 응답 + res.setHeader('Access-Control-Allow-Origin', origin); + } + + // 프런트엔드 js 코드에 대한 응답을 노출할지 여부를 브라우저에 알려준다. + res.setHeader('Access-Control-Allow-Credentials', 'true'); + // 리소스에 엑세스할 때 허용되는 하나 이상의 메서드를 지정해준다. + res.setHeader( + 'Access-Control-Allow-Methods', // + 'GET, HEAD, OPTIONS, POST, PUT', + ); + // 실제 요청 중에 사용할 수 있는 HTTP 헤더를 나타내는 실행 전 요청에 대한 응답. + // X-Custom-Header => 서버에 대한 cors 요청에 의해 지원 + // Upgrade-Insecure-Requests => 여러 헤더에 대한 지원을 지정 + res.setHeader( + 'Access-Control-Allow-Headers', + 'Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers', + ); + + res.clearCookie('refreshToken', { + path: '/', + domain: '.groomeong.shop', + secure: true, + httpOnly: true, + sameSite: 'none', + maxAge: 0, + }); + return '로그아웃 성공'; } catch (err) { throw new UnauthorizedException('로그아웃 실패'); @@ -102,7 +144,11 @@ export class AuthService { { secret: process.env.JWT_REFRESH_KEY, expiresIn: '2w' }, ); console.log('🐳🐳🐳🐳🐳', refreshToken); - + + // 로컬(개발환경) + // res.setHeader('set-Cookie', `refreshToken=${refreshToken}; path=/;`); + + // 배포 환경 const originList = [ 'http://localhost:3000', 'http://127.0.0.1:3000', @@ -116,7 +162,6 @@ export class AuthService { res.setHeader('Access-Control-Allow-Origin', origin); } - // res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000'); // 프런트엔드 js 코드에 대한 응답을 노출할지 여부를 브라우저에 알려준다. res.setHeader('Access-Control-Allow-Credentials', 'true'); // 리소스에 엑세스할 때 허용되는 하나 이상의 메서드를 지정해준다. From e3330ccd55316a8c79789517a705df3fc69b8e4c Mon Sep 17 00:00:00 2001 From: YR8002 <120006167+YR8002@users.noreply.github.com> Date: Fri, 31 Mar 2023 13:57:55 +0900 Subject: [PATCH 6/7] =?UTF-8?q?test:=20review=20service=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9E=90=EC=9E=98=ED=95=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/auth/__test__/auth.resolver.spec.ts | 6 +- src/apis/auth/__test__/auth.service.spec.ts | 2 +- .../reviews/__test__/reviews.moking.dummy.ts | 4 -- .../reviews/__test__/reviews.service.spec.ts | 59 +++++++++++++------ .../__test__/shopImage.service.spec.ts | 7 ++- 5 files changed, 51 insertions(+), 27 deletions(-) delete mode 100644 src/apis/reviews/__test__/reviews.moking.dummy.ts diff --git a/src/apis/auth/__test__/auth.resolver.spec.ts b/src/apis/auth/__test__/auth.resolver.spec.ts index 38ddc8c..4233021 100644 --- a/src/apis/auth/__test__/auth.resolver.spec.ts +++ b/src/apis/auth/__test__/auth.resolver.spec.ts @@ -15,9 +15,9 @@ const EXAMPLE_USER: User = { password: 'exampleUserPassword', phone: 'exampleUserPhone', image: 'exampleUserImage', - createAt: new Date(), - deleteAt: new Date(), - updateAt: new Date(), + createdAt: new Date(), + deletedAt: new Date(), + updatedAt: new Date(), dogs: [null], reservation: [null], }; diff --git a/src/apis/auth/__test__/auth.service.spec.ts b/src/apis/auth/__test__/auth.service.spec.ts index 217ab80..4fd7978 100644 --- a/src/apis/auth/__test__/auth.service.spec.ts +++ b/src/apis/auth/__test__/auth.service.spec.ts @@ -31,7 +31,7 @@ const EXAMPLE_USER: User = { describe('AuthResolver', () => { let authService: AuthService; let usersService: UsersService; - let jwt: JwtService; // Fix here + let jwt: JwtService; let cacheManager: Cache; const context: IContext = { diff --git a/src/apis/reviews/__test__/reviews.moking.dummy.ts b/src/apis/reviews/__test__/reviews.moking.dummy.ts deleted file mode 100644 index 713bf9d..0000000 --- a/src/apis/reviews/__test__/reviews.moking.dummy.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Repository } from 'typeorm'; -import { Review } from '../entities/review.entity'; - -export class diff --git a/src/apis/reviews/__test__/reviews.service.spec.ts b/src/apis/reviews/__test__/reviews.service.spec.ts index 1ae83ca..4fb4b5c 100644 --- a/src/apis/reviews/__test__/reviews.service.spec.ts +++ b/src/apis/reviews/__test__/reviews.service.spec.ts @@ -1,6 +1,7 @@ import { UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { ShopsService } from 'src/apis/shops/shops.service'; import { Repository } from 'typeorm'; import { Review } from '../entities/review.entity'; import { ReviewsService } from '../reviews.service'; @@ -15,7 +16,7 @@ const EXAMPLE_REVIEW = { shop: { id: '500d75e0-0223-4046-be13-55887bfbf6f0' }, }; -const MockReviewsRepository = { +const MockReviewsRepository = () => ({ find: jest.fn((where, skip, take, order, relations) => { EXAMPLE_REVIEW; }), @@ -25,11 +26,14 @@ const MockReviewsRepository = { save: jest.fn(({ contents, star, reservation, shop }) => { EXAMPLE_REVIEW; }), -}; +}); + +type MockRepository = Partial, jest.Mock>>; describe('ReviewsService', () => { let reviewsService: ReviewsService; - let mockReviewsRepository: Repository; + let shopsService: ShopsService; + let mockReviewsRepository: MockRepository; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -37,40 +41,61 @@ describe('ReviewsService', () => { ReviewsService, { provide: getRepositoryToken(Review), - useClass: Repository, + useValue: MockReviewsRepository(), + }, + { + provide: ShopsService, + useValue: { + findById: jest.fn(() => true), + }, }, ], }).compile(); reviewsService = module.get(ReviewsService); - mockReviewsRepository = module.get>( - getRepositoryToken(Review), - ); + mockReviewsRepository = module.get(getRepositoryToken(Review)); }); const reviewId = EXAMPLE_REVIEW.id; + const shopId = 'shopId'; describe('find', () => { it('reviewsRepository의 findOne을 실행하고 값이 없으면 error 반환해야함', async () => { - jest.spyOn(mockReviewsRepository, 'findOne').mockResolvedValue(undefined); - expect(mockReviewsRepository.findOne).not.toBeCalled(); - try { - await reviewsService.find({ reviewId }); + const result = await mockReviewsRepository.findOne({ + where: { id: reviewId }, + relations: ['shop', 'reservation'], + }); } catch (error) { expect(error).toBeInstanceOf(UnprocessableEntityException); expect(error.message).toEqual('아이디를 찾을 수 없습니다'); } - - expect(mockReviewsRepository.findOne).toBeCalledWith({ - where: { id: reviewId }, - relations: ['shop', 'reservation'], - }); + expect(mockReviewsRepository.findOne).toBeCalled(); }); }); describe('findByShopIdWithPage', () => { - it('', () => {}); + it('', () => { + try { + shopsService.findById({ shopId }); + } catch (error) { + expect(error).toBeInstanceOf(Error); + throw new UnprocessableEntityException('유효하지 않은 가게ID 입니다'); + } + const page = 1; + const count = 12; + const result = mockReviewsRepository.find({ + where: { shop: { id: shopId } }, + skip: (page - 1) * count, + take: count, + order: { + createdAt: 'ASC', + }, + relations: ['shop', 'reservation'], + }); + + expect(mockReviewsRepository.find).toBeCalled(); + }); }); describe('create', () => { diff --git a/src/apis/shopImages/__test__/shopImage.service.spec.ts b/src/apis/shopImages/__test__/shopImage.service.spec.ts index 9cef58d..d5d4294 100644 --- a/src/apis/shopImages/__test__/shopImage.service.spec.ts +++ b/src/apis/shopImages/__test__/shopImage.service.spec.ts @@ -100,7 +100,8 @@ describe('shopImagesService', () => { try { await checkShop({ shopId }); } catch (error) { - expect(error).toThrow(UnprocessableEntityException); + throw new UnprocessableEntityException(); + expect(error).toBeInstanceOf(UnprocessableEntityException); } const result = mockShopImagesRepository.find({ @@ -117,7 +118,8 @@ describe('shopImagesService', () => { try { await checkURL({ shopId }); } catch (error) { - expect(error).toThrow(ConflictException); + throw new ConflictException(); + expect(error).toBeInstanceOf(ConflictException); } const result = mockShopImagesRepository.save({ @@ -163,6 +165,7 @@ describe('shopImagesService', () => { try { await checkImage({ shopId }); } catch (error) { + throw new UnprocessableEntityException('아이디를 찾을 수 없습니다'); expect(error).toThrow(UnprocessableEntityException); } const result = await mockShopImagesRepository.delete({ From c3579cc1eb32708649d4f3b20371dad7678ff0f5 Mon Sep 17 00:00:00 2001 From: cabbage556 Date: Fri, 31 Mar 2023 14:04:18 +0900 Subject: [PATCH 7/7] =?UTF-8?q?release:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20API=20=EC=88=98=EC=A0=95=20=EB=B0=B0=ED=8F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release-#3 --- cloudbuild.yaml | 2 +- docker-compose.prod.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudbuild.yaml b/cloudbuild.yaml index 7e8d331..3f96196 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -16,7 +16,7 @@ steps: - set - image - deployment/groomeong-backend - - backend-sha256-1=asia.gcr.io/project-groomeong/backend:1.6 + - backend-sha256-1=asia.gcr.io/project-groomeong/backend:1.7 env: - CLOUDSDK_COMPUTE_ZONE=asia-northeast3 - CLOUDSDK_CONTAINER_CLUSTER=autopilot-cluster-5 diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml index e822bfd..44f327e 100644 --- a/docker-compose.prod.yaml +++ b/docker-compose.prod.yaml @@ -2,7 +2,7 @@ version: '3.7' services: my-backend: - image: asia.gcr.io/project-groomeong/backend:1.6 + image: asia.gcr.io/project-groomeong/backend:1.7 platform: linux/x86_64 build: context: .