Skip to content

Commit 404ff53

Browse files
committed
test: drop Playwright login flow
1 parent ca76aee commit 404ff53

2 files changed

Lines changed: 166 additions & 62 deletions

File tree

pom.xml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
<testcontainers-keycloak.version>3.9.0</testcontainers-keycloak.version>
2929
<rest-assured.version>5.3.2</rest-assured.version>
3030
<slf4j-simple.version>2.0.17</slf4j-simple.version>
31-
<playwright.version>1.56.0</playwright.version>
31+
<jsoup.version>1.18.3</jsoup.version>
3232
<testcontainers.version>1.21.3</testcontainers.version>
3333
</properties>
3434

@@ -87,18 +87,18 @@
8787
<version>${slf4j-simple.version}</version>
8888
<scope>test</scope>
8989
</dependency>
90-
<dependency>
91-
<groupId>com.microsoft.playwright</groupId>
92-
<artifactId>playwright</artifactId>
93-
<version>${playwright.version}</version>
94-
<scope>test</scope>
95-
</dependency>
9690
<dependency>
9791
<groupId>io.rest-assured</groupId>
9892
<artifactId>rest-assured</artifactId>
9993
<version>${rest-assured.version}</version>
10094
<scope>test</scope>
10195
</dependency>
96+
<dependency>
97+
<groupId>org.jsoup</groupId>
98+
<artifactId>jsoup</artifactId>
99+
<version>${jsoup.version}</version>
100+
<scope>test</scope>
101+
</dependency>
102102
</dependencies>
103103

104104
<build>

src/test/java/fr/cnieg/keycloak/providers/login/attribute/KeycloakLoginAttributeProviderTest.java

Lines changed: 159 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,39 @@
11
package fr.cnieg.keycloak.providers.login.attribute;
22

3-
import com.microsoft.playwright.Browser;
4-
import com.microsoft.playwright.BrowserContext;
5-
import com.microsoft.playwright.Page;
6-
import com.microsoft.playwright.Playwright;
7-
import com.microsoft.playwright.options.AriaRole;
83
import dasniko.testcontainers.keycloak.KeycloakContainer;
94
import io.restassured.common.mapper.TypeRef;
105
import io.restassured.http.ContentType;
6+
import io.restassured.response.Response;
7+
import io.restassured.specification.RequestSpecification;
8+
import org.jsoup.Jsoup;
9+
import org.jsoup.nodes.Document;
10+
import org.jsoup.nodes.Element;
1111
import org.junit.jupiter.api.*;
1212
import org.subethamail.wiser.Wiser;
1313
import org.testcontainers.junit.jupiter.Container;
1414
import org.testcontainers.junit.jupiter.Testcontainers;
1515

1616
import java.time.Duration;
17+
import java.util.HashMap;
1718
import java.util.Collections;
1819
import java.util.List;
1920
import java.util.Map;
2021
import java.util.Objects;
2122
import java.util.function.Predicate;
23+
import java.util.UUID;
2224
import java.util.stream.Collectors;
2325

2426
import static io.restassured.RestAssured.given;
2527
import static org.junit.jupiter.api.Assertions.assertEquals;
2628
import static org.junit.jupiter.api.Assertions.assertNull;
29+
import static org.junit.jupiter.api.Assertions.assertNotNull;
2730
import static org.junit.jupiter.api.Assertions.fail;
2831

2932
@Testcontainers
3033
class KeycloakLoginAttributeProviderTest {
3134
private static final int SMTP_PORT = 2525;
35+
private static final String REALM = "testloginattribute";
36+
private static final String CLIENT_ID = "account-console";
3237

3338
static {
3439
org.testcontainers.Testcontainers.exposeHostPorts(SMTP_PORT);
@@ -40,49 +45,36 @@ class KeycloakLoginAttributeProviderTest {
4045
.withAdminPassword("admin")
4146
.withDefaultProviderClasses()
4247
.withRealmImportFile("/testloginattribute-realm.json");
43-
private static Playwright playwright;
44-
private static Browser browser;
4548
private static KeycloakEventsClient eventsClient;
4649
private static Wiser smtpServer;
47-
BrowserContext context;
48-
Page page;
4950

5051
@BeforeAll
51-
static void launchBrowser() {
52+
static void startInfrastructure() {
5253
smtpServer = new Wiser();
5354
smtpServer.setPort(SMTP_PORT);
5455
smtpServer.start();
55-
playwright = Playwright.create();
56-
browser = playwright.chromium().launch();
57-
eventsClient = new KeycloakEventsClient(KEYCLOAK_CONTAINER, "testloginattribute");
56+
eventsClient = new KeycloakEventsClient(KEYCLOAK_CONTAINER, REALM);
5857
}
5958

6059
@AfterAll
61-
static void closeBrowser() {
60+
static void stopInfrastructure() {
6261
smtpServer.stop();
63-
playwright.close();
6462
}
6563

6664
@BeforeEach
67-
void createContextAndPage() {
68-
context = browser.newContext();
69-
page = context.newPage();
65+
void clearEvents() {
7066
eventsClient.clearEvents();
7167
}
7268

73-
@AfterEach
74-
void closeContext() {
75-
context.close();
76-
}
77-
7869
@Test
7970
void test_should_publish_login_event_for_jane_with_login_name() {
8071
// Given
8172
String username = "janedoe";
8273
String password = "s3cr3t";
8374
// When
84-
openAccountConsole();
85-
submitLoginForm(username, password);
75+
HttpSession session = new HttpSession();
76+
LoginForm loginForm = loadLoginForm(session);
77+
submitLoginForm(session, loginForm, username, password);
8678
// Then
8779
KeycloakEvent loginEvent = awaitLoginEvent(username);
8880
assertNull(loginEvent.error());
@@ -95,8 +87,9 @@ void test_should_publish_login_event_for_john_with_attribute() {
9587
String attributeValueOfJohnDoe = "SHOULDBEOKFORLOGIN";
9688
String password = "s3cr3t";
9789
// When
98-
openAccountConsole();
99-
submitLoginForm(attributeValueOfJohnDoe, password);
90+
HttpSession session = new HttpSession();
91+
LoginForm loginForm = loadLoginForm(session);
92+
submitLoginForm(session, loginForm, attributeValueOfJohnDoe, password);
10093
// Then
10194
KeycloakEvent loginEvent = awaitLoginEvent(attributeValueOfJohnDoe, "johndoe");
10295
assertNull(loginEvent.error());
@@ -109,8 +102,9 @@ void test_should_publish_login_error_for_unknown_attribute() {
109102
String attributeValueOfJaneDoe = "SHOULDBEkoFORLOGIN";
110103
String password = "s3cr3t";
111104
// When
112-
openAccountConsole();
113-
submitLoginForm(attributeValueOfJaneDoe, password);
105+
HttpSession session = new HttpSession();
106+
LoginForm loginForm = loadLoginForm(session);
107+
submitLoginForm(session, loginForm, attributeValueOfJaneDoe, password);
114108
// Then
115109
KeycloakEvent loginError = awaitLoginErrorEvent(attributeValueOfJaneDoe, "user_not_found");
116110
assertEquals(attributeValueOfJaneDoe, loginError.details().get("username"));
@@ -122,12 +116,13 @@ void test_user_bill_should_be_locked_after_two_invalid_attempts_with_attribute()
122116
String attributeValueOfBillDoe = "SHOULDBEOKFORLOGINTOO";
123117
String invalidPassword = "fakes3cr3t";
124118
// When
125-
openAccountConsole();
126-
submitLoginForm(attributeValueOfBillDoe, invalidPassword);
127-
page.getByLabel("Password", new Page.GetByLabelOptions().setExact(true)).fill(invalidPassword);
128-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign In")).click();
129-
page.getByLabel("Password", new Page.GetByLabelOptions().setExact(true)).fill("s3cr3t");
130-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign In")).click();
119+
HttpSession session = new HttpSession();
120+
LoginForm loginForm = loadLoginForm(session);
121+
loginForm = submitLoginForm(session, loginForm, attributeValueOfBillDoe, invalidPassword);
122+
assertNotNull(loginForm, "Expected to remain on login form after invalid password");
123+
loginForm = submitLoginForm(session, loginForm, attributeValueOfBillDoe, invalidPassword);
124+
assertNotNull(loginForm, "Expected to remain on login form after second invalid password");
125+
submitLoginForm(session, loginForm, attributeValueOfBillDoe, "s3cr3t");
131126
// Then
132127
KeycloakEvent lockEvent = awaitLoginErrorEvent(attributeValueOfBillDoe, "user_temporarily_disabled");
133128
assertEquals(attributeValueOfBillDoe, lockEvent.details().get("username"));
@@ -138,8 +133,10 @@ void test_should_publish_reset_event_for_jane_with_login_name() {
138133
// Given
139134
String username = "janedoe";
140135
// When
141-
openForgotPasswordForm();
142-
submitResetForm(username);
136+
HttpSession session = new HttpSession();
137+
LoginForm loginForm = loadLoginForm(session);
138+
ResetPasswordForm resetForm = loadResetPasswordForm(session, loginForm.resetPasswordUrl());
139+
submitResetForm(session, resetForm, username);
143140
// Then
144141
KeycloakEvent resetEvent = awaitResetPasswordEvent(username);
145142
assertNull(resetEvent.error());
@@ -150,8 +147,10 @@ void test_should_publish_reset_event_for_john_with_attribute() {
150147
// Given
151148
String attributeValueOfJohnDoe = "SHOULDBEOKFORLOGIN";
152149
// When
153-
openForgotPasswordForm();
154-
submitResetForm(attributeValueOfJohnDoe);
150+
HttpSession session = new HttpSession();
151+
LoginForm loginForm = loadLoginForm(session);
152+
ResetPasswordForm resetForm = loadResetPasswordForm(session, loginForm.resetPasswordUrl());
153+
submitResetForm(session, resetForm, attributeValueOfJohnDoe);
155154
// Then
156155
KeycloakEvent resetEvent = awaitResetPasswordEvent(attributeValueOfJohnDoe, "johndoe");
157156
assertNull(resetEvent.error());
@@ -162,8 +161,10 @@ void test_should_publish_reset_error_for_unknown_attribute() {
162161
// Given
163162
String attributeValueOfJaneDoe = "SHOULDBEkoFORLOGIN";
164163
// When
165-
openForgotPasswordForm();
166-
submitResetForm(attributeValueOfJaneDoe);
164+
HttpSession session = new HttpSession();
165+
LoginForm loginForm = loadLoginForm(session);
166+
ResetPasswordForm resetForm = loadResetPasswordForm(session, loginForm.resetPasswordUrl());
167+
submitResetForm(session, resetForm, attributeValueOfJaneDoe);
167168
// Then
168169
KeycloakEvent resetError = awaitResetPasswordErrorEvent(attributeValueOfJaneDoe, "user_not_found");
169170
assertEquals(attributeValueOfJaneDoe, resetError.details().get("username"));
@@ -228,31 +229,134 @@ private boolean matchesUsernamesIfPresent(KeycloakEvent event, String... usernam
228229
return false;
229230
}
230231

231-
private void openAccountConsole() {
232-
page.navigate(KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/testloginattribute/account");
233-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")).click();
232+
private LoginForm loadLoginForm(HttpSession session) {
233+
Response response = session.get(authorizationEndpoint(), Map.of(
234+
"client_id", CLIENT_ID,
235+
"redirect_uri", accountRedirectUri(),
236+
"response_type", "code",
237+
"scope", "openid",
238+
"state", UUID.randomUUID().toString(),
239+
"nonce", UUID.randomUUID().toString()
240+
));
241+
assertEquals(200, response.statusCode(), "Unable to load login page");
242+
return LoginForm.parse(response.getBody().asString());
243+
}
244+
245+
private LoginForm submitLoginForm(HttpSession session, LoginForm form, String username, String password) {
246+
Response response = session.postForm(form.action(), Map.of(
247+
"username", username,
248+
"password", password,
249+
"credentialId", form.credentialId()
250+
));
251+
if (response.statusCode() == 200) {
252+
return LoginForm.parse(response.getBody().asString());
253+
}
254+
if (response.statusCode() == 302 || response.statusCode() == 303) {
255+
return null;
256+
}
257+
fail("Unexpected status code when submitting login form: " + response.statusCode());
258+
return null;
259+
}
260+
261+
private ResetPasswordForm loadResetPasswordForm(HttpSession session, String url) {
262+
Response response = session.get(url, Collections.emptyMap());
263+
assertEquals(200, response.statusCode(), "Unable to load reset password form");
264+
return ResetPasswordForm.parse(response.getBody().asString());
265+
}
266+
267+
private void submitResetForm(HttpSession session, ResetPasswordForm form, String username) {
268+
Response response = session.postForm(form.action(), Map.of("username", username));
269+
if (response.statusCode() == 200 || response.statusCode() == 302) {
270+
return;
271+
}
272+
fail("Unexpected status code when submitting reset password form: " + response.statusCode());
234273
}
235274

236-
private void submitLoginForm(String username, String password) {
237-
page.getByLabel("Username").fill(username);
238-
page.getByLabel("Password", new Page.GetByLabelOptions().setExact(true)).fill(password);
239-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign In")).click();
275+
private String authorizationEndpoint() {
276+
return KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + REALM + "/protocol/openid-connect/auth";
240277
}
241278

242-
private void openForgotPasswordForm() {
243-
openAccountConsole();
244-
page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("Forgot Password?"))
245-
.click();
279+
private String accountRedirectUri() {
280+
return KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + REALM + "/account/";
246281
}
247282

248-
private void submitResetForm(String username) {
249-
page.getByLabel("Username").fill(username);
250-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit")).click();
283+
private static String toAbsoluteUrl(String url) {
284+
if (url == null || url.isBlank()) {
285+
return url;
286+
}
287+
if (url.startsWith("http://") || url.startsWith("https://")) {
288+
return url;
289+
}
290+
if (!url.startsWith("/")) {
291+
url = "/" + url;
292+
}
293+
return KEYCLOAK_CONTAINER.getAuthServerUrl() + url;
251294
}
252295

253296
private record KeycloakEvent(String type, String error, Map<String, String> details) {
254297
}
255298

299+
private record LoginForm(String action, String resetPasswordUrl, String credentialId) {
300+
static LoginForm parse(String html) {
301+
Document document = Jsoup.parse(html);
302+
Element form = document.getElementById("kc-form-login");
303+
if (form == null) {
304+
fail("Login form not found in response");
305+
}
306+
Element resetLink = document.getElementById("kc-reset-password");
307+
if (resetLink == null) {
308+
fail("Reset password link not found in login page");
309+
}
310+
Element credentialInput = form.selectFirst("input[name=credentialId]");
311+
String credentialId = credentialInput != null ? credentialInput.attr("value") : "";
312+
return new LoginForm(
313+
toAbsoluteUrl(form.attr("action")),
314+
toAbsoluteUrl(resetLink.attr("href")),
315+
credentialId
316+
);
317+
}
318+
}
319+
320+
private record ResetPasswordForm(String action) {
321+
static ResetPasswordForm parse(String html) {
322+
Document document = Jsoup.parse(html);
323+
Element form = document.getElementById("kc-reset-password-form");
324+
if (form == null) {
325+
fail("Reset password form not found in response");
326+
}
327+
return new ResetPasswordForm(toAbsoluteUrl(form.attr("action")));
328+
}
329+
}
330+
331+
private static class HttpSession {
332+
private final Map<String, String> cookies = new HashMap<>();
333+
334+
Response get(String url, Map<String, ?> queryParams) {
335+
RequestSpecification specification = given()
336+
.redirects().follow(false)
337+
.cookies(cookies);
338+
if (queryParams != null && !queryParams.isEmpty()) {
339+
specification.queryParams(queryParams);
340+
}
341+
Response response = specification.get(url);
342+
cookies.putAll(response.getCookies());
343+
return response;
344+
}
345+
346+
Response postForm(String url, Map<String, ?> formParams) {
347+
RequestSpecification specification = given()
348+
.redirects().follow(false)
349+
.cookies(cookies)
350+
.contentType(ContentType.URLENC);
351+
if (formParams != null && !formParams.isEmpty()) {
352+
specification.formParams(formParams);
353+
}
354+
Response response = specification.post(url);
355+
cookies.putAll(response.getCookies());
356+
return response;
357+
}
358+
}
359+
256360
private static class KeycloakEventsClient {
257361
private final String authServerUrl;
258362
private final String realm;

0 commit comments

Comments
 (0)