diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..02180e6 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,49 @@ +name: E2E Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + e2e: + name: E2E Tests (${{ matrix.neos }}) + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + neos: [neos8, neos9] + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version-file: Tests/E2E/.nvmrc + cache: npm + cache-dependency-path: Tests/E2E/package-lock.json + + - name: Install dependencies + working-directory: Tests/E2E + run: npm ci + + - name: Install Playwright browsers + working-directory: Tests/E2E + run: npx playwright install --with-deps chromium + + - name: Pre-build Docker image + run: docker compose -f Tests/E2E/system_under_test/${{ matrix.neos }}/docker-compose.yaml build --pull + + - name: Run Tests + working-directory: Tests/E2E + run: npm run test:${{ matrix.neos }} + + - name: Upload Playwright report + uses: actions/upload-artifact@v7 + if: ${{ always() }} + with: + name: playwright-report-${{ matrix.neos }} + path: Tests/E2E/playwright-report/ + retention-days: 7 diff --git a/Tests/E2E/.gitignore b/Tests/E2E/.gitignore new file mode 100644 index 0000000..cb5de66 --- /dev/null +++ b/Tests/E2E/.gitignore @@ -0,0 +1,7 @@ +# 3rd party sources +node_modules/ + +# transient test files +.features-gen/ +test-results/ +playwright-report/ diff --git a/Tests/E2E/.npmrc b/Tests/E2E/.npmrc new file mode 100644 index 0000000..7230105 --- /dev/null +++ b/Tests/E2E/.npmrc @@ -0,0 +1 @@ +min-release-age = 7 # days diff --git a/Tests/E2E/.nvmrc b/Tests/E2E/.nvmrc new file mode 100644 index 0000000..8e35034 --- /dev/null +++ b/Tests/E2E/.nvmrc @@ -0,0 +1 @@ +24.14.1 diff --git a/Tests/E2E/.prettierrc.yaml b/Tests/E2E/.prettierrc.yaml new file mode 100644 index 0000000..0e3ebcd --- /dev/null +++ b/Tests/E2E/.prettierrc.yaml @@ -0,0 +1 @@ +# using prettier defaults diff --git a/Tests/E2E/Makefile b/Tests/E2E/Makefile new file mode 100644 index 0000000..1fb8432 --- /dev/null +++ b/Tests/E2E/Makefile @@ -0,0 +1,100 @@ +NEOS8_COMPOSE = $(CURDIR)/system_under_test/neos8/docker-compose.yaml +NEOS9_COMPOSE = $(CURDIR)/system_under_test/neos9/docker-compose.yaml + +.SILENT: +.PHONY: help \ + setup setup-sut setup-test \ + generate-bdd-files \ + test test-neos8 test-neos9 \ + start-sut-neos8 start-sut-neos9 \ + log-sut-neos8 log-sut-neos9 \ + enter-sut-neos8 enter-sut-neos9 \ + sut-down + +# COLORS +GREEN := $(shell tput -Txterm setaf 2) +YELLOW := $(shell tput -Txterm setaf 3) +RESET := $(shell tput -Txterm sgr0) + +# running `make` without a target shows the help +.DEFAULT_GOAL := help + +## Show this help +help: + echo "" + echo "Usage: $(YELLOW)make $(RESET)" + awk -v green="$(GREEN)" -v yellow="$(YELLOW)" -v reset="$(RESET)" ' \ + /^##@ / { printf "\n%s%s%s\n", green, substr($$0, 5), reset; next } \ + /^## / { doc = substr($$0, 4); next } \ + /^[a-zA-Z0-9_-]+:/ { if (doc != "") { sub(":$$", "", $$1); printf " %s%-20s%s %s\n", yellow, $$1, reset, doc; doc = "" } } \ + ' $(MAKEFILE_LIST) + echo "" + +##@ Setup + +## Build SUT images and install the test setup +setup: setup-sut setup-test + +## Build the SUT docker images +setup-sut: + docker compose -f $(NEOS8_COMPOSE) build --pull + docker compose -f $(NEOS9_COMPOSE) build --pull + +## Install nodejs, npm dependencies and playwright browsers +setup-test: + echo "${GREEN}Installing test setup.${RESET}" + if [ -s "$$NVM_DIR" ]; then \ + . "$$NVM_DIR/nvm.sh" && echo "${GREEN}Found nvm on system -> using it to install nodejs!${RESET}" && nvm install; \ + fi && \ + npm install && npx playwright install --with-deps chromium && \ + echo "" && echo "${GREEN}generate BDD files from feature files${RESET}" && npm run generate-tests + +##@ Tests + +## Generate BDD files from feature files +generate-bdd-files: + echo "${GREEN}generate BDD files from feature files${RESET}" + npm run generate-tests + +## Run all E2E tests +test: test-neos8 test-neos9 + +## Run all Neos 8 E2E tests +test-neos8: + npm run test:neos8 + +## Run all neos9 E2E tests +test-neos9: + npm run test:neos9 + +##@ System under test (SUT) + +## Start the Neos 8 SUT containers +start-sut-neos8: + docker compose -f $(NEOS8_COMPOSE) up -d --build + +## Start the Neos 9 SUT containers +start-sut-neos9: + docker compose -f $(NEOS9_COMPOSE) up -d --build + +## Show Neos 8 SUT container logs +log-sut-neos8: + docker compose -f $(NEOS8_COMPOSE) logs -f + +## Show Neos 9 SUT container logs +log-sut-neos9: + docker compose -f $(NEOS9_COMPOSE) logs -f + +## Enter Neos 8 SUT +enter-sut-neos8: + docker compose -f $(NEOS8_COMPOSE) exec neos bash + +## Enter Neos 9 SUT +enter-sut-neos9: + docker compose -f $(NEOS9_COMPOSE) exec neos bash + +## Tear down all docker compose environments and remove volumes +sut-down: + echo "${YELLOW}Shutting down all SUTs and removing their volumes.${RESET}" + docker compose -f $(NEOS8_COMPOSE) down -v + docker compose -f $(NEOS9_COMPOSE) down -v diff --git a/Tests/E2E/README.md b/Tests/E2E/README.md new file mode 100644 index 0000000..474900f --- /dev/null +++ b/Tests/E2E/README.md @@ -0,0 +1,263 @@ +# E2E Tests + +End-to-end tests for `sandstorm/usermanagement`, using [Playwright](https://playwright.dev) with [playwright-bdd](https://vitalets.github.io/playwright-bdd/) for Gherkin-style BDD scenarios. Tests run against a Dockerised Neos instance (the *system under test*, SUT) — no local Neos installation required. + +Tests are executed against both **Neos 8** (PHP 8.2, MariaDB 10.11) and **Neos 9** (PHP 8.5, MariaDB 11.4). + +## Prerequisites + +- Docker +- Node.js (the version pinned in `.nvmrc`) — or [nvm](https://github.com/nvm-sh/nvm), which the setup script uses automatically if available +- make + +## Setup + +Run once after cloning: + +```bash +cd Tests/E2E +make setup +``` + +This will: +1. Build the Docker images for both Neos 8 and Neos 9 (`make setup-sut`) +2. Install the pinned Node.js version via nvm (if nvm is available) +3. Install npm dependencies and the Playwright Chromium browser +4. Generate the Playwright test files from the Gherkin feature files + +All `make` targets are run from `Tests/E2E`. Running `make` without a target prints the list of available targets. + +> `.npmrc` sets `min-release-age = 7` days, so `npm install` ignores package versions published within the last week. This is a deliberate supply-chain safeguard — remove it only if you know what you are doing. + +## Running tests + +```bash +# Run against both Neos 8 and Neos 9 +make test + +# Run against Neos 8 only +make test-neos8 + +# Run against Neos 9 only +make test-neos9 +``` + +Playwright starts the Docker containers automatically before each run and stops them afterwards (see `global-teardown.ts`). The first run may take a few minutes while Neos sets itself up inside the container (migrations, demo site import). + +### How the SUT works + +- `system_under_test/Dockerfile` builds a [FrankenPHP](https://frankenphp.dev) image and installs a `neos/neos-base-distribution` matching the target Neos major version. +- Docker Compose mounts this package's `Classes/`, `Configuration/`, `Migrations/`, `Resources/` and `composer.json` into the container at `/app/DistributionPackages/Sandstorm.Usermanagement`, so your local changes are picked up without rebuilding the image. +- The entrypoint registers that directory as a Composer path repository, requires `sandstorm/usermanagement:@dev`, waits for the database, runs `doctrine:migrate`, imports the `Neos.Demo` site and serves the instance on — which is Playwright's `baseURL`. + +### SUT and FLOW_CONTEXT + +Each npm test script sets two environment variables: + +- **`SUT`** (`neos8` or `neos9`) — selects which Docker Compose environment to start. It is also used to derive the container name (`$SUT-neos-1`) for the Flow CLI helpers in `helpers/system.ts`. +- **`FLOW_CONTEXT`** — selects a Neos Flow configuration context. Both scripts default to `Production/E2E-SUT`, which loads the configuration files in `system_under_test/sut_file_system_overrides/app/Configuration/Production/E2E-SUT/`. + +To test a different application configuration (e.g. with or without a feature enabled), add configuration files under a sub-context such as `Production/E2E-SUT/my-variant/` and add a matching npm script in `package.json` that sets `FLOW_CONTEXT=Production/E2E-SUT/my-variant`. + +## Container management + +When you need to inspect a running container or debug a failure: + +```bash +# Start containers in the background (without running tests) +make start-sut-neos8 +make start-sut-neos9 + +# Stream container logs +make log-sut-neos8 +make log-sut-neos9 + +# Open a bash shell inside a running container +make enter-sut-neos8 +make enter-sut-neos9 + +# Stop all containers and delete their volumes +make sut-down +``` + +Neos 8 and Neos 9 use separate volumes and networks, but both publish the same host ports (`8081` for Neos, `13306` for MariaDB) — so only one SUT can run at a time. `make test` therefore runs them one after another. + +## Continuous integration + +`.github/workflows/e2e.yml` runs the suite on pushes and pull requests against `main`, as a matrix over `neos8` and `neos9`. The Playwright HTML report is uploaded as a build artifact (`playwright-report-`) for every run, including failures. + +## Directory structure + +``` +Tests/E2E/ +├── Makefile # Run `make` for a list of targets +├── README.md # this file +├── features/ # Gherkin feature files (.feature) +│ └── login.feature +├── steps/ # TypeScript step definitions +│ ├── login.steps.ts +│ └── hooks.ts # AfterScenario cleanup +├── helpers/ +│ ├── pages/ # Page Object Model classes +│ │ ├── loginPage.ts +│ │ └── contentPage.ts +│ └── system.ts # Flow CLI utilities (via docker exec) +├── playwright.config.ts +├── global-teardown.ts # shuts the SUT down after a run +├── package.json +├── tsconfig.json +├── .nvmrc # pinned Node.js version +├── .npmrc +├── .prettierrc.yaml +└── system_under_test/ + ├── Dockerfile + ├── sut-base-docker-compose.yaml # shared compose base (neos, db, redis) + ├── neos8/ + │ ├── docker-compose.yaml # includes the base + the overrides below + │ ├── compose-overrides-neos8.yaml # PHP 8.2, Neos 8, own volume/network + │ └── entrypoint.sh + ├── neos9/ + │ ├── docker-compose.yaml + │ ├── compose-overrides-neos9.yaml # PHP 8.5, Neos 9, MariaDB 11.4 + │ └── entrypoint.sh + └── sut_file_system_overrides/ # Neos/Caddy config baked into the image +``` + +--- + +## Writing new tests + +Tests are written in two parts: a **feature file** (what to test, in plain language) and a **steps file** (how to do it, in TypeScript). + +### 0. IDE Goodies + +Syntax highlighting, "Go to Definition" from feature files to step implementations, and other IDE features are available with the right setup: + +#### VSCode +1. Install the official Cucumber extension (`CucumberOpen.cucumber-official`) for syntax highlighting and step definition navigation. +2. Add the path "./steps/**/*.ts" to the extension's `glue` configuration to enable "Go to Definition" from feature files to step implementations. + ``` + { + "cucumber.glue": [ + "steps/**/*.steps.ts", + ... + ] + } + ``` + +#### JetBrains IDEs +1. Install the "Gherkin" plugin by JetBrains. +2. Install the "Cucumber.js" plugin by JetBrains for step definition navigation. +3. Configure the `.features-gen` to be "Excluded" in the project structure to avoid cluttering the navigation context menu with auto-generated files. + +### 1. Write a feature file + +Create a `.feature` file under `features/`. For larger suites, organise by feature area in sub-directories: + +```gherkin +# features/my-feature/my-scenario.feature +@default-context +Feature: My feature description + + Background: + Given A user with username "admin", password "password" and role "Neos.Neos:Administrator" exists + + Scenario: Admin can do the thing + When I log in with username "admin" and password "password" + And I navigate to the thing + Then I should see the expected result +``` + +Tags (like `@default-context` above) are optional documentation by default, but they can be used to select scenarios via `npx playwright test --grep @default-context` — handy when a group of scenarios only makes sense for a specific `FLOW_CONTEXT`. + +### 2. Implement missing steps + +Reuse existing steps from `steps/` where possible. `login.steps.ts` already provides: + +- `Given A user with username {string}, password {string} and role {string} exists` +- `When I log in with username {string} and password {string}` +- `When I log out` +- `Then I should see the Neos content page` +- `Then I cannot access the Neos content page` + +If a step doesn't exist yet, add it to a new or existing steps file: + +```typescript +// steps/my-feature.steps.ts +import { expect } from "@playwright/test"; +import { createBdd } from "playwright-bdd"; + +const { Given, When, Then } = createBdd(); + +When("I navigate to the thing", async ({ page }) => { + await page.goto("/my-path"); +}); + +Then("I should see the expected result", async ({ page }) => { + await expect(page.locator(".my-selector")).toBeVisible(); +}); +``` + +Steps are matched by exact string (including `{string}` parameters). A step defined in any file under `steps/` is available in all feature files. + +### 3. Add Page Objects for new pages + +If you are testing a new page, add a class under `helpers/pages/`, following the existing `loginPage.ts` / `contentPage.ts`: + +```typescript +// helpers/pages/myFeaturePage.ts +import type { Page } from "@playwright/test"; + +export default class MyFeaturePage { + constructor(private readonly page: Page) {} + + async goto() { + await this.page.goto("/my-path"); + } + + async clickTheButton() { + await this.page.locator(".my-button").click(); + } +} +``` + +Keep selectors inside the page object and out of the steps — that way a UI change only has to be fixed in one place. + +### 4. Regenerate test files + +playwright-bdd generates Playwright test files (into `.features-gen/`) from your feature files. After adding or changing feature files run: + +```bash +make generate-bdd-files +``` + +This is done automatically by `make setup` and by every `make test*` target, but you can run it manually during development. + +### 5. Use Flow CLI in steps + +`helpers/system.ts` exposes utilities that run Neos Flow CLI commands inside the Docker container of the currently selected `SUT`: + +```typescript +import { createUser, removeAllUsers, logout } from "../helpers/system.ts"; + +// Create a Flow user (synchronous — runs docker exec) +createUser("myuser", "password", ["Neos.Neos:Administrator"]); + +// Remove all users (used in the AfterScenario hook for cleanup) +removeAllUsers(); + +// End the current browser session +await logout(page); +``` + +You can add more Flow CLI wrappers to `system.ts` following the same pattern. + +### Cleanup + +The `AfterScenario` hook in `steps/hooks.ts` logs out the current browser session and removes all users after every scenario, keeping tests isolated. If your tests create other persistent data, add cleanup logic there. + +## Disclaimer + +This is just a template. It is meant to jump start your own E2E test suite. + +You can use all the playwright features you want (like `--ui`, `--debug`, `--grep`, etc.) — the Makefile targets are just thin wrappers around `npx playwright test` that set up the environment variables and Docker containers for you. Feel free to modify the setup as needed. diff --git a/Tests/E2E/features/login.feature b/Tests/E2E/features/login.feature new file mode 100644 index 0000000..4ed2efa --- /dev/null +++ b/Tests/E2E/features/login.feature @@ -0,0 +1,14 @@ +@default-context +Feature: Login flow with default settings + + Background: + Given A user with username "admin", password "password" and role "Neos.Neos:Administrator" exists + And A user with username "editor", password "password" and role "Neos.Neos:Editor" exists + + Scenario: Admin user can log in + When I log in with username "admin" and password "password" + Then I should see the Neos content page + + Scenario: Editor user can log in + When I log in with username "editor" and password "password" + Then I should see the Neos content page diff --git a/Tests/E2E/global-teardown.ts b/Tests/E2E/global-teardown.ts new file mode 100644 index 0000000..52f5135 --- /dev/null +++ b/Tests/E2E/global-teardown.ts @@ -0,0 +1,15 @@ +import { execSync } from "node:child_process"; +import { dirname } from "node:path"; + +const SUT = process.env.SUT; + +/** + * This is run after all tests have finished. + * Because we use docker containers, we only have to shut them down. + */ +export default async function globalTeardown() { + execSync(`docker compose -f ./system_under_test/${SUT}/docker-compose.yaml down -v`, { + stdio: "inherit", + cwd: dirname("."), + }); +} diff --git a/Tests/E2E/helpers/pages/contentPage.ts b/Tests/E2E/helpers/pages/contentPage.ts new file mode 100644 index 0000000..b951077 --- /dev/null +++ b/Tests/E2E/helpers/pages/contentPage.ts @@ -0,0 +1,11 @@ +import type { Page } from "@playwright/test"; + +export default class NeosContentPage { + public readonly URL_REGEX = /neos\/content/; + + constructor(private readonly page: Page) {} + + async goto() { + await this.page.goto("/neos/content"); + } +} diff --git a/Tests/E2E/helpers/pages/loginPage.ts b/Tests/E2E/helpers/pages/loginPage.ts new file mode 100644 index 0000000..0b384dd --- /dev/null +++ b/Tests/E2E/helpers/pages/loginPage.ts @@ -0,0 +1,15 @@ +import type { Page } from "@playwright/test"; + +export default class NeosLoginPage { + constructor(private readonly page: Page) {} + + async goto() { + await this.page.goto("/neos/login"); + } + + async login(username: string, password: string) { + await this.page.locator('input[type="text"]').fill(username); + await this.page.locator('input[type="password"]').fill(password); + await this.page.locator(".neos-login-btn:not(.neos-disabled):not(.neos-hidden)").click(); + } +} diff --git a/Tests/E2E/helpers/system.ts b/Tests/E2E/helpers/system.ts new file mode 100644 index 0000000..f61456d --- /dev/null +++ b/Tests/E2E/helpers/system.ts @@ -0,0 +1,23 @@ +import { execSync } from "node:child_process"; +import { dirname } from "node:path"; +import type { Page } from "@playwright/test"; + +const CONTAINER = `${process.env.SUT || "neos8"}-neos-1`; + +export function createUser(name: string, password: string, roles: string[]) { + execSync( + `docker exec -u www-data -w /app ${CONTAINER} bash -c "./flow user:create ${name} ${password} Test${name} User${name} --roles ${roles.join(",")}"`, + { stdio: "ignore", cwd: dirname(".") }, + ); +} + +export function removeAllUsers() { + execSync(`docker exec -u www-data -w /app ${CONTAINER} bash -c "./flow user:delete --assume-yes '*'"`, { + stdio: "ignore", + cwd: dirname("."), + }); +} + +export async function logout(page: Page) { + await page.context().request.post("/neos/logout"); +} diff --git a/Tests/E2E/package-lock.json b/Tests/E2E/package-lock.json new file mode 100644 index 0000000..17c8306 --- /dev/null +++ b/Tests/E2E/package-lock.json @@ -0,0 +1,511 @@ +{ + "name": "sandstorm-neostwofactorauthentication-e2e", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sandstorm-neostwofactorauthentication-e2e", + "dependencies": { + "@playwright/test": "^1.58.2", + "playwright-bdd": "^9.2.0" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "prettier": "^3.8.1", + "typescript": "^6.0.2" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cucumber/ci-environment": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/ci-environment/-/ci-environment-13.0.0.tgz", + "integrity": "sha512-cs+3NzfNkGbcmHPddjEv4TKFiBpZRQ6WJEEufB9mw+ExS22V/4R/zpDSEG+fsJ/iSNCd6A2sATdY8PFOyY3YnA==", + "license": "MIT" + }, + "node_modules/@cucumber/cucumber-expressions": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber-expressions/-/cucumber-expressions-19.0.0.tgz", + "integrity": "sha512-4FKoOQh2Uf6F6/Ln+1OxuK8LkTg6PyAqekhf2Ix8zqV2M54sH+m7XNJNLhOFOAW/t9nxzRbw2CcvXbCLjcvHZg==", + "license": "MIT", + "dependencies": { + "regexp-match-indices": "1.0.2" + } + }, + "node_modules/@cucumber/gherkin": { + "version": "39.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-39.1.0.tgz", + "integrity": "sha512-pqmSO2bUWxJm3TbNrKXlDaHjL6c77+ez9kWmfCd9oRPeTRPEVH3spZvpAqdXYWOZYSNYwWFCAAeZ4RGpkauNoQ==", + "license": "MIT", + "dependencies": { + "@cucumber/messages": ">=31.0.0 <33" + } + }, + "node_modules/@cucumber/gherkin-utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-11.0.0.tgz", + "integrity": "sha512-LJ+s4+TepHTgdKWDR4zbPyT7rQjmYIcukTwNbwNwgqr6i8Gjcmzf6NmtbYDA19m1ZFg6kWbFsmHnj37ZuX+kZA==", + "license": "MIT", + "dependencies": { + "@cucumber/gherkin": "^38.0.0", + "@cucumber/messages": "^32.0.0", + "@teppeis/multimaps": "3.0.0", + "commander": "14.0.2", + "source-map-support": "^0.5.21" + }, + "bin": { + "gherkin-utils": "bin/gherkin-utils" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/@cucumber/gherkin": { + "version": "38.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-38.0.0.tgz", + "integrity": "sha512-duEXK+KDfQUzu3vsSzXjkxQ2tirF5PRsc1Xrts6THKHJO6mjw4RjM8RV+vliuDasmhhrmdLcOcM7d9nurNTJKw==", + "license": "MIT", + "dependencies": { + "@cucumber/messages": ">=31.0.0 <33" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@cucumber/html-formatter": { + "version": "23.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-23.1.0.tgz", + "integrity": "sha512-DcCSFoGs6jbwzXPgX1CwgJKEE+ZMcIEzq/0Memg0o24maNn9NJizBFHmoFWG4iv/OxHza+mvc+56cTHetfHndw==", + "license": "MIT", + "peerDependencies": { + "@cucumber/messages": ">=18" + } + }, + "node_modules/@cucumber/junit-xml-formatter": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@cucumber/junit-xml-formatter/-/junit-xml-formatter-0.13.3.tgz", + "integrity": "sha512-w9ujOxiuKDtU6fLzJz+wp4Sgp5Xu6ba7ls00LHJccVmQU0Ba7zs+AHnv3iIgPjKZAQe1w8x93dr8Gaubh7Vqkg==", + "license": "MIT", + "dependencies": { + "@cucumber/query": "^15.0.1", + "@teppeis/multimaps": "^3.0.0", + "luxon": "^3.5.0", + "xmlbuilder": "^15.1.1" + }, + "peerDependencies": { + "@cucumber/messages": "*" + } + }, + "node_modules/@cucumber/messages": { + "version": "32.3.1", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-32.3.1.tgz", + "integrity": "sha512-yNQq1KoXRYaEKrWMFmpUQX7TdeQuU9jeGgJAZ3dArTsC/T4NpJ6DnqaJIIgwPnz/wtQIQTNX7/h0rOuF5xY4qQ==", + "license": "MIT", + "dependencies": { + "class-transformer": "0.5.1", + "reflect-metadata": "0.2.2" + } + }, + "node_modules/@cucumber/query": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/query/-/query-15.0.1.tgz", + "integrity": "sha512-FMfT3orJblRsOxvU2doECBvQmauizYlj+5JsM8atAKKPbnQTj7v2/OrnuykvQpfZNBf19DYbRq1e832vllRP/g==", + "license": "MIT", + "dependencies": { + "@teppeis/multimaps": "3.0.0", + "lodash.sortby": "^4.7.0" + }, + "peerDependencies": { + "@cucumber/messages": "*" + } + }, + "node_modules/@cucumber/tag-expressions": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-9.1.0.tgz", + "integrity": "sha512-bvHjcRFZ+J1TqIa9eFNO1wGHqwx4V9ZKV3hYgkuK/VahHx73uiP4rKV3JVrvWSMrwrFvJG6C8aEwnCWSvbyFdQ==", + "license": "MIT" + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@teppeis/multimaps": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@teppeis/multimaps/-/multimaps-3.0.0.tgz", + "integrity": "sha512-ID7fosbc50TbT0MK0EG12O+gAP3W3Aa/Pz4DaTtQtEvlc9Odaqi0de+xuZ7Li2GtK4HzEX7IuRWS/JmZLksR3Q==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/node": { + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "license": "MIT" + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-bdd": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/playwright-bdd/-/playwright-bdd-9.2.0.tgz", + "integrity": "sha512-1tBTmo4DpOhLsc+A6PB4isWO3DHKb4BQ3Tzw5+ze/PmgBW9W2m9c+nc0TPy2nByWJtw0gKSfMoexyBR+y82+pg==", + "license": "MIT", + "dependencies": { + "@cucumber/ci-environment": "^13.0.0", + "@cucumber/cucumber-expressions": "19.0.0", + "@cucumber/gherkin": "^39.1.0", + "@cucumber/gherkin-utils": "^11.0.0", + "@cucumber/html-formatter": "^23.1.0", + "@cucumber/junit-xml-formatter": "^0.13.3", + "@cucumber/messages": "^32.3.1", + "@cucumber/query": "^15.0.1", + "@cucumber/tag-expressions": "^9.1.0", + "cli-table3": "0.6.5", + "commander": "^13.1.0", + "mime-types": "^3.0.2", + "tinyglobby": "0.2.17" + }, + "bin": { + "bddgen": "dist/cli/index.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/vitalets" + }, + "peerDependencies": { + "@playwright/test": ">=1.44" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/regexp-match-indices": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz", + "integrity": "sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==", + "license": "Apache-2.0", + "dependencies": { + "regexp-tree": "^0.1.11" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "license": "MIT", + "engines": { + "node": ">=8.0" + } + } + } +} diff --git a/Tests/E2E/package.json b/Tests/E2E/package.json new file mode 100644 index 0000000..59f1e32 --- /dev/null +++ b/Tests/E2E/package.json @@ -0,0 +1,19 @@ +{ + "name": "sandstorm-neostwofactorauthentication-e2e", + "private": true, + "type": "module", + "scripts": { + "generate-tests": "SUT=notRelevantForBddgen FLOW_CONTEXT=notRelevantForBddgen npx bddgen", + "test:neos8": "npm run generate-tests && SUT=neos8 FLOW_CONTEXT=Production/E2E-SUT npx playwright test", + "test:neos9": "npm run generate-tests && SUT=neos9 FLOW_CONTEXT=Production/E2E-SUT npx playwright test" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "typescript": "^6.0.2", + "prettier": "^3.8.1" + }, + "dependencies": { + "@playwright/test": "^1.58.2", + "playwright-bdd": "^9.2.0" + } +} diff --git a/Tests/E2E/playwright.config.ts b/Tests/E2E/playwright.config.ts new file mode 100644 index 0000000..9451b73 --- /dev/null +++ b/Tests/E2E/playwright.config.ts @@ -0,0 +1,43 @@ +import { defineConfig, devices } from "@playwright/test"; +import { defineBddConfig } from "playwright-bdd"; + +// env API to select system under test (SUT) (neos8 | neos9) and flow context for the configuration to be used (default, enforce for all users, etc.) +const SUT = process.env.SUT; +const FLOW_CONTEXT = process.env.FLOW_CONTEXT; + +if (SUT == null || FLOW_CONTEXT == null) { + throw new Error("SUT and FLOW_CONTEXT environment variables must be set!"); +} + +const testDir = defineBddConfig({ + features: "features/**/*.feature", + steps: "steps/**/*.ts", +}); + +export default defineConfig({ + testDir, + fullyParallel: false, + workers: 1, + retries: 0, + use: { + baseURL: "http://localhost:8081", + trace: "on-first-retry", + screenshot: "only-on-failure", + }, + globalTeardown: "./global-teardown.ts", + webServer: { + command: `echo "starting SUT ${SUT} with context ${FLOW_CONTEXT}"; FLOW_CONTEXT=${FLOW_CONTEXT} docker compose -f ./system_under_test/${SUT}/docker-compose.yaml up`, + url: "http://localhost:8081/", + timeout: 600_000, + stdout: "pipe", + stderr: "pipe", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + // Add more devices here if you need to test them. Make sure they are installed in Makefile setup step. + ], + reporter: process.env.CI ? "html" : "list", +}); diff --git a/Tests/E2E/steps/hooks.ts b/Tests/E2E/steps/hooks.ts new file mode 100644 index 0000000..4b85b1b --- /dev/null +++ b/Tests/E2E/steps/hooks.ts @@ -0,0 +1,11 @@ +import { createBdd } from "playwright-bdd"; +import { logout, removeAllUsers } from "../helpers/system.ts"; + +const { AfterScenario } = createBdd(); + +// cleanup for each scenario +AfterScenario(async ({ page }) => { + await logout(page); + + removeAllUsers(); +}); diff --git a/Tests/E2E/steps/login.steps.ts b/Tests/E2E/steps/login.steps.ts new file mode 100644 index 0000000..d5b1ee7 --- /dev/null +++ b/Tests/E2E/steps/login.steps.ts @@ -0,0 +1,44 @@ +import { expect } from "@playwright/test"; +import { createBdd } from "playwright-bdd"; +import NeosContentPage from "../helpers/pages/contentPage.ts"; +import NeosLoginPage from "../helpers/pages/loginPage.ts"; +import { createUser, logout } from "../helpers/system.ts"; + +const { Given, When, Then } = createBdd(); + +// ── Background / Given ──────────────────────────────────────────────────────── + +Given( + "A user with username {string}, password {string} and role {string} exists", + async ({}, username: string, password: string, role: string) => { + createUser(username, password, [role]); + }, +); + +// ── When ────────────────────────────────────────────────────────────────────── + +When("I log in with username {string} and password {string}", async ({ page }, username: string, password: string) => { + const loginPage = new NeosLoginPage(page); + await loginPage.goto(); + await loginPage.login(username, password); + await page.waitForLoadState("networkidle"); +}); + +When("I log out", async ({ page }) => { + await logout(page); +}); + +// ── Then ────────────────────────────────────────────────────────────────────── + +Then("I should see the Neos content page", async ({ page }) => { + const neosContentPage = new NeosContentPage(page); + await expect(page).toHaveURL(neosContentPage.URL_REGEX); +}); + +Then("I cannot access the Neos content page", async ({ page }) => { + const neosContentPage = new NeosContentPage(page); + await neosContentPage.goto(); + + // expecting to be redirected (e.g. to /neos/login) + await expect(page).not.toHaveURL(neosContentPage.URL_REGEX); +}); diff --git a/Tests/E2E/system_under_test/Dockerfile b/Tests/E2E/system_under_test/Dockerfile new file mode 100644 index 0000000..3e66b19 --- /dev/null +++ b/Tests/E2E/system_under_test/Dockerfile @@ -0,0 +1,44 @@ +ARG PHP_VERSION +FROM dunglas/frankenphp:1-php${PHP_VERSION}-trixie + +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + +# reference: https://github.com/mlocati/docker-php-extension-installer +RUN install-php-extensions \ + intl \ + pdo_mysql \ + gd \ + redis + +RUN apt update \ + && apt install -y git unzip mariadb-client \ + && apt clean \ + && rm -rf /var/lib/apt/lists/* + +ARG USER=www-data + +# Give write access to /config/caddy and /data/caddy +RUN \ + useradd ${USER}; \ + chown -R ${USER}:${USER} /config/caddy /data/caddy + +# Install Neos base distribution +ARG NEOS_VERSION +RUN rm -rf /app \ + && composer create-project neos/neos-base-distribution:${NEOS_VERSION} /app + +# Add config files +COPY Tests/E2E/system_under_test/sut_file_system_overrides/ / + +ARG ENTRY_POINT_FILE +COPY ${ENTRY_POINT_FILE} /entrypoint.sh + +# chown for neos data folder and Resources ONLY +RUN mkdir -p /app/Data /app/Web/_Resources \ + && chown -R ${USER} /app \ + && chmod +x /entrypoint.sh + +WORKDIR /app +USER ${USER} + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/Tests/E2E/system_under_test/neos8/compose-overrides-neos8.yaml b/Tests/E2E/system_under_test/neos8/compose-overrides-neos8.yaml new file mode 100644 index 0000000..c607695 --- /dev/null +++ b/Tests/E2E/system_under_test/neos8/compose-overrides-neos8.yaml @@ -0,0 +1,15 @@ +services: + neos: + build: + args: + PHP_VERSION: '8.2' + NEOS_VERSION: '^8' + ENTRY_POINT_FILE: 'Tests/E2E/system_under_test/neos8/entrypoint.sh' + +volumes: + db_neos_data: + name: db_neos8_data + +networks: + neos_SUT: + name: neos8_SUT diff --git a/Tests/E2E/system_under_test/neos8/docker-compose.yaml b/Tests/E2E/system_under_test/neos8/docker-compose.yaml new file mode 100644 index 0000000..0b7ca30 --- /dev/null +++ b/Tests/E2E/system_under_test/neos8/docker-compose.yaml @@ -0,0 +1,7 @@ +# WHY: GitHub actions does not support Docker Compose v5 yet - which in turn does not support overrides of included files. +# This is how it's supposed to be done anyway: https://docs.docker.com/compose/how-tos/multiple-compose-files/include/#using-overrides-with-included-compose-files + +include: + - path: + - ../sut-base-docker-compose.yaml + - ./compose-overrides-neos8.yaml diff --git a/Tests/E2E/system_under_test/neos8/entrypoint.sh b/Tests/E2E/system_under_test/neos8/entrypoint.sh new file mode 100644 index 0000000..ef32f3e --- /dev/null +++ b/Tests/E2E/system_under_test/neos8/entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -eou pipefail + +# Register local path repository and require local package +# The code will be mounted into the container by docker-compose, so we can use it as a path repository +composer config repositories.sandstorm-2fa \ + '{"type":"path","url":"/app/DistributionPackages/Sandstorm.Usermanagement","options":{"symlink":true}}' \ + && composer require sandstorm/usermanagement:@dev + +echo "Waiting for database..." +until mariadb -h"${DB_NEOS_HOST}" -P"${DB_NEOS_PORT}" -u"${DB_NEOS_USER}" -p"${DB_NEOS_PASSWORD}" -D"${DB_NEOS_DATABASE}" --disable-ssl --silent -e "SELECT 1;" 1>/dev/null 2>/dev/null; do + sleep 2 +done +echo "Database is ready." + +./flow flow:cache:flush + +./flow doctrine:migrate + +yes y | ./flow resource:clean || true + +./flow site:import --package-key Neos.Demo + +./flow resource:publish --collection static + +frankenphp run --config /etc/frankenphp/Caddyfile diff --git a/Tests/E2E/system_under_test/neos9/compose-overrides-neos9.yaml b/Tests/E2E/system_under_test/neos9/compose-overrides-neos9.yaml new file mode 100644 index 0000000..f884690 --- /dev/null +++ b/Tests/E2E/system_under_test/neos9/compose-overrides-neos9.yaml @@ -0,0 +1,18 @@ +services: + neos: + build: + args: + PHP_VERSION: '8.4' + NEOS_VERSION: '9.1.6' + ENTRY_POINT_FILE: 'Tests/E2E/system_under_test/neos9/entrypoint.sh' + + db: + image: mariadb:11.4 + +volumes: + db_neos_data: + name: db_neos9_data + +networks: + neos_SUT: + name: neos9_SUT diff --git a/Tests/E2E/system_under_test/neos9/docker-compose.yaml b/Tests/E2E/system_under_test/neos9/docker-compose.yaml new file mode 100644 index 0000000..52289f0 --- /dev/null +++ b/Tests/E2E/system_under_test/neos9/docker-compose.yaml @@ -0,0 +1,7 @@ +# WHY: GitHub actions does not support Docker Compose v5 yet - which in turn does not support overrides of included files. +# This is how it's supposed to be done anyway: https://docs.docker.com/compose/how-tos/multiple-compose-files/include/#using-overrides-with-included-compose-files + +include: + - path: + - ../sut-base-docker-compose.yaml + - ./compose-overrides-neos9.yaml diff --git a/Tests/E2E/system_under_test/neos9/entrypoint.sh b/Tests/E2E/system_under_test/neos9/entrypoint.sh new file mode 100644 index 0000000..e9ab72c --- /dev/null +++ b/Tests/E2E/system_under_test/neos9/entrypoint.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -eou pipefail + +# Register local path repository and require local package +# The code will be mounted into the container by docker-compose, so we can use it as a path repository +composer config repositories.sandstorm-2fa \ + '{"type":"path","url":"/app/DistributionPackages/Sandstorm.Usermanagement","options":{"symlink":true}}' \ + && composer require sandstorm/usermanagement:@dev + +echo "Waiting for database..." +until mariadb -h"${DB_NEOS_HOST}" -P"${DB_NEOS_PORT}" -u"${DB_NEOS_USER}" -p"${DB_NEOS_PASSWORD}" -D"${DB_NEOS_DATABASE}" --disable-ssl --silent -e "SELECT 1;" 1>/dev/null 2>/dev/null; do + sleep 2 +done +echo "Database is ready." + +./flow flow:cache:flush + +./flow doctrine:migrate + +yes y | ./flow resource:clean || true + +./flow cr:setup +./flow cr:status + +./flow site:importall --package-key Neos.Demo + +./flow resource:publish --collection static + +frankenphp run --config /etc/frankenphp/Caddyfile diff --git a/Tests/E2E/system_under_test/sut-base-docker-compose.yaml b/Tests/E2E/system_under_test/sut-base-docker-compose.yaml new file mode 100644 index 0000000..6341029 --- /dev/null +++ b/Tests/E2E/system_under_test/sut-base-docker-compose.yaml @@ -0,0 +1,74 @@ +services: + neos: + user: www-data:www-data + build: + # the package root, so that the package sources can be copied into the image + # NOTE: relative paths in this file are resolved from its own directory (Tests/E2E/system_under_test) + context: ../../../ + dockerfile: Tests/E2E/system_under_test/Dockerfile + args: + # minimum PHP version is '8.2' because that's the lowest version frankenPHP provides an image for + PHP_VERSION: '8.2' + # used to install the system under test + # which is a neos-base-distribution:^${NEOS_VERSION} + NEOS_VERSION: '8' + # docker will use this as entrypoint + ENTRY_POINT_FILE: 'Tests/E2E/system_under_test/neos8/entrypoint.sh' + environment: + FLOW_CONTEXT: "${FLOW_CONTEXT:-Production/E2E-SUT}" + # DB connection + DB_NEOS_HOST: 'db' + DB_NEOS_PORT: 3306 + DB_NEOS_USER: 'neos' + DB_NEOS_PASSWORD: 'neos' + DB_NEOS_DATABASE: 'neos' + # Redis connection + REDIS_HOST: 'redis' + REDIS_PORT: 6379 + # this is safe because the neos container port is only exposed to the local interface + # This means that the neos container is ALWAYS accessed through the front facing Ingress + FLOW_HTTP_TRUSTED_PROXIES: '*' + volumes: + - ../../../Classes:/app/DistributionPackages/Sandstorm.Usermanagement/Classes:cached + - ../../../Configuration:/app/DistributionPackages/Sandstorm.Usermanagement/Configuration:cached + - ../../../Migrations:/app/DistributionPackages/Sandstorm.Usermanagement/Migrations:cached + - ../../../Resources:/app/DistributionPackages/Sandstorm.Usermanagement/Resources:cached + - ../../../composer.json:/app/DistributionPackages/Sandstorm.Usermanagement/composer.json:cached + networks: + - neos_SUT + ports: + - 8081:8081 + depends_on: + - db + - redis + + db: + image: mariadb:10.11 + restart: always + ports: + - "13306:3306" + networks: + - neos_SUT + environment: + MARIADB_RANDOM_ROOT_PASSWORD: 'true' + MARIADB_DATABASE: 'neos' + MARIADB_USER: 'neos' + MARIADB_PASSWORD: 'neos' + MARIADB_AUTO_UPGRADE: 1 + volumes: + - db_neos_data:/var/lib/mysql + command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + + redis: + image: redis:7 + restart: always + networks: + - neos_SUT + +volumes: + db_neos_data: + name: db_neos_data + +networks: + neos_SUT: + name: neos_SUT diff --git a/Tests/E2E/system_under_test/sut_file_system_overrides/app/Configuration/Production/E2E-SUT/Caches.yaml b/Tests/E2E/system_under_test/sut_file_system_overrides/app/Configuration/Production/E2E-SUT/Caches.yaml new file mode 100644 index 0000000..3844103 --- /dev/null +++ b/Tests/E2E/system_under_test/sut_file_system_overrides/app/Configuration/Production/E2E-SUT/Caches.yaml @@ -0,0 +1,56 @@ +Flow_Mvc_Routing_Route: + backend: 'Neos\Cache\Backend\RedisBackend' + backendOptions: + hostname: '%env:REDIS_HOST%' + port: '%env:REDIS_PORT%' + # starting with database 2 here, since 0 and 1 are used and flushed by + # the core unit tests and should not be used if possible. + database: 2 + defaultLifetime: 0 + +Flow_Mvc_Routing_Resolve: + backend: 'Neos\Cache\Backend\RedisBackend' + backendOptions: + hostname: '%env:REDIS_HOST%' + port: '%env:REDIS_PORT%' + database: 2 + defaultLifetime: 0 + +# We want to test cache settings on Fusion components. +# Therefore, at one moment in the BDD test, we need to actively trigger the ContentCacheFlusher to invalidate the tags. +# Since we have multiple Flow contexts during tests, the cache settings are unified for all contexts. +# Explanation: +# - The system under test runs in Production/E2E-SUT; it writes cache entries +# - The Test Runner (behat) runs in Testing/Behat; it needs to invalidate cache for the SuT via service API call +# For now, we simply keep the cache settings in sync between those two profiles (like we do with the DB config). +Neos_Fusion_Content: + backend: 'Neos\Cache\Backend\RedisBackend' + backendOptions: + hostname: '%env:REDIS_HOST%' + port: '%env:REDIS_PORT%' + database: 2 + defaultLifetime: 0 + +Flow_Session_MetaData: + backend: 'Neos\Cache\Backend\RedisBackend' + backendOptions: + hostname: '%env:REDIS_HOST%' + port: '%env:REDIS_PORT%' + database: 2 + defaultLifetime: 0 + +Flow_Session_Storage: + backend: 'Neos\Cache\Backend\RedisBackend' + backendOptions: + hostname: '%env:REDIS_HOST%' + port: '%env:REDIS_PORT%' + database: 2 + defaultLifetime: 0 + +Neos_Media_ImageSize: + backend: 'Neos\Cache\Backend\RedisBackend' + backendOptions: + hostname: '%env:REDIS_HOST%' + port: '%env:REDIS_PORT%' + database: 2 + defaultLifetime: 0 diff --git a/Tests/E2E/system_under_test/sut_file_system_overrides/app/Configuration/Production/E2E-SUT/Settings.yaml b/Tests/E2E/system_under_test/sut_file_system_overrides/app/Configuration/Production/E2E-SUT/Settings.yaml new file mode 100644 index 0000000..28bf875 --- /dev/null +++ b/Tests/E2E/system_under_test/sut_file_system_overrides/app/Configuration/Production/E2E-SUT/Settings.yaml @@ -0,0 +1,16 @@ +Neos: + Flow: + persistence: + backendOptions: + driver: 'pdo_mysql' + charset: 'utf8mb4' + host: '%env:DB_NEOS_HOST%' + port: '%env:DB_NEOS_PORT%' + password: '%env:DB_NEOS_PASSWORD%' + user: '%env:DB_NEOS_USER%' + dbname: '%env:DB_NEOS_DATABASE%' + cache: + applicationIdentifier: 'app' + + Imagine: + driver: Gd diff --git a/Tests/E2E/system_under_test/sut_file_system_overrides/etc/frankenphp/Caddyfile b/Tests/E2E/system_under_test/sut_file_system_overrides/etc/frankenphp/Caddyfile new file mode 100644 index 0000000..8c6b738 --- /dev/null +++ b/Tests/E2E/system_under_test/sut_file_system_overrides/etc/frankenphp/Caddyfile @@ -0,0 +1,41 @@ +# The Caddyfile is an easy way to configure FrankenPHP and the Caddy web server. +# +# https://frankenphp.dev/docs/config +# https://caddyserver.com/docs/caddyfile +# https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile +{ + skip_install_trust + + # debug + + frankenphp { + #num_threads # Sets the number of PHP threads to start. Default: 2x the number of available CPUs. + #max_threads # Limits the number of additional PHP threads that can be started at runtime. Default: num_threads. Can be set to 'auto'. + #max_wait_time # Sets the maximum time a request may wait for a free PHP thread before timing out. Default: disabled. + #php_ini # Set a php.ini directive. Can be used several times to set multiple directives. + } +} + +:8081 { + # log + + root /app/Web + encode zstd br gzip + + request_body { + max_size 256MB + } + + # Block direct access to PHP files except index.php + @blockPhp { + path *.php + not path /index.php + } + + handle @blockPhp { + respond 404 + } + + + php_server +} diff --git a/Tests/E2E/tsconfig.json b/Tests/E2E/tsconfig.json new file mode 100644 index 0000000..4bb79b3 --- /dev/null +++ b/Tests/E2E/tsconfig.json @@ -0,0 +1,21 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + "noEmit": true, + "module": "nodenext", + "target": "ESNext", + "lib": ["esnext"], + "types": ["node"], + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + // Recommended Options + "strict": true, + "verbatimModuleSyntax": false, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + "allowImportingTsExtensions": true + } +}