Skip to content

Commit c00e849

Browse files
committed
fix: seed the e2e OAuth2 test client without depending on TestSeeder
CI was red: tests/e2e/tests/oauth2/auth-code-flow.spec.ts authorizes against a client_id that only exists as a side effect of database/seeds/TestSeeder.php, which is wired ONLY into PHPUnit's BrowserKitTestCase ($this->seed('TestSeeder')) - never into `php artisan db:seed`, which is all the CI workflow runs. On a genuinely fresh database the client_id never resolves, so InteractiveGrantType::handle() throws InvalidClientException before ever reaching the "redirect to login" branch, and the very first oauth2 test ("unauthenticated request redirects to login") gets a 400 error page instead of a redirect - exactly what the failing CI run showed. Local testing never caught this because the long-lived docker-compose dev database already had TestSeeder's fixtures from past PHPUnit runs. TestSeeder itself is not a safe fix for CI: its run() truncates users/groups/oauth2_client (and otp/consent/session-adjacent tables) before reseeding its own fixed set - correct for PHPUnit's isolated test lifecycle, destructive against the same shared database this workflow also seeds idp:create-super-admin/idp:create-raw-user users into. - app/Console/Commands/CreateOAuth2TestClient.php (idp:create-oauth2-test-client): idempotent, additive-only - creates just the one confidential client (same client_id/secret/redirect_uri the e2e suite already uses) plus a dedicated owner user (the consent screen's getDeveloperEmail() dereferences the owner unconditionally - an ownerless client 500s as soon as a real login reaches /accounts/user/consent) and grants it the 'profile' scope. Registered in app/Console/Kernel.php. - .github/workflows/{pull_request,push}_frontend_tests.yml: run the new command alongside the existing user fixtures. Verified: 16/17 e2e via `docker compose --profile e2e run --rm playwright npx playwright test` (the 17th, trusted-device, is the pre-existing environment-only miss - Secure cookies don't persist over http://nginx), 40/40 PHP, 23/23 Jest.
1 parent 052220a commit c00e849

4 files changed

Lines changed: 109 additions & 0 deletions

File tree

.github/workflows/pull_request_frontend_tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ jobs:
108108
php artisan idp:create-super-admin mfa-oauth2@test.com '1Qaz2wsx!'
109109
php artisan idp:create-super-admin mfa-oauth2-consent@test.com '1Qaz2wsx!'
110110
php artisan idp:create-super-admin mfa-oauth2-trust@test.com '1Qaz2wsx!'
111+
php artisan idp:create-oauth2-test-client
111112
- name: Install Playwright Chromium
112113
run: npx playwright install --with-deps chromium
113114
- name: Start web server

.github/workflows/push_frontend_tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ jobs:
109109
php artisan idp:create-super-admin mfa-oauth2@test.com '1Qaz2wsx!'
110110
php artisan idp:create-super-admin mfa-oauth2-consent@test.com '1Qaz2wsx!'
111111
php artisan idp:create-super-admin mfa-oauth2-trust@test.com '1Qaz2wsx!'
112+
php artisan idp:create-oauth2-test-client
112113
- name: Install Playwright Chromium
113114
run: npx playwright install --with-deps chromium
114115
- name: Start web server
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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 App\Models\OAuth2\Factories\ClientFactory;
16+
use Auth\User;
17+
use Illuminate\Console\Command;
18+
use LaravelDoctrine\ORM\Facades\EntityManager;
19+
use Models\OAuth2\ApiScope;
20+
use Models\OAuth2\Client;
21+
use OAuth2\Models\IClient;
22+
use OAuth2\OAuth2Protocol;
23+
24+
/**
25+
* Class CreateOAuth2TestClient
26+
*
27+
* Creates (idempotently) the confidential OAuth2 client the e2e suite
28+
* authorizes against (tests/e2e/tests/oauth2/auth-code-flow.spec.ts).
29+
*
30+
* database/seeds/TestSeeder.php already seeds a client with this exact
31+
* client_id, but only as a side effect of truncating and rebuilding the
32+
* ENTIRE users/groups/oauth2_client tables from scratch - correct for
33+
* PHPUnit's isolated per-suite runs, but destructive if run against a
34+
* shared CI/dev database that also has other fixtures (e.g. the
35+
* idp:create-super-admin/idp:create-raw-user users this same workflow
36+
* seeds). This command creates only the one client, without touching
37+
* anything else.
38+
*
39+
* @package App\Console\Commands
40+
*/
41+
class CreateOAuth2TestClient extends Command
42+
{
43+
protected $signature = 'idp:create-oauth2-test-client';
44+
45+
protected $description = 'Create the confidential OAuth2 client the e2e oauth2 suite authorizes against (idempotent)';
46+
47+
private const CLIENT_ID = '.-_~87D8/Vcvr6fvQbH4HyNgwTlfSyQ3x.openstack.client';
48+
private const CLIENT_SECRET = 'ITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhgITc/6Y5N7kOtGKhg';
49+
private const REDIRECT_URI = 'https://www.test.com/oauth2';
50+
private const OWNER_EMAIL = 'oauth2-test-client-owner@test.com';
51+
52+
public function handle(): int
53+
{
54+
$repository = EntityManager::getRepository(Client::class);
55+
$client = $repository->findOneBy(['client_id' => self::CLIENT_ID]);
56+
57+
if (is_null($client)) {
58+
// The consent screen's getDeveloperEmail() dereferences the
59+
// client's owner unconditionally - a client without one 500s
60+
// as soon as a real login reaches /accounts/user/consent.
61+
$owner = EntityManager::getRepository(User::class)->findOneBy(['email' => self::OWNER_EMAIL]);
62+
if (is_null($owner)) {
63+
$owner = new User();
64+
$owner->setEmail(self::OWNER_EMAIL);
65+
$owner->verifyEmail();
66+
$owner->setPassword('1Qaz2wsx!');
67+
$owner->setFirstName(self::OWNER_EMAIL);
68+
$owner->setLastName(self::OWNER_EMAIL);
69+
$owner->setIdentifier(self::OWNER_EMAIL);
70+
EntityManager::persist($owner);
71+
EntityManager::flush();
72+
}
73+
74+
$client = ClientFactory::build([
75+
'app_name' => 'oauth2_test_app',
76+
'app_description' => 'oauth2_test_app',
77+
'client_id' => self::CLIENT_ID,
78+
'client_secret' => self::CLIENT_SECRET,
79+
'client_type' => IClient::ClientType_Confidential,
80+
'application_type' => IClient::ApplicationType_Web_App,
81+
'token_endpoint_auth_method' => OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic,
82+
'owner' => $owner,
83+
'rotate_refresh_token' => true,
84+
'use_refresh_token' => true,
85+
'redirect_uris' => self::REDIRECT_URI,
86+
]);
87+
EntityManager::persist($client);
88+
EntityManager::flush();
89+
$this->info('Created client: ' . self::CLIENT_ID);
90+
} else {
91+
$this->info('Client already exists: ' . self::CLIENT_ID);
92+
}
93+
94+
$scope = EntityManager::getRepository(ApiScope::class)->findOneBy(['name' => 'profile']);
95+
if (is_null($scope)) {
96+
$this->error("api scope 'profile' not found - run php artisan db:seed first");
97+
return 1;
98+
}
99+
100+
$client->addScope($scope);
101+
EntityManager::persist($client);
102+
EntityManager::flush();
103+
104+
return 0;
105+
}
106+
}

app/Console/Kernel.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class Kernel extends ConsoleKernel
3030
Commands\CleanOpenIdStaleData::class,
3131
Commands\CreateSuperAdmin::class,
3232
Commands\CreateRawUser::class,
33+
Commands\CreateOAuth2TestClient::class,
3334
Commands\GetLatestOtp::class,
3435
Commands\SpammerProcess\RebuildUserSpammerEstimator::class,
3536
Commands\SpammerProcess\UserSpammerProcessor::class,

0 commit comments

Comments
 (0)