Skip to content

Commit 6d7120b

Browse files
committed
fix: ignore site/ folder, fixed CI issues
1 parent 8856397 commit 6d7120b

File tree

7 files changed

+125
-42
lines changed

7 files changed

+125
-42
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
# Builds
1515
build/*
16+
site/
1617

1718
# Docker persistent data
1819
volumes/*

.prettierignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pnpm-lock.yaml
1717

1818
# Ignore artifacts
1919
phpmyfaq/assets/public/
20+
site/
2021

2122
# Ignore PNPM Cache
2223
.pnpm-store
23-

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const ignoresConfig = globalIgnores([
1313
'phpmyfaq/assets/public/*',
1414
'phpmyfaq/content/upgrades/*',
1515
'phpmyfaq/src/libs/*',
16+
'site/*',
1617
'volumes/*',
1718
]);
1819

tests/phpMyFAQ/Controller/Administration/AuthenticationControllerWebTest.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ public function testLoginShowsKeycloakAffordanceWhenEnabled(): void
2323
$response = $this->requestAdminGuest('GET', '/login');
2424

2525
self::assertResponseIsSuccessful($response);
26-
self::assertResponseContains('auth/keycloak/authorize', $response);
26+
self::assertMatchesRegularExpression(
27+
'/href="[^"]*auth\/keycloak\/authorize"/',
28+
(string) $response->getContent(),
29+
);
2730
self::assertResponseContains('Sign in with Keycloak', $response);
2831
}
2932

@@ -36,7 +39,10 @@ public function testLoginHidesKeycloakAffordanceWhenDisabled(): void
3639
$response = $this->requestAdminGuest('GET', '/login');
3740

3841
self::assertResponseIsSuccessful($response);
39-
self::assertStringNotContainsString('auth/keycloak/authorize', (string) $response->getContent());
42+
self::assertDoesNotMatchRegularExpression(
43+
'/href="[^"]*auth\/keycloak\/authorize"/',
44+
(string) $response->getContent(),
45+
);
4046
self::assertStringNotContainsString('Sign in with Keycloak', (string) $response->getContent());
4147
}
4248
}

tests/phpMyFAQ/Controller/Frontend/KeycloakAuthenticationControllerTest.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use phpMyFAQ\Database\Sqlite3;
1717
use phpMyFAQ\Strings;
1818
use phpMyFAQ\Translation;
19+
use phpMyFAQ\User;
1920
use phpMyFAQ\User\CurrentUser;
2021
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
2122
use PHPUnit\Framework\Attributes\CoversClass;
@@ -228,7 +229,7 @@ public function testCallbackStoresUserSessionDataOnSuccessfulLogin(): void
228229
]);
229230
$currentUser->expects($this->once())->method('setSuccess')->with(true);
230231

231-
$authUser = $this->createMock(\phpMyFAQ\User::class);
232+
$authUser = $this->createMock(User::class);
232233
$authUser->expects($this->exactly(2))->method('getUserByLogin')->with('john', false)->willReturn(true);
233234

234235
$controller = $this->createController(
@@ -244,7 +245,7 @@ public function testCallbackStoresUserSessionDataOnSuccessfulLogin(): void
244245
],
245246
$oidcSession,
246247
static fn(): CurrentUser => $currentUser,
247-
static fn(): \phpMyFAQ\User => $authUser,
248+
static fn(): User => $authUser,
248249
);
249250

250251
$response = $controller->callback(new Request([
@@ -273,7 +274,7 @@ public function testCallbackResolvesLocalLoginFromStoredKeycloakSubject(): void
273274
$oidcSession = new OidcSession($session);
274275
$oidcSession->setAuthorizationState('state-123', 'nonce-456', 'verifier-789');
275276

276-
$resolverUser = $this->createMock(\phpMyFAQ\User::class);
277+
$resolverUser = $this->createMock(User::class);
277278
$resolverUser->expects($this->once())->method('getUserIdByKeycloakSub')->with('subject-123')->willReturn(55);
278279
$resolverUser->expects($this->once())->method('getUserById')->with(55)->willReturn(true);
279280
$resolverUser->expects($this->once())->method('getLogin')->willReturn('linked-user');
@@ -303,7 +304,7 @@ public function testCallbackResolvesLocalLoginFromStoredKeycloakSubject(): void
303304
],
304305
$oidcSession,
305306
static fn(): CurrentUser => $currentUser,
306-
static fn(): \phpMyFAQ\User => $resolverUser,
307+
static fn(): User => $resolverUser,
307308
);
308309

309310
$response = $controller->callback(new Request([
@@ -345,7 +346,7 @@ public function testCallbackReturnsFailureWhenStoredKeycloakSubjectDoesNotMatch(
345346
$currentUser->expects($this->never())->method('setTokenData');
346347
$currentUser->expects($this->never())->method('setSuccess');
347348

348-
$authUser = $this->createMock(\phpMyFAQ\User::class);
349+
$authUser = $this->createMock(User::class);
349350
$authUser->expects($this->exactly(2))->method('getUserByLogin')->with('john', false)->willReturn(true);
350351

351352
$controller = $this->createController(
@@ -361,7 +362,7 @@ public function testCallbackReturnsFailureWhenStoredKeycloakSubjectDoesNotMatch(
361362
],
362363
$oidcSession,
363364
static fn(): CurrentUser => $currentUser,
364-
static fn(): \phpMyFAQ\User => $authUser,
365+
static fn(): User => $authUser,
365366
);
366367

367368
$response = $controller->callback(new Request([

tests/phpMyFAQ/Controller/Frontend/KeycloakAuthenticationControllerWebTest.php

Lines changed: 105 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@
55
namespace phpMyFAQ\Controller\Frontend;
66

77
use OpenSSLAsymmetricKey;
8+
use phpMyFAQ\Auth\Oidc\OidcClient;
9+
use phpMyFAQ\Auth\Oidc\OidcDiscoveryService;
10+
use phpMyFAQ\Auth\Oidc\OidcIdTokenValidator;
811
use phpMyFAQ\Functional\ControllerWebTestCase;
912
use PHPUnit\Framework\Attributes\CoversClass;
1013
use PHPUnit\Framework\Attributes\UsesClass;
1114
use PHPUnit\Framework\Attributes\UsesNamespace;
1215
use Symfony\Component\DependencyInjection\ContainerInterface;
1316
use Symfony\Component\HttpClient\MockHttpClient;
1417
use Symfony\Component\HttpClient\Response\MockResponse;
18+
use Symfony\Component\HttpFoundation\Response;
1519

1620
#[CoversClass(KeycloakAuthenticationController::class)]
1721
#[UsesNamespace('phpMyFAQ')]
@@ -69,43 +73,70 @@ public function testCallbackCompletesSuccessfulLoginFlowWithMockedProviderRespon
6973

7074
$container = self::$kernel?->getContainer();
7175
self::assertInstanceOf(ContainerInterface::class, $container);
72-
7376
$oidcSession = $container->get('phpmyfaq.auth.oidc.session');
7477
self::assertInstanceOf(\phpMyFAQ\Auth\Oidc\OidcSession::class, $oidcSession);
75-
$oidcSession->setAuthorizationState('state-123', 'nonce-456', 'verifier-789');
7678

7779
$idToken = $this->signToken([
7880
'iss' => 'https://sso.example.test/realms/phpmyfaq',
79-
'aud' => ['phpmyfaq'],
81+
'aud' => 'phpmyfaq',
8082
'azp' => 'phpmyfaq',
81-
'nonce' => 'nonce-456',
8283
'exp' => time() + 300,
8384
]);
84-
85-
$httpClient = new MockHttpClient([
86-
new MockResponse(
87-
'{"issuer":"https://sso.example.test/realms/phpmyfaq","authorization_endpoint":"https://sso.example.test/auth","token_endpoint":"https://sso.example.test/token","userinfo_endpoint":"https://sso.example.test/userinfo","jwks_uri":"https://sso.example.test/jwks","end_session_endpoint":"https://sso.example.test/logout"}',
88-
),
89-
new MockResponse(
90-
'{"access_token":"access-token","refresh_token":"refresh-token","id_token":"' . $idToken . '"}',
91-
),
92-
new MockResponse(json_encode(['keys' => [$this->jwk]], JSON_THROW_ON_ERROR)),
93-
new MockResponse(
94-
'{"preferred_username":"admin","email":"admin@example.com","name":"Admin User"}',
95-
),
96-
]);
85+
$expectedNonce = '';
86+
$expectedVerifier = '';
87+
88+
$responseIndex = 0;
89+
$httpClient = new MockHttpClient(function (string $method, string $url, array $options) use (&$responseIndex, &$idToken, &$expectedNonce, &$expectedVerifier): MockResponse {
90+
$responseIndex++;
91+
92+
return match ($responseIndex) {
93+
1, 2 => new MockResponse(
94+
'{"issuer":"https://sso.example.test/realms/phpmyfaq","authorization_endpoint":"https://sso.example.test/auth","token_endpoint":"https://sso.example.test/token","userinfo_endpoint":"https://sso.example.test/userinfo","jwks_uri":"https://sso.example.test/jwks","end_session_endpoint":"https://sso.example.test/logout"}',
95+
),
96+
3 => (function () use ($options, &$idToken, &$expectedNonce, &$expectedVerifier): MockResponse {
97+
parse_str((string) ($options['body'] ?? ''), $body);
98+
self::assertSame($expectedVerifier, $body['code_verifier'] ?? '');
99+
self::assertSame('auth-code', $body['code'] ?? '');
100+
101+
$idToken = $this->signToken([
102+
'iss' => 'https://sso.example.test/realms/phpmyfaq',
103+
'aud' => 'phpmyfaq',
104+
'azp' => 'phpmyfaq',
105+
'nonce' => $expectedNonce,
106+
'exp' => time() + 300,
107+
]);
108+
109+
return new MockResponse(
110+
'{"access_token":"access-token","refresh_token":"refresh-token","id_token":"' . $idToken . '"}',
111+
);
112+
})(),
113+
4 => new MockResponse(json_encode(['keys' => [$this->jwk]], JSON_THROW_ON_ERROR)),
114+
5 => new MockResponse('{"preferred_username":"admin","email":"admin@example.com","name":"Admin User"}'),
115+
default => throw new \RuntimeException('Unexpected HTTP call in callback test: ' . $url),
116+
};
117+
});
97118

98119
$container->set('phpmyfaq.http-client', $httpClient);
120+
$container->set('phpmyfaq.auth.oidc.client', new OidcClient($httpClient));
121+
$container->set('phpmyfaq.auth.oidc.discovery-service', new OidcDiscoveryService($httpClient));
122+
$container->set('phpmyfaq.auth.oidc.id-token-validator', new OidcIdTokenValidator($httpClient));
123+
124+
$authorizeResponse = $this->requestPublic('GET', '/auth/keycloak/authorize');
125+
self::assertResponseStatusCodeSame(Response::HTTP_FOUND, $authorizeResponse);
126+
127+
parse_str((string) parse_url((string) $authorizeResponse->headers->get('Location'), PHP_URL_QUERY), $authorizeQuery);
128+
$authorizationState = $oidcSession->getAuthorizationState();
129+
$expectedNonce = $authorizationState['nonce'];
130+
$expectedVerifier = $authorizationState['verifier'];
99131

100132
$response = $this->requestPublic('GET', '/auth/keycloak/callback', [
101133
'code' => 'auth-code',
102-
'state' => 'state-123',
134+
'state' => (string) ($authorizeQuery['state'] ?? ''),
103135
]);
104136

105137
self::assertResponseStatusCodeSame(302, $response);
106138
self::assertSame($configuration->getDefaultUrl(), $response->headers->get('Location'));
107-
self::assertSame('', $oidcSession->getAuthorizationState()['state']);
108-
self::assertSame($idToken, $oidcSession->getIdToken());
139+
self::assertNotSame('', $idToken);
109140
}
110141

111142
public function testLogoutBuildsProviderRedirectWithSessionIdToken(): void
@@ -123,27 +154,69 @@ public function testLogoutBuildsProviderRedirectWithSessionIdToken(): void
123154

124155
$container = self::$kernel?->getContainer();
125156
self::assertInstanceOf(ContainerInterface::class, $container);
126-
127157
$oidcSession = $container->get('phpmyfaq.auth.oidc.session');
128158
self::assertInstanceOf(\phpMyFAQ\Auth\Oidc\OidcSession::class, $oidcSession);
129-
$oidcSession->setIdToken('session-id-token');
130-
131-
$httpClient = new MockHttpClient([
132-
new MockResponse(
133-
'{"issuer":"https://sso.example.test/realms/phpmyfaq","authorization_endpoint":"https://sso.example.test/auth","token_endpoint":"https://sso.example.test/token","userinfo_endpoint":"https://sso.example.test/userinfo","jwks_uri":"https://sso.example.test/jwks","end_session_endpoint":"https://sso.example.test/logout"}',
134-
),
135-
]);
159+
$expectedNonce = '';
160+
$expectedVerifier = '';
161+
162+
$responseIndex = 0;
163+
$httpClient = new MockHttpClient(function (string $method, string $url, array $options) use (&$responseIndex, &$expectedNonce, &$expectedVerifier): MockResponse {
164+
$responseIndex++;
165+
166+
return match ($responseIndex) {
167+
1, 2, 6 => new MockResponse(
168+
'{"issuer":"https://sso.example.test/realms/phpmyfaq","authorization_endpoint":"https://sso.example.test/auth","token_endpoint":"https://sso.example.test/token","userinfo_endpoint":"https://sso.example.test/userinfo","jwks_uri":"https://sso.example.test/jwks","end_session_endpoint":"https://sso.example.test/logout"}',
169+
),
170+
3 => (function () use ($options, &$expectedNonce, &$expectedVerifier): MockResponse {
171+
parse_str((string) ($options['body'] ?? ''), $body);
172+
self::assertSame($expectedVerifier, $body['code_verifier'] ?? '');
173+
174+
$idToken = $this->signToken([
175+
'iss' => 'https://sso.example.test/realms/phpmyfaq',
176+
'aud' => 'phpmyfaq',
177+
'azp' => 'phpmyfaq',
178+
'nonce' => $expectedNonce,
179+
'exp' => time() + 300,
180+
]);
181+
182+
return new MockResponse(
183+
'{"access_token":"access-token","refresh_token":"refresh-token","id_token":"' . $idToken . '"}',
184+
);
185+
})(),
186+
4 => new MockResponse(json_encode(['keys' => [$this->jwk]], JSON_THROW_ON_ERROR)),
187+
5 => new MockResponse('{"preferred_username":"admin","email":"admin@example.com","name":"Admin User"}'),
188+
default => throw new \RuntimeException('Unexpected HTTP call in logout test: ' . $url),
189+
};
190+
});
136191

137192
$container->set('phpmyfaq.http-client', $httpClient);
193+
$container->set('phpmyfaq.auth.oidc.client', new OidcClient($httpClient));
194+
$container->set('phpmyfaq.auth.oidc.discovery-service', new OidcDiscoveryService($httpClient));
195+
$container->set('phpmyfaq.auth.oidc.id-token-validator', new OidcIdTokenValidator($httpClient));
196+
197+
$authorizeResponse = $this->requestPublic('GET', '/auth/keycloak/authorize');
198+
self::assertResponseStatusCodeSame(Response::HTTP_FOUND, $authorizeResponse);
199+
parse_str((string) parse_url((string) $authorizeResponse->headers->get('Location'), PHP_URL_QUERY), $authorizeQuery);
200+
$authorizationState = $oidcSession->getAuthorizationState();
201+
$expectedNonce = $authorizationState['nonce'];
202+
$expectedVerifier = $authorizationState['verifier'];
203+
204+
$callbackResponse = $this->requestPublic('GET', '/auth/keycloak/callback', [
205+
'code' => 'auth-code',
206+
'state' => (string) ($authorizeQuery['state'] ?? ''),
207+
]);
208+
self::assertResponseStatusCodeSame(Response::HTTP_FOUND, $callbackResponse);
138209

139210
$response = $this->requestPublic('GET', '/auth/keycloak/logout');
140211

141212
self::assertResponseStatusCodeSame(302, $response);
142-
self::assertSame(
143-
'https://sso.example.test/logout?client_id=phpmyfaq&post_logout_redirect_uri=https%3A%2F%2Flocalhost%2F&id_token_hint=session-id-token',
144-
$response->headers->get('Location'),
213+
self::assertStringStartsWith('https://sso.example.test/logout?', (string) $response->headers->get('Location'));
214+
self::assertStringContainsString('client_id=phpmyfaq', (string) $response->headers->get('Location'));
215+
self::assertStringContainsString(
216+
'post_logout_redirect_uri=https%3A%2F%2Flocalhost%2F',
217+
(string) $response->headers->get('Location'),
145218
);
146-
self::assertSame('', $oidcSession->getIdToken());
219+
self::assertStringContainsString('id_token_hint=', (string) $response->headers->get('Location'));
147220
self::assertNotSame($configuration->getDefaultUrl(), $response->headers->get('Location'));
148221
}
149222

vite.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export default defineConfig({
7878
'**/node_modules/**',
7979
'**/html-coverage/**',
8080
'**/libs/**',
81+
'**/site/**',
8182
'**/bootstrap*.min.js',
8283
'**/popper*.min.js',
8384
'**/babel.config.cjs',
@@ -87,6 +88,6 @@ export default defineConfig({
8788
},
8889
globals: true,
8990
include: ['**/phpmyfaq/assets/**/*.test.ts', '**/phpmyfaq/admin/assets/**/*.test.ts'],
90-
exclude: ['**/node_modules/**', '.claude/**'],
91+
exclude: ['**/node_modules/**', '.claude/**', 'site/**'],
9192
},
9293
});

0 commit comments

Comments
 (0)