diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 0000000..db379c1 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,35 @@ +name: E2E Tests +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./e2e + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run Playwright tests + run: npx playwright test + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: ./e2e/playwright-report/ + retention-days: 30 diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 0000000..68c5d18 --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/dex.config.yml b/e2e/dex.config.yml new file mode 100644 index 0000000..534ab4c --- /dev/null +++ b/e2e/dex.config.yml @@ -0,0 +1,61 @@ +# The base path of dex and the external name of the OpenID Connect service. +# This is the canonical URL that all clients MUST use to refer to dex. If a +# path is provided, dex's HTTP service will listen at a non-root URL. +issuer: http://localhost:5556/dex + +storage: + type: sqlite3 + config: + file: /tmp/dex.db + +# Configuration for the HTTP endpoints. +web: + http: 0.0.0.0:5556 + +# Configuration for dex appearance +# frontend: +# issuer: dex +# logoURL: theme/logo.png +# dir: web/ +# theme: light + +# Options for controlling the logger. +# logger: +# level: "debug" +# format: "text" # can also be "json" + +staticClients: +- id: traefik + redirectURIs: + - "http://localhost:9080/oidc/callback" + - "https://localhost:9443/oidc/callback" + name: 'Traefik App' + secret: ZXhhbXBsZS1hcHAtc2VjcmV0 +# - id: example-device-client +# redirectURIs: +# - /device/callback +# name: 'Static Client for Device Flow' +# public: true +connectors: +- type: mockCallback + id: mock + name: Example + +# Let dex keep a list of passwords which can be used to login to dex. +enablePasswordDB: true + +# A static list of passwords to login the end user. By identifying here, dex +# won't look in its underlying storage for passwords. +# +# If this option isn't chosen users may be added through the gRPC API. +staticPasswords: +- email: "admin@example.com" + # bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2) + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" + username: "admin" + userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" + +# Allow password grants with local users +oauth2: + skipApprovalScreen: true +# passwordConnector: local \ No newline at end of file diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml new file mode 100644 index 0000000..085b45a --- /dev/null +++ b/e2e/docker-compose.yml @@ -0,0 +1,29 @@ +services: + dex: + image: "ghcr.io/dexidp/dex:latest-alpine" + restart: unless-stopped + ports: + - "5556:5556" + volumes: + - "./dex.config.yml:/etc/dex/config.docker.yaml:ro" + + traefik: + image: "traefik:v3.1.4" + ports: + - "9080:80" + - "9443:443" + - "8080:8080" + extra_hosts: + - "localhost:172.17.0.1" # To make OIDC discovery work correctly + volumes: + - "../traefik-config.yml:/etc/traefik/traefik.yml:ro" + - "./http.yml:/etc/traefik/configs/http.yml:ro" + - "..:/plugins-local/src/github.com/sevensolutions/traefik-oidc-auth:ro" + - /var/run/docker.sock:/var/run/docker.sock + environment: + PROVIDER_URL: http://dex:5556/dex + CLIENT_ID: traefik + CLIENT_SECRET: ZXhhbXBsZS1hcHAtc2VjcmV0 + + whoami: + image: "traefik/whoami:latest" diff --git a/e2e/http.yml b/e2e/http.yml new file mode 100644 index 0000000..c7ca43d --- /dev/null +++ b/e2e/http.yml @@ -0,0 +1,31 @@ +http: + services: + whoami: + loadBalancer: + servers: + - url: http://whoami:80 + + middlewares: + oidc-auth: + plugin: + traefik-oidc-auth: + LogLevel: DEBUG + Provider: + UrlEnv: "PROVIDER_URL" + ClientIdEnv: "CLIENT_ID" + ClientSecretEnv: "CLIENT_SECRET" + UsePkce: false + Scopes: ["openid", "profile", "email"] + + routers: + whoami: + entryPoints: ["web"] + rule: "HostRegexp(`.+`)" + service: whoami + middlewares: ["oidc-auth@file"] + whoami-secure: + entryPoints: ["websecure"] + tls: {} + rule: "HostRegexp(`.+`)" + service: whoami + middlewares: ["oidc-auth@file"] diff --git a/e2e/package-lock.json b/e2e/package-lock.json new file mode 100644 index 0000000..87fea52 --- /dev/null +++ b/e2e/package-lock.json @@ -0,0 +1,97 @@ +{ + "name": "e2e", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "e2e", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.49.1", + "@types/node": "^22.10.3" + } + }, + "node_modules/@playwright/test": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", + "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.3.tgz", + "integrity": "sha512-DifAyw4BkrufCILvD3ucnuN8eydUfc/C1GlyrnI+LK6543w5/L3VeVgf05o3B4fqSXP1dKYLOZsKfutpxPzZrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", + "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000..8885f85 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,16 @@ +{ + "name": "e2e", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "playwright test" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@playwright/test": "^1.49.1", + "@types/node": "^22.10.3" + } +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 0000000..37566ba --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: false, + /* 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. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: '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: 'http://127.0.0.1:3000', + + /* 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'] }, + }, + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'docker compose up', + url: 'http://localhost:9080', + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/e2e/tests/simple-login.spec.ts b/e2e/tests/simple-login.spec.ts new file mode 100644 index 0000000..51d23c7 --- /dev/null +++ b/e2e/tests/simple-login.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test'; + +test.use({ + ignoreHTTPSErrors: true, + //headless: false +}); + +test('login http', async ({ page }) => { + await page.goto("http://localhost:9080"); + + await page.waitForURL("http://localhost:5556/dex/auth**"); + + await page.locator(':text("Log in with Email")').click(); + + await page.locator("#login").fill("admin@example.com"); + await page.locator("#password").fill("password"); + + const responsePromise = page.waitForResponse("http://localhost:9080"); + + await page.locator('button:text("Login")').click(); + + const response = await responsePromise; + + expect(response.status()).toBe(200); +}); + +test('login https', async ({ page }) => { + await page.goto("https://localhost:9443"); + + await page.waitForURL("http://localhost:5556/dex/auth**"); + + await page.locator(':text("Log in with Email")').click(); + + await page.locator("#login").fill("admin@example.com"); + await page.locator("#password").fill("password"); + + const responsePromise = page.waitForResponse("https://localhost:9443"); + + await page.locator('button:text("Login")').click(); + + const response = await responsePromise; + + expect(response.status()).toBe(200); +}); diff --git a/traefik-config.yml b/traefik-config.yml index 5957175..56253a1 100644 --- a/traefik-config.yml +++ b/traefik-config.yml @@ -5,6 +5,8 @@ global: entryPoints: web: address: :80 + websecure: + address: :443 # Enable API and dashboard api: