From 2898b433abc7df6ec36f9ef3e378c0124e9916cc Mon Sep 17 00:00:00 2001 From: Daniel Peinhopf <84123899+sevensolutions@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:18:12 +0100 Subject: [PATCH] #34: Improve e2e test setup. (#47) * #34: Improve e2e test setup. * #34: More tests --- .editorconfig | 19 ++ e2e/.gitignore | 1 + e2e/README.md | 13 ++ e2e/http.yml | 31 ---- e2e/package-lock.json | 29 ++- e2e/package.json | 3 +- e2e/playwright.config.ts | 9 +- e2e/{ => tests/dex}/dex.config.yml | 10 +- e2e/{ => tests/dex}/docker-compose.yml | 6 +- e2e/tests/dex/simple-login.spec.ts | 236 +++++++++++++++++++++++++ e2e/tests/simple-login.spec.ts | 44 ----- e2e/utils.ts | 18 ++ traefik-config.yml | 7 +- 13 files changed, 336 insertions(+), 90 deletions(-) create mode 100644 .editorconfig create mode 100644 e2e/README.md delete mode 100644 e2e/http.yml rename e2e/{ => tests/dex}/dex.config.yml (82%) rename e2e/{ => tests/dex}/docker-compose.yml (72%) create mode 100644 e2e/tests/dex/simple-login.spec.ts delete mode 100644 e2e/tests/simple-login.spec.ts create mode 100644 e2e/utils.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5e536ee --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig is awesome: https://editorconfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +[*.{yml,yaml}] +charset = utf-8 +indent_style = space +indent_size = 2 + +[*.ts] +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/e2e/.gitignore b/e2e/.gitignore index 68c5d18..50c233a 100644 --- a/e2e/.gitignore +++ b/e2e/.gitignore @@ -3,3 +3,4 @@ node_modules/ /playwright-report/ /blob-report/ /playwright/.cache/ +.http.yml \ No newline at end of file diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 0000000..b77390f --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,13 @@ +# E2E Tests + +## How to run the tests + +``` +npx playwright test +``` + +You can also run the tests and watch the browser by running: + +``` +npx playwright test --headed +``` diff --git a/e2e/http.yml b/e2e/http.yml deleted file mode 100644 index c7ca43d..0000000 --- a/e2e/http.yml +++ /dev/null @@ -1,31 +0,0 @@ -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 index 87fea52..fac59cc 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -10,7 +10,8 @@ "license": "ISC", "devDependencies": { "@playwright/test": "^1.49.1", - "@types/node": "^22.10.3" + "@types/node": "^22.10.3", + "docker-compose": "^1.1.0" } }, "node_modules/@playwright/test": { @@ -39,6 +40,19 @@ "undici-types": "~6.20.0" } }, + "node_modules/docker-compose": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.1.0.tgz", + "integrity": "sha512-VrkQJNafPQ5d6bGULW0P6KqcxSkv3ZU5Wn2wQA19oB71o7+55vQ9ogFe2MMeNbK+jc9rrKVy280DnHO5JLMWOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -92,6 +106,19 @@ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } } } } diff --git a/e2e/package.json b/e2e/package.json index 8885f85..596e951 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -11,6 +11,7 @@ "description": "", "devDependencies": { "@playwright/test": "^1.49.1", - "@types/node": "^22.10.3" + "@types/node": "^22.10.3", + "docker-compose": "^1.1.0" } } diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 37566ba..bc1c7f3 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -68,12 +68,5 @@ export default defineConfig({ // 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/dex.config.yml b/e2e/tests/dex/dex.config.yml similarity index 82% rename from e2e/dex.config.yml rename to e2e/tests/dex/dex.config.yml index 534ab4c..2925041 100644 --- a/e2e/dex.config.yml +++ b/e2e/tests/dex/dex.config.yml @@ -54,8 +54,16 @@ staticPasswords: hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" username: "admin" userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" +- email: "alice@example.com" + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" + username: "alice" + userID: "7134d97d-58b0-417d-936f-762661122b00" +- email: "bob@example.com" + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" + username: "bob" + userID: "4cbac28e-39f8-4d88-8408-079a97d60c9c" # Allow password grants with local users oauth2: skipApprovalScreen: true -# passwordConnector: local \ No newline at end of file +# passwordConnector: local diff --git a/e2e/docker-compose.yml b/e2e/tests/dex/docker-compose.yml similarity index 72% rename from e2e/docker-compose.yml rename to e2e/tests/dex/docker-compose.yml index 085b45a..edcd558 100644 --- a/e2e/docker-compose.yml +++ b/e2e/tests/dex/docker-compose.yml @@ -16,10 +16,10 @@ services: 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 + - "../../../traefik-config.yml:/etc/traefik/traefik.yml:ro" + - "../../..:/plugins-local/src/github.com/sevensolutions/traefik-oidc-auth:ro" + - "../../.http.yml:/etc/traefik/configs/http.yml:ro" # Will be generated by tests environment: PROVIDER_URL: http://dex:5556/dex CLIENT_ID: traefik diff --git a/e2e/tests/dex/simple-login.spec.ts b/e2e/tests/dex/simple-login.spec.ts new file mode 100644 index 0000000..4dd10c4 --- /dev/null +++ b/e2e/tests/dex/simple-login.spec.ts @@ -0,0 +1,236 @@ +import { test, expect, Page, Response } from "@playwright/test"; +import * as dockerCompose from "docker-compose"; +import { configureTraefik } from "../../utils"; + +//----------------------------------------------------------------------------- +// Test Setup +//----------------------------------------------------------------------------- + +test.use({ + ignoreHTTPSErrors: true +}); + +test.beforeAll("Starting traefik", async () => { + await configureTraefik(` +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 + + 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"] +`); + + await dockerCompose.upAll({ + cwd: __dirname, + log: true + }); +}); + +test.afterAll("Stopping traefik", async () => { + await dockerCompose.downAll({ + cwd: __dirname, + log: true + }); +}); + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +test("login http", async ({ page }) => { + await page.goto("http://localhost:9080"); + + const response = await login(page, "admin@example.com", "password", "http://localhost:9080"); + + expect(response.status()).toBe(200); +}); + +test("login https", async ({ page }) => { + await page.goto("https://localhost:9443"); + + const response = await login(page, "admin@example.com", "password", "https://localhost:9443"); + + expect(response.status()).toBe(200); +}); + +// Seems like logout is not supported by dey yet :( +// https://github.com/dexidp/dex/issues/1697 +// test("logout", async ({ page }) => { +// await page.goto("http://localhost:9080"); + +// const response = await login(page, "admin@example.com", "password", "http://localhost:9080"); + +// expect(response.status()).toBe(200); + +// await page.goto("http://localhost:9080/logout"); + +// }); + +test("test headers", async ({ page }) => { + await configureTraefik(` +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 + Headers: + - Name: "Authorization" + Value: "{{\`Bearer: {{ .accessToken }}\`}}" + - Name: "X-Static-Header" + Value: "42" + + routers: + whoami: + entryPoints: ["web"] + rule: "HostRegexp(\`.+\`)" + service: whoami + middlewares: ["oidc-auth@file"] +`); + + await page.goto("http://localhost:9080"); + + const response = await login(page, "admin@example.com", "password", "http://localhost:9080"); + + expect(response.status()).toBe(200); + + const authHeaderExists = await page.locator(`text=Authorization: Bearer: ey`).isVisible(); + expect(authHeaderExists).toBeTruthy(); + + const staticHeaderExists = await page.locator(`text=X-Static-Header: 42`).isVisible(); + expect(staticHeaderExists).toBeTruthy(); +}); + +test("test authorization", async ({ page }) => { + await configureTraefik(` +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 + Authorization: + AssertClaims: + - Name: email + AnyOf: ["admin@example.com", "alice@example.com"] + + routers: + whoami: + entryPoints: ["web"] + rule: "HostRegexp(\`.+\`)" + service: whoami + middlewares: ["oidc-auth@file"] +`); + + await page.goto("http://localhost:9080"); + + const response = await login(page, "alice@example.com", "password", "http://localhost:9080"); + + expect(response.status()).toBe(200); +}); + +test("test authorization failing", async ({ page }) => { + await configureTraefik(` +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 + Authorization: + AssertClaims: + - Name: email + AnyOf: ["admin@example.com", "alice@example.com"] + + routers: + whoami: + entryPoints: ["web"] + rule: "HostRegexp(\`.+\`)" + service: whoami + middlewares: ["oidc-auth@file"] +`); + + await page.goto("http://localhost:9080"); + + const response = await login(page, "bob@example.com", "password", "http://localhost:9080/oidc/callback**"); + + expect(response.status()).toBe(401); +}); + +//----------------------------------------------------------------------------- +// Helper functions +//----------------------------------------------------------------------------- + +async function login(page: Page, username: string, password: string, waitForUrl: string): Promise { + await page.waitForURL("http://localhost:5556/dex/auth**"); + + await page.locator(':text("Log in with Email")').click(); + + await page.locator("#login").fill(username); + await page.locator("#password").fill(password); + + const responsePromise = page.waitForResponse(waitForUrl); + + await page.locator('button:text("Login")').click(); + + const response = await responsePromise; + + return response; +} diff --git a/e2e/tests/simple-login.spec.ts b/e2e/tests/simple-login.spec.ts deleted file mode 100644 index 51d23c7..0000000 --- a/e2e/tests/simple-login.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -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/e2e/utils.ts b/e2e/utils.ts new file mode 100644 index 0000000..0865340 --- /dev/null +++ b/e2e/utils.ts @@ -0,0 +1,18 @@ +import * as fs from "fs"; +import * as path from "path"; + +export async function configureTraefik(yaml: string) { + const filePath = path.join(__dirname, ".http.yml"); + + let existing: string = ""; + + if (fs.existsSync(filePath)) + existing = fs.readFileSync(filePath).toString(); + + if (existing !== yaml) { + fs.writeFileSync(filePath, yaml); + + // Wait some time for traefik to reload the config + await new Promise(r => setTimeout(r, 2000)); + } +} diff --git a/traefik-config.yml b/traefik-config.yml index 56253a1..2e065ff 100644 --- a/traefik-config.yml +++ b/traefik-config.yml @@ -18,7 +18,12 @@ experimental: localPlugins: traefik-oidc-auth: moduleName: github.com/sevensolutions/traefik-oidc-auth - + # To test the released version + #plugins: + # traefik-oidc-auth: + # moduleName: "github.com/sevensolutions/traefik-oidc-auth" + # version: v0.5.0 + providers: file: filename: /etc/traefik/configs/http.yml