Skip to content

Commit 7acd03d

Browse files
committed
feat: add testing infrastructure for login MFA flow and E2E suite
Signed-off-by: romanetar <roman_ag@hotmail.com>
1 parent f7b8a91 commit 7acd03d

37 files changed

Lines changed: 1243 additions & 107 deletions
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
name: Front End Tests On Pull Request
2+
3+
on:
4+
pull_request:
5+
types: [opened, reopened, edited, synchronize]
6+
branches: ["main"]
7+
8+
jobs:
9+
10+
js-unit-tests:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Check out repository code
14+
uses: actions/checkout@v4
15+
- name: Set up Node.js
16+
uses: actions/setup-node@v4
17+
with:
18+
node-version: '22'
19+
cache: 'yarn'
20+
- name: Install JS dependencies
21+
run: yarn install --frozen-lockfile
22+
- name: Run Jest unit tests
23+
run: yarn test:unit:ci
24+
- name: Upload Jest coverage
25+
uses: actions/upload-artifact@v4
26+
if: always()
27+
with:
28+
name: jest-coverage
29+
path: tests/js/coverage
30+
retention-days: 5
31+
32+
e2e-tests:
33+
runs-on: ubuntu-latest
34+
env:
35+
APP_ENV: testing
36+
APP_DEBUG: true
37+
APP_KEY: base64:4vh0op/S1dAsXKQ2bbdCfWRyCI9r8NNIdPXyZWt9PX4=
38+
APP_URL: http://localhost:8001
39+
DEV_EMAIL_TO: smarcet@gmail.com
40+
DB_CONNECTION: mysql
41+
DB_HOST: 127.0.0.1
42+
DB_PORT: 3306
43+
DB_DATABASE: idp_test
44+
DB_USERNAME: root
45+
DB_PASSWORD: 1qaz2wsx
46+
REDIS_HOST: 127.0.0.1
47+
REDIS_PORT: 6379
48+
REDIS_DB: 0
49+
REDIS_PASSWORD: 1qaz2wsx
50+
REDIS_DATABASES: 16
51+
SSL_ENABLED: false
52+
SESSION_DRIVER: redis
53+
SESSION_COOKIE_SECURE: false
54+
PHP_VERSION: 8.3
55+
OTEL_SDK_DISABLED: true
56+
OTEL_SERVICE_ENABLED: false
57+
TURNSTILE_SITE_KEY: ''
58+
TURNSTILE_SECRET_KEY: ''
59+
services:
60+
mysql:
61+
image: mysql:8.0
62+
env:
63+
MYSQL_ROOT_PASSWORD: 1qaz2wsx
64+
MYSQL_DATABASE: idp_test
65+
ports:
66+
- 3306:3306
67+
options: >-
68+
--health-cmd="mysqladmin ping"
69+
--health-interval=10s
70+
--health-timeout=5s
71+
--health-retries=3
72+
steps:
73+
- name: Create Redis
74+
uses: supercharge/redis-github-action@1.8.1
75+
with:
76+
redis-port: 6379
77+
redis-password: 1qaz2wsx
78+
- name: Check out repository code
79+
uses: actions/checkout@v4
80+
- name: Install PHP
81+
uses: shivammathur/setup-php@v2
82+
with:
83+
php-version: ${{ env.PHP_VERSION }}
84+
extensions: pdo_mysql, mbstring, exif, pcntl, bcmath, sockets, gettext, apcu
85+
- name: Install PHP dependencies
86+
uses: ramsey/composer-install@v3
87+
env:
88+
COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.PAT }}"} }'
89+
- name: Set up Node.js
90+
uses: actions/setup-node@v4
91+
with:
92+
node-version: '22'
93+
cache: 'yarn'
94+
- name: Install JS dependencies
95+
run: yarn install --frozen-lockfile
96+
- name: Build frontend assets
97+
run: yarn build
98+
- name: Prepare application
99+
run: |
100+
cp .env.example .env
101+
./update_doctrine.sh
102+
php artisan doctrine:migrations:migrate --no-interaction
103+
php artisan db:seed --force
104+
php artisan idp:create-super-admin test@test.com '1Qaz2wsx!'
105+
php artisan idp:create-raw-user e2e@test.com '1Qaz2wsx!'
106+
- name: Install Playwright Chromium
107+
run: npx playwright install --with-deps chromium
108+
- name: Start web server
109+
run: php artisan serve --host=127.0.0.1 --port=8001 &
110+
- name: Wait for server to be ready
111+
run: |
112+
for i in $(seq 1 20); do
113+
curl -sf http://localhost:8001 > /dev/null 2>&1 && echo "Server ready" && exit 0
114+
sleep 2
115+
done
116+
echo "Server did not start in time" && exit 1
117+
- name: Run E2E tests
118+
run: yarn test:e2e --reporter=list
119+
- name: Upload Playwright report
120+
uses: actions/upload-artifact@v4
121+
if: always()
122+
with:
123+
name: playwright-report
124+
path: tests/e2e/report
125+
retention-days: 7
126+
- name: Upload Playwright traces
127+
uses: actions/upload-artifact@v4
128+
if: failure()
129+
with:
130+
name: playwright-traces
131+
path: test-results/
132+
retention-days: 7
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php namespace App\Console\Commands;
2+
/**
3+
* Copyright 2026 OpenStack Foundation
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
**/
14+
15+
use Auth\User;
16+
use Illuminate\Console\Command;
17+
use LaravelDoctrine\ORM\Facades\EntityManager;
18+
19+
/**
20+
* Class CreateRawUser
21+
*
22+
* Creates a plain, verified user with no group memberships.
23+
* Because this user belongs to none of the groups listed in
24+
* two_factor.enforced_groups, MFA is never required — making it
25+
* safe to use in automated E2E tests that must complete the full
26+
* login → redirect flow without a 2FA challenge.
27+
*
28+
* @package App\Console\Commands
29+
*/
30+
class CreateRawUser extends Command
31+
{
32+
protected $signature = 'idp:create-raw-user {email} {password}';
33+
34+
protected $description = 'Create a plain verified user with no group memberships (useful for E2E tests)';
35+
36+
public function handle()
37+
{
38+
$email = trim($this->argument('email'));
39+
$password = trim($this->argument('password'));
40+
41+
$user = EntityManager::getRepository(User::class)->findOneBy(['email' => $email]);
42+
if (is_null($user)) {
43+
$user = new User();
44+
$user->setEmail($email);
45+
$user->verifyEmail();
46+
$user->setPassword($password);
47+
$user->setFirstName($email);
48+
$user->setLastName($email);
49+
$user->setIdentifier($email);
50+
EntityManager::persist($user);
51+
EntityManager::flush();
52+
$this->info("Created user: {$email}");
53+
} else {
54+
$this->info("User already exists: {$email}");
55+
}
56+
}
57+
}

app/Console/Kernel.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class Kernel extends ConsoleKernel
2929
Commands\CleanOAuth2StaleData::class,
3030
Commands\CleanOpenIdStaleData::class,
3131
Commands\CreateSuperAdmin::class,
32+
Commands\CreateRawUser::class,
3233
Commands\SpammerProcess\RebuildUserSpammerEstimator::class,
3334
Commands\SpammerProcess\UserSpammerProcessor::class,
3435
];

app/Http/Controllers/Auth/RegisterController.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,15 +173,18 @@ public function showRegistrationForm(LaravelRequest $request)
173173
protected function validator(array $data)
174174
{
175175
$rules = [
176-
'first_name' => 'required|string|max:100',
177-
'last_name' => 'required|string|max:100',
178-
'country_iso_code' => 'required|string|country_iso_alpha2_code',
179-
'email' => 'required|string|email|max:255',
180-
'password' => 'required|string|confirmed|password_policy',
181-
'cf-turnstile-response' => ['required', new Turnstile()],
176+
'first_name' => 'required|string|max:100',
177+
'last_name' => 'required|string|max:100',
178+
'country_iso_code' => 'required|string|country_iso_alpha2_code',
179+
'email' => 'required|string|email|max:255',
180+
'password' => 'required|string|confirmed|password_policy',
182181
];
183182

184-
if(!empty(Config::get("app.code_of_conduct_link", null))){
183+
if (!empty(Config::get("services.turnstile.secret", null))) {
184+
$rules['cf-turnstile-response'] = ['required', new Turnstile()];
185+
}
186+
187+
if (!empty(Config::get("app.code_of_conduct_link", null))) {
185188
$rules['agree_code_of_conduct'] = 'required|string|in:true';
186189
}
187190

babel.config.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,21 @@ module.exports = {
2121
plugins: [
2222
"@babel/plugin-proposal-object-rest-spread",
2323
"@babel/plugin-proposal-class-properties"
24-
]
24+
],
25+
env: {
26+
test: {
27+
presets: [
28+
[
29+
"@babel/preset-env",
30+
{
31+
"targets": { "node": "current" },
32+
"useBuiltIns": false
33+
}
34+
],
35+
"@babel/preset-react",
36+
"@babel/preset-flow"
37+
]
38+
}
39+
}
2540
};
2641

config/session.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@
148148
|
149149
*/
150150

151-
'secure' => true,
151+
'secure' => env('SESSION_SECURE_COOKIE', false),
152152

153153
/*
154154
|--------------------------------------------------------------------------
@@ -176,6 +176,6 @@
176176
|
177177
*/
178178

179-
'same_site' => 'none',
179+
'same_site' => env('SESSION_COOKIE_SAME_SITE', 'lax'),
180180

181181
];

0 commit comments

Comments
 (0)