diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 37d29b9..68c1929 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,13 +1,21 @@ ## ๐Ÿ“š PR ์š”์•ฝ / Linked Issue -ํ•ด๋‹น PR์—์„œ ์ž‘์—…ํ•œ ๋‚ด์šฉ์„ ํ•œ ์ค„๋กœ ์š”์•ฝํ•ด์ฃผ์„ธ์š”. + +ํ•ด๋‹น PR์—์„œ ์ž‘์—…ํ•œ ๋‚ด์šฉ์„ ํ•œ ์ค„๋กœ ์š”์•ฝํ•ด์ฃผ์„ธ์š”. close #{no} ## ๐Ÿ’ก ๋ณ€๊ฒฝ ์‚ฌํ•ญ + ๋””ํ…Œ์ผํ•œ ์ž‘์—… ๋‚ด์—ญ์„ ์ ์–ด์ฃผ์„ธ์š”. ์ฃผ์˜ํ•  ์‚ฌํ•ญ์ด ์žˆ๋‹ค๋ฉด ์ ์–ด์ฃผ์„ธ์š”. ๋ณ€๊ฒฝ์‚ฌํ•ญ (๋ชจ๋“ˆ ์„ค์น˜ ๋“ฑ)์ด ์žˆ๋‹ค๋ฉด ์ ์–ด์ฃผ์„ธ์š”. -## โœ… PR check list -- [ ] ์ปค๋ฐ‹ ์ปจ๋ฒค์…˜, ์ œ๋ชฉ ๋“ฑ์„ ํ™•์ธํ–ˆ๋‚˜์š”? -- [ ] ์•Œ๋งž์€ ๋ผ๋ฒจ์„ ๋‹ฌ์•˜๋‚˜์š”? -- [ ] ์…€ํ”„ ์ฝ”๋“œ๋ฆฌ๋ทฐ๋ฅผ ์ž‘์„ฑํ–ˆ๋‚˜์š”? +## ๐Ÿ“– Swagger + +API Swagger ๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค๋ฉด ์บก์ณํ•ด์ฃผ์„ธ์š”. + +## โœ… PR check list + +- [ ] ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋‚˜์š”? (unit/e2e) +- [ ] ์ปค๋ฐ‹ ์ปจ๋ฒค์…˜, ์ œ๋ชฉ ๋“ฑ์„ ํ™•์ธํ–ˆ๋‚˜์š”? +- [ ] ์•Œ๋งž์€ ๋ผ๋ฒจ์„ ๋‹ฌ์•˜๋‚˜์š”? +- [ ] ์…€ํ”„ ์ฝ”๋“œ๋ฆฌ๋ทฐ๋ฅผ ์ž‘์„ฑํ–ˆ๋‚˜์š”? diff --git a/.gitignore b/.gitignore index 4db2ce9..1fead7e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ lerna-debug.log* # env .env +.env.test # OS .DS_Store diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..0f84884 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,20 @@ +# Set the version of docker compose to use +version: '3.9' + +# The containers that compose the project +services: + db: + image: mysql:8.0 + restart: always + container_name: e2e-test-prisma + ports: + - '3306:3306' + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test + volumes: + - /var/lib/mysql + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --skip-character-set-client-handshake diff --git a/package.json b/package.json index 55772e4..8a8661c 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,14 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "yarn docker:up && sleep 6.5 && yarn migrate:test && yarn seed:test && dotenv -e .env.test -- jest --config ./test/jest-e2e.json && yarn docker:down", + "migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy", + "seed:test": "dotenv -e .env.test -- npx prisma db seed", + "docker:up": "docker-compose -f docker-compose.test.yml up -d", + "docker:down": "docker-compose -f docker-compose.test.yml down -v" + }, + "prisma": { + "seed": "cross-env NODE_ENV=test ts-node prisma/seed.ts" }, "dependencies": { "@nestjs/axios": "^2.0.0", @@ -39,6 +46,7 @@ "class-validator": "^0.14.0", "dayjs": "^1.11.9", "dotenv": "^16.0.3", + "dotenv-cli": "^7.2.1", "jest-mock-extended": "^3.0.4", "nanoid": "3.3.4", "passport": "^0.6.0", @@ -58,6 +66,7 @@ "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", + "cross-env": "^7.0.3", "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..0674ebf --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,274 @@ +import { PrismaClient } from '@prisma/client'; +import * as dayjs from 'dayjs'; +import * as utc from 'dayjs/plugin/utc'; +import * as timezone from 'dayjs/plugin/timezone'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +const prisma = new PrismaClient(); + +async function main() { + const user = await prisma.user.create({ + data: { + id: 1, + email: 'ddd', + createdAt: dayjs('2023-07-22 08:15:37.225Z').toDate(), + fcmToken: 'dd', + nickname: 'ddd', + password: 'dd', + phone: 'ddd', + profileImageURL: 'dd', + refreshToken: 'dd', + socialType: 'kakao', + title: 'dd', + updatedAt: dayjs('2023-07-22 08:15:37.225Z').toDate(), + uuid: 'dd', + appleId: null, + appleRefreshToken: null, + kakaoId: 1, + }, + }); + + const plants = await prisma.plant.createMany({ + data: [ + { + id: 1, + name: '์˜ค๋ Œ์ง€ ์ž์Šค๋ฏผ', + cycle: 2, + introduction: '๋ถ™์ž„์„ฑ์ด ์ข‹์€\n์•™์ฆ๋งž์€ ์˜ค๋ Œ์ง€ ์ž์Šค๋ฏผ', + meaning: '๋‹น์‹ ์„ ํ–ฅํ•ด', + explanation: + '1~2์ผ์— ํ•œ ๋ฒˆ ๋ฌผ์„ ์ฃผ๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•ด์š”\n๋ฌผ์„ ์ข‹์•„ํ•˜๋Š” ์ž์Šค๋ฏผ์ด ๊ณง ๊ท€์—ฌ์šด ์—ด๋งค๋ฅผ ์„ ๋ฌผํ• ๊ฑฐ์—์š”!', + circleImageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/circleImages/mindlere-circle.png', + gifURL: 'aaa', + }, + { + id: 2, + name: '๋กœ์ฆˆ๋งˆ๋ฆฌ', + cycle: 4, + introduction: '๋‹น์‹ ์˜ ํ•˜๋ฃจ๋ฅผ ์น˜์œ ํ•˜๋Š”\nํ–ฅ๊ธฐ๋กœ์šด ๋กœ์ฆˆ๋งˆ๋ฆฌ', + meaning: '๊ธฐ์–ตํ•ด ์ฃผ์„ธ์š”', + explanation: + '3~4์ผ์— ํ•œ ๋ฒˆ ๋ฌผ์„ ์ฃผ๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•ด์š”\n์ž์ฃผ ์—ฐ๋ฝํ•˜๊ณ  ๋งŽ์€ ์‹œ๊ฐ„์„ ํ•จ๊ป˜ํ•˜๋ฉฐ ์ถ”์–ต์„ ์Œ“์•„๊ฐ€์š”!', + circleImageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/circleImages/rosemari-circle.png', + gifURL: 'aaa', + }, + { + id: 3, + name: '์•„๋ฉ”๋ฆฌ์นธ ๋ธ”๋ฃจ', + cycle: 6, + introduction: '๋งค์ผ๋งค์ผ ๊ฝƒ์ด ํ”ผ๋Š”\nํ‘ธ๋ฅธ ๋น›์˜ ์•„๋ฉ”๋ฆฌ์นธ ๋ธ”๋ฃจ', + meaning: '๋‘ ์‚ฌ๋žŒ์˜ ์ธ์—ฐ', + explanation: + '์ผ์ฃผ์ผ์— ํ•œ ๋ฒˆ ๋ฌผ์„ ์ฃผ๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•ด์š”\n์ข…์ข… ์•ˆ๋ถ€๋ฅผ ๋ฌผ์œผ๋ฉด์„œ ์˜ค์†๋„์† ์ด์•ผ๊ธฐ๋ฅผ ๋‚˜๋ˆ„์–ด์š”!', + circleImageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/circleImages/americanblue-circle.png', + gifURL: 'aaa', + }, + { + id: 4, + name: '๋ฏผ๋“ค๋ ˆ', + cycle: 13, + introduction: '๊ฐ์‚ฌํ•˜๋Š” ๋งˆ์Œ์„ ๊ฐ€์ง„\n๋”ฐ๋œปํ•œ ๋ฏผ๋“ค๋ ˆ', + meaning: '์ธ์—ฐ์—์„œ์˜ ํ–‰๋ณต', + explanation: + '๋ณด๋ฆ„์— ํ•œ ๋ฒˆ ๋ฌผ์„ ์ฃผ๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•ด์š”\n๋‹น์‹ ์˜ ์—ฐ๋ฝ์ด ํ™€์”จ๊ฐ€ ๋˜์–ด ๋‚ ์•„๊ฐ€ ํ–‰๋ณต์œผ๋กœ ํ”ผ์–ด๋‚ ๊ฑฐ์—์š”!', + circleImageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/circleImages/mindlere-circle.png', + gifURL: 'ใ…ใ…', + }, + { + id: 5, + name: '์Šคํˆฌํ‚ค', + cycle: 29, + introduction: '์–ธ์ œ๋‚˜ ๋‹น์‹ ์„ ์ง€์ผœ์ฃผ๋Š”\n๋“ ๋“ ํ•œ ์Šคํˆฌํ‚ค', + meaning: '๋„ˆ๊ทธ๋Ÿฌ์›€', + explanation: + 'ํ•œ๋‹ฌ์— ํ•œ ๋ฒˆ ๋ฌผ์„ ์ฃผ๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•ด์š”\n๊ฐ€๋”์”ฉ ์—ฐ๋ฝํ•˜๋”๋ผ๋„ ์˜ค๋ž˜ ๋งŒ๋‚  ์ˆ˜ ์žˆ๊ธธ ๋ฐ”๋ผ์š”!', + circleImageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/circleImages/stuki-circle.png', + gifURL: 'ใ…ใ…', + }, + { + id: 6, + name: '๋‹จ๋ชจํ™˜', + cycle: 90, + introduction: '๋‹น์‹ ์˜ ๋ฐค์„ ์ง€์ผœ์ฃผ๋Š”\n์”ฉ์”ฉํ•œ ๋‹จ๋ชจํ™˜', + meaning: '์‚ฌ๋ž‘๊ณผ ์—ด์ •', + explanation: + '์„ธ ๋‹ฌ์— ํ•œ ๋ฒˆ ๋ฌผ์„ ์ฃผ๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•ด์š”\n์ž์ฃผ ๋ณด์ง€ ๋ชปํ•ด๋„ ๋ถ„๋ช… ๋‹น์‹ ์˜ ์—ฐ๋ฝ์„ ๊ธฐ๋‹ค๋ฆด๊ฑฐ์—์š”!', + circleImageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/circleImages/danmohwan-circle.png', + gifURL: 'ใ…ใ…ใ…', + }, + ], + skipDuplicates: true, + }); + + const plantLevels = await prisma.plantLevel.createMany({ + data: [ + { + id: 1, + plantId: 1, + level: 0, + levelName: '์–ด๋ฆฐ ๋‚˜๋ฌด', + description: '๋ฌด๋Ÿญ๋ฌด๋Ÿญ ์ž๋ž„ ์ค€๋น„๋ฅผ ํ•˜๊ณ  ์žˆ์–ด์š”!', + imageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/plantLevel/mindlere-1.png', + }, + { + id: 2, + plantId: 1, + level: 1, + levelName: '๊ฐœํ™”', + description: 'ํ•˜์–€ ๊ฝƒ์ด ํ™œ์ง ํ”ผ์–ด๋‚ฌ์–ด์š”!', + imageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/plantLevel/mindlere-2.png', + }, + { + id: 3, + plantId: 1, + level: 2, + levelName: '์—ด๋งค', + description: '๊ฝƒ์ด ๋จธ๋ฌผ๋‹ค๊ฐ„ ์ž๋ฆฌ์— ์•™์ฆ๋งž์€ ์—ด๋งค๊ฐ€ ์—ด๋ ธ๋„ค์š”!', + imageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/plantLevel/mindlere-complete.png', + }, + { + id: 4, + plantId: 2, + level: 0, + levelName: '์ƒˆ์‹น', + description: '์ƒˆ์‹น์ด ์˜์˜ฅ ์–ผ๊ตด์„ ๋‚ด๋ฐ€์—ˆ์–ด์š”!', + imageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/plantLevel/rosemari-1.png', + }, + { + id: 5, + plantId: 2, + level: 1, + levelName: '๊ฝƒ๋ง์šธ', + description: '๊ฝƒ๋ง์šธ์ด ๋ฐฉ์šธ๋ฐฉ์šธ ๋งบํ˜”์–ด์š”!', + imageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/plantLevel/rosemari-2.png', + }, + { + id: 6, + plantId: 2, + level: 2, + levelName: '๊ฐœํ™”', + description: '์˜ˆ์œ ๊ฝƒ์ด ํ™œ์ง ํ”ผ์–ด๋‚ฌ์–ด์š”!', + imageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/plantLevel/rosemari-complete.png', + }, + { + id: 7, + plantId: 3, + level: 0, + levelName: '์ƒˆ์‹น', + description: '์ƒˆ์‹น์ด ์˜์˜ฅ ์–ผ๊ตด์„ ๋‚ด๋ฐ€์—ˆ์–ด์š”!', + imageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/plantLevel/americanblue-1.png', + }, + { + id: 8, + plantId: 3, + level: 1, + levelName: '๊ฝƒ๋ง์šธ', + description: '๊ฝƒ๋ง์šธ์ด ๋ฐฉ์šธ๋ฐฉ์šธ ๋งบํ˜”์–ด์š”!', + imageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/plantLevel/americanblue-2.png', + }, + { + id: 9, + plantId: 3, + level: 2, + levelName: '๊ฐœํ™”', + description: '์˜ˆ์œ ๊ฝƒ์ด ํ™œ์ง ํ”ผ์–ด๋‚ฌ์–ด์š”!', + imageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/plantLevel/americanblue-complete.png', + }, + { + id: 10, + plantId: 4, + level: 0, + levelName: '์ƒˆ์‹น', + description: '์ƒˆ์‹น์ด ์˜์˜ฅ ์–ผ๊ตด์„ ๋‚ด๋ฐ€์—ˆ์–ด์š”!', + imageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/plantLevel/mindlere-1.png', + }, + ], + skipDuplicates: true, + }); + + const userPlant = await prisma.userPlant.create({ + data: { + id: 1, + userId: 1, + nickname: 'test', + instagram: null, + phone: null, + waterCycle: 14, + waterCount: 0, + isNotified: true, + loveGauge: 0, + createdAt: dayjs('2023-07-22 08:16:52.538Z').toDate(), + updatedAt: dayjs('2023-07-22 08:16:52.538Z').toDate(), + waterTime: null, + plantId: 4, + }, + }); + + const water = await prisma.water.createMany({ + data: [ + { + id: 1, + userPlantId: 1, + review: '๋ฆฌ๋ทฐ1', + wateringDate: dayjs('2023-07-22 08:55:31.799Z').toDate(), + updatedAt: dayjs('2023-07-22 08:55:31.799Z').toDate(), + }, + { + id: 3, + userPlantId: 1, + review: '๋ฆฌ๋ทฐ2', + wateringDate: dayjs('2023-07-22 18:16:53Z').toDate(), + updatedAt: dayjs('2023-07-22 09:16:54.635Z').toDate(), + }, + ], + skipDuplicates: true, + }); + + const waterKeword = await prisma.waterKeyword.createMany({ + data: [ + { + id: 1, + waterId: 4, + keyword: 'keyword1', + createdAt: dayjs('2023-07-22 12:20:11.257Z').toDate(), + updatedAt: dayjs('2023-07-22 12:20:11.257Z').toDate(), + }, + { + id: 2, + waterId: 4, + keyword: 'keyword2', + createdAt: dayjs('2023-07-22 12:20:11.257Z').toDate(), + updatedAt: dayjs('2023-07-22 12:20:11.257Z').toDate(), + }, + ], + skipDuplicates: true, + }); +} + +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/src/app.module.ts b/src/app.module.ts index 41fcd50..a6e120c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -17,7 +17,7 @@ import { PrismaService } from 'src/prisma.service'; ConfigModule.forRoot({ isGlobal: true, load: [configuration], - envFilePath: '.env', + envFilePath: process.env.NODE_ENV === 'test' ? '.env.test' : '.env', }), AuthModule, HttpModule, diff --git a/src/common/objects/response-message.object.ts b/src/common/objects/response-message.object.ts index d00a631..6723972 100644 --- a/src/common/objects/response-message.object.ts +++ b/src/common/objects/response-message.object.ts @@ -19,6 +19,7 @@ export const RESPONSE_MESSAGE: { REISSUED_TOKEN_SUCCESS: 'ํ† ํฐ ์žฌ๋ฐœ๊ธ‰ ์„ฑ๊ณต', // plants + READ_PLANT_DETAIL_SUCCESS: '์‹๋ฌผ ์ƒ์„ธ ์กฐํšŒ ์„ฑ๊ณต', READ_PLANT_INFORMATION_SUCCESS: '์‹๋ฌผ ๋‹จ๊ณ„ ์กฐํšŒ ์„ฑ๊ณต', READ_PLANT_WATER_LOG_SUCCESS: '์‹๋ฌผ ๋ฌผ์ฃผ๊ธฐ ๊ธฐ๋ก ์กฐํšŒ ์„ฑ๊ณต', }; diff --git a/src/constants/swagger/index.ts b/src/constants/swagger/index.ts index 8bdc276..c15451c 100644 --- a/src/constants/swagger/index.ts +++ b/src/constants/swagger/index.ts @@ -1,8 +1,17 @@ import { SIGNIN_DESCRIPTION } from './auth'; -import { PLANT_INFORMATION, PLANT_WATER_LOG } from './plants'; +import { + READ_PLANT_DETAIL, + READ_PLANT_INFORMATION, + READ_PLANT_WATER_LOG, +} from './plants'; export const ERROR_DESCRIPTION = { INTERNAL_SERVER_ERROR: 'Internal Server Error', }; -export { SIGNIN_DESCRIPTION, PLANT_INFORMATION, PLANT_WATER_LOG }; +export { + SIGNIN_DESCRIPTION, + READ_PLANT_DETAIL, + READ_PLANT_INFORMATION, + READ_PLANT_WATER_LOG, +}; diff --git a/src/constants/swagger/plants.ts b/src/constants/swagger/plants.ts index 93a6720..51b764a 100644 --- a/src/constants/swagger/plants.ts +++ b/src/constants/swagger/plants.ts @@ -1,11 +1,45 @@ -export const PLANT_INFORMATION = { +export const READ_PLANT_DETAIL = { API_OPERATION: { - SUMMARY: '์‹๋ฌผ ์ •๋ณด ์กฐํšŒ API', - DESCRIPTION: '์‹๋ฌผ ๋‹จ๊ณ„๋ณ„ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.', + summary: '์‹๋ฌผ ์ƒ์„ธ ์กฐํšŒ API', + description: '์‹๋ฌผ ์นด๋“œ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.', }, API_PARAM: { - NAME: 'id', - DESCRIPTION: 'plant id', + type: Number, + name: 'id', + required: true, + description: 'userPlant id', + }, + DTO_DESCRIPTION: { + RESPONSE: { + ID: 'userPlant id', + NICKNAME: '์‹๋ฌผ ๋‹‰๋„ค์ž„', + DURATION: 'ํ•จ๊ป˜ํ•œ ๋‚ ', + INSTAGRAM: '์ธ์Šคํƒ€๊ทธ๋žจ id', + D_DAY: '๋ฌผ ์ฃผ๊ธฐ D-Day', + PLANT_ID: 'plant id', + PLANT_IMAGE: '์‹๋ฌผ ์ด๋ฏธ์ง€ url', + PLANT_NAME: '์‹๋ฌผ ์ด๋ฆ„', + LEVEL_NAME: '์‹๋ฌผ ๋ ˆ๋ฒจ ์ด๋ฆ„', + STATUS_MESSAGE: '์‹๋ฌผ ์ƒํƒœ ๋ฉ”์‹œ์ง€', + STATUS_GAGUE: '์‹๋ฌผ ์ƒํƒœ ๊ฒŒ์ด์ง€', + }, + }, + ERROR_DESCRIPTION: { + BAD_REQUEST: 'Bad Request - ์š”์ฒญ id ๊ฐ€ ์—†๊ฑฐ๋‚˜, number๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ ๋“ฑ', + NOT_FOUND: 'Not Found - ์š”์ฒญํ•œ id์— ํ•ด๋‹นํ•˜๋Š” ์‹๋ฌผ ์ž์›์ด ์—†๋Š” ๊ฒฝ์šฐ', + }, +}; + +export const READ_PLANT_INFORMATION = { + API_OPERATION: { + summary: '์‹๋ฌผ ์ •๋ณด ์กฐํšŒ API', + description: '์‹๋ฌผ ๋‹จ๊ณ„๋ณ„ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.', + }, + API_PARAM: { + type: Number, + name: 'id', + required: true, + description: 'plant id', }, DTO_DESCRIPTION: { RESPONSE: { @@ -27,14 +61,16 @@ export const PLANT_INFORMATION = { }, }; -export const PLANT_WATER_LOG = { +export const READ_PLANT_WATER_LOG = { API_OPERATION: { - SUMMARY: '์‹๋ฌผ ๋ฌผ์ฃผ๊ธฐ ์กฐํšŒ API', - DESCRIPTION: '์‹๋ฌผ๋ณ„ ๋ฌผ์ฃผ๊ธฐ ๊ธฐ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.', + summary: '์‹๋ฌผ ๋ฌผ์ฃผ๊ธฐ ์กฐํšŒ API', + description: '์‹๋ฌผ๋ณ„ ๋ฌผ์ฃผ๊ธฐ ๊ธฐ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.', }, API_PARAM: { - NAME: 'id', - DESCRIPTION: 'user_plant_id', + type: Number, + name: 'id', + required: true, + description: 'user_plant_id', }, DTO_DESCRIPTION: { RESPONSE: { diff --git a/src/interceptors/webhook.interceptor.ts b/src/interceptors/webhook.interceptor.ts index 5f6b172..409ae59 100644 --- a/src/interceptors/webhook.interceptor.ts +++ b/src/interceptors/webhook.interceptor.ts @@ -16,22 +16,23 @@ export class WebhookInterceptor implements NestInterceptor { catchError((error) => { Sentry.captureException(error); const webhook = new IncomingWebhook(configuration().sentryWebhookUrl); - webhook.send({ - attachments: [ - { - color: 'danger', - text: '๐Ÿšจ Cherish Dev - ์—๋Ÿฌ ๋ฐœ์ƒ ๐Ÿšจ', - fields: [ - { - title: `Request Message: ${error.message}`, - value: error.stack, - short: false, - }, - ], - ts: Math.floor(new Date().getTime() / 1000).toString(), - }, - ], - }); + process.env.NODE_ENV !== 'test' && + webhook.send({ + attachments: [ + { + color: 'danger', + text: '๐Ÿšจ Cherish Dev - ์—๋Ÿฌ ๋ฐœ์ƒ ๐Ÿšจ', + fields: [ + { + title: `Request Message: ${error.message}`, + value: error.stack, + short: false, + }, + ], + ts: Math.floor(new Date().getTime() / 1000).toString(), + }, + ], + }); return throwError(() => error); }), ); diff --git a/src/plants/constants/plant-status.ts b/src/plants/constants/plant-status.ts new file mode 100644 index 0000000..3706d2b --- /dev/null +++ b/src/plants/constants/plant-status.ts @@ -0,0 +1,34 @@ +export const PLANT_STATUS: Record< + string, + { statusMessage: string; statusGague: number } +> = { + healthy: { + statusMessage: 'ํž˜์ด ์†Ÿ์•„์š”', + statusGague: 1, + }, + waterDay: { + statusMessage: '๋ฌผ ์ฃผ๋Š” ๋‚ ์ด์—์š”!', + statusGague: 1, + }, + happy: { + statusMessage: '๊ธฐ๋ถ„์ด ์ข‹์•„์š”', + statusGague: 0.75, + }, + thirsty: { + statusMessage: '๊ฐˆ์ฆ๋‚˜์š”', + statusGague: 0.5, + }, + veryThirsty: { + statusMessage: '๋ฌผ ์ฃผ์„ธ์š”', + statusGague: 0.25, + }, +}; + +export const PLANT_D_DAY: Record = { + 1: [0, 3, 7], + 2: [0, 4, 13], + 3: [0, 6, 13], + 4: [0, 3, 7], + 5: [0, 3, 7], + 6: [0, 3, 7], +}; diff --git a/src/plants/dto/response-plant-detail.dto.ts b/src/plants/dto/response-plant-detail.dto.ts new file mode 100644 index 0000000..93fbe22 --- /dev/null +++ b/src/plants/dto/response-plant-detail.dto.ts @@ -0,0 +1,48 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ResponseSuccessDto } from 'src/common/dto/response-success.dto'; +import { READ_PLANT_DETAIL } from 'src/constants/swagger'; + +const DTO_RESPONSE_DESCRIPTION = READ_PLANT_DETAIL.DTO_DESCRIPTION.RESPONSE; + +export class ResponsePlantDetailData { + @ApiProperty({ description: DTO_RESPONSE_DESCRIPTION.ID }) + id: number; + + @ApiProperty({ description: DTO_RESPONSE_DESCRIPTION.NICKNAME }) + nickname: string; + + @ApiProperty({ description: DTO_RESPONSE_DESCRIPTION.DURATION }) + duration: number; + + @ApiProperty({ + required: false, + description: DTO_RESPONSE_DESCRIPTION.INSTAGRAM, + }) + instagram?: string; + + @ApiProperty({ description: DTO_RESPONSE_DESCRIPTION.D_DAY }) + dDay: number; + + @ApiProperty({ description: DTO_RESPONSE_DESCRIPTION.PLANT_ID }) + plantId: number; + + @ApiProperty({ description: DTO_RESPONSE_DESCRIPTION.PLANT_IMAGE }) + plantImage: string; + + @ApiProperty({ description: DTO_RESPONSE_DESCRIPTION.PLANT_NAME }) + plantName: string; + + @ApiProperty({ description: DTO_RESPONSE_DESCRIPTION.LEVEL_NAME }) + levelName: string; + + @ApiProperty({ description: DTO_RESPONSE_DESCRIPTION.STATUS_MESSAGE }) + statusMessage: string; + + @ApiProperty({ description: DTO_RESPONSE_DESCRIPTION.STATUS_GAGUE }) + statusGague: number; +} + +export class ResponsePlantDetailDto extends ResponseSuccessDto { + @ApiProperty() + data: ResponsePlantDetailData; +} diff --git a/src/plants/dto/response-plantInformation.dto.ts b/src/plants/dto/response-plant-information.dto.ts similarity index 84% rename from src/plants/dto/response-plantInformation.dto.ts rename to src/plants/dto/response-plant-information.dto.ts index 7e21d1b..913a26d 100644 --- a/src/plants/dto/response-plantInformation.dto.ts +++ b/src/plants/dto/response-plant-information.dto.ts @@ -1,8 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; import { ResponseSuccessDto } from 'src/common/dto/response-success.dto'; -import { PLANT_INFORMATION } from 'src/constants/swagger'; +import { READ_PLANT_INFORMATION } from 'src/constants/swagger'; -const DTO_RESPONSE_DESCRIPTION = PLANT_INFORMATION.DTO_DESCRIPTION.RESPONSE; +const DTO_RESPONSE_DESCRIPTION = + READ_PLANT_INFORMATION.DTO_DESCRIPTION.RESPONSE; export class PlantLevelData { @ApiProperty({ description: DTO_RESPONSE_DESCRIPTION.PLANT_LEVEL.LEVEL_NAME }) @@ -30,7 +31,10 @@ export class ResponsePlantInformationData { @ApiProperty({ description: DTO_RESPONSE_DESCRIPTION.EXPLANATION }) explanation: string; - @ApiProperty() + @ApiProperty({ + isArray: true, + type: PlantLevelData, + }) plantLevel: PlantLevelData[]; @ApiProperty({ description: DTO_RESPONSE_DESCRIPTION.CIRCLE_IMAGE_URL }) diff --git a/src/plants/dto/response-plantwaterlog.dto.ts b/src/plants/dto/response-plant-water-log.dto.ts similarity index 82% rename from src/plants/dto/response-plantwaterlog.dto.ts rename to src/plants/dto/response-plant-water-log.dto.ts index e5c8505..817fe36 100644 --- a/src/plants/dto/response-plantwaterlog.dto.ts +++ b/src/plants/dto/response-plant-water-log.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { ResponseSuccessDto } from 'src/common/dto/response-success.dto'; -import { PLANT_WATER_LOG } from 'src/constants/swagger'; +import { READ_PLANT_WATER_LOG } from 'src/constants/swagger'; -const DTO_RESPONSE_DESCRIPTION = PLANT_WATER_LOG.DTO_DESCRIPTION.RESPONSE; +const DTO_RESPONSE_DESCRIPTION = READ_PLANT_WATER_LOG.DTO_DESCRIPTION.RESPONSE; export class PlantWaterReviewData { @ApiProperty({ description: DTO_RESPONSE_DESCRIPTION.REVIEWS.ID }) diff --git a/src/plants/plants.controller.spec.ts b/src/plants/plants.controller.spec.ts index 5f2363e..294ac79 100644 --- a/src/plants/plants.controller.spec.ts +++ b/src/plants/plants.controller.spec.ts @@ -3,7 +3,11 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PlantsController } from './plants.controller'; import { PlantsService } from './plants.service'; -import { mockPlant } from '../../test/mock/plants.mock'; +import { + mockPlant, + mockUserPlantDetailData, + mockUserPlantDetailSuccessResponse, +} from '../../test/mock/plants.mock'; describe('PlantsController', () => { let controller: PlantsController; @@ -16,6 +20,7 @@ describe('PlantsController', () => { { provide: PlantsService, useValue: { + getUserPlantDetail: jest.fn(), getPlantInformation: jest.fn(), getPlantWaterLog: jest.fn(), }, @@ -31,6 +36,20 @@ describe('PlantsController', () => { expect(controller).toBeDefined(); }); + describe('get plant detail by userPlantId', () => { + const mockUserPlantId: number = 1; + + it('์กด์žฌํ•˜๋Š” ์‹๋ฌผ id๊ฐ€ ์ฃผ์–ด์ง€๋ฉด ์„ฑ๊ณต response ๋ฐ˜ํ™˜', async () => { + jest + .spyOn(service, 'getUserPlantDetail') + .mockResolvedValueOnce(mockUserPlantDetailData); + + const result = await controller.getPlantDetail({ id: mockUserPlantId }); + + expect(result).toEqual(mockUserPlantDetailSuccessResponse); + }); + }); + describe('get plant information by plantId', () => { const mockPlantId: number = 1; const mockResult = { diff --git a/src/plants/plants.controller.ts b/src/plants/plants.controller.ts index 0117ca9..5fb7ca6 100644 --- a/src/plants/plants.controller.ts +++ b/src/plants/plants.controller.ts @@ -10,17 +10,18 @@ import { } from '@nestjs/swagger'; import { PlantsService } from './plants.service'; -import { ResponsePlantInformationDto } from './dto/response-plantInformation.dto'; +import { ResponsePlantInformationDto } from './dto/response-plant-information.dto'; import { CommonParamsDto } from 'src/common/dto/common-params.dto'; import { ERROR_DESCRIPTION, - PLANT_INFORMATION, - PLANT_WATER_LOG, + READ_PLANT_DETAIL, + READ_PLANT_INFORMATION, + READ_PLANT_WATER_LOG, } from 'src/constants/swagger'; import { wrapSuccess } from 'src/utils/success'; import { RESPONSE_MESSAGE } from 'src/common/objects'; -import { ResponsePlantWaterLogDto } from './dto/response-plantwaterlog.dto'; - +import { ResponsePlantWaterLogDto } from './dto/response-plant-water-log.dto'; +import { ResponsePlantDetailDto } from './dto/response-plant-detail.dto'; @Controller('plants') @ApiTags('Plants') @ApiInternalServerErrorResponse({ @@ -29,23 +30,37 @@ import { ResponsePlantWaterLogDto } from './dto/response-plantwaterlog.dto'; export class PlantsController { constructor(private readonly plantsService: PlantsService) {} - @Get(':id/information') - @ApiOperation({ - summary: PLANT_INFORMATION.API_OPERATION.SUMMARY, - description: PLANT_INFORMATION.API_OPERATION.DESCRIPTION, + @Get(':id') + @ApiOperation(READ_PLANT_DETAIL.API_OPERATION) + @ApiParam(READ_PLANT_DETAIL.API_PARAM) + @ApiOkResponse({ type: ResponsePlantDetailDto }) + @ApiBadRequestResponse({ + description: READ_PLANT_DETAIL.ERROR_DESCRIPTION.BAD_REQUEST, }) - @ApiParam({ - type: Number, - name: PLANT_INFORMATION.API_PARAM.NAME, - required: true, - description: PLANT_INFORMATION.API_PARAM.DESCRIPTION, + @ApiNotFoundResponse({ + description: READ_PLANT_DETAIL.ERROR_DESCRIPTION.NOT_FOUND, }) + async getPlantDetail( + @Param() { id }: CommonParamsDto, + ): Promise { + const data = await this.plantsService.getUserPlantDetail(id); + + return wrapSuccess( + HttpStatus.OK, + RESPONSE_MESSAGE.READ_PLANT_DETAIL_SUCCESS, + data, + ); + } + + @Get(':id/information') + @ApiOperation(READ_PLANT_INFORMATION.API_OPERATION) + @ApiParam(READ_PLANT_INFORMATION.API_PARAM) @ApiOkResponse({ type: ResponsePlantInformationDto }) @ApiBadRequestResponse({ - description: PLANT_INFORMATION.ERROR_DESCRIPTION.BAD_REQUEST, + description: READ_PLANT_INFORMATION.ERROR_DESCRIPTION.BAD_REQUEST, }) @ApiNotFoundResponse({ - description: PLANT_INFORMATION.ERROR_DESCRIPTION.NOT_FOUND, + description: READ_PLANT_INFORMATION.ERROR_DESCRIPTION.NOT_FOUND, }) async getPlantInformation( @Param() { id }: CommonParamsDto, @@ -60,19 +75,11 @@ export class PlantsController { } @Get(':id/water') - @ApiOperation({ - summary: PLANT_WATER_LOG.API_OPERATION.SUMMARY, - description: PLANT_WATER_LOG.API_OPERATION.DESCRIPTION, - }) - @ApiParam({ - type: Number, - name: PLANT_WATER_LOG.API_PARAM.NAME, - required: true, - description: PLANT_WATER_LOG.API_PARAM.DESCRIPTION, - }) + @ApiOperation(READ_PLANT_WATER_LOG.API_OPERATION) + @ApiParam(READ_PLANT_WATER_LOG.API_PARAM) @ApiOkResponse({ type: ResponsePlantWaterLogDto }) @ApiBadRequestResponse({ - description: PLANT_WATER_LOG.ERROR_DESCRIPTION.BAD_REQUEST, + description: READ_PLANT_WATER_LOG.ERROR_DESCRIPTION.BAD_REQUEST, }) async getPlantWaterLog( @Param() { id }: CommonParamsDto, diff --git a/src/plants/plants.service.spec.ts b/src/plants/plants.service.spec.ts index 5d80291..3811d6d 100644 --- a/src/plants/plants.service.spec.ts +++ b/src/plants/plants.service.spec.ts @@ -8,17 +8,25 @@ import { PlantsService } from './plants.service'; import { notFound } from 'src/utils/error'; import * as objectUtils from 'src/utils/object'; -import { mockPlant } from '../../test/mock/plants.mock'; +import { + mockPlant, + mockUserPlantDetailData, + mockUserPlantResponse, +} from '../../test/mock/plants.mock'; describe('PlantsService', () => { let service: PlantsService; let plantPrisma: MockProxy> = mock(); let waterPrisma: MockProxy> = mock(); + let userPlantPrisma: MockProxy< + Pick + > = mock(); const mockPrismaClient = { plant: plantPrisma, water: waterPrisma, + userPlant: userPlantPrisma, }; beforeEach(async () => { @@ -36,6 +44,43 @@ describe('PlantsService', () => { expect(service).toBeDefined(); }); + describe('get plant detail by userPlantId', () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2023-08-13 18:00')); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + const mockUserPlantId: number = 1; + + it('์กด์žฌํ•˜๋Š” userPlantId ๊ฐ€ ์ฃผ์–ด์ง€๋ฉด ์‹๋ฌผ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.', async () => { + const mockFindUnique = userPlantPrisma.findUnique.mockResolvedValueOnce( + mockUserPlantResponse as any, + ); + jest + .spyOn(service, 'getPlantLevelNameByLoveGague') + .mockResolvedValueOnce({ levelName: '์ƒˆ์‹น' }); + + const result = await service.getUserPlantDetail(mockUserPlantId); + + expect(result).toEqual(mockUserPlantDetailData); + }); + + it('์กด์žฌํ•˜์ง€ ์•Š๋Š” userPlantId ๊ฐ€ ์ฃผ์–ด์ง€๋ฉด Not Found ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.', async () => { + const mockFindUnique = + userPlantPrisma.findUnique.mockResolvedValueOnce(null); + jest.spyOn(service, 'getPlantLevelNameByLoveGague').mockImplementation(); + + await expect( + service.getUserPlantDetail(mockUserPlantId), + ).rejects.toThrowError(notFound()); + expect(service.getPlantLevelNameByLoveGague).not.toHaveBeenCalled(); + }); + }); + describe('get plant information by plantId', () => { const mockPlantId: number = 1; const mockResult = { diff --git a/src/plants/plants.service.ts b/src/plants/plants.service.ts index 599d3d0..2fded3f 100644 --- a/src/plants/plants.service.ts +++ b/src/plants/plants.service.ts @@ -1,16 +1,113 @@ import { Injectable } from '@nestjs/common'; +import { PlantLevel } from '@prisma/client'; +import * as dayjs from 'dayjs'; import { PrismaService } from 'src/prisma.service'; import { notFound } from 'src/utils/error'; import { renameObjectKey } from 'src/utils/object'; -import { ResponsePlantInformationData } from './dto/response-plantInformation.dto'; -import { ResponsePlantWaterLogData } from './dto/response-plantwaterlog.dto'; -import * as dayjs from 'dayjs'; +import * as utilPlants from './utils/plants'; +import * as utilDay from 'src/utils/day'; +import { ResponsePlantDetailData } from './dto/response-plant-detail.dto'; +import { ResponsePlantInformationData } from './dto/response-plant-information.dto'; +import { ResponsePlantWaterLogData } from './dto/response-plant-water-log.dto'; @Injectable() export class PlantsService { constructor(private prisma: PrismaService) {} + async getUserPlantDetail(id: number): Promise { + const userPlant = await this.prisma.userPlant.findUnique({ + where: { id, isDeleted: false }, + select: { + nickname: true, + instagram: true, + createdAt: true, + plantId: true, + waterCycle: true, + loveGauge: true, + plant: { + select: { + name: true, + circleImageURL: true, + }, + }, + Water: { + select: { + wateringDate: true, + }, + orderBy: { + wateringDate: 'desc', + }, + take: 1, + }, + }, + }); + if (!userPlant) { + throw notFound(); + } + + const { + nickname, + instagram, + plantId, + loveGauge, + waterCycle, + createdAt, + Water, + plant, + } = userPlant; + const { wateringDate } = Water[0]; + const { name: plantName, circleImageURL: plantImage } = plant; + + const { levelName } = await this.getPlantLevelNameByLoveGague( + plantId, + loveGauge, + ); + + const nextWateringDate: Date = utilPlants.calculateNextWateringDate( + wateringDate, + waterCycle, + ); + + const dDay: number = utilDay.calculateDday(new Date(), nextWateringDate); + const duration: number = -utilDay.calculateDday(new Date(), createdAt); + + const { statusMessage, statusGague } = utilPlants.calculatePlantStatus( + plantId, + -dDay, + ); + + return { + id, + nickname, + instagram, + duration, + dDay, + plantId, + plantName, + plantImage, + levelName, + statusMessage, + statusGague, + }; + } + + async getPlantLevelNameByLoveGague( + plantId: number, + loveGague: number, + ): Promise> { + const level = utilPlants.calculatePlantLevel(loveGague); + + const plantLevel = await this.prisma.plantLevel.findUnique({ + where: { id: plantId, level, isDeleted: false }, + select: { + levelName: true, + }, + }); + + return plantLevel; + } + async getPlantInformation(id: number): Promise { const plant = await this.prisma.plant.findUnique({ where: { id, isDeleted: false }, diff --git a/src/plants/utils/plants.ts b/src/plants/utils/plants.ts new file mode 100644 index 0000000..62e6ef1 --- /dev/null +++ b/src/plants/utils/plants.ts @@ -0,0 +1,51 @@ +import * as dayjs from 'dayjs'; +import { PLANT_D_DAY, PLANT_STATUS } from '../constants/plant-status'; + +export const calculatePlantLevel = (loveGague: number): number => { + let level = 0; + + if (loveGague > 3 && loveGague <= 7) { + level = 1; + } else if (loveGague > 7 && loveGague <= 12) { + level = 2; + } + + return level; +}; + +export const calculateNextWateringDate = ( + wateringDate: Date, + waterCycle: number, +): Date => { + const lastWateringDate = dayjs(wateringDate); + const nextWateringDate = lastWateringDate.add(waterCycle, 'day'); + + return new Date(nextWateringDate.format()); +}; + +export const calculatePlantStatus = ( + plantId: number, + dDay: number, +): { statusMessage: string; statusGague: number } => { + let plantStatus: string = ''; + + if (dDay < PLANT_D_DAY[plantId][0]) { + plantStatus = 'healthy'; + } else if (dDay === PLANT_D_DAY[plantId][0]) { + plantStatus = 'waterDay'; + } else if ( + dDay > PLANT_D_DAY[plantId][0] && + dDay <= PLANT_D_DAY[plantId][1] + ) { + plantStatus = 'happy'; + } else if ( + dDay > PLANT_D_DAY[plantId][1] && + dDay <= PLANT_D_DAY[plantId][2] + ) { + plantStatus = 'thirsty'; + } else if (dDay > PLANT_D_DAY[plantId][2]) { + plantStatus = 'veryThirsty'; + } + + return PLANT_STATUS[plantStatus]; +}; diff --git a/test/mock/plants.mock.ts b/test/mock/plants.mock.ts index ab344b7..a1cc9be 100644 --- a/test/mock/plants.mock.ts +++ b/test/mock/plants.mock.ts @@ -1,3 +1,44 @@ +export const mockUserPlantResponse = { + nickname: 'test', + instagram: null, + createdAt: new Date('2023-07-22T08:16:52.538Z'), + plantId: 4, + waterCycle: 14, + loveGauge: 0, + plant: { + name: '๋ฏผ๋“ค๋ ˆ', + circleImageURL: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/circleImages/mindlere-circle.png', + }, + Water: [ + { + wateringDate: new Date('2023-07-22T18:16:53.000Z'), + }, + ], +}; + +export const mockUserPlantDetailData = { + id: 1, + nickname: 'test', + instagram: null, + duration: 22, + dDay: -7, + plantId: 4, + plantName: '๋ฏผ๋“ค๋ ˆ', + plantImage: + 'https://cherish-static-dev.s3.ap-northeast-2.amazonaws.com/circleImages/mindlere-circle.png', + levelName: '์ƒˆ์‹น', + statusMessage: '๊ฐˆ์ฆ๋‚˜์š”', + statusGague: 0.5, +}; + +export const mockUserPlantDetailSuccessResponse = { + statusCode: 200, + success: true, + message: '์‹๋ฌผ ์ƒ์„ธ ์กฐํšŒ ์„ฑ๊ณต', + data: mockUserPlantDetailData, +}; + export const mockPlantsInformationResponse = { success: true, statusCode: 200, diff --git a/test/plants.e2e-spec.ts b/test/plants.e2e-spec.ts index 77ae76a..0690e00 100644 --- a/test/plants.e2e-spec.ts +++ b/test/plants.e2e-spec.ts @@ -1,6 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import * as request from 'supertest'; +import * as dayjs from 'dayjs'; +import * as utc from 'dayjs/plugin/utc'; +import * as timezone from 'dayjs/plugin/timezone'; + +dayjs.extend(utc); +dayjs.extend(timezone); import { AppModule } from '../src/app.module'; import { PlantsModule } from 'src/plants/plants.module'; @@ -10,6 +16,7 @@ import { mockNotFoundResponse, mockPlantWaterLogsResponse, mockPlantsInformationResponse, + mockUserPlantDetailSuccessResponse, } from './mock/plants.mock'; describe('Plants (e2e)', () => { @@ -31,6 +38,38 @@ describe('Plants (e2e)', () => { await app.close(); }); + describe('[GET] /plants/:id', () => { + beforeAll(() => { + jest.useFakeTimers({ doNotFake: ['nextTick', 'setImmediate'] }); + jest.setSystemTime(new Date('2023-08-13 18:00')); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('200 OK', () => { + return request(app.getHttpServer()) + .get(`/plants/1`) + .expect(200) + .expect(mockUserPlantDetailSuccessResponse); + }); + + it('400 Bad Request', () => { + return request(app.getHttpServer()) + .get(`/plants/hi`) + .expect(400) + .expect(mockBadRequestResponse); + }); + + it('404 Not Found', () => { + return request(app.getHttpServer()) + .get(`/plants/100`) + .expect(404) + .expect(mockNotFoundResponse); + }); + }); + describe('[GET] /plants/:id/information', () => { it('200 OK', () => { return request(app.getHttpServer()) diff --git a/yarn.lock b/yarn.lock index a5e8c6b..fb707fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2214,7 +2214,14 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -2326,7 +2333,17 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dotenv-expand@10.0.0: +dotenv-cli@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/dotenv-cli/-/dotenv-cli-7.2.1.tgz#e595afd9ebfb721df9da809a435b9aa966c92062" + integrity sha512-ODHbGTskqRtXAzZapDPvgNuDVQApu4oKX8lZW7Y0+9hKA6le1ZJlyRS687oU9FXjOVEDU/VFV6zI125HzhM1UQ== + dependencies: + cross-spawn "^7.0.3" + dotenv "^16.0.0" + dotenv-expand "^10.0.0" + minimist "^1.2.6" + +dotenv-expand@10.0.0, dotenv-expand@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== @@ -2336,7 +2353,7 @@ dotenv@16.1.4: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.1.4.tgz#67ac1a10cd9c25f5ba604e4e08bc77c0ebe0ca8c" integrity sha512-m55RtE8AsPeJBpOIFKihEmqUcoVncQIwo7x9U8ZwLEZw9ZpXboz2c+rvog+jUaJvVrZ5kBOeYQBX5+8Aa/OZQw== -dotenv@^16.0.3: +dotenv@^16.0.0, dotenv@^16.0.3: version "16.3.1" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==