Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.git/
.idea/
node_modules/
vendor/
Packages
.github
.claude
.idea
composer.lock
60 changes: 60 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: E2E Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
e2e:
name: E2E Tests (${{ matrix.neos }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
neos: [neos8, neos9]

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
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/system_under_test/${{ matrix.neos }}/docker-compose.yaml build --pull

- name: Test - defaults
working-directory: Tests/E2E
run: npm run test:${{ matrix.neos }}:defaults

- name: Test - enforce-all
working-directory: Tests/E2E
run: npm run test:${{ matrix.neos }}:enforce-all

- name: Test - enforce-role
working-directory: Tests/E2E
run: npm run test:${{ matrix.neos }}:enforce-role

- name: Test - enforce-provider
working-directory: Tests/E2E
run: npm run test:${{ matrix.neos }}:enforce-provider

- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-${{ matrix.neos }}
path: Tests/E2E/playwright-report/
retention-days: 7
8 changes: 5 additions & 3 deletions Classes/Controller/BackendController.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public function newAction(): void
* @throws IllegalObjectTypeException
* @throws StopActionException
*/
public function createAction(string $secret, string $secondFactorFromApp): void
public function createAction(string $secret, string $secondFactorFromApp, string $name = ''): void
{
$isValid = TOTPService::checkIfOtpIsValid($secret, $secondFactorFromApp);

Expand All @@ -157,7 +157,7 @@ public function createAction(string $secret, string $secondFactorFromApp): void
$this->redirect('new');
}

$this->secondFactorRepository->createSecondFactorForAccount($secret, $this->securityContext->getAccount());
$this->secondFactorRepository->createSecondFactorForAccount($secret, $this->securityContext->getAccount(), $name);

$this->secondFactorSessionStorageService->setAuthenticationStatus(AuthenticationStatus::AUTHENTICATED);

Expand Down Expand Up @@ -189,7 +189,9 @@ public function deleteAction(SecondFactor $secondFactor): void
if ($isAdministrator || ($isOwner && $this->secondFactorService->canOneSecondFactorBeDeletedForAccount($account))) {
// User is admin or has more than one second factor
$this->secondFactorRepository->remove($secondFactor);
$this->persistenceManager->persistAll();
// neos8 backwards compatibility
$this->persistenceManager?->persistAll();

$this->addFlashMessage(
$this->translator->translateById(
'module.index.delete.flashMessage.secondFactorDeleted',
Expand Down
5 changes: 3 additions & 2 deletions Classes/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,13 @@ public function setupSecondFactorAction(?string $username = null): void
/**
* @param string $secret
* @param string $secondFactorFromApp
* @param string $name
* @return void
* @throws IllegalObjectTypeException
* @throws SessionNotStartedException
* @throws StopActionException
*/
public function createSecondFactorAction(string $secret, string $secondFactorFromApp): void
public function createSecondFactorAction(string $secret, string $secondFactorFromApp, string $name = ''): void
{
$isValid = TOTPService::checkIfOtpIsValid($secret, $secondFactorFromApp);

Expand All @@ -204,7 +205,7 @@ public function createSecondFactorAction(string $secret, string $secondFactorFro

$account = $this->securityContext->getAccount();

$this->secondFactorRepository->createSecondFactorForAccount($secret, $account);
$this->secondFactorRepository->createSecondFactorForAccount($secret, $account, $name);

$this->addFlashMessage(
$this->translator->translateById(
Expand Down
21 changes: 21 additions & 0 deletions Classes/Domain/Model/SecondFactor.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use Doctrine\ORM\Mapping as ORM;
use Neos\Flow\Annotations as Flow;

// TODO: refactor to PHP8 code

/**
* Store the secrets needed for two factor authentication
*
Expand Down Expand Up @@ -39,6 +41,11 @@ class SecondFactor
*/
protected string $secret;

/**
* @var string
*/
protected string $name;

/**
* Introduced with version 1.4.0
* Nullable for backwards compatibility. Null values will be shown as '-' in backend module.
Expand Down Expand Up @@ -73,6 +80,7 @@ public function getType(): int
}

/**
* Used in Fusion rendering
* @return string
*/
public function getTypeAsName(): string
Expand Down Expand Up @@ -104,6 +112,19 @@ public function setSecret(string $secret): void
$this->secret = $secret;
}

public function setName(string $name): void
{
$this->name = $name;
}

public function getName(): string
{
return $this->name;
}

/**
* Used in Fusion rendering
*/
public function getCreationDate(): DateTime|null
{
return $this->creationDate;
Expand Down
3 changes: 2 additions & 1 deletion Classes/Domain/Repository/SecondFactorRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ class SecondFactorRepository extends Repository
/**
* @throws IllegalObjectTypeException
*/
public function createSecondFactorForAccount(string $secret, Account $account): void
public function createSecondFactorForAccount(string $secret, Account $account, string $name): void
{
$secondFactor = new SecondFactor();
$secondFactor->setAccount($account);
$secondFactor->setSecret($secret);
$secondFactor->setType(SecondFactor::TYPE_TOTP);
$secondFactor->setName($name);
$secondFactor->setCreationDate(new \DateTime());
$this->add($secondFactor);
$this->persistenceManager->persistAll();
Expand Down
169 changes: 169 additions & 0 deletions Concept_Composer_Package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Concept: Reusable E2E Infrastructure as a Composer Package

## Goal

Extract the E2E test infrastructure from this plugin into a standalone Composer package
(`sandstorm/neos-e2e-testing`) that any Neos plugin can install to get a working
end-to-end test scaffold with minimal effort.

**Scope:** Infrastructure only — Docker/SUT setup, Playwright project scaffold (config,
package.json, tsconfig, teardown), Makefile, and GitHub Actions workflow. Step definitions,
helpers (system, pages, totp, etc.), and feature files are entirely the plugin developer's
responsibility.

---

## Package Structure

### Type: `library`

Not `neos-package` — this is a dev tool. It belongs in `vendor/`, not `Packages/`, and
has no Flow/Neos runtime dependency.

```
sandstorm/neos-e2e-testing/
├── composer.json
├── bin/
│ └── neos-e2e-setup ← self-contained PHP CLI script
└── templates/
├── Tests/
│ ├── E2E/
│ │ ├── package.json
│ │ ├── playwright.config.ts
│ │ ├── tsconfig.json
│ │ ├── global-teardown.ts
│ │ └── .nvmrc
│ └── system_under_test/
│ ├── neos8/
│ │ ├── Dockerfile
│ │ ├── docker-compose.yaml
│ │ └── sut-files/
│ │ ├── entrypoint.sh
│ │ ├── etc/caretakerd.yaml
│ │ ├── etc/frankenphp/Caddyfile
│ │ └── usr/local/etc/php/conf.d/php-ini-overrides.ini
│ └── neos9/
│ └── (same)
├── .dockerignore
├── Makefile
└── .github/
└── workflows/
└── e2e.yml
```

### `composer.json`

```json
{
"name": "sandstorm/neos-e2e-testing",
"type": "library",
"description": "Scaffolds E2E test infrastructure for Neos plugins",
"require": { "php": "^8.1" },
"bin": ["bin/neos-e2e-setup"]
}
```

---

## Usage

```sh
composer require --dev sandstorm/neos-e2e-testing
vendor/bin/neos-e2e-setup --plugin-package sandstorm/neostwofactorauthentication
```

The script copies templates into the plugin root, substitutes tokens, and skips files that
already exist (safe to re-run).

---

## Template Tokens

Only two things vary between plugins:

| Token | Example | Used in |
|---|---|---|
| `{{PLUGIN_PACKAGE}}` | `sandstorm/neostwofactorauthentication` | Dockerfile, docker-compose project name, Makefile, GH Actions |
| `{{PLUGIN_SLUG}}` | `sandstorm-2fa` | Docker container/volume names |

The slug is derived automatically from the package name (everything after `/`, hyphens
preserved), so the developer only needs to provide `--plugin-package`.

---

## The Dockerfile Template

The only plugin-specific lines in the Dockerfile are the `composer require` call. These
are made generic via a build arg:

```dockerfile
ARG PLUGIN_PACKAGE={{PLUGIN_PACKAGE}}

COPY . /tmp/plugin/
RUN --mount=type=cache,target=/root/.composer \
composer config repositories.plugin \
'{"type":"path","url":"/tmp/plugin","options":{"symlink":false}}' \
&& composer require ${PLUGIN_PACKAGE}:@dev
```

Everything else (PHP extensions, caretakerd, FrankenPHP, Neos base distribution, config
copy) is already generic across plugins.

---

## `.dockerignore` (generated at plugin root)

Without this, `COPY . /tmp/plugin/` in the Dockerfile would include the test
infrastructure itself inside the image:

```
Tests/E2E/node_modules
Tests/E2E/.playwright
Tests/system_under_test
```

---

## The `bin/neos-e2e-setup` Script

A self-contained PHP script — no autoloading, no framework. It:

1. Parses `--plugin-package` from `$argv`
2. Derives `{{PLUGIN_SLUG}}` from the package name
3. Iterates over `templates/` recursively
4. For each template file: substitutes tokens, copies to the target path
5. Skips files that already exist (idempotent)

---

## What the Developer Adds After Scaffolding

The scaffolded layout has intentional empty directories (with `.gitkeep`) for the
plugin-specific content:

```
Tests/E2E/features/
← write .feature files here

Tests/E2E/steps/
← write step definitions here

Tests/system_under_test/neos8/sut-files/app/Configuration/Production/E2E-SUT/
Tests/system_under_test/neos9/sut-files/app/Configuration/Production/E2E-SUT/
← plugin-specific Settings.yaml, Policy.yaml, context subdirectories, etc.
```

The scaffold writes the base `Configuration/Production/E2E-SUT/Settings.yaml` (DB + Redis
connection) and `Caches.yaml`. The plugin only adds its own configuration on top.

---

## Open Question: Container Name Convention

The `SUT` environment variable (used in step definitions as `${SUT}-neos-1` to target
`docker exec`) is a contract between the scaffolded `docker-compose.yaml` and the
developer's step helpers. Since step helpers are entirely the plugin developer's business,
this convention needs to be documented somewhere visible — either in a generated `README`
in `Tests/E2E/`, or as a comment in the scaffolded `playwright.config.ts`.

A decision is needed on where this contract lives before the package is built.
10 changes: 10 additions & 0 deletions Configuration/Settings.2FA.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Sandstorm:
NeosTwoFactorAuthentication:
# enforce 2FA for all users
enforceTwoFactorAuthentication: false
# enforce 2FA for specific authentication providers (e.g. Neos.Neos:Backend)
enforce2FAForAuthenticationProviders : []
# enforce 2FA for specific roles (e.g. Neos.Neos:Administrator)
enforce2FAForRoles: []
# (optional) if set this will be used as a naming convention for the TOTP. If empty the Site name will be used
issuerName: ''
11 changes: 0 additions & 11 deletions Configuration/Settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,3 @@ Neos:
pattern: 'ControllerObjectName'
patternOptions:
controllerObjectNamePattern: 'Sandstorm\NeosTwoFactorAuthentication\Controller\(LoginController|BackendController)'

Sandstorm:
NeosTwoFactorAuthentication:
# enforce 2FA for all users
enforceTwoFactorAuthentication: false
# enforce 2FA for specific authentication providers
enforce2FAForAuthenticationProviders : []
# enforce 2FA for specific roles
enforce2FAForRoles: []
# (optional) if set this will be used as a naming convention for the TOTP. If empty the Site name will be used
issuerName: ''
Loading
Loading