From 5f9e9e548f8d98ca2cd2e6c7e5f7f55b231e6dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez=20Mu=C3=B1oz?= Date: Tue, 8 Oct 2024 11:34:31 +0200 Subject: [PATCH 1/8] implements e2e auth --- .github/workflows/e2e-tests.yml | 69 +++++++++++++ .gitignore | 6 +- client/.env.test | 3 + client/package.json | 6 +- e2e/package.json | 20 ++++ e2e/playwright.config.ts | 48 +++++++++ e2e/tests/auth/auth.spec.ts | 62 ++++++++++++ e2e/tsconfig.json | 10 ++ pnpm-lock.yaml | 166 ++++++++++++++++++++++++++++---- pnpm-workspace.yaml | 1 + shared/lib/db-entities.ts | 7 ++ shared/lib/db-helpers.ts | 54 +++++++++++ shared/lib/e2e-test-manager.ts | 87 +++++++++++++++++ shared/lib/entity-mocks.ts | 21 ++++ shared/package.json | 20 ++-- 15 files changed, 552 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/e2e-tests.yml create mode 100644 client/.env.test create mode 100644 e2e/package.json create mode 100644 e2e/playwright.config.ts create mode 100644 e2e/tests/auth/auth.spec.ts create mode 100644 e2e/tsconfig.json create mode 100644 shared/lib/db-entities.ts create mode 100644 shared/lib/db-helpers.ts create mode 100644 shared/lib/e2e-test-manager.ts create mode 100644 shared/lib/entity-mocks.ts diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 00000000..9205ba81 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,69 @@ +name: e2e Tests + +on: + push: + paths: + - 'e2e/**' + - 'shared/**' + - 'api/**' + - 'client/**' + - '.github/workflows/e2e-tests.yml' + - '/*' # include changes in root + - '!infrastructure/**' # exclude infra folder + - 'package.json' + + workflow_dispatch: + + +jobs: + e2e-tests: + name: e2e tests + runs-on: ubuntu-22.04 + + services: + database: + image: postgres:13 + ports: + - 5432:5432 + env: + POSTGRES_USER: blue-carbon-cost + POSTGRES_PASSWORD: blue-carbon-cost + POSTGRES_DB: blc + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Node setup + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + + - uses: pnpm/action-setup@v4 + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: node_modules + key: ${{ runner.os }}-node_modules-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-node_modules- + + - name: Install dependencies + working-directory: . + run: pnpm install + + - name: Install Chromium browser + working-directory: e2e + run: npx playwright install --with-deps chromium + + - name: Run e2e tests + working-directory: e2e + run: pnpm test + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 27bdfd87..033fe1e2 100644 --- a/.gitignore +++ b/.gitignore @@ -135,4 +135,8 @@ dist /infrastructure/.terraform/ # MacOs -.DS_Store \ No newline at end of file +.DS_Store + +# e2e +/e2e/playwright-report/ +/e2e/test-results/ \ No newline at end of file diff --git a/client/.env.test b/client/.env.test new file mode 100644 index 00000000..5acc95e2 --- /dev/null +++ b/client/.env.test @@ -0,0 +1,3 @@ +NEXTAUTH_URL=http://localhost:$PORT +NEXTAUTH_SECRET=WAzjpS46vFxp17TsRDU3FXo+TF0vrfy6uhCXwGMBUE8= +NEXT_PUBLIC_API_URL=https://dev.blue-carbon-cost-tool.dev-vizzuality.com/api \ No newline at end of file diff --git a/client/package.json b/client/package.json index 9abe719e..47d69352 100644 --- a/client/package.json +++ b/client/package.json @@ -26,7 +26,8 @@ "react": "^18", "react-dom": "^18", "tailwind-merge": "2.5.3", - "tailwindcss-animate": "1.0.7" + "tailwindcss-animate": "1.0.7", + "zod": "catalog:" }, "devDependencies": { "@types/node": "catalog:", @@ -40,7 +41,6 @@ "prettier-plugin-tailwindcss": "0.6.8", "react-hook-form": "7.53.0", "tailwindcss": "^3.4.1", - "typescript": "catalog:", - "zod": "catalog:" + "typescript": "catalog:" } } diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 00000000..d99c4107 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,20 @@ +{ + "name": "e2e", + "private": true, + "dependencies": { + "shared": "workspace:*" + }, + "devDependencies": { + "@playwright/test": "1.44.1", + "@types/lodash": "4.17.4", + "@types/node": "catalog:", + "tsc-alias": "1.8.10", + "typescript": "catalog:" + }, + "scripts": { + "pretest": "tsc && tsc-alias", + "test": "playwright test -c ./dist/e2e", + "test:ui": "pnpm pretest && playwright test --ui -c ./dist/e2e", + "codegen": "pnpm --filter api start:dev & pnpm --filter client dev & playwright codegen localhost:3000" + } +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 00000000..984e86b4 --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,48 @@ +import { defineConfig, devices } from '@playwright/test'; + +const API_URL = 'http://localhost:4000'; +const APP_URL = 'http://localhost:3000'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + /* Run your local dev server before starting the tests */ + webServer: [ + { + command: 'pnpm --filter api run build && NODE_ENV=test pnpm --filter api run start:prod', + url: API_URL, + reuseExistingServer: !process.env.CI, + }, + { + command: 'NODE_ENV=test pnpm --filter client run build && NODE_ENV=test pnpm --filter client run start', + url: APP_URL, + reuseExistingServer: !process.env.CI, + }, + ], + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: false, + workers: 1, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI ? 'list' : 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: APP_URL, + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/e2e/tests/auth/auth.spec.ts b/e2e/tests/auth/auth.spec.ts new file mode 100644 index 00000000..1702007e --- /dev/null +++ b/e2e/tests/auth/auth.spec.ts @@ -0,0 +1,62 @@ +import { test, expect, Page } from '@playwright/test'; +import { E2eTestManager } from '@shared/lib/e2e-test-manager'; +import { User } from '@shared/entities/users/user.entity'; + +let testManager: E2eTestManager; +let page: Page; + +test.beforeAll(async ({ browser }) => { + page = await browser.newPage(); + testManager = await E2eTestManager.load(page); +}); + +test.beforeEach(async () => { + await testManager.clearDatabase(); +}); + +test.afterEach(async () => { + await testManager.clearDatabase(); +}); + +test.afterAll(async () => { + await testManager.close(); +}); + +// test('an user signs up successfully', async ({ page }) => { +// const user: Pick = { +// email: 'johndoe@test.com', +// password: 'password', +// }; +// +// await page.goto('/auth/signup'); +// +// await page.getByLabel('Email').fill(user.email); +// await page.locator('input[type="password"]').fill(user.password); +// await page.getByRole('checkbox').check(); +// +// await page.getByRole('button', { name: /sign up/i }).click(); +// +// await page.waitForURL('/auth/signin'); +// +// await page.getByLabel('Email').fill(user.email); +// await page.locator('input[type="password"]').fill(user.password); +// +// await page.getByRole('button', { name: /log in/i }).click(); +// +// await page.waitForURL('/profile'); +// await expect(await page.locator('input[type="email"]')).toHaveValue( +// user.email +// ); +// }); + +test('an user signs in successfully', async ({ page }) => { + const user: Pick = { + email: 'jhondoe@test.com', + password: '12345678', + partnerName: 'partner-test' + }; + + await testManager.mocks().createUser(user); + await testManager.login(user as User) + await expect(page.getByText(`Email: ${user.email}`)).toBeVisible(); +}); diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 00000000..78bfa233 --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "module": "CommonJS", + "moduleResolution": "Node", + "sourceMap": false, + "outDir": "./dist", + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0302487..eda800f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -284,10 +284,10 @@ importers: version: 0.447.0(react@18.3.1) next: specifier: 14.2.10 - version: 14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 14.2.10(@playwright/test@1.44.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-auth: specifier: 4.24.8 - version: 4.24.8(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.24.8(next@14.2.10(@playwright/test@1.44.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18 version: 18.3.1 @@ -300,6 +300,9 @@ importers: tailwindcss-animate: specifier: 1.0.7 version: 1.0.7(tailwindcss@3.4.10(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.4.5))) + zod: + specifier: 'catalog:' + version: 3.23.8 devDependencies: '@types/node': specifier: 'catalog:' @@ -337,37 +340,74 @@ importers: typescript: specifier: 'catalog:' version: 5.4.5 - zod: + + e2e: + dependencies: + shared: + specifier: workspace:* + version: link:../shared + devDependencies: + '@playwright/test': + specifier: 1.44.1 + version: 1.44.1 + '@types/lodash': + specifier: 4.17.4 + version: 4.17.4 + '@types/node': specifier: 'catalog:' - version: 3.23.8 + version: 20.14.2 + tsc-alias: + specifier: 1.8.10 + version: 1.8.10 + typescript: + specifier: 'catalog:' + version: 5.4.5 shared: dependencies: - '@nestjs/mapped-types': - specifier: ^2.0.5 - version: 2.0.5(@nestjs/common@10.4.1(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(reflect-metadata@0.2.2) '@ts-rest/core': specifier: ^3.51.0 version: 3.51.0(@types/node@20.14.2)(zod@3.23.8) + bcrypt: + specifier: 5.1.1 + version: 5.1.1 class-transformer: specifier: 'catalog:' version: 0.5.1 - devDependencies: - '@types/node': - specifier: 'catalog:' - version: 20.14.2 + lodash: + specifier: 4.17.21 + version: 4.17.21 pg: specifier: 'catalog:' version: 8.12.0 typeorm: specifier: 'catalog:' version: 0.3.20(pg@8.12.0)(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.4.5)) - typescript: - specifier: 'catalog:' - version: 5.4.5 zod: specifier: 'catalog:' version: 3.23.8 + devDependencies: + '@nestjs/mapped-types': + specifier: ^2.0.5 + version: 2.0.5(@nestjs/common@10.4.1(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(reflect-metadata@0.2.2) + '@types/bcrypt': + specifier: ^5.0.2 + version: 5.0.2 + '@types/jsonwebtoken': + specifier: 9.0.7 + version: 9.0.7 + '@types/lodash': + specifier: 4.17.10 + version: 4.17.10 + '@types/node': + specifier: 'catalog:' + version: 20.14.2 + jsonwebtoken: + specifier: 9.0.2 + version: 9.0.2 + typescript: + specifier: 'catalog:' + version: 5.4.5 packages: @@ -1855,6 +1895,11 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.44.1': + resolution: {integrity: sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==} + engines: {node: '>=16'} + hasBin: true + '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -2754,9 +2799,18 @@ packages: '@types/jsonwebtoken@9.0.5': resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} + '@types/jsonwebtoken@9.0.7': + resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} + '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + '@types/lodash@4.17.10': + resolution: {integrity: sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==} + + '@types/lodash@4.17.4': + resolution: {integrity: sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==} + '@types/lodash@4.17.7': resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==} @@ -3521,6 +3575,10 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + comment-json@4.2.3: resolution: {integrity: sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==} engines: {node: '>= 6'} @@ -4217,6 +4275,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -5163,6 +5226,10 @@ packages: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + mylas@2.1.13: + resolution: {integrity: sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==} + engines: {node: '>=12.0.0'} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -5518,6 +5585,20 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + playwright-core@1.44.1: + resolution: {integrity: sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==} + engines: {node: '>=16'} + hasBin: true + + playwright@1.44.1: + resolution: {integrity: sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==} + engines: {node: '>=16'} + hasBin: true + + plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -5771,6 +5852,10 @@ packages: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} + queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -6513,6 +6598,10 @@ packages: '@swc/wasm': optional: true + tsc-alias@1.8.10: + resolution: {integrity: sha512-Ibv4KAWfFkFdKJxnWfVtdOmB0Zi1RJVxcbPGiCDsFpCQSsmpWyuzHG3rQyI5YkobWwxFPEyQfu1hdo4qLG2zPw==} + hasBin: true + tsconfig-paths-webpack-plugin@4.1.0: resolution: {integrity: sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==} engines: {node: '>=10.13.0'} @@ -9019,6 +9108,10 @@ snapshots: '@pkgr/core@0.1.1': {} + '@playwright/test@1.44.1': + dependencies: + playwright: 1.44.1 + '@popperjs/core@2.11.8': {} '@radix-ui/primitive@1.1.0': {} @@ -9973,8 +10066,16 @@ snapshots: dependencies: '@types/node': 22.7.5 + '@types/jsonwebtoken@9.0.7': + dependencies: + '@types/node': 22.7.5 + '@types/linkify-it@5.0.0': {} + '@types/lodash@4.17.10': {} + + '@types/lodash@4.17.4': {} + '@types/lodash@4.17.7': {} '@types/markdown-it@14.1.2': @@ -10959,6 +11060,8 @@ snapshots: commander@4.1.1: {} + commander@9.5.0: {} + comment-json@4.2.3: dependencies: array-timsort: 1.0.3 @@ -11903,6 +12006,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -13057,6 +13163,8 @@ snapshots: mute-stream@1.0.0: {} + mylas@2.1.13: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -13071,13 +13179,13 @@ snapshots: neo-async@2.6.2: {} - next-auth@4.24.8(next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-auth@4.24.8(next@14.2.10(@playwright/test@1.44.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.15)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.25.7 '@panva/hkdf': 1.2.1 cookie: 0.5.0 jose: 4.15.9 - next: 14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.10(@playwright/test@1.44.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) oauth: 0.9.15 openid-client: 5.7.0 preact: 10.24.2 @@ -13088,7 +13196,7 @@ snapshots: optionalDependencies: nodemailer: 6.9.15 - next@14.2.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.2.10(@playwright/test@1.44.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.10 '@swc/helpers': 0.5.5 @@ -13109,6 +13217,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 14.2.10 '@next/swc-win32-ia32-msvc': 14.2.10 '@next/swc-win32-x64-msvc': 14.2.10 + '@playwright/test': 1.44.1 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -13412,6 +13521,18 @@ snapshots: dependencies: find-up: 4.1.0 + playwright-core@1.44.1: {} + + playwright@1.44.1: + dependencies: + playwright-core: 1.44.1 + optionalDependencies: + fsevents: 2.3.2 + + plimit-lit@1.6.1: + dependencies: + queue-lit: 1.5.2 + pluralize@8.0.0: {} polished@4.3.1: @@ -13640,6 +13761,8 @@ snapshots: dependencies: side-channel: 1.0.6 + queue-lit@1.5.2: {} + queue-microtask@1.2.3: {} raf-schd@4.0.3: {} @@ -14510,6 +14633,15 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + tsc-alias@1.8.10: + dependencies: + chokidar: 3.6.0 + commander: 9.5.0 + globby: 11.1.0 + mylas: 2.1.13 + normalize-path: 3.0.0 + plimit-lit: 1.6.1 + tsconfig-paths-webpack-plugin@4.1.0: dependencies: chalk: 4.1.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1626bf0d..eb0a64bf 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,7 @@ packages: - 'api/**' - 'client/**' - 'shared/**' + - 'e2e/**' - 'data/**' - 'admin/**' diff --git a/shared/lib/db-entities.ts b/shared/lib/db-entities.ts new file mode 100644 index 00000000..aaa25d42 --- /dev/null +++ b/shared/lib/db-entities.ts @@ -0,0 +1,7 @@ +import { MixedList } from 'typeorm/common/MixedList'; +import { EntitySchema } from 'typeorm/entity-schema/EntitySchema'; +import { User } from "@shared/entities/users/user.entity"; + +export const DB_ENTITIES: MixedList = [ + User, +]; diff --git a/shared/lib/db-helpers.ts b/shared/lib/db-helpers.ts new file mode 100644 index 00000000..1542da94 --- /dev/null +++ b/shared/lib/db-helpers.ts @@ -0,0 +1,54 @@ +import { DataSource, EntityMetadata } from 'typeorm'; +import { difference } from 'lodash'; + +export async function clearTestDataFromDatabase( + dataSource: DataSource, +): Promise { + const queryRunner = dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { + const entityTableNames: string[] = dataSource.entityMetadatas + .filter( + (entityMetadata: EntityMetadata) => + entityMetadata.tableType === 'regular' || + entityMetadata.tableType === 'junction', + ) + .map((entityMetadata: EntityMetadata) => entityMetadata.tableName); + + await Promise.all( + entityTableNames.map((entityTableName: string) => + queryRunner.query(`TRUNCATE TABLE "${entityTableName}" CASCADE`), + ), + ); + + entityTableNames.push(dataSource.metadataTableName); + entityTableNames.push( + dataSource.options.migrationsTableName || 'migrations', + ); + entityTableNames.push('spatial_ref_sys'); + + const databaseTableNames: string[] = ( + await dataSource.query( + `SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE'`, + ) + ).map((e: Record) => e.table_name); + + // todo: Alex to take a look later + // const tablesToDrop = difference(databaseTableNames, entityTableNames); + // + // await Promise.all( + // tablesToDrop.map((tableToDrop: string) => + // queryRunner.dropTable(tableToDrop), + // ), + // ); + await queryRunner.commitTransaction(); + } catch (err) { + // rollback changes before throwing error + await queryRunner.rollbackTransaction(); + throw err; + } finally { + // release query runner which is manually created + await queryRunner.release(); + } +} diff --git a/shared/lib/e2e-test-manager.ts b/shared/lib/e2e-test-manager.ts new file mode 100644 index 00000000..0ebc3b83 --- /dev/null +++ b/shared/lib/e2e-test-manager.ts @@ -0,0 +1,87 @@ +import { DataSource } from 'typeorm'; +import { User } from "@shared/entities/users/user.entity"; +import { + createUser, +} from '@shared/lib/entity-mocks'; +import { clearTestDataFromDatabase } from '@shared/lib/db-helpers'; +import { DB_ENTITIES } from '@shared/lib/db-entities'; +import { sign } from 'jsonwebtoken'; + +const AppDataSource = new DataSource({ + type: 'postgres', + host: 'localhost', + port: 5432, + username: 'blue-carbon-cost', + password: 'blue-carbon-cost', + database: 'blc', + entities: DB_ENTITIES, +}); + +export class E2eTestManager { + dataSource: DataSource; + page: any; + + constructor(dataSource: DataSource, page?: any) { + this.dataSource = dataSource; + this.page = page; + } + + static async load(page?: any) { + await AppDataSource.initialize(); + + return new E2eTestManager(AppDataSource, page); + } + + async clearDatabase() { + await clearTestDataFromDatabase(this.dataSource); + } + + getDataSource() { + return this.dataSource; + } + + async close() { + if (this.page) { + await this.page.close(); + } + await this.dataSource.destroy(); + } + + async createUser(additionalData?: Partial) { + return createUser(this.dataSource, additionalData); + } + + mocks() { + return { + createUser: (additionalData?: Partial) => + createUser(this.getDataSource(), additionalData), + }; + } + + getPage() { + if (!this.page) throw new Error('Playwright Page is not initialized'); + return this.page; + } + + async login(user?: User) { + if (!user) { + user = await this.mocks().createUser(); + } + await this.page.goto('/auth/signin'); + await this.page.getByLabel('Email').fill(user.email); + await this.page.locator('input[type="password"]').fill(user.password); + await this.page.getByRole('button', { name: /log in/i }).click(); + await this.page.waitForURL('/profile'); + return user; + } + + async logout() { + await this.page.goto('/auth/api/signout'); + await this.page.getByRole('button', { name: 'Sign out' }).click(); + } + + async generateToken(user: User) { + // the secret must match the provided for the api when built for e2e tests + return sign({ id: user.id }, 'mysupersecretfortests'); + } +} diff --git a/shared/lib/entity-mocks.ts b/shared/lib/entity-mocks.ts new file mode 100644 index 00000000..9b2f3db1 --- /dev/null +++ b/shared/lib/entity-mocks.ts @@ -0,0 +1,21 @@ +import { genSalt, hash } from 'bcrypt'; +import { DataSource, DeepPartial } from 'typeorm'; +import { User } from "@shared/entities/users/user.entity"; + + +export const createUser = async ( + dataSource: DataSource, + additionalData?: Partial, +): Promise => { + const salt = await genSalt(); + const usedPassword = additionalData?.password ?? '12345678'; + const defaultData: DeepPartial = { + email: 'test@user.com', + password: await hash(usedPassword, salt), + }; + + const user = { ...defaultData, ...additionalData }; + + await dataSource.getRepository(User).save(user); + return { ...user, password: usedPassword } as User; +}; \ No newline at end of file diff --git a/shared/package.json b/shared/package.json index 3fa41ac6..b308739b 100644 --- a/shared/package.json +++ b/shared/package.json @@ -1,16 +1,22 @@ { "name": "shared", "private": true, - "devDependencies": { + "dependencies": { + "@ts-rest/core": "^3.51.0", + "bcrypt": "5.1.1", + "class-transformer": "catalog:", + "lodash": "4.17.21", "pg": "catalog:", "typeorm": "catalog:", - "zod": "catalog:", - "@types/node": "catalog:", - "typescript": "catalog:" + "zod": "catalog:" }, - "dependencies": { + "devDependencies": { "@nestjs/mapped-types": "^2.0.5", - "@ts-rest/core": "^3.51.0", - "class-transformer": "catalog:" + "@types/bcrypt": "^5.0.2", + "@types/jsonwebtoken": "9.0.7", + "@types/lodash": "4.17.10", + "@types/node": "catalog:", + "jsonwebtoken": "9.0.2", + "typescript": "catalog:" } } From 1156aead69412e7143d4767d0e78de0bc9f97c31 Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 10 Oct 2024 07:13:19 +0200 Subject: [PATCH 2/8] fix inactive user login bug --- api/src/modules/auth/authentication.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/modules/auth/authentication.service.ts b/api/src/modules/auth/authentication.service.ts index e24d1204..30633aa0 100644 --- a/api/src/modules/auth/authentication.service.ts +++ b/api/src/modules/auth/authentication.service.ts @@ -22,7 +22,7 @@ export class AuthenticationService { ) {} async validateUser(email: string, password: string): Promise { const user = await this.usersService.findByEmail(email); - if (user && (await bcrypt.compare(password, user.password))) { + if (user?.isActive && (await bcrypt.compare(password, user.password))) { return user; } throw new UnauthorizedException(`Invalid credentials`); From 33298965d2933fb7158767645bc1dddd5e0ef0ed Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 10 Oct 2024 09:01:50 +0200 Subject: [PATCH 3/8] use common lib, fix inactive user login --- api/test/e2e/steps/sign-in.steps.ts | 127 +++++++++------------------ api/test/utils/mocks/entity-mocks.ts | 20 ----- api/test/utils/test-manager.ts | 2 +- 3 files changed, 42 insertions(+), 107 deletions(-) delete mode 100644 api/test/utils/mocks/entity-mocks.ts diff --git a/api/test/e2e/steps/sign-in.steps.ts b/api/test/e2e/steps/sign-in.steps.ts index 43e27dd9..195ce1d3 100644 --- a/api/test/e2e/steps/sign-in.steps.ts +++ b/api/test/e2e/steps/sign-in.steps.ts @@ -1,18 +1,18 @@ -import { defineFeature, loadFeature } from 'jest-cucumber'; -import { Response } from 'supertest'; -import { TestManager } from 'api/test/utils/test-manager'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; import { User } from '@shared/entities/users/user.entity'; +import { TestManager } from '../../utils/test-manager'; -const feature = loadFeature('./test/e2e/features/sign-in.feature'); - -defineFeature(feature, (test) => { +describe('Sign-in E2E Tests', () => { + let app: INestApplication; let testManager: TestManager; beforeAll(async () => { testManager = await TestManager.createTestManager(); + app = testManager.getApp(); }); - beforeEach(async () => { + afterEach(async () => { await testManager.clearDatabase(); }); @@ -20,95 +20,50 @@ defineFeature(feature, (test) => { await testManager.close(); }); - test('A user tries to sign in with non-existing credentials', ({ - when, - then, - and, - }) => { - let response: Response; - - when( - 'a user attempts to sign in with non-existing credentials', - async () => { - response = await testManager - .request() - .post('/authentication/login') - .send({ email: 'non-existing@user.com', password: '12345567' }); - }, - ); - - then( - /^the user should receive a (\d+) status code$/, - (statusCode: string) => { - expect(response.status).toBe(Number.parseInt(statusCode, 10)); - }, - ); + it('should return 404 when user tries to sign in with non-existing credentials', async () => { + const response = await request(app.getHttpServer()) + .post('/authentication/login') + .send({ email: 'non-existing@user.com', password: '12345567' }); - and(/^the response message should be "(.*)"$/, (message: string) => { - expect(response.body.message).toEqual(message); - }); + expect(response.status).toBe(401); + expect(response.body.message).toEqual('Invalid credentials'); }); - test('A user tries to sign in with an incorrect password', ({ - given, - when, - then, - and, - }) => { - let user: User; - let response: Response; + it('should return 401 when user tries to sign in with an incorrect password', async () => { + const user: User = await testManager + .mocks() + .createUser({ email: 'test@test.com', password: '12345678' }); - given('a user exists with valid credentials', async () => { - user = await testManager - .mocks() - .createUser({ email: 'test@test.com', password: '12345678' }); - }); + const response = await request(app.getHttpServer()) + .post('/authentication/login') + .send({ email: user.email, password: 'wrongpassword' }); - when('a user attempts to sign in with an incorrect password', async () => { - response = await testManager - .request() - .post('/authentication/login') - .send({ email: user.email, password: 'wrongpassword' }); - }); - - then( - /^the user should receive a (\d+) status code$/, - (statusCode: string) => { - expect(response.status).toBe(Number.parseInt(statusCode, 10)); - }, - ); - - and(/^the response message should be "(.*)"$/, (message: string) => { - expect(response.body.message).toEqual(message); - }); + expect(response.status).toBe(401); + expect(response.body.message).toEqual('Invalid credentials'); }); - test('A user successfully signs in', ({ given, when, then, and }) => { - let user: User; - let response: Response; + it('should return 200 and an access token when user successfully signs in', async () => { + const user: User = await testManager + .mocks() + .createUser({ email: 'test@test.com', password: '12345678' }); - given('a user exists with valid credentials', async () => { - user = await testManager - .mocks() - .createUser({ email: 'test@test.com', password: '12345678' }); - }); + const response = await request(app.getHttpServer()) + .post('/authentication/login') + .send({ email: user.email, password: user.password }); - when('a user attempts to sign in with valid credentials', async () => { - response = await testManager - .request() - .post('/authentication/login') - .send({ email: user.email, password: user.password }); - }); + expect(response.status).toBe(201); + expect(response.body.accessToken).toBeDefined(); + }); + it('should return 401 when user tries to sign in with an inactive account', async () => { + const inactiveUser: User = await testManager + .mocks() + .createUser({ isActive: false }); - then( - /^the user should receive a (\d+) status code$/, - (statusCode: string) => { - expect(response.status).toBe(Number.parseInt(statusCode, 10)); - }, - ); + const response = await testManager + .request() + .post('/authentication/login') + .send({ email: inactiveUser.email, password: inactiveUser.password }); - and('the access token should be defined', () => { - expect(response.body.accessToken).toBeDefined(); - }); + expect(response.status).toBe(401); }); }); diff --git a/api/test/utils/mocks/entity-mocks.ts b/api/test/utils/mocks/entity-mocks.ts deleted file mode 100644 index ca050b05..00000000 --- a/api/test/utils/mocks/entity-mocks.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { DataSource } from 'typeorm'; -import { User } from '@shared/entities/users/user.entity'; -import { genSalt, hash } from 'bcrypt'; - -export const createUser = async ( - dataSource: DataSource, - additionalData?: Partial, -): Promise => { - const salt = await genSalt(); - const usedPassword = additionalData?.password ?? '12345678'; - const user = { - email: 'test@user.com', - ...additionalData, - password: await hash(usedPassword, salt), - isActive: true, - }; - - await dataSource.getRepository(User).save(user); - return { ...user, password: usedPassword } as User; -}; diff --git a/api/test/utils/test-manager.ts b/api/test/utils/test-manager.ts index 6675970c..f7888c39 100644 --- a/api/test/utils/test-manager.ts +++ b/api/test/utils/test-manager.ts @@ -9,11 +9,11 @@ import * as request from 'supertest'; import { getDataSourceToken } from '@nestjs/typeorm'; import { clearTestDataFromDatabase } from './db-helpers'; -import { createUser } from './mocks/entity-mocks'; import { User } from '@shared/entities/users/user.entity'; import { IEmailServiceToken } from '@api/modules/notifications/email/email-service.interface'; import { MockEmailService } from './mocks/mock-email.service'; import { ROLES } from '@shared/entities/users/roles.enum'; +import { createUser } from '@shared/lib/entity-mocks'; /** * @description: Abstraction for NestJS testing workflow. For now its a basic implementation to create a test app, but can be extended to encapsulate From d6824ef553bba4955ebd0d45ce7edb613a6480b9 Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 10 Oct 2024 09:20:46 +0200 Subject: [PATCH 4/8] refactor createUser mock --- shared/lib/entity-mocks.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/shared/lib/entity-mocks.ts b/shared/lib/entity-mocks.ts index 9b2f3db1..d1aff50b 100644 --- a/shared/lib/entity-mocks.ts +++ b/shared/lib/entity-mocks.ts @@ -1,21 +1,20 @@ -import { genSalt, hash } from 'bcrypt'; -import { DataSource, DeepPartial } from 'typeorm'; +import { genSalt, hash } from "bcrypt"; +import { DataSource, DeepPartial } from "typeorm"; import { User } from "@shared/entities/users/user.entity"; - export const createUser = async ( dataSource: DataSource, additionalData?: Partial, ): Promise => { const salt = await genSalt(); - const usedPassword = additionalData?.password ?? '12345678'; - const defaultData: DeepPartial = { - email: 'test@user.com', + const usedPassword = additionalData?.password ?? "12345678"; + const user = { + email: "test@user.com", + ...additionalData, password: await hash(usedPassword, salt), + isActive: additionalData?.isActive ?? true, }; - const user = { ...defaultData, ...additionalData }; - await dataSource.getRepository(User).save(user); return { ...user, password: usedPassword } as User; -}; \ No newline at end of file +}; From 3c7a56f5e121f9cdfaa990996d7e88dc87d662b9 Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 10 Oct 2024 09:21:09 +0200 Subject: [PATCH 5/8] fix test files --- api/test/e2e/steps/password-recovery-reset-email.steps.ts | 4 ++-- api/test/e2e/steps/password-recovery-send-email.steps.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/api/test/e2e/steps/password-recovery-reset-email.steps.ts b/api/test/e2e/steps/password-recovery-reset-email.steps.ts index f1254b42..2a08fc76 100644 --- a/api/test/e2e/steps/password-recovery-reset-email.steps.ts +++ b/api/test/e2e/steps/password-recovery-reset-email.steps.ts @@ -73,7 +73,7 @@ describe('Reset Password', () => { then( /the user should receive a (\d+) status code/, - (statusCode: string) => { + async (statusCode: string) => { expect(response.status).toBe(Number.parseInt(statusCode)); }, ); @@ -123,7 +123,7 @@ describe('Reset Password', () => { then( /the user should receive a (\d+) status code/, - (statusCode: string) => { + async (statusCode: string) => { expect(response.status).toBe(Number.parseInt(statusCode)); }, ); diff --git a/api/test/e2e/steps/password-recovery-send-email.steps.ts b/api/test/e2e/steps/password-recovery-send-email.steps.ts index fc0c43d6..1d1f9377 100644 --- a/api/test/e2e/steps/password-recovery-send-email.steps.ts +++ b/api/test/e2e/steps/password-recovery-send-email.steps.ts @@ -27,6 +27,9 @@ describe('Password Recovery - Send Email', () => { testUser = user; jest.clearAllMocks(); }); + afterEach(async () => { + await testManager.clearDatabase(); + }); afterAll(async () => { await testManager.close(); From f8ef226c70c676b28d2df10d3d761085019c26e3 Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 10 Oct 2024 09:31:59 +0200 Subject: [PATCH 6/8] load env for jest context --- api/test/jest-config.json | 1 + api/test/set-up-jest-env.ts | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 api/test/set-up-jest-env.ts diff --git a/api/test/jest-config.json b/api/test/jest-config.json index a3717c69..581efaca 100644 --- a/api/test/jest-config.json +++ b/api/test/jest-config.json @@ -3,6 +3,7 @@ "rootDir": "../", "roots": ["/src/", "/test/"], "testEnvironment": "node", + "setupFiles": ["/test/set-up-jest-env.ts"], "testRegex": "(.steps.ts|.spec.ts)$", "transform": { "^.+\\.(t|j)s$": "ts-jest" diff --git a/api/test/set-up-jest-env.ts b/api/test/set-up-jest-env.ts new file mode 100644 index 00000000..890fdf5b --- /dev/null +++ b/api/test/set-up-jest-env.ts @@ -0,0 +1,11 @@ +import { config } from 'dotenv'; +import { resolve } from 'path'; + +/** + * @description: Since Jest is executed in a different context, we need to load the environment variables manually for this specific context + * for e2e tests, the default route + env suffix is used + */ + +const envPath = resolve(__dirname, '../../shared/config/.env.test'); + +config({ path: envPath }); From 152f5fa1b20b9d6a3c4a48cdd4272d06fc58bf6a Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 10 Oct 2024 10:48:35 +0200 Subject: [PATCH 7/8] WIP e2e test env --- api/package.json | 5 +- api/src/modules/config/app-config.module.ts | 2 + api/src/modules/config/path-resolver.ts | 7 +- client/.env.development | 3 +- client/.env.test | 2 +- e2e/package.json | 4 +- e2e/playwright.config.ts | 83 +++++++++++---------- e2e/tests/auth/auth.spec.ts | 76 +++++++++++++------ pnpm-lock.yaml | 21 +----- shared/lib/e2e-test-manager.ts | 38 +++++----- 10 files changed, 129 insertions(+), 112 deletions(-) diff --git a/api/package.json b/api/package.json index 1e5e39c8..1bb9b79c 100644 --- a/api/package.json +++ b/api/package.json @@ -26,8 +26,10 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/typeorm": "^10.0.2", "@ts-rest/nest": "^3.51.0", + "@types/multer": "^1.4.12", "bcrypt": "catalog:", "class-transformer": "catalog:", + "dotenv": "16.4.5", "lodash": "^4.17.21", "nodemailer": "^6.9.15", "passport": "^0.7.0", @@ -38,8 +40,7 @@ "rxjs": "^7.8.1", "typeorm": "catalog:", "xlsx": "^0.18.5", - "zod": "catalog:", - "@types/multer": "^1.4.12" + "zod": "catalog:" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/api/src/modules/config/app-config.module.ts b/api/src/modules/config/app-config.module.ts index de6c3806..0fdec226 100644 --- a/api/src/modules/config/app-config.module.ts +++ b/api/src/modules/config/app-config.module.ts @@ -5,6 +5,8 @@ import { DatabaseModule } from '@api/modules/config/database/database.module'; import { resolveConfigPath } from '@api/modules/config/path-resolver'; import { JwtConfigHandler } from '@api/modules/config/auth-config.handler'; +const DEFAULT_RELATIVE_PATH = '../../../../../../'; + @Global() @Module({ imports: [ diff --git a/api/src/modules/config/path-resolver.ts b/api/src/modules/config/path-resolver.ts index 01909aad..1e225098 100644 --- a/api/src/modules/config/path-resolver.ts +++ b/api/src/modules/config/path-resolver.ts @@ -6,12 +6,9 @@ const TEST_RELATIVE_PATH = '../../../../'; const DEFAULT_RELATIVE_PATH = '../../../../../../'; /** - * @description: Resolve the path of the config file depending on the environment + * @description: Resolve the path of the dotenv config file relative to shared folder */ export function resolveConfigPath(relativePath: string): string { - const rootDir = - process.env.NODE_ENV === 'test' - ? TEST_RELATIVE_PATH - : DEFAULT_RELATIVE_PATH; + const rootDir = DEFAULT_RELATIVE_PATH; return join(__dirname, rootDir, relativePath); } diff --git a/client/.env.development b/client/.env.development index 5acc95e2..9c6a2e7e 100644 --- a/client/.env.development +++ b/client/.env.development @@ -1,3 +1,4 @@ NEXTAUTH_URL=http://localhost:$PORT NEXTAUTH_SECRET=WAzjpS46vFxp17TsRDU3FXo+TF0vrfy6uhCXwGMBUE8= -NEXT_PUBLIC_API_URL=https://dev.blue-carbon-cost-tool.dev-vizzuality.com/api \ No newline at end of file +#NEXT_PUBLIC_API_URL=https://dev.blue-carbon-cost-tool.dev-vizzuality.com/api +NEXT_PUBLIC_API_URL=http://localhost:4000 \ No newline at end of file diff --git a/client/.env.test b/client/.env.test index 5acc95e2..d99a62f9 100644 --- a/client/.env.test +++ b/client/.env.test @@ -1,3 +1,3 @@ NEXTAUTH_URL=http://localhost:$PORT NEXTAUTH_SECRET=WAzjpS46vFxp17TsRDU3FXo+TF0vrfy6uhCXwGMBUE8= -NEXT_PUBLIC_API_URL=https://dev.blue-carbon-cost-tool.dev-vizzuality.com/api \ No newline at end of file +NEXT_PUBLIC_API_URL=http://localhost:4000 \ No newline at end of file diff --git a/e2e/package.json b/e2e/package.json index d99c4107..7006a817 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -15,6 +15,8 @@ "pretest": "tsc && tsc-alias", "test": "playwright test -c ./dist/e2e", "test:ui": "pnpm pretest && playwright test --ui -c ./dist/e2e", - "codegen": "pnpm --filter api start:dev & pnpm --filter client dev & playwright codegen localhost:3000" + "codegen": "pnpm --filter api start:dev & pnpm --filter client dev & playwright codegen localhost:3000", + "codegen:prod": "pnpm --filter api run build & NODE_ENV=test pnpm --filter api run start:prod & NODE_ENV=test pnpm --filter client run build & NODE_ENV=test pnpm --filter client run start & playwright codegen localhost:3000", + "test2": "playwright test" } } diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 984e86b4..67a6abc9 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -1,48 +1,51 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; -const API_URL = 'http://localhost:4000'; -const APP_URL = 'http://localhost:3000'; +// +const API_URL = "http://localhost:4000"; +const APP_URL = "http://localhost:3000"; /** * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - /* Run your local dev server before starting the tests */ - webServer: [ - { - command: 'pnpm --filter api run build && NODE_ENV=test pnpm --filter api run start:prod', - url: API_URL, - reuseExistingServer: !process.env.CI, - }, - { - command: 'NODE_ENV=test pnpm --filter client run build && NODE_ENV=test pnpm --filter client run start', - url: APP_URL, - reuseExistingServer: !process.env.CI, - }, - ], - testDir: './tests', - /* Run tests in files in parallel */ - fullyParallel: false, - workers: 1, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: process.env.CI ? 'list' : 'html', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: APP_URL, - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + /* Run your local dev server before starting the tests */ + webServer: [ + { + command: + "pnpm --filter api run build && NODE_ENV=test pnpm --filter api run start:prod", + url: API_URL, + reuseExistingServer: !process.env.CI, }, - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - ], + { + command: + "NODE_ENV=test pnpm --filter client run build && NODE_ENV=test pnpm --filter client run start", + url: APP_URL, + reuseExistingServer: !process.env.CI, + }, + ], + testDir: "./tests", + /* Run tests in files in parallel */ + fullyParallel: false, + workers: 1, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI ? "list" : "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: APP_URL, + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], }); diff --git a/e2e/tests/auth/auth.spec.ts b/e2e/tests/auth/auth.spec.ts index 1702007e..11f6375d 100644 --- a/e2e/tests/auth/auth.spec.ts +++ b/e2e/tests/auth/auth.spec.ts @@ -1,6 +1,6 @@ -import { test, expect, Page } from '@playwright/test'; -import { E2eTestManager } from '@shared/lib/e2e-test-manager'; -import { User } from '@shared/entities/users/user.entity'; +import { test, expect, Page } from "@playwright/test"; +import { E2eTestManager } from "@shared/lib/e2e-test-manager"; +import { User } from "@shared/entities/users/user.entity"; let testManager: E2eTestManager; let page: Page; @@ -15,48 +15,76 @@ test.beforeEach(async () => { }); test.afterEach(async () => { - await testManager.clearDatabase(); + //await testManager.clearDatabase(); }); test.afterAll(async () => { + await testManager.logout(); await testManager.close(); }); -// test('an user signs up successfully', async ({ page }) => { -// const user: Pick = { -// email: 'johndoe@test.com', -// password: 'password', +// test("an user signs up successfully", async ({ page }) => { +// const user: Pick = { +// email: "johndoe@test.com", +// password: "password", // }; // -// await page.goto('/auth/signup'); +// await page.goto("/auth/signup"); // -// await page.getByLabel('Email').fill(user.email); +// await page.getByLabel("Email").fill(user.email); // await page.locator('input[type="password"]').fill(user.password); -// await page.getByRole('checkbox').check(); +// await page.getByRole("checkbox").check(); // -// await page.getByRole('button', { name: /sign up/i }).click(); +// await page.getByRole("button", { name: /sign up/i }).click(); // -// await page.waitForURL('/auth/signin'); +// await page.waitForURL("/auth/signin"); // -// await page.getByLabel('Email').fill(user.email); +// await page.getByLabel("Email").fill(user.email); // await page.locator('input[type="password"]').fill(user.password); // -// await page.getByRole('button', { name: /log in/i }).click(); +// await page.getByRole("button", { name: /log in/i }).click(); // -// await page.waitForURL('/profile'); +// await page.waitForURL("/profile"); // await expect(await page.locator('input[type="email"]')).toHaveValue( -// user.email +// user.email, // ); // }); -test('an user signs in successfully', async ({ page }) => { - const user: Pick = { - email: 'jhondoe@test.com', - password: '12345678', - partnerName: 'partner-test' +test("an user signs in successfully", async ({ page }) => { + const user: Pick = { + email: "jhondoe@test.com", + password: "12345678", + partnerName: "admin", }; await testManager.mocks().createUser(user); - await testManager.login(user as User) - await expect(page.getByText(`Email: ${user.email}`)).toBeVisible(); + await page.goto("/auth/signin"); + await page.getByLabel("Email").fill(user.email); + await page.locator('input[type="password"]').fill(user.password); + await page.getByRole("button", { name: /log in/i }).click(); +}); + +test("test", async ({ page }) => { + await page.goto("http://localhost:3000/"); + await page.getByRole("link", { name: "Sign in" }).click(); + await page.getByPlaceholder("Enter your email").click(); + await page.getByPlaceholder("Enter your email").fill("jhondoe@test.com"); + await page.getByPlaceholder("*******").click(); + await page.getByPlaceholder("*******").fill("12345678"); + await page.getByRole("button", { name: "Log in" }).click(); }); +// test("test", async ({ page }) => { +// const user: Pick = { +// email: "jhondoe@test.com", +// password: "12345678", +// partnerName: "admin", +// }; +// await page.goto("http://localhost:3000/"); +// await page.getByRole("link", { name: "Sign in" }).click(); +// await page.getByPlaceholder("Enter your email").click(); +// await page.getByPlaceholder("Enter your email").fill(user.email); +// await page.getByPlaceholder("*******").click(); +// await page.getByPlaceholder("*******").fill(user.password); +// await page.getByRole("button", { name: "Log in" }).click(); +// await expect(page.getByText(`Email: ${user.email}`)).toBeVisible(); +// }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eda800f9..181c1003 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,27 +9,9 @@ catalogs: '@types/node': specifier: 20.14.2 version: 20.14.2 - bcrypt: - specifier: 5.1.1 - version: 5.1.1 - class-transformer: - specifier: 0.5.1 - version: 0.5.1 - pg: - specifier: 8.12.0 - version: 8.12.0 - reflect-metadata: - specifier: ^0.2.0 - version: 0.2.2 - typeorm: - specifier: 0.3.20 - version: 0.3.20 typescript: specifier: 5.4.5 version: 5.4.5 - zod: - specifier: 3.23.8 - version: 3.23.8 importers: @@ -128,6 +110,9 @@ importers: class-transformer: specifier: 'catalog:' version: 0.5.1 + dotenv: + specifier: 16.4.5 + version: 16.4.5 lodash: specifier: ^4.17.21 version: 4.17.21 diff --git a/shared/lib/e2e-test-manager.ts b/shared/lib/e2e-test-manager.ts index 0ebc3b83..1876112e 100644 --- a/shared/lib/e2e-test-manager.ts +++ b/shared/lib/e2e-test-manager.ts @@ -1,19 +1,17 @@ -import { DataSource } from 'typeorm'; +import { DataSource } from "typeorm"; import { User } from "@shared/entities/users/user.entity"; -import { - createUser, -} from '@shared/lib/entity-mocks'; -import { clearTestDataFromDatabase } from '@shared/lib/db-helpers'; -import { DB_ENTITIES } from '@shared/lib/db-entities'; -import { sign } from 'jsonwebtoken'; +import { createUser } from "@shared/lib/entity-mocks"; +import { clearTestDataFromDatabase } from "@shared/lib/db-helpers"; +import { DB_ENTITIES } from "@shared/lib/db-entities"; +import { sign } from "jsonwebtoken"; const AppDataSource = new DataSource({ - type: 'postgres', - host: 'localhost', + type: "postgres", + host: "localhost", port: 5432, - username: 'blue-carbon-cost', - password: 'blue-carbon-cost', - database: 'blc', + username: "blue-carbon-cost", + password: "blue-carbon-cost", + database: "blc", entities: DB_ENTITIES, }); @@ -59,7 +57,7 @@ export class E2eTestManager { } getPage() { - if (!this.page) throw new Error('Playwright Page is not initialized'); + if (!this.page) throw new Error("Playwright Page is not initialized"); return this.page; } @@ -67,21 +65,21 @@ export class E2eTestManager { if (!user) { user = await this.mocks().createUser(); } - await this.page.goto('/auth/signin'); - await this.page.getByLabel('Email').fill(user.email); + await this.page.goto("/auth/signin"); + await this.page.getByLabel("Email").fill(user.email); await this.page.locator('input[type="password"]').fill(user.password); - await this.page.getByRole('button', { name: /log in/i }).click(); - await this.page.waitForURL('/profile'); + await this.page.getByRole("button", { name: /log in/i }).click(); + await this.page.waitForURL("/profile"); return user; } async logout() { - await this.page.goto('/auth/api/signout'); - await this.page.getByRole('button', { name: 'Sign out' }).click(); + await this.page.goto("/auth/api/signout"); + await this.page.getByRole("button", { name: "Sign out" }).click(); } async generateToken(user: User) { // the secret must match the provided for the api when built for e2e tests - return sign({ id: user.id }, 'mysupersecretfortests'); + return sign({ id: user.id }, "mysupersecretfortests"); } } From 74d67e213a535f8f8570aa997021769f2fc130ec Mon Sep 17 00:00:00 2001 From: alexeh Date: Thu, 10 Oct 2024 11:53:54 +0200 Subject: [PATCH 8/8] fix e2e tests --- e2e/package.json | 4 +- e2e/tests/auth/auth.spec.ts | 80 +++++++++++++------------------------ 2 files changed, 29 insertions(+), 55 deletions(-) diff --git a/e2e/package.json b/e2e/package.json index 7006a817..d99c4107 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -15,8 +15,6 @@ "pretest": "tsc && tsc-alias", "test": "playwright test -c ./dist/e2e", "test:ui": "pnpm pretest && playwright test --ui -c ./dist/e2e", - "codegen": "pnpm --filter api start:dev & pnpm --filter client dev & playwright codegen localhost:3000", - "codegen:prod": "pnpm --filter api run build & NODE_ENV=test pnpm --filter api run start:prod & NODE_ENV=test pnpm --filter client run build & NODE_ENV=test pnpm --filter client run start & playwright codegen localhost:3000", - "test2": "playwright test" + "codegen": "pnpm --filter api start:dev & pnpm --filter client dev & playwright codegen localhost:3000" } } diff --git a/e2e/tests/auth/auth.spec.ts b/e2e/tests/auth/auth.spec.ts index 11f6375d..cebb0ae6 100644 --- a/e2e/tests/auth/auth.spec.ts +++ b/e2e/tests/auth/auth.spec.ts @@ -5,22 +5,37 @@ import { User } from "@shared/entities/users/user.entity"; let testManager: E2eTestManager; let page: Page; -test.beforeAll(async ({ browser }) => { - page = await browser.newPage(); - testManager = await E2eTestManager.load(page); -}); +test.describe.configure({ mode: "serial" }); -test.beforeEach(async () => { - await testManager.clearDatabase(); -}); +test.describe("Auth", () => { + test.beforeAll(async ({ browser }) => { + page = await browser.newPage(); + testManager = await E2eTestManager.load(page); + }); -test.afterEach(async () => { - //await testManager.clearDatabase(); -}); + test.beforeEach(async () => { + await testManager.clearDatabase(); + }); -test.afterAll(async () => { - await testManager.logout(); - await testManager.close(); + test.afterEach(async () => { + // await testManager.clearDatabase(); + }); + + test.afterAll(async () => { + //await testManager.logout(); + await testManager.close(); + }); + + test("an user signs in successfully", async () => { + const user: Pick = { + email: "jhondoe@test.com", + password: "12345678", + partnerName: "admin", + }; + await testManager.mocks().createUser(user); + await testManager.login(user as User); + await expect(page.getByText(`Email: ${user.email}`)).toBeVisible(); + }); }); // test("an user signs up successfully", async ({ page }) => { @@ -49,42 +64,3 @@ test.afterAll(async () => { // user.email, // ); // }); - -test("an user signs in successfully", async ({ page }) => { - const user: Pick = { - email: "jhondoe@test.com", - password: "12345678", - partnerName: "admin", - }; - - await testManager.mocks().createUser(user); - await page.goto("/auth/signin"); - await page.getByLabel("Email").fill(user.email); - await page.locator('input[type="password"]').fill(user.password); - await page.getByRole("button", { name: /log in/i }).click(); -}); - -test("test", async ({ page }) => { - await page.goto("http://localhost:3000/"); - await page.getByRole("link", { name: "Sign in" }).click(); - await page.getByPlaceholder("Enter your email").click(); - await page.getByPlaceholder("Enter your email").fill("jhondoe@test.com"); - await page.getByPlaceholder("*******").click(); - await page.getByPlaceholder("*******").fill("12345678"); - await page.getByRole("button", { name: "Log in" }).click(); -}); -// test("test", async ({ page }) => { -// const user: Pick = { -// email: "jhondoe@test.com", -// password: "12345678", -// partnerName: "admin", -// }; -// await page.goto("http://localhost:3000/"); -// await page.getByRole("link", { name: "Sign in" }).click(); -// await page.getByPlaceholder("Enter your email").click(); -// await page.getByPlaceholder("Enter your email").fill(user.email); -// await page.getByPlaceholder("*******").click(); -// await page.getByPlaceholder("*******").fill(user.password); -// await page.getByRole("button", { name: "Log in" }).click(); -// await expect(page.getByText(`Email: ${user.email}`)).toBeVisible(); -// });