diff --git a/tests/.prettierignore b/tests/.prettierignore new file mode 100644 index 00000000..30151ad1 --- /dev/null +++ b/tests/.prettierignore @@ -0,0 +1,6 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock + +.output/ \ No newline at end of file diff --git a/tests/.prettierrc b/tests/.prettierrc new file mode 100644 index 00000000..ff2677ef --- /dev/null +++ b/tests/.prettierrc @@ -0,0 +1,6 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100 +} diff --git a/tests/data.ts b/tests/data.ts index ada131e8..3970602e 100644 --- a/tests/data.ts +++ b/tests/data.ts @@ -1,134 +1,134 @@ export const users = { - tim: { - id: 'f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e', - firstname: 'Tim', - lastname: 'Cook', - email: 'tim.cook@test.com', - username: 'tim', - }, - craig: { - id: '1cd19686-f9a6-43f4-a41f-14a0bf5b4036', - firstname: 'Craig', - lastname: 'Federighi', - email: 'craig.federighi@test.com', - username: 'craig', - }, - steve: { - firstname: 'Steve', - lastname: 'Jobs', - email: 'steve.jobs@test.com', - username: 'steve', - }, + tim: { + id: 'f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e', + firstname: 'Tim', + lastname: 'Cook', + email: 'tim.cook@test.com', + username: 'tim' + }, + craig: { + id: '1cd19686-f9a6-43f4-a41f-14a0bf5b4036', + firstname: 'Craig', + lastname: 'Federighi', + email: 'craig.federighi@test.com', + username: 'craig' + }, + steve: { + firstname: 'Steve', + lastname: 'Jobs', + email: 'steve.jobs@test.com', + username: 'steve' + } }; export const oidcClients = { - nextcloud: { - id: '3654a746-35d4-4321-ac61-0bdcff2b4055', - name: 'Nextcloud', - callbackUrl: 'http://nextcloud/auth/callback', - logoutCallbackUrl: 'http://nextcloud/auth/logout/callback', - secret: 'w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY', - }, - immich: { - id: '606c7782-f2b1-49e5-8ea9-26eb1b06d018', - name: 'Immich', - callbackUrl: 'http://immich/auth/callback', - secret: 'PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x', - }, - federated: { - id: 'c48232ff-ff65-45ed-ae96-7afa8a9b443b', - name: 'Federated', - callbackUrl: 'http://federated/auth/callback', - federatedJWT: { - issuer: 'https://external-idp.local', - audience: 'api://PocketID', - subject: 'c48232ff-ff65-45ed-ae96-7afa8a9b443b', - }, - accessCodes: ['federated'], - }, - pingvinShare: { - name: 'Pingvin Share', - callbackUrl: 'http://pingvin.share/auth/callback', - secondCallbackUrl: 'http://pingvin.share/auth/callback2', - }, + nextcloud: { + id: '3654a746-35d4-4321-ac61-0bdcff2b4055', + name: 'Nextcloud', + callbackUrl: 'http://nextcloud/auth/callback', + logoutCallbackUrl: 'http://nextcloud/auth/logout/callback', + secret: 'w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY' + }, + immich: { + id: '606c7782-f2b1-49e5-8ea9-26eb1b06d018', + name: 'Immich', + callbackUrl: 'http://immich/auth/callback', + secret: 'PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x' + }, + federated: { + id: 'c48232ff-ff65-45ed-ae96-7afa8a9b443b', + name: 'Federated', + callbackUrl: 'http://federated/auth/callback', + federatedJWT: { + issuer: 'https://external-idp.local', + audience: 'api://PocketID', + subject: 'c48232ff-ff65-45ed-ae96-7afa8a9b443b' + }, + accessCodes: ['federated'] + }, + pingvinShare: { + name: 'Pingvin Share', + callbackUrl: 'http://pingvin.share/auth/callback', + secondCallbackUrl: 'http://pingvin.share/auth/callback2' + } }; export const userGroups = { - developers: { - id: '4110f814-56f1-4b28-8998-752b69bc97c0e', - friendlyName: 'Developers', - name: 'developers', - }, - designers: { - id: 'adab18bf-f89d-4087-9ee1-70ff15b48211', - friendlyName: 'Designers', - name: 'designers', - }, - humanResources: { - friendlyName: 'Human Resources', - name: 'human_resources', - }, + developers: { + id: '4110f814-56f1-4b28-8998-752b69bc97c0e', + friendlyName: 'Developers', + name: 'developers' + }, + designers: { + id: 'adab18bf-f89d-4087-9ee1-70ff15b48211', + friendlyName: 'Designers', + name: 'designers' + }, + humanResources: { + friendlyName: 'Human Resources', + name: 'human_resources' + } }; export const oneTimeAccessTokens = [ - { token: 'HPe6k6uiDRRVuAQV', expired: false }, - { token: 'YCGDtftvsvYWiXd0', expired: true }, + { token: 'HPe6k6uiDRRVuAQV', expired: false }, + { token: 'YCGDtftvsvYWiXd0', expired: true } ]; export const apiKeys = [ - { - id: '5f1fa856-c164-4295-961e-175a0d22d725', - key: '6c34966f57ef2bb7857649aff0e7ab3ad67af93c846342ced3f5a07be8706c20', - name: 'Test API Key', - }, + { + id: '5f1fa856-c164-4295-961e-175a0d22d725', + key: '6c34966f57ef2bb7857649aff0e7ab3ad67af93c846342ced3f5a07be8706c20', + name: 'Test API Key' + } ]; export const refreshTokens = [ - { - token: 'ou87UDg249r1StBLYkMEqy9TXDbV5HmGuDpMcZDo', - clientId: oidcClients.nextcloud.id, - userId: 'f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e', - expired: false, - }, - { - token: 'X4vqwtRyCUaq51UafHea4Fsg8Km6CAns6vp3tuX4', - clientId: oidcClients.nextcloud.id, - userId: 'f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e', - expired: true, - }, + { + token: 'ou87UDg249r1StBLYkMEqy9TXDbV5HmGuDpMcZDo', + clientId: oidcClients.nextcloud.id, + userId: 'f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e', + expired: false + }, + { + token: 'X4vqwtRyCUaq51UafHea4Fsg8Km6CAns6vp3tuX4', + clientId: oidcClients.nextcloud.id, + userId: 'f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e', + expired: true + } ]; export const signupTokens = { - valid: { - id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', - token: 'VALID1234567890A', - expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - usageLimit: 1, - usageCount: 0, - createdAt: new Date().toISOString(), - }, - partiallyUsed: { - id: 'b2c3d4e5-f6g7-8901-bcde-f12345678901', - token: 'PARTIAL567890ABC', - expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), - usageLimit: 5, - usageCount: 2, - createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), - }, - expired: { - id: 'c3d4e5f6-g7h8-9012-cdef-123456789012', - token: 'EXPIRED34567890B', - expiresAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), - usageLimit: 3, - usageCount: 1, - createdAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), - }, - fullyUsed: { - id: 'd4e5f6g7-h8i9-0123-def0-234567890123', - token: 'FULLYUSED567890C', - expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), - usageLimit: 1, - usageCount: 1, - createdAt: new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString(), - }, + valid: { + id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + token: 'VALID1234567890A', + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + usageLimit: 1, + usageCount: 0, + createdAt: new Date().toISOString() + }, + partiallyUsed: { + id: 'b2c3d4e5-f6g7-8901-bcde-f12345678901', + token: 'PARTIAL567890ABC', + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), + usageLimit: 5, + usageCount: 2, + createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString() + }, + expired: { + id: 'c3d4e5f6-g7h8-9012-cdef-123456789012', + token: 'EXPIRED34567890B', + expiresAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), + usageLimit: 3, + usageCount: 1, + createdAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString() + }, + fullyUsed: { + id: 'd4e5f6g7-h8i9-0123-def0-234567890123', + token: 'FULLYUSED567890C', + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + usageLimit: 1, + usageCount: 1, + createdAt: new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString() + } }; diff --git a/tests/package-lock.json b/tests/package-lock.json index 1ddd7698..32fd9bc8 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -1,118 +1,116 @@ { - "name": "tests", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "devDependencies": { - "@playwright/test": "^1.52.0", - "@types/node": "^22.15.21", - "dotenv": "^16.5.0", - "jose": "^6.0.11" - } - }, - "node_modules/@playwright/test": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", - "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.52.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@types/node": { - "version": "22.15.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", - "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "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/jose": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz", - "integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/playwright": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", - "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.52.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", - "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - } - } + "name": "tests", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@playwright/test": "^1.52.0", + "@types/node": "^22.15.21", + "dotenv": "^16.5.0", + "jose": "^6.0.11", + "prettier": "^3.6.2" + } + }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.15.21", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/jose": { + "version": "6.0.11", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/playwright": { + "version": "1.52.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "dev": true, + "license": "MIT" + } + } } diff --git a/tests/package.json b/tests/package.json index 88485685..3137df4b 100644 --- a/tests/package.json +++ b/tests/package.json @@ -1,9 +1,14 @@ { - "type": "module", - "devDependencies": { - "@playwright/test": "^1.52.0", - "@types/node": "^22.15.21", - "jose": "^6.0.11", - "dotenv": "^16.5.0" - } + "type": "module", + "scripts": { + "test": "playwright test", + "format": "prettier --write ." + }, + "devDependencies": { + "@playwright/test": "^1.52.0", + "@types/node": "^22.15.21", + "dotenv": "^16.5.0", + "jose": "^6.0.11", + "prettier": "^3.6.2" + } } diff --git a/tests/playwright.config.ts b/tests/playwright.config.ts index 48ddf2ef..0b0cc460 100644 --- a/tests/playwright.config.ts +++ b/tests/playwright.config.ts @@ -1,31 +1,31 @@ -import { defineConfig, devices } from "@playwright/test"; -import "dotenv/config"; +import { defineConfig, devices } from '@playwright/test'; +import 'dotenv/config'; /** * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - outputDir: "./.output", - timeout: 10000, - testDir: "./specs", - fullyParallel: false, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 1 : 0, - workers: 1, - reporter: process.env.CI - ? [["html", { outputFolder: ".report" }], ["github"]] - : [["line"], ["html", { open: "never", outputFolder: ".report" }]], - use: { - baseURL: process.env.APP_URL ?? "http://localhost:1411", - video: "retain-on-failure", - trace: "on-first-retry", - }, - projects: [ - { name: "setup", testMatch: /.*\.setup\.ts/ }, - { - name: "chromium", - use: { ...devices["Desktop Chrome"], storageState: ".auth/user.json" }, - dependencies: ["setup"], - }, - ], + outputDir: './.output', + timeout: 10000, + testDir: './specs', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: process.env.CI + ? [['html', { outputFolder: '.report' }], ['github']] + : [['line'], ['html', { open: 'never', outputFolder: '.report' }]], + use: { + baseURL: process.env.APP_URL ?? 'http://localhost:1411', + video: 'retain-on-failure', + trace: 'on-first-retry' + }, + projects: [ + { name: 'setup', testMatch: /.*\.setup\.ts/ }, + { + name: 'chromium', + use: { ...devices['Desktop Chrome'], storageState: '.auth/user.json' }, + dependencies: ['setup'] + } + ] }); diff --git a/tests/setup/docker-compose-postgres.yml b/tests/setup/docker-compose-postgres.yml index 44f95975..ccd18b15 100644 --- a/tests/setup/docker-compose-postgres.yml +++ b/tests/setup/docker-compose-postgres.yml @@ -11,7 +11,7 @@ services: - POSTGRES_PASSWORD=postgres - POSTGRES_DB=pocket-id healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] + test: ['CMD-SHELL', 'pg_isready -U postgres'] interval: 5s timeout: 5s retries: 5 @@ -21,4 +21,4 @@ services: service: pocket-id depends_on: postgres: - condition: service_healthy \ No newline at end of file + condition: service_healthy diff --git a/tests/setup/docker-compose.yml b/tests/setup/docker-compose.yml index 22d1c267..635b975c 100644 --- a/tests/setup/docker-compose.yml +++ b/tests/setup/docker-compose.yml @@ -11,11 +11,11 @@ services: pocket-id: image: pocket-id:test ports: - - "1411:1411" + - '1411:1411' environment: - - APP_ENV=test + - APP_ENV=test build: args: - BUILD_TAGS=e2etest context: ../.. - dockerfile: Dockerfile \ No newline at end of file + dockerfile: Dockerfile diff --git a/tests/specs/account-settings.spec.ts b/tests/specs/account-settings.spec.ts index 2cc20acb..0c6fd25a 100644 --- a/tests/specs/account-settings.spec.ts +++ b/tests/specs/account-settings.spec.ts @@ -1,135 +1,116 @@ -import test, { expect } from "@playwright/test"; -import { users } from "../data"; -import authUtil from "../utils/auth.util"; -import { cleanupBackend } from "../utils/cleanup.util"; -import passkeyUtil from "../utils/passkey.util"; +import test, { expect } from '@playwright/test'; +import { users } from '../data'; +import authUtil from '../utils/auth.util'; +import { cleanupBackend } from '../utils/cleanup.util'; +import passkeyUtil from '../utils/passkey.util'; test.beforeEach(cleanupBackend); -test("Update account details", async ({ page }) => { - await page.goto("/settings/account"); +test('Update account details', async ({ page }) => { + await page.goto('/settings/account'); - await page.getByLabel("First name").fill("Timothy"); - await page.getByLabel("Last name").fill("Apple"); - await page.getByLabel("Email").fill("timothy.apple@test.com"); - await page.getByLabel("Username").fill("timothy"); - await page.getByRole("button", { name: "Save" }).click(); + await page.getByLabel('First name').fill('Timothy'); + await page.getByLabel('Last name').fill('Apple'); + await page.getByLabel('Email').fill('timothy.apple@test.com'); + await page.getByLabel('Username').fill('timothy'); + await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "Account details updated successfully" - ); + await expect(page.locator('[data-type="success"]')).toHaveText( + 'Account details updated successfully' + ); }); -test("Update account details fails with already taken email", async ({ - page, -}) => { - await page.goto("/settings/account"); +test('Update account details fails with already taken email', async ({ page }) => { + await page.goto('/settings/account'); - await page.getByLabel("Email").fill(users.craig.email); + await page.getByLabel('Email').fill(users.craig.email); - await page.getByRole("button", { name: "Save" }).click(); + await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.locator('[data-type="error"]')).toHaveText( - "Email is already in use" - ); + await expect(page.locator('[data-type="error"]')).toHaveText('Email is already in use'); }); -test("Update account details fails with already taken username", async ({ - page, -}) => { - await page.goto("/settings/account"); +test('Update account details fails with already taken username', async ({ page }) => { + await page.goto('/settings/account'); - await page.getByLabel("Username").fill(users.craig.username); + await page.getByLabel('Username').fill(users.craig.username); - await page.getByRole("button", { name: "Save" }).click(); + await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.locator('[data-type="error"]')).toHaveText( - "Username is already in use" - ); + await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use'); }); -test("Change Locale", async ({ page }) => { - await page.goto("/settings/account"); +test('Change Locale', async ({ page }) => { + await page.goto('/settings/account'); - await page.getByLabel("Select Locale").click(); - await page.getByRole("option", { name: "Nederlands" }).click(); + await page.getByLabel('Select Locale').click(); + await page.getByRole('option', { name: 'Nederlands' }).click(); - // Check if th language heading now says 'Taal' instead of 'Language' - await expect(page.getByText("Taal", { exact: true })).toBeVisible(); + // Check if th language heading now says 'Taal' instead of 'Language' + await expect(page.getByText('Taal', { exact: true })).toBeVisible(); - // Check if the validation messages are translated because they are provided by Zod - await page.getByRole("textbox", { name: "Voornaam" }).fill(""); - await page.getByRole("button", { name: "Opslaan" }).click(); - await expect(page.getByText("Te kort: verwacht dat string")).toBeVisible(); + // Check if the validation messages are translated because they are provided by Zod + await page.getByRole('textbox', { name: 'Voornaam' }).fill(''); + await page.getByRole('button', { name: 'Opslaan' }).click(); + await expect(page.getByText('Te kort: verwacht dat string')).toBeVisible(); - // Clear all cookies and sign in again to check if the language is still set to Dutch - await page.context().clearCookies(); - await authUtil.authenticate(page); + // Clear all cookies and sign in again to check if the language is still set to Dutch + await page.context().clearCookies(); + await authUtil.authenticate(page); - await expect(page.getByText("Taal", { exact: true })).toBeVisible(); + await expect(page.getByText('Taal', { exact: true })).toBeVisible(); - await page.getByRole("textbox", { name: "Voornaam" }).fill(""); - await page.getByRole("button", { name: "Opslaan" }).click(); - await expect(page.getByText("Te kort: verwacht dat string")).toBeVisible(); + await page.getByRole('textbox', { name: 'Voornaam' }).fill(''); + await page.getByRole('button', { name: 'Opslaan' }).click(); + await expect(page.getByText('Te kort: verwacht dat string')).toBeVisible(); }); -test("Add passkey to an account", async ({ page }) => { - await page.goto("/settings/account"); +test('Add passkey to an account', async ({ page }) => { + await page.goto('/settings/account'); - await (await passkeyUtil.init(page)).addPasskey("timNew"); + await (await passkeyUtil.init(page)).addPasskey('timNew'); - await page.getByRole("button", { name: "Add Passkey" }).click(); + await page.getByRole('button', { name: 'Add Passkey' }).click(); - await page.getByLabel("Name", { exact: true }).fill("Test Passkey"); - await page - .getByLabel("Name Passkey") - .getByRole("button", { name: "Save" }) - .click(); + await page.getByLabel('Name', { exact: true }).fill('Test Passkey'); + await page.getByLabel('Name Passkey').getByRole('button', { name: 'Save' }).click(); - await expect(page.getByText("Test Passkey")).toBeVisible(); + await expect(page.getByText('Test Passkey')).toBeVisible(); }); -test("Rename passkey", async ({ page }) => { - await page.goto("/settings/account"); +test('Rename passkey', async ({ page }) => { + await page.goto('/settings/account'); - await page.getByLabel("Rename").first().click(); + await page.getByLabel('Rename').first().click(); - await page.getByLabel("Name", { exact: true }).fill("Renamed Passkey"); - await page - .getByLabel("Name Passkey") - .getByRole("button", { name: "Save" }) - .click(); + await page.getByLabel('Name', { exact: true }).fill('Renamed Passkey'); + await page.getByLabel('Name Passkey').getByRole('button', { name: 'Save' }).click(); - await expect(page.getByText("Renamed Passkey")).toBeVisible(); + await expect(page.getByText('Renamed Passkey')).toBeVisible(); }); -test("Delete passkey from account", async ({ page }) => { - await page.goto("/settings/account"); +test('Delete passkey from account', async ({ page }) => { + await page.goto('/settings/account'); - await page.getByLabel("Delete").first().click(); - await page.getByText("Delete", { exact: true }).click(); + await page.getByLabel('Delete').first().click(); + await page.getByText('Delete', { exact: true }).click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "Passkey deleted successfully" - ); + await expect(page.locator('[data-type="success"]')).toHaveText('Passkey deleted successfully'); }); -test("Generate own one time access token as non admin", async ({ - page, - context, -}) => { - await context.clearCookies(); - await page.goto("/login"); - await (await passkeyUtil.init(page)).addPasskey("craig"); +test('Generate own one time access token as non admin', async ({ page, context }) => { + await context.clearCookies(); + await page.goto('/login'); + await (await passkeyUtil.init(page)).addPasskey('craig'); - await page.getByRole("button", { name: "Authenticate" }).click(); - await page.waitForURL("/settings/account"); + await page.getByRole('button', { name: 'Authenticate' }).click(); + await page.waitForURL('/settings/account'); - await page.getByRole("button", { name: "Create" }).click(); - const link = await page.getByTestId("login-code-link").textContent(); + await page.getByRole('button', { name: 'Create' }).click(); + const link = await page.getByTestId('login-code-link').textContent(); - await context.clearCookies(); + await context.clearCookies(); - await page.goto(link!); - await page.waitForURL("/settings/account"); + await page.goto(link!); + await page.waitForURL('/settings/account'); }); diff --git a/tests/specs/api-key.spec.ts b/tests/specs/api-key.spec.ts index 8c8e8b10..df2c4b43 100644 --- a/tests/specs/api-key.spec.ts +++ b/tests/specs/api-key.spec.ts @@ -1,79 +1,70 @@ // frontend/tests/api-key.spec.ts -import { expect, test } from "@playwright/test"; -import { apiKeys } from "../data"; -import { cleanupBackend } from "../utils/cleanup.util"; +import { expect, test } from '@playwright/test'; +import { apiKeys } from '../data'; +import { cleanupBackend } from '../utils/cleanup.util'; -test.describe("API Key Management", () => { - test.beforeEach(async ({ page }) => { - await cleanupBackend(); - await page.goto("/settings/admin/api-keys"); - }); +test.describe('API Key Management', () => { + test.beforeEach(async ({ page }) => { + await cleanupBackend(); + await page.goto('/settings/admin/api-keys'); + }); - test("Create new API key", async ({ page }) => { - await page.getByRole("button", { name: "Add API Key" }).click(); + test('Create new API key', async ({ page }) => { + await page.getByRole('button', { name: 'Add API Key' }).click(); - // Fill out the API key form - const name = "New Test API Key"; - await page.getByLabel("Name").fill(name); - await page.getByLabel("Description").fill("Created by automated test"); + // Fill out the API key form + const name = 'New Test API Key'; + await page.getByLabel('Name').fill(name); + await page.getByLabel('Description').fill('Created by automated test'); - // Choose the date - const currentDate = new Date(); - await page.getByRole("button", { name: "Select a date" }).click(); - await page.getByLabel("Select year").click(); - // Select the next year - await page.getByText((currentDate.getFullYear() + 1).toString()).click(); - // Select the first day of the month - await page - .getByRole("button", { name: /([A-Z][a-z]+), ([A-Z][a-z]+) 1, (\d{4})/ }) - .first() - .click(); + // Choose the date + const currentDate = new Date(); + await page.getByRole('button', { name: 'Select a date' }).click(); + await page.getByLabel('Select year').click(); + // Select the next year + await page.getByText((currentDate.getFullYear() + 1).toString()).click(); + // Select the first day of the month + await page + .getByRole('button', { name: /([A-Z][a-z]+), ([A-Z][a-z]+) 1, (\d{4})/ }) + .first() + .click(); - // Submit the form - await page.getByRole("button", { name: "Save" }).click(); + // Submit the form + await page.getByRole('button', { name: 'Save' }).click(); - // Verify the success dialog appears - await expect( - page.getByRole("heading", { name: "API Key Created" }) - ).toBeVisible(); + // Verify the success dialog appears + await expect(page.getByRole('heading', { name: 'API Key Created' })).toBeVisible(); - // Verify the key details are shown - await expect(page.getByRole("cell", { name })).toBeVisible(); + // Verify the key details are shown + await expect(page.getByRole('cell', { name })).toBeVisible(); - // Verify the token is displayed (should be 32 characters) - const token = await page.locator(".font-mono").textContent(); - expect(token?.length).toBe(32); + // Verify the token is displayed (should be 32 characters) + const token = await page.locator('.font-mono').textContent(); + expect(token?.length).toBe(32); - // Close the dialog - await page - .getByRole("button", { name: "Close", exact: true }) - .nth(1) - .click(); + // Close the dialog + await page.getByRole('button', { name: 'Close', exact: true }).nth(1).click(); - await page.reload(); + await page.reload(); - // Verify the key appears in the list - await expect(page.getByRole("cell", { name }).first()).toContainText(name); - }); + // Verify the key appears in the list + await expect(page.getByRole('cell', { name }).first()).toContainText(name); + }); - test("Revoke API key", async ({ page }) => { - const apiKey = apiKeys[0]; + test('Revoke API key', async ({ page }) => { + const apiKey = apiKeys[0]; - await page - .getByRole("row", { name: apiKey.name }) - .getByRole("button", { name: "Revoke" }) - .click(); + await page + .getByRole('row', { name: apiKey.name }) + .getByRole('button', { name: 'Revoke' }) + .click(); - await page.getByText("Revoke", { exact: true }).click(); + await page.getByText('Revoke', { exact: true }).click(); - // Verify success message - await expect(page.locator('[data-type="success"]')).toHaveText( - "API key revoked successfully" - ); + // Verify success message + await expect(page.locator('[data-type="success"]')).toHaveText('API key revoked successfully'); - // Verify key is no longer in the list - await expect( - page.getByRole("cell", { name: apiKey.name }) - ).not.toBeVisible(); - }); + // Verify key is no longer in the list + await expect(page.getByRole('cell', { name: apiKey.name })).not.toBeVisible(); + }); }); diff --git a/tests/specs/application-configuration.spec.ts b/tests/specs/application-configuration.spec.ts index 882de1f3..f8c828b4 100644 --- a/tests/specs/application-configuration.spec.ts +++ b/tests/specs/application-configuration.spec.ts @@ -1,99 +1,83 @@ -import test, { expect } from "@playwright/test"; -import { cleanupBackend } from "../utils/cleanup.util"; +import test, { expect } from '@playwright/test'; +import { cleanupBackend } from '../utils/cleanup.util'; test.beforeEach(cleanupBackend); -test("Update general configuration", async ({ page }) => { - await page.goto("/settings/admin/application-configuration"); +test('Update general configuration', async ({ page }) => { + await page.goto('/settings/admin/application-configuration'); - await page - .getByLabel("Application Name", { exact: true }) - .fill("Updated Name"); - await page.getByLabel("Session Duration").fill("30"); - await page.getByRole("button", { name: "Save" }).first().click(); + await page.getByLabel('Application Name', { exact: true }).fill('Updated Name'); + await page.getByLabel('Session Duration').fill('30'); + await page.getByRole('button', { name: 'Save' }).first().click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "Application configuration updated successfully" - ); - await expect(page.getByTestId("application-name")).toHaveText("Updated Name"); + await expect(page.locator('[data-type="success"]')).toHaveText( + 'Application configuration updated successfully' + ); + await expect(page.getByTestId('application-name')).toHaveText('Updated Name'); - await page.reload(); + await page.reload(); - await expect( - page.getByLabel("Application Name", { exact: true }) - ).toHaveValue("Updated Name"); - await expect(page.getByLabel("Session Duration")).toHaveValue("30"); + await expect(page.getByLabel('Application Name', { exact: true })).toHaveValue('Updated Name'); + await expect(page.getByLabel('Session Duration')).toHaveValue('30'); }); -test("Update email configuration", async ({ page }) => { - await page.goto("/settings/admin/application-configuration"); +test('Update email configuration', async ({ page }) => { + await page.goto('/settings/admin/application-configuration'); - await page.getByRole("button", { name: "Expand card" }).nth(1).click(); + await page.getByRole('button', { name: 'Expand card' }).nth(1).click(); - await page.getByLabel("SMTP Host").fill("smtp.gmail.com"); - await page.getByLabel("SMTP Port").fill("587"); - await page.getByLabel("SMTP User").fill("test@gmail.com"); - await page.getByLabel("SMTP Password").fill("password"); - await page.getByLabel("SMTP From").fill("test@gmail.com"); - await page.getByLabel("Email Login Notification").click(); - await page.getByLabel("Email Login Code Requested by User").click(); - await page.getByLabel("Email Login Code from Admin").click(); - await page.getByLabel("API Key Expiration").click(); + await page.getByLabel('SMTP Host').fill('smtp.gmail.com'); + await page.getByLabel('SMTP Port').fill('587'); + await page.getByLabel('SMTP User').fill('test@gmail.com'); + await page.getByLabel('SMTP Password').fill('password'); + await page.getByLabel('SMTP From').fill('test@gmail.com'); + await page.getByLabel('Email Login Notification').click(); + await page.getByLabel('Email Login Code Requested by User').click(); + await page.getByLabel('Email Login Code from Admin').click(); + await page.getByLabel('API Key Expiration').click(); - await page.getByRole("button", { name: "Save" }).nth(1).click(); + await page.getByRole('button', { name: 'Save' }).nth(1).click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "Email configuration updated successfully" - ); + await expect(page.locator('[data-type="success"]')).toHaveText( + 'Email configuration updated successfully' + ); - await page.reload(); + await page.reload(); - await expect(page.getByLabel("SMTP Host")).toHaveValue("smtp.gmail.com"); - await expect(page.getByLabel("SMTP Port")).toHaveValue("587"); - await expect(page.getByLabel("SMTP User")).toHaveValue("test@gmail.com"); - await expect(page.getByLabel("SMTP Password")).toHaveValue("password"); - await expect(page.getByLabel("SMTP From")).toHaveValue("test@gmail.com"); - await expect(page.getByLabel("Email Login Notification")).toBeChecked(); - await expect( - page.getByLabel("Email Login Code Requested by User") - ).toBeChecked(); - await expect(page.getByLabel("Email Login Code from Admin")).toBeChecked(); - await expect(page.getByLabel("API Key Expiration")).toBeChecked(); + await expect(page.getByLabel('SMTP Host')).toHaveValue('smtp.gmail.com'); + await expect(page.getByLabel('SMTP Port')).toHaveValue('587'); + await expect(page.getByLabel('SMTP User')).toHaveValue('test@gmail.com'); + await expect(page.getByLabel('SMTP Password')).toHaveValue('password'); + await expect(page.getByLabel('SMTP From')).toHaveValue('test@gmail.com'); + await expect(page.getByLabel('Email Login Notification')).toBeChecked(); + await expect(page.getByLabel('Email Login Code Requested by User')).toBeChecked(); + await expect(page.getByLabel('Email Login Code from Admin')).toBeChecked(); + await expect(page.getByLabel('API Key Expiration')).toBeChecked(); }); -test("Update application images", async ({ page }) => { - await page.goto("/settings/admin/application-configuration"); +test('Update application images', async ({ page }) => { + await page.goto('/settings/admin/application-configuration'); - await page.getByRole("button", { name: "Expand card" }).nth(3).click(); + await page.getByRole('button', { name: 'Expand card' }).nth(3).click(); - await page - .getByLabel("Favicon") - .setInputFiles("assets/w3-schools-favicon.ico"); - await page - .getByLabel("Light Mode Logo") - .setInputFiles("assets/pingvin-share-logo.png"); - await page - .getByLabel("Dark Mode Logo") - .setInputFiles("assets/nextcloud-logo.png"); - await page - .getByLabel("Background Image") - .setInputFiles("assets/clouds.jpg"); - await page.getByRole("button", { name: "Save" }).nth(1).click(); + await page.getByLabel('Favicon').setInputFiles('assets/w3-schools-favicon.ico'); + await page.getByLabel('Light Mode Logo').setInputFiles('assets/pingvin-share-logo.png'); + await page.getByLabel('Dark Mode Logo').setInputFiles('assets/nextcloud-logo.png'); + await page.getByLabel('Background Image').setInputFiles('assets/clouds.jpg'); + await page.getByRole('button', { name: 'Save' }).nth(1).click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "Images updated successfully" - ); + await expect(page.locator('[data-type="success"]')).toHaveText('Images updated successfully'); - await page.request - .get("/api/application-configuration/favicon") - .then((res) => expect.soft(res.status()).toBe(200)); - await page.request - .get("/api/application-configuration/logo?light=true") - .then((res) => expect.soft(res.status()).toBe(200)); - await page.request - .get("/api/application-configuration/logo?light=false") - .then((res) => expect.soft(res.status()).toBe(200)); - await page.request - .get("/api/application-configuration/background-image") - .then((res) => expect.soft(res.status()).toBe(200)); + await page.request + .get('/api/application-configuration/favicon') + .then((res) => expect.soft(res.status()).toBe(200)); + await page.request + .get('/api/application-configuration/logo?light=true') + .then((res) => expect.soft(res.status()).toBe(200)); + await page.request + .get('/api/application-configuration/logo?light=false') + .then((res) => expect.soft(res.status()).toBe(200)); + await page.request + .get('/api/application-configuration/background-image') + .then((res) => expect.soft(res.status()).toBe(200)); }); diff --git a/tests/specs/ldap.spec.ts b/tests/specs/ldap.spec.ts index 72bee8a0..0925ecd3 100644 --- a/tests/specs/ldap.spec.ts +++ b/tests/specs/ldap.spec.ts @@ -4,85 +4,94 @@ import { cleanupBackend } from '../utils/cleanup.util'; test.beforeEach(cleanupBackend); test.describe('LDAP Integration', () => { - test.skip(process.env.SKIP_LDAP_TESTS === 'true', 'Skipping LDAP tests due to SKIP_LDAP_TESTS environment variable'); + test.skip( + process.env.SKIP_LDAP_TESTS === 'true', + 'Skipping LDAP tests due to SKIP_LDAP_TESTS environment variable' + ); - test('LDAP configuration is working properly', async ({ page }) => { - await page.goto('/settings/admin/application-configuration'); + test('LDAP configuration is working properly', async ({ page }) => { + await page.goto('/settings/admin/application-configuration'); - await page.getByRole('button', { name: 'Expand card' }).nth(2).click(); + await page.getByRole('button', { name: 'Expand card' }).nth(2).click(); - await expect(page.getByRole('button', { name: 'Disable', exact: true })).toBeVisible(); - await expect(page.getByLabel('LDAP URL')).toHaveValue(/ldap:\/\/.*/); - await expect(page.getByLabel('LDAP Base DN')).not.toBeEmpty(); + await expect(page.getByRole('button', { name: 'Disable', exact: true })).toBeVisible(); + await expect(page.getByLabel('LDAP URL')).toHaveValue(/ldap:\/\/.*/); + await expect(page.getByLabel('LDAP Base DN')).not.toBeEmpty(); - await expect(page.getByLabel('User Unique Identifier Attribute')).not.toBeEmpty(); - await expect(page.getByLabel('Username Attribute')).not.toBeEmpty(); - await expect(page.getByLabel('User Mail Attribute')).not.toBeEmpty(); - await expect(page.getByLabel('Group Name Attribute')).not.toBeEmpty(); + await expect(page.getByLabel('User Unique Identifier Attribute')).not.toBeEmpty(); + await expect(page.getByLabel('Username Attribute')).not.toBeEmpty(); + await expect(page.getByLabel('User Mail Attribute')).not.toBeEmpty(); + await expect(page.getByLabel('Group Name Attribute')).not.toBeEmpty(); - const syncButton = page.getByRole('button', { name: 'Sync now' }); - await syncButton.click(); - await expect(page.locator('[data-type="success"]')).toHaveText('LDAP sync finished'); - }); + const syncButton = page.getByRole('button', { name: 'Sync now' }); + await syncButton.click(); + await expect(page.locator('[data-type="success"]')).toHaveText('LDAP sync finished'); + }); - test('LDAP users are synced into PocketID', async ({ page }) => { - // Navigate to user management - await page.goto('/settings/admin/users'); + test('LDAP users are synced into PocketID', async ({ page }) => { + // Navigate to user management + await page.goto('/settings/admin/users'); - // Verify the LDAP users exist - await expect(page.getByText('testuser1@pocket-id.org')).toBeVisible(); - await expect(page.getByText('testuser2@pocket-id.org')).toBeVisible(); + // Verify the LDAP users exist + await expect(page.getByText('testuser1@pocket-id.org')).toBeVisible(); + await expect(page.getByText('testuser2@pocket-id.org')).toBeVisible(); - // Check LDAP user details - await page.getByRole('row', { name: 'testuser1' }).getByRole('button').click(); - await page.getByRole('menuitem', { name: 'Edit' }).click(); + // Check LDAP user details + await page.getByRole('row', { name: 'testuser1' }).getByRole('button').click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); - // Verify user source is LDAP - await expect(page.getByText('LDAP').first()).toBeVisible(); + // Verify user source is LDAP + await expect(page.getByText('LDAP').first()).toBeVisible(); - // Verify essential fields are filled - await expect(page.getByLabel('Username')).not.toBeEmpty(); - await expect(page.getByLabel('Email')).not.toBeEmpty(); - }); + // Verify essential fields are filled + await expect(page.getByLabel('Username')).not.toBeEmpty(); + await expect(page.getByLabel('Email')).not.toBeEmpty(); + }); - test('LDAP groups are synced into PocketID', async ({ page }) => { - // Navigate to user groups - await page.goto('/settings/admin/user-groups'); + test('LDAP groups are synced into PocketID', async ({ page }) => { + // Navigate to user groups + await page.goto('/settings/admin/user-groups'); - // Verify LDAP groups exist - await expect(page.getByRole('cell', { name: 'test_group' }).first()).toBeVisible(); - await expect(page.getByRole('cell', { name: 'admin_group' }).first()).toBeVisible(); + // Verify LDAP groups exist + await expect(page.getByRole('cell', { name: 'test_group' }).first()).toBeVisible(); + await expect(page.getByRole('cell', { name: 'admin_group' }).first()).toBeVisible(); - await page.getByRole('row', { name: 'test_group' }).getByRole('button', { name: 'Toggle menu' }).click(); - await page.getByRole('menuitem', { name: 'Edit' }).click(); + await page + .getByRole('row', { name: 'test_group' }) + .getByRole('button', { name: 'Toggle menu' }) + .click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); - // Verify group source is LDAP - await expect(page.getByText('LDAP').first()).toBeVisible(); - }); + // Verify group source is LDAP + await expect(page.getByText('LDAP').first()).toBeVisible(); + }); - test('LDAP users cannot be modified in PocketID', async ({ page }) => { - // Navigate to LDAP user details - await page.goto('/settings/admin/users'); - await page.waitForLoadState('networkidle'); + test('LDAP users cannot be modified in PocketID', async ({ page }) => { + // Navigate to LDAP user details + await page.goto('/settings/admin/users'); + await page.waitForLoadState('networkidle'); - await page.getByRole('row', { name: 'testuser1' }).getByRole('button').click(); - await page.getByRole('menuitem', { name: 'Edit' }).click(); + await page.getByRole('row', { name: 'testuser1' }).getByRole('button').click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); - // Verify key fields are disabled - const usernameInput = page.getByLabel('Username'); - await expect(usernameInput).toBeDisabled(); - }); + // Verify key fields are disabled + const usernameInput = page.getByLabel('Username'); + await expect(usernameInput).toBeDisabled(); + }); - test('LDAP groups cannot be modified in PocketID', async ({ page }) => { - // Navigate to LDAP group details - await page.goto('/settings/admin/user-groups'); - await page.waitForLoadState('networkidle'); + test('LDAP groups cannot be modified in PocketID', async ({ page }) => { + // Navigate to LDAP group details + await page.goto('/settings/admin/user-groups'); + await page.waitForLoadState('networkidle'); - await page.getByRole('row', { name: 'test_group' }).getByRole('button', { name: 'Toggle menu' }).click(); - await page.getByRole('menuitem', { name: 'Edit' }).click(); + await page + .getByRole('row', { name: 'test_group' }) + .getByRole('button', { name: 'Toggle menu' }) + .click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); - // Verify key fields are disabled - const nameInput = page.getByLabel('Name', { exact: true }); - await expect(nameInput).toBeDisabled(); - }); + // Verify key fields are disabled + const nameInput = page.getByLabel('Name', { exact: true }); + await expect(nameInput).toBeDisabled(); + }); }); diff --git a/tests/specs/oidc-client-settings.spec.ts b/tests/specs/oidc-client-settings.spec.ts index 1d051bfb..4bbff886 100644 --- a/tests/specs/oidc-client-settings.spec.ts +++ b/tests/specs/oidc-client-settings.spec.ts @@ -1,100 +1,80 @@ -import test, { expect } from "@playwright/test"; -import { oidcClients } from "../data"; -import { cleanupBackend } from "../utils/cleanup.util"; +import test, { expect } from '@playwright/test'; +import { oidcClients } from '../data'; +import { cleanupBackend } from '../utils/cleanup.util'; test.beforeEach(cleanupBackend); -test("Create OIDC client", async ({ page }) => { - await page.goto("/settings/admin/oidc-clients"); - const oidcClient = oidcClients.pingvinShare; +test('Create OIDC client', async ({ page }) => { + await page.goto('/settings/admin/oidc-clients'); + const oidcClient = oidcClients.pingvinShare; - await page.getByRole("button", { name: "Add OIDC Client" }).click(); - await page.getByLabel("Name").fill(oidcClient.name); + await page.getByRole('button', { name: 'Add OIDC Client' }).click(); + await page.getByLabel('Name').fill(oidcClient.name); - await page.getByRole("button", { name: "Add" }).nth(1).click(); - await page.getByTestId("callback-url-1").fill(oidcClient.callbackUrl); - await page.getByRole("button", { name: "Add another" }).click(); - await page.getByTestId("callback-url-2").fill(oidcClient.secondCallbackUrl!); + await page.getByRole('button', { name: 'Add' }).nth(1).click(); + await page.getByTestId('callback-url-1').fill(oidcClient.callbackUrl); + await page.getByRole('button', { name: 'Add another' }).click(); + await page.getByTestId('callback-url-2').fill(oidcClient.secondCallbackUrl!); - await page.getByLabel("logo").setInputFiles("assets/pingvin-share-logo.png"); - await page.getByRole("button", { name: "Save" }).click(); + await page.getByLabel('logo').setInputFiles('assets/pingvin-share-logo.png'); + await page.getByRole('button', { name: 'Save' }).click(); - const clientId = await page.getByTestId("client-id").textContent(); + const clientId = await page.getByTestId('client-id').textContent(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "OIDC client created successfully" - ); - expect(clientId?.length).toBe(36); - expect((await page.getByTestId("client-secret").textContent())?.length).toBe( - 32 - ); - await expect(page.getByLabel("Name")).toHaveValue(oidcClient.name); - await expect(page.getByTestId("callback-url-1")).toHaveValue( - oidcClient.callbackUrl - ); - await expect(page.getByTestId("callback-url-2")).toHaveValue( - oidcClient.secondCallbackUrl! - ); - await expect( - page.getByRole("img", { name: `${oidcClient.name} logo` }) - ).toBeVisible(); - await page.request - .get(`/api/oidc/clients/${clientId}/logo`) - .then((res) => expect.soft(res.status()).toBe(200)); + await expect(page.locator('[data-type="success"]')).toHaveText( + 'OIDC client created successfully' + ); + expect(clientId?.length).toBe(36); + expect((await page.getByTestId('client-secret').textContent())?.length).toBe(32); + await expect(page.getByLabel('Name')).toHaveValue(oidcClient.name); + await expect(page.getByTestId('callback-url-1')).toHaveValue(oidcClient.callbackUrl); + await expect(page.getByTestId('callback-url-2')).toHaveValue(oidcClient.secondCallbackUrl!); + await expect(page.getByRole('img', { name: `${oidcClient.name} logo` })).toBeVisible(); + await page.request + .get(`/api/oidc/clients/${clientId}/logo`) + .then((res) => expect.soft(res.status()).toBe(200)); }); -test("Edit OIDC client", async ({ page }) => { - const oidcClient = oidcClients.nextcloud; - await page.goto(`/settings/admin/oidc-clients/${oidcClient.id}`); +test('Edit OIDC client', async ({ page }) => { + const oidcClient = oidcClients.nextcloud; + await page.goto(`/settings/admin/oidc-clients/${oidcClient.id}`); - await page.getByLabel("Name").fill("Nextcloud updated"); - await page - .getByTestId("callback-url-1") - .first() - .fill("http://nextcloud-updated/auth/callback"); - await page.getByLabel("logo").setInputFiles("assets/nextcloud-logo.png"); - await page.getByRole("button", { name: "Save" }).click(); + await page.getByLabel('Name').fill('Nextcloud updated'); + await page.getByTestId('callback-url-1').first().fill('http://nextcloud-updated/auth/callback'); + await page.getByLabel('logo').setInputFiles('assets/nextcloud-logo.png'); + await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "OIDC client updated successfully" - ); - await expect( - page.getByRole("img", { name: "Nextcloud updated logo" }) - ).toBeVisible(); - await page.request - .get(`/api/oidc/clients/${oidcClient.id}/logo`) - .then((res) => expect.soft(res.status()).toBe(200)); + await expect(page.locator('[data-type="success"]')).toHaveText( + 'OIDC client updated successfully' + ); + await expect(page.getByRole('img', { name: 'Nextcloud updated logo' })).toBeVisible(); + await page.request + .get(`/api/oidc/clients/${oidcClient.id}/logo`) + .then((res) => expect.soft(res.status()).toBe(200)); }); -test("Create new OIDC client secret", async ({ page }) => { - const oidcClient = oidcClients.nextcloud; - await page.goto(`/settings/admin/oidc-clients/${oidcClient.id}`); +test('Create new OIDC client secret', async ({ page }) => { + const oidcClient = oidcClients.nextcloud; + await page.goto(`/settings/admin/oidc-clients/${oidcClient.id}`); - await page.getByLabel("Create new client secret").click(); - await page.getByRole("button", { name: "Generate" }).click(); + await page.getByLabel('Create new client secret').click(); + await page.getByRole('button', { name: 'Generate' }).click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "New client secret created successfully" - ); - expect((await page.getByTestId("client-secret").textContent())?.length).toBe( - 32 - ); + await expect(page.locator('[data-type="success"]')).toHaveText( + 'New client secret created successfully' + ); + expect((await page.getByTestId('client-secret').textContent())?.length).toBe(32); }); -test("Delete OIDC client", async ({ page }) => { - const oidcClient = oidcClients.nextcloud; - await page.goto("/settings/admin/oidc-clients"); +test('Delete OIDC client', async ({ page }) => { + const oidcClient = oidcClients.nextcloud; + await page.goto('/settings/admin/oidc-clients'); - await page - .getByRole("row", { name: oidcClient.name }) - .getByLabel("Delete") - .click(); - await page.getByText("Delete", { exact: true }).click(); + await page.getByRole('row', { name: oidcClient.name }).getByLabel('Delete').click(); + await page.getByText('Delete', { exact: true }).click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "OIDC client deleted successfully" - ); - await expect( - page.getByRole("row", { name: oidcClient.name }) - ).not.toBeVisible(); + await expect(page.locator('[data-type="success"]')).toHaveText( + 'OIDC client deleted successfully' + ); + await expect(page.getByRole('row', { name: oidcClient.name })).not.toBeVisible(); }); diff --git a/tests/specs/oidc.spec.ts b/tests/specs/oidc.spec.ts index 9c1c78a9..6cb958b8 100644 --- a/tests/specs/oidc.spec.ts +++ b/tests/specs/oidc.spec.ts @@ -1,670 +1,596 @@ -import test, { expect } from "@playwright/test"; -import { oidcClients, refreshTokens, users } from "../data"; -import { cleanupBackend } from "../utils/cleanup.util"; -import { generateIdToken, generateOauthAccessToken } from "../utils/jwt.util"; -import * as oidcUtil from "../utils/oidc.util"; -import passkeyUtil from "../utils/passkey.util"; +import test, { expect } from '@playwright/test'; +import { oidcClients, refreshTokens, users } from '../data'; +import { cleanupBackend } from '../utils/cleanup.util'; +import { generateIdToken, generateOauthAccessToken } from '../utils/jwt.util'; +import * as oidcUtil from '../utils/oidc.util'; +import passkeyUtil from '../utils/passkey.util'; test.beforeEach(cleanupBackend); -test("Authorize existing client", async ({ page }) => { - const oidcClient = oidcClients.nextcloud; - const urlParams = createUrlParams(oidcClient); - await page.goto(`/authorize?${urlParams.toString()}`); +test('Authorize existing client', async ({ page }) => { + const oidcClient = oidcClients.nextcloud; + const urlParams = createUrlParams(oidcClient); + await page.goto(`/authorize?${urlParams.toString()}`); - // Ignore DNS resolution error as the callback URL is not reachable - await page.waitForURL(oidcClient.callbackUrl).catch((e) => { - if (!e.message.includes("net::ERR_NAME_NOT_RESOLVED")) { - throw e; - } - }); + // Ignore DNS resolution error as the callback URL is not reachable + await page.waitForURL(oidcClient.callbackUrl).catch((e) => { + if (!e.message.includes('net::ERR_NAME_NOT_RESOLVED')) { + throw e; + } + }); }); -test("Authorize existing client while not signed in", async ({ page }) => { - const oidcClient = oidcClients.nextcloud; - const urlParams = createUrlParams(oidcClient); - await page.context().clearCookies(); - await page.goto(`/authorize?${urlParams.toString()}`); +test('Authorize existing client while not signed in', async ({ page }) => { + const oidcClient = oidcClients.nextcloud; + const urlParams = createUrlParams(oidcClient); + await page.context().clearCookies(); + await page.goto(`/authorize?${urlParams.toString()}`); - await (await passkeyUtil.init(page)).addPasskey(); - await page.getByRole("button", { name: "Sign in" }).click(); + await (await passkeyUtil.init(page)).addPasskey(); + await page.getByRole('button', { name: 'Sign in' }).click(); - // Ignore DNS resolution error as the callback URL is not reachable - await page.waitForURL(oidcClient.callbackUrl).catch((e) => { - if (!e.message.includes("net::ERR_NAME_NOT_RESOLVED")) { - throw e; - } - }); + // Ignore DNS resolution error as the callback URL is not reachable + await page.waitForURL(oidcClient.callbackUrl).catch((e) => { + if (!e.message.includes('net::ERR_NAME_NOT_RESOLVED')) { + throw e; + } + }); }); -test("Authorize new client", async ({ page }) => { - const oidcClient = oidcClients.immich; - const urlParams = createUrlParams(oidcClient); - await page.goto(`/authorize?${urlParams.toString()}`); +test('Authorize new client', async ({ page }) => { + const oidcClient = oidcClients.immich; + const urlParams = createUrlParams(oidcClient); + await page.goto(`/authorize?${urlParams.toString()}`); - await expect( - page.getByTestId("scopes").getByRole("heading", { name: "Email" }) - ).toBeVisible(); - await expect( - page.getByTestId("scopes").getByRole("heading", { name: "Profile" }) - ).toBeVisible(); + await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible(); + await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Profile' })).toBeVisible(); - await page.getByRole("button", { name: "Sign in" }).click(); + await page.getByRole('button', { name: 'Sign in' }).click(); - // Ignore DNS resolution error as the callback URL is not reachable - await page.waitForURL(oidcClient.callbackUrl).catch((e) => { - if (!e.message.includes("net::ERR_NAME_NOT_RESOLVED")) { - throw e; - } - }); + // Ignore DNS resolution error as the callback URL is not reachable + await page.waitForURL(oidcClient.callbackUrl).catch((e) => { + if (!e.message.includes('net::ERR_NAME_NOT_RESOLVED')) { + throw e; + } + }); }); -test("Authorize new client while not signed in", async ({ page }) => { - const oidcClient = oidcClients.immich; - const urlParams = createUrlParams(oidcClient); - await page.context().clearCookies(); - await page.goto(`/authorize?${urlParams.toString()}`); +test('Authorize new client while not signed in', async ({ page }) => { + const oidcClient = oidcClients.immich; + const urlParams = createUrlParams(oidcClient); + await page.context().clearCookies(); + await page.goto(`/authorize?${urlParams.toString()}`); - await (await passkeyUtil.init(page)).addPasskey(); - await page.getByRole("button", { name: "Sign in" }).click(); + await (await passkeyUtil.init(page)).addPasskey(); + await page.getByRole('button', { name: 'Sign in' }).click(); - await expect( - page.getByTestId("scopes").getByRole("heading", { name: "Email" }) - ).toBeVisible(); - await expect( - page.getByTestId("scopes").getByRole("heading", { name: "Profile" }) - ).toBeVisible(); + await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible(); + await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Profile' })).toBeVisible(); - await page.getByRole("button", { name: "Sign in" }).click(); + await page.getByRole('button', { name: 'Sign in' }).click(); - // Ignore DNS resolution error as the callback URL is not reachable - await page.waitForURL(oidcClient.callbackUrl).catch((e) => { - if (!e.message.includes("net::ERR_NAME_NOT_RESOLVED")) { - throw e; - } - }); + // Ignore DNS resolution error as the callback URL is not reachable + await page.waitForURL(oidcClient.callbackUrl).catch((e) => { + if (!e.message.includes('net::ERR_NAME_NOT_RESOLVED')) { + throw e; + } + }); }); -test("Authorize new client fails with user group not allowed", async ({ - page, -}) => { - const oidcClient = oidcClients.immich; - const urlParams = createUrlParams(oidcClient); - await page.context().clearCookies(); - await page.goto(`/authorize?${urlParams.toString()}`); +test('Authorize new client fails with user group not allowed', async ({ page }) => { + const oidcClient = oidcClients.immich; + const urlParams = createUrlParams(oidcClient); + await page.context().clearCookies(); + await page.goto(`/authorize?${urlParams.toString()}`); - await (await passkeyUtil.init(page)).addPasskey("craig"); - await page.getByRole("button", { name: "Sign in" }).click(); + await (await passkeyUtil.init(page)).addPasskey('craig'); + await page.getByRole('button', { name: 'Sign in' }).click(); - await expect( - page.getByTestId("scopes").getByRole("heading", { name: "Email" }) - ).toBeVisible(); - await expect( - page.getByTestId("scopes").getByRole("heading", { name: "Profile" }) - ).toBeVisible(); + await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible(); + await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Profile' })).toBeVisible(); - await page.getByRole("button", { name: "Sign in" }).click(); + await page.getByRole('button', { name: 'Sign in' }).click(); - await expect(page.getByRole("paragraph").first()).toHaveText( - "You're not allowed to access this service." - ); + await expect(page.getByRole('paragraph').first()).toHaveText( + "You're not allowed to access this service." + ); }); function createUrlParams(oidcClient: { id: string; callbackUrl: string }) { - return new URLSearchParams({ - client_id: oidcClient.id, - response_type: "code", - scope: "openid profile email", - redirect_uri: oidcClient.callbackUrl, - state: "nXx-6Qr-owc1SHBa", - nonce: "P1gN3PtpKHJgKUVcLpLjm", - }); + return new URLSearchParams({ + client_id: oidcClient.id, + response_type: 'code', + scope: 'openid profile email', + redirect_uri: oidcClient.callbackUrl, + state: 'nXx-6Qr-owc1SHBa', + nonce: 'P1gN3PtpKHJgKUVcLpLjm' + }); } -test("End session without id token hint shows confirmation page", async ({ - page, +test('End session without id token hint shows confirmation page', async ({ page }) => { + await page.goto('/api/oidc/end-session'); + + await expect(page).toHaveURL('/logout'); + await page.getByRole('button', { name: 'Sign out' }).click(); + + await expect(page).toHaveURL('/login'); +}); + +test('End session with id token hint redirects to callback URL', async ({ page }) => { + const client = oidcClients.nextcloud; + const idToken = await generateIdToken(users.tim, client.id); + let redirectedCorrectly = false; + await page + .goto( + `/api/oidc/end-session?id_token_hint=${idToken}&post_logout_redirect_uri=${client.logoutCallbackUrl}` + ) + .catch((e) => { + if (e.message.includes('net::ERR_NAME_NOT_RESOLVED')) { + redirectedCorrectly = true; + } else { + throw e; + } + }); + + expect(redirectedCorrectly).toBeTruthy(); +}); + +test('Successfully refresh tokens with valid refresh token', async ({ request }) => { + const { token, clientId, userId } = refreshTokens.filter((token) => !token.expired)[0]; + const clientSecret = 'w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY'; + + // Sign the refresh token + const refreshToken = await request + .post('/api/test/refreshtoken', { + data: { + rt: token, + client: clientId, + user: userId + } + }) + .then((r) => r.text()); + + // Perform the exchange + const refreshResponse = await request.post('/api/oidc/token', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + form: { + grant_type: 'refresh_token', + client_id: clientId, + refresh_token: refreshToken, + client_secret: clientSecret + } + }); + + // Verify we got new tokens + const tokenData = await refreshResponse.json(); + expect(tokenData.access_token).toBeDefined(); + expect(tokenData.refresh_token).toBeDefined(); + expect(tokenData.token_type).toBe('Bearer'); + expect(tokenData.expires_in).toBe(3600); + + // The new refresh token should be different from the old one + expect(tokenData.refresh_token).not.toBe(token); +}); + +test('Refresh token fails when used for the wrong client', async ({ request }) => { + const { token, clientId, userId } = refreshTokens.filter((token) => !token.expired)[0]; + const clientSecret = 'w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY'; + + // Sign the refresh token + const refreshToken = await request + .post('/api/test/refreshtoken', { + data: { + rt: token, + client: 'bad-client', + user: userId + } + }) + .then((r) => r.text()); + + // Perform the exchange + const refreshResponse = await request.post('/api/oidc/token', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + form: { + grant_type: 'refresh_token', + client_id: clientId, + refresh_token: refreshToken, + client_secret: clientSecret + } + }); + + expect(refreshResponse.status()).toBe(400); +}); + +test('Refresh token fails when used for the wrong user', async ({ request }) => { + const { token, clientId } = refreshTokens.filter((token) => !token.expired)[0]; + const clientSecret = 'w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY'; + + // Sign the refresh token + const refreshToken = await request + .post('/api/test/refreshtoken', { + data: { + rt: token, + client: clientId, + user: 'bad-user' + } + }) + .then((r) => r.text()); + + // Perform the exchange + const refreshResponse = await request.post('/api/oidc/token', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + form: { + grant_type: 'refresh_token', + client_id: clientId, + refresh_token: refreshToken, + client_secret: clientSecret + } + }); + + expect(refreshResponse.status()).toBe(400); +}); + +test('Using refresh token invalidates it for future use', async ({ request }) => { + const { token, clientId, userId } = refreshTokens.filter((token) => !token.expired)[0]; + const clientSecret = 'w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY'; + + // Sign the refresh token + const refreshToken = await request + .post('/api/test/refreshtoken', { + data: { + rt: token, + client: clientId, + user: userId + } + }) + .then((r) => r.text()); + + // Perform the exchange + await request.post('/api/oidc/token', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + form: { + grant_type: 'refresh_token', + client_id: clientId, + refresh_token: refreshToken, + client_secret: clientSecret + } + }); + + // Try again + const refreshResponse = await request.post('/api/oidc/token', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + form: { + grant_type: 'refresh_token', + client_id: clientId, + refresh_token: refreshToken, + client_secret: clientSecret + } + }); + expect(refreshResponse.status()).toBe(400); +}); + +test.describe('Introspection endpoint', () => { + test('fails without client credentials', async ({ request }) => { + const validAccessToken = await generateOauthAccessToken(users.tim, oidcClients.nextcloud.id); + const introspectionResponse = await request.post('/api/oidc/introspect', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + form: { + token: validAccessToken + } + }); + + expect(introspectionResponse.status()).toBe(400); + }); + + test('succeeds with client credentials', async ({ request, baseURL }) => { + const validAccessToken = await generateOauthAccessToken(users.tim, oidcClients.nextcloud.id); + const introspectionResponse = await request.post('/api/oidc/introspect', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: + 'Basic ' + + Buffer.from(`${oidcClients.nextcloud.id}:${oidcClients.nextcloud.secret}`).toString( + 'base64' + ) + }, + form: { + token: validAccessToken + } + }); + + expect(introspectionResponse.status()).toBe(200); + const introspectionBody = await introspectionResponse.json(); + expect(introspectionBody.active).toBe(true); + expect(introspectionBody.token_type).toBe('access_token'); + expect(introspectionBody.iss).toBe(baseURL); + expect(introspectionBody.sub).toBe(users.tim.id); + expect(introspectionBody.aud).toStrictEqual([oidcClients.nextcloud.id]); + }); + + test('succeeds with federated client credentials', async ({ page, request, baseURL }) => { + const validAccessToken = await generateOauthAccessToken(users.tim, oidcClients.federated.id); + const clientAssertion = await oidcUtil.getClientAssertion( + page, + oidcClients.federated.federatedJWT + ); + const introspectionResponse = await request.post('/api/oidc/introspect', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: 'Bearer ' + clientAssertion + }, + form: { + token: validAccessToken + } + }); + + expect(introspectionResponse.status()).toBe(200); + const introspectionBody = await introspectionResponse.json(); + expect(introspectionBody.active).toBe(true); + expect(introspectionBody.token_type).toBe('access_token'); + expect(introspectionBody.iss).toBe(baseURL); + expect(introspectionBody.sub).toBe(users.tim.id); + expect(introspectionBody.aud).toStrictEqual([oidcClients.federated.id]); + }); + + test('fails with client credentials for wrong app', async ({ request }) => { + const validAccessToken = await generateOauthAccessToken(users.tim, oidcClients.nextcloud.id); + const introspectionResponse = await request.post('/api/oidc/introspect', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: + 'Basic ' + + Buffer.from(`${oidcClients.immich.id}:${oidcClients.immich.secret}`).toString('base64') + }, + form: { + token: validAccessToken + } + }); + + expect(introspectionResponse.status()).toBe(400); + }); + + test('fails with federated credentials for wrong app', async ({ page, request }) => { + const validAccessToken = await generateOauthAccessToken(users.tim, oidcClients.nextcloud.id); + const clientAssertion = await oidcUtil.getClientAssertion( + page, + oidcClients.federated.federatedJWT + ); + const introspectionResponse = await request.post('/api/oidc/introspect', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: 'Bearer ' + clientAssertion + }, + form: { + token: validAccessToken + } + }); + + expect(introspectionResponse.status()).toBe(400); + }); + + test('non-expired refresh_token can be verified', async ({ request }) => { + const { token, clientId, userId } = refreshTokens.filter((token) => !token.expired)[0]; + + // Sign the refresh token + const refreshToken = await request + .post('/api/test/refreshtoken', { + data: { + rt: token, + client: clientId, + user: userId + } + }) + .then((r) => r.text()); + + const introspectionResponse = await request.post('/api/oidc/introspect', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: + 'Basic ' + + Buffer.from(`${oidcClients.nextcloud.id}:${oidcClients.nextcloud.secret}`).toString( + 'base64' + ) + }, + form: { + token: refreshToken + } + }); + + expect(introspectionResponse.status()).toBe(200); + const introspectionBody = await introspectionResponse.json(); + expect(introspectionBody.active).toBe(true); + expect(introspectionBody.token_type).toBe('refresh_token'); + }); + + test('expired refresh_token can be verified', async ({ request }) => { + const { token, clientId, userId } = refreshTokens.filter((token) => token.expired)[0]; + + // Sign the refresh token + const refreshToken = await request + .post('/api/test/refreshtoken', { + data: { + rt: token, + client: clientId, + user: userId + } + }) + .then((r) => r.text()); + + const introspectionResponse = await request.post('/api/oidc/introspect', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: + 'Basic ' + + Buffer.from(`${oidcClients.nextcloud.id}:${oidcClients.nextcloud.secret}`).toString( + 'base64' + ) + }, + form: { + token: refreshToken + } + }); + + expect(introspectionResponse.status()).toBe(200); + const introspectionBody = await introspectionResponse.json(); + expect(introspectionBody.active).toBe(false); + }); + + test("expired access_token can't be verified", async ({ request }) => { + const expiredAccessToken = await generateOauthAccessToken( + users.tim, + oidcClients.nextcloud.id, + true + ); + const introspectionResponse = await request.post('/api/oidc/introspect', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + form: { + token: expiredAccessToken + } + }); + + expect(introspectionResponse.status()).toBe(400); + }); +}); + +test('Authorize new client with device authorization flow', async ({ page }) => { + const client = oidcClients.immich; + const userCode = await oidcUtil.getUserCode(page, client.id, client.secret); + + await page.goto(`/device?code=${userCode}`); + + await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible(); + await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Profile' })).toBeVisible(); + + await page.getByRole('button', { name: 'Authorize' }).click(); + + await expect( + page.getByRole('paragraph').filter({ hasText: 'The device has been authorized.' }) + ).toBeVisible(); +}); + +test('Authorize new client with device authorization flow while not signed in', async ({ + page }) => { - await page.goto("/api/oidc/end-session"); + await page.context().clearCookies(); + const client = oidcClients.immich; + const userCode = await oidcUtil.getUserCode(page, client.id, client.secret); - await expect(page).toHaveURL("/logout"); - await page.getByRole("button", { name: "Sign out" }).click(); + await page.goto(`/device?code=${userCode}`); - await expect(page).toHaveURL("/login"); + await (await passkeyUtil.init(page)).addPasskey(); + await page.getByRole('button', { name: 'Authorize' }).click(); + + await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible(); + await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Profile' })).toBeVisible(); + + await page.getByRole('button', { name: 'Authorize' }).click(); + + await expect( + page.getByRole('paragraph').filter({ hasText: 'The device has been authorized.' }) + ).toBeVisible(); }); -test("End session with id token hint redirects to callback URL", async ({ - page, +test('Authorize existing client with device authorization flow', async ({ page }) => { + const client = oidcClients.nextcloud; + const userCode = await oidcUtil.getUserCode(page, client.id, client.secret); + + await page.goto(`/device?code=${userCode}`); + + await expect( + page.getByRole('paragraph').filter({ hasText: 'The device has been authorized.' }) + ).toBeVisible(); +}); + +test('Authorize existing client with device authorization flow while not signed in', async ({ + page }) => { - const client = oidcClients.nextcloud; - const idToken = await generateIdToken(users.tim, client.id); - let redirectedCorrectly = false; - await page - .goto( - `/api/oidc/end-session?id_token_hint=${idToken}&post_logout_redirect_uri=${client.logoutCallbackUrl}` - ) - .catch((e) => { - if (e.message.includes("net::ERR_NAME_NOT_RESOLVED")) { - redirectedCorrectly = true; - } else { - throw e; - } - }); + await page.context().clearCookies(); + const client = oidcClients.nextcloud; + const userCode = await oidcUtil.getUserCode(page, client.id, client.secret); - expect(redirectedCorrectly).toBeTruthy(); + await page.goto(`/device?code=${userCode}`); + + await (await passkeyUtil.init(page)).addPasskey(); + await page.getByRole('button', { name: 'Authorize' }).click(); + + await expect( + page.getByRole('paragraph').filter({ hasText: 'The device has been authorized.' }) + ).toBeVisible(); }); -test("Successfully refresh tokens with valid refresh token", async ({ - request, +test('Authorize client with device authorization flow with invalid code', async ({ page }) => { + await page.goto('/device?code=invalid-code'); + + await expect( + page.getByRole('paragraph').filter({ hasText: 'Invalid device code.' }) + ).toBeVisible(); +}); + +test('Authorize new client with device authorization with user group not allowed', async ({ + page }) => { - const { token, clientId, userId } = refreshTokens.filter( - (token) => !token.expired - )[0]; - const clientSecret = "w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY"; + await page.context().clearCookies(); + const client = oidcClients.immich; + const userCode = await oidcUtil.getUserCode(page, client.id, client.secret); - // Sign the refresh token - const refreshToken = await request.post("/api/test/refreshtoken", { - data: { - rt: token, - client: clientId, - user: userId, - } - }).then((r) => r.text()) + await page.goto(`/device?code=${userCode}`); - // Perform the exchange - const refreshResponse = await request.post("/api/oidc/token", { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - form: { - grant_type: "refresh_token", - client_id: clientId, - refresh_token: refreshToken, - client_secret: clientSecret, - }, - }); + await (await passkeyUtil.init(page)).addPasskey('craig'); + await page.getByRole('button', { name: 'Authorize' }).click(); - // Verify we got new tokens - const tokenData = await refreshResponse.json(); - expect(tokenData.access_token).toBeDefined(); - expect(tokenData.refresh_token).toBeDefined(); - expect(tokenData.token_type).toBe("Bearer"); - expect(tokenData.expires_in).toBe(3600); + await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible(); + await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Profile' })).toBeVisible(); - // The new refresh token should be different from the old one - expect(tokenData.refresh_token).not.toBe(token); + await page.getByRole('button', { name: 'Authorize' }).click(); + + await expect( + page.getByRole('paragraph').filter({ hasText: "You're not allowed to access this service." }) + ).toBeVisible(); }); +test('Federated identity fails with invalid client assertion', async ({ page }) => { + const client = oidcClients.federated; -test("Refresh token fails when used for the wrong client", async ({ - request, -}) => { - const { token, clientId, userId } = refreshTokens.filter( - (token) => !token.expired - )[0]; - const clientSecret = "w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY"; + const res = await oidcUtil.exchangeCode(page, { + client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + grant_type: 'authorization_code', + redirect_uri: client.callbackUrl, + code: client.accessCodes[0], + client_id: client.id, + client_assertion: 'not-an-assertion' + }); - // Sign the refresh token - const refreshToken = await request.post("/api/test/refreshtoken", { - data: { - rt: token, - client: 'bad-client', - user: userId, - } - }).then((r) => r.text()) - - // Perform the exchange - const refreshResponse = await request.post("/api/oidc/token", { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - form: { - grant_type: "refresh_token", - client_id: clientId, - refresh_token: refreshToken, - client_secret: clientSecret, - }, - }); - - expect(refreshResponse.status()).toBe(400); + expect(res?.error).toBe('Invalid client assertion'); }); -test("Refresh token fails when used for the wrong user", async ({ - request, -}) => { - const { token, clientId } = refreshTokens.filter( - (token) => !token.expired - )[0]; - const clientSecret = "w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY"; +test('Authorize existing client with federated identity', async ({ page }) => { + const client = oidcClients.federated; + const clientAssertion = await oidcUtil.getClientAssertion(page, client.federatedJWT); - // Sign the refresh token - const refreshToken = await request.post("/api/test/refreshtoken", { - data: { - rt: token, - client: clientId, - user: 'bad-user', - } - }).then((r) => r.text()) + const res = await oidcUtil.exchangeCode(page, { + client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + grant_type: 'authorization_code', + redirect_uri: client.callbackUrl, + code: client.accessCodes[0], + client_id: client.id, + client_assertion: clientAssertion + }); - // Perform the exchange - const refreshResponse = await request.post("/api/oidc/token", { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - form: { - grant_type: "refresh_token", - client_id: clientId, - refresh_token: refreshToken, - client_secret: clientSecret, - }, - }); - - expect(refreshResponse.status()).toBe(400); -}); - -test("Using refresh token invalidates it for future use", async ({ - request, -}) => { - const { token, clientId, userId } = refreshTokens.filter( - (token) => !token.expired - )[0]; - const clientSecret = "w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY"; - - // Sign the refresh token - const refreshToken = await request.post("/api/test/refreshtoken", { - data: { - rt: token, - client: clientId, - user: userId, - } - }).then((r) => r.text()) - - // Perform the exchange - await request.post("/api/oidc/token", { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - form: { - grant_type: "refresh_token", - client_id: clientId, - refresh_token: refreshToken, - client_secret: clientSecret, - }, - }); - - // Try again - const refreshResponse = await request.post("/api/oidc/token", { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - form: { - grant_type: "refresh_token", - client_id: clientId, - refresh_token: refreshToken, - client_secret: clientSecret, - }, - }); - expect(refreshResponse.status()).toBe(400); -}); - -test.describe("Introspection endpoint", () => { - test("fails without client credentials", async ({ request }) => { - const validAccessToken = await generateOauthAccessToken( - users.tim, - oidcClients.nextcloud.id - ); - const introspectionResponse = await request.post("/api/oidc/introspect", { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - form: { - token: validAccessToken, - }, - }); - - expect(introspectionResponse.status()).toBe(400); - }); - - test("succeeds with client credentials", async ({ - request, - baseURL, - }) => { - const validAccessToken = await generateOauthAccessToken( - users.tim, - oidcClients.nextcloud.id - ); - const introspectionResponse = await request.post("/api/oidc/introspect", { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Authorization: - "Basic " + - Buffer.from(`${oidcClients.nextcloud.id}:${oidcClients.nextcloud.secret}`).toString("base64"), - }, - form: { - token: validAccessToken, - }, - }); - - expect(introspectionResponse.status()).toBe(200); - const introspectionBody = await introspectionResponse.json(); - expect(introspectionBody.active).toBe(true); - expect(introspectionBody.token_type).toBe("access_token"); - expect(introspectionBody.iss).toBe(baseURL); - expect(introspectionBody.sub).toBe(users.tim.id); - expect(introspectionBody.aud).toStrictEqual([oidcClients.nextcloud.id]); - }); - - test("succeeds with federated client credentials", async ({ - page, - request, - baseURL, - }) => { - const validAccessToken = await generateOauthAccessToken( - users.tim, - oidcClients.federated.id - ); - const clientAssertion = await oidcUtil.getClientAssertion(page, oidcClients.federated.federatedJWT); - const introspectionResponse = await request.post("/api/oidc/introspect", { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Authorization: "Bearer " + clientAssertion, - }, - form: { - token: validAccessToken, - }, - }); - - expect(introspectionResponse.status()).toBe(200); - const introspectionBody = await introspectionResponse.json(); - expect(introspectionBody.active).toBe(true); - expect(introspectionBody.token_type).toBe("access_token"); - expect(introspectionBody.iss).toBe(baseURL); - expect(introspectionBody.sub).toBe(users.tim.id); - expect(introspectionBody.aud).toStrictEqual([oidcClients.federated.id]); - }); - - test("fails with client credentials for wrong app", async ({ - request, - }) => { - const validAccessToken = await generateOauthAccessToken( - users.tim, - oidcClients.nextcloud.id - ); - const introspectionResponse = await request.post("/api/oidc/introspect", { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Authorization: - "Basic " + - Buffer.from(`${oidcClients.immich.id}:${oidcClients.immich.secret}`).toString("base64"), - }, - form: { - token: validAccessToken, - }, - }); - - expect(introspectionResponse.status()).toBe(400); - }); - - test("fails with federated credentials for wrong app", async ({ - page, - request, - }) => { - const validAccessToken = await generateOauthAccessToken( - users.tim, - oidcClients.nextcloud.id - ); - const clientAssertion = await oidcUtil.getClientAssertion(page, oidcClients.federated.federatedJWT); - const introspectionResponse = await request.post("/api/oidc/introspect", { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Authorization: "Bearer " + clientAssertion, - }, - form: { - token: validAccessToken, - }, - }); - - expect(introspectionResponse.status()).toBe(400); - }); - - test("non-expired refresh_token can be verified", async ({ request }) => { - const { token, clientId, userId } = refreshTokens.filter( - (token) => !token.expired - )[0]; - - // Sign the refresh token - const refreshToken = await request.post("/api/test/refreshtoken", { - data: { - rt: token, - client: clientId, - user: userId, - } - }).then((r) => r.text()) - - const introspectionResponse = await request.post("/api/oidc/introspect", { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Authorization: - "Basic " + - Buffer.from(`${oidcClients.nextcloud.id}:${oidcClients.nextcloud.secret}`).toString("base64"), - }, - form: { - token: refreshToken, - }, - }); - - expect(introspectionResponse.status()).toBe(200); - const introspectionBody = await introspectionResponse.json(); - expect(introspectionBody.active).toBe(true); - expect(introspectionBody.token_type).toBe("refresh_token"); - }); - - test("expired refresh_token can be verified", async ({ request }) => { - const { token, clientId, userId } = refreshTokens.filter( - (token) => token.expired - )[0]; - - // Sign the refresh token - const refreshToken = await request.post("/api/test/refreshtoken", { - data: { - rt: token, - client: clientId, - user: userId, - } - }).then((r) => r.text()) - - const introspectionResponse = await request.post("/api/oidc/introspect", { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Authorization: - "Basic " + - Buffer.from(`${oidcClients.nextcloud.id}:${oidcClients.nextcloud.secret}`).toString("base64"), - }, - form: { - token: refreshToken, - }, - }); - - expect(introspectionResponse.status()).toBe(200); - const introspectionBody = await introspectionResponse.json(); - expect(introspectionBody.active).toBe(false); - }); - - test("expired access_token can't be verified", async ({ request }) => { - const expiredAccessToken = await generateOauthAccessToken( - users.tim, - oidcClients.nextcloud.id, - true - ); - const introspectionResponse = await request.post("/api/oidc/introspect", { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - form: { - token: expiredAccessToken, - }, - }); - - expect(introspectionResponse.status()).toBe(400); - }); -}); - -test("Authorize new client with device authorization flow", async ({ - page, -}) => { - const client = oidcClients.immich; - const userCode = await oidcUtil.getUserCode(page, client.id, client.secret); - - await page.goto(`/device?code=${userCode}`); - - await expect( - page.getByTestId("scopes").getByRole("heading", { name: "Email" }) - ).toBeVisible(); - await expect( - page.getByTestId("scopes").getByRole("heading", { name: "Profile" }) - ).toBeVisible(); - - await page.getByRole("button", { name: "Authorize" }).click(); - - await expect( - page - .getByRole("paragraph") - .filter({ hasText: "The device has been authorized." }) - ).toBeVisible(); -}); - -test("Authorize new client with device authorization flow while not signed in", async ({ - page, -}) => { - await page.context().clearCookies(); - const client = oidcClients.immich; - const userCode = await oidcUtil.getUserCode(page, client.id, client.secret); - - await page.goto(`/device?code=${userCode}`); - - await (await passkeyUtil.init(page)).addPasskey(); - await page.getByRole("button", { name: "Authorize" }).click(); - - await expect( - page.getByTestId("scopes").getByRole("heading", { name: "Email" }) - ).toBeVisible(); - await expect( - page.getByTestId("scopes").getByRole("heading", { name: "Profile" }) - ).toBeVisible(); - - await page.getByRole("button", { name: "Authorize" }).click(); - - await expect( - page - .getByRole("paragraph") - .filter({ hasText: "The device has been authorized." }) - ).toBeVisible(); -}); - -test("Authorize existing client with device authorization flow", async ({ - page, -}) => { - const client = oidcClients.nextcloud; - const userCode = await oidcUtil.getUserCode(page, client.id, client.secret); - - await page.goto(`/device?code=${userCode}`); - - await expect( - page - .getByRole("paragraph") - .filter({ hasText: "The device has been authorized." }) - ).toBeVisible(); -}); - -test("Authorize existing client with device authorization flow while not signed in", async ({ - page, -}) => { - await page.context().clearCookies(); - const client = oidcClients.nextcloud; - const userCode = await oidcUtil.getUserCode(page, client.id, client.secret); - - await page.goto(`/device?code=${userCode}`); - - await (await passkeyUtil.init(page)).addPasskey(); - await page.getByRole("button", { name: "Authorize" }).click(); - - await expect( - page - .getByRole("paragraph") - .filter({ hasText: "The device has been authorized." }) - ).toBeVisible(); -}); - -test("Authorize client with device authorization flow with invalid code", async ({ - page, -}) => { - await page.goto("/device?code=invalid-code"); - - await expect( - page.getByRole("paragraph").filter({ hasText: "Invalid device code." }) - ).toBeVisible(); -}); - -test("Authorize new client with device authorization with user group not allowed", async ({ - page, -}) => { - await page.context().clearCookies(); - const client = oidcClients.immich; - const userCode = await oidcUtil.getUserCode(page, client.id, client.secret); - - await page.goto(`/device?code=${userCode}`); - - await (await passkeyUtil.init(page)).addPasskey("craig"); - await page.getByRole("button", { name: "Authorize" }).click(); - - await expect( - page.getByTestId("scopes").getByRole("heading", { name: "Email" }) - ).toBeVisible(); - await expect( - page.getByTestId("scopes").getByRole("heading", { name: "Profile" }) - ).toBeVisible(); - - await page.getByRole("button", { name: "Authorize" }).click(); - - await expect( - page - .getByRole("paragraph") - .filter({ hasText: "You're not allowed to access this service." }) - ).toBeVisible(); -}); - -test("Federated identity fails with invalid client assertion", async ({ - page, -}) => { - const client = oidcClients.federated; - - const res = await oidcUtil.exchangeCode(page, { - client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', - grant_type: 'authorization_code', - redirect_uri: client.callbackUrl, - code: client.accessCodes[0], - client_id: client.id, - client_assertion:'not-an-assertion', - }); - - expect(res?.error).toBe('Invalid client assertion'); -}); - -test("Authorize existing client with federated identity", async ({ - page, -}) => { - const client = oidcClients.federated; - const clientAssertion = await oidcUtil.getClientAssertion(page, client.federatedJWT); - - const res = await oidcUtil.exchangeCode(page, { - client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', - grant_type: 'authorization_code', - redirect_uri: client.callbackUrl, - code: client.accessCodes[0], - client_id: client.id, - client_assertion: clientAssertion, - }); - - expect(res.access_token).not.toBeNull; - expect(res.expires_in).not.toBeNull; - expect(res.token_type).toBe('Bearer'); + expect(res.access_token).not.toBeNull; + expect(res.expires_in).not.toBeNull; + expect(res.token_type).toBe('Bearer'); }); diff --git a/tests/specs/one-time-access-token.spec.ts b/tests/specs/one-time-access-token.spec.ts index 3b443092..5ebf217b 100644 --- a/tests/specs/one-time-access-token.spec.ts +++ b/tests/specs/one-time-access-token.spec.ts @@ -1,48 +1,48 @@ -import test, { expect } from "@playwright/test"; -import { oneTimeAccessTokens } from "../data"; -import { cleanupBackend } from "../utils/cleanup.util"; +import test, { expect } from '@playwright/test'; +import { oneTimeAccessTokens } from '../data'; +import { cleanupBackend } from '../utils/cleanup.util'; test.beforeEach(cleanupBackend); // Disable authentication for these tests test.use({ storageState: { cookies: [], origins: [] } }); -test("Sign in with login code", async ({ page }) => { - const token = oneTimeAccessTokens.filter((t) => !t.expired)[0]; - await page.goto(`/lc/${token.token}`); +test('Sign in with login code', async ({ page }) => { + const token = oneTimeAccessTokens.filter((t) => !t.expired)[0]; + await page.goto(`/lc/${token.token}`); - await page.waitForURL("/settings/account"); + await page.waitForURL('/settings/account'); }); -test("Sign in with login code entered manually", async ({ page }) => { - const token = oneTimeAccessTokens.filter((t) => !t.expired)[0]; - await page.goto("/lc"); +test('Sign in with login code entered manually', async ({ page }) => { + const token = oneTimeAccessTokens.filter((t) => !t.expired)[0]; + await page.goto('/lc'); - await page.getByPlaceholder("Code").first().fill(token.token); + await page.getByPlaceholder('Code').first().fill(token.token); - await page.getByText("Submit").first().click(); + await page.getByText('Submit').first().click(); - await page.waitForURL("/settings/account"); + await page.waitForURL('/settings/account'); }); -test("Sign in with expired login code fails", async ({ page }) => { - const token = oneTimeAccessTokens.filter((t) => t.expired)[0]; - await page.goto(`/lc/${token.token}`); +test('Sign in with expired login code fails', async ({ page }) => { + const token = oneTimeAccessTokens.filter((t) => t.expired)[0]; + await page.goto(`/lc/${token.token}`); - await expect(page.getByRole("paragraph")).toHaveText( - "Token is invalid or expired. Please try again." - ); + await expect(page.getByRole('paragraph')).toHaveText( + 'Token is invalid or expired. Please try again.' + ); }); -test("Sign in with login code entered manually fails", async ({ page }) => { - const token = oneTimeAccessTokens.filter((t) => t.expired)[0]; - await page.goto("/lc"); +test('Sign in with login code entered manually fails', async ({ page }) => { + const token = oneTimeAccessTokens.filter((t) => t.expired)[0]; + await page.goto('/lc'); - await page.getByPlaceholder("Code").first().fill(token.token); + await page.getByPlaceholder('Code').first().fill(token.token); - await page.getByText("Submit").first().click(); + await page.getByText('Submit').first().click(); - await expect(page.getByRole("paragraph")).toHaveText( - "Token is invalid or expired. Please try again." - ); + await expect(page.getByRole('paragraph')).toHaveText( + 'Token is invalid or expired. Please try again.' + ); }); diff --git a/tests/specs/user-group.spec.ts b/tests/specs/user-group.spec.ts index 03ddca1b..8bc11192 100644 --- a/tests/specs/user-group.spec.ts +++ b/tests/specs/user-group.spec.ts @@ -1,152 +1,115 @@ -import test, { expect } from "@playwright/test"; -import { userGroups, users } from "../data"; -import { cleanupBackend } from "../utils/cleanup.util"; +import test, { expect } from '@playwright/test'; +import { userGroups, users } from '../data'; +import { cleanupBackend } from '../utils/cleanup.util'; test.beforeEach(cleanupBackend); -test("Create user group", async ({ page }) => { - await page.goto("/settings/admin/user-groups"); - const group = userGroups.humanResources; +test('Create user group', async ({ page }) => { + await page.goto('/settings/admin/user-groups'); + const group = userGroups.humanResources; - await page.getByRole("button", { name: "Add Group" }).click(); - await page.getByLabel("Friendly Name").fill(group.friendlyName); + await page.getByRole('button', { name: 'Add Group' }).click(); + await page.getByLabel('Friendly Name').fill(group.friendlyName); - await page.getByRole("button", { name: "Save" }).click(); + await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "User group created successfully" - ); + await expect(page.locator('[data-type="success"]')).toHaveText('User group created successfully'); - await page.waitForURL("/settings/admin/user-groups/*"); + await page.waitForURL('/settings/admin/user-groups/*'); - await expect(page.getByLabel("Friendly Name")).toHaveValue( - group.friendlyName - ); - await expect(page.getByLabel("Name", { exact: true })).toHaveValue( - group.name - ); + await expect(page.getByLabel('Friendly Name')).toHaveValue(group.friendlyName); + await expect(page.getByLabel('Name', { exact: true })).toHaveValue(group.name); }); -test("Edit user group", async ({ page }) => { - await page.goto("/settings/admin/user-groups"); - const group = userGroups.developers; +test('Edit user group', async ({ page }) => { + await page.goto('/settings/admin/user-groups'); + const group = userGroups.developers; - await page - .getByRole("row", { name: group.name }) - .getByRole("button") - .click(); - await page.getByRole("menuitem", { name: "Edit" }).click(); + await page.getByRole('row', { name: group.name }).getByRole('button').click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); - await page.getByLabel("Friendly Name").fill("Developers updated"); + await page.getByLabel('Friendly Name').fill('Developers updated'); - await expect(page.getByLabel("Name", { exact: true })).toHaveValue( - group.name - ); + await expect(page.getByLabel('Name', { exact: true })).toHaveValue(group.name); - await page.getByLabel("Name", { exact: true }).fill("developers_updated"); + await page.getByLabel('Name', { exact: true }).fill('developers_updated'); - await page.getByRole("button", { name: "Save" }).nth(0).click(); + await page.getByRole('button', { name: 'Save' }).nth(0).click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "User group updated successfully" - ); - await expect(page.getByLabel("Friendly Name")).toHaveValue( - "Developers updated" - ); - await expect(page.getByLabel("Name", { exact: true })).toHaveValue( - "developers_updated" - ); + await expect(page.locator('[data-type="success"]')).toHaveText('User group updated successfully'); + await expect(page.getByLabel('Friendly Name')).toHaveValue('Developers updated'); + await expect(page.getByLabel('Name', { exact: true })).toHaveValue('developers_updated'); }); -test("Update user group users", async ({ page }) => { - const group = userGroups.designers; - await page.goto(`/settings/admin/user-groups/${group.id}`); +test('Update user group users', async ({ page }) => { + const group = userGroups.designers; + await page.goto(`/settings/admin/user-groups/${group.id}`); - await page - .getByRole("row", { name: users.tim.email }) - .getByRole("checkbox") - .click(); - await page - .getByRole("row", { name: users.craig.email }) - .getByRole("checkbox") - .click(); + await page.getByRole('row', { name: users.tim.email }).getByRole('checkbox').click(); + await page.getByRole('row', { name: users.craig.email }).getByRole('checkbox').click(); - await page.getByRole("button", { name: "Save" }).nth(1).click(); + await page.getByRole('button', { name: 'Save' }).nth(1).click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "Users updated successfully" - ); + await expect(page.locator('[data-type="success"]')).toHaveText('Users updated successfully'); - await page.reload(); + await page.reload(); - await expect( - page.getByRole("row", { name: users.tim.email }).getByRole("checkbox") - ).toHaveAttribute("data-state", "unchecked"); - await expect( - page.getByRole("row", { name: users.craig.email }).getByRole("checkbox") - ).toHaveAttribute("data-state", "checked"); + await expect( + page.getByRole('row', { name: users.tim.email }).getByRole('checkbox') + ).toHaveAttribute('data-state', 'unchecked'); + await expect( + page.getByRole('row', { name: users.craig.email }).getByRole('checkbox') + ).toHaveAttribute('data-state', 'checked'); }); -test("Delete user group", async ({ page }) => { - const group = userGroups.developers; - await page.goto("/settings/admin/user-groups"); +test('Delete user group', async ({ page }) => { + const group = userGroups.developers; + await page.goto('/settings/admin/user-groups'); - await page.getByRole("row", { name: group.name }).getByRole("button").click(); - await page.getByRole("menuitem", { name: "Delete" }).click(); - await page.getByRole("button", { name: "Delete" }).click(); + await page.getByRole('row', { name: group.name }).getByRole('button').click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'Delete' }).click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "User group deleted successfully" - ); - await expect(page.getByRole("row", { name: group.name })).not.toBeVisible(); + await expect(page.locator('[data-type="success"]')).toHaveText('User group deleted successfully'); + await expect(page.getByRole('row', { name: group.name })).not.toBeVisible(); }); -test("Update user group custom claims", async ({ page }) => { - await page.goto(`/settings/admin/user-groups/${userGroups.designers.id}`); +test('Update user group custom claims', async ({ page }) => { + await page.goto(`/settings/admin/user-groups/${userGroups.designers.id}`); - await page.getByRole("button", { name: "Expand card" }).click(); + await page.getByRole('button', { name: 'Expand card' }).click(); - // Add two custom claims - await page.getByRole("button", { name: "Add custom claim" }).click(); + // Add two custom claims + await page.getByRole('button', { name: 'Add custom claim' }).click(); - await page.getByPlaceholder("Key").fill("customClaim1"); - await page.getByPlaceholder("Value").fill("customClaim1_value"); + await page.getByPlaceholder('Key').fill('customClaim1'); + await page.getByPlaceholder('Value').fill('customClaim1_value'); - await page.getByRole("button", { name: "Add another" }).click(); - await page.getByPlaceholder("Key").nth(1).fill("customClaim2"); - await page.getByPlaceholder("Value").nth(1).fill("customClaim2_value"); + await page.getByRole('button', { name: 'Add another' }).click(); + await page.getByPlaceholder('Key').nth(1).fill('customClaim2'); + await page.getByPlaceholder('Value').nth(1).fill('customClaim2_value'); - await page.getByRole("button", { name: "Save" }).nth(2).click(); + await page.getByRole('button', { name: 'Save' }).nth(2).click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "Custom claims updated successfully" - ); + await expect(page.locator('[data-type="success"]')).toHaveText( + 'Custom claims updated successfully' + ); - await page.reload(); + await page.reload(); - // Check if custom claims are saved - await expect(page.getByPlaceholder("Key").first()).toHaveValue( - "customClaim1" - ); - await expect(page.getByPlaceholder("Value").first()).toHaveValue( - "customClaim1_value" - ); - await expect(page.getByPlaceholder("Key").nth(1)).toHaveValue("customClaim2"); - await expect(page.getByPlaceholder("Value").nth(1)).toHaveValue( - "customClaim2_value" - ); + // Check if custom claims are saved + await expect(page.getByPlaceholder('Key').first()).toHaveValue('customClaim1'); + await expect(page.getByPlaceholder('Value').first()).toHaveValue('customClaim1_value'); + await expect(page.getByPlaceholder('Key').nth(1)).toHaveValue('customClaim2'); + await expect(page.getByPlaceholder('Value').nth(1)).toHaveValue('customClaim2_value'); - // Remove one custom claim - await page.getByLabel("Remove custom claim").first().click(); - await page.getByRole("button", { name: "Save" }).nth(2).click(); + // Remove one custom claim + await page.getByLabel('Remove custom claim').first().click(); + await page.getByRole('button', { name: 'Save' }).nth(2).click(); - await page.reload(); + await page.reload(); - // Check if custom claim is removed - await expect(page.getByPlaceholder("Key").first()).toHaveValue( - "customClaim2" - ); - await expect(page.getByPlaceholder("Value").first()).toHaveValue( - "customClaim2_value" - ); + // Check if custom claim is removed + await expect(page.getByPlaceholder('Key').first()).toHaveValue('customClaim2'); + await expect(page.getByPlaceholder('Value').first()).toHaveValue('customClaim2_value'); }); diff --git a/tests/specs/user-settings.spec.ts b/tests/specs/user-settings.spec.ts index 2183eb1a..1b2df8d7 100644 --- a/tests/specs/user-settings.spec.ts +++ b/tests/specs/user-settings.spec.ts @@ -1,253 +1,217 @@ -import test, { expect } from "@playwright/test"; -import { userGroups, users } from "../data"; -import { cleanupBackend } from "../utils/cleanup.util"; +import test, { expect } from '@playwright/test'; +import { userGroups, users } from '../data'; +import { cleanupBackend } from '../utils/cleanup.util'; test.beforeEach(cleanupBackend); -test("Create user", async ({ page }) => { - const user = users.steve; +test('Create user', async ({ page }) => { + const user = users.steve; - await page.goto("/settings/admin/users"); + await page.goto('/settings/admin/users'); - await page.getByRole("button", { name: "Add User" }).click(); - await page.getByLabel("First name").fill(user.firstname); - await page.getByLabel("Last name").fill(user.lastname); - await page.getByLabel("Email").fill(user.email); - await page.getByLabel("Username").fill(user.username); - await page.getByRole("button", { name: "Save" }).click(); + await page.getByRole('button', { name: 'Add User' }).click(); + await page.getByLabel('First name').fill(user.firstname); + await page.getByLabel('Last name').fill(user.lastname); + await page.getByLabel('Email').fill(user.email); + await page.getByLabel('Username').fill(user.username); + await page.getByRole('button', { name: 'Save' }).click(); - await expect( - page.getByRole("row", { name: `${user.firstname} ${user.lastname}` }) - ).toBeVisible(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "User created successfully" - ); + await expect(page.getByRole('row', { name: `${user.firstname} ${user.lastname}` })).toBeVisible(); + await expect(page.locator('[data-type="success"]')).toHaveText('User created successfully'); }); -test("Create user fails with already taken email", async ({ page }) => { - const user = users.steve; +test('Create user fails with already taken email', async ({ page }) => { + const user = users.steve; - await page.goto("/settings/admin/users"); + await page.goto('/settings/admin/users'); - await page.getByRole("button", { name: "Add User" }).click(); - await page.getByLabel("First name").fill(user.firstname); - await page.getByLabel("Last name").fill(user.lastname); - await page.getByLabel("Email").fill(users.tim.email); - await page.getByLabel("Username").fill(user.username); - await page.getByRole("button", { name: "Save" }).click(); + await page.getByRole('button', { name: 'Add User' }).click(); + await page.getByLabel('First name').fill(user.firstname); + await page.getByLabel('Last name').fill(user.lastname); + await page.getByLabel('Email').fill(users.tim.email); + await page.getByLabel('Username').fill(user.username); + await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.locator('[data-type="error"]')).toHaveText( - "Email is already in use" - ); + await expect(page.locator('[data-type="error"]')).toHaveText('Email is already in use'); }); -test("Create user fails with already taken username", async ({ page }) => { - const user = users.steve; +test('Create user fails with already taken username', async ({ page }) => { + const user = users.steve; - await page.goto("/settings/admin/users"); + await page.goto('/settings/admin/users'); - await page.getByRole("button", { name: "Add User" }).click(); - await page.getByLabel("First name").fill(user.firstname); - await page.getByLabel("Last name").fill(user.lastname); - await page.getByLabel("Email").fill(user.email); - await page.getByLabel("Username").fill(users.tim.username); - await page.getByRole("button", { name: "Save" }).click(); + await page.getByRole('button', { name: 'Add User' }).click(); + await page.getByLabel('First name').fill(user.firstname); + await page.getByLabel('Last name').fill(user.lastname); + await page.getByLabel('Email').fill(user.email); + await page.getByLabel('Username').fill(users.tim.username); + await page.getByRole('button', { name: 'Save' }).click(); - await expect(page.locator('[data-type="error"]')).toHaveText( - "Username is already in use" - ); + await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use'); }); -test("Create one time access token", async ({ page, context }) => { - await page.goto("/settings/admin/users"); +test('Create one time access token', async ({ page, context }) => { + await page.goto('/settings/admin/users'); - await page - .getByRole("row", { - name: `${users.craig.firstname} ${users.craig.lastname}`, - }) - .getByRole("button") - .click(); + await page + .getByRole('row', { + name: `${users.craig.firstname} ${users.craig.lastname}` + }) + .getByRole('button') + .click(); - await page.getByRole("menuitem", { name: "Login Code" }).click(); + await page.getByRole('menuitem', { name: 'Login Code' }).click(); - await page.getByLabel("Expiration").click(); - await page.getByRole("option", { name: "12 hours" }).click(); - await page.getByRole("button", { name: "Show Code" }).click(); + await page.getByLabel('Expiration').click(); + await page.getByRole('option', { name: '12 hours' }).click(); + await page.getByRole('button', { name: 'Show Code' }).click(); - const link = await page.getByTestId("login-code-link").textContent(); - await context.clearCookies(); + const link = await page.getByTestId('login-code-link').textContent(); + await context.clearCookies(); - await page.goto(link!); - await page.waitForURL("/settings/account"); + await page.goto(link!); + await page.waitForURL('/settings/account'); }); -test("Delete user", async ({ page }) => { - await page.goto("/settings/admin/users"); +test('Delete user', async ({ page }) => { + await page.goto('/settings/admin/users'); - await page - .getByRole("row", { - name: `${users.craig.firstname} ${users.craig.lastname}`, - }) - .getByRole("button") - .click(); - await page.getByRole("menuitem", { name: "Delete" }).click(); - await page.getByRole("button", { name: "Delete" }).click(); + await page + .getByRole('row', { + name: `${users.craig.firstname} ${users.craig.lastname}` + }) + .getByRole('button') + .click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'Delete' }).click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "User deleted successfully" - ); - await expect( - page.getByRole("row", { - name: `${users.craig.firstname} ${users.craig.lastname}`, - }) - ).not.toBeVisible(); + await expect(page.locator('[data-type="success"]')).toHaveText('User deleted successfully'); + await expect( + page.getByRole('row', { + name: `${users.craig.firstname} ${users.craig.lastname}` + }) + ).not.toBeVisible(); }); -test("Update user", async ({ page }) => { - const user = users.craig; +test('Update user', async ({ page }) => { + const user = users.craig; - await page.goto("/settings/admin/users"); + await page.goto('/settings/admin/users'); - await page - .getByRole("row", { name: `${user.firstname} ${user.lastname}` }) - .getByRole("button") - .click(); - await page.getByRole("menuitem", { name: "Edit" }).click(); + await page + .getByRole('row', { name: `${user.firstname} ${user.lastname}` }) + .getByRole('button') + .click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); - await page.getByLabel("First name").fill("Crack"); - await page.getByLabel("Last name").fill("Apple"); - await page.getByLabel("Email").fill("crack.apple@test.com"); - await page.getByLabel("Username").fill("crack"); - await page.getByRole("button", { name: "Save" }).first().click(); + await page.getByLabel('First name').fill('Crack'); + await page.getByLabel('Last name').fill('Apple'); + await page.getByLabel('Email').fill('crack.apple@test.com'); + await page.getByLabel('Username').fill('crack'); + await page.getByRole('button', { name: 'Save' }).first().click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "User updated successfully" - ); + await expect(page.locator('[data-type="success"]')).toHaveText('User updated successfully'); }); -test("Update user fails with already taken email", async ({ page }) => { - const user = users.craig; +test('Update user fails with already taken email', async ({ page }) => { + const user = users.craig; - await page.goto("/settings/admin/users"); + await page.goto('/settings/admin/users'); - await page - .getByRole("row", { name: `${user.firstname} ${user.lastname}` }) - .getByRole("button") - .click(); - await page.getByRole("menuitem", { name: "Edit" }).click(); + await page + .getByRole('row', { name: `${user.firstname} ${user.lastname}` }) + .getByRole('button') + .click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); - await page.getByLabel("Email").fill(users.tim.email); - await page.getByRole("button", { name: "Save" }).first().click(); + await page.getByLabel('Email').fill(users.tim.email); + await page.getByRole('button', { name: 'Save' }).first().click(); - await expect(page.locator('[data-type="error"]')).toHaveText( - "Email is already in use" - ); + await expect(page.locator('[data-type="error"]')).toHaveText('Email is already in use'); }); -test("Update user fails with already taken username", async ({ page }) => { - const user = users.craig; +test('Update user fails with already taken username', async ({ page }) => { + const user = users.craig; - await page.goto("/settings/admin/users"); + await page.goto('/settings/admin/users'); - await page - .getByRole("row", { name: `${user.firstname} ${user.lastname}` }) - .getByRole("button") - .click(); - await page.getByRole("menuitem", { name: "Edit" }).click(); + await page + .getByRole('row', { name: `${user.firstname} ${user.lastname}` }) + .getByRole('button') + .click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); - await page.getByLabel("Username").fill(users.tim.username); - await page.getByRole("button", { name: "Save" }).first().click(); + await page.getByLabel('Username').fill(users.tim.username); + await page.getByRole('button', { name: 'Save' }).first().click(); - await expect(page.locator('[data-type="error"]')).toHaveText( - "Username is already in use" - ); + await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use'); }); -test("Update user custom claims", async ({ page }) => { - await page.goto(`/settings/admin/users/${users.craig.id}`); +test('Update user custom claims', async ({ page }) => { + await page.goto(`/settings/admin/users/${users.craig.id}`); - await page.getByRole("button", { name: "Expand card" }).nth(1).click(); + await page.getByRole('button', { name: 'Expand card' }).nth(1).click(); - // Add two custom claims - await page.getByRole("button", { name: "Add custom claim" }).click(); + // Add two custom claims + await page.getByRole('button', { name: 'Add custom claim' }).click(); - await page.getByPlaceholder("Key").fill("customClaim1"); - await page.getByPlaceholder("Value").fill("customClaim1_value"); + await page.getByPlaceholder('Key').fill('customClaim1'); + await page.getByPlaceholder('Value').fill('customClaim1_value'); - await page.getByRole("button", { name: "Add another" }).click(); - await page.getByPlaceholder("Key").nth(1).fill("customClaim2"); - await page.getByPlaceholder("Value").nth(1).fill("customClaim2_value"); + await page.getByRole('button', { name: 'Add another' }).click(); + await page.getByPlaceholder('Key').nth(1).fill('customClaim2'); + await page.getByPlaceholder('Value').nth(1).fill('customClaim2_value'); - await page.getByRole("button", { name: "Save" }).nth(1).click(); + await page.getByRole('button', { name: 'Save' }).nth(1).click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "Custom claims updated successfully" - ); + await expect(page.locator('[data-type="success"]')).toHaveText( + 'Custom claims updated successfully' + ); - await page.reload(); + await page.reload(); - // Check if custom claims are saved - await expect(page.getByPlaceholder("Key").first()).toHaveValue( - "customClaim1" - ); - await expect(page.getByPlaceholder("Value").first()).toHaveValue( - "customClaim1_value" - ); - await expect(page.getByPlaceholder("Key").nth(1)).toHaveValue("customClaim2"); - await expect(page.getByPlaceholder("Value").nth(1)).toHaveValue( - "customClaim2_value" - ); + // Check if custom claims are saved + await expect(page.getByPlaceholder('Key').first()).toHaveValue('customClaim1'); + await expect(page.getByPlaceholder('Value').first()).toHaveValue('customClaim1_value'); + await expect(page.getByPlaceholder('Key').nth(1)).toHaveValue('customClaim2'); + await expect(page.getByPlaceholder('Value').nth(1)).toHaveValue('customClaim2_value'); - // Remove one custom claim - await page.getByLabel("Remove custom claim").first().click(); - await page.getByRole("button", { name: "Save" }).nth(1).click(); + // Remove one custom claim + await page.getByLabel('Remove custom claim').first().click(); + await page.getByRole('button', { name: 'Save' }).nth(1).click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "Custom claims updated successfully" - ); + await expect(page.locator('[data-type="success"]')).toHaveText( + 'Custom claims updated successfully' + ); - await page.reload(); + await page.reload(); - // Check if custom claim is removed - await expect(page.getByPlaceholder("Key").first()).toHaveValue( - "customClaim2" - ); - await expect(page.getByPlaceholder("Value").first()).toHaveValue( - "customClaim2_value" - ); + // Check if custom claim is removed + await expect(page.getByPlaceholder('Key').first()).toHaveValue('customClaim2'); + await expect(page.getByPlaceholder('Value').first()).toHaveValue('customClaim2_value'); }); -test("Update user group assignments", async ({ page }) => { - const user = users.craig; - await page.goto(`/settings/admin/users/${user.id}`); +test('Update user group assignments', async ({ page }) => { + const user = users.craig; + await page.goto(`/settings/admin/users/${user.id}`); - page.getByRole("button", { name: "Expand card" }).first().click(); + page.getByRole('button', { name: 'Expand card' }).first().click(); - await page - .getByRole("row", { name: userGroups.developers.name }) - .getByRole("checkbox") - .click(); - await page - .getByRole("row", { name: userGroups.designers.name }) - .getByRole("checkbox") - .click(); + await page.getByRole('row', { name: userGroups.developers.name }).getByRole('checkbox').click(); + await page.getByRole('row', { name: userGroups.designers.name }).getByRole('checkbox').click(); - await page.getByRole("button", { name: "Save" }).nth(1).click(); + await page.getByRole('button', { name: 'Save' }).nth(1).click(); - await expect(page.locator('[data-type="success"]')).toHaveText( - "User groups updated successfully" - ); + await expect(page.locator('[data-type="success"]')).toHaveText( + 'User groups updated successfully' + ); - await page.reload(); + await page.reload(); - await expect( - page - .getByRole("row", { name: userGroups.designers.name }) - .getByRole("checkbox") - ).toHaveAttribute("data-state", "checked"); - await expect( - page - .getByRole("row", { name: userGroups.developers.name }) - .getByRole("checkbox") - ).toHaveAttribute("data-state", "unchecked"); + await expect( + page.getByRole('row', { name: userGroups.designers.name }).getByRole('checkbox') + ).toHaveAttribute('data-state', 'checked'); + await expect( + page.getByRole('row', { name: userGroups.developers.name }).getByRole('checkbox') + ).toHaveAttribute('data-state', 'unchecked'); }); diff --git a/tests/specs/user-signup.spec.ts b/tests/specs/user-signup.spec.ts index 08e836ed..9560a44e 100644 --- a/tests/specs/user-signup.spec.ts +++ b/tests/specs/user-signup.spec.ts @@ -6,170 +6,174 @@ import { users, signupTokens } from 'data'; test.beforeEach(cleanupBackend); test.describe('User Signup', () => { - async function setSignupMode(page: any, mode: 'Disabled' | 'Signup with token' | 'Open Signup') { - await page.goto('/settings/admin/application-configuration'); + async function setSignupMode(page: any, mode: 'Disabled' | 'Signup with token' | 'Open Signup') { + await page.goto('/settings/admin/application-configuration'); - await page.getByLabel('Enable user signups').click(); - await page.getByRole('option', { name: mode }).click(); + await page.getByLabel('Enable user signups').click(); + await page.getByRole('option', { name: mode }).click(); - await page.getByRole('button', { name: 'Save' }).first().click(); - await expect(page.locator('[data-type="success"]')).toHaveText('Application configuration updated successfully'); - await page.waitForLoadState('networkidle'); + await page.getByRole('button', { name: 'Save' }).first().click(); + await expect(page.locator('[data-type="success"]')).toHaveText( + 'Application configuration updated successfully' + ); + await page.waitForLoadState('networkidle'); - await page.context().clearCookies(); - await page.goto('/login'); - } + await page.context().clearCookies(); + await page.goto('/login'); + } - test('Signup is disabled - shows error message', async ({ page }) => { - await setSignupMode(page, 'Disabled'); + test('Signup is disabled - shows error message', async ({ page }) => { + await setSignupMode(page, 'Disabled'); - await page.goto('/signup'); + await page.goto('/signup'); - await expect(page.getByText('User signups are currently disabled')).toBeVisible(); - }); + await expect(page.getByText('User signups are currently disabled')).toBeVisible(); + }); - test('Signup with token - success flow', async ({ page }) => { - await setSignupMode(page, 'Signup with token'); + test('Signup with token - success flow', async ({ page }) => { + await setSignupMode(page, 'Signup with token'); - await page.goto(`/st/${signupTokens.valid.token}`); + await page.goto(`/st/${signupTokens.valid.token}`); - await page.getByLabel('First name').fill('John'); - await page.getByLabel('Last name').fill('Doe'); - await page.getByLabel('Username').fill('johndoe'); - await page.getByLabel('Email').fill('john.doe@test.com'); + await page.getByLabel('First name').fill('John'); + await page.getByLabel('Last name').fill('Doe'); + await page.getByLabel('Username').fill('johndoe'); + await page.getByLabel('Email').fill('john.doe@test.com'); - await page.getByRole('button', { name: 'Sign Up' }).click(); + await page.getByRole('button', { name: 'Sign Up' }).click(); - await page.waitForURL('/signup/add-passkey'); - await expect(page.getByText('Set up your passkey')).toBeVisible(); - }); + await page.waitForURL('/signup/add-passkey'); + await expect(page.getByText('Set up your passkey')).toBeVisible(); + }); - test('Signup with token - invalid token shows error', async ({ page }) => { - await setSignupMode(page, 'Signup with token'); + test('Signup with token - invalid token shows error', async ({ page }) => { + await setSignupMode(page, 'Signup with token'); - await page.goto('/st/invalid-token-123'); - await page.getByLabel('First name').fill('Complete'); - await page.getByLabel('Last name').fill('User'); - await page.getByLabel('Username').fill('completeuser'); - await page.getByLabel('Email').fill('complete.user@test.com'); - await page.getByRole('button', { name: 'Sign Up' }).click(); + await page.goto('/st/invalid-token-123'); + await page.getByLabel('First name').fill('Complete'); + await page.getByLabel('Last name').fill('User'); + await page.getByLabel('Username').fill('completeuser'); + await page.getByLabel('Email').fill('complete.user@test.com'); + await page.getByRole('button', { name: 'Sign Up' }).click(); - await expect(page.getByText('Token is invalid or expired.')).toBeVisible(); - }); + await expect(page.getByText('Token is invalid or expired.')).toBeVisible(); + }); - test('Signup with token - no token in URL shows error', async ({ page }) => { - await setSignupMode(page, 'Signup with token'); + test('Signup with token - no token in URL shows error', async ({ page }) => { + await setSignupMode(page, 'Signup with token'); - await page.goto('/signup'); + await page.goto('/signup'); - await expect(page.getByText('A valid signup token is required to create an account.')).toBeVisible(); - }); + await expect( + page.getByText('A valid signup token is required to create an account.') + ).toBeVisible(); + }); - test('Open signup - success flow', async ({ page }) => { - await setSignupMode(page, 'Open Signup'); + test('Open signup - success flow', async ({ page }) => { + await setSignupMode(page, 'Open Signup'); - await page.goto('/signup'); + await page.goto('/signup'); - await expect(page.getByText('Create your account to get started')).toBeVisible(); + await expect(page.getByText('Create your account to get started')).toBeVisible(); - await page.getByLabel('First name').fill('Jane'); - await page.getByLabel('Last name').fill('Smith'); - await page.getByLabel('Username').fill('janesmith'); - await page.getByLabel('Email').fill('jane.smith@test.com'); + await page.getByLabel('First name').fill('Jane'); + await page.getByLabel('Last name').fill('Smith'); + await page.getByLabel('Username').fill('janesmith'); + await page.getByLabel('Email').fill('jane.smith@test.com'); - await page.getByRole('button', { name: 'Sign Up' }).click(); + await page.getByRole('button', { name: 'Sign Up' }).click(); - await page.waitForURL('/signup/add-passkey'); - await expect(page.getByText('Set up your passkey')).toBeVisible(); - }); + await page.waitForURL('/signup/add-passkey'); + await expect(page.getByText('Set up your passkey')).toBeVisible(); + }); - test('Open signup - validation errors', async ({ page }) => { - await setSignupMode(page, 'Open Signup'); + test('Open signup - validation errors', async ({ page }) => { + await setSignupMode(page, 'Open Signup'); - await page.goto('/signup'); + await page.goto('/signup'); - await page.getByRole('button', { name: 'Sign Up' }).click(); + await page.getByRole('button', { name: 'Sign Up' }).click(); - await expect(page.getByText('Invalid input').first()).toBeVisible(); - }); + await expect(page.getByText('Invalid input').first()).toBeVisible(); + }); - test('Open signup - duplicate email shows error', async ({ page }) => { - await setSignupMode(page, 'Open Signup'); + test('Open signup - duplicate email shows error', async ({ page }) => { + await setSignupMode(page, 'Open Signup'); - await page.goto('/signup'); + await page.goto('/signup'); - await page.getByLabel('First name').fill('Test'); - await page.getByLabel('Last name').fill('User'); - await page.getByLabel('Username').fill('testuser123'); - await page.getByLabel('Email').fill(users.tim.email); + await page.getByLabel('First name').fill('Test'); + await page.getByLabel('Last name').fill('User'); + await page.getByLabel('Username').fill('testuser123'); + await page.getByLabel('Email').fill(users.tim.email); - await page.getByRole('button', { name: 'Sign Up' }).click(); + await page.getByRole('button', { name: 'Sign Up' }).click(); - await expect(page.getByText('Email is already in use.')).toBeVisible(); - }); + await expect(page.getByText('Email is already in use.')).toBeVisible(); + }); - test('Open signup - duplicate username shows error', async ({ page }) => { - await setSignupMode(page, 'Open Signup'); + test('Open signup - duplicate username shows error', async ({ page }) => { + await setSignupMode(page, 'Open Signup'); - await page.goto('/signup'); + await page.goto('/signup'); - await page.getByLabel('First name').fill('Test'); - await page.getByLabel('Last name').fill('User'); - await page.getByLabel('Username').fill(users.tim.username); - await page.getByLabel('Email').fill('newuser@test.com'); + await page.getByLabel('First name').fill('Test'); + await page.getByLabel('Last name').fill('User'); + await page.getByLabel('Username').fill(users.tim.username); + await page.getByLabel('Email').fill('newuser@test.com'); - await page.getByRole('button', { name: 'Sign Up' }).click(); + await page.getByRole('button', { name: 'Sign Up' }).click(); - await expect(page.getByText('Username is already in use.')).toBeVisible(); - }); + await expect(page.getByText('Username is already in use.')).toBeVisible(); + }); - test('Complete signup flow with passkey creation', async ({ page }) => { - await setSignupMode(page, 'Open Signup'); + test('Complete signup flow with passkey creation', async ({ page }) => { + await setSignupMode(page, 'Open Signup'); - await page.goto('/signup'); - await page.getByLabel('First name').fill('Complete'); - await page.getByLabel('Last name').fill('User'); - await page.getByLabel('Username').fill('completeuser'); - await page.getByLabel('Email').fill('complete.user@test.com'); - await page.getByRole('button', { name: 'Sign Up' }).click(); + await page.goto('/signup'); + await page.getByLabel('First name').fill('Complete'); + await page.getByLabel('Last name').fill('User'); + await page.getByLabel('Username').fill('completeuser'); + await page.getByLabel('Email').fill('complete.user@test.com'); + await page.getByRole('button', { name: 'Sign Up' }).click(); - await page.waitForURL('/signup/add-passkey'); + await page.waitForURL('/signup/add-passkey'); - await (await passkeyUtil.init(page)).addPasskey('timNew'); - await page.getByRole('button', { name: 'Add Passkey' }).click(); + await (await passkeyUtil.init(page)).addPasskey('timNew'); + await page.getByRole('button', { name: 'Add Passkey' }).click(); - await page.waitForURL('/settings/account'); - await expect(page.getByText('Single Passkey Configured')).toBeVisible(); - }); + await page.waitForURL('/settings/account'); + await expect(page.getByText('Single Passkey Configured')).toBeVisible(); + }); - test('Skip passkey creation during signup', async ({ page }) => { - await setSignupMode(page, 'Open Signup'); + test('Skip passkey creation during signup', async ({ page }) => { + await setSignupMode(page, 'Open Signup'); - await page.goto('/signup'); - await page.getByLabel('First name').fill('Skip'); - await page.getByLabel('Last name').fill('User'); - await page.getByLabel('Username').fill('skipuser'); - await page.getByLabel('Email').fill('skip.user@test.com'); - await page.getByRole('button', { name: 'Sign Up' }).click(); + await page.goto('/signup'); + await page.getByLabel('First name').fill('Skip'); + await page.getByLabel('Last name').fill('User'); + await page.getByLabel('Username').fill('skipuser'); + await page.getByLabel('Email').fill('skip.user@test.com'); + await page.getByRole('button', { name: 'Sign Up' }).click(); - await page.waitForURL('/signup/add-passkey'); + await page.waitForURL('/signup/add-passkey'); - await page.getByRole('button', { name: 'Skip for now' }).click(); + await page.getByRole('button', { name: 'Skip for now' }).click(); - await page.waitForURL('/settings/account'); - await expect(page.getByText('Passkey missing')).toBeVisible(); - }); + await page.waitForURL('/settings/account'); + await expect(page.getByText('Passkey missing')).toBeVisible(); + }); - test('Token usage limit is enforced', async ({ page }) => { - await setSignupMode(page, 'Signup with token'); + test('Token usage limit is enforced', async ({ page }) => { + await setSignupMode(page, 'Signup with token'); - await page.goto(`/st/${signupTokens.fullyUsed.token}`); - await page.getByLabel('First name').fill('Complete'); - await page.getByLabel('Last name').fill('User'); - await page.getByLabel('Username').fill('completeuser'); - await page.getByLabel('Email').fill('complete.user@test.com'); - await page.getByRole('button', { name: 'Sign Up' }).click(); + await page.goto(`/st/${signupTokens.fullyUsed.token}`); + await page.getByLabel('First name').fill('Complete'); + await page.getByLabel('Last name').fill('User'); + await page.getByLabel('Username').fill('completeuser'); + await page.getByLabel('Email').fill('complete.user@test.com'); + await page.getByRole('button', { name: 'Sign Up' }).click(); - await expect(page.getByText('Token is invalid or expired.')).toBeVisible(); - }); + await expect(page.getByText('Token is invalid or expired.')).toBeVisible(); + }); }); diff --git a/tests/tsconfig.json b/tests/tsconfig.json index 2adb063f..0b392b76 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -1,6 +1,6 @@ { - "compilerOptions": { - "baseUrl": ".", - "lib": ["ES2022"] - } + "compilerOptions": { + "baseUrl": ".", + "lib": ["ES2022"] + } } diff --git a/tests/utils/cleanup.util.ts b/tests/utils/cleanup.util.ts index a2317f95..f9e25c7f 100644 --- a/tests/utils/cleanup.util.ts +++ b/tests/utils/cleanup.util.ts @@ -1,19 +1,17 @@ -import playwrightConfig from "../playwright.config"; +import playwrightConfig from '../playwright.config'; export async function cleanupBackend() { - const url = new URL("/api/test/reset", playwrightConfig.use!.baseURL); + const url = new URL('/api/test/reset', playwrightConfig.use!.baseURL); - if (process.env.SKIP_LDAP_TESTS === "true") { - url.searchParams.append("skip-ldap", "true"); - } + if (process.env.SKIP_LDAP_TESTS === 'true') { + url.searchParams.append('skip-ldap', 'true'); + } - const response = await fetch(url, { - method: "POST", - }); + const response = await fetch(url, { + method: 'POST' + }); - if (!response.ok) { - throw new Error( - `Failed to reset backend: ${response.status} ${response.statusText}` - ); - } + if (!response.ok) { + throw new Error(`Failed to reset backend: ${response.status} ${response.statusText}`); + } } diff --git a/tests/utils/jwt.util.ts b/tests/utils/jwt.util.ts index 07a2b584..081c08c7 100644 --- a/tests/utils/jwt.util.ts +++ b/tests/utils/jwt.util.ts @@ -1,64 +1,56 @@ -import * as jose from "jose"; -import playwrightConfig from "../playwright.config"; +import * as jose from 'jose'; +import playwrightConfig from '../playwright.config'; const PRIVATE_KEY_STRING = `{"alg":"RS256","d":"mvMDWSdPPvcum0c0iEHE2gbqtV2NKMmLwrl9E6K7g8lTV95SePLnW_bwyMPV7EGp7PQk3l17I5XRhFjze7GqTnFIOgKzMianPs7jv2ELtBMGK0xOPATgu1iGb70xZ6vcvuEfRyY3dJ0zr4jpUdVuXwKmx9rK4IdZn2dFCKfvSuspqIpz11RhF1ALrqDLkxGVv7ZwNh0_VhJZU9hcjG5l6xc7rQEKpPRkZp0IdjkGS8Z0FskoVaiRIWAbZuiVFB9WCW8k1czC4HQTPLpII01bUQx2ludbm0UlXRgVU9ptUUbU7GAImQqTOW8LfPGklEvcgzlIlR_oqw4P9yBxLi-yMQ","dp":"pvNCSnnhbo8Igw9psPR-DicxFnkXlu_ix4gpy6efTrxA-z1VDFDioJ814vKQNioYDzpyAP1gfMPhRkvG_q0hRZsJah3Sb9dfA-WkhSWY7lURQP4yIBTMU0PF_rEATuS7lRciYk1SOx5fqXZd3m_LP0vpBC4Ujlq6NAq6CIjCnms","dq":"TtUVGCCkPNgfOLmkYXu7dxxUCV5kB01-xAEK2OY0n0pG8vfDophH4_D_ZC7nvJ8J9uDhs_3JStexq1lIvaWtG99RNTChIEDzpdn6GH9yaVcb_eB4uJjrNm64FhF8PGCCwxA-xMCZMaARKwhMB2_IOMkxUbWboL3gnhJ2rDO_QO0","e":"AQAB","kid":"8uHDw3M6rf8","kty":"RSA","n":"yaeEL0VKoPBXIAaWXsUgmu05lAvEIIdJn0FX9lHh4JE5UY9B83C5sCNdhs9iSWzpeP11EVjWp8i3Yv2CF7c7u50BXnVBGtxpZpFC-585UXacoJ0chUmarL9GRFJcM1nPHBTFu68aRrn1rIKNHUkNaaxFo0NFGl_4EDDTO8HwawTjwkPoQlRzeByhlvGPVvwgB3Fn93B8QJ_cZhXKxJvjjrC_8Pk76heC_ntEMru71Ix77BoC3j2TuyiN7m9RNBW8BU5q6lKoIdvIeZfTFLzi37iufyfvMrJTixp9zhNB1NxlLCeOZl2MXegtiGqd2H3cbAyqoOiv9ihUWTfXj7SxJw","p":"_Yylc9e07CKdqNRD2EosMC2mrhrEa9j5oY_l00Qyy4-jmCA59Q9viyqvveRo0U7cRvFA5BWgWN6GGLh1DG3X-QBqVr0dnk3uzbobb55RYUXyPLuBZI2q6w2oasbiDwPdY7KpkVv_H-bpITQlyDvO8hhucA6rUV7F6KTQVz8M3Ms","q":"y5p3hch-7jJ21TkAhp_Vk1fLCAuD4tbErwQs2of9ja8sB4iJOs5Wn6HD3P7Mc8Plye7qaLHvzc8I5g0tPKWvC0DPd_FLPXiWwMVAzee3NUX_oGeJNOQp11y1w_KqdO9qZqHSEPZ3NcFL_SZMFgggxhM1uzRiPzsVN0lnD_6prZU","qi":"2Grt6uXHm61ji3xSdkBWNtUnj19vS1-7rFJp5SoYztVQVThf_W52BAiXKBdYZDRVoItC_VS2NvAOjeJjhYO_xQ_q3hK7MdtuXfEPpLnyXKkmWo3lrJ26wbeF6l05LexCkI7ShsOuSt-dsyaTJTszuKDIA6YOfWvfo3aVZmlWRaI","use":"sig"}`; type User = { - id: string; - email: string; - firstname: string; - lastname: string; + id: string; + email: string; + firstname: string; + lastname: string; }; const privateKey = JSON.parse(PRIVATE_KEY_STRING); -const privateKeyImported = await jose.importJWK(privateKey, "RS256"); +const privateKeyImported = await jose.importJWK(privateKey, 'RS256'); -export async function generateIdToken( - user: User, - clientId: string, - expired = false -) { - const now = Math.floor(Date.now() / 1000); - const expiration = expired ? now + 1 : now + 1000000000; // Either expired or valid for a long time +export async function generateIdToken(user: User, clientId: string, expired = false) { + const now = Math.floor(Date.now() / 1000); + const expiration = expired ? now + 1 : now + 1000000000; // Either expired or valid for a long time - const payload = { - aud: clientId, - email: user.email, - email_verified: true, - exp: expiration, - family_name: user.lastname, - given_name: user.firstname, - iat: now, - iss: playwrightConfig.use!.baseURL, - name: `${user.firstname} ${user.lastname}`, - nonce: "oW1A1O78GQ15D73OsHEx7WQKj7ZqvHLZu_37mdXIqAQ", - sub: user.id, - type: "id-token", - }; + const payload = { + aud: clientId, + email: user.email, + email_verified: true, + exp: expiration, + family_name: user.lastname, + given_name: user.firstname, + iat: now, + iss: playwrightConfig.use!.baseURL, + name: `${user.firstname} ${user.lastname}`, + nonce: 'oW1A1O78GQ15D73OsHEx7WQKj7ZqvHLZu_37mdXIqAQ', + sub: user.id, + type: 'id-token' + }; - return await new jose.SignJWT(payload) - .setProtectedHeader({ alg: "RS256", kid: privateKey.kid, typ: "JWT" }) - .sign(privateKeyImported); + return await new jose.SignJWT(payload) + .setProtectedHeader({ alg: 'RS256', kid: privateKey.kid, typ: 'JWT' }) + .sign(privateKeyImported); } -export async function generateOauthAccessToken( - user: User, - clientId: string, - expired = false -) { - const now = Math.floor(Date.now() / 1000); - const expiration = expired ? now - 1000 : now + 1000000000; // Either expired or valid for a long time +export async function generateOauthAccessToken(user: User, clientId: string, expired = false) { + const now = Math.floor(Date.now() / 1000); + const expiration = expired ? now - 1000 : now + 1000000000; // Either expired or valid for a long time - const payload = { - aud: [clientId], - exp: expiration, - iat: now, - iss: playwrightConfig.use!.baseURL, - sub: user.id, - type: "oauth-access-token", - }; + const payload = { + aud: [clientId], + exp: expiration, + iat: now, + iss: playwrightConfig.use!.baseURL, + sub: user.id, + type: 'oauth-access-token' + }; - return await new jose.SignJWT(payload) - .setProtectedHeader({ alg: "RS256", kid: privateKey.kid, typ: "JWT" }) - .sign(privateKeyImported); + return await new jose.SignJWT(payload) + .setProtectedHeader({ alg: 'RS256', kid: privateKey.kid, typ: 'JWT' }) + .sign(privateKeyImported); } diff --git a/tests/utils/oidc.util.ts b/tests/utils/oidc.util.ts index 2600c063..27527c4f 100644 --- a/tests/utils/oidc.util.ts +++ b/tests/utils/oidc.util.ts @@ -1,6 +1,10 @@ import type { Page } from '@playwright/test'; -export async function getUserCode(page: Page, clientId: string, clientSecret: string): Promise { +export async function getUserCode( + page: Page, + clientId: string, + clientSecret: string +): Promise { return page.request .post('/api/oidc/device/authorize', { headers: { @@ -16,25 +20,31 @@ export async function getUserCode(page: Page, clientId: string, clientSecret: st .then((r) => r.user_code); } -export async function exchangeCode(page: Page, params: Record): Promise<{access_token?: string, token_type?: string, expires_in?: number, error?: string}> { +export async function exchangeCode( + page: Page, + params: Record +): Promise<{ access_token?: string; token_type?: string; expires_in?: number; error?: string }> { return page.request .post('/api/oidc/token', { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - form: params, + form: params }) .then((r) => r.json()); } -export async function getClientAssertion(page: Page, data: {issuer: string, audience: string, subject: string}): Promise { +export async function getClientAssertion( + page: Page, + data: { issuer: string; audience: string; subject: string } +): Promise { return page.request .post('/api/externalidp/sign', { data: { iss: data.issuer, aud: data.audience, - sub: data.subject, - }, + sub: data.subject + } }) .then((r) => r.text()); -} \ No newline at end of file +}