From a8d1974d36ec886ccbb4a20719f663061e60b538 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Aug 2022 11:04:52 +0000 Subject: [PATCH 01/48] Chore(deps): Bump terser from 4.8.0 to 4.8.1 Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1. - [Release notes](https://github.com/terser/terser/releases) - [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md) - [Commits](https://github.com/terser/terser/compare/v4.8.0...v4.8.1) --- updated-dependencies: - dependency-name: terser dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 65c254d2..e88bbb01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7873,9 +7873,9 @@ terser-webpack-plugin@^5.1.3: terser "^5.7.2" terser@^4.7.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" - integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== + version "4.8.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f" + integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw== dependencies: commander "^2.20.0" source-map "~0.6.1" From bc7409d9195e0e91bddd636daa13c01c52120980 Mon Sep 17 00:00:00 2001 From: siontama Date: Fri, 12 Aug 2022 18:47:44 +0900 Subject: [PATCH 02/48] Fix: exclude img from search --- apps/api/src/article/article.service.ts | 9 +++++++++ apps/api/test/e2e/article.e2e-spec.ts | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/api/src/article/article.service.ts b/apps/api/src/article/article.service.ts index 2bb56087..8a3dee4c 100644 --- a/apps/api/src/article/article.service.ts +++ b/apps/api/src/article/article.service.ts @@ -66,6 +66,15 @@ export class ArticleService { categoryIds = [options.categoryId]; } const { articles, totalCount } = await this.articleRepository.search(options, categoryIds); + for (let i = 0; i < articles.length; i++) { + if ( + articles[i].content.replace(/]*src=[\"']?([^>\"']+)[\"']?[^>]*>/g, '').indexOf(options.q) === -1 && + articles[i].title.indexOf(options.q) === -1 + ) { + articles.splice(i, 1); + i--; + } + } return { articles, totalCount }; } diff --git a/apps/api/test/e2e/article.e2e-spec.ts b/apps/api/test/e2e/article.e2e-spec.ts index 8eea4a66..8e5f482e 100644 --- a/apps/api/test/e2e/article.e2e-spec.ts +++ b/apps/api/test/e2e/article.e2e-spec.ts @@ -674,7 +674,8 @@ describe('Article', () => { const titleWithSearchWord = 'aaa42aaa'; const titleWithoutSearchWord = 'aaaaaa'; const contentWithSearchWord = 'bbb42bbb'; - const contentWithoutSearchWord = 'bbbbbb'; + const contentWithoutSearchWord = + 'bbbbbb'; const SearchArticleRequestDto = { q: searchWord, }; From 5f2077b26525b69e955a537c02ab8508405210a3 Mon Sep 17 00:00:00 2001 From: siontama Date: Fri, 12 Aug 2022 18:56:26 +0900 Subject: [PATCH 03/48] Fix: markdown img --- apps/api/src/article/article.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/article/article.service.ts b/apps/api/src/article/article.service.ts index 8a3dee4c..676a3d95 100644 --- a/apps/api/src/article/article.service.ts +++ b/apps/api/src/article/article.service.ts @@ -68,7 +68,7 @@ export class ArticleService { const { articles, totalCount } = await this.articleRepository.search(options, categoryIds); for (let i = 0; i < articles.length; i++) { if ( - articles[i].content.replace(/]*src=[\"']?([^>\"']+)[\"']?[^>]*>/g, '').indexOf(options.q) === -1 && + articles[i].content.replace(/![image](\S+)/g, '').indexOf(options.q) === -1 && articles[i].title.indexOf(options.q) === -1 ) { articles.splice(i, 1); From 002e01730e1cf7f50f9a7842b4817d32eaa17abc Mon Sep 17 00:00:00 2001 From: siontama Date: Fri, 12 Aug 2022 18:58:20 +0900 Subject: [PATCH 04/48] Fix: content without search word article test --- apps/api/test/e2e/article.e2e-spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/test/e2e/article.e2e-spec.ts b/apps/api/test/e2e/article.e2e-spec.ts index 8e5f482e..308ccc34 100644 --- a/apps/api/test/e2e/article.e2e-spec.ts +++ b/apps/api/test/e2e/article.e2e-spec.ts @@ -675,7 +675,7 @@ describe('Article', () => { const titleWithoutSearchWord = 'aaaaaa'; const contentWithSearchWord = 'bbb42bbb'; const contentWithoutSearchWord = - 'bbbbbb'; + 'bbbbbb![image.png](https://42world-image.s3.ap-northeast-2.amazonaws.com/111111111.png)'; const SearchArticleRequestDto = { q: searchWord, }; From b3a8e66d35c05e66fc8182ca59fa7071c3888abe Mon Sep 17 00:00:00 2001 From: siontama Date: Sat, 13 Aug 2022 17:29:59 +0900 Subject: [PATCH 05/48] Fix: markdown image --- apps/api/src/article/article.service.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/api/src/article/article.service.ts b/apps/api/src/article/article.service.ts index 676a3d95..a5ae002c 100644 --- a/apps/api/src/article/article.service.ts +++ b/apps/api/src/article/article.service.ts @@ -66,16 +66,12 @@ export class ArticleService { categoryIds = [options.categoryId]; } const { articles, totalCount } = await this.articleRepository.search(options, categoryIds); - for (let i = 0; i < articles.length; i++) { - if ( - articles[i].content.replace(/![image](\S+)/g, '').indexOf(options.q) === -1 && - articles[i].title.indexOf(options.q) === -1 - ) { - articles.splice(i, 1); - i--; - } - } - return { articles, totalCount }; + const filteredArticles = articles.filter( + (article) => + article.content.replace(/![\S*](\S+)/g, '').indexOf(options.q) !== -1 || + article.title.indexOf(options.q) !== -1, + ); + return { articles: filteredArticles, totalCount }; } async findAllByWriterId( From ef6460265b53b20c67907d776a85519a6c2f8717 Mon Sep 17 00:00:00 2001 From: huni Date: Sat, 13 Aug 2022 23:39:25 +0900 Subject: [PATCH 06/48] =?UTF-8?q?fix:=20=EC=BA=90=EB=A6=AD=ED=84=B0=20?= =?UTF-8?q?=EA=B0=9C=EC=88=98=20=EC=88=98=EC=A0=95=EB=90=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/user/dto/base-user.dto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/user/dto/base-user.dto.ts b/apps/api/src/user/dto/base-user.dto.ts index a08fd04f..2179471a 100644 --- a/apps/api/src/user/dto/base-user.dto.ts +++ b/apps/api/src/user/dto/base-user.dto.ts @@ -19,7 +19,7 @@ export class BaseUserDto { @Max(10) @ApiProperty({ minimum: 0, - maximum: 10, + maximum: 11, }) character!: number; From 407b8da4ece1f931296c68fbc301ca9a81ba5516 Mon Sep 17 00:00:00 2001 From: Sion Kang <31057849+Yaminyam@users.noreply.github.com> Date: Wed, 17 Aug 2022 16:24:38 +0900 Subject: [PATCH 07/48] Chore: skip test --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e59f9087..bb44e977 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,7 @@ on: jobs: test: + if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }} runs-on: ubuntu-latest env: From e8b83c07e7a05b246b168283d828bb482270b553 Mon Sep 17 00:00:00 2001 From: Sion Kang <31057849+Yaminyam@users.noreply.github.com> Date: Thu, 18 Aug 2022 17:12:09 +0900 Subject: [PATCH 08/48] CI: labeled trigger issue --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bb44e977..ad283014 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,7 @@ name: Test CI on: pull_request: branches: ['**'] + types: [labeled, unlabeled, opened, synchronize, reopened] jobs: test: From db98dae792d07e78c47aa33ca09a017b7489cbc8 Mon Sep 17 00:00:00 2001 From: Sion Kang <31057849+Yaminyam@users.noreply.github.com> Date: Sun, 28 Aug 2022 00:07:22 +0900 Subject: [PATCH 09/48] CI: remove strategy --- .github/workflows/test.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ad283014..55c0f49e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,11 +27,6 @@ jobs: ACCESS_TOKEN_KEY: ${{secrets.ACCESS_TOKEN_KEY}} FRONT_URL: ${{secrets.FRONT_URL}} - strategy: - matrix: - node-version: [16.x] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} From 79b90446f1ad2662978d43a538e44f61de58e345 Mon Sep 17 00:00:00 2001 From: huni Date: Sun, 28 Aug 2022 00:21:59 +0900 Subject: [PATCH 10/48] =?UTF-8?q?feat:=20mysql=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/docker-compose.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 152797db..8f12e48e 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -61,9 +61,8 @@ services: order: start-first #----------------------------------------------------------------------------------- db: - image: mysql:5.7 + image: mysql:8.0 container_name: 42world-backend-db - platform: linux/x86_64 ports: - '${DB_PORT}:3306' environment: From e6496771616de651d23517af60d42ed66342127c Mon Sep 17 00:00:00 2001 From: Sion Kang <31057849+Yaminyam@users.noreply.github.com> Date: Sun, 28 Aug 2022 00:24:19 +0900 Subject: [PATCH 11/48] CI: auto label in issue --- .github/workflows/auto-label.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/auto-label.yml diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml new file mode 100644 index 00000000..061820cf --- /dev/null +++ b/.github/workflows/auto-label.yml @@ -0,0 +1,14 @@ +name: 'Auto Label' + +on: + pull_request: + types: [labeled, unlabeled, opened, synchronize, reopened] + +permissions: + pull-requests: write + +jobs: + auto-label: + runs-on: ubuntu-latest + steps: + - uses: Yaminyam/auto-label-in-issue@1.1.0 From bf899777d82ebe3cdb77f62b4b3cf002a55b23ae Mon Sep 17 00:00:00 2001 From: huni Date: Sun, 28 Aug 2022 00:27:13 +0900 Subject: [PATCH 12/48] =?UTF-8?q?fix:=20test=20=EC=97=90=EB=8F=84=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/run_test_db.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/infra/run_test_db.sh b/infra/run_test_db.sh index 8f62976c..02231ef0 100755 --- a/infra/run_test_db.sh +++ b/infra/run_test_db.sh @@ -2,7 +2,6 @@ if ! $( docker container inspect -f '{{.State.Running}}' ft_world-mysql-test 2> /dev/null ); then docker run -d --rm --name ft_world-mysql-test \ - --platform linux/x86_64 \ -e MYSQL_DATABASE=ft_world \ -e MYSQL_USER=ft_world \ -e MYSQL_PASSWORD=ft_world \ @@ -14,6 +13,6 @@ if ! $( docker container inspect -f '{{.State.Running}}' ft_world-mysql-test 2> --health-start-period=0s \ --health-timeout=1s \ -e TZ=Asia/Seoul \ - -p 3308:3306 mysql:5.7 \ + -p 3308:3306 mysql:8.0 \ mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci fi \ No newline at end of file From 691d9f3c37e1ae0b6bb8b9291e0ab1b9cecdf46c Mon Sep 17 00:00:00 2001 From: huni Date: Sun, 28 Aug 2022 00:37:00 +0900 Subject: [PATCH 13/48] =?UTF-8?q?feat:=20=EC=A4=91=EB=B3=B5=20workflow=20?= =?UTF-8?q?=EC=95=88=EB=8F=8C=EA=B2=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55c0f49e..93bd017d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,13 @@ on: branches: ['**'] types: [labeled, unlabeled, opened, synchronize, reopened] +# Cancel previous workflows if they are the same workflow on same ref (branch/tags) +# with the same event (push/pull_request) even they are in progress. +# This setting will help reduce the number of duplicated workflows. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} + cancel-in-progress: true + jobs: test: if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }} From 30aab3cfc93bf2e90dbefa66d639b9b0ddac85aa Mon Sep 17 00:00:00 2001 From: huni Date: Sun, 28 Aug 2022 00:39:00 +0900 Subject: [PATCH 14/48] test: workflow test --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a7fb8965..1468ca40 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ COMPOSE_FLAGS = -f ./infra/docker-compose.yml --env-file ./infra/config/.env .PHONY: all all: dev - +# ใ…ใ… # Development ================================================= .PHONY: ready ready: From fa806ee285182f4323346cba7b4dfdaa061d7d14 Mon Sep 17 00:00:00 2001 From: huni Date: Sun, 28 Aug 2022 00:41:43 +0900 Subject: [PATCH 15/48] =?UTF-8?q?feat:=20codeql=20=EB=8F=84=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20workflow=20=EC=95=88=EB=8F=8C=EA=B2=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/codeql-analysis.yml | 69 +++++++++++++++------------ 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7607f702..3181ed22 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -9,20 +9,27 @@ # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # -name: "CodeQL" +name: 'CodeQL' on: push: - branches: [ develop ] + branches: [develop] pull_request: # The branches below must be a subset of the branches above - branches: [ develop ] + branches: [develop] paths-ignore: - - '**/*.md' - - '**/*.txt' + - '**/*.md' + - '**/*.txt' schedule: - cron: '17 16 * * 5' +# Cancel previous workflows if they are the same workflow on same ref (branch/tags) +# with the same event (push/pull_request) even they are in progress. +# This setting will help reduce the number of duplicated workflows. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} + cancel-in-progress: true + jobs: analyze: name: Analyze @@ -35,39 +42,39 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'javascript' ] + language: ['javascript'] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support steps: - - name: Checkout repository - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v3 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 - # โ„น๏ธ Command-line programs to run using the OS shell. - # ๐Ÿ“š https://git.io/JvXDl + # โ„น๏ธ Command-line programs to run using the OS shell. + # ๐Ÿ“š https://git.io/JvXDl - # โœ๏ธ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language + # โœ๏ธ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language - #- run: | - # make bootstrap - # make release + #- run: | + # make bootstrap + # make release - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 From 2c46674e8d239b232265ae80fcf827e7f03821c3 Mon Sep 17 00:00:00 2001 From: Sion Kang <31057849+Yaminyam@users.noreply.github.com> Date: Sun, 28 Aug 2022 00:42:41 +0900 Subject: [PATCH 16/48] CI: set up node.js version 16.x --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55c0f49e..4ab67d16 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,10 +29,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} + - name: Use Node.js 16.x uses: actions/setup-node@v2 with: - node-version: ${{ matrix.node-version }} + node-version: 16.x cache: 'yarn' - run: yarn - run: make test From bb0e1e2f15201050d48f7e95f0c79911654de528 Mon Sep 17 00:00:00 2001 From: huni Date: Sun, 28 Aug 2022 00:42:58 +0900 Subject: [PATCH 17/48] test: workflow test --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1468ca40..a7fb8965 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ COMPOSE_FLAGS = -f ./infra/docker-compose.yml --env-file ./infra/config/.env .PHONY: all all: dev -# ใ…ใ… + # Development ================================================= .PHONY: ready ready: From 8ec739db5dc83af5c15a8308e29aed8e511c4ccd Mon Sep 17 00:00:00 2001 From: rockpell Date: Sun, 28 Aug 2022 13:47:43 +0900 Subject: [PATCH 18/48] =?UTF-8?q?Feat:=20cookie=20maxAge=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - maxAge๊ฐ€ ์—†์–ด์„œ ์ฟ ํ‚ค๊ฐ€ ๋งค๋ฒˆ ์‚ฌ๋ผ์ง€๋Š” ๋ฌธ์ œ ์ˆ˜์ • - jwt token์˜ ์œ ํšจ๊ธฐ๊ฐ„์ด 7์ผ๋กœ ๋˜์–ด ์žˆ์–ด์„œ ๋™์ผํ•˜๊ฒŒ ๋งž์ถค --- libs/utils/src/utils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libs/utils/src/utils.ts b/libs/utils/src/utils.ts index daa460dd..5f16b61d 100644 --- a/libs/utils/src/utils.ts +++ b/libs/utils/src/utils.ts @@ -25,12 +25,14 @@ export const isExpired = (exp: Date): boolean => { }; export const getCookieOption = (): CookieOptions => { + const maxAge = 24 * 60 * 60 * 1000 * 7; // 24h * 7, 7days + if (process.env.NODE_ENV === 'prod') { - return { httpOnly: true, secure: true, sameSite: 'lax' }; + return { httpOnly: true, secure: true, sameSite: 'lax', maxAge }; } else if (process.env.NODE_ENV === 'alpha') { - return { httpOnly: true, secure: true, sameSite: 'none' }; + return { httpOnly: true, secure: true, sameSite: 'none', maxAge }; } - return {}; + return { httpOnly: true, maxAge }; }; export const errorHook = async (exceptionName: string, exceptionMessage: string) => { From 6af30ff3a163a39257b721b451d289df7dd875a5 Mon Sep 17 00:00:00 2001 From: rockpell Date: Sun, 28 Aug 2022 16:43:13 +0900 Subject: [PATCH 19/48] =?UTF-8?q?Refactor:=20cookie=20maxAge,=20=EC=A2=80?= =?UTF-8?q?=20=EB=8D=94=20=EB=AA=85=EC=8B=9C=EC=A0=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/utils/src/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/utils/src/utils.ts b/libs/utils/src/utils.ts index 5f16b61d..43ad980b 100644 --- a/libs/utils/src/utils.ts +++ b/libs/utils/src/utils.ts @@ -25,7 +25,8 @@ export const isExpired = (exp: Date): boolean => { }; export const getCookieOption = (): CookieOptions => { - const maxAge = 24 * 60 * 60 * 1000 * 7; // 24h * 7, 7days + const oneHour = 60 * 60 * 1000; + const maxAge = 24 * oneHour * 7; // 24h * 7, 7days if (process.env.NODE_ENV === 'prod') { return { httpOnly: true, secure: true, sameSite: 'lax', maxAge }; From b6690672511480cff209d5f81fc76f47f3865375 Mon Sep 17 00:00:00 2001 From: huni Date: Mon, 5 Sep 2022 06:05:45 +0900 Subject: [PATCH 20/48] =?UTF-8?q?fix:=20test=20=EA=B4=80=EB=A0=A8=20packag?= =?UTF-8?q?e.json=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 36 +++++------------------------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index a3552859..b8f79232 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,9 @@ "admin": "NODE_ENV=dev nest start admin", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "prettier": "prettier --write ./**/*.ts", - "test": "jest", - "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": "NODE_ENV=test jest --maxWorkers=6 --config ./jest.config.json", + "test:watch": "yarn test --watch", + "test:cov": "yarn test --coverage", "test:e2e": "NODE_ENV=test jest --runInBand --config ./apps/api/test/e2e/jest-e2e.json", "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js --config libs/common/src/database/ormconfig.ts", "typeorm:migrate": "yarn typeorm migration:generate -n", @@ -94,6 +93,7 @@ "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^3.4.0", "jest": "^27.0.6", + "jest-mock-extended": "^2.0.7", "prettier": "^2.3.2", "prettier-plugin-organize-imports": "^2.3.4", "supertest": "^6.1.3", @@ -102,32 +102,6 @@ "ts-mockito": "^2.6.1", "ts-node": "^10.0.0", "tsconfig-paths": "^3.10.1", - "typescript": "^4.3.5" - }, - "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], - "rootDir": ".", - "testRegex": ".*\\.spec\\.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, - "collectCoverageFrom": [ - "**/*.(t|j)s" - ], - "coverageDirectory": "./coverage", - "testEnvironment": "node", - "roots": [ - "/apps/", - "/libs/" - ], - "moduleNameMapper": { - "^@app/common(|/.*)$": "/libs/common/src/$1", - "^@app/utils(|/.*)$": "/libs/utils/src/$1", - "^@app/entity(|/.*)$": "/libs/entity/src/$1" - } + "typescript": "4.3.5" } } From b8834d933ea62e734b4f371d17fd83a41a97ed3d Mon Sep 17 00:00:00 2001 From: huni Date: Mon, 5 Sep 2022 06:05:56 +0900 Subject: [PATCH 21/48] =?UTF-8?q?feat:=20jest=20config=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jest.config.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 jest.config.json diff --git a/jest.config.json b/jest.config.json new file mode 100644 index 00000000..f12a9a36 --- /dev/null +++ b/jest.config.json @@ -0,0 +1,16 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testMatch": ["**/?(*.)+(spec|test).+(ts)"], + "transform": { "^.+\\.ts$": "ts-jest" }, + "collectCoverageFrom": ["**/*.ts", "!**/__test__/**/*.ts"], + "coverageDirectory": "./coverage", + "testEnvironment": "node", + "moduleNameMapper": { + "^@api(|/.*)$": "/apps/api/src/$1", + "^@test/(.*)$": "/apps/api/test/$1", + "^@app/common(|/.*)$": "/libs/common/src/$1", + "^@app/utils(|/.*)$": "/libs/utils/src/$1", + "^@app/entity(|/.*)$": "/libs/entity/src/$1" + } +} From 664a7adaffe849c934143948183d43b26bb3e2fa Mon Sep 17 00:00:00 2001 From: huni Date: Mon, 5 Sep 2022 06:06:13 +0900 Subject: [PATCH 22/48] =?UTF-8?q?fix:=20eslintrc=20ts=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js => .eslintrc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .eslintrc.js => .eslintrc.ts (94%) diff --git a/.eslintrc.js b/.eslintrc.ts similarity index 94% rename from .eslintrc.js rename to .eslintrc.ts index 28d7eb2d..6e28eb68 100644 --- a/.eslintrc.js +++ b/.eslintrc.ts @@ -12,7 +12,7 @@ module.exports = { node: true, jest: true, }, - ignorePatterns: ['.eslintrc.js'], + ignorePatterns: ['.eslintrc.ts'], rules: { '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/explicit-function-return-type': 'off', From e82f70bdbcc342303daa2833d0b1a518dcdbf3ee Mon Sep 17 00:00:00 2001 From: huni Date: Mon, 5 Sep 2022 06:06:35 +0900 Subject: [PATCH 23/48] =?UTF-8?q?feat:=20build=20=ED=95=A0=EB=95=8C=20test?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tsconfig.build.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.build.json b/tsconfig.build.json index 64f86c6b..8391f52b 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] + "exclude": ["node_modules", "test", "dist", "**/*spec.ts", "**/*test.ts", "**/__test__/**/*.ts"] } From a62cfda947a7f4444aa0c179e387e267251878b4 Mon Sep 17 00:00:00 2001 From: huni Date: Mon, 5 Sep 2022 06:06:50 +0900 Subject: [PATCH 24/48] =?UTF-8?q?feat:=20jest-mock-extended=20=EC=84=A4?= =?UTF-8?q?=EC=B9=98=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yarn.lock | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index e88bbb01..df691182 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5448,6 +5448,13 @@ jest-message-util@^27.5.1: slash "^3.0.0" stack-utils "^2.0.3" +jest-mock-extended@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/jest-mock-extended/-/jest-mock-extended-2.0.7.tgz#73ad87d8a744949bc3415d840f03468229f73f2a" + integrity sha512-h8brJJN5BZb03hTwplvt+raT6Nj0U2U71Z26Py12Qc3kvYnAjDW/zSuQJLnXCNyyufy592VC9k3X7AOz+2H52g== + dependencies: + ts-essentials "^7.0.3" + jest-mock@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" @@ -8024,6 +8031,11 @@ triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== +ts-essentials@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38" + integrity sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ== + ts-jest@^27.0.3: version "27.1.4" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.1.4.tgz#84d42cf0f4e7157a52e7c64b1492c46330943e00" @@ -8212,16 +8224,16 @@ typeorm@^0.2.41: yargs "^17.0.1" zen-observable-ts "^1.0.0" +typescript@4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" + integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== + typescript@4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4" integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg== -typescript@^4.3.5: - version "4.6.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" - integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== - uid-safe@2.1.5, uid-safe@~2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" From 23e17ec3a612eba3b02c6a15033bb9f14575e057 Mon Sep 17 00:00:00 2001 From: huni Date: Mon, 5 Sep 2022 06:07:30 +0900 Subject: [PATCH 25/48] =?UTF-8?q?chore:=20constant=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/auth/{auth.constant.ts => constant.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/api/src/auth/{auth.constant.ts => constant.ts} (100%) diff --git a/apps/api/src/auth/auth.constant.ts b/apps/api/src/auth/constant.ts similarity index 100% rename from apps/api/src/auth/auth.constant.ts rename to apps/api/src/auth/constant.ts From f12805eaf6c8ed03fcc23df07ef708bf8513400e Mon Sep 17 00:00:00 2001 From: huni Date: Mon, 5 Sep 2022 06:08:36 +0900 Subject: [PATCH 26/48] =?UTF-8?q?feat:=20github-auth=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EB=A1=9C=20=EB=AC=B6=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/auth/auth.module.ts | 9 +++------ .../auth/{ => github-auth}/github-auth.guard.ts | 0 .../api/src/auth/github-auth/github-auth.module.ts | 9 +++++++++ .../github-auth.strategy.ts} | 14 ++++++++------ 4 files changed, 20 insertions(+), 12 deletions(-) rename apps/api/src/auth/{ => github-auth}/github-auth.guard.ts (100%) create mode 100644 apps/api/src/auth/github-auth/github-auth.module.ts rename apps/api/src/auth/{github.strategy.ts => github-auth/github-auth.strategy.ts} (52%) diff --git a/apps/api/src/auth/auth.module.ts b/apps/api/src/auth/auth.module.ts index 21730a48..3572f3f4 100644 --- a/apps/api/src/auth/auth.module.ts +++ b/apps/api/src/auth/auth.module.ts @@ -2,17 +2,14 @@ import { UserModule } from '@api/user/user.module'; import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; -import { PassportModule } from '@nestjs/passport'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; -import { GithubStrategy } from './github.strategy'; -import { JwtStrategy } from './jwt.strategy'; +import { GithubAuthModule } from './github-auth/github-auth.module'; @Module({ imports: [ UserModule, - PassportModule, - ConfigModule, + GithubAuthModule, JwtModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], @@ -22,7 +19,7 @@ import { JwtStrategy } from './jwt.strategy'; }), }), ], - providers: [AuthService, GithubStrategy, JwtStrategy], + providers: [AuthService], exports: [AuthService], controllers: [AuthController], }) diff --git a/apps/api/src/auth/github-auth.guard.ts b/apps/api/src/auth/github-auth/github-auth.guard.ts similarity index 100% rename from apps/api/src/auth/github-auth.guard.ts rename to apps/api/src/auth/github-auth/github-auth.guard.ts diff --git a/apps/api/src/auth/github-auth/github-auth.module.ts b/apps/api/src/auth/github-auth/github-auth.module.ts new file mode 100644 index 00000000..e71c1355 --- /dev/null +++ b/apps/api/src/auth/github-auth/github-auth.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { PassportModule } from '@nestjs/passport'; +import { GithubAuthStrategy } from './github-auth.strategy'; + +@Module({ + imports: [PassportModule], + providers: [GithubAuthStrategy], +}) +export class GithubAuthModule {} diff --git a/apps/api/src/auth/github.strategy.ts b/apps/api/src/auth/github-auth/github-auth.strategy.ts similarity index 52% rename from apps/api/src/auth/github.strategy.ts rename to apps/api/src/auth/github-auth/github-auth.strategy.ts index 07a2873e..aad15023 100644 --- a/apps/api/src/auth/github.strategy.ts +++ b/apps/api/src/auth/github-auth/github-auth.strategy.ts @@ -1,17 +1,19 @@ import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; import { Strategy, VerifyCallback } from 'passport-github2'; -import { GithubProfile } from './interfaces/github-profile.interface'; +import { GithubProfile } from '../types'; @Injectable() -export class GithubStrategy extends PassportStrategy(Strategy) { - constructor() { +export class GithubAuthStrategy extends PassportStrategy(Strategy) { + constructor(readonly configService: ConfigService) { super({ - clientID: process.env.GITHUB_CLIENT_ID, - clientSecret: process.env.GITHUB_CLIENT_SECRET, - callbackURL: process.env.GITHUB_CALLBACK_URL, // frontend url + clientID: configService.get('GITHUB_CLIENT_ID'), + clientSecret: configService.get('GITHUB_CLIENT_SECRET'), + callbackURL: configService.get('GITHUB_CALLBACK_URL'), // frontend url }); } + async validate(accessToken: string, refreshToken: string, profile: any, done: VerifyCallback): Promise { const githubProfile: GithubProfile = { id: profile.id, From e79a38e1f63b2af8e58677228030aa51fad94fc8 Mon Sep 17 00:00:00 2001 From: huni Date: Mon, 5 Sep 2022 06:09:49 +0900 Subject: [PATCH 27/48] =?UTF-8?q?feat:=20jwt-auth=20module=20=EB=A1=9C=20?= =?UTF-8?q?=EB=AC=B6=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/auth/auth.module.ts | 2 + apps/api/src/auth/jwt-auth.guard.ts | 61 ----------------- apps/api/src/auth/jwt-auth/jwt-auth.guard.ts | 65 +++++++++++++++++++ apps/api/src/auth/jwt-auth/jwt-auth.module.ts | 10 +++ .../jwt-auth.strategy.ts} | 10 +-- 5 files changed, 82 insertions(+), 66 deletions(-) delete mode 100644 apps/api/src/auth/jwt-auth.guard.ts create mode 100644 apps/api/src/auth/jwt-auth/jwt-auth.guard.ts create mode 100644 apps/api/src/auth/jwt-auth/jwt-auth.module.ts rename apps/api/src/auth/{jwt.strategy.ts => jwt-auth/jwt-auth.strategy.ts} (72%) diff --git a/apps/api/src/auth/auth.module.ts b/apps/api/src/auth/auth.module.ts index 3572f3f4..69f1540d 100644 --- a/apps/api/src/auth/auth.module.ts +++ b/apps/api/src/auth/auth.module.ts @@ -5,11 +5,13 @@ import { JwtModule } from '@nestjs/jwt'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { GithubAuthModule } from './github-auth/github-auth.module'; +import { JwtAuthModule } from './jwt-auth/jwt-auth.module'; @Module({ imports: [ UserModule, GithubAuthModule, + JwtAuthModule, JwtModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], diff --git a/apps/api/src/auth/jwt-auth.guard.ts b/apps/api/src/auth/jwt-auth.guard.ts deleted file mode 100644 index e8e5121b..00000000 --- a/apps/api/src/auth/jwt-auth.guard.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { FORBIDDEN_USER_ROLE, REQUIRE_ROLES } from '@api/auth/auth.constant'; -import { UserRole } from '@app/entity/user/interfaces/userrole.interface'; -import { User } from '@app/entity/user/user.entity'; -import { ExecutionContext, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { AuthGuard } from '@nestjs/passport'; -import { AuthDecoratorParam } from './auth.decorator'; -import { getAccessToken } from './jwt.strategy'; - -/** - * Custom AuthGuard to check public handler and user roles - * @see also https://docs.nestjs.com/security/authentication#extending-guards - * - * 1. JwtAuthGuard.canActivate -> check if handler is public or not - * 2. JwtStrategy.validate -> get user from jwt payload or db - * 3. JwtAuthGuard.handleRequest -> check user roles - */ -@Injectable() -export class JwtAuthGuard extends AuthGuard('jwt') { - constructor(private reflector: Reflector) { - super(); - } - - canActivate(context: ExecutionContext) { - const requireAuth = this.reflector.get(REQUIRE_ROLES, context.getHandler()); - - if (!requireAuth) { - return true; - } - - const request = context.switchToHttp().getRequest(); - if (!getAccessToken(request)) { - request.user = new User(); - request.user.id = -1; - request.user.role = UserRole.GUEST; - try { - this.handleRequest(null, request.user, null, context); - } catch (error) { - throw new UnauthorizedException(); - } - return true; - } - - return super.canActivate(context); - } - - handleRequest(err: any, user: any, info: any, context: any, status?: any): TUser { - const u = super.handleRequest(err, user, info, context, status) as User; - const requireAuth = this.reflector.get(REQUIRE_ROLES, context.getHandler()); - - if (requireAuth[0] === 'allow' && !requireAuth.includes(u.role)) { - throw new ForbiddenException(FORBIDDEN_USER_ROLE); - } - - if (requireAuth[0] === 'deny' && requireAuth.includes(u.role)) { - throw new ForbiddenException(FORBIDDEN_USER_ROLE); - } - - return user; - } -} diff --git a/apps/api/src/auth/jwt-auth/jwt-auth.guard.ts b/apps/api/src/auth/jwt-auth/jwt-auth.guard.ts new file mode 100644 index 00000000..060829b9 --- /dev/null +++ b/apps/api/src/auth/jwt-auth/jwt-auth.guard.ts @@ -0,0 +1,65 @@ +import { FORBIDDEN_USER_ROLE, REQUIRE_ROLES } from '@api/auth/constant'; +import { UserRole } from '@app/entity/user/interfaces/userrole.interface'; +import { User } from '@app/entity/user/user.entity'; +import { ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; +import { AuthDecoratorParam } from '../types'; + +/** + * Custom AuthGuard to check public handler and user roles + * @see also https://docs.nestjs.com/security/authentication#extending-guards + * + * 1. JwtAuthGuard.canActivate -> check if handler is public or not + * 2. JwtStrategy.validate -> get user from jwt payload or db + * 3. JwtAuthGuard.handleRequest -> check user roles + */ +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') { + constructor(private readonly reflector: Reflector) { + super(); + } + + canActivate(context: ExecutionContext) { + const requireAuth = this.reflector.get(REQUIRE_ROLES, context.getHandler()); + + if (!requireAuth) { + return true; + } + + return super.canActivate(context); + } + + handleRequest(err: any, _user: any, info: any, context: any, status?: any): TUser { + const requireAuth = this.reflector.get(REQUIRE_ROLES, context.getHandler()); + + let user: User; + + try { + // verity JWT token + user = super.handleRequest(err, _user, info, context, status); + } catch (error) { + // Auth('public') ์ธ๊ฒฝ์šฐ Guest ๋กœ ์ฒ˜๋ฆฌ + if (this.isAuthAllowRole(requireAuth, UserRole.GUEST)) { + user = new User(); + user.id = -1; + user.role = UserRole.GUEST; + return user as unknown as TUser; + } + throw error; + } + + if (!this.isAuthAllowRole(requireAuth, user.role)) { + throw new ForbiddenException(FORBIDDEN_USER_ROLE); + } + + return user as unknown as TUser; + } + + private isAuthAllowRole(requireAuth: AuthDecoratorParam, role: UserRole): boolean { + return ( + (requireAuth[0] === 'allow' && requireAuth.includes(role)) || + (requireAuth[0] === 'deny' && !requireAuth.includes(role)) + ); + } +} diff --git a/apps/api/src/auth/jwt-auth/jwt-auth.module.ts b/apps/api/src/auth/jwt-auth/jwt-auth.module.ts new file mode 100644 index 00000000..1a4e6661 --- /dev/null +++ b/apps/api/src/auth/jwt-auth/jwt-auth.module.ts @@ -0,0 +1,10 @@ +import { UserModule } from '@api/user/user.module'; +import { Module } from '@nestjs/common'; +import { PassportModule } from '@nestjs/passport'; +import { JwtAuthStrategy } from './jwt-auth.strategy'; + +@Module({ + imports: [UserModule, PassportModule], + providers: [JwtAuthStrategy], +}) +export class JwtAuthModule {} diff --git a/apps/api/src/auth/jwt.strategy.ts b/apps/api/src/auth/jwt-auth/jwt-auth.strategy.ts similarity index 72% rename from apps/api/src/auth/jwt.strategy.ts rename to apps/api/src/auth/jwt-auth/jwt-auth.strategy.ts index 72acf235..6390f1fc 100644 --- a/apps/api/src/auth/jwt.strategy.ts +++ b/apps/api/src/auth/jwt-auth/jwt-auth.strategy.ts @@ -4,17 +4,17 @@ import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/co import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; -import { JWTPayload } from './interfaces/jwt-payload.interface'; +import { JWTPayload } from '../types'; -export const getAccessToken = (request: any): string => { - return request.cookies[process.env.ACCESS_TOKEN_KEY]; +export const getAccessToken = (key: string, request: any): string => { + return request.cookies[key]; }; @Injectable() -export class JwtStrategy extends PassportStrategy(Strategy) { +export class JwtAuthStrategy extends PassportStrategy(Strategy) { constructor(private userService: UserService, private configService: ConfigService) { super({ - jwtFromRequest: ExtractJwt.fromExtractors([getAccessToken]), + jwtFromRequest: ExtractJwt.fromExtractors([getAccessToken.bind(null, configService.get('ACCESS_TOKEN_KEY'))]), ignoreExpiration: false, secretOrKey: configService.get('JWT_SECRET'), }); From ee16b5356b19ad5435f4743144ca0b35dbe08d7a Mon Sep 17 00:00:00 2001 From: huni Date: Mon, 5 Sep 2022 06:10:57 +0900 Subject: [PATCH 28/48] =?UTF-8?q?fix:=20=EB=8D=B0=EC=BD=94=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=ED=84=B0,=20type=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/auth/auth.decorator.ts | 50 ++++++++++--------- apps/api/src/auth/types/auth.type.ts | 5 ++ .../github-profile.interface.ts | 0 apps/api/src/auth/types/index.ts | 3 ++ .../jwt-payload.interface.ts | 0 5 files changed, 34 insertions(+), 24 deletions(-) create mode 100644 apps/api/src/auth/types/auth.type.ts rename apps/api/src/auth/{interfaces => types}/github-profile.interface.ts (100%) create mode 100644 apps/api/src/auth/types/index.ts rename apps/api/src/auth/{interfaces => types}/jwt-payload.interface.ts (100%) diff --git a/apps/api/src/auth/auth.decorator.ts b/apps/api/src/auth/auth.decorator.ts index 8748fea4..d11ac97a 100644 --- a/apps/api/src/auth/auth.decorator.ts +++ b/apps/api/src/auth/auth.decorator.ts @@ -1,32 +1,15 @@ import { UserRole } from '@app/entity/user/interfaces/userrole.interface'; import { User } from '@app/entity/user/user.entity'; import { createParamDecorator, ExecutionContext, SetMetadata } from '@nestjs/common'; -import { REQUIRE_ROLES } from './auth.constant'; -import { GithubProfile } from './interfaces/github-profile.interface'; +import { REQUIRE_ROLES } from './constant'; +import { AuthType, GithubProfile } from './types'; /** - * @description Auth decorator๋ฅผ ์จ์•ผ ์‚ฌ์šฉ๊ฐ€๋Šฅ - */ -export const AuthUser = createParamDecorator((data: 'id' | null, ctx: ExecutionContext): User => { - const req = ctx.switchToHttp().getRequest(); - if (data) return req.user[data]; - return req.user; -}); - -export const ReqGithubProfile = createParamDecorator((data, ctx: ExecutionContext): GithubProfile => { - const req = ctx.switchToHttp().getRequest(); - return req.user; -}); - -type AuthType = 'allow' | 'deny'; - -export type AuthDecoratorParam = [AuthType, ...UserRole[]]; - -/** - * 1. ๋ˆ„๊ตฌ๋‚˜ ์‚ฌ์šฉ๊ฐ€๋Šฅ -> Auth() ์—†์Œ - * 2. ๋ˆ„๊ตฌ๋‚˜ ์ ‘์†๊ฐ€๋Šฅํ•˜์ง€๋งŒ ๊ถŒํ•œ๋”ฐ๋ผ ์‚ฌ์šฉ๊ฐ€๋Šฅ -> Auth('public') ==> Auth('deny') - * 3. ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋งŒ ์‚ฌ์šฉ๊ฐ€๋Šฅ -> Auth() ==> Auth('deny', UserRole.GUEST) - * 4. ํŠน์ • ์‚ฌ์šฉ์ž๋งŒ ์‚ฌ์šฉ๊ฐ€๋Šฅ -> Auth('only', UserRole.ADMIN) + * @description + * 1. Auth() ์—†์Œ => ๋ˆ„๊ตฌ๋‚˜ ์‚ฌ์šฉ๊ฐ€๋Šฅ(์ธ์ฆ X, ์ธ๊ฐ€ X) + * 2. Auth('public') or Auth('deny') => ๋ˆ„๊ตฌ๋‚˜ ์ ‘์†๊ฐ€๋Šฅํ•˜์ง€๋งŒ ๊ถŒํ•œ๋”ฐ๋ผ ์‚ฌ์šฉ๊ฐ€๋Šฅ(์ธ์ฆ โ–ณ, ์ธ๊ฐ€ O) + * 3. Auth() or Auth('deny', UserRole.GUEST) => ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋งŒ ๊ถŒํ•œ๋”ฐ๋ผ ์‚ฌ์šฉ๊ฐ€๋Šฅ(์ธ์ฆ O, ์ธ๊ฐ€ O) + * 4. Auth('only', UserRole.ADMIN) => ํŠน์ • ์‚ฌ์šฉ์ž๋งŒ ์‚ฌ์šฉ๊ฐ€๋Šฅ(์ธ์ฆ O, ์ธ๊ฐ€ O) * * 1 ๋ฒˆ๊ณผ 2๋ฒˆ์ด ๋‹ค๋ฅธ์  : 2๋ฒˆ์€ AuthUser๋ฅผ ์“ธ ์ˆ˜ ์žˆ์ง€๋งŒ, 1๋ฒˆ์€ ๋ชป์”€! */ @@ -41,3 +24,22 @@ export const Auth = (allow?: AuthType | 'public', ...param: UserRole[]) => { return SetMetadata(REQUIRE_ROLES, [allow, ...param]); }; + +/** + * @description Request์— ๋‹ด๊ธด User๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. + * @note Auth decorator๋ฅผ ์จ์•ผ ์‚ฌ์šฉ๊ฐ€๋Šฅ + */ +export const AuthUser = createParamDecorator((data: 'id' | null, ctx: ExecutionContext): User => { + const req = ctx.switchToHttp().getRequest(); + if (data) return req.user[data]; + return req.user; +}); + +/** + * @description Request์— ๋‹ด๊ธด GithubProfile๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. + * @note GithubAuthGuard ๋ฅผ ์จ์•ผ ์‚ฌ์šฉ๊ฐ€๋Šฅ + */ +export const ReqGithubProfile = createParamDecorator((data, ctx: ExecutionContext): GithubProfile => { + const req = ctx.switchToHttp().getRequest(); + return req.user; +}); diff --git a/apps/api/src/auth/types/auth.type.ts b/apps/api/src/auth/types/auth.type.ts new file mode 100644 index 00000000..618ae70a --- /dev/null +++ b/apps/api/src/auth/types/auth.type.ts @@ -0,0 +1,5 @@ +import { UserRole } from '@app/entity/user/interfaces/userrole.interface'; + +export type AuthType = 'allow' | 'deny'; + +export type AuthDecoratorParam = [AuthType, ...UserRole[]]; diff --git a/apps/api/src/auth/interfaces/github-profile.interface.ts b/apps/api/src/auth/types/github-profile.interface.ts similarity index 100% rename from apps/api/src/auth/interfaces/github-profile.interface.ts rename to apps/api/src/auth/types/github-profile.interface.ts diff --git a/apps/api/src/auth/types/index.ts b/apps/api/src/auth/types/index.ts new file mode 100644 index 00000000..1c4fb419 --- /dev/null +++ b/apps/api/src/auth/types/index.ts @@ -0,0 +1,3 @@ +export * from './auth.type'; +export * from './github-profile.interface'; +export * from './jwt-payload.interface'; diff --git a/apps/api/src/auth/interfaces/jwt-payload.interface.ts b/apps/api/src/auth/types/jwt-payload.interface.ts similarity index 100% rename from apps/api/src/auth/interfaces/jwt-payload.interface.ts rename to apps/api/src/auth/types/jwt-payload.interface.ts From 57ba0a403b0033ee7f4627350905305699caca28 Mon Sep 17 00:00:00 2001 From: huni Date: Mon, 5 Sep 2022 06:13:34 +0900 Subject: [PATCH 29/48] =?UTF-8?q?fix:=20JwtAuthGuard=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/app.module.ts | 2 +- apps/api/test/e2e/e2e-test.base.module.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 69a4ac88..f690c174 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -1,7 +1,7 @@ import { AppController } from '@api/app.controller'; import { ArticleModule } from '@api/article/article.module'; import { AuthModule } from '@api/auth/auth.module'; -import { JwtAuthGuard } from '@api/auth/jwt-auth.guard'; +import { JwtAuthGuard } from '@api/auth/jwt-auth/jwt-auth.guard'; import { BestModule } from '@api/best/best.module'; import { CategoryModule } from '@api/category/category.module'; import { CommentModule } from '@api/comment/comment.module'; diff --git a/apps/api/test/e2e/e2e-test.base.module.ts b/apps/api/test/e2e/e2e-test.base.module.ts index a002f6db..07b955eb 100644 --- a/apps/api/test/e2e/e2e-test.base.module.ts +++ b/apps/api/test/e2e/e2e-test.base.module.ts @@ -1,4 +1,4 @@ -import { JwtAuthGuard } from '@api/auth/jwt-auth.guard'; +import { JwtAuthGuard } from '@api/auth/jwt-auth/jwt-auth.guard'; import { AWS_ACCESS_KEY, AWS_REGION, AWS_SECRET_KEY } from '@api/image/image.constant'; import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; From 8c8834abe3f97daf23db3267eaface0d6f3a7e49 Mon Sep 17 00:00:00 2001 From: huni Date: Mon, 5 Sep 2022 06:13:58 +0900 Subject: [PATCH 30/48] =?UTF-8?q?fix:=20auth=20service=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/auth/auth.controller.ts | 26 +++++++-------- apps/api/src/auth/auth.service.ts | 44 ++++++++++++++++++++++++-- apps/api/src/user/user.service.ts | 47 ++++++++++++++++++---------- apps/api/test/e2e/utils/dummy.ts | 6 +--- libs/utils/src/utils.ts | 15 --------- 5 files changed, 84 insertions(+), 54 deletions(-) diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts index 3f1fc710..78358cc1 100644 --- a/apps/api/src/auth/auth.controller.ts +++ b/apps/api/src/auth/auth.controller.ts @@ -1,18 +1,16 @@ -import { UserService } from '@api/user/user.service'; -import { getCookieOption } from '@app/utils/utils'; import { Controller, Delete, Get, Res, UseGuards } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { ApiCookieAuth, ApiOkResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; import { Response } from 'express'; import { Auth, ReqGithubProfile } from './auth.decorator'; import { AuthService } from './auth.service'; -import { GithubAuthGuard } from './github-auth.guard'; -import { GithubProfile } from './interfaces/github-profile.interface'; -import { JWTPayload } from './interfaces/jwt-payload.interface'; +import { GithubAuthGuard } from './github-auth/github-auth.guard'; +import { GithubProfile } from './types'; @ApiTags('Auth') @Controller('auth') export class AuthController { - constructor(private readonly authService: AuthService, private readonly userService: UserService) {} + constructor(private readonly authService: AuthService, private readonly configService: ConfigService) {} @Get('github') @UseGuards(GithubAuthGuard) @@ -24,7 +22,6 @@ export class AuthController { }) @ApiOkResponse({ description: '๊นƒํ—ˆ๋ธŒ ํŽ˜์ด์ง€' }) githubLogin(): void { - console.log('send to login page'); return; } @@ -42,12 +39,11 @@ export class AuthController { @ReqGithubProfile() githubProfile: GithubProfile, @Res({ passthrough: true }) response: Response, ): Promise { - const user = await this.userService.githubLogin(githubProfile); - const jwt = this.authService.getJWT({ - userId: user.id, - userRole: user.role, - } as JWTPayload); - response.cookie(process.env.ACCESS_TOKEN_KEY, jwt, getCookieOption()); + const user = await this.authService.login(githubProfile); + const jwt = this.authService.getJwt(user); + const cookieOption = this.authService.getCookieOption(); + + response.cookie(this.configService.get('ACCESS_TOKEN_KEY'), jwt, cookieOption); } @Delete('signout') @@ -57,6 +53,8 @@ export class AuthController { @ApiOkResponse({ description: '๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต' }) @ApiUnauthorizedResponse({ description: '์ธ์ฆ ์‹คํŒจ' }) signout(@Res({ passthrough: true }) response: Response): void { - response.clearCookie(process.env.ACCESS_TOKEN_KEY, getCookieOption()); + const cookieOption = this.authService.getCookieOption(); + + response.clearCookie(this.configService.get('ACCESS_TOKEN_KEY'), cookieOption); } } diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index d19b29b0..cbe204ef 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -1,12 +1,50 @@ +import { UserService } from '@api/user/user.service'; +import { User } from '@app/entity/user/user.entity'; import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; -import { JWTPayload } from './interfaces/jwt-payload.interface'; +import { CookieOptions } from 'express'; +import { GithubProfile, JWTPayload } from './types'; @Injectable() export class AuthService { - constructor(private jwtService: JwtService) {} + constructor( + private readonly userService: UserService, + private readonly jwtService: JwtService, + private readonly configService: ConfigService, + ) {} - getJWT(payload: JWTPayload): string { + async login(githubProfile: GithubProfile): Promise { + const user = await this.userService.findOne({ where: { githubId: githubProfile.id } }); + + if (user) { + user.lastLogin = new Date(); + return await user.save(); + } + + const newUser = new User(); + newUser.nickname = githubProfile.username; + newUser.githubUsername = githubProfile.username; + newUser.githubUid = githubProfile.id; + newUser.lastLogin = new Date(); + + return await this.userService.create(newUser); + } + + getJwt(user: User): string { + const payload: JWTPayload = { + userId: user.id, + userRole: user.role, + }; return this.jwtService.sign(payload); } + + getCookieOption(): CookieOptions { + if (this.configService.get('NODE_ENV') === 'prod') { + return { httpOnly: true, secure: true, sameSite: 'lax' }; + } else if (this.configService.get('NODE_ENV') === 'alpha') { + return { httpOnly: true, secure: true, sameSite: 'none' }; + } + return {}; + } } diff --git a/apps/api/src/user/user.service.ts b/apps/api/src/user/user.service.ts index e11945bf..826f4a1a 100644 --- a/apps/api/src/user/user.service.ts +++ b/apps/api/src/user/user.service.ts @@ -1,7 +1,6 @@ -import { GithubProfile } from '@api/auth/interfaces/github-profile.interface'; import { User } from '@app/entity/user/user.entity'; -import { getNextMonth } from '@app/utils/utils'; import { Injectable } from '@nestjs/common'; +import { FindOneOptions } from 'typeorm'; import { UpdateUserProfileRequestDto } from './dto/request/update-user-profile-request.dto'; import { UpdateToCadetDto } from './dto/update-user-to-cadet.dto'; import { UserRepository } from './repositories/user.repository'; @@ -10,24 +9,38 @@ import { UserRepository } from './repositories/user.repository'; export class UserService { constructor(private readonly userRepository: UserRepository) {} - async githubLogin(githubProfile: GithubProfile): Promise { - const user = await this.userRepository.findOne({ - githubUid: githubProfile.id, - }); + async create(user: User): Promise { + return this.userRepository.save(user); + } + + // async githubLogin(githubProfile: GithubProfile): Promise { + // const user = await this.userRepository.findOne({ + // githubUid: githubProfile.id, + // }); + + // if (user) { + // user.lastLogin = getNextMonth(); + // return this.userRepository.save(user); + // } + + // const newUser = new User(); + // newUser.nickname = githubProfile.username; + // newUser.githubUsername = githubProfile.username; + // newUser.githubUid = githubProfile.id; + // newUser.lastLogin = getNextMonth(); - if (user) { - user.lastLogin = getNextMonth(); - return this.userRepository.save(user); - } + // // const newUser = { + // // nickname: githubProfile.username, + // // githubUsername: githubProfile.username, + // // githubUid: githubProfile.id, + // // lastLogin: getNextMonth(), + // // }; - const newUser = { - nickname: githubProfile.username, - githubUsername: githubProfile.username, - githubUid: githubProfile.id, - lastLogin: getNextMonth(), - }; + // return newUser.save(); + // } - return this.userRepository.save(newUser); + async findOne(options: FindOneOptions): Promise { + return this.userRepository.findOne(options); } async findOneByIdOrFail(id: number): Promise { diff --git a/apps/api/test/e2e/utils/dummy.ts b/apps/api/test/e2e/utils/dummy.ts index e50a7fb9..4888b6b9 100644 --- a/apps/api/test/e2e/utils/dummy.ts +++ b/apps/api/test/e2e/utils/dummy.ts @@ -1,6 +1,5 @@ import { ArticleRepository } from '@api/article/repositories/article.repository'; import { AuthService } from '@api/auth/auth.service'; -import { JWTPayload } from '@api/auth/interfaces/jwt-payload.interface'; import { CategoryRepository } from '@api/category/repositories/category.repository'; import { CommentRepository } from '@api/comment/repositories/comment.repository'; import { UserRepository } from '@api/user/repositories/user.repository'; @@ -14,10 +13,7 @@ import { UserRole } from '@app/entity/user/interfaces/userrole.interface'; import { User } from '@app/entity/user/user.entity'; export const jwt = (user: User, authService: AuthService): string => { - return authService.getJWT({ - userId: user.id, - userRole: user.role, - } as JWTPayload); + return authService.getJwt(user); }; export const user = ( diff --git a/libs/utils/src/utils.ts b/libs/utils/src/utils.ts index daa460dd..37f60940 100644 --- a/libs/utils/src/utils.ts +++ b/libs/utils/src/utils.ts @@ -1,7 +1,6 @@ import { PaginationRequestDto } from '@api/pagination/dto/pagination-request.dto'; import { UserRole } from '@app/entity/user/interfaces/userrole.interface'; import axios from 'axios'; -import { CookieOptions } from 'express'; import { logger } from './logger'; export const MINUTE = 60; @@ -14,25 +13,11 @@ export function getRandomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min)) + min; //์ตœ๋Œ“๊ฐ’์€ ์ œ์™ธ, ์ตœ์†Ÿ๊ฐ’์€ ํฌํ•จ } -export const getNextMonth = () => { - const now = new Date(); - return new Date(now.setMonth(now.getMonth() + 1)); -}; - export const isExpired = (exp: Date): boolean => { const now = new Date(); return now >= exp; }; -export const getCookieOption = (): CookieOptions => { - if (process.env.NODE_ENV === 'prod') { - return { httpOnly: true, secure: true, sameSite: 'lax' }; - } else if (process.env.NODE_ENV === 'alpha') { - return { httpOnly: true, secure: true, sameSite: 'none' }; - } - return {}; -}; - export const errorHook = async (exceptionName: string, exceptionMessage: string) => { const phase = process.env.NODE_ENV; const slackMessage = `[${phase}] ${exceptionName}: ${exceptionMessage}`; From 57ffce970cc84870a3cc293e9451b506cbdd43d4 Mon Sep 17 00:00:00 2001 From: huni Date: Mon, 5 Sep 2022 06:14:15 +0900 Subject: [PATCH 31/48] =?UTF-8?q?test:=20github-auth=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/github-auth.guard.test.ts | 41 +++++++++++++++++++ .../__test__/github-auth.module.test.ts | 25 +++++++++++ .../__test__/github-auth.strategy.test.ts | 31 ++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 apps/api/src/auth/github-auth/__test__/github-auth.guard.test.ts create mode 100644 apps/api/src/auth/github-auth/__test__/github-auth.module.test.ts create mode 100644 apps/api/src/auth/github-auth/__test__/github-auth.strategy.test.ts diff --git a/apps/api/src/auth/github-auth/__test__/github-auth.guard.test.ts b/apps/api/src/auth/github-auth/__test__/github-auth.guard.test.ts new file mode 100644 index 00000000..84c02f5d --- /dev/null +++ b/apps/api/src/auth/github-auth/__test__/github-auth.guard.test.ts @@ -0,0 +1,41 @@ +import { BadRequestException } from '@nestjs/common'; +import { mockFn } from 'jest-mock-extended'; +import { GithubAuthGuard } from '../github-auth.guard'; + +describe('GithubAuthGuard', () => { + const guard = new GithubAuthGuard(); + const mockSuperHandleRequest = mockFn(); + + beforeAll(async () => { + // ๋ถ€๋ชจ ํด๋ž˜์Šค mock ์ฒ˜๋ฆฌ + (GithubAuthGuard.prototype as any).__proto__.handleRequest = mockSuperHandleRequest; + }); + + beforeEach(async () => { + mockSuperHandleRequest.mockClear(); + jest.clearAllTimers(); + }); + + describe('handleRequest', () => { + it('์ •์ƒ์ ์ธ ์š”์ฒญ์€ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.', async () => { + const githubProfile = { id: 1 }; + mockSuperHandleRequest.mockReturnValue(githubProfile); + + const result = guard.handleRequest(null, null, null, null); + + expect(result).toBe(githubProfile); + expect(mockSuperHandleRequest).toBeCalledTimes(1); + }); + + it('์ •์ƒ์ ์ธ ์š”์ฒญ์ด ์•„๋‹ˆ๋ฉด BadRequestException ์—๋Ÿฌ๋ฅผ ๋˜์ง„๋‹ค.', async () => { + mockSuperHandleRequest.mockImplementation(() => { + throw new Error('error'); + }); + + const act = () => guard.handleRequest(null, null, null, null); + + expect(act).toThrowError(BadRequestException); + expect(mockSuperHandleRequest).toBeCalledTimes(1); + }); + }); +}); diff --git a/apps/api/src/auth/github-auth/__test__/github-auth.module.test.ts b/apps/api/src/auth/github-auth/__test__/github-auth.module.test.ts new file mode 100644 index 00000000..7697799f --- /dev/null +++ b/apps/api/src/auth/github-auth/__test__/github-auth.module.test.ts @@ -0,0 +1,25 @@ +import { ConfigModule } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { GithubAuthModule } from '../github-auth.module'; + +describe('GithubAuthModule', () => { + it('๋ชจ๋“ˆ์ด ์ž˜ ์ปดํŒŒ์ผ๋œ๋‹ค.', async () => { + const module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [ + () => ({ + GITHUB_CLIENT_ID: '123', + GITHUB_CLIENT_SECRET: '123', + GITHUB_CALLBACK_URL: '123', + }), + ], + }), + GithubAuthModule, + ], + }).compile(); + + expect(module).toBeDefined(); + }); +}); diff --git a/apps/api/src/auth/github-auth/__test__/github-auth.strategy.test.ts b/apps/api/src/auth/github-auth/__test__/github-auth.strategy.test.ts new file mode 100644 index 00000000..6a52a257 --- /dev/null +++ b/apps/api/src/auth/github-auth/__test__/github-auth.strategy.test.ts @@ -0,0 +1,31 @@ +import { ConfigService } from '@nestjs/config'; +import { mock, mockFn } from 'jest-mock-extended'; +import { VerifiedCallback } from 'passport-jwt'; +import { GithubAuthStrategy } from '../github-auth.strategy'; + +describe('GithubAuthStrategy', () => { + const mockConfigService = mock({ + get: mockFn().mockReturnValue('test'), + }); + const githubAuthStrategy = new GithubAuthStrategy(mockConfigService); + + describe('validate', () => { + it('GithubProfile์ด ๋ฐ˜ํ™˜๋œ๋‹ค', async () => { + const accessToken = ''; + const refreshToken = ''; + const profile = { + id: 'testid', + username: 'testusername', + other: 'zzz', + }; + const done = mockFn(); + + await githubAuthStrategy.validate(accessToken, refreshToken, profile, done); + + expect(done).toBeCalledWith(null, { + id: 'testid', + username: 'testusername', + }); + }); + }); +}); From 69020dc524320d0a31bf1ea9354d7bb65786235a Mon Sep 17 00:00:00 2001 From: huni Date: Mon, 5 Sep 2022 06:14:24 +0900 Subject: [PATCH 32/48] =?UTF-8?q?test:=20jwt-auth=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jwt-auth/__test__/jwt-auth.guard.test.ts | 163 ++++++++++++++++++ .../jwt-auth/__test__/jwt-auth.module.test.ts | 13 ++ .../__test__/jwt-auth.strategy.test.ts | 69 ++++++++ 3 files changed, 245 insertions(+) create mode 100644 apps/api/src/auth/jwt-auth/__test__/jwt-auth.guard.test.ts create mode 100644 apps/api/src/auth/jwt-auth/__test__/jwt-auth.module.test.ts create mode 100644 apps/api/src/auth/jwt-auth/__test__/jwt-auth.strategy.test.ts diff --git a/apps/api/src/auth/jwt-auth/__test__/jwt-auth.guard.test.ts b/apps/api/src/auth/jwt-auth/__test__/jwt-auth.guard.test.ts new file mode 100644 index 00000000..a3a5e8c7 --- /dev/null +++ b/apps/api/src/auth/jwt-auth/__test__/jwt-auth.guard.test.ts @@ -0,0 +1,163 @@ +import { UserRole } from '@app/entity/user/interfaces/userrole.interface'; +import { User } from '@app/entity/user/user.entity'; +import { ForbiddenException, UnauthorizedException } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'; +import { mock, mockFn } from 'jest-mock-extended'; +import { AuthDecoratorParam } from '../../types'; +import { JwtAuthGuard } from '../jwt-auth.guard'; + +describe('JwtAuthGuard', () => { + const mockRelfector = mock(); + const context = new ExecutionContextHost([]); + const mockSuperCanActivate = mockFn(); + const mockSuperHandleRequest = mockFn(); + const jwtAuthGuard = new JwtAuthGuard(mockRelfector); + + beforeAll(async () => { + // ๋ถ€๋ชจ ํด๋ž˜์Šค mock ์ฒ˜๋ฆฌ + (JwtAuthGuard.prototype as any).__proto__.canActivate = mockSuperCanActivate; + (JwtAuthGuard.prototype as any).__proto__.handleRequest = mockSuperHandleRequest; + }); + + beforeEach(async () => { + mockRelfector.get.mockClear(); + mockSuperCanActivate.mockClear(); + mockSuperHandleRequest.mockClear(); + jest.clearAllTimers(); + }); + + describe('canActivate', () => { + it('REQUIRE_ROLES ๊ฐ€ ์—†์œผ๋ฉด ๋ฌด์กฐ๊ฑด true ์ด๋‹ค. ', async () => { + mockRelfector.get.mockReturnValue(undefined); + + const result = jwtAuthGuard.canActivate(context); + + expect(result).toBe(true); + expect(mockSuperCanActivate).toBeCalledTimes(0); + }); + + it('REQUIRE_ROLES ๊ฐ€ ์žˆ์œผ๋ฉด super๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.', async () => { + mockRelfector.get.mockReturnValue(['allow'] as AuthDecoratorParam); + mockSuperCanActivate.mockReturnValue(true); + + const result = jwtAuthGuard.canActivate(context); + + expect(result).toBe(true); + expect(mockSuperCanActivate).toBeCalledTimes(1); + }); + }); + + describe('handleRequest', () => { + it('allow ํ•˜๋Š” ๊ถŒํ•œ์ด ์•„์˜ˆ ์—†์œผ๋ฉด ์•„๋ฌด ๊ถŒํ•œ๋„ ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•œ๋‹ค.', async () => { + mockRelfector.get.mockReturnValue(['allow'] as AuthDecoratorParam); + + const user = new User(); + user.role = UserRole.ADMIN; + mockSuperHandleRequest.mockReturnValue(user); + + const act = () => jwtAuthGuard.handleRequest(null, null, null, context); + + expect(act).toThrowError(ForbiddenException); + expect(mockSuperHandleRequest).toBeCalledTimes(1); + expect(act).toThrowErrorMatchingInlineSnapshot(`"์ ‘๊ทผ ๊ถŒํ•œ ์—†์Œ"`); + }); + + it('allow ํ•˜๋Š” ๊ถŒํ•œ์— ํฌํ•จ๋˜์–ด์žˆ์œผ๋ฉด ํ†ต๊ณผํ•œ๋‹ค.', async () => { + mockRelfector.get.mockReturnValue(['allow', UserRole.ADMIN] as AuthDecoratorParam); + + const user = new User(); + user.role = UserRole.ADMIN; + mockSuperHandleRequest.mockReturnValue(user); + + const result = jwtAuthGuard.handleRequest(null, null, null, context); + + expect(result).toBe(user); + expect(mockSuperHandleRequest).toBeCalledTimes(1); + }); + + it('allow ํ•˜๋Š” ๊ถŒํ•œ์— ํฌํ•จ๋˜์–ด์žˆ์ง€ ์•Š์œผ๋ฉด ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•œ๋‹ค.', async () => { + mockRelfector.get.mockReturnValue(['allow', UserRole.ADMIN] as AuthDecoratorParam); + + const user = new User(); + user.role = UserRole.GUEST; + mockSuperHandleRequest.mockReturnValue(user); + + const act = () => jwtAuthGuard.handleRequest(null, null, null, context); + + expect(act).toThrowError(ForbiddenException); + expect(mockSuperHandleRequest).toBeCalledTimes(1); + expect(act).toThrowErrorMatchingInlineSnapshot(`"์ ‘๊ทผ ๊ถŒํ•œ ์—†์Œ"`); + }); + + it('deny ํ•˜๋Š” ๊ถŒํ•œ์ด ์•„์˜ˆ ์—†์œผ๋ฉด ๋ชจ๋“  ๊ถŒํ•œ์ด ํ†ต๊ณผํ•œ๋‹ค.', async () => { + mockRelfector.get.mockReturnValue(['deny'] as AuthDecoratorParam); + + const user = new User(); + user.role = UserRole.ADMIN; + mockSuperHandleRequest.mockReturnValue(user); + + const result = jwtAuthGuard.handleRequest(null, null, null, context); + + expect(result).toBe(user); + expect(mockSuperHandleRequest).toBeCalledTimes(1); + }); + + it('deny ํ•˜๋Š” ๊ถŒํ•œ์— ํฌํ•จ๋˜์–ด์žˆ์œผ๋ฉด ํ†ต๊ณผํ•˜์ง€ ์•Š๋Š”๋‹ค.', async () => { + mockRelfector.get.mockReturnValue(['deny', UserRole.ADMIN] as AuthDecoratorParam); + + const user = new User(); + user.role = UserRole.ADMIN; + mockSuperHandleRequest.mockReturnValue(user); + + const act = () => jwtAuthGuard.handleRequest(null, null, null, context); + + expect(act).toThrowError(ForbiddenException); + expect(mockSuperHandleRequest).toBeCalledTimes(1); + expect(act).toThrowErrorMatchingInlineSnapshot(`"์ ‘๊ทผ ๊ถŒํ•œ ์—†์Œ"`); + }); + + it('deny ํ•˜๋Š” ๊ถŒํ•œ์— ํฌํ•จ๋˜์–ด์žˆ์ง€ ์•Š์œผ๋ฉด ํ†ต๊ณผํ•œ๋‹ค.', async () => { + mockRelfector.get.mockReturnValue(['deny', UserRole.GUEST] as AuthDecoratorParam); + + const user = new User(); + user.role = UserRole.ADMIN; + mockSuperHandleRequest.mockReturnValue(user); + + const result = jwtAuthGuard.handleRequest(null, null, null, context); + + expect(result).toBe(user); + expect(mockSuperHandleRequest).toBeCalledTimes(1); + }); + + it('jwt ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์„๋•Œ, GUEST ๊ถŒํ•œ์ด ํ—ˆ๋ฝ๋˜๋ฉด GUEST๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.', async () => { + mockRelfector.get.mockReturnValue(['allow', UserRole.GUEST] as AuthDecoratorParam); + + // jwt ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์„๋•Œ ์—๋Ÿฌ๋ฅผ ๋˜์ง. + mockSuperHandleRequest.mockImplementation(() => { + throw new UnauthorizedException(); + }); + + const result = jwtAuthGuard.handleRequest(null, null, null, context) as User; + + expect(result.id).toBe(-1); + expect(result.role).toBe(UserRole.GUEST); + expect(mockSuperHandleRequest).toBeCalledTimes(1); + }); + + it('jwt ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์„๋•Œ, GUEST ๊ถŒํ•œ๋„ ํ—ˆ๋ฝ๋˜์ง€ ์•Š์œผ๋ฉด ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•œ๋‹ค.', async () => { + mockRelfector.get.mockReturnValue(['deny', UserRole.GUEST] as AuthDecoratorParam); + + // jwt ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์„๋•Œ ์—๋Ÿฌ๋ฅผ ๋˜์ง. + mockSuperHandleRequest.mockImplementation(() => { + throw new UnauthorizedException(); + }); + + const act = () => jwtAuthGuard.handleRequest(null, null, null, context) as User; + + expect(act).toThrowError(UnauthorizedException); + expect(mockSuperHandleRequest).toBeCalledTimes(1); + expect(act).toThrowErrorMatchingInlineSnapshot(`"Unauthorized"`); + }); + }); +}); diff --git a/apps/api/src/auth/jwt-auth/__test__/jwt-auth.module.test.ts b/apps/api/src/auth/jwt-auth/__test__/jwt-auth.module.test.ts new file mode 100644 index 00000000..ebb86078 --- /dev/null +++ b/apps/api/src/auth/jwt-auth/__test__/jwt-auth.module.test.ts @@ -0,0 +1,13 @@ +import { JwtAuthModule } from '../jwt-auth.module'; + +describe('JwtAuthGuard', () => { + it('๋ชจ๋“ˆ์ด ์ž˜ ์ปดํŒŒ์ผ๋œ๋‹ค.', async () => { + // TODO: Test.createTestingModule ๋กœ complie ํ• ๊ฒƒ + // const module = await Test.createTestingModule({ + // imports: [JwtAuthModule], + // }).compile(); + const module = new JwtAuthModule(); + + expect(module).toBeDefined(); + }); +}); diff --git a/apps/api/src/auth/jwt-auth/__test__/jwt-auth.strategy.test.ts b/apps/api/src/auth/jwt-auth/__test__/jwt-auth.strategy.test.ts new file mode 100644 index 00000000..d8ab92b3 --- /dev/null +++ b/apps/api/src/auth/jwt-auth/__test__/jwt-auth.strategy.test.ts @@ -0,0 +1,69 @@ +import { UserService } from '@api/user/user.service'; +import { UserRole } from '@app/entity/user/interfaces/userrole.interface'; +import { User } from '@app/entity/user/user.entity'; +import { NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { mock, mockFn } from 'jest-mock-extended'; +import { JWTPayload } from '../../types'; +import { getAccessToken, JwtAuthStrategy } from '../jwt-auth.strategy'; + +describe('JwtAuthStrategy', () => { + const mockConfigService = mock({ + get: mockFn().mockReturnValue('test'), + }); + const mockUserService = mock(); + const jwtAuthStrategy = new JwtAuthStrategy(mockUserService, mockConfigService); + const payload: JWTPayload = { + userId: 1, + userRole: UserRole.GUEST, + }; + + beforeEach(() => { + mockUserService.findOneByIdOrFail.mockClear(); + jest.clearAllTimers(); + }); + + describe('validate', () => { + it('์œ ์ €๊ฐ€ ์กด์žฌํ•˜๋ฉด ์œ ์ €๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค', async () => { + const user = new User(); + mockUserService.findOneByIdOrFail.mockResolvedValue(user); + + const result = await jwtAuthStrategy.validate(payload); + + expect(result).toBe(user); + expect(mockUserService.findOneByIdOrFail).toBeCalledTimes(1); + }); + + it('์œ ์ €๊ฐ€ ์—†์œผ๋ฉด UnauthorizedException ์—๋Ÿฌ๋ฅผ ๋˜์ง„๋‹ค', async () => { + mockUserService.findOneByIdOrFail.mockRejectedValue(new NotFoundException()); + + const act = async () => await jwtAuthStrategy.validate(payload); + + expect(act).rejects.toThrowError(UnauthorizedException); + expect(mockUserService.findOneByIdOrFail).toBeCalledTimes(1); + }); + + it('์—๋Ÿฌ๊ฐ€ ๋‚˜๋ฉด ์—๋Ÿฌ๋ฅผ ๋˜์ง„๋‹ค', async () => { + mockUserService.findOneByIdOrFail.mockRejectedValue(new Error()); + + const act = async () => await jwtAuthStrategy.validate(payload); + + expect(act).rejects.toThrowError(Error); + expect(mockUserService.findOneByIdOrFail).toBeCalledTimes(1); + }); + }); + + describe('getAccessToken', () => { + it('์ฟ ํ‚ค์—์„œ ACCESS_TOKEN_KEY๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => { + const request = { + cookies: { + ACCESS_TOKEN_KEY: 'test', + }, + }; + + const result = getAccessToken('ACCESS_TOKEN_KEY', request); + + expect(result).toBe('test'); + }); + }); +}); From f0fcb1a4292786028b65a7ff4423280b491280de Mon Sep 17 00:00:00 2001 From: huni Date: Mon, 5 Sep 2022 06:14:35 +0900 Subject: [PATCH 33/48] =?UTF-8?q?test:=20auth=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/auth/__test__/auth.controller.test.ts | 57 ++++++++ .../src/auth/__test__/auth.decorator.test.ts | 128 ++++++++++++++++++ .../api/src/auth/__test__/auth.module.test.ts | 13 ++ .../src/auth/__test__/auth.service.test.ts | 94 +++++++++++++ .../auth/__test__/getParamDecoratorFactory.ts | 21 +++ 5 files changed, 313 insertions(+) create mode 100644 apps/api/src/auth/__test__/auth.controller.test.ts create mode 100644 apps/api/src/auth/__test__/auth.decorator.test.ts create mode 100644 apps/api/src/auth/__test__/auth.module.test.ts create mode 100644 apps/api/src/auth/__test__/auth.service.test.ts create mode 100644 apps/api/src/auth/__test__/getParamDecoratorFactory.ts diff --git a/apps/api/src/auth/__test__/auth.controller.test.ts b/apps/api/src/auth/__test__/auth.controller.test.ts new file mode 100644 index 00000000..2b887377 --- /dev/null +++ b/apps/api/src/auth/__test__/auth.controller.test.ts @@ -0,0 +1,57 @@ +import { ConfigService } from '@nestjs/config'; +import { Response } from 'express'; +import { mock, mockFn } from 'jest-mock-extended'; +import { AuthController } from '../auth.controller'; +import { AuthService } from '../auth.service'; +import { GithubProfile } from '../types'; + +describe('AuthController', () => { + const mockAuthService = mock({ + login: mockFn().mockResolvedValue({ id: 1 }), + getJwt: mockFn().mockReturnValue('jwt'), + getCookieOption: mockFn().mockReturnValue({ cookie: 'test' }), + }); + const mockConfigService = mock({ + get: mockFn().mockReturnValue('access-token-key'), + }); + const authController = new AuthController(mockAuthService, mockConfigService); + + beforeEach(() => { + jest.clearAllTimers(); + }); + + describe('githubLogin', () => { + it('์ •์ƒ ํ˜ธ์ถœ', async () => { + const actual = () => authController.githubLogin(); + + expect(actual).not.toThrow(); + }); + }); + + describe('githubCallback', () => { + it('๋กœ๊ทธ์ธํ•˜๋ฉด ์ฟ ํ‚ค๋ฅผ ์„ธํŒ…ํ•œ๋‹ค', async () => { + const githubProfile: GithubProfile = { id: '1', username: 'test' }; + const mockResponse = mock({ + cookie: mockFn().mockReturnThis(), + }); + + await authController.githubCallback(githubProfile, mockResponse); + + expect(mockResponse.cookie).toBeCalledTimes(1); + expect(mockResponse.cookie).toBeCalledWith('access-token-key', 'jwt', { cookie: 'test' }); + }); + }); + + describe('signout', () => { + it('๋กœ๊ทธ์•„์›ƒํ•˜๋ฉด ์ฟ ํ‚ค๋ฅผ ๋น„์šด๋‹ค', async () => { + const mockResponse = mock({ + clearCookie: mockFn().mockReturnThis(), + }); + + authController.signout(mockResponse); + + expect(mockResponse.clearCookie).toBeCalledTimes(1); + expect(mockResponse.clearCookie).toBeCalledWith('access-token-key', { cookie: 'test' }); + }); + }); +}); diff --git a/apps/api/src/auth/__test__/auth.decorator.test.ts b/apps/api/src/auth/__test__/auth.decorator.test.ts new file mode 100644 index 00000000..6d106406 --- /dev/null +++ b/apps/api/src/auth/__test__/auth.decorator.test.ts @@ -0,0 +1,128 @@ +import { UserRole } from '@app/entity/user/interfaces/userrole.interface'; +import { User } from '@app/entity/user/user.entity'; +import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'; +import 'reflect-metadata'; +import { Auth, AuthUser, ReqGithubProfile } from '../auth.decorator'; +import { REQUIRE_ROLES } from '../constant'; +import { getParamDecorator } from './getParamDecoratorFactory'; + +describe('AuthDecorator', () => { + describe('Auth', () => { + it('Auth๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉด ๊ถŒํ•œ์ด ์„ค์ •์ด ๋˜์ง€ ์•Š๋Š”๋‹ค.', async () => { + class TestClass { + testMethod() {} + } + + const result = Reflect.getMetadata(REQUIRE_ROLES, new TestClass().testMethod); + + expect(result).toBeUndefined(); + }); + + it(`Auth('allow', UserRole.GUEST, UserRole.ADMIN) ๋Š” GUEST, ADMIN ๋‘˜๋‹ค ํ—ˆ๋ฝํ•œ๋‹ค`, async () => { + class TestClass { + @Auth('allow', UserRole.GUEST, UserRole.ADMIN) + testMethod() {} + } + + const result = Reflect.getMetadata(REQUIRE_ROLES, new TestClass().testMethod); + + expect(result).toStrictEqual(['allow', UserRole.GUEST, UserRole.ADMIN]); + }); + + it(`Auth('allow') ๋Š” ์•„๋ฌด๊ถŒํ•œ๋„ ํ—ˆ๋ฝํ•˜์ง€ ์•Š๋Š”๋‹ค.`, async () => { + class TestClass { + @Auth('allow') + testMethod() {} + } + + const result = Reflect.getMetadata(REQUIRE_ROLES, new TestClass().testMethod); + + expect(result).toStrictEqual(['allow']); + }); + + it(`Auth('deny', UserRole.GUEST, UserRole.ADMIN) ๋Š” GUEST, ADMIN ๋‘˜๋‹ค ํ—ˆ๋ฝํ•œ๋‹ค`, async () => { + class TestClass { + @Auth('deny', UserRole.GUEST, UserRole.ADMIN) + testMethod() {} + } + + const result = Reflect.getMetadata(REQUIRE_ROLES, new TestClass().testMethod); + + expect(result).toStrictEqual(['deny', UserRole.GUEST, UserRole.ADMIN]); + }); + + it(`Auth('deny') ๋Š” ๋ชจ๋“  ๊ถŒํ•œ์„ ํ—ˆ๋ฝํ•œ๋‹ค.`, async () => { + class TestClass { + @Auth('deny') + testMethod() {} + } + + const result = Reflect.getMetadata(REQUIRE_ROLES, new TestClass().testMethod); + + expect(result).toStrictEqual(['deny']); + }); + + it(`Auth('public') ์€ ๋ชจ๋“  ๊ถŒํ•œ์„ ํ—ˆ๋ฝํ•œ๋‹ค.`, async () => { + class TestClass { + @Auth('public') + testMethod() {} + } + + const result = Reflect.getMetadata(REQUIRE_ROLES, new TestClass().testMethod); + + expect(result).toStrictEqual(['deny']); + }); + + it(`Auth() ์€ GUEST๋ฅผ ์ œ์™ธํ•œ ๊ถŒํ•œ์„ ํ—ˆ๋ฝํ•œ๋‹ค.`, async () => { + class TestClass { + @Auth() + testMethod() {} + } + + const result = Reflect.getMetadata(REQUIRE_ROLES, new TestClass().testMethod); + + expect(result).toStrictEqual(['deny', UserRole.GUEST]); + }); + }); + + describe('AuthUser', () => { + it('Request ์— ๋‹ด๊ธด user๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค', async () => { + const user = new User(); + const context = new ExecutionContextHost([{ user }, {}]); + + const result = getParamDecorator(AuthUser)(null, context); + + expect(result).toStrictEqual(user); + }); + + it('Request ์— ๋‹ด๊ธด user์˜ id๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค', async () => { + const user = new User(); + user.id = 1; + const context = new ExecutionContextHost([{ user }, {}]); + + const result = getParamDecorator(AuthUser)('id', context); + + expect(result).toStrictEqual(user.id); + }); + + it('Request ์— ๋‹ด๊ธด user์˜ id๊ฐ€ ์—†๋Š”๊ฒฝ์šฐ', async () => { + const user = new User(); + const context = new ExecutionContextHost([{ user }, {}]); + + const result = getParamDecorator(AuthUser)('id', context); + + expect(result).toBeUndefined(); + }); + }); + + describe('ReqGithubProfile', () => { + it('Request ์— ๋‹ด๊ธด githubprofile๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค', async () => { + const user = { id: 1, username: 'test' }; + const context = new ExecutionContextHost([{ user }, {}]); + + const result = getParamDecorator(ReqGithubProfile)(null, context); + + expect(result).toStrictEqual(user); + }); + }); +}); diff --git a/apps/api/src/auth/__test__/auth.module.test.ts b/apps/api/src/auth/__test__/auth.module.test.ts new file mode 100644 index 00000000..12c9b220 --- /dev/null +++ b/apps/api/src/auth/__test__/auth.module.test.ts @@ -0,0 +1,13 @@ +import { AuthModule } from '../auth.module'; + +describe('AuthModule', () => { + it('๋ชจ๋“ˆ์ด ์ž˜ ์ปดํŒŒ์ผ๋œ๋‹ค.', async () => { + // TODO: Test.createTestingModule ๋กœ complie ํ• ๊ฒƒ + // const module = await Test.createTestingModule({ + // imports: [AuthModule], + // }).compile(); + const module = new AuthModule(); + + expect(module).toBeDefined(); + }); +}); diff --git a/apps/api/src/auth/__test__/auth.service.test.ts b/apps/api/src/auth/__test__/auth.service.test.ts new file mode 100644 index 00000000..d2a87024 --- /dev/null +++ b/apps/api/src/auth/__test__/auth.service.test.ts @@ -0,0 +1,94 @@ +import { UserService } from '@api/user/user.service'; +import { UserRole } from '@app/entity/user/interfaces/userrole.interface'; +import { User } from '@app/entity/user/user.entity'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { mock, mockFn } from 'jest-mock-extended'; +import { AuthService } from '../auth.service'; +import { GithubProfile } from '../types'; + +describe('AuthService', () => { + const mockUserSerivce = mock(); + const mockJwtService = mock({ + sign: mockFn().mockReturnValue('jwt'), + }); + const mockConfigService = mock(); + const authService = new AuthService(mockUserSerivce, mockJwtService, mockConfigService); + + beforeEach(() => { + mockUserSerivce.findOne.mockClear(); + mockConfigService.get.mockClear(); + jest.clearAllTimers(); + }); + + describe('login', () => { + it('์ด๋ฏธ ๊ฐ€์ž…ํ•œ ์œ ์ €๋Š” ๋กœ๊ทธ์ธ ์‹œ๊ฐ„์„ ์—…๋ฐ์ดํŠธ ํ•œ๋‹ค', async () => { + const user = new User(); + const oldLastLogin = new Date(); + user.lastLogin = oldLastLogin; + user.save = mockFn().mockReturnThis(); + + const githubProfile: GithubProfile = { id: '1', username: 'test' }; + mockUserSerivce.findOne.mockResolvedValue(user); + + const result: User = await authService.login(githubProfile); + + expect(result).toBeDefined(); + expect(result.lastLogin).not.toBe(oldLastLogin); + }); + + it('์ฒ˜์Œ ๋กœ๊ทธ์ธํ•˜๋Š” ์œ ์ €๋Š” ์œ ์ €๋ฅผ ์ƒ์„ฑํ•œ๋‹ค', async () => { + const githubProfile: GithubProfile = { id: '1', username: 'test' }; + mockUserSerivce.findOne.mockResolvedValue(undefined); + mockUserSerivce.create.mockImplementation(async (user: User) => user); + + const result: User = await authService.login(githubProfile); + + expect(result).toBeDefined(); + expect(result.nickname).toBe(githubProfile.username); + expect(result.githubUsername).toBe(githubProfile.username); + expect(result.githubUid).toBe(githubProfile.id); + expect(mockUserSerivce.create).toBeCalledTimes(1); + }); + }); + + describe('getJwt', () => { + it('jwt๋ฅผ ๋งŒ๋“ ๋‹ค', async () => { + const user = new User(); + user.id = 1; + user.role = UserRole.ADMIN; + + const result = authService.getJwt(user); + + expect(result).toBe('jwt'); + expect(mockJwtService.sign).toBeCalledTimes(1); + expect(mockJwtService.sign).toBeCalledWith({ userId: 1, userRole: UserRole.ADMIN }); + }); + }); + + describe('getCookieOption', () => { + it('prod ์ผ๋–„ ์ฟ ํ‚ค ์˜ต์…˜', async () => { + mockConfigService.get.mockReturnValue('prod'); + + const result = authService.getCookieOption(); + + expect(result).toStrictEqual({ httpOnly: true, secure: true, sameSite: 'lax' }); + }); + + it('alpha ์ผ๋–„ ์ฟ ํ‚ค ์˜ต์…˜', async () => { + mockConfigService.get.mockReturnValue('alpha'); + + const result = authService.getCookieOption(); + + expect(result).toStrictEqual({ httpOnly: true, secure: true, sameSite: 'none' }); + }); + + it('dev/test ์ผ๋•Œ ์ฟ ํ‚ค ์˜ต์…˜', async () => { + mockConfigService.get.mockReturnValue('dev'); + + const result = authService.getCookieOption(); + + expect(result).toStrictEqual({}); + }); + }); +}); diff --git a/apps/api/src/auth/__test__/getParamDecoratorFactory.ts b/apps/api/src/auth/__test__/getParamDecoratorFactory.ts new file mode 100644 index 00000000..8f8560a4 --- /dev/null +++ b/apps/api/src/auth/__test__/getParamDecoratorFactory.ts @@ -0,0 +1,21 @@ +import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; + +/** + * @description Param Decorator๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ํ—ฌํผ ํ•จ์ˆ˜ + * + * @example + * ``` + * const result = getParamDecorator(AuthUser)('id', context) + * ``` + * + * @see https://github.com/nestjs/nest/issues/1020 + * @see https://github.com/EnricoFerro/test-NestJs7-Decorator/blob/master/src/app.controller.spec.ts + */ +export function getParamDecorator(decorator: Function) { + class Test { + public test(@decorator() value: unknown) {} + } + + const args = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test'); + return args[Object.keys(args)[0]].factory; +} From 3f7504b6f8c1f8b40c507df6e761fc34d699a2c4 Mon Sep 17 00:00:00 2001 From: huni Date: Mon, 5 Sep 2022 06:14:44 +0900 Subject: [PATCH 34/48] =?UTF-8?q?test:=20types=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/auth/types/__test__/index.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 apps/api/src/auth/types/__test__/index.test.ts diff --git a/apps/api/src/auth/types/__test__/index.test.ts b/apps/api/src/auth/types/__test__/index.test.ts new file mode 100644 index 00000000..28e7903d --- /dev/null +++ b/apps/api/src/auth/types/__test__/index.test.ts @@ -0,0 +1,15 @@ +import * as Types from '../index'; + +/** + * @description test coverage ๋•Œ๋ฌธ์— ๋„ฃ์Œ + * @see https://stackoverflow.com/questions/67388165/how-to-get-jest-to-have-coverage-for-export-only-lines + */ +describe('Types', () => { + it('should have exports', () => { + expect(typeof Types).toBe('object'); + }); + + it('should not have undefined exports', () => { + Object.keys(Types).forEach((exportKey) => expect(Boolean(Types[exportKey])).toBe(true)); + }); +}); From c9f382d26b27af874ed9792f72783fc25928efaa Mon Sep 17 00:00:00 2001 From: huni Date: Mon, 5 Sep 2022 15:56:40 +0900 Subject: [PATCH 35/48] =?UTF-8?q?chore:=20=EC=A3=BC=EC=84=9D=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/user/user.service.ts | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/apps/api/src/user/user.service.ts b/apps/api/src/user/user.service.ts index 826f4a1a..5ade90aa 100644 --- a/apps/api/src/user/user.service.ts +++ b/apps/api/src/user/user.service.ts @@ -13,32 +13,6 @@ export class UserService { return this.userRepository.save(user); } - // async githubLogin(githubProfile: GithubProfile): Promise { - // const user = await this.userRepository.findOne({ - // githubUid: githubProfile.id, - // }); - - // if (user) { - // user.lastLogin = getNextMonth(); - // return this.userRepository.save(user); - // } - - // const newUser = new User(); - // newUser.nickname = githubProfile.username; - // newUser.githubUsername = githubProfile.username; - // newUser.githubUid = githubProfile.id; - // newUser.lastLogin = getNextMonth(); - - // // const newUser = { - // // nickname: githubProfile.username, - // // githubUsername: githubProfile.username, - // // githubUid: githubProfile.id, - // // lastLogin: getNextMonth(), - // // }; - - // return newUser.save(); - // } - async findOne(options: FindOneOptions): Promise { return this.userRepository.findOne(options); } From 82817e926348a3d673d64851d5aa71a002b2874b Mon Sep 17 00:00:00 2001 From: huni Date: Sun, 28 Aug 2022 00:21:59 +0900 Subject: [PATCH 36/48] =?UTF-8?q?feat:=20mysql=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/docker-compose.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 152797db..8f12e48e 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -61,9 +61,8 @@ services: order: start-first #----------------------------------------------------------------------------------- db: - image: mysql:5.7 + image: mysql:8.0 container_name: 42world-backend-db - platform: linux/x86_64 ports: - '${DB_PORT}:3306' environment: From 14085e10fe34706e4396d7c19f9884e88108d309 Mon Sep 17 00:00:00 2001 From: huni Date: Sun, 28 Aug 2022 00:27:13 +0900 Subject: [PATCH 37/48] =?UTF-8?q?fix:=20test=20=EC=97=90=EB=8F=84=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/run_test_db.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/infra/run_test_db.sh b/infra/run_test_db.sh index 8f62976c..02231ef0 100755 --- a/infra/run_test_db.sh +++ b/infra/run_test_db.sh @@ -2,7 +2,6 @@ if ! $( docker container inspect -f '{{.State.Running}}' ft_world-mysql-test 2> /dev/null ); then docker run -d --rm --name ft_world-mysql-test \ - --platform linux/x86_64 \ -e MYSQL_DATABASE=ft_world \ -e MYSQL_USER=ft_world \ -e MYSQL_PASSWORD=ft_world \ @@ -14,6 +13,6 @@ if ! $( docker container inspect -f '{{.State.Running}}' ft_world-mysql-test 2> --health-start-period=0s \ --health-timeout=1s \ -e TZ=Asia/Seoul \ - -p 3308:3306 mysql:5.7 \ + -p 3308:3306 mysql:8.0 \ mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci fi \ No newline at end of file From 5f8df6ed287d74818179f8e230c7d44c07121327 Mon Sep 17 00:00:00 2001 From: siontama Date: Wed, 7 Sep 2022 02:40:02 +0900 Subject: [PATCH 38/48] Fix: exclude img tag for search --- apps/api/src/article/article.service.ts | 7 +------ .../src/article/repositories/article.repository.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/apps/api/src/article/article.service.ts b/apps/api/src/article/article.service.ts index a5ae002c..2bb56087 100644 --- a/apps/api/src/article/article.service.ts +++ b/apps/api/src/article/article.service.ts @@ -66,12 +66,7 @@ export class ArticleService { categoryIds = [options.categoryId]; } const { articles, totalCount } = await this.articleRepository.search(options, categoryIds); - const filteredArticles = articles.filter( - (article) => - article.content.replace(/![\S*](\S+)/g, '').indexOf(options.q) !== -1 || - article.title.indexOf(options.q) !== -1, - ); - return { articles: filteredArticles, totalCount }; + return { articles, totalCount }; } async findAllByWriterId( diff --git a/apps/api/src/article/repositories/article.repository.ts b/apps/api/src/article/repositories/article.repository.ts index 4ad48b3d..50518428 100644 --- a/apps/api/src/article/repositories/article.repository.ts +++ b/apps/api/src/article/repositories/article.repository.ts @@ -41,18 +41,20 @@ export class ArticleRepository extends Repository
{ }) .andWhere( new Brackets((qb) => { - qb.where('article.title like :q', { q: `%${options.q}%` }).orWhere('article.content like :q', { - q: `%${options.q}%`, - }); + qb.where('article.title like :q', { q: `%${options.q}%` }).orWhere( + 'regexp_replace(`article`.`content`, "!\\[[[:print:]]+\\]\\([[:print:]]+\\)", "") like :q', + { + q: `%${options.q}%`, + }, + ); }), ) .skip(getPaginationSkip(options)) .take(options.take) .orderBy('article.createdAt', options.order); - const totalCount = await query.getCount(); + const totalCount = 0; //await query.getCount(); const articles = await query.getMany(); - return { articles, totalCount }; } From 40aed3ddb0890c3b920bcd1624c056c0135f5e78 Mon Sep 17 00:00:00 2001 From: siontama Date: Wed, 7 Sep 2022 14:41:47 +0900 Subject: [PATCH 39/48] Fix: search totalCount --- apps/api/src/article/repositories/article.repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/article/repositories/article.repository.ts b/apps/api/src/article/repositories/article.repository.ts index 50518428..2711e137 100644 --- a/apps/api/src/article/repositories/article.repository.ts +++ b/apps/api/src/article/repositories/article.repository.ts @@ -53,7 +53,7 @@ export class ArticleRepository extends Repository
{ .take(options.take) .orderBy('article.createdAt', options.order); - const totalCount = 0; //await query.getCount(); + const totalCount = await query.getCount(); const articles = await query.getMany(); return { articles, totalCount }; } From c2a161556ffb56553c74012988258aaa9446bcab Mon Sep 17 00:00:00 2001 From: Yami Date: Thu, 8 Sep 2022 22:44:11 +0900 Subject: [PATCH 40/48] =?UTF-8?q?Test:=20image=20=ED=83=9C=EA=B7=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=9C=EC=99=B8=ED=95=9C=20=EA=B2=80=EC=83=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/test/e2e/article.e2e-spec.ts | 44 +++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/apps/api/test/e2e/article.e2e-spec.ts b/apps/api/test/e2e/article.e2e-spec.ts index 308ccc34..a5b5bed6 100644 --- a/apps/api/test/e2e/article.e2e-spec.ts +++ b/apps/api/test/e2e/article.e2e-spec.ts @@ -1,28 +1,52 @@ import { ArticleModule } from '@api/article/article.module'; import { CreateArticleRequestDto } from '@api/article/dto/request/create-article-request.dto'; + import { FindAllArticleRequestDto } from '@api/article/dto/request/find-all-article-request.dto'; + import { UpdateArticleRequestDto } from '@api/article/dto/request/update-article-request.dto'; + import { CreateArticleResponseDto } from '@api/article/dto/response/create-article-response.dto'; + import { FindOneArticleResponseDto } from '@api/article/dto/response/find-one-article-response.dto'; + import { ArticleRepository } from '@api/article/repositories/article.repository'; + import { AuthModule } from '@api/auth/auth.module'; + import { AuthService } from '@api/auth/auth.service'; + import { CategoryModule } from '@api/category/category.module'; + import { CategoryRepository } from '@api/category/repositories/category.repository'; + import { CommentModule } from '@api/comment/comment.module'; + import { CommentRepository } from '@api/comment/repositories/comment.repository'; + import { UserRepository } from '@api/user/repositories/user.repository'; + import { ANONY_USER_CHARACTER, ANONY_USER_ID, ANONY_USER_NICKNAME } from '@api/user/user.constant'; + import { UserModule } from '@api/user/user.module'; + import { Article } from '@app/entity/article/article.entity'; + import { Comment } from '@app/entity/comment/comment.entity'; + import { HttpStatus, INestApplication } from '@nestjs/common'; + import { Test, TestingModule } from '@nestjs/testing'; + import * as request from 'supertest'; + import { getConnection } from 'typeorm'; + import { E2eTestBaseModule } from './e2e-test.base.module'; + import * as dummy from './utils/dummy'; + import { clearDB, createTestApp } from './utils/utils'; + import { testDto } from './utils/validate-test'; describe('Article', () => { @@ -673,9 +697,10 @@ describe('Article', () => { const searchWord = '42'; const titleWithSearchWord = 'aaa42aaa'; const titleWithoutSearchWord = 'aaaaaa'; + const titleWithImage = 'cccccc'; const contentWithSearchWord = 'bbb42bbb'; - const contentWithoutSearchWord = - 'bbbbbb![image.png](https://42world-image.s3.ap-northeast-2.amazonaws.com/111111111.png)'; + const contentWithoutSearchWord = 'bbbbbb'; + const contentWithImage = '![image.png](https://42world-image.s3.ap-northeast-2.amazonaws.com/111111111.png)'; const SearchArticleRequestDto = { q: searchWord, }; @@ -780,6 +805,21 @@ describe('Article', () => { expect(responseArticles.length).toBe(1); expect(responseArticles[0].content).toBe(contentWithSearchWord); }); + + test('[์„ฑ๊ณต] GET - ์ด๋ฏธ์ง€ ํฌํ•จ', async () => { + await articleRepository.save( + dummy.article(categories.free.id, users.cadet[0].id, titleWithImage, contentWithImage), + ); + + const response = await request(httpServer) + .get('/articles/search') + .query(SearchArticleRequestDto) + .set('Cookie', `${process.env.ACCESS_TOKEN_KEY}=${cadetJWT}`); + + expect(response.status).toEqual(HttpStatus.OK); + const responseArticles = response.body.data as Article[]; + expect(responseArticles.length).toBe(0); + }); }); // ์นดํ…Œ๊ณ ๋ฆฌ ๋ณ„ ๊ฒ€์ƒ‰ From 8b688b8169aa45538856cc1131ff39f230836da5 Mon Sep 17 00:00:00 2001 From: Yami Date: Thu, 8 Sep 2022 22:45:28 +0900 Subject: [PATCH 41/48] Fix: import lint --- apps/api/test/e2e/article.e2e-spec.ts | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/apps/api/test/e2e/article.e2e-spec.ts b/apps/api/test/e2e/article.e2e-spec.ts index a5b5bed6..88f568c2 100644 --- a/apps/api/test/e2e/article.e2e-spec.ts +++ b/apps/api/test/e2e/article.e2e-spec.ts @@ -1,52 +1,28 @@ import { ArticleModule } from '@api/article/article.module'; import { CreateArticleRequestDto } from '@api/article/dto/request/create-article-request.dto'; - import { FindAllArticleRequestDto } from '@api/article/dto/request/find-all-article-request.dto'; - import { UpdateArticleRequestDto } from '@api/article/dto/request/update-article-request.dto'; - import { CreateArticleResponseDto } from '@api/article/dto/response/create-article-response.dto'; - import { FindOneArticleResponseDto } from '@api/article/dto/response/find-one-article-response.dto'; - import { ArticleRepository } from '@api/article/repositories/article.repository'; - import { AuthModule } from '@api/auth/auth.module'; - import { AuthService } from '@api/auth/auth.service'; - import { CategoryModule } from '@api/category/category.module'; - import { CategoryRepository } from '@api/category/repositories/category.repository'; - import { CommentModule } from '@api/comment/comment.module'; - import { CommentRepository } from '@api/comment/repositories/comment.repository'; - import { UserRepository } from '@api/user/repositories/user.repository'; - import { ANONY_USER_CHARACTER, ANONY_USER_ID, ANONY_USER_NICKNAME } from '@api/user/user.constant'; - import { UserModule } from '@api/user/user.module'; - import { Article } from '@app/entity/article/article.entity'; - import { Comment } from '@app/entity/comment/comment.entity'; - import { HttpStatus, INestApplication } from '@nestjs/common'; - import { Test, TestingModule } from '@nestjs/testing'; - import * as request from 'supertest'; - import { getConnection } from 'typeorm'; - import { E2eTestBaseModule } from './e2e-test.base.module'; - import * as dummy from './utils/dummy'; - import { clearDB, createTestApp } from './utils/utils'; - import { testDto } from './utils/validate-test'; describe('Article', () => { From aba16520088ad4729beefe01476403e31ebef30f Mon Sep 17 00:00:00 2001 From: huni Date: Sat, 17 Sep 2022 00:20:11 +0900 Subject: [PATCH 42/48] refactor: it -> test --- .../src/auth/__test__/auth.controller.test.ts | 6 +- .../src/auth/__test__/auth.decorator.test.ts | 22 +++---- .../api/src/auth/__test__/auth.module.test.ts | 2 +- .../src/auth/__test__/auth.service.test.ts | 59 +++++++++++++++++-- .../__test__/github-auth.guard.test.ts | 4 +- .../__test__/github-auth.module.test.ts | 2 +- .../__test__/github-auth.strategy.test.ts | 2 +- .../jwt-auth/__test__/jwt-auth.guard.test.ts | 20 +++---- .../jwt-auth/__test__/jwt-auth.module.test.ts | 2 +- .../__test__/jwt-auth.strategy.test.ts | 8 +-- .../api/src/auth/types/__test__/index.test.ts | 4 +- 11 files changed, 89 insertions(+), 42 deletions(-) diff --git a/apps/api/src/auth/__test__/auth.controller.test.ts b/apps/api/src/auth/__test__/auth.controller.test.ts index 2b887377..5a198cae 100644 --- a/apps/api/src/auth/__test__/auth.controller.test.ts +++ b/apps/api/src/auth/__test__/auth.controller.test.ts @@ -21,7 +21,7 @@ describe('AuthController', () => { }); describe('githubLogin', () => { - it('์ •์ƒ ํ˜ธ์ถœ', async () => { + test('์ •์ƒ ํ˜ธ์ถœ', async () => { const actual = () => authController.githubLogin(); expect(actual).not.toThrow(); @@ -29,7 +29,7 @@ describe('AuthController', () => { }); describe('githubCallback', () => { - it('๋กœ๊ทธ์ธํ•˜๋ฉด ์ฟ ํ‚ค๋ฅผ ์„ธํŒ…ํ•œ๋‹ค', async () => { + test('๋กœ๊ทธ์ธํ•˜๋ฉด ์ฟ ํ‚ค๋ฅผ ์„ธํŒ…ํ•œ๋‹ค', async () => { const githubProfile: GithubProfile = { id: '1', username: 'test' }; const mockResponse = mock({ cookie: mockFn().mockReturnThis(), @@ -43,7 +43,7 @@ describe('AuthController', () => { }); describe('signout', () => { - it('๋กœ๊ทธ์•„์›ƒํ•˜๋ฉด ์ฟ ํ‚ค๋ฅผ ๋น„์šด๋‹ค', async () => { + test('๋กœ๊ทธ์•„์›ƒํ•˜๋ฉด ์ฟ ํ‚ค๋ฅผ ๋น„์šด๋‹ค', async () => { const mockResponse = mock({ clearCookie: mockFn().mockReturnThis(), }); diff --git a/apps/api/src/auth/__test__/auth.decorator.test.ts b/apps/api/src/auth/__test__/auth.decorator.test.ts index 6d106406..4b65a76d 100644 --- a/apps/api/src/auth/__test__/auth.decorator.test.ts +++ b/apps/api/src/auth/__test__/auth.decorator.test.ts @@ -8,7 +8,7 @@ import { getParamDecorator } from './getParamDecoratorFactory'; describe('AuthDecorator', () => { describe('Auth', () => { - it('Auth๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉด ๊ถŒํ•œ์ด ์„ค์ •์ด ๋˜์ง€ ์•Š๋Š”๋‹ค.', async () => { + test('Auth๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉด ๊ถŒํ•œ์ด ์„ค์ •์ด ๋˜์ง€ ์•Š๋Š”๋‹ค.', async () => { class TestClass { testMethod() {} } @@ -18,7 +18,7 @@ describe('AuthDecorator', () => { expect(result).toBeUndefined(); }); - it(`Auth('allow', UserRole.GUEST, UserRole.ADMIN) ๋Š” GUEST, ADMIN ๋‘˜๋‹ค ํ—ˆ๋ฝํ•œ๋‹ค`, async () => { + test(`Auth('allow', UserRole.GUEST, UserRole.ADMIN) ๋Š” GUEST, ADMIN ๋‘˜๋‹ค ํ—ˆ๋ฝํ•œ๋‹ค`, async () => { class TestClass { @Auth('allow', UserRole.GUEST, UserRole.ADMIN) testMethod() {} @@ -29,7 +29,7 @@ describe('AuthDecorator', () => { expect(result).toStrictEqual(['allow', UserRole.GUEST, UserRole.ADMIN]); }); - it(`Auth('allow') ๋Š” ์•„๋ฌด๊ถŒํ•œ๋„ ํ—ˆ๋ฝํ•˜์ง€ ์•Š๋Š”๋‹ค.`, async () => { + test(`Auth('allow') ๋Š” ์•„๋ฌด๊ถŒํ•œ๋„ ํ—ˆ๋ฝํ•˜์ง€ ์•Š๋Š”๋‹ค.`, async () => { class TestClass { @Auth('allow') testMethod() {} @@ -40,7 +40,7 @@ describe('AuthDecorator', () => { expect(result).toStrictEqual(['allow']); }); - it(`Auth('deny', UserRole.GUEST, UserRole.ADMIN) ๋Š” GUEST, ADMIN ๋‘˜๋‹ค ํ—ˆ๋ฝํ•œ๋‹ค`, async () => { + test(`Auth('deny', UserRole.GUEST, UserRole.ADMIN) ๋Š” GUEST, ADMIN ๋‘˜๋‹ค ํ—ˆ๋ฝํ•œ๋‹ค`, async () => { class TestClass { @Auth('deny', UserRole.GUEST, UserRole.ADMIN) testMethod() {} @@ -51,7 +51,7 @@ describe('AuthDecorator', () => { expect(result).toStrictEqual(['deny', UserRole.GUEST, UserRole.ADMIN]); }); - it(`Auth('deny') ๋Š” ๋ชจ๋“  ๊ถŒํ•œ์„ ํ—ˆ๋ฝํ•œ๋‹ค.`, async () => { + test(`Auth('deny') ๋Š” ๋ชจ๋“  ๊ถŒํ•œ์„ ํ—ˆ๋ฝํ•œ๋‹ค.`, async () => { class TestClass { @Auth('deny') testMethod() {} @@ -62,7 +62,7 @@ describe('AuthDecorator', () => { expect(result).toStrictEqual(['deny']); }); - it(`Auth('public') ์€ ๋ชจ๋“  ๊ถŒํ•œ์„ ํ—ˆ๋ฝํ•œ๋‹ค.`, async () => { + test(`Auth('public') ์€ ๋ชจ๋“  ๊ถŒํ•œ์„ ํ—ˆ๋ฝํ•œ๋‹ค.`, async () => { class TestClass { @Auth('public') testMethod() {} @@ -73,7 +73,7 @@ describe('AuthDecorator', () => { expect(result).toStrictEqual(['deny']); }); - it(`Auth() ์€ GUEST๋ฅผ ์ œ์™ธํ•œ ๊ถŒํ•œ์„ ํ—ˆ๋ฝํ•œ๋‹ค.`, async () => { + test(`Auth() ์€ GUEST๋ฅผ ์ œ์™ธํ•œ ๊ถŒํ•œ์„ ํ—ˆ๋ฝํ•œ๋‹ค.`, async () => { class TestClass { @Auth() testMethod() {} @@ -86,7 +86,7 @@ describe('AuthDecorator', () => { }); describe('AuthUser', () => { - it('Request ์— ๋‹ด๊ธด user๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค', async () => { + test('Request ์— ๋‹ด๊ธด user๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค', async () => { const user = new User(); const context = new ExecutionContextHost([{ user }, {}]); @@ -95,7 +95,7 @@ describe('AuthDecorator', () => { expect(result).toStrictEqual(user); }); - it('Request ์— ๋‹ด๊ธด user์˜ id๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค', async () => { + test('Request ์— ๋‹ด๊ธด user์˜ id๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค', async () => { const user = new User(); user.id = 1; const context = new ExecutionContextHost([{ user }, {}]); @@ -105,7 +105,7 @@ describe('AuthDecorator', () => { expect(result).toStrictEqual(user.id); }); - it('Request ์— ๋‹ด๊ธด user์˜ id๊ฐ€ ์—†๋Š”๊ฒฝ์šฐ', async () => { + test('Request ์— ๋‹ด๊ธด user์˜ id๊ฐ€ ์—†๋Š”๊ฒฝ์šฐ', async () => { const user = new User(); const context = new ExecutionContextHost([{ user }, {}]); @@ -116,7 +116,7 @@ describe('AuthDecorator', () => { }); describe('ReqGithubProfile', () => { - it('Request ์— ๋‹ด๊ธด githubprofile๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค', async () => { + test('Request ์— ๋‹ด๊ธด githubprofile๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค', async () => { const user = { id: 1, username: 'test' }; const context = new ExecutionContextHost([{ user }, {}]); diff --git a/apps/api/src/auth/__test__/auth.module.test.ts b/apps/api/src/auth/__test__/auth.module.test.ts index 12c9b220..a8d933ab 100644 --- a/apps/api/src/auth/__test__/auth.module.test.ts +++ b/apps/api/src/auth/__test__/auth.module.test.ts @@ -1,7 +1,7 @@ import { AuthModule } from '../auth.module'; describe('AuthModule', () => { - it('๋ชจ๋“ˆ์ด ์ž˜ ์ปดํŒŒ์ผ๋œ๋‹ค.', async () => { + test('๋ชจ๋“ˆ์ด ์ž˜ ์ปดํŒŒ์ผ๋œ๋‹ค.', async () => { // TODO: Test.createTestingModule ๋กœ complie ํ• ๊ฒƒ // const module = await Test.createTestingModule({ // imports: [AuthModule], diff --git a/apps/api/src/auth/__test__/auth.service.test.ts b/apps/api/src/auth/__test__/auth.service.test.ts index d2a87024..faeb5285 100644 --- a/apps/api/src/auth/__test__/auth.service.test.ts +++ b/apps/api/src/auth/__test__/auth.service.test.ts @@ -7,6 +7,9 @@ import { mock, mockFn } from 'jest-mock-extended'; import { AuthService } from '../auth.service'; import { GithubProfile } from '../types'; +// ํด๋ž˜์Šค ๋ชจํ‚น +// ํ•จ์ˆ˜ ๋ชจํ‚น + describe('AuthService', () => { const mockUserSerivce = mock(); const mockJwtService = mock({ @@ -22,7 +25,7 @@ describe('AuthService', () => { }); describe('login', () => { - it('์ด๋ฏธ ๊ฐ€์ž…ํ•œ ์œ ์ €๋Š” ๋กœ๊ทธ์ธ ์‹œ๊ฐ„์„ ์—…๋ฐ์ดํŠธ ํ•œ๋‹ค', async () => { + test('์ด๋ฏธ ๊ฐ€์ž…ํ•œ ์œ ์ €๋Š” ๋กœ๊ทธ์ธ ์‹œ๊ฐ„์„ ์—…๋ฐ์ดํŠธ ํ•œ๋‹ค', async () => { const user = new User(); const oldLastLogin = new Date(); user.lastLogin = oldLastLogin; @@ -37,7 +40,7 @@ describe('AuthService', () => { expect(result.lastLogin).not.toBe(oldLastLogin); }); - it('์ฒ˜์Œ ๋กœ๊ทธ์ธํ•˜๋Š” ์œ ์ €๋Š” ์œ ์ €๋ฅผ ์ƒ์„ฑํ•œ๋‹ค', async () => { + test('์ฒ˜์Œ ๋กœ๊ทธ์ธํ•˜๋Š” ์œ ์ €๋Š” ์œ ์ €๋ฅผ ์ƒ์„ฑํ•œ๋‹ค', async () => { const githubProfile: GithubProfile = { id: '1', username: 'test' }; mockUserSerivce.findOne.mockResolvedValue(undefined); mockUserSerivce.create.mockImplementation(async (user: User) => user); @@ -53,7 +56,7 @@ describe('AuthService', () => { }); describe('getJwt', () => { - it('jwt๋ฅผ ๋งŒ๋“ ๋‹ค', async () => { + test('jwt๋ฅผ ๋งŒ๋“ ๋‹ค', async () => { const user = new User(); user.id = 1; user.role = UserRole.ADMIN; @@ -67,7 +70,7 @@ describe('AuthService', () => { }); describe('getCookieOption', () => { - it('prod ์ผ๋–„ ์ฟ ํ‚ค ์˜ต์…˜', async () => { + test('prod ์ผ๋–„ ์ฟ ํ‚ค ์˜ต์…˜', async () => { mockConfigService.get.mockReturnValue('prod'); const result = authService.getCookieOption(); @@ -75,7 +78,7 @@ describe('AuthService', () => { expect(result).toStrictEqual({ httpOnly: true, secure: true, sameSite: 'lax' }); }); - it('alpha ์ผ๋–„ ์ฟ ํ‚ค ์˜ต์…˜', async () => { + test('alpha ์ผ๋–„ ์ฟ ํ‚ค ์˜ต์…˜', async () => { mockConfigService.get.mockReturnValue('alpha'); const result = authService.getCookieOption(); @@ -83,7 +86,7 @@ describe('AuthService', () => { expect(result).toStrictEqual({ httpOnly: true, secure: true, sameSite: 'none' }); }); - it('dev/test ์ผ๋•Œ ์ฟ ํ‚ค ์˜ต์…˜', async () => { + test('dev/test ์ผ๋•Œ ์ฟ ํ‚ค ์˜ต์…˜', async () => { mockConfigService.get.mockReturnValue('dev'); const result = authService.getCookieOption(); @@ -92,3 +95,47 @@ describe('AuthService', () => { }); }); }); + +class TestClass { + testMethod(a: string) { + console.log('testMethodCalled', a); + return 'test'; + } +} + +class UserClass { + constructor(private readonly testClass: TestClass) {} + + testMethod() { + return this.testClass.testMethod('test'); + } +} + +// UserClass์˜ testMethod๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด TestClass์˜ testMethod๊ฐ€ ํ˜ธ์ถœ๋˜๋Š”์ง€ ํ™•์ธ + return ์ด test ์ธ์ง€?? + +describe('UserClass', () => { + const mockTestClass = mock({ + testMethod: mockFn().mockReturnValue('test'), + }); + const userClass = new UserClass(mockTestClass); + + describe('testMethod', () => { + beforeEach(() => { + mockTestClass.testMethod.mockClear(); + }); + + test('์ •์ƒ', () => { + const result = userClass.testMethod(); + + expect(result).toBe('test'); + expect(mockTestClass.testMethod).toBeCalledTimes(1); + }); + + test('์ •์ƒ2', () => { + const result = userClass.testMethod(); + + expect(result).toBe('test'); + expect(mockTestClass.testMethod).toBeCalledTimes(1); + }); + }); +}); diff --git a/apps/api/src/auth/github-auth/__test__/github-auth.guard.test.ts b/apps/api/src/auth/github-auth/__test__/github-auth.guard.test.ts index 84c02f5d..551a8051 100644 --- a/apps/api/src/auth/github-auth/__test__/github-auth.guard.test.ts +++ b/apps/api/src/auth/github-auth/__test__/github-auth.guard.test.ts @@ -17,7 +17,7 @@ describe('GithubAuthGuard', () => { }); describe('handleRequest', () => { - it('์ •์ƒ์ ์ธ ์š”์ฒญ์€ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.', async () => { + test('์ •์ƒ์ ์ธ ์š”์ฒญ์€ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.', async () => { const githubProfile = { id: 1 }; mockSuperHandleRequest.mockReturnValue(githubProfile); @@ -27,7 +27,7 @@ describe('GithubAuthGuard', () => { expect(mockSuperHandleRequest).toBeCalledTimes(1); }); - it('์ •์ƒ์ ์ธ ์š”์ฒญ์ด ์•„๋‹ˆ๋ฉด BadRequestException ์—๋Ÿฌ๋ฅผ ๋˜์ง„๋‹ค.', async () => { + test('์ •์ƒ์ ์ธ ์š”์ฒญ์ด ์•„๋‹ˆ๋ฉด BadRequestException ์—๋Ÿฌ๋ฅผ ๋˜์ง„๋‹ค.', async () => { mockSuperHandleRequest.mockImplementation(() => { throw new Error('error'); }); diff --git a/apps/api/src/auth/github-auth/__test__/github-auth.module.test.ts b/apps/api/src/auth/github-auth/__test__/github-auth.module.test.ts index 7697799f..49ac4db0 100644 --- a/apps/api/src/auth/github-auth/__test__/github-auth.module.test.ts +++ b/apps/api/src/auth/github-auth/__test__/github-auth.module.test.ts @@ -3,7 +3,7 @@ import { Test } from '@nestjs/testing'; import { GithubAuthModule } from '../github-auth.module'; describe('GithubAuthModule', () => { - it('๋ชจ๋“ˆ์ด ์ž˜ ์ปดํŒŒ์ผ๋œ๋‹ค.', async () => { + test('๋ชจ๋“ˆ์ด ์ž˜ ์ปดํŒŒ์ผ๋œ๋‹ค.', async () => { const module = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ diff --git a/apps/api/src/auth/github-auth/__test__/github-auth.strategy.test.ts b/apps/api/src/auth/github-auth/__test__/github-auth.strategy.test.ts index 6a52a257..c473e2b9 100644 --- a/apps/api/src/auth/github-auth/__test__/github-auth.strategy.test.ts +++ b/apps/api/src/auth/github-auth/__test__/github-auth.strategy.test.ts @@ -10,7 +10,7 @@ describe('GithubAuthStrategy', () => { const githubAuthStrategy = new GithubAuthStrategy(mockConfigService); describe('validate', () => { - it('GithubProfile์ด ๋ฐ˜ํ™˜๋œ๋‹ค', async () => { + test('GithubProfile์ด ๋ฐ˜ํ™˜๋œ๋‹ค', async () => { const accessToken = ''; const refreshToken = ''; const profile = { diff --git a/apps/api/src/auth/jwt-auth/__test__/jwt-auth.guard.test.ts b/apps/api/src/auth/jwt-auth/__test__/jwt-auth.guard.test.ts index a3a5e8c7..ebde23af 100644 --- a/apps/api/src/auth/jwt-auth/__test__/jwt-auth.guard.test.ts +++ b/apps/api/src/auth/jwt-auth/__test__/jwt-auth.guard.test.ts @@ -28,7 +28,7 @@ describe('JwtAuthGuard', () => { }); describe('canActivate', () => { - it('REQUIRE_ROLES ๊ฐ€ ์—†์œผ๋ฉด ๋ฌด์กฐ๊ฑด true ์ด๋‹ค. ', async () => { + test('REQUIRE_ROLES ๊ฐ€ ์—†์œผ๋ฉด ๋ฌด์กฐ๊ฑด true ์ด๋‹ค. ', async () => { mockRelfector.get.mockReturnValue(undefined); const result = jwtAuthGuard.canActivate(context); @@ -37,7 +37,7 @@ describe('JwtAuthGuard', () => { expect(mockSuperCanActivate).toBeCalledTimes(0); }); - it('REQUIRE_ROLES ๊ฐ€ ์žˆ์œผ๋ฉด super๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.', async () => { + test('REQUIRE_ROLES ๊ฐ€ ์žˆ์œผ๋ฉด super๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.', async () => { mockRelfector.get.mockReturnValue(['allow'] as AuthDecoratorParam); mockSuperCanActivate.mockReturnValue(true); @@ -49,7 +49,7 @@ describe('JwtAuthGuard', () => { }); describe('handleRequest', () => { - it('allow ํ•˜๋Š” ๊ถŒํ•œ์ด ์•„์˜ˆ ์—†์œผ๋ฉด ์•„๋ฌด ๊ถŒํ•œ๋„ ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•œ๋‹ค.', async () => { + test('allow ํ•˜๋Š” ๊ถŒํ•œ์ด ์•„์˜ˆ ์—†์œผ๋ฉด ์•„๋ฌด ๊ถŒํ•œ๋„ ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•œ๋‹ค.', async () => { mockRelfector.get.mockReturnValue(['allow'] as AuthDecoratorParam); const user = new User(); @@ -63,7 +63,7 @@ describe('JwtAuthGuard', () => { expect(act).toThrowErrorMatchingInlineSnapshot(`"์ ‘๊ทผ ๊ถŒํ•œ ์—†์Œ"`); }); - it('allow ํ•˜๋Š” ๊ถŒํ•œ์— ํฌํ•จ๋˜์–ด์žˆ์œผ๋ฉด ํ†ต๊ณผํ•œ๋‹ค.', async () => { + test('allow ํ•˜๋Š” ๊ถŒํ•œ์— ํฌํ•จ๋˜์–ด์žˆ์œผ๋ฉด ํ†ต๊ณผํ•œ๋‹ค.', async () => { mockRelfector.get.mockReturnValue(['allow', UserRole.ADMIN] as AuthDecoratorParam); const user = new User(); @@ -76,7 +76,7 @@ describe('JwtAuthGuard', () => { expect(mockSuperHandleRequest).toBeCalledTimes(1); }); - it('allow ํ•˜๋Š” ๊ถŒํ•œ์— ํฌํ•จ๋˜์–ด์žˆ์ง€ ์•Š์œผ๋ฉด ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•œ๋‹ค.', async () => { + test('allow ํ•˜๋Š” ๊ถŒํ•œ์— ํฌํ•จ๋˜์–ด์žˆ์ง€ ์•Š์œผ๋ฉด ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•œ๋‹ค.', async () => { mockRelfector.get.mockReturnValue(['allow', UserRole.ADMIN] as AuthDecoratorParam); const user = new User(); @@ -90,7 +90,7 @@ describe('JwtAuthGuard', () => { expect(act).toThrowErrorMatchingInlineSnapshot(`"์ ‘๊ทผ ๊ถŒํ•œ ์—†์Œ"`); }); - it('deny ํ•˜๋Š” ๊ถŒํ•œ์ด ์•„์˜ˆ ์—†์œผ๋ฉด ๋ชจ๋“  ๊ถŒํ•œ์ด ํ†ต๊ณผํ•œ๋‹ค.', async () => { + test('deny ํ•˜๋Š” ๊ถŒํ•œ์ด ์•„์˜ˆ ์—†์œผ๋ฉด ๋ชจ๋“  ๊ถŒํ•œ์ด ํ†ต๊ณผํ•œ๋‹ค.', async () => { mockRelfector.get.mockReturnValue(['deny'] as AuthDecoratorParam); const user = new User(); @@ -103,7 +103,7 @@ describe('JwtAuthGuard', () => { expect(mockSuperHandleRequest).toBeCalledTimes(1); }); - it('deny ํ•˜๋Š” ๊ถŒํ•œ์— ํฌํ•จ๋˜์–ด์žˆ์œผ๋ฉด ํ†ต๊ณผํ•˜์ง€ ์•Š๋Š”๋‹ค.', async () => { + test('deny ํ•˜๋Š” ๊ถŒํ•œ์— ํฌํ•จ๋˜์–ด์žˆ์œผ๋ฉด ํ†ต๊ณผํ•˜์ง€ ์•Š๋Š”๋‹ค.', async () => { mockRelfector.get.mockReturnValue(['deny', UserRole.ADMIN] as AuthDecoratorParam); const user = new User(); @@ -117,7 +117,7 @@ describe('JwtAuthGuard', () => { expect(act).toThrowErrorMatchingInlineSnapshot(`"์ ‘๊ทผ ๊ถŒํ•œ ์—†์Œ"`); }); - it('deny ํ•˜๋Š” ๊ถŒํ•œ์— ํฌํ•จ๋˜์–ด์žˆ์ง€ ์•Š์œผ๋ฉด ํ†ต๊ณผํ•œ๋‹ค.', async () => { + test('deny ํ•˜๋Š” ๊ถŒํ•œ์— ํฌํ•จ๋˜์–ด์žˆ์ง€ ์•Š์œผ๋ฉด ํ†ต๊ณผํ•œ๋‹ค.', async () => { mockRelfector.get.mockReturnValue(['deny', UserRole.GUEST] as AuthDecoratorParam); const user = new User(); @@ -130,7 +130,7 @@ describe('JwtAuthGuard', () => { expect(mockSuperHandleRequest).toBeCalledTimes(1); }); - it('jwt ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์„๋•Œ, GUEST ๊ถŒํ•œ์ด ํ—ˆ๋ฝ๋˜๋ฉด GUEST๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.', async () => { + test('jwt ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์„๋•Œ, GUEST ๊ถŒํ•œ์ด ํ—ˆ๋ฝ๋˜๋ฉด GUEST๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.', async () => { mockRelfector.get.mockReturnValue(['allow', UserRole.GUEST] as AuthDecoratorParam); // jwt ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์„๋•Œ ์—๋Ÿฌ๋ฅผ ๋˜์ง. @@ -145,7 +145,7 @@ describe('JwtAuthGuard', () => { expect(mockSuperHandleRequest).toBeCalledTimes(1); }); - it('jwt ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์„๋•Œ, GUEST ๊ถŒํ•œ๋„ ํ—ˆ๋ฝ๋˜์ง€ ์•Š์œผ๋ฉด ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•œ๋‹ค.', async () => { + test('jwt ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์„๋•Œ, GUEST ๊ถŒํ•œ๋„ ํ—ˆ๋ฝ๋˜์ง€ ์•Š์œผ๋ฉด ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•œ๋‹ค.', async () => { mockRelfector.get.mockReturnValue(['deny', UserRole.GUEST] as AuthDecoratorParam); // jwt ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š์„๋•Œ ์—๋Ÿฌ๋ฅผ ๋˜์ง. diff --git a/apps/api/src/auth/jwt-auth/__test__/jwt-auth.module.test.ts b/apps/api/src/auth/jwt-auth/__test__/jwt-auth.module.test.ts index ebb86078..8fd70e75 100644 --- a/apps/api/src/auth/jwt-auth/__test__/jwt-auth.module.test.ts +++ b/apps/api/src/auth/jwt-auth/__test__/jwt-auth.module.test.ts @@ -1,7 +1,7 @@ import { JwtAuthModule } from '../jwt-auth.module'; describe('JwtAuthGuard', () => { - it('๋ชจ๋“ˆ์ด ์ž˜ ์ปดํŒŒ์ผ๋œ๋‹ค.', async () => { + test('๋ชจ๋“ˆ์ด ์ž˜ ์ปดํŒŒ์ผ๋œ๋‹ค.', async () => { // TODO: Test.createTestingModule ๋กœ complie ํ• ๊ฒƒ // const module = await Test.createTestingModule({ // imports: [JwtAuthModule], diff --git a/apps/api/src/auth/jwt-auth/__test__/jwt-auth.strategy.test.ts b/apps/api/src/auth/jwt-auth/__test__/jwt-auth.strategy.test.ts index d8ab92b3..59328e68 100644 --- a/apps/api/src/auth/jwt-auth/__test__/jwt-auth.strategy.test.ts +++ b/apps/api/src/auth/jwt-auth/__test__/jwt-auth.strategy.test.ts @@ -24,7 +24,7 @@ describe('JwtAuthStrategy', () => { }); describe('validate', () => { - it('์œ ์ €๊ฐ€ ์กด์žฌํ•˜๋ฉด ์œ ์ €๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค', async () => { + test('์œ ์ €๊ฐ€ ์กด์žฌํ•˜๋ฉด ์œ ์ €๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค', async () => { const user = new User(); mockUserService.findOneByIdOrFail.mockResolvedValue(user); @@ -34,7 +34,7 @@ describe('JwtAuthStrategy', () => { expect(mockUserService.findOneByIdOrFail).toBeCalledTimes(1); }); - it('์œ ์ €๊ฐ€ ์—†์œผ๋ฉด UnauthorizedException ์—๋Ÿฌ๋ฅผ ๋˜์ง„๋‹ค', async () => { + test('์œ ์ €๊ฐ€ ์—†์œผ๋ฉด UnauthorizedException ์—๋Ÿฌ๋ฅผ ๋˜์ง„๋‹ค', async () => { mockUserService.findOneByIdOrFail.mockRejectedValue(new NotFoundException()); const act = async () => await jwtAuthStrategy.validate(payload); @@ -43,7 +43,7 @@ describe('JwtAuthStrategy', () => { expect(mockUserService.findOneByIdOrFail).toBeCalledTimes(1); }); - it('์—๋Ÿฌ๊ฐ€ ๋‚˜๋ฉด ์—๋Ÿฌ๋ฅผ ๋˜์ง„๋‹ค', async () => { + test('์—๋Ÿฌ๊ฐ€ ๋‚˜๋ฉด ์—๋Ÿฌ๋ฅผ ๋˜์ง„๋‹ค', async () => { mockUserService.findOneByIdOrFail.mockRejectedValue(new Error()); const act = async () => await jwtAuthStrategy.validate(payload); @@ -54,7 +54,7 @@ describe('JwtAuthStrategy', () => { }); describe('getAccessToken', () => { - it('์ฟ ํ‚ค์—์„œ ACCESS_TOKEN_KEY๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => { + test('์ฟ ํ‚ค์—์„œ ACCESS_TOKEN_KEY๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค', () => { const request = { cookies: { ACCESS_TOKEN_KEY: 'test', diff --git a/apps/api/src/auth/types/__test__/index.test.ts b/apps/api/src/auth/types/__test__/index.test.ts index 28e7903d..1452e130 100644 --- a/apps/api/src/auth/types/__test__/index.test.ts +++ b/apps/api/src/auth/types/__test__/index.test.ts @@ -5,11 +5,11 @@ import * as Types from '../index'; * @see https://stackoverflow.com/questions/67388165/how-to-get-jest-to-have-coverage-for-export-only-lines */ describe('Types', () => { - it('should have exports', () => { + test('should have exports', () => { expect(typeof Types).toBe('object'); }); - it('should not have undefined exports', () => { + test('should not have undefined exports', () => { Object.keys(Types).forEach((exportKey) => expect(Boolean(Types[exportKey])).toBe(true)); }); }); From 644d309530e5c95a6fded054d9668e179596ae53 Mon Sep 17 00:00:00 2001 From: huni Date: Sat, 17 Sep 2022 00:51:23 +0900 Subject: [PATCH 43/48] =?UTF-8?q?fix:=20cookie=20option=20=EB=B3=B5?= =?UTF-8?q?=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/auth/auth.service.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index cbe204ef..817587dc 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -39,12 +39,16 @@ export class AuthService { return this.jwtService.sign(payload); } - getCookieOption(): CookieOptions { + getCookieOption = (): CookieOptions => { + const oneHour = 60 * 60 * 1000; + const maxAge = 7 * 24 * oneHour; // 7days + if (this.configService.get('NODE_ENV') === 'prod') { - return { httpOnly: true, secure: true, sameSite: 'lax' }; + return { httpOnly: true, secure: true, sameSite: 'lax', maxAge }; } else if (this.configService.get('NODE_ENV') === 'alpha') { - return { httpOnly: true, secure: true, sameSite: 'none' }; + return { httpOnly: true, secure: true, sameSite: 'none', maxAge }; } - return {}; - } + + return { httpOnly: true, maxAge }; + }; } From d18674393d13db6592bcd4eaf1f7ddc3520dc831 Mon Sep 17 00:00:00 2001 From: huni Date: Sat, 17 Sep 2022 00:51:45 +0900 Subject: [PATCH 44/48] =?UTF-8?q?test:=20cookie=20option=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/auth/__test__/auth.service.test.ts | 72 ++++++------------- 1 file changed, 22 insertions(+), 50 deletions(-) diff --git a/apps/api/src/auth/__test__/auth.service.test.ts b/apps/api/src/auth/__test__/auth.service.test.ts index faeb5285..b135cff3 100644 --- a/apps/api/src/auth/__test__/auth.service.test.ts +++ b/apps/api/src/auth/__test__/auth.service.test.ts @@ -7,9 +7,6 @@ import { mock, mockFn } from 'jest-mock-extended'; import { AuthService } from '../auth.service'; import { GithubProfile } from '../types'; -// ํด๋ž˜์Šค ๋ชจํ‚น -// ํ•จ์ˆ˜ ๋ชจํ‚น - describe('AuthService', () => { const mockUserSerivce = mock(); const mockJwtService = mock({ @@ -75,7 +72,14 @@ describe('AuthService', () => { const result = authService.getCookieOption(); - expect(result).toStrictEqual({ httpOnly: true, secure: true, sameSite: 'lax' }); + expect(result).toMatchInlineSnapshot(` + Object { + "httpOnly": true, + "maxAge": 604800000, + "sameSite": "lax", + "secure": true, + } + `); }); test('alpha ์ผ๋–„ ์ฟ ํ‚ค ์˜ต์…˜', async () => { @@ -83,7 +87,14 @@ describe('AuthService', () => { const result = authService.getCookieOption(); - expect(result).toStrictEqual({ httpOnly: true, secure: true, sameSite: 'none' }); + expect(result).toMatchInlineSnapshot(` + Object { + "httpOnly": true, + "maxAge": 604800000, + "sameSite": "none", + "secure": true, + } + `); }); test('dev/test ์ผ๋•Œ ์ฟ ํ‚ค ์˜ต์…˜', async () => { @@ -91,51 +102,12 @@ describe('AuthService', () => { const result = authService.getCookieOption(); - expect(result).toStrictEqual({}); - }); - }); -}); - -class TestClass { - testMethod(a: string) { - console.log('testMethodCalled', a); - return 'test'; - } -} - -class UserClass { - constructor(private readonly testClass: TestClass) {} - - testMethod() { - return this.testClass.testMethod('test'); - } -} - -// UserClass์˜ testMethod๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด TestClass์˜ testMethod๊ฐ€ ํ˜ธ์ถœ๋˜๋Š”์ง€ ํ™•์ธ + return ์ด test ์ธ์ง€?? - -describe('UserClass', () => { - const mockTestClass = mock({ - testMethod: mockFn().mockReturnValue('test'), - }); - const userClass = new UserClass(mockTestClass); - - describe('testMethod', () => { - beforeEach(() => { - mockTestClass.testMethod.mockClear(); - }); - - test('์ •์ƒ', () => { - const result = userClass.testMethod(); - - expect(result).toBe('test'); - expect(mockTestClass.testMethod).toBeCalledTimes(1); - }); - - test('์ •์ƒ2', () => { - const result = userClass.testMethod(); - - expect(result).toBe('test'); - expect(mockTestClass.testMethod).toBeCalledTimes(1); + expect(result).toMatchInlineSnapshot(` + Object { + "httpOnly": true, + "maxAge": 604800000, + } + `); }); }); }); From 86a4e64a99b40f984b772974e04c0c0f157531f5 Mon Sep 17 00:00:00 2001 From: huni Date: Sat, 17 Sep 2022 01:04:37 +0900 Subject: [PATCH 45/48] =?UTF-8?q?test:=20index=20test=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/auth/types/__test__/index.test.ts | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 apps/api/src/auth/types/__test__/index.test.ts diff --git a/apps/api/src/auth/types/__test__/index.test.ts b/apps/api/src/auth/types/__test__/index.test.ts deleted file mode 100644 index 1452e130..00000000 --- a/apps/api/src/auth/types/__test__/index.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as Types from '../index'; - -/** - * @description test coverage ๋•Œ๋ฌธ์— ๋„ฃ์Œ - * @see https://stackoverflow.com/questions/67388165/how-to-get-jest-to-have-coverage-for-export-only-lines - */ -describe('Types', () => { - test('should have exports', () => { - expect(typeof Types).toBe('object'); - }); - - test('should not have undefined exports', () => { - Object.keys(Types).forEach((exportKey) => expect(Boolean(Types[exportKey])).toBe(true)); - }); -}); From bc521d8e6bde23bd93760882e34e665b7fc61242 Mon Sep 17 00:00:00 2001 From: huni Date: Sat, 17 Sep 2022 01:04:51 +0900 Subject: [PATCH 46/48] =?UTF-8?q?test:=20coverage=20=EC=97=90=EC=84=9C=20i?= =?UTF-8?q?ndex=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jest.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.json b/jest.config.json index f12a9a36..69d7ccb6 100644 --- a/jest.config.json +++ b/jest.config.json @@ -3,7 +3,7 @@ "rootDir": ".", "testMatch": ["**/?(*.)+(spec|test).+(ts)"], "transform": { "^.+\\.ts$": "ts-jest" }, - "collectCoverageFrom": ["**/*.ts", "!**/__test__/**/*.ts"], + "collectCoverageFrom": ["**/*.ts", "!**/__test__/**/*.ts", "!**/index.ts"], "coverageDirectory": "./coverage", "testEnvironment": "node", "moduleNameMapper": { From d7ae8758a488a8be7d6d69e235eff5a1951d9d63 Mon Sep 17 00:00:00 2001 From: Sion Kang <31057849+Yaminyam@users.noreply.github.com> Date: Sat, 17 Sep 2022 02:22:29 +0900 Subject: [PATCH 47/48] =?UTF-8?q?Fix:=20user=20character=20max=2011?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ApiProperty ๊ฐ’๋งŒ ์ˆ˜์ •์ด ๋˜๊ณ  ๋ˆ„๋ฝ๋œ anotation ๊ฐ’๋„ ์ˆ˜์ • --- apps/api/src/user/dto/base-user.dto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/user/dto/base-user.dto.ts b/apps/api/src/user/dto/base-user.dto.ts index 2179471a..b7fe83c4 100644 --- a/apps/api/src/user/dto/base-user.dto.ts +++ b/apps/api/src/user/dto/base-user.dto.ts @@ -16,7 +16,7 @@ export class BaseUserDto { @IsInt() @Min(0) - @Max(10) + @Max(11) @ApiProperty({ minimum: 0, maximum: 11, From 3824000d849f541f702690c57d90fd3358a8b0ba Mon Sep 17 00:00:00 2001 From: huni Date: Sat, 17 Sep 2022 02:33:07 +0900 Subject: [PATCH 48/48] =?UTF-8?q?feat:=20findOneByGithubUId=20=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/auth/__test__/auth.service.test.ts | 6 +++--- apps/api/src/auth/auth.service.ts | 2 +- apps/api/src/user/user.service.ts | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/api/src/auth/__test__/auth.service.test.ts b/apps/api/src/auth/__test__/auth.service.test.ts index b135cff3..50a4a83f 100644 --- a/apps/api/src/auth/__test__/auth.service.test.ts +++ b/apps/api/src/auth/__test__/auth.service.test.ts @@ -16,7 +16,7 @@ describe('AuthService', () => { const authService = new AuthService(mockUserSerivce, mockJwtService, mockConfigService); beforeEach(() => { - mockUserSerivce.findOne.mockClear(); + mockUserSerivce.findOneByGithubUId.mockClear(); mockConfigService.get.mockClear(); jest.clearAllTimers(); }); @@ -29,7 +29,7 @@ describe('AuthService', () => { user.save = mockFn().mockReturnThis(); const githubProfile: GithubProfile = { id: '1', username: 'test' }; - mockUserSerivce.findOne.mockResolvedValue(user); + mockUserSerivce.findOneByGithubUId.mockResolvedValue(user); const result: User = await authService.login(githubProfile); @@ -39,7 +39,7 @@ describe('AuthService', () => { test('์ฒ˜์Œ ๋กœ๊ทธ์ธํ•˜๋Š” ์œ ์ €๋Š” ์œ ์ €๋ฅผ ์ƒ์„ฑํ•œ๋‹ค', async () => { const githubProfile: GithubProfile = { id: '1', username: 'test' }; - mockUserSerivce.findOne.mockResolvedValue(undefined); + mockUserSerivce.findOneByGithubUId.mockResolvedValue(undefined); mockUserSerivce.create.mockImplementation(async (user: User) => user); const result: User = await authService.login(githubProfile); diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index 817587dc..0c4b06fe 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -15,7 +15,7 @@ export class AuthService { ) {} async login(githubProfile: GithubProfile): Promise { - const user = await this.userService.findOne({ where: { githubId: githubProfile.id } }); + const user = await this.userService.findOneByGithubUId(githubProfile.id); if (user) { user.lastLogin = new Date(); diff --git a/apps/api/src/user/user.service.ts b/apps/api/src/user/user.service.ts index 5ade90aa..b27c67c7 100644 --- a/apps/api/src/user/user.service.ts +++ b/apps/api/src/user/user.service.ts @@ -1,6 +1,5 @@ import { User } from '@app/entity/user/user.entity'; import { Injectable } from '@nestjs/common'; -import { FindOneOptions } from 'typeorm'; import { UpdateUserProfileRequestDto } from './dto/request/update-user-profile-request.dto'; import { UpdateToCadetDto } from './dto/update-user-to-cadet.dto'; import { UserRepository } from './repositories/user.repository'; @@ -13,8 +12,8 @@ export class UserService { return this.userRepository.save(user); } - async findOne(options: FindOneOptions): Promise { - return this.userRepository.findOne(options); + async findOneByGithubUId(githubUid: string): Promise { + return this.userRepository.findOne({ where: { githubUid } }); } async findOneByIdOrFail(id: number): Promise {