diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml deleted file mode 100644 index 6791901..0000000 --- a/.github/workflows/cypress.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Cypress Tests - -on: - push: - branches: [main, e2e-tests] - pull_request: - branches: [main, e2e-tests] - -jobs: - test: - timeout-minutes: 60 - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: "npm" - - - name: Install dependencies - run: npm ci - - - name: Build Next.js application - run: npm run build - env: - NEXT_PUBLIC_WORKOS_REDIRECT_URI: ${{ vars.NEXT_PUBLIC_WORKOS_REDIRECT_URI }} - - - name: Start Next.js application - run: npm start & - env: - # Runtime environment variables for server - WORKOS_API_KEY: ${{ secrets.WORKOS_API_KEY }} - WORKOS_CLIENT_ID: ${{ secrets.WORKOS_CLIENT_ID }} - WORKOS_COOKIE_PASSWORD: ${{ secrets.WORKOS_COOKIE_PASSWORD }} - PORT: 3000 - - - name: Wait for application to start - run: | - timeout 30 bash -c 'until curl -s http://localhost:3000 > /dev/null; do sleep 1; done' - - - name: Run Cypress tests - run: npm run test:cypress - env: - # Test environment variables - WORKOS_API_KEY: ${{ secrets.WORKOS_API_KEY }} - WORKOS_CLIENT_ID: ${{ secrets.WORKOS_CLIENT_ID }} - WORKOS_COOKIE_PASSWORD: ${{ secrets.WORKOS_COOKIE_PASSWORD }} - TEST_BASE_URL: http://localhost:3000 - TEST_EMAIL: ${{ secrets.TEST_EMAIL }} - TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }} - - - name: Upload Cypress Screenshots - uses: actions/upload-artifact@v4 - if: failure() - with: - name: cypress-screenshots - path: cypress/screenshots - retention-days: 1 - - - name: Upload Cypress Videos - uses: actions/upload-artifact@v4 - if: failure() - with: - name: cypress-videos - path: cypress/videos - retention-days: 1 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..c4c62f4 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,115 @@ +name: E2E Tests + +on: + push: + branches: [main, e2e-tests] + pull_request: + branches: [main, e2e-tests] + +jobs: + playwright: + name: Playwright Tests + timeout-minutes: 60 + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v1.41.2-jammy + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: npm ci + + - name: Build Next.js application + run: npm run build + env: + NEXT_PUBLIC_WORKOS_REDIRECT_URI: ${{ vars.NEXT_PUBLIC_WORKOS_REDIRECT_URI }} + + - name: Run Playwright tests + run: npx playwright test + env: + # Test environment variables + WORKOS_API_KEY: ${{ secrets.WORKOS_API_KEY }} + WORKOS_CLIENT_ID: ${{ secrets.WORKOS_CLIENT_ID }} + WORKOS_COOKIE_PASSWORD: ${{ secrets.WORKOS_COOKIE_PASSWORD }} + TEST_BASE_URL: http://localhost:3000 + TEST_EMAIL: ${{ secrets.TEST_EMAIL }} + TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }} + + - name: Upload Playwright Report + uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 1 + + - name: Upload Test Results + uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-test-results + path: test-results/ + retention-days: 1 + + cypress: + name: Cypress Tests + timeout-minutes: 60 + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Build Next.js application + run: npm run build + env: + NEXT_PUBLIC_WORKOS_REDIRECT_URI: ${{ vars.NEXT_PUBLIC_WORKOS_REDIRECT_URI }} + + - name: Start Next.js application + run: npm start & + env: + # Runtime environment variables for server + WORKOS_API_KEY: ${{ secrets.WORKOS_API_KEY }} + WORKOS_CLIENT_ID: ${{ secrets.WORKOS_CLIENT_ID }} + WORKOS_COOKIE_PASSWORD: ${{ secrets.WORKOS_COOKIE_PASSWORD }} + PORT: 3000 + + - name: Wait for application to start + run: | + timeout 30 bash -c 'until curl -s http://localhost:3000 > /dev/null; do sleep 1; done' + + - name: Run Cypress tests + run: npm run test:cypress + env: + # Test environment variables + WORKOS_API_KEY: ${{ secrets.WORKOS_API_KEY }} + WORKOS_CLIENT_ID: ${{ secrets.WORKOS_CLIENT_ID }} + WORKOS_COOKIE_PASSWORD: ${{ secrets.WORKOS_COOKIE_PASSWORD }} + TEST_BASE_URL: http://localhost:3000 + TEST_EMAIL: ${{ secrets.TEST_EMAIL }} + TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }} + + - name: Upload Cypress Screenshots + uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: cypress/screenshots + retention-days: 1 + + - name: Upload Cypress Videos + uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-videos + path: cypress/videos + retention-days: 1 diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml deleted file mode 100644 index 6ac360a..0000000 --- a/.github/workflows/playwright.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Playwright Tests - -on: - push: - branches: [main, e2e-tests] - pull_request: - branches: [main, e2e-tests] - -jobs: - test: - timeout-minutes: 60 - runs-on: ubuntu-latest - container: - image: mcr.microsoft.com/playwright:v1.55.0-jammy - options: --user 1001 - - steps: - - uses: actions/checkout@v4 - - - name: Install dependencies - run: npm ci - - - name: Build Next.js application - run: npm run build - env: - NEXT_PUBLIC_WORKOS_REDIRECT_URI: ${{ vars.NEXT_PUBLIC_WORKOS_REDIRECT_URI }} - - - name: Run Playwright tests - run: npx playwright test - env: - # Test environment variables - WORKOS_API_KEY: ${{ secrets.WORKOS_API_KEY }} - WORKOS_CLIENT_ID: ${{ secrets.WORKOS_CLIENT_ID }} - WORKOS_COOKIE_PASSWORD: ${{ secrets.WORKOS_COOKIE_PASSWORD }} - TEST_BASE_URL: http://localhost:3000 - TEST_EMAIL: ${{ secrets.TEST_EMAIL }} - TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }} - - - name: Upload Playwright Report - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: playwright-report - path: playwright-report/ - retention-days: 1 - - - name: Upload Test Results - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: test-results - path: test-results/ - retention-days: 1 diff --git a/next.config.js b/next.config.js index 658404a..ccd7735 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,8 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + experimental: { + testProxy: process.env.NODE_ENV !== "production", + }, +}; module.exports = nextConfig; diff --git a/package-lock.json b/package-lock.json index 48140ca..36f04cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "react-dom": "^18" }, "devDependencies": { - "@playwright/test": "1.55.0", + "@playwright/test": "1.41.2", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", @@ -25,6 +25,7 @@ "dotenv": "^17.2.1", "eslint": "^8.57.0", "eslint-config-next": "^14.2.5", + "jose": "5.2.3", "relative-deps": "^1.0.7", "typescript": "^5" } @@ -1006,18 +1007,19 @@ } }, "node_modules/@playwright/test": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", - "integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==", + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", + "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==", + "deprecated": "Please update to the latest version of Playwright to test up-to-date browsers.", "devOptional": true, "dependencies": { - "playwright": "1.55.0" + "playwright": "1.41.2" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=16" } }, "node_modules/@radix-ui/colors": { @@ -2838,10 +2840,18 @@ "react-dom": "^18.0 || ^19.0.0" } }, + "node_modules/@workos-inc/authkit-nextjs/node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@workos-inc/node": { - "version": "7.69.1", - "resolved": "https://registry.npmjs.org/@workos-inc/node/-/node-7.69.1.tgz", - "integrity": "sha512-ml2TqUHjUVkubq4EnIIM1O1g+eR0ctKnpdHUJntG/1PuVt64CfntJrAUi/5ePgR4d12EeXunHyjOTK75k+f9Ww==", + "version": "7.72.2", + "resolved": "https://registry.npmjs.org/@workos-inc/node/-/node-7.72.2.tgz", + "integrity": "sha512-yecC6eP1OhPwg37GK4PJG8EUk4zs6WYjlRuCRKbcJuCamZxgJoYx2vRctoqr/lHSJlQyWKTUWNhp1od0dTERPQ==", "dependencies": { "iron-session": "~6.3.1", "jose": "~5.6.3", @@ -3747,9 +3757,9 @@ } }, "node_modules/ci-info": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", - "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", "dev": true, "funding": [ { @@ -4263,9 +4273,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.15", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.15.tgz", - "integrity": "sha512-MC+DfnSWiM9APs7fpiurHGCoeIx0Gdl6QZBy+5lu8MbYKN5FZEXqOgrundfibdfhGZ15o9hzmZ2xJjZnbvgKXQ==", + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", "dev": true }, "node_modules/debug": { @@ -4437,9 +4447,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", - "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "dev": true, "engines": { "node": ">=12" @@ -6788,9 +6798,10 @@ } }, "node_modules/jose": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.3.tgz", - "integrity": "sha512-egLIoYSpcd+QUF+UHgobt5YzI2Pkw/H39ou9suW687MY6PmCwPmkNV/4TNjn1p2tX5xO3j0d0sq5hiYE24bSlg==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.3.tgz", + "integrity": "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -8010,33 +8021,33 @@ } }, "node_modules/playwright": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", - "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz", + "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==", "devOptional": true, "dependencies": { - "playwright-core": "1.55.0" + "playwright-core": "1.41.2" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=16" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", - "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz", + "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==", "devOptional": true, "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=18" + "node": ">=16" } }, "node_modules/pluralize": { diff --git a/package.json b/package.json index b285cfc..d0b3ca8 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "start-local-dev": "rm -rf .next && npm run prepare && npm run dev", "lint": "next lint", "prepare": "relative-deps", + "playwright:install": "playwright install", "test:playwright": "playwright test", "test:playwright:ui": "playwright test --ui", "test:playwright:debug": "playwright test --debug", @@ -26,7 +27,7 @@ "react-dom": "^18" }, "devDependencies": { - "@playwright/test": "1.55.0", + "@playwright/test": "1.41.2", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", @@ -35,6 +36,7 @@ "dotenv": "^17.2.1", "eslint": "^8.57.0", "eslint-config-next": "^14.2.5", + "jose": "5.2.3", "relative-deps": "^1.0.7", "typescript": "^5" }, diff --git a/tests/authenticated-flows.proxy.spec.ts b/tests/authenticated-flows.proxy.spec.ts new file mode 100644 index 0000000..473a554 --- /dev/null +++ b/tests/authenticated-flows.proxy.spec.ts @@ -0,0 +1,99 @@ +import { + test as baseTest, + expect, +} from "next/experimental/testmode/playwright"; +import * as jose from "jose"; +import { AccessToken, UserResponse } from "@workos-inc/node"; + +const DEFAULT_REFRESH_TOKEN = "1234567890"; + +const DEFAULT_USER: UserResponse = { + first_name: "John", + last_name: "Doe", + email: "john.doe@example.com", + email_verified: true, + profile_picture_url: "https://example.com/profile.jpg", + object: "user", + id: "12345", + created_at: "2025-01-01T00:00:00.000Z", + updated_at: "2025-01-01T00:00:00.000Z", + external_id: "12345", + metadata: {}, + last_sign_in_at: "2025-01-01T00:00:00.000Z", + locale: "en-US", +}; + +const DEFAULT_ACCESS_TOKEN: AccessToken = { + sid: "123456", +}; + +const test = baseTest.extend<{ + user: UserResponse; + accessTokenClaims: AccessToken; + refreshToken: string; + _login: void; +}>({ + user: [DEFAULT_USER, { option: true }], + accessTokenClaims: [DEFAULT_ACCESS_TOKEN, { option: true }], + refreshToken: [DEFAULT_REFRESH_TOKEN, { option: true }], + _login: [ + async ({ page, user, refreshToken, accessTokenClaims, next }, use) => { + const keyPair = await jose.generateKeyPair("RS256"); + const accessToken = await new jose.SignJWT({ ...accessTokenClaims }) + .setProtectedHeader({ alg: "RS256" }) + .setExpirationTime("1h") + .setIssuedAt("-1h") + .sign(keyPair.privateKey); + + next.onFetch(async (request) => { + // request to the refresh token endpoint + if ( + request.url.includes("api.workos.com/user_management/authenticate") + ) { + return Response.json({ accessToken, refreshToken, user }); + } + + // pass through + return "continue"; + }); + + const baseURL = process.env.TEST_BASE_URL; + + // login by calling our test endpoint which calls saveSession + await page.request.post(`${baseURL}/api/test/set-session`, { + data: { user, accessToken, refreshToken }, + headers: { "Content-Type": "application/json" }, + }); + + await use(); + }, + { auto: true }, + ], +}); + +test.use({ nextOptions: { fetchLoopback: true } }); + +test.describe("Authenticated User Flows", async () => { + test("homepage shows authenticated state", async ({ page }) => { + await page.goto("/"); + + // Should see welcome message for authenticated user + await expect(page.getByText(/welcome back/i)).toBeVisible(); + + // Should see account navigation + await expect( + page.getByRole("link", { name: /view account/i }) + ).toBeVisible(); + + // Should see sign out button + // There are multiple sign out buttons, so we need to make sure we see at least one + await expect( + page.getByRole("button", { name: /sign out/i }).first() + ).toBeVisible(); + + // Should NOT see sign in button + await expect( + page.getByRole("link", { name: /sign in/i }) + ).not.toBeVisible(); + }); +}); diff --git a/tests/authkit.spec.ts b/tests/authkit.spec.ts new file mode 100644 index 0000000..dcdac94 --- /dev/null +++ b/tests/authkit.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from "./fixtures"; + +test.describe("AuthKit - Authenticated User Flows", () => { + test("Can log in", async ({ page, context }) => { + await page.goto("/"); + + // Click sign in with authkit + await page.getByRole("link", { name: /sign in with authkit/i }).click(); + + // Should see login form + await expect( + page.getByLabel(/email/i).or(page.getByRole("textbox").first()) + ).toBeVisible(); + + // Fill in email and password + await page.getByLabel(/email/i).fill(process.env.TEST_EMAIL!); + + // click continue + await page.getByRole("button", { name: /continue/i }).click(); + + // Should see password input + await expect(page.getByLabel(/password/i)).toBeVisible(); + + // Fill in password + await page.getByLabel(/password/i).fill(process.env.TEST_PASSWORD!); + + // click sign in + await page.getByRole("button", { name: /sign in/i }).click(); + + // Should see welcome message for authenticated user + await expect(page.getByText(/welcome back/i)).toBeVisible(); + }); +});