From 8128c2d4fa3221dad549dd63c7eb48d62a57f2e2 Mon Sep 17 00:00:00 2001 From: Heesung Youn Date: Tue, 24 Jan 2023 14:27:11 +0900 Subject: [PATCH 1/7] modify to use bash for executing prebuilt.sh (#471) --- .github/workflows/deploy-development.yml | 2 +- .github/workflows/deploy-production-1.yml | 2 +- .github/workflows/deploy-production-2.yml | 2 +- .github/workflows/deploy-production.yml | 2 +- .github/workflows/exec-mocha-integral-test.yml | 2 +- .github/workflows/exec-mocha-unit-test.yml | 2 +- script/prebuild.sh | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy-development.yml b/.github/workflows/deploy-development.yml index 8dbe4f8d..c15bc545 100644 --- a/.github/workflows/deploy-development.yml +++ b/.github/workflows/deploy-development.yml @@ -28,7 +28,7 @@ jobs: node-version: ${{ matrix.node-version }} - run: cp ~/config-injection/ecosystem-${{ env.SERVER_PROFILE }}.json ${{ env.PROJECT_PATH }}/ecosystem.json - run: cp ~/config-injection/envs/.env.${{ env.NODE_ENV }} ${{ env.PROJECT_PATH }}/.env - - run: sh ${{ env.PROJECT_PATH }}/script/prebuild.sh + - run: bash ${{ env.PROJECT_PATH }}/script/prebuild.sh - run: sh ${{ env.PROJECT_PATH }}/script/reload.sh env: NODE_ENV: development diff --git a/.github/workflows/deploy-production-1.yml b/.github/workflows/deploy-production-1.yml index 3f028a2d..d0fe9a5a 100644 --- a/.github/workflows/deploy-production-1.yml +++ b/.github/workflows/deploy-production-1.yml @@ -28,7 +28,7 @@ jobs: node-version: ${{ matrix.node-version }} - run: cp ~/config-injection/ecosystem-${{ env.SERVER_PROFILE }}.json ${{ env.PROJECT_PATH }}/ecosystem.json - run: cp ~/config-injection/envs/.env.${{ env.NODE_ENV }} ${{ env.PROJECT_PATH }}/.env - - run: sh ${{ env.PROJECT_PATH }}/script/prebuild.sh + - run: bash ${{ env.PROJECT_PATH }}/script/prebuild.sh - run: sh ${{ env.PROJECT_PATH }}/script/reload.sh env: NODE_ENV: production diff --git a/.github/workflows/deploy-production-2.yml b/.github/workflows/deploy-production-2.yml index 2c56be63..8b2bc874 100644 --- a/.github/workflows/deploy-production-2.yml +++ b/.github/workflows/deploy-production-2.yml @@ -28,7 +28,7 @@ jobs: node-version: ${{ matrix.node-version }} - run: cp ~/config-injection/ecosystem-${{ env.SERVER_PROFILE }}.json ${{ env.PROJECT_PATH }}/ecosystem.json - run: cp ~/config-injection/envs/.env.${{ env.NODE_ENV }} ${{ env.PROJECT_PATH }}/.env - - run: sh ${{ env.PROJECT_PATH }}/script/prebuild.sh + - run: bash ${{ env.PROJECT_PATH }}/script/prebuild.sh - run: sh ${{ env.PROJECT_PATH }}/script/reload.sh env: NODE_ENV: production diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 734ac992..f330e40e 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -28,7 +28,7 @@ jobs: node-version: ${{ matrix.node-version }} - run: cp ~/config-injection/ecosystem-${{ env.SERVER_PROFILE }}.json ${{ env.PROJECT_PATH }}/ecosystem.json - run: cp ~/config-injection/envs/.env.${{ env.NODE_ENV }} ${{ env.PROJECT_PATH }}/.env - - run: sh ${{ env.PROJECT_PATH }}/script/prebuild.sh + - run: bash ${{ env.PROJECT_PATH }}/script/prebuild.sh - run: sh ${{ env.PROJECT_PATH }}/script/reload.sh env: NODE_ENV: production diff --git a/.github/workflows/exec-mocha-integral-test.yml b/.github/workflows/exec-mocha-integral-test.yml index dbe02cb6..4e344e63 100644 --- a/.github/workflows/exec-mocha-integral-test.yml +++ b/.github/workflows/exec-mocha-integral-test.yml @@ -17,7 +17,7 @@ jobs: - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 - run: sh ./script/setup-global.sh - - run: sh ./script/prebuild.sh + - run: bash ./script/prebuild.sh - name: run integral test code run: npm run test-integral env: diff --git a/.github/workflows/exec-mocha-unit-test.yml b/.github/workflows/exec-mocha-unit-test.yml index 0965def4..76f77313 100644 --- a/.github/workflows/exec-mocha-unit-test.yml +++ b/.github/workflows/exec-mocha-unit-test.yml @@ -17,7 +17,7 @@ jobs: - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 - run: sh ./script/setup-global.sh - - run: sh ./script/prebuild.sh + - run: bash ./script/prebuild.sh - name: run unit test code run: npm run test-unit env: diff --git a/script/prebuild.sh b/script/prebuild.sh index b478c2ea..3a8a83a9 100755 --- a/script/prebuild.sh +++ b/script/prebuild.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash if [[ -f "ecosystem.json" ]]; then pm2 stop ecosystem.json From 199a975907874aa46e10efd4e9f98cd99b2e162e Mon Sep 17 00:00:00 2001 From: Heesung Youn Date: Tue, 24 Jan 2023 16:16:08 +0900 Subject: [PATCH 2/7] implements logic for convert between null and zero on controller layer (#472) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * implements logic for convert between null and zero on controller layer * Send as private if null * Update src/controllers/User.ts Co-authored-by: 조하담 <35447878+ChoHadam@users.noreply.github.com> * modify to use constant Co-authored-by: 조하담 <35447878+ChoHadam@users.noreply.github.com> --- src/controllers/User.ts | 23 ++++-- src/controllers/definitions/request/user.ts | 42 +++++------ src/controllers/definitions/response/user.ts | 31 +++++--- src/service/ReviewService.js | 74 ++++++++++++-------- src/service/UserService.ts | 23 +++++- src/utils/constants.ts | 10 ++- src/utils/strings.ts | 3 + 7 files changed, 136 insertions(+), 70 deletions(-) diff --git a/src/controllers/User.ts b/src/controllers/User.ts index be21ecc8..042ac0af 100644 --- a/src/controllers/User.ts +++ b/src/controllers/User.ts @@ -4,7 +4,7 @@ import { logger, LoggerHelper } from '@modules/winston'; import { UnAuthorizedError } from '@errors'; -import { GRADE_USER } from '@utils/constants'; +import { GENDER_NONE, BIRTH_NONE, GRADE_USER } from '@utils/constants'; import { MSG_REGISTER_SUCCESS, @@ -36,7 +36,13 @@ import { LoginResponse, } from '@response/user'; -import { UserAuthDTO, UserInputDTO, LoginInfoDTO, SurveyDTO } from '@dto/index'; +import { + UserAuthDTO, + UserInputDTO, + LoginInfoDTO, + SurveyDTO, + UserDTO, +} from '@dto/index'; const LOG_TAG: string = '[User/Controller]'; @@ -165,7 +171,16 @@ const loginUser: RequestHandler = ( const password: string = req.body.password; User.loginUser(email, password) .then((result: LoginInfoDTO) => { - return LoginResponse.createByJson(result); + const loginResponse: any = LoginResponse.createByJson(result); + // TODO gender, birth nullable로 변경한 이후 아래 코드 삭제하기 + if (loginResponse.gender == GENDER_NONE) { + delete loginResponse.gender; + } + if (loginResponse.birth == BIRTH_NONE) { + delete loginResponse.birth; + } + + return loginResponse; }) .then((response: LoginResponse) => { LoggerHelper.logTruncated( @@ -673,7 +688,7 @@ const updateUser: RequestHandler = ( } const userEditRequest = UserEditRequest.createByJson(req.body); User.updateUser(userEditRequest.toUserInputDTO(userIdx)) - .then((result: UserResponse) => { + .then((result: UserDTO) => { return UserResponse.createByJson(result); }) .then((response: UserResponse) => { diff --git a/src/controllers/definitions/request/user.ts b/src/controllers/definitions/request/user.ts index 4bb9b08b..7f098448 100644 --- a/src/controllers/definitions/request/user.ts +++ b/src/controllers/definitions/request/user.ts @@ -1,17 +1,17 @@ import { logger } from '@modules/winston'; import { InvalidInputError } from '@src/utils/errors/errors'; -import { GenderMap, GradeMap, GradeKey, GenderKey } from '@utils/enumType'; -import { GRADE_USER } from '@utils/constants'; +import { GenderMap, GradeMap, GradeKey } from '@utils/enumType'; +import { GRADE_USER, GENDER_NONE } from '@utils/constants'; import { UserInputDTO } from '@src/data/dto'; interface UserInputRequest { grade?: GradeKey; - gender?: GenderKey; + gender?: string | null; nickname?: string; password?: string; email?: string; - birth?: number; + birth?: number | null; } /** @@ -37,17 +37,17 @@ interface UserInputRequest { * */ class UserEditRequest implements UserInputRequest { readonly grade?: GradeKey; - readonly gender?: GenderKey; + readonly gender?: string | null; readonly nickname?: string; readonly password?: string; readonly email?: string; - readonly birth?: number; + readonly birth?: number | null; constructor( nickname?: string, password?: string, - gender?: GenderKey, + gender?: string | null, email?: string, - birth?: number, + birth?: number | null, grade?: GradeKey ) { this.grade = grade; @@ -99,11 +99,13 @@ class UserEditRequest implements UserInputRequest { * gender: * type: string * enum: [MAN, WOMAN] - * required: true + * required: false + * nullable: true * example: MAN * birth: * type: integer - * required: true + * required: false + * nullable: true * example: 1995 * grade: * type: string @@ -113,16 +115,16 @@ class UserEditRequest implements UserInputRequest { class UserRegisterRequest implements UserInputRequest { readonly nickname: string; readonly password: string; - readonly gender: GenderKey; - readonly birth: number; + readonly gender: string | null; + readonly birth: number | null; readonly email: string; readonly grade: GradeKey; constructor( nickname: string, password: string, - gender: GenderKey, + gender: string | null, email: string, - birth: number, + birth: number | null, grade: GradeKey ) { this.grade = grade; @@ -145,9 +147,9 @@ class UserRegisterRequest implements UserInputRequest { return new UserRegisterRequest( json.nickname, json.password, - json.gender, + json.gender || null, json.email, - json.birth, + json.birth || null, json.grade | GRADE_USER ); } @@ -159,13 +161,13 @@ function createByRequest( userIdx: number | undefined, request: UserInputRequest ): UserInputDTO { - let genderCode: any = undefined; + let genderVal: number = GENDER_NONE; if (request.gender) { if (GenderMap[request.gender] == undefined) { logger.debug(`${LOG_TAG} invalid gender: ${request.gender}`); throw new InvalidInputError(); } - genderCode = GenderMap[request.gender]; + genderVal = GenderMap[request.gender]; } let gradeCode: number = GRADE_USER; if (request.grade) { @@ -180,9 +182,9 @@ function createByRequest( userIdx, request.nickname, request.password, - genderCode, + genderVal, request.email, - request.birth, + request.birth || 0, gradeCode, undefined ); diff --git a/src/controllers/definitions/response/user.ts b/src/controllers/definitions/response/user.ts index 61cd1d0a..04967254 100644 --- a/src/controllers/definitions/response/user.ts +++ b/src/controllers/definitions/response/user.ts @@ -1,4 +1,5 @@ -import { GenderInvMap, GenderKey } from '@utils/enumType'; +import { BIRTH_NONE } from '@src/utils/constants'; +import { GenderInvMap } from '@utils/enumType'; /** * @swagger @@ -14,10 +15,13 @@ import { GenderInvMap, GenderKey } from '@utils/enumType'; * example: nickname * gender: * type: string + * enum: [MAN, WOMAN] * example: MAN + * nullable: true * birth: * type: integer * example: 1995 + * nullable: true * token: * type: string * description: login용 userToken @@ -30,17 +34,17 @@ import { GenderInvMap, GenderKey } from '@utils/enumType'; class LoginResponse { readonly userIdx: number; readonly nickname: string; - readonly gender: GenderKey; + readonly gender: string | null; readonly email: string; - readonly birth: number; + readonly birth: number | null; readonly token: string; readonly refreshToken: string; constructor( userIdx: number, nickname: string, - gender: GenderKey, + gender: string | null, email: string, - birth: number, + birth: number | null, token: string, refreshToken: string ) { @@ -58,12 +62,15 @@ class LoginResponse { } static createByJson(json: any): LoginResponse { + const genderString: string | null = GenderInvMap[json.gender] || null; + const birth: number | null = + json.birth == BIRTH_NONE ? null : json.birth; return new LoginResponse( json.userIdx, json.nickname, - GenderInvMap[json.gender], + genderString, json.email, - json.birth, + birth, json.token, json.refreshToken ); @@ -157,8 +164,10 @@ class UserRegisterResponse { * gender: * type: string * enum: [MAN, WOMAN] + * nullable: true * birth: * type: integer + * nullable: true * grade: * type: string * enum: [USER, MANAGER, SYSTEM_ADMIN] @@ -172,15 +181,15 @@ class UserRegisterResponse { class UserResponse { readonly userIdx: number; readonly nickname: string; - readonly gender: GenderKey; + readonly gender: string | null; readonly email: string; - readonly birth: number; + readonly birth: number | null; constructor( userIdx: number, nickname: string, - gender: GenderKey, + gender: string | null, email: string, - birth: number + birth: number | null ) { this.userIdx = userIdx; this.nickname = nickname; diff --git a/src/service/ReviewService.js b/src/service/ReviewService.js index 4af1381d..258c2bfc 100644 --- a/src/service/ReviewService.js +++ b/src/service/ReviewService.js @@ -12,6 +12,8 @@ import UserDao from '@dao/UserDao'; import LikePerfumeDao from '@dao/LikePerfumeDao'; import ReviewDao from '@dao/ReviewDao'; import KeywordDao from '../dao/KeywordDao'; +import { PRIVATE } from '@src/utils/strings'; +import { BIRTH_NONE, GENDER_NONE } from '@src/utils/constants'; const userDao = new UserDao(); const likePerfumeDao = new LikePerfumeDao(); @@ -70,14 +72,13 @@ exports.postReview = async ({ ); try { await likePerfumeDao.delete(userIdx, perfumeIdx); - } - catch (err) { - if (err instanceof NotMatchedError) {} - else throw err; + } catch (err) { + if (err instanceof NotMatchedError) { + } else throw err; } return reviewIdx; } catch (err) { - console.log(err) + console.log(err); throw err; } }; @@ -242,15 +243,19 @@ exports.getReviewOfPerfumeByLike = async ({ perfumeIdx, userIdx }) => { const reviewList = await reviewDao.readAllOfPerfume(perfumeIdx); // 유저가 신고한 시향노트 인덱스 목록 조회 - const allReportedReviewByUser = await reportReviewDao.readAllReportedReviewByUser(userIdx) + const allReportedReviewByUser = + await reportReviewDao.readAllReportedReviewByUser(userIdx); const reportedReviewIdxList = allReportedReviewByUser.map((it) => { - return it.reviewIdx; + return it.reviewIdx; }); const result = await reviewList.reduce(async (prevPromise, it) => { let prevResult = await prevPromise.then(); const approxAge = getApproxAge(it.User.birth); - const readLikeResult = await likeReviewDao.read(userIdx, it.reviewIdx); + const readLikeResult = await likeReviewDao.read( + userIdx, + it.reviewIdx + ); const currentResult = { reviewIdx: it.reviewIdx, score: it.score, @@ -262,15 +267,21 @@ exports.getReviewOfPerfumeByLike = async ({ perfumeIdx, userIdx }) => { age: approxAge, nickname: it.User.nickname, createTime: it.createdAt, - isReported: reportedReviewIdxList.includes(it.reviewIdx) + isReported: reportedReviewIdxList.includes(it.reviewIdx), }; + if (currentResult.userGender == GENDER_NONE) { + currentResult.userGender = GENDER_NONE; // PRIVATE; + } + if (it.User.birth == BIRTH_NONE) { + currentResult.age = PRIVATE; + } prevResult.push(currentResult); return Promise.resolve(prevResult); }, Promise.resolve([])); return result; } catch (err) { - console.log(err) - throw err + console.log(err); + throw err; } }; @@ -302,28 +313,30 @@ exports.likeReview = async (reviewIdx, userIdx) => { * @param {Number} userIdx * @returns {Promise} **/ -exports.reportReview = async ({ - userIdx, - reviewIdx, - reason -}) => { +exports.reportReview = async ({ userIdx, reviewIdx, reason }) => { try { - const userInfo = await userDao.readByIdx(userIdx) + const userInfo = await userDao.readByIdx(userIdx); const userNickname = userInfo.nickname; const reviewData = await reviewDao.read(reviewIdx); - const perfumeName = reviewData.Perfume.name - const reviewContent = reviewData.content + const perfumeName = reviewData.Perfume.name; + const reviewContent = reviewData.content; // 신고 정보 저장 - await reportReviewDao.create({ reporterIdx: userIdx, reviewIdx, reason }) + await reportReviewDao.create({ + reporterIdx: userIdx, + reviewIdx, + reason, + }); // 디스코드로 신고 알림 전송 - await discordHook.send(`시향노트 신고가 들어왔습니다.\n\n신고 사유 : ${reason} \n향수명 : ${perfumeName} \n시향노트 내용 : ${reviewContent} \n신고자 : ${userNickname} \n시향노트 Idx : ${reviewIdx} `); + await discordHook.send( + `시향노트 신고가 들어왔습니다.\n\n신고 사유 : ${reason} \n향수명 : ${perfumeName} \n시향노트 내용 : ${reviewContent} \n신고자 : ${userNickname} \n시향노트 Idx : ${reviewIdx} ` + ); - return true; + return true; } catch (err) { - console.log(err) - throw err + console.log(err); + throw err; } }; @@ -334,16 +347,17 @@ exports.reportReview = async ({ * @returns {Promise} reviewIdx List */ - module.exports.readAllReportedReviewByUser = async (userIdx) => { +module.exports.readAllReportedReviewByUser = async (userIdx) => { try { - const allReportedReviewByUser = await reportReviewDao.readAllReportedReviewByUser(userIdx) - console.log('allReportedReviewByUser', allReportedReviewByUser) + const allReportedReviewByUser = + await reportReviewDao.readAllReportedReviewByUser(userIdx); + console.log('allReportedReviewByUser', allReportedReviewByUser); return result.map((it) => { - return it.reviewIdx; + return it.reviewIdx; }); } catch (err) { - console.log(err) - throw err + console.log(err); + throw err; } }; diff --git a/src/service/UserService.ts b/src/service/UserService.ts index 0fb3b936..cc94ea2e 100644 --- a/src/service/UserService.ts +++ b/src/service/UserService.ts @@ -24,6 +24,7 @@ import { SurveyDTO, TokenSetDTO, } from '@dto/index'; +import { BIRTH_NONE, GENDER_NONE } from '@src/utils/constants'; const LOG_TAG: string = '[User/Service]'; @@ -65,6 +66,14 @@ class UserService { .then((user: UserDTO | any) => { delete user.password; const payload = Object.assign({}, user); + // TODO gender, birth nullable로 변경한 이후 아래 코드 삭제하기 + if (payload.gender == GENDER_NONE) { + delete payload.gender; + } + if (payload.birth == BIRTH_NONE) { + delete payload.birth; + } + const { userIdx } = user; const { token, refreshToken } = this.jwt.publish(payload); return TokenGroupDTO.createByJSON({ @@ -163,9 +172,17 @@ class UserService { throw new WrongPasswordError(); } this.userDao.updateAccessTime(user.userIdx); - const { token, refreshToken } = this.jwt.publish( - TokenPayloadDTO.createByJson(user) - ); + + const payload: any = TokenPayloadDTO.createByJson(user); + // TODO gender, birth nullable로 변경한 이후 아래 코드 삭제하기 + if (payload.gender == GENDER_NONE) { + delete payload.gender; + } + if (payload.birth == BIRTH_NONE) { + delete payload.birth; + } + + const { token, refreshToken } = this.jwt.publish(payload); await this.tokenDao .create(new TokenSetDTO(token, refreshToken)) .then((result: boolean) => { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index d3c6a4c6..367479ed 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -11,6 +11,10 @@ import { SINGLE, } from '@utils/strings'; +// TODO: Remove below variables after refactoring +const BIRTH_NONE: number = 0; +const GENDER_NONE: number = 0; + const GENDER_MAN: number = 1; const GENDER_WOMAN: number = 2; const GRADE_USER: number = 0; @@ -50,12 +54,14 @@ const DEFAULT_RECENT_ADDED_PERFUME_REQUEST_SIZE: number = 7; const DEFAULT_RECOMMEND_COMMON_REQUEST_SIZE: number = 15; const DEFAULT_BRAND_REQUEST_SIZE: number = 1000; const DEFAULT_INGREDIENT_REQUEST_SIZE: number = 2000; -const DEFAULT_OP_CODE:number = 0; +const DEFAULT_OP_CODE: number = 0; const ACCESS_PUBLIC: number = 1; const ACCESS_PRIVATE: number = 0; export { + BIRTH_NONE, + GENDER_NONE, GENDER_MAN, GENDER_WOMAN, GRADE_USER, @@ -82,5 +88,5 @@ export { DEFAULT_INGREDIENT_REQUEST_SIZE, ACCESS_PUBLIC, ACCESS_PRIVATE, - DEFAULT_OP_CODE + DEFAULT_OP_CODE, }; diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 3bcb5e91..3406f007 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -40,6 +40,8 @@ const MIDDLE: string = 'middle'; const BASE: string = 'base'; const SINGLE: string = 'single'; +const PRIVATE: string = '비공개'; + const MSG_GET_BRAND_FILTER_SUCCESS: string = `브랜드 필터 ${_get} ${_success}`; const MSG_GET_BRAND_ALL_SUCCESS: string = `브랜드 ${_all} ${_get} ${_success}`; const MSG_GET_SEARCH_INGREDIENT_SUCCESS: string = `재료 ${_search} ${_get} ${_success}`; @@ -143,6 +145,7 @@ export { MIDDLE, BASE, SINGLE, + PRIVATE, MSG_GET_BRAND_FILTER_SUCCESS, MSG_GET_BRAND_ALL_SUCCESS, MSG_GET_SEARCH_INGREDIENT_SUCCESS, From 13d7809318d0aa48772b0282af42293527cca297 Mon Sep 17 00:00:00 2001 From: Heesung Youn Date: Fri, 27 Jan 2023 08:51:00 +0900 Subject: [PATCH 3/7] Refactoring/issue 473 (#474) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * extract commonTest method * fix fail of test Sequelize에서 가져온 accessTime은 string 타입이 아닌 object 타입 * Add MockGenerator class * apply MockGenerator * update UserDao.spec.ts * fix some code * modify UserDao::update for passing test * modify to run presets.js only first time * update setTimeout 1000 to 3000 * apply async/await on UserDao.spec.ts * implement TestSingle and ParameterizedTest * Add job queue on presets.js * Add Precondition api * Update UserService.spec.ts with SingleTest class * Add MockGenerator on UserServices.spec.ts --- src/dao/UserDao.ts | 52 +- src/data/dto/UserDTO.ts | 6 +- src/data/dto/UserInputDTO.ts | 13 + src/modules/winston.ts | 2 +- src/service/UserService.ts | 4 +- tests/internal/TestHelper.ts | 115 +++++ tests/test_integral/auth.spec.ts | 2 +- tests/test_unit/dao/UserDao.spec.ts | 535 +++++++++++++------- tests/test_unit/dao/common/presets.js | 57 ++- tests/test_unit/service/UserService.spec.ts | 465 +++++++++-------- 10 files changed, 829 insertions(+), 422 deletions(-) create mode 100644 tests/internal/TestHelper.ts diff --git a/src/dao/UserDao.ts b/src/dao/UserDao.ts index 78141403..dd290f54 100644 --- a/src/dao/UserDao.ts +++ b/src/dao/UserDao.ts @@ -34,11 +34,13 @@ class UserDao { ); }) .catch((err: Error | any) => { - if ( - err.parent.errno === 1062 || - err.parent.code === 'ER_DUP_ENTRY' - ) { - throw new DuplicatedEntryError(); + if (err.parent) { + if ( + err.parent.errno === 1062 || + err.parent.code === 'ER_DUP_ENTRY' + ) { + throw new DuplicatedEntryError(); + } } throw err; }); @@ -85,17 +87,39 @@ class UserDao { */ async update(userInputDTO: UserInputDTO): Promise { logger.debug(`${LOG_TAG} update(userInputDTO = ${userInputDTO})`); - const result = await User.update( - { ...userInputDTO }, - { - where: { userIdx: userInputDTO.userIdx }, - } - ); - const affectedRows = result[0]; - if (affectedRows == 0) { + const user: any = await User.findOne({ + where: { userIdx: userInputDTO.userIdx }, + }); + if (user == null) { throw new NotMatchedError(); } - return affectedRows; + + if (userInputDTO.accessTime != null) { + user.accessTime = userInputDTO.accessTime; + } + if (userInputDTO.nickname != null) { + user.nickname = userInputDTO.nickname; + } + if (userInputDTO.password != null) { + user.password = userInputDTO.password; + } + if (userInputDTO.gender != null) { + user.gender = userInputDTO.gender; + } + if (userInputDTO.email != null) { + user.email = userInputDTO.email; + } + if (userInputDTO.birth != null) { + user.birth = userInputDTO.birth; + } + if (userInputDTO.grade != null) { + user.grade = userInputDTO.grade; + } + if (userInputDTO.accessTime != null) { + user.accessTime = userInputDTO.accessTime; + } + await user.save(); + return 1; } /** diff --git a/src/data/dto/UserDTO.ts b/src/data/dto/UserDTO.ts index 30b91fd8..ad074f34 100644 --- a/src/data/dto/UserDTO.ts +++ b/src/data/dto/UserDTO.ts @@ -48,9 +48,9 @@ class UserDTO { json.email, json.birth, json.grade, - json.accessTime, - json.createdAt, - json.updatedAt + json.accessTime + '', + json.createdAt + '', + json.updatedAt + '' ); } } diff --git a/src/data/dto/UserInputDTO.ts b/src/data/dto/UserInputDTO.ts index 14b002d4..4c29aec2 100644 --- a/src/data/dto/UserInputDTO.ts +++ b/src/data/dto/UserInputDTO.ts @@ -32,6 +32,19 @@ class UserInputDTO { public toString(): string { return `${this.constructor.name} (${JSON.stringify(this)})`; } + + static createByJson(json: any): UserInputDTO { + return new UserInputDTO( + json.userIdx, + json.nickname, + json.password, + json.gender, + json.email, + json.birth, + json.grade, + json.accessTime + ); + } } export { UserInputDTO }; diff --git a/src/modules/winston.ts b/src/modules/winston.ts index cd18f05a..21fdb8e9 100644 --- a/src/modules/winston.ts +++ b/src/modules/winston.ts @@ -97,7 +97,7 @@ class TestLoggerAdapter implements ILoggerAdapter { transports: [ new winston.transports.Stream({ stream: process.stderr, - level: 'debug', + level: 'error', }), ], }); diff --git a/src/service/UserService.ts b/src/service/UserService.ts index cc94ea2e..f02b4843 100644 --- a/src/service/UserService.ts +++ b/src/service/UserService.ts @@ -53,10 +53,10 @@ class UserService { * 유저 회원 가입 * * @param {UserInputDTO} UserInputDTO - * @returns {Promise} + * @returns {Promise} * @throws {FailedToCreateError} if failed to create user **/ - async createUser(userInputDTO: UserInputDTO) { + async createUser(userInputDTO: UserInputDTO): Promise { logger.debug(`${LOG_TAG} createUser(userInputDTO = ${userInputDTO})`); return this.userDao .create(userInputDTO) diff --git a/tests/internal/TestHelper.ts b/tests/internal/TestHelper.ts new file mode 100644 index 00000000..a177edc1 --- /dev/null +++ b/tests/internal/TestHelper.ts @@ -0,0 +1,115 @@ +import { + ExclusiveSuiteFunction, + PendingSuiteFunction, + SuiteFunction, + TestFunction, +} from 'mocha'; + +const NO_OP = () => {}; + +type Executor = (...args: any[]) => Promise; +type Validator = (result: T | null, err: any, parameter: any[]) => void; +type DescribeExtended = + | SuiteFunction + | PendingSuiteFunction + | ExclusiveSuiteFunction; + +class TestSingle { + readonly title: string; + readonly executor: Executor; + readonly parameter: any[]; + readonly validator: Validator; + private precondition?: () => void; + constructor( + title: string, + executor: Executor, + parameter: any[], + validator: Validator = NO_OP + ) { + this.title = title; + this.executor = executor; + this.parameter = parameter; + this.validator = validator; + } + + addPrecondition(precondition: () => void) { + this.precondition = precondition; + return this; + } + + test(it: TestFunction) { + it(`# case: ${this.title}`, () => { + this.command(); + }); + } + + async command() { + try { + if (this.precondition) { + this.precondition(); + } + const result: T = await this.executor.apply( + this.executor, + this.parameter + ); + this.validator(result, null, this.parameter); + } catch (err: any) { + this.validator(null, err, this.parameter); + } + } +} + +class ParameterizedTest { + readonly title: string; + readonly executor: Executor; + readonly validator: Validator; + readonly parameterList: any[][]; + constructor( + title: string, + executor: Executor, + validator: Validator = NO_OP + ) { + this.title = title; + this.executor = executor; + this.validator = validator; + this.parameterList = []; + } + + addParameter(parameter: any[]): ParameterizedTest { + this.parameterList.push(parameter); + return this; + } + + addParameterAll(parameterList: any[][]): ParameterizedTest { + this.parameterList.push(...parameterList); + return this; + } + + async test(describe: DescribeExtended, it: TestFunction) { + describe(`# case: ${this.title}`, () => { + this.parameterList.forEach((parameter: any[], index: number) => { + it(`# P${index + 1}`, async () => { + try { + const result: T = await this.executor.apply( + this.executor, + parameter + ); + this.validator(result, null, parameter); + } catch (err: any) { + this.validator(null, err, parameter); + } + }); + }); + }); + } +} + +function composeValidator(...validatorList: Validator[]) { + return (result: T | null, err: any, parameter: any[]): void => { + validatorList.forEach((validator: Validator) => { + validator(result, err, parameter); + }); + }; +} + +export { TestSingle, ParameterizedTest, composeValidator, Executor, Validator }; diff --git a/tests/test_integral/auth.spec.ts b/tests/test_integral/auth.spec.ts index b9215283..21f87cb1 100644 --- a/tests/test_integral/auth.spec.ts +++ b/tests/test_integral/auth.spec.ts @@ -128,7 +128,7 @@ describe('# Auth Controller Test', () => { } ); }); - }, 1000); + }, 3000); }); }); }); diff --git a/tests/test_unit/dao/UserDao.spec.ts b/tests/test_unit/dao/UserDao.spec.ts index 1135fbad..11522545 100644 --- a/tests/test_unit/dao/UserDao.spec.ts +++ b/tests/test_unit/dao/UserDao.spec.ts @@ -1,217 +1,404 @@ import { expect } from 'chai'; -import dotenv from 'dotenv'; import { Done } from 'mocha'; -dotenv.config(); -import { - NotMatchedError, - DuplicatedEntryError, - UnExpectedError, -} from '@errors'; +import { NotMatchedError, DuplicatedEntryError } from '@errors'; -import { GENDER_MAN, GENDER_WOMAN } from '@utils/constants'; +import { + GENDER_MAN, + GENDER_WOMAN, + GRADE_MANAGER, + GRADE_USER, + GRADE_SYSTEM_ADMIN, +} from '@utils/constants'; import UserDao from '@dao/UserDao'; -import { CreatedResultDTO, UserDTO } from '@dto/index'; +import { CreatedResultDTO, UserDTO, UserInputDTO } from '@dto/index'; -import UserMockHelper from '../mock_helper/UserMockHelper'; +import { + composeValidator, + Executor, + ParameterizedTest, + TestSingle, + Validator, +} from '../../internal/TestHelper'; const userDao = new UserDao(); const { User } = require('@sequelize'); -describe('# userDao Test', () => { +class MockGenerator { + static _userIdx: number = 6; + + static createUserInputDTO(json: any = {}): UserInputDTO { + const userIdx: number = json.userIdx ? json.userIdx : this._userIdx++; + const expected: any = { + userIdx, + nickname: `user${userIdx}`, + password: 'hashed', + gender: 1, + email: `user${userIdx}@scents.note.com`, + birth: '1995', + grade: 1, + }; + return UserInputDTO.createByJson(Object.assign(expected, json)); + } +} + +describe('▣ UserDao', () => { before(async function () { await require('./common/presets.js')(this); }); - describe('# create Test', () => { - before(async () => { - await User.destroy({ where: { email: 'createTest@afume.com' } }); - }); - it('# success case', (done: Done) => { - const expected: { [index: string]: string | number } = { - nickname: '생성 테스트', - password: 'hashed', - gender: 1, - email: 'createTest@afume.com', - birth: '1995', - grade: 1, - }; - userDao - .create(expected) - .then((result: CreatedResultDTO) => { - expect(result).instanceOf(CreatedResultDTO); - for (const key in expected) { - const value = expected[key]; - expect(result.created[key]).to.be.eq(value); - } - done(); - }) - .catch((err: Error) => done(err)); - }); - it('# DuplicatedEntryError case', (done: Done) => { - userDao - .create({ - nickname: '생성 테스트', - password: 'hashed', - gender: GENDER_MAN, - email: 'createTest@afume.com', - birth: 1995, - grade: 1, - }) - .then(() => { - done(new UnExpectedError(DuplicatedEntryError)); - }) - .catch((err: Error) => { - expect(err).instanceOf(DuplicatedEntryError); - done(); - }) - .catch((err: Error) => done(err)); - }); - after(async () => { - await User.destroy({ where: { email: 'createTest@afume.com' } }); + describe('▶ create Test', function () { + function commonValidator( + result: CreatedResultDTO | null, + err: any, + parameter: any[] + ) { + expect(result).to.be.not.null; + expect(err).to.be.eq(null); + + expect(result).instanceOf(CreatedResultDTO); + const created: UserDTO = result!!.created; + + const input: UserInputDTO = parameter[0]; + expect(created.nickname).to.be.eq(input.nickname); + expect(created.password).to.be.eq(input.password); + expect(created.gender).to.be.eq(input.gender); + expect(created.email).to.be.eq(input.email); + expect(created.birth).to.be.eq(input.birth); + expect(created.grade).to.be.eq(input.grade); + } + + describe('# method: create', function () { + new TestSingle>( + 'common', + userDao.create, + [ + MockGenerator.createUserInputDTO({ + userIdx: null, + }), + ], + commonValidator + ).test(it); + + new TestSingle>( + 'duplicated email', + userDao.create, + [ + MockGenerator.createUserInputDTO({ + userIdx: null, + }), + (result: CreatedResultDTO, err: any) => { + expect(result).to.be.eq(null); + expect(err).instanceOf(DuplicatedEntryError); + }, + ] + ).test(it); + + new ParameterizedTest>( + 'null of gender or birth', + userDao.create, + commonValidator + ) + .addParameterAll( + [ + MockGenerator.createUserInputDTO({ + userIdx: null, + gender: null, + }), + MockGenerator.createUserInputDTO({ + userIdx: null, + birth: null, + }), + MockGenerator.createUserInputDTO({ + userIdx: null, + gender: null, + birth: null, + }), + MockGenerator.createUserInputDTO({ + userIdx: null, + gender: undefined, + }), + MockGenerator.createUserInputDTO({ + userIdx: null, + birth: undefined, + }), + MockGenerator.createUserInputDTO({ + userIdx: null, + gender: undefined, + birth: undefined, + }), + ].map((it: UserInputDTO): any[] => [it]) + ) + .test(describe.skip, it); }); }); - describe(' # read Test', () => { - describe('# readByEmail Test', () => { - it('# success case', (done: Done) => { - userDao - .read({ email: 'email1@afume.com' }) - .then((result: UserDTO) => { - UserMockHelper.validTest.call(result); - done(); - }) - .catch((err: Error) => done(err)); - }); - it('# Not Matched case', (done: Done) => { - userDao - .read({ email: '존재하지 않는 아이디' }) - .then(() => { - throw new UnExpectedError(NotMatchedError); - }) - .catch((err: Error) => { - expect(err).instanceOf(NotMatchedError); - done(); - }) - .catch((err: Error) => done(err)); - }); + describe('▶ read Test', () => { + function commonValidator(result: UserDTO | null, err: any, _: any[]) { + expect(result).to.be.not.null; + expect(err).to.be.null; + + expect(result).to.be.instanceOf(UserDTO); + const userDto: UserDTO = result!!; + expect(userDto.userIdx).to.be.gt(0); + expect(userDto.nickname).to.be.ok; + expect(userDto.email).to.be.ok; + expect(userDto.password).to.be.ok; + expect(userDto.gender).to.be.oneOf([GENDER_MAN, GENDER_WOMAN]); + expect(userDto.birth).to.be.ok; + expect(userDto.grade).to.be.oneOf([ + GRADE_USER, + GRADE_MANAGER, + GRADE_SYSTEM_ADMIN, + ]); + expect(userDto.createdAt).to.be.ok; + expect(userDto.updatedAt).to.be.ok; + } + + describe('# method: read', () => { + new TestSingle( + 'common', + userDao.read, + [{ email: 'email1@afume.com' }], + composeValidator( + commonValidator, + (result: UserDTO | null, _: any, parameter: any[]) => { + const condition: any = parameter[0]; + expect(result!!.email).to.be.eq(condition.email); + } + ) + ).test(it); + + new TestSingle( + 'common', + userDao.read, + [{ email: 'email1@afume.com' }], + composeValidator( + commonValidator, + (result: UserDTO | null, _: any, parameter: any[]) => { + const condition: any = parameter[0]; + expect(result!!.email).to.be.eq(condition.email); + } + ) + ).test(it); + + new TestSingle( + 'not matched', + userDao.read, + [{ email: '존재하지 않는 아이디' }], + (result: UserDTO | null, err: any, _: any[]) => { + expect(result).to.be.null; + expect(err).instanceOf(NotMatchedError); + } + ).test(it); }); - describe('# readByIdx Test', () => { - it('# success case', (done: Done) => { - userDao - .readByIdx(1) - .then((result: UserDTO) => { - UserMockHelper.validTest.call(result); - expect(result.userIdx).to.be.eq(1); - done(); - }) - .catch((err: Error) => done(err)); - }); - it('# Not Matched case', (done: Done) => { - userDao - .readByIdx(0) - .then(() => { - throw new UnExpectedError(NotMatchedError); - }) - .catch((err: Error) => { + + describe('# method: readByIdx', () => { + new TestSingle( + 'common', + userDao.readByIdx, + [[1]], + composeValidator( + commonValidator, + (result: UserDTO | null, _: any, __: any[]) => { + expect(result!!.userIdx).to.be.eq(1); + } + ) + ).test(it); + + new TestSingle( + 'not matched', + userDao.readByIdx, + [[0]], + composeValidator( + commonValidator, + (result: UserDTO | null, err: any, __: any[]) => { + expect(result).to.be.null; expect(err).instanceOf(NotMatchedError); - done(); - }) - .catch((err: Error) => done(err)); - }); + } + ) + ).test(it); }); }); - describe('# update Test', () => { - let userIdx: number = 0; - before(async () => { - userIdx = ( - await userDao.create({ - nickname: '수정 테스트', - password: 'hashed', - gender: GENDER_MAN, - email: 'updateTest@afume.com', - birth: 1995, - grade: 1, - }) - ).idx; - }); - it('# success case', (done: Done) => { - userDao - .update({ - userIdx, - nickname: '수정 테스트(完)', - password: '변경', - gender: GENDER_WOMAN, - email: 'updateTest@afume.com', - birth: 1995, - grade: 0, - }) - .then((result: number) => { - expect(result).eq(1); - return userDao.readByIdx(userIdx); - }) - .then((result: UserDTO) => { - UserMockHelper.validTest.call(result); - expect(result.userIdx).to.be.eq(userIdx); - expect(result.nickname).to.be.eq('수정 테스트(完)'); - expect(result.email).to.be.eq('updateTest@afume.com'); - expect(result.password).to.be.eq('변경'); - expect(result.gender).to.be.eq(GENDER_WOMAN); - expect(result.birth).to.be.eq(1995); - expect(result.grade).to.be.eq(0); - done(); - }) - .catch((err: Error) => done(err)); + describe('▶ update Test', () => { + function generatorExecutorAndValidator(): [ + executor: Executor, + validator: Validator + ] { + let input: UserInputDTO; + let original: UserDTO; + return [ + async (...args: any[]): Promise => { + input = args[0]; + original = await userDao.readByIdx(input.userIdx!!); + return userDao.update(input); + }, + async (result: number | null, err: any, _: any) => { + expect(result).to.be.not.null; + expect(err).to.be.null; + + expect(result).to.be.eq(1); + + const userDto: UserDTO = await userDao.readByIdx( + input.userIdx!! + ); + expect(userDto.nickname).to.be.eq( + input.nickname != null + ? input.nickname + : original.nickname + ); + expect(userDto.password).to.be.eq( + input.password != null + ? input.password + : original.password + ); + expect(userDto.gender).to.be.eq( + input.gender != null ? input.gender : original.gender + ); + expect(userDto.email).to.be.eq( + input.email != null ? input.email : original.email + ); + expect(userDto.birth).to.be.eq( + input.birth != null ? input.birth : original.birth + ); + expect(userDto.grade).to.be.eq( + input.grade != null ? input.grade : original.grade + ); + expect(userDto.accessTime).to.be.eq( + input.accessTime != null + ? input.accessTime + : original.accessTime + ); + }, + ]; + } + + describe('# method: update', () => { + const funcs: [ + executor: Executor, + validator: Validator + ] = generatorExecutorAndValidator(); + + new TestSingle( + 'common', + funcs[0], + [ + { + userIdx: 5, + nickname: '수정 테스트(完)', + password: '변경', + gender: GENDER_WOMAN, + birth: 1995, + grade: 0, + }, + ], + funcs[1] + ).test(it); + + let userIdx: number = 4; + new ParameterizedTest( + 'null of gender or birth', + funcs[0], + funcs[1] + ) + .addParameterAll( + [ + { + userIdx, + birth: null, + }, + { + userIdx, + gender: null, + }, + { + userIdx, + birth: null, + gender: null, + }, + { + userIdx, + birth: undefined, + }, + { + userIdx, + gender: undefined, + }, + { + userIdx, + birth: undefined, + gender: undefined, + }, + ].map((it: any) => [it]) + ) + .test(describe.skip, it); }); - it('# updateAccessTime success case', (done: Done) => { - setTimeout(() => { - userDao - .updateAccessTime(userIdx) - .then((result: number) => { - expect(result).eq(1); - done(); - }) - .catch((err: Error) => done(err)); - }, 1000); + + describe('# method: updateAccessTime', () => { + new TestSingle( + 'common', + (...args: any[]): Promise => { + const userIdx: number = args[0]; + return new Promise( + ( + resolve: (value: number) => void, + reject: (reason?: any) => void + ) => { + try { + setTimeout(async () => { + const affectedRows: number = + await userDao.updateAccessTime(userIdx); + resolve(affectedRows); + }, 3000); + } catch (err: any) { + reject(err); + } + } + ); + }, + [[1]], + (res: number | null, err: any, _: any[]) => { + expect(err).to.be.not.null; + expect(res).to.be.eq(1); + } + ).test(it); }); + after(async () => { await User.destroy({ where: { email: 'updateTest@afume.com' } }); }); }); - describe('# delete Test', () => { + describe('▶ delete Test', () => { let userIdx: number = 0; before(async () => { userIdx = ( - await userDao.create({ - nickname: '삭제 테스트', - password: 'hashed', - gender: GENDER_MAN, - email: 'deleteTest@afume.com', - birth: 1995, - grade: 0, - }) - ).idx; - }); - describe('# delete Test', () => { - it('# success case', (done: Done) => { - userDao - .delete(userIdx) - .then((result: number) => { - expect(result).eq(1); - done(); + await userDao.create( + MockGenerator.createUserInputDTO({ + email: 'deleteTest@afume.com', }) - .catch((err: Error) => done(err)); - }); + ) + ).idx; }); + + new TestSingle( + 'common', + userDao.delete, + [[userIdx]], + (res: number | null, err: any, _: any[]) => { + expect(err).to.be.not.null; + expect(res).to.be.eq(1); + } + ).test(it); + after(async () => { await User.destroy({ where: { email: 'deleteTest@afume.com' } }); }); }); - describe('# postSurvey Test', () => { + describe.skip('# postSurvey Test', () => { it('# success case', (done: Done) => { // TODO set mongoDB test done(); diff --git a/tests/test_unit/dao/common/presets.js b/tests/test_unit/dao/common/presets.js index 37303f4f..c2829694 100644 --- a/tests/test_unit/dao/common/presets.js +++ b/tests/test_unit/dao/common/presets.js @@ -5,11 +5,17 @@ if (properties.NODE_ENV != 'test') { throw new Error('Only allow TEST ENV'); } -module.exports = async (context) => { - context.timeout(100000); - if (properties.NODE_ENV != 'test') { - throw new Error('Only allow TEST ENV'); - } +let firstTime = true; +let ready = false; + +const LONG_TIME = 60000; +const VERY_LONG_TIME = 120000; + +async function initialDatabase(context) { + console.log(`presets start`); + firstTime = false; + const start = new Date().getSeconds(); + context.timeout(LONG_TIME); await Promise.all([ sequelize .query('SET FOREIGN_KEY_CHECKS = 0') @@ -28,6 +34,45 @@ module.exports = async (context) => { await sequelize.query( 'CREATE FULLTEXT INDEX ft_idx_brand_name ON brands(`name`, `english_name`)' ); - context.timeout(); + const end = new Date().getSeconds(); + console.log(`presets done / time : ${end - start}s`); + ready = true; +} + +const resolveCapture = (context, resolve, limitTimeout) => { + context.timeout(LONG_TIME); + return () => { + clearTimeout(limitTimeout); + resolve(); + context.timeout(); + }; +}; +const queue = []; + +module.exports = async (context) => { + if (properties.NODE_ENV != 'test') { + throw new Error('Only allow TEST ENV'); + } + + if (firstTime) { + await initialDatabase(context); + for (const resolveCapture of queue) { + resolveCapture(); + } + return true; + } + if (!ready) { + return new Promise((resolve, reject) => { + const limitTimeout = setTimeout(() => { + reject( + new Error( + `More than ${VERY_LONG_TIME} seconds have passed in the queue.` + ) + ); + }, VERY_LONG_TIME); + queue.push(resolveCapture(context, resolve, limitTimeout)); + }); + } + return true; }; diff --git a/tests/test_unit/service/UserService.spec.ts b/tests/test_unit/service/UserService.spec.ts index df3bc08e..d843c6d0 100644 --- a/tests/test_unit/service/UserService.spec.ts +++ b/tests/test_unit/service/UserService.spec.ts @@ -1,21 +1,19 @@ import dotenv from 'dotenv'; import { expect } from 'chai'; -import { Done } from 'mocha'; dotenv.config(); -import { - WrongPasswordError, - PasswordPolicyError, - UnExpectedError, -} from '@errors'; +import { WrongPasswordError, PasswordPolicyError } from '@errors'; + +import { TokenGroupDTO, LoginInfoDTO } from '@dto/index'; import UserService from '@services/UserService'; import { UserAuthDTO, UserDTO, TokenSetDTO } from '@dto/index'; -import LoginInfoMockHelper from '../mock_helper/LoginInfoMockHelper'; -import TokenGroupMockHelper from '../mock_helper/TokenGroupMockHelper'; +import { GENDER_WOMAN, GENDER_MAN } from '@utils/constants'; + import UserMockHelper from '../mock_helper/UserMockHelper'; +import { composeValidator, TestSingle } from '../../internal/TestHelper'; const mockUserDao = Object.assign({}, require('../dao/UserDao.mock.js')); const mockTokenDao = Object.assign({}); @@ -27,249 +25,274 @@ const userService = new UserService( mockJWT ); -describe('# User Service Test', () => { - describe('# createUser Test', () => { - it('# success Test', (done: Done) => { - userService - .createUser({}) - .then((result) => { - TokenGroupMockHelper.validTest.call(result); - done(); - }) - .catch((err: Error) => done(err)); +class MockGenerator { + static _userIdx: number = 6; + + static createUserDTO(json: any = {}): UserDTO { + const userIdx: number = json.userIdx ? json.userIdx : this._userIdx++; + const expected: any = { + userIdx, + nickname: `user${userIdx}`, + password: userService.crypto.encrypt('encrypted'), + gender: GENDER_WOMAN, + email: `email${userIdx}@afume.com`, + birth: 1995, + grade: 1, + accessTime: '2021-07-13T11:33:49.000Z', + createdAt: '2021-07-13T11:33:49.000Z', + updatedAt: '2021-08-07T09:20:29.000Z', + }; + return UserDTO.createByJson(Object.assign(expected, json)); + } +} + +describe('▣ UserService', () => { + describe('▶ create Test', () => { + describe('# method: createUser', () => { + new TestSingle( + 'common', + userService.createUser, + [{}], + (result: TokenGroupDTO | null, err: any, _: any[]) => { + expect(result).to.be.not.null; + expect(err).to.be.eq(null); + + const token: TokenGroupDTO = result!!; + expect(token).to.be.instanceOf(TokenGroupDTO); + expect(token.userIdx).to.be.ok; + expect(token.token).to.be.ok; + expect(token.refreshToken).to.be.ok; + } + ).test(it); }); }); - describe('# authUser Test', () => { - it('# success Test', (done: Done) => { - userService - .authUser('token') - .then((result) => { - expect(result).to.be.instanceOf(UserAuthDTO); - expect(result.isAuth).to.be.true; - expect(result.isAdmin).to.be.true; - done(); - }) - .catch((err: Error) => done(err)); - }); - it('# With No Auth', (done: Done) => { + describe('▶ auth Test', () => { + describe('# method: authUser', () => { + function commonValidator( + result: UserAuthDTO | null, + err: any, + _: any[] + ) { + expect(result).to.be.not.null; + expect(err).to.be.eq(null); + + expect(result).to.be.instanceOf(UserAuthDTO); + } + + new TestSingle( + 'common', + userService.authUser, + ['token'], + composeValidator( + commonValidator, + (result: UserAuthDTO | null, _: any, __: any[]) => { + expect(result!!.isAuth).to.be.true; + expect(result!!.isAdmin).to.be.true; + } + ) + ).test(it); + mockJWT.verify = () => { throw 'error'; }; - userService - .authUser('token') - .then((result) => { - expect(result.isAuth).to.be.false; - expect(result.isAdmin).to.be.false; - done(); - }) - .catch((err: Error) => done(err)); + + new TestSingle( + 'no auth', + userService.authUser, + ['token'], + composeValidator( + commonValidator, + (result: UserAuthDTO | null, _: any, __: any[]) => { + expect(result!!.isAuth).to.be.false; + expect(result!!.isAdmin).to.be.false; + } + ) + ).test(it); }); - }); + describe('# method: loginUser', () => { + function loginInfoValidator( + result: LoginInfoDTO | null, + _: any, + __: any[] + ) { + const loginInfo: LoginInfoDTO = result!!; + expect(loginInfo.userIdx).to.be.ok; + expect(loginInfo.nickname).to.be.ok; + expect(loginInfo.gender).to.be.oneOf([ + GENDER_WOMAN, + GENDER_MAN, + ]); + expect(loginInfo.birth).to.be.gte(1900); + expect(loginInfo.email).to.be.ok; + expect(loginInfo.token).to.be.ok; + expect(loginInfo.refreshToken).to.be.ok; + } - describe('# loginUser Test', () => { - mockTokenDao.create = async (_: TokenSetDTO) => { - return true; - }; - it('# wrong password', (done: Done) => { - mockUserDao.read = () => { - return UserDTO.createByJson({ - userIdx: 1, - nickname: 'user1', - password: userService.crypto.encrypt('encrypted'), - gender: 2, - email: 'email1@afume.com', - birth: 1995, - grade: 1, - accessTime: '2021-07-13T11:33:49.000Z', - createdAt: '2021-07-13T11:33:49.000Z', - updatedAt: '2021-08-07T09:20:29.000Z', - }); - }; - userService - .loginUser('', userService.crypto.encrypt('password')) - .then(() => { - done(new UnExpectedError(WrongPasswordError)); - }) - .catch((err) => { + new TestSingle( + 'wrong password', + userService.loginUser, + ['', userService.crypto.encrypt('password')], + (result: LoginInfoDTO | null, err: any, __: any[]) => { + expect(result).to.be.null; expect(err).to.be.instanceOf(WrongPasswordError); - done(); + } + ) + .addPrecondition(() => { + mockTokenDao.create = async (_: TokenSetDTO) => { + return true; + }; + + mockUserDao.read = () => { + return MockGenerator.createUserDTO({ + password: userService.crypto.encrypt('encrypted'), + }); + }; }) - .catch((err: Error) => done(err)); - }); - it('# success case', (done: Done) => { - mockUserDao.read = () => { - return UserDTO.createByJson({ - userIdx: 1, - nickname: 'user1', - password: userService.crypto.encrypt('encrypted'), - gender: 2, - email: 'email1@afume.com', - birth: 1995, - grade: 1, - accessTime: '2021-07-13T11:33:49.000Z', - createdAt: '2021-07-13T11:33:49.000Z', - updatedAt: '2021-08-07T09:20:29.000Z', - }); - }; - userService - .loginUser('', userService.crypto.encrypt('encrypted')) - .then((result) => { - LoginInfoMockHelper.validTest.call(result); - done(); + .test(it); + + new TestSingle( + 'common', + userService.loginUser, + ['', userService.crypto.encrypt('encrypted')], + composeValidator( + loginInfoValidator, + (result: LoginInfoDTO | null, err: any, __: any[]) => { + expect(result).to.be.not.null; + expect(err).to.be.null; + } + ) + ) + .addPrecondition(() => { + mockUserDao.read = () => { + return MockGenerator.createUserDTO({ + password: userService.crypto.encrypt('encrypted'), + }); + }; }) - .catch((err: Error) => done(err)); + .test(it); }); - }); - describe('# updateUser Test', () => { - it('# success Test', (done: Done) => { - userService - .updateUser({ userIdx: 1 }) - .then((it: UserDTO) => { - UserMockHelper.validTest.call(it); - done(); + describe('# method: checkPassword', () => { + new TestSingle( + 'valid password', + userService.checkPassword, + [1, userService.crypto.encrypt('encrypted')], + (result: boolean | null, err: any, __: any[]) => { + expect(result).to.be.true; + expect(err).to.be.null; + } + ) + .addPrecondition(() => { + mockUserDao.readByIdx = () => { + return MockGenerator.createUserDTO({ + password: userService.crypto.encrypt('encrypted'), + }); + }; + }) + .test(it); + + new TestSingle( + 'invalid password', + userService.checkPassword, + [1, userService.crypto.encrypt('encrypted2')], + (result: boolean | null, err: any, __: any[]) => { + expect(result).to.be.false; + expect(err).to.be.null; + } + ) + .addPrecondition(() => { + mockUserDao.readByIdx = () => { + return MockGenerator.createUserDTO({ + password: userService.crypto.encrypt('encrypted'), + }); + }; }) - .catch((err: Error) => done(err)); + .test(it); }); }); - describe('# changePassword Test', () => { - it('# success case', (done: Done) => { - mockUserDao.readByIdx = () => { - return UserDTO.createByJson({ - userIdx: 1, - nickname: 'user1', - password: userService.crypto.encrypt('encrypted'), - gender: 2, - email: 'email1@afume.com', - birth: 1995, - grade: 1, - accessTime: '2021-07-13T11:33:49.000Z', - createdAt: '2021-07-13T11:33:49.000Z', - updatedAt: '2021-08-07T09:20:29.000Z', - }); - }; - userService - .changePassword( + describe('▶ update Test', () => { + describe('# method: updateUser', () => { + new TestSingle( + 'common', + userService.updateUser, + [{ userIdx: 1 }], + (result: UserDTO | null, err: any, _: any[]) => { + expect(result).to.be.not.null; + expect(err).to.be.null; + UserMockHelper.validTest.call(result!!); + } + ).test(it); + }); + + describe('# method: changePassword', () => { + new TestSingle( + 'common', + userService.changePassword, + [ 1, userService.crypto.encrypt('encrypted'), - userService.crypto.encrypt('newpassword') - ) - .then(() => { - done(); + userService.crypto.encrypt('newpassword'), + ], + (result: number | null, err: any, _: any[]) => { + expect(result).to.be.not.null; + expect(err).to.be.null; + expect(result).to.be.eq(1); + } + ) + .addPrecondition(() => { + mockUserDao.readByIdx = () => { + return MockGenerator.createUserDTO({ + password: userService.crypto.encrypt('encrypted'), + }); + }; }) - .catch((err: Error) => done(err)); - }); + .test(it); - it('# wrong prev password', (done: Done) => { - mockUserDao.readByIdx = () => { - return UserDTO.createByJson({ - userIdx: 1, - nickname: 'user1', - password: userService.crypto.encrypt('encrypted'), - gender: 2, - email: 'email1@afume.com', - birth: 1995, - grade: 1, - accessTime: '2021-07-13T11:33:49.000Z', - createdAt: '2021-07-13T11:33:49.000Z', - updatedAt: '2021-08-07T09:20:29.000Z', - }); - }; - userService - .changePassword( + new TestSingle( + 'wrong password', + userService.changePassword, + [ 1, userService.crypto.encrypt('wrong'), - userService.crypto.encrypt('') - ) - .then(() => { - done(new UnExpectedError(WrongPasswordError)); - }) - .catch((err: Error) => { + userService.crypto.encrypt(''), + ], + (result: number | null, err: any, _: any[]) => { + expect(result).to.be.null; expect(err).to.be.instanceOf(WrongPasswordError); - done(); + } + ) + .addPrecondition(() => { + mockUserDao.readByIdx = () => { + return MockGenerator.createUserDTO({ + password: userService.crypto.encrypt('encrypted'), + }); + }; }) - .catch((err: Error) => done(err)); - }); + .test(it); - it('# same password(restrict by password policy)', (done: Done) => { - mockUserDao.readByIdx = () => { - return UserDTO.createByJson({ - userIdx: 1, - nickname: 'user1', - password: userService.crypto.encrypt('encrypted'), - gender: 2, - email: 'email1@afume.com', - birth: 1995, - grade: 1, - accessTime: '2021-07-13T11:33:49.000Z', - createdAt: '2021-07-13T11:33:49.000Z', - updatedAt: '2021-08-07T09:20:29.000Z', - }); - }; - userService - .changePassword( + new TestSingle( + 'same password', + userService.changePassword, + [ 1, userService.crypto.encrypt('encrypted'), - userService.crypto.encrypt('encrypted') - ) - .then(() => { - done(new UnExpectedError(PasswordPolicyError)); - }) - .catch((err: Error) => { + userService.crypto.encrypt('encrypted'), + ], + (result: number | null, err: any, _: any[]) => { + expect(result).to.be.null; expect(err).to.be.instanceOf(PasswordPolicyError); - done(); - }) - .catch((err: Error) => done(err)); - }); - }); - describe('# checkPassword Test', () => { - it('# case: valid password', (done: Done) => { - mockUserDao.readByIdx = () => { - return UserDTO.createByJson({ - userIdx: 1, - nickname: 'user1', - password: userService.crypto.encrypt('encrypted'), - gender: 2, - email: 'email1@afume.com', - birth: 1995, - grade: 1, - accessTime: '2021-07-13T11:33:49.000Z', - createdAt: '2021-07-13T11:33:49.000Z', - updatedAt: '2021-08-07T09:20:29.000Z', - }); - }; - userService - .checkPassword(1, userService.crypto.encrypt('encrypted')) - .then((isSuccess: boolean) => { - expect(isSuccess).to.be.true; - done(); - }) - .catch((err: Error) => done(err)); - }); - it('# case: invalid password', (done: Done) => { - mockUserDao.readByIdx = () => { - return UserDTO.createByJson({ - userIdx: 1, - nickname: 'user1', - password: userService.crypto.encrypt('encrypted'), - gender: 2, - email: 'email1@afume.com', - birth: 1995, - grade: 1, - accessTime: '2021-07-13T11:33:49.000Z', - createdAt: '2021-07-13T11:33:49.000Z', - updatedAt: '2021-08-07T09:20:29.000Z', - }); - }; - userService - .checkPassword(1, userService.crypto.encrypt('encrypted2')) - .then((isSuccess: boolean) => { - expect(isSuccess).to.be.false; - done(); + } + ) + .addPrecondition(() => { + mockUserDao.readByIdx = () => { + return MockGenerator.createUserDTO({ + password: userService.crypto.encrypt('encrypted'), + }); + }; }) - .catch((err: Error) => done(err)); + .test(it); }); }); }); From b31367b7996ff2db8dbd5b5fbd0fb7626edd52ca Mon Sep 17 00:00:00 2001 From: Heesung Youn Date: Sat, 28 Jan 2023 01:45:16 +0900 Subject: [PATCH 4/7] Feat/issue 446 (#475) * It is better to check for errors first * Set gender and birth to nullable --- src/controllers/User.ts | 19 ++---- src/controllers/definitions/request/user.ts | 70 +++++++++----------- src/controllers/definitions/response/user.ts | 4 +- src/dao/PerfumeDao.ts | 38 +++++++---- src/data/dto/UserDTO.ts | 32 ++++----- src/data/dto/UserInputDTO.ts | 50 -------------- src/data/dto/index.ts | 1 - src/models/user.js | 4 +- src/service/PerfumeService.ts | 25 ++++--- src/service/ReviewService.js | 13 ++-- src/service/UserService.ts | 15 ----- src/utils/constants.ts | 6 -- tests/test_unit/dao/UserDao.spec.ts | 15 +++-- 13 files changed, 111 insertions(+), 181 deletions(-) delete mode 100644 src/data/dto/UserInputDTO.ts diff --git a/src/controllers/User.ts b/src/controllers/User.ts index 042ac0af..4cacf3aa 100644 --- a/src/controllers/User.ts +++ b/src/controllers/User.ts @@ -4,7 +4,7 @@ import { logger, LoggerHelper } from '@modules/winston'; import { UnAuthorizedError } from '@errors'; -import { GENDER_NONE, BIRTH_NONE, GRADE_USER } from '@utils/constants'; +import { GRADE_USER } from '@utils/constants'; import { MSG_REGISTER_SUCCESS, @@ -171,16 +171,7 @@ const loginUser: RequestHandler = ( const password: string = req.body.password; User.loginUser(email, password) .then((result: LoginInfoDTO) => { - const loginResponse: any = LoginResponse.createByJson(result); - // TODO gender, birth nullable로 변경한 이후 아래 코드 삭제하기 - if (loginResponse.gender == GENDER_NONE) { - delete loginResponse.gender; - } - if (loginResponse.birth == BIRTH_NONE) { - delete loginResponse.birth; - } - - return loginResponse; + return LoginResponse.createByJson(result); }) .then((response: LoginResponse) => { LoggerHelper.logTruncated( @@ -686,8 +677,10 @@ const updateUser: RequestHandler = ( next(new UnAuthorizedError()); return; } - const userEditRequest = UserEditRequest.createByJson(req.body); - User.updateUser(userEditRequest.toUserInputDTO(userIdx)) + const userEditRequest = UserEditRequest.createByJson( + Object.assign({}, { userIdx }, req.body) + ); + User.updateUser(userEditRequest.toUserInputDTO()) .then((result: UserDTO) => { return UserResponse.createByJson(result); }) diff --git a/src/controllers/definitions/request/user.ts b/src/controllers/definitions/request/user.ts index 7f098448..aebb92f9 100644 --- a/src/controllers/definitions/request/user.ts +++ b/src/controllers/definitions/request/user.ts @@ -2,18 +2,9 @@ import { logger } from '@modules/winston'; import { InvalidInputError } from '@src/utils/errors/errors'; import { GenderMap, GradeMap, GradeKey } from '@utils/enumType'; -import { GRADE_USER, GENDER_NONE } from '@utils/constants'; +import { GRADE_USER } from '@utils/constants'; import { UserInputDTO } from '@src/data/dto'; -interface UserInputRequest { - grade?: GradeKey; - gender?: string | null; - nickname?: string; - password?: string; - email?: string; - birth?: number | null; -} - /** * @swagger * definitions: @@ -35,7 +26,8 @@ interface UserInputRequest { * type: string * enum: [USER, MANAGER, SYSTEM_ADMIN] * */ -class UserEditRequest implements UserInputRequest { +class UserEditRequest { + readonly userIdx?: number; readonly grade?: GradeKey; readonly gender?: string | null; readonly nickname?: string; @@ -43,6 +35,7 @@ class UserEditRequest implements UserInputRequest { readonly email?: string; readonly birth?: number | null; constructor( + userIdx?: number, nickname?: string, password?: string, gender?: string | null, @@ -50,6 +43,7 @@ class UserEditRequest implements UserInputRequest { birth?: number | null, grade?: GradeKey ) { + this.userIdx = userIdx; this.grade = grade; this.gender = gender; this.nickname = nickname; @@ -62,12 +56,13 @@ class UserEditRequest implements UserInputRequest { return `${this.constructor.name} (${JSON.stringify(this)})`; } - public toUserInputDTO(userIdx: number): UserInputDTO { - return createByRequest(userIdx, this); + public toUserInputDTO(): UserInputDTO { + return createByRequest(this); } static createByJson(json: any): UserEditRequest { return new UserEditRequest( + json.userIdx, json.nickname, json.password, json.gender, @@ -112,7 +107,7 @@ class UserEditRequest implements UserInputRequest { * enum: [USER, MANAGER, SYSTEM_ADMIN] * example: USER * */ -class UserRegisterRequest implements UserInputRequest { +class UserRegisterRequest { readonly nickname: string; readonly password: string; readonly gender: string | null; @@ -140,7 +135,7 @@ class UserRegisterRequest implements UserInputRequest { } public toUserInputDTO(): UserInputDTO { - return createByRequest(undefined, this); + return createByRequest(this); } static createByJson(json: any): UserRegisterRequest { @@ -157,37 +152,34 @@ class UserRegisterRequest implements UserInputRequest { const LOG_TAG: string = '[definition/UserInputRequest]'; -function createByRequest( - userIdx: number | undefined, - request: UserInputRequest -): UserInputDTO { - let genderVal: number = GENDER_NONE; - if (request.gender) { - if (GenderMap[request.gender] == undefined) { - logger.debug(`${LOG_TAG} invalid gender: ${request.gender}`); +function createByRequest(json: any): UserInputDTO { + let genderVal: number | null = null; + if (json.gender) { + if (GenderMap[json.gender] == undefined) { + logger.debug(`${LOG_TAG} invalid gender: ${json.gender}`); throw new InvalidInputError(); } - genderVal = GenderMap[request.gender]; + genderVal = GenderMap[json.gender]; } let gradeCode: number = GRADE_USER; - if (request.grade) { - if (GradeMap[request.grade] == undefined) { - logger.debug(`${LOG_TAG} invalid grade: ${request.grade}`); + if (json.grade) { + if (GradeMap[json.grade] == undefined) { + logger.debug(`${LOG_TAG} invalid grade: ${json.grade}`); throw new InvalidInputError(); } - gradeCode = GradeMap[request.grade]; + gradeCode = GradeMap[json.grade]; } - return new UserInputDTO( - userIdx, - request.nickname, - request.password, - genderVal, - request.email, - request.birth || 0, - gradeCode, - undefined - ); + return { + userIdx: json!!.userIdx, + nickname: json!!.nickname, + password: json!!.password, + gender: genderVal || null, + email: json.email, + birth: json.birth || null, + grade: gradeCode, + accessTime: undefined, + }; } -export { UserInputRequest, UserEditRequest, UserRegisterRequest }; +export { UserEditRequest, UserRegisterRequest }; diff --git a/src/controllers/definitions/response/user.ts b/src/controllers/definitions/response/user.ts index 04967254..22e2f8dd 100644 --- a/src/controllers/definitions/response/user.ts +++ b/src/controllers/definitions/response/user.ts @@ -1,4 +1,3 @@ -import { BIRTH_NONE } from '@src/utils/constants'; import { GenderInvMap } from '@utils/enumType'; /** @@ -63,8 +62,7 @@ class LoginResponse { static createByJson(json: any): LoginResponse { const genderString: string | null = GenderInvMap[json.gender] || null; - const birth: number | null = - json.birth == BIRTH_NONE ? null : json.birth; + const birth: number | null = json.birth; return new LoginResponse( json.userIdx, json.nickname, diff --git a/src/dao/PerfumeDao.ts b/src/dao/PerfumeDao.ts index 0300a752..1be4029e 100644 --- a/src/dao/PerfumeDao.ts +++ b/src/dao/PerfumeDao.ts @@ -484,16 +484,21 @@ class PerfumeDao { * @returns {Promise} */ async readPerfumeSurvey( - gender: number + gender: number | null ): Promise> { - logger.debug(`${LOG_TAG} readPerfumeSurvey(gender = ${gender})`); + logger.debug( + `${LOG_TAG} readPerfumeSurvey(gender = ${gender || 'all'})` + ); const options = _.merge({}, defaultOption); + const where: any = gender + ? { + gender, + } + : {}; options.include.push({ model: PerfumeSurvey, as: 'PerfumeSurvey', - where: { - gender, - }, + where, require: true, }); return Perfume.findAndCountAll(options).then((it: any) => { @@ -522,18 +527,27 @@ class PerfumeDao { * @param {number} size * @returns {Promise} */ - async getSimilarPerfumeIdxList(perfumeIdx: number, size: number): Promise { + async getSimilarPerfumeIdxList( + perfumeIdx: number, + size: number + ): Promise { try { - logger.debug(`${LOG_TAG} getSimilarPerfumeIdxList(perfumeIdx = ${perfumeIdx}, size = ${size})`); - + logger.debug( + `${LOG_TAG} getSimilarPerfumeIdxList(perfumeIdx = ${perfumeIdx}, size = ${size})` + ); + const client = require('@utils/db/redis.js'); - const result = await client.lrange(`recs.perfume:${perfumeIdx}`, 0, size-1); - - return result.map((it: string) => Number(it)); + const result = await client.lrange( + `recs.perfume:${perfumeIdx}`, + 0, + size - 1 + ); + + return result.map((it: string) => Number(it)); } catch (err) { throw err; - } + } } /** diff --git a/src/data/dto/UserDTO.ts b/src/data/dto/UserDTO.ts index ad074f34..95efb43c 100644 --- a/src/data/dto/UserDTO.ts +++ b/src/data/dto/UserDTO.ts @@ -4,9 +4,9 @@ class UserDTO { readonly userIdx: number; readonly nickname: string; readonly password: string; - readonly gender: Gender; + readonly gender: Gender | null; readonly email: string; - readonly birth: number; + readonly birth: number | null; readonly grade: Grade; readonly accessTime: string; readonly createdAt: string; @@ -15,9 +15,9 @@ class UserDTO { userIdx: number, nickname: string, password: string, - gender: Gender, + gender: Gender | null, email: string, - birth: number, + birth: number | null, grade: Grade, accessTime: string, createdAt: string, @@ -41,18 +41,20 @@ class UserDTO { static createByJson(json: any): UserDTO { return new UserDTO( - json.userIdx, - json.nickname, - json.password, - json.gender, - json.email, - json.birth, - json.grade, - json.accessTime + '', - json.createdAt + '', - json.updatedAt + '' + json.userIdx!!, + json.nickname!!, + json.password!!, + json.gender || null, + json.email!!, + json.birth || null, + json.grade!!, + json.accessTime!! + '', + json.createdAt!! + '', + json.updatedAt!! + '' ); } } -export { UserDTO }; +type UserInputDTO = Partial; + +export { UserDTO, UserInputDTO }; diff --git a/src/data/dto/UserInputDTO.ts b/src/data/dto/UserInputDTO.ts deleted file mode 100644 index 4c29aec2..00000000 --- a/src/data/dto/UserInputDTO.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Gender, Grade } from '@utils/enumType'; - -class UserInputDTO { - readonly userIdx?: number; - readonly nickname?: string; - readonly password?: string; - readonly gender?: Gender; - readonly email?: string; - readonly birth?: number; - readonly grade?: Grade; - readonly accessTime?: string; - constructor( - userIdx?: number, - nickname?: string, - password?: string, - gender?: Gender, - email?: string, - birth?: number, - grade?: Grade, - accessTime?: string - ) { - this.userIdx = userIdx; - this.nickname = nickname; - this.password = password; - this.gender = gender; - this.email = email; - this.birth = birth; - this.grade = grade; - this.accessTime = accessTime; - } - - public toString(): string { - return `${this.constructor.name} (${JSON.stringify(this)})`; - } - - static createByJson(json: any): UserInputDTO { - return new UserInputDTO( - json.userIdx, - json.nickname, - json.password, - json.gender, - json.email, - json.birth, - json.grade, - json.accessTime - ); - } -} - -export { UserInputDTO }; diff --git a/src/data/dto/index.ts b/src/data/dto/index.ts index 657432fc..be965a47 100644 --- a/src/data/dto/index.ts +++ b/src/data/dto/index.ts @@ -27,5 +27,4 @@ export * from './TokenGroupDTO'; export * from './TokenPayloadDTO'; export * from './UserAuthDTO'; export * from './UserDTO'; -export * from './UserInputDTO'; export * from './TokenSetDTO'; diff --git a/src/models/user.js b/src/models/user.js index e0c97c92..ed44ab25 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -25,12 +25,12 @@ module.exports = (sequelize, DataTypes) => { }, gender: { type: DataTypes.INTEGER, - allowNull: false, + allowNull: true, comment: '1: 남자, 2: 여자', }, birth: { type: DataTypes.INTEGER, - allowNull: false, + allowNull: true, }, grade: { type: DataTypes.INTEGER, diff --git a/src/service/PerfumeService.ts b/src/service/PerfumeService.ts index 5eaf1c4e..f9c65243 100644 --- a/src/service/PerfumeService.ts +++ b/src/service/PerfumeService.ts @@ -57,6 +57,9 @@ const commonJob = [ 'updatedAt' ), ]; + +const DEFAULT_GENDER: number = GENDER_WOMAN; +const DEFAULT_AGE: number = 20; class PerfumeService { /** * 향수 세부 정보 조회 @@ -219,14 +222,16 @@ class PerfumeService { try { logger.debug( `${LOG_TAG} updateSimilarPerfumes(perfumeSimilarRequest = ${perfumeSimilarRequest})` - ); - - return await perfumeDao.updateSimilarPerfumes(perfumeSimilarRequest) - } catch(err: any) { + ); + + return await perfumeDao.updateSimilarPerfumes( + perfumeSimilarRequest + ); + } catch (err: any) { throw err; } } - + /** * 향수 좋아요 * @@ -551,14 +556,16 @@ class PerfumeService { ): Promise<{ gender: number; ageGroup: number }> { if (userIdx == -1) { return { - gender: GENDER_WOMAN, - ageGroup: 20, + gender: DEFAULT_GENDER, + ageGroup: DEFAULT_AGE, }; } const user: UserDTO = await userDao.readByIdx(userIdx); const today: Date = new Date(); - const age: number = today.getFullYear() - user.birth + 1; - const gender: number = user.gender; + const age: number = user.birth + ? today.getFullYear() - user.birth + 1 + : DEFAULT_AGE; + const gender: number = user.gender || DEFAULT_GENDER; const ageGroup: number = Math.floor(age / 10) * 10; return { gender, ageGroup }; } diff --git a/src/service/ReviewService.js b/src/service/ReviewService.js index 258c2bfc..f0263533 100644 --- a/src/service/ReviewService.js +++ b/src/service/ReviewService.js @@ -13,7 +13,6 @@ import LikePerfumeDao from '@dao/LikePerfumeDao'; import ReviewDao from '@dao/ReviewDao'; import KeywordDao from '../dao/KeywordDao'; import { PRIVATE } from '@src/utils/strings'; -import { BIRTH_NONE, GENDER_NONE } from '@src/utils/constants'; const userDao = new UserDao(); const likePerfumeDao = new LikePerfumeDao(); @@ -251,7 +250,9 @@ exports.getReviewOfPerfumeByLike = async ({ perfumeIdx, userIdx }) => { const result = await reviewList.reduce(async (prevPromise, it) => { let prevResult = await prevPromise.then(); - const approxAge = getApproxAge(it.User.birth); + const approxAge = it.User.birth + ? getApproxAge(it.User.birth) + : PRIVATE; const readLikeResult = await likeReviewDao.read( userIdx, it.reviewIdx @@ -263,18 +264,12 @@ exports.getReviewOfPerfumeByLike = async ({ perfumeIdx, userIdx }) => { content: it.content, likeCount: it.LikeReview.likeCount, isLiked: readLikeResult ? true : false, - userGender: it.User.gender, + userGender: it.User.gender || PRIVATE, age: approxAge, nickname: it.User.nickname, createTime: it.createdAt, isReported: reportedReviewIdxList.includes(it.reviewIdx), }; - if (currentResult.userGender == GENDER_NONE) { - currentResult.userGender = GENDER_NONE; // PRIVATE; - } - if (it.User.birth == BIRTH_NONE) { - currentResult.age = PRIVATE; - } prevResult.push(currentResult); return Promise.resolve(prevResult); }, Promise.resolve([])); diff --git a/src/service/UserService.ts b/src/service/UserService.ts index f02b4843..6aa25a1e 100644 --- a/src/service/UserService.ts +++ b/src/service/UserService.ts @@ -24,7 +24,6 @@ import { SurveyDTO, TokenSetDTO, } from '@dto/index'; -import { BIRTH_NONE, GENDER_NONE } from '@src/utils/constants'; const LOG_TAG: string = '[User/Service]'; @@ -66,13 +65,6 @@ class UserService { .then((user: UserDTO | any) => { delete user.password; const payload = Object.assign({}, user); - // TODO gender, birth nullable로 변경한 이후 아래 코드 삭제하기 - if (payload.gender == GENDER_NONE) { - delete payload.gender; - } - if (payload.birth == BIRTH_NONE) { - delete payload.birth; - } const { userIdx } = user; const { token, refreshToken } = this.jwt.publish(payload); @@ -174,13 +166,6 @@ class UserService { this.userDao.updateAccessTime(user.userIdx); const payload: any = TokenPayloadDTO.createByJson(user); - // TODO gender, birth nullable로 변경한 이후 아래 코드 삭제하기 - if (payload.gender == GENDER_NONE) { - delete payload.gender; - } - if (payload.birth == BIRTH_NONE) { - delete payload.birth; - } const { token, refreshToken } = this.jwt.publish(payload); await this.tokenDao diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 367479ed..86a28fc4 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -11,10 +11,6 @@ import { SINGLE, } from '@utils/strings'; -// TODO: Remove below variables after refactoring -const BIRTH_NONE: number = 0; -const GENDER_NONE: number = 0; - const GENDER_MAN: number = 1; const GENDER_WOMAN: number = 2; const GRADE_USER: number = 0; @@ -60,8 +56,6 @@ const ACCESS_PUBLIC: number = 1; const ACCESS_PRIVATE: number = 0; export { - BIRTH_NONE, - GENDER_NONE, GENDER_MAN, GENDER_WOMAN, GRADE_USER, diff --git a/tests/test_unit/dao/UserDao.spec.ts b/tests/test_unit/dao/UserDao.spec.ts index 11522545..9b27f77b 100644 --- a/tests/test_unit/dao/UserDao.spec.ts +++ b/tests/test_unit/dao/UserDao.spec.ts @@ -40,7 +40,7 @@ class MockGenerator { birth: '1995', grade: 1, }; - return UserInputDTO.createByJson(Object.assign(expected, json)); + return Object.assign(expected, json); } } @@ -54,8 +54,8 @@ describe('▣ UserDao', () => { err: any, parameter: any[] ) { - expect(result).to.be.not.null; expect(err).to.be.eq(null); + expect(result).to.be.not.null; expect(result).instanceOf(CreatedResultDTO); const created: UserDTO = result!!.created; @@ -63,9 +63,10 @@ describe('▣ UserDao', () => { const input: UserInputDTO = parameter[0]; expect(created.nickname).to.be.eq(input.nickname); expect(created.password).to.be.eq(input.password); - expect(created.gender).to.be.eq(input.gender); + + expect(created.gender).to.be.eq(input.gender || null); expect(created.email).to.be.eq(input.email); - expect(created.birth).to.be.eq(input.birth); + expect(created.birth).to.be.eq(input.birth || null); expect(created.grade).to.be.eq(input.grade); } @@ -89,8 +90,8 @@ describe('▣ UserDao', () => { userIdx: null, }), (result: CreatedResultDTO, err: any) => { - expect(result).to.be.eq(null); expect(err).instanceOf(DuplicatedEntryError); + expect(result).to.be.eq(null); }, ] ).test(it); @@ -130,7 +131,7 @@ describe('▣ UserDao', () => { }), ].map((it: UserInputDTO): any[] => [it]) ) - .test(describe.skip, it); + .test(describe, it); }); }); @@ -333,7 +334,7 @@ describe('▣ UserDao', () => { }, ].map((it: any) => [it]) ) - .test(describe.skip, it); + .test(describe, it); }); describe('# method: updateAccessTime', () => { From 6be986cc12f85ef9daf642bcc4cb8c1549b21733 Mon Sep 17 00:00:00 2001 From: Heesung Youn Date: Sat, 28 Jan 2023 01:57:06 +0900 Subject: [PATCH 5/7] update version on package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 16892b89..50b636c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "a-fume-server", - "version": "0.0.4", + "version": "0.0.8", "description": "향수 정보 서비스 A.fume Server Api 문서", "main": "index.js", "type": "commonjs", From 5ecf746cc59235400a0d7ee6cb8b0b92d058ad22 Mon Sep 17 00:00:00 2001 From: Heesung Youn Date: Wed, 8 Feb 2023 15:59:30 +0900 Subject: [PATCH 6/7] Feature/issue 477 device os (#478) * Add deviceOs on System Controller * Add test code for iOS case * change latest version of iOS * apply comments * Update README.md --- README.md | 2 + src/controllers/System.ts | 78 +++++++++++++++++++---- tests/test_unit/controller/System.spec.ts | 73 ++++++++++++++++++++- 3 files changed, 137 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index d0842103..ef254702 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,8 @@ npm run test-each ./test/... # 업데이트 내역 +- stage + - 엔드포인트 변경 - `/system/supportable` 내 deviceOS 파라미터 추가 - 0.0.8 - ioredis 의존 추가 - 엔드포인트 추가 - 비슷한 향수 추천 데이터 DB 반영 diff --git a/src/controllers/System.ts b/src/controllers/System.ts index d3e425ab..f7efb7f1 100644 --- a/src/controllers/System.ts +++ b/src/controllers/System.ts @@ -27,6 +27,14 @@ const LOG_TAG: string = '[System/Controller]'; * in: query * type: string * required: true + * - name: deviceOS + * in: query + * type: string + * required: false + * default: android + * enum: + * - android + * - iOS * responses: * 200: * description: 성공 @@ -47,8 +55,14 @@ const getSupportable: RequestHandler = ( _: NextFunction ) => { const apkVersion: string = req.query.apkversion?.toString() || ''; - logger.debug(`${LOG_TAG} getSupportable(apkVersion = ${apkVersion})`); - if (isSupportVersion(apkVersion)) { + // 초기 안드로이드 apk의 경우 deviceOS를 보내주지 않고 있어서 기본 값을 안드로이드로 설정. deviceOS server v0.0.9에서 추가 + const deviceOS: string = req.query.deviceOS?.toString() || 'android'; + logger.debug( + `${LOG_TAG} getSupportable(apkVersion = ${apkVersion}, deviceOS = ${deviceOS})` + ); + const versionChecker: IVersionChecker = + VersionCheckerFactory.factory(deviceOS); + if (versionChecker.isSupportVersion(apkVersion)) { res.status(StatusCode.OK).json( new ResponseDTO(MSG_GET_SUPPORTABLE_YES, true) ); @@ -103,20 +117,58 @@ class Version { } } -const prevVersion: Version = new Version(1, 4, 0); -const latestVersion: Version = new Version(1, 4, 1); +class VersionCheckerFactory { + static factory(deviceOS: string): IVersionChecker { + if (deviceOS == 'iOS') { + return new VersionCheckeriOS(); + } + return new VersionCheckerAndroid(); + } +} + +interface IVersionChecker { + isSupportVersion(apkVersion: string): Boolean; +} + +class VersionCheckerAndroid implements IVersionChecker { + // TODO: prevVersion, latestVersion을 삭제하고 minimumVersion으로 변경 + // 유연한 변경을 위해서 코드 레벨이 아닌 환경 변수 주입으로 변경 필요 + prevVersion: Version = new Version(1, 4, 0); + latestVersion: Version = new Version(1, 4, 1); -function isSupportVersion(apkVersion: string): Boolean { - const version: Version = Version.create(apkVersion); - if (prevVersion.isEqual(version) || latestVersion.isEqual(version)) { - return true; + isSupportVersion(apkVersion: string): Boolean { + const version: Version = Version.create(apkVersion); + if ( + this.prevVersion.isEqual(version) || + this.latestVersion.isEqual(version) + ) { + return true; + } + if (version.isOverThan(this.latestVersion)) { + return true; + } + return false; } - if (version.isOverThan(latestVersion)) { - return true; +} + +class VersionCheckeriOS implements IVersionChecker { + prevVersion: Version = new Version(1, 0, 0); + latestVersion: Version = new Version(1, 0, 1); + + isSupportVersion(apkVersion: string): Boolean { + const version: Version = Version.create(apkVersion); + if ( + this.prevVersion.isEqual(version) || + this.latestVersion.isEqual(version) + ) { + return true; + } + if (version.isOverThan(this.latestVersion)) { + return true; + } + return false; } - return false; } module.exports.getSupportable = getSupportable; -module.exports.prevVersion = prevVersion; -module.exports.latestVersion = latestVersion; +module.exports.VersionCheckerFactory = VersionCheckerFactory; diff --git a/tests/test_unit/controller/System.spec.ts b/tests/test_unit/controller/System.spec.ts index 7ea2369a..acdb35c5 100644 --- a/tests/test_unit/controller/System.spec.ts +++ b/tests/test_unit/controller/System.spec.ts @@ -17,13 +17,31 @@ import app from '@src/app'; const basePath: string = BASE_PATH; const System: any = require('@controllers/System'); +const androidChecker = System.VersionCheckerFactory.factory('android'); +const iOSChecker = System.VersionCheckerFactory.factory('iOS'); describe('# System Controller Test', () => { describe('# getSupportable Test', () => { it('# case : same version', (done: Done) => { request(app) .get( - `${basePath}/system/supportable?apkversion=${System.latestVersion}` + `${basePath}/system/supportable?apkversion=${androidChecker.latestVersion}` + ) + .expect((res: any) => { + expect(res.status).to.be.eq(StatusCode.OK); + const { message, data } = res.body; + + expect(message).to.be.eq(MSG_GET_SUPPORTABLE_YES); + expect(data).to.be.eq(true); + done(); + }) + .catch((err: Error) => done(err)); + }); + + it('# case : same version', (done: Done) => { + request(app) + .get( + `${basePath}/system/supportable?apkversion=${iOSChecker.latestVersion}&deviceOS=iOS` ) .expect((res: any) => { expect(res.status).to.be.eq(StatusCode.OK); @@ -39,7 +57,23 @@ describe('# System Controller Test', () => { it('# case : over version', (done: Done) => { request(app) .get( - `${basePath}/system/supportable?apkversion=${System.latestVersion.increase()}` + `${basePath}/system/supportable?apkversion=${androidChecker.latestVersion.increase()}` + ) + .expect((res: any) => { + expect(res.status).to.be.eq(StatusCode.OK); + const { message, data } = res.body; + + expect(message).to.be.eq(MSG_GET_SUPPORTABLE_YES); + expect(data).to.be.eq(true); + done(); + }) + .catch((err: Error) => done(err)); + }); + + it('# case : over version', (done: Done) => { + request(app) + .get( + `${basePath}/system/supportable?apkversion=${iOSChecker.latestVersion.increase()}&deviceOS=iOS` ) .expect((res: any) => { expect(res.status).to.be.eq(StatusCode.OK); @@ -55,7 +89,23 @@ describe('# System Controller Test', () => { it('# case : previous version', (done: Done) => { request(app) .get( - `${basePath}/system/supportable?apkversion=${System.prevVersion}` + `${basePath}/system/supportable?apkversion=${androidChecker.prevVersion}` + ) + .expect((res: any) => { + expect(res.status).to.be.eq(StatusCode.OK); + const { message, data } = res.body; + + expect(message).to.be.eq(MSG_GET_SUPPORTABLE_YES); + expect(data).to.be.eq(true); + done(); + }) + .catch((err: Error) => done(err)); + }); + + it('# case : previous version', (done: Done) => { + request(app) + .get( + `${basePath}/system/supportable?apkversion=${iOSChecker.prevVersion}&deviceOS=iOS` ) .expect((res: any) => { expect(res.status).to.be.eq(StatusCode.OK); @@ -81,6 +131,7 @@ describe('# System Controller Test', () => { }) .catch((err: Error) => done(err)); }); + it('# case : old version', (done: Done) => { request(app) .get(`${basePath}/system/supportable?apkversion=0.9.9`) @@ -94,5 +145,21 @@ describe('# System Controller Test', () => { }) .catch((err: Error) => done(err)); }); + + it('# case : old version', (done: Done) => { + request(app) + .get( + `${basePath}/system/supportable?apkversion=0.9.9&deviceOS=iOS` + ) + .expect((res: any) => { + expect(res.status).to.be.eq(StatusCode.OK); + const { message, data } = res.body; + + expect(message).to.be.eq(MSG_GET_SUPPORTABLE_NO); + expect(data).to.be.eq(false); + done(); + }) + .catch((err: Error) => done(err)); + }); }); }); From 7475471e1b517b3ca10c9d2f7f346781eb869978 Mon Sep 17 00:00:00 2001 From: "hee.youn" Date: Tue, 14 Feb 2023 22:58:23 +0900 Subject: [PATCH 7/7] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ef254702..c60e0698 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,8 @@ npm run test-each ./test/... # 업데이트 내역 -- stage +- 0.0.9 + - User gender, birth nullable 로 변경 - 엔드포인트 변경 - `/system/supportable` 내 deviceOS 파라미터 추가 - 0.0.8 - ioredis 의존 추가