From aa62497b8ae8507e001b600f6e395501598b0f48 Mon Sep 17 00:00:00 2001 From: J_Coder Date: Tue, 26 Nov 2024 23:24:35 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=85=20test:=20=EB=AA=A8=EB=93=A0=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=20=EC=A1=B0=ED=9A=8C=EC=88=98=20=ED=86=B5?= =?UTF-8?q?=EA=B3=84=20API=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=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 --- server/test/statistic/all.e2e-spec.ts | 177 ++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 server/test/statistic/all.e2e-spec.ts diff --git a/server/test/statistic/all.e2e-spec.ts b/server/test/statistic/all.e2e-spec.ts new file mode 100644 index 00000000..0319e536 --- /dev/null +++ b/server/test/statistic/all.e2e-spec.ts @@ -0,0 +1,177 @@ +import * as request from 'supertest'; +import { INestApplication } from '@nestjs/common'; +import { TestingModule } from '@nestjs/testing'; +import { DataSource } from 'typeorm'; +import { Feed } from '../../src/feed/feed.entity'; +import { RssAccept } from '../../src/rss/rss.entity'; +import { RedisService } from '../../src/common/redis/redis.service'; + +describe('All view count statistic E2E Test : GET /api/statistic/all', () => { + let app: INestApplication; + beforeAll(async () => { + app = global.testApp; + const moduleFixture: TestingModule = global.testModuleFixture; + const dataSource = moduleFixture.get(DataSource); + const rssAcceptRepository = dataSource.getRepository(RssAccept); + const feedRepository = dataSource.getRepository(Feed); + const redisService = app.get(RedisService); + const [blog] = await Promise.all([ + rssAcceptRepository.save({ + id: 1, + name: 'test', + userName: 'test', + email: 'test@test.com', + rssUrl: 'https://test.com/rss', + }), + redisService.redisClient.set('test1234', 'test'), + ]); + await feedRepository.save([ + { + id: 1, + createdAt: '2024-11-26 09:00:00', + title: 'test1', + path: 'test1', + viewCount: 5, + thumbnail: 'https://test.com/test.png', + blog: blog, + }, + { + id: 2, + createdAt: '2024-11-26 09:00:00', + title: 'test2', + path: 'test2', + viewCount: 4, + thumbnail: 'https://test.com/test.png', + blog: blog, + }, + { + id: 3, + createdAt: '2024-11-26 09:00:00', + title: 'test3', + path: 'test3', + viewCount: 3, + thumbnail: 'https://test.com/test.png', + blog: blog, + }, + { + id: 4, + createdAt: '2024-11-26 09:00:00', + title: 'test4', + path: 'test4', + viewCount: 2, + thumbnail: 'https://test.com/test.png', + blog: blog, + }, + { + id: 5, + createdAt: '2024-11-26 09:00:00', + title: 'test5', + path: 'test5', + viewCount: 1, + thumbnail: 'https://test.com/test.png', + blog: blog, + }, + ]); + }); + + describe('관리자 권한이 없을 경우', () => { + it('관리자 쿠키가 없다.', async () => { + const response = await request(app.getHttpServer()).get( + '/api/statistic/all?limit=1.1', + ); + expect(response.status).toBe(401); + expect(response.body.message).toBe('인증되지 않은 요청입니다.'); + }); + it('관리자 쿠키가 만료되었다.', async () => { + const response = await request(app.getHttpServer()) + .get('/api/statistic/all?limit=1.1') + .set('Cookie', 'sessionId=test4321'); + expect(response.status).toBe(401); + expect(response.body.message).toBe('인증되지 않은 요청입니다.'); + }); + }); + + describe('관리자 권한이 있을 경우', () => { + describe('limit 값을 올바르게 입력하지 않았을 경우', () => { + it('실수를 입력한다.', async () => { + const response = await request(app.getHttpServer()) + .get('/api/statistic/all?limit=1.1') + .set('Cookie', 'sessionId=test1234'); + expect(response.status).toBe(400); + expect(response.body.message).toBe('정수로 입력해주세요.'); + }); + it('문자열을 입력한다.', async () => { + const response = await request(app.getHttpServer()) + .get('/api/statistic/all?limit=test') + .set('Cookie', 'sessionId=test1234'); + expect(response.status).toBe(400); + expect(response.body.message).toBe('정수로 입력해주세요.'); + }); + + it('음수를 입력한다.', async () => { + const response = await request(app.getHttpServer()) + .get('/api/statistic/all?limit=-100') + .set('Cookie', 'sessionId=test1234'); + expect(response.status).toBe(400); + expect(response.body).toStrictEqual({ + message: 'limit 값은 1 이상이어야 합니다.', + }); + }); + }); + + describe('limit 값을 올바르게 입력했을 경우', () => { + it('값을 입력 하지 않는다.', async () => { + const response = await request(app.getHttpServer()) + .get('/api/statistic/all') + .set('Cookie', 'sessionId=test1234'); + expect(response.status).toBe(200); + expect(response.body).toStrictEqual({ + message: '전체 조회수 통계 조회 완료', + data: [ + { + id: 1, + title: 'test1', + viewCount: 5, + }, + { + id: 2, + title: 'test2', + viewCount: 4, + }, + { + id: 3, + title: 'test3', + viewCount: 3, + }, + { + id: 4, + title: 'test4', + viewCount: 2, + }, + { + id: 5, + title: 'test5', + viewCount: 1, + }, + ], + }); + }); + it('양수를 입력한다.', async () => { + const response = await request(app.getHttpServer()) + .get('/api/statistic/all?limit=1') + .set('Cookie', 'sessionId=test1234'); + expect(response.status).toBe(200); + expect(response.body).toStrictEqual({ + message: '전체 조회수 통계 조회 완료', + data: [ + { + id: 1, + title: 'test1', + viewCount: 5, + }, + ], + }); + }); + }); + }); +}); From 3f42563b3418ef81ad17623971440ba6980f14a8 Mon Sep 17 00:00:00 2001 From: J_Coder Date: Tue, 26 Nov 2024 23:25:12 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=93=9D=20docs:=20=EA=B8=88=EC=9D=BC?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=EC=88=98=20=ED=86=B5=EA=B3=84=20API=20Swa?= =?UTF-8?q?gger=20=EC=9D=91=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/statistic/statistic.api-docs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/statistic/statistic.api-docs.ts b/server/src/statistic/statistic.api-docs.ts index 3797193c..9e5151d2 100644 --- a/server/src/statistic/statistic.api-docs.ts +++ b/server/src/statistic/statistic.api-docs.ts @@ -5,7 +5,7 @@ import { ApiUnauthorizedResponse, } from '@nestjs/swagger'; -export function ApiTodayStatistic() { +export function ApiStatistic(type: 'today' | 'all') { return applyDecorators( ApiQuery({ name: 'limit', @@ -41,7 +41,7 @@ export function ApiTodayStatistic() { }, }, example: { - message: '금일 조회수 통계 조회 완료', + message: `${type === 'all' ? '전체' : '금일'} 조회수 통계 조회 완료`, data: [ { id: 1, From 70b6a071f0dbe6afc5e8dfb805650cca764ef1c2 Mon Sep 17 00:00:00 2001 From: J_Coder Date: Tue, 26 Nov 2024 23:26:16 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=E2=9C=A8=20feat:=20=EB=AA=A8=EB=93=A0=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=20=EC=A1=B0=ED=9A=8C=EC=88=98=20=ED=86=B5?= =?UTF-8?q?=EA=B3=84=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/statistic/dto/statistic-query.dto.ts | 3 ++- server/src/statistic/statistic.controller.ts | 17 +++++++++++++++-- server/src/statistic/statistic.service.ts | 11 +++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/server/src/statistic/dto/statistic-query.dto.ts b/server/src/statistic/dto/statistic-query.dto.ts index a4e1a096..ced27df9 100644 --- a/server/src/statistic/dto/statistic-query.dto.ts +++ b/server/src/statistic/dto/statistic-query.dto.ts @@ -1,12 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsInt, IsOptional } from 'class-validator'; +import { IsInt, IsOptional, Min } from 'class-validator'; export class StatisticQueryDto { @ApiProperty({ description: '최대로 가져올 데이터 개수를 입력하세요.', }) @IsOptional() + @Min(1, { message: 'limit 값은 1 이상이어야 합니다.' }) @IsInt({ message: '정수로 입력해주세요.' }) @Type(() => Number) limit?: number = 10; diff --git a/server/src/statistic/statistic.controller.ts b/server/src/statistic/statistic.controller.ts index 3d401965..2a5782d6 100644 --- a/server/src/statistic/statistic.controller.ts +++ b/server/src/statistic/statistic.controller.ts @@ -6,7 +6,7 @@ import { UsePipes, ValidationPipe, } from '@nestjs/common'; -import { ApiTodayStatistic } from './statistic.api-docs'; +import { ApiStatistic } from './statistic.api-docs'; import { StatisticService } from './statistic.service'; import { ApiResponse } from '../common/response/common.response'; import { ApiTags } from '@nestjs/swagger'; @@ -18,7 +18,7 @@ import { StatisticQueryDto } from './dto/statistic-query.dto'; export class StatisticController { constructor(private readonly statisticService: StatisticService) {} - @ApiTodayStatistic() + @ApiStatistic('today') @UseGuards(CookieAuthGuard) @Get('today') @UsePipes( @@ -30,4 +30,17 @@ export class StatisticController { const data = await this.statisticService.getTodayViewCount(queryObj.limit); return ApiResponse.responseWithData('금일 조회수 통계 조회 완료', data); } + + @ApiStatistic('all') + @UseGuards(CookieAuthGuard) + @Get('all') + @UsePipes( + new ValidationPipe({ + transform: true, + }), + ) + async getAllStatistic(@Query() queryObj: StatisticQueryDto) { + const data = await this.statisticService.getAllViewCount(queryObj.limit); + return ApiResponse.responseWithData('전체 조회수 통계 조회 완료', data); + } } diff --git a/server/src/statistic/statistic.service.ts b/server/src/statistic/statistic.service.ts index b32ef3f2..33550855 100644 --- a/server/src/statistic/statistic.service.ts +++ b/server/src/statistic/statistic.service.ts @@ -33,4 +33,15 @@ export class StatisticService { return result; } + + async getAllViewCount(limit: number) { + const ranking = await this.feedRepository.find({ + select: ['id', 'title', 'viewCount'], + order: { + viewCount: 'DESC', + }, + take: limit, + }); + return ranking; + } } From 0fdce1e965fa3df380340ec6fd1aedb4f7e5d9bb Mon Sep 17 00:00:00 2001 From: J_Coder Date: Tue, 26 Nov 2024 23:50:38 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=93=9D=20docs:=20bad=20Request=20?= =?UTF-8?q?=EC=B2=A8=EB=B6=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/statistic/statistic.api-docs.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server/src/statistic/statistic.api-docs.ts b/server/src/statistic/statistic.api-docs.ts index 9e5151d2..4c054c68 100644 --- a/server/src/statistic/statistic.api-docs.ts +++ b/server/src/statistic/statistic.api-docs.ts @@ -1,5 +1,6 @@ import { applyDecorators } from '@nestjs/common'; import { + ApiBadRequestResponse, ApiOkResponse, ApiQuery, ApiUnauthorizedResponse, @@ -51,6 +52,19 @@ export function ApiStatistic(type: 'today' | 'all') { ], }, }), + ApiBadRequestResponse({ + description: 'Bad Request', + schema: { + properties: { + message: { + type: 'string', + }, + }, + }, + example: { + message: '오류 메세지', + }, + }), ApiUnauthorizedResponse({ description: 'Unauthorized', schema: { From 91de6a252112c46cac5d1830c01b7ee32b6010c4 Mon Sep 17 00:00:00 2001 From: J_Coder Date: Wed, 27 Nov 2024 13:43:55 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=93=9D=20docs:=20api=20=EC=9A=94?= =?UTF-8?q?=EC=95=BD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/statistic/statistic.api-docs.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/server/src/statistic/statistic.api-docs.ts b/server/src/statistic/statistic.api-docs.ts index 77cd25df..c617a62c 100644 --- a/server/src/statistic/statistic.api-docs.ts +++ b/server/src/statistic/statistic.api-docs.ts @@ -2,12 +2,17 @@ import { applyDecorators } from '@nestjs/common'; import { ApiBadRequestResponse, ApiOkResponse, + ApiOperation, ApiQuery, ApiUnauthorizedResponse, } from '@nestjs/swagger'; -export function ApiStatistic(type: 'today' | 'all') { +export function ApiStatistic(category: 'today' | 'all') { + const type = category === 'all' ? '전체' : '금일'; return applyDecorators( + ApiOperation({ + summary: `${type} 게시글 조회수 통계 API`, + }), ApiQuery({ name: 'limit', required: false, @@ -42,7 +47,7 @@ export function ApiStatistic(type: 'today' | 'all') { }, }, example: { - message: `${type === 'all' ? '전체' : '금일'} 조회수 통계 조회 완료`, + message: `${type} 조회수 통계 조회 완료`, data: [ { id: 1,