Skip to content

Commit 680100f

Browse files
committed
test: assert keycloak events instead of ui
1 parent 725c693 commit 680100f

2 files changed

Lines changed: 171 additions & 64 deletions

File tree

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

Lines changed: 170 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,44 @@
66
import com.microsoft.playwright.Playwright;
77
import com.microsoft.playwright.options.AriaRole;
88
import dasniko.testcontainers.keycloak.KeycloakContainer;
9+
import io.restassured.common.mapper.TypeRef;
10+
import io.restassured.http.ContentType;
911
import org.junit.jupiter.api.*;
1012
import org.testcontainers.junit.jupiter.Container;
1113
import org.testcontainers.junit.jupiter.Testcontainers;
1214

13-
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;
15+
import java.time.Duration;
16+
import java.util.Collections;
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.util.Objects;
20+
import java.util.function.Predicate;
21+
import java.util.stream.Collectors;
22+
23+
import static io.restassured.RestAssured.given;
24+
import static org.junit.jupiter.api.Assertions.assertEquals;
25+
import static org.junit.jupiter.api.Assertions.assertNull;
26+
import static org.junit.jupiter.api.Assertions.fail;
1427

1528
@Testcontainers
1629
class KeycloakLoginAttributeProviderTest {
1730
@Container
1831
private static final KeycloakContainer KEYCLOAK_CONTAINER = new KeycloakContainer()
32+
.withAdminUsername("admin")
33+
.withAdminPassword("admin")
1934
.withDefaultProviderClasses()
2035
.withRealmImportFile("/testloginattribute-realm.json");
2136
private static Playwright playwright;
2237
private static Browser browser;
38+
private static KeycloakEventsClient eventsClient;
2339
BrowserContext context;
2440
Page page;
2541

2642
@BeforeAll
2743
static void launchBrowser() {
2844
playwright = Playwright.create();
2945
browser = playwright.chromium().launch();
46+
eventsClient = new KeycloakEventsClient(KEYCLOAK_CONTAINER, "testloginattribute");
3047
}
3148

3249
@AfterAll
@@ -38,6 +55,7 @@ static void closeBrowser() {
3855
void createContextAndPage() {
3956
context = browser.newContext();
4057
page = context.newPage();
58+
eventsClient.clearEvents();
4159
}
4260

4361
@AfterEach
@@ -46,120 +64,209 @@ void closeContext() {
4664
}
4765

4866
@Test
49-
void test_should_identify_jane_with_Login_name() {
67+
void test_should_publish_login_event_for_jane_with_login_name() {
5068
// Given
5169
String username = "janedoe";
5270
String password = "s3cr3t";
53-
String expected = username;
5471
// When
55-
page.navigate(KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/testloginattribute/account");
56-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")).click();
57-
page.getByLabel("Username").fill(username);
58-
page.getByLabel("Password", new Page.GetByLabelOptions().setExact(true)).fill(password);
59-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign In")).click();
72+
openAccountConsole();
73+
submitLoginForm(username, password);
6074
// Then
61-
assertThat(page.getByRole(AriaRole.TEXTBOX, new Page.GetByRoleOptions().setName("username"))).hasValue(expected);
75+
KeycloakEvent loginEvent = eventsClient.awaitEvent(event -> "LOGIN".equals(event.type()));
76+
assertNull(loginEvent.error());
77+
assertEquals(username, loginEvent.details().get("username"));
6278
}
6379

6480
@Test
65-
void test_should_identify_john_with_attribute() {
81+
void test_should_publish_login_event_for_john_with_attribute() {
6682
// Given
6783
String attributeValueOfJohnDoe = "SHOULDBEOKFORLOGIN";
6884
String password = "s3cr3t";
69-
String expected = "johndoe";
7085
// When
71-
page.navigate(KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/testloginattribute/account");
72-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")).click();
73-
page.getByLabel("Username").fill(attributeValueOfJohnDoe);
74-
page.getByLabel("Password", new Page.GetByLabelOptions().setExact(true)).fill(password);
75-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign In")).click();
86+
openAccountConsole();
87+
submitLoginForm(attributeValueOfJohnDoe, password);
7688
// Then
77-
assertThat(page.getByRole(AriaRole.TEXTBOX, new Page.GetByRoleOptions().setName("username"))).hasValue(expected);
89+
KeycloakEvent loginEvent = eventsClient.awaitEvent(event -> "LOGIN".equals(event.type()));
90+
assertNull(loginEvent.error());
91+
assertEquals(attributeValueOfJohnDoe, loginEvent.details().get("username"));
7892
}
7993

8094
@Test
81-
void test_should_not_identify_jane_with_attribute() {
95+
void test_should_publish_login_error_for_unknown_attribute() {
8296
// Given
8397
String attributeValueOfJaneDoe = "SHOULDBEkoFORLOGIN";
8498
String password = "s3cr3t";
85-
String expected = "Invalid username or password.";
8699
// When
87-
page.navigate(KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/testloginattribute/account");
88-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")).click();
89-
page.getByLabel("Username").fill(attributeValueOfJaneDoe);
90-
page.getByLabel("Password", new Page.GetByLabelOptions().setExact(true)).fill(password);
91-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign In")).click();
100+
openAccountConsole();
101+
submitLoginForm(attributeValueOfJaneDoe, password);
92102
// Then
93-
assertThat(page.getByText(expected)).isVisible();
103+
KeycloakEvent loginError = eventsClient.awaitEvent(event -> "LOGIN_ERROR".equals(event.type())
104+
&& "user_not_found".equals(event.error()));
105+
assertEquals(attributeValueOfJaneDoe, loginError.details().get("username"));
94106
}
95107

96108
@Test
97-
void test_user_bill_should_be_locked_after_two_attempts_with_attribute() {
109+
void test_user_bill_should_be_locked_after_two_invalid_attempts_with_attribute() {
98110
// Given
99111
String attributeValueOfBillDoe = "SHOULDBEOKFORLOGINTOO";
100-
String password = "fakes3cr3t";
101-
String expected = "Invalid username or password.";
112+
String invalidPassword = "fakes3cr3t";
102113
// When
103-
page.navigate(KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/testloginattribute/account");
104-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")).click();
105-
page.getByLabel("Username").fill(attributeValueOfBillDoe);
106-
page.getByLabel("Password", new Page.GetByLabelOptions().setExact(true)).fill(password);
114+
openAccountConsole();
115+
submitLoginForm(attributeValueOfBillDoe, invalidPassword);
116+
page.getByLabel("Password", new Page.GetByLabelOptions().setExact(true)).fill(invalidPassword);
107117
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign In")).click();
108-
page.getByLabel("Password", new Page.GetByLabelOptions().setExact(true)).fill(password);
118+
page.getByLabel("Password", new Page.GetByLabelOptions().setExact(true)).fill("s3cr3t");
109119
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign In")).click();
110120
// Then
111-
assertThat(page.getByText(expected)).isVisible();
112-
// Given
113-
password = "s3cr3t";
114-
// When
115-
page.getByLabel("Password", new Page.GetByLabelOptions().setExact(true)).fill(password);
116-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign In")).click();
117-
118-
assertThat(page.getByText(expected)).isVisible();
121+
KeycloakEvent lockEvent = eventsClient.awaitEvent(event -> "LOGIN_ERROR".equals(event.type())
122+
&& "user_temporarily_disabled".equals(event.error()));
123+
assertEquals(attributeValueOfBillDoe, lockEvent.details().get("username"));
119124
}
120125

121126
@Test
122-
void test_should_reset_jane_with_login_name() {
127+
void test_should_publish_reset_event_for_jane_with_login_name() {
123128
// Given
124129
String username = "janedoe";
125-
String expected = "You should receive an email shortly with further instructions.";
126130
// When
127-
page.navigate(KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/testloginattribute/account");
128-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")).click();
129-
page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("Forgot Password?")).click();
130-
page.getByLabel("Username").fill(username);
131-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit")).click();
131+
openForgotPasswordForm();
132+
submitResetForm(username);
132133
// Then
133-
assertThat(page.getByText(expected)).isVisible();
134+
KeycloakEvent resetEvent = eventsClient.awaitEvent(event -> "SEND_RESET_PASSWORD".equals(event.type())
135+
&& event.error() == null);
136+
assertNull(resetEvent.error());
134137
}
135138

136139
@Test
137-
void test_should_reset_john_with_attribute() {
140+
void test_should_publish_reset_event_for_john_with_attribute() {
138141
// Given
139142
String attributeValueOfJohnDoe = "SHOULDBEOKFORLOGIN";
140-
String expected = "You should receive an email shortly with further instructions.";
141143
// When
142-
page.navigate(KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/testloginattribute/account");
143-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")).click();
144-
page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("Forgot Password?")).click();
145-
page.getByLabel("Username").fill(attributeValueOfJohnDoe);
146-
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit")).click();
144+
openForgotPasswordForm();
145+
submitResetForm(attributeValueOfJohnDoe);
147146
// Then
148-
assertThat(page.getByText(expected)).isVisible();
147+
KeycloakEvent resetEvent = eventsClient.awaitEvent(event -> "SEND_RESET_PASSWORD".equals(event.type())
148+
&& event.error() == null);
149+
assertNull(resetEvent.error());
149150
}
150151

151152
@Test
152-
void test_should_not_reset_jane_with_attribute() {
153+
void test_should_publish_reset_error_for_unknown_attribute() {
153154
// Given
154155
String attributeValueOfJaneDoe = "SHOULDBEkoFORLOGIN";
155-
String expected = "You should receive an email shortly with further instructions.";
156156
// When
157+
openForgotPasswordForm();
158+
submitResetForm(attributeValueOfJaneDoe);
159+
// Then
160+
KeycloakEvent resetError = eventsClient.awaitEvent(event -> "SEND_RESET_PASSWORD_ERROR".equals(event.type())
161+
&& "user_not_found".equals(event.error()));
162+
assertEquals(attributeValueOfJaneDoe, resetError.details().get("username"));
163+
}
164+
165+
private void openAccountConsole() {
157166
page.navigate(KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/testloginattribute/account");
158167
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign in")).click();
159-
page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("Forgot Password?")).click();
160-
page.getByLabel("Username").fill(attributeValueOfJaneDoe);
168+
}
169+
170+
private void submitLoginForm(String username, String password) {
171+
page.getByLabel("Username").fill(username);
172+
page.getByLabel("Password", new Page.GetByLabelOptions().setExact(true)).fill(password);
173+
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Sign In")).click();
174+
}
175+
176+
private void openForgotPasswordForm() {
177+
openAccountConsole();
178+
page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("Forgot Password?"))
179+
.click();
180+
}
181+
182+
private void submitResetForm(String username) {
183+
page.getByLabel("Username").fill(username);
161184
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit")).click();
162-
// Then
163-
assertThat(page.getByText(expected)).isVisible();
185+
}
186+
187+
private record KeycloakEvent(String type, String error, Map<String, String> details) {
188+
}
189+
190+
private static class KeycloakEventsClient {
191+
private final String authServerUrl;
192+
private final String realm;
193+
private final String adminUsername;
194+
private final String adminPassword;
195+
196+
KeycloakEventsClient(KeycloakContainer container, String realm) {
197+
this.authServerUrl = container.getAuthServerUrl();
198+
this.realm = realm;
199+
this.adminUsername = container.getAdminUsername();
200+
this.adminPassword = container.getAdminPassword();
201+
}
202+
203+
void clearEvents() {
204+
given()
205+
.auth().oauth2(adminAccessToken())
206+
.delete(authServerUrl + "/admin/realms/" + realm + "/events")
207+
.then()
208+
.statusCode(204);
209+
}
210+
211+
KeycloakEvent awaitEvent(Predicate<KeycloakEvent> predicate) {
212+
long deadline = System.currentTimeMillis() + Duration.ofSeconds(5).toMillis();
213+
while (System.currentTimeMillis() < deadline) {
214+
List<KeycloakEvent> events = events();
215+
for (KeycloakEvent event : events) {
216+
if (predicate.test(event)) {
217+
return event;
218+
}
219+
}
220+
try {
221+
Thread.sleep(200);
222+
} catch (InterruptedException e) {
223+
Thread.currentThread().interrupt();
224+
fail("Interrupted while waiting for Keycloak event");
225+
}
226+
}
227+
fail("No Keycloak event matched predicate before timeout");
228+
return null;
229+
}
230+
231+
private List<KeycloakEvent> events() {
232+
List<Map<String, Object>> rawEvents = given()
233+
.auth().oauth2(adminAccessToken())
234+
.get(authServerUrl + "/admin/realms/" + realm + "/events")
235+
.then()
236+
.statusCode(200)
237+
.extract()
238+
.as(new TypeRef<>() {
239+
});
240+
return rawEvents.stream()
241+
.map(this::toEvent)
242+
.collect(Collectors.toList());
243+
}
244+
245+
private KeycloakEvent toEvent(Map<String, Object> rawEvent) {
246+
String type = Objects.toString(rawEvent.get("type"), null);
247+
String error = Objects.toString(rawEvent.get("error"), null);
248+
Object details = rawEvent.getOrDefault("details", Collections.emptyMap());
249+
Map<String, String> stringDetails = Collections.emptyMap();
250+
if (details instanceof Map<?, ?> mapDetails) {
251+
stringDetails = mapDetails.entrySet().stream()
252+
.collect(Collectors.toMap(entry -> Objects.toString(entry.getKey(), null),
253+
entry -> Objects.toString(entry.getValue(), null)));
254+
}
255+
return new KeycloakEvent(type, error, stringDetails);
256+
}
257+
258+
private String adminAccessToken() {
259+
return given()
260+
.contentType(ContentType.URLENC)
261+
.formParam("grant_type", "password")
262+
.formParam("client_id", "admin-cli")
263+
.formParam("username", adminUsername)
264+
.formParam("password", adminPassword)
265+
.post(authServerUrl + "/realms/master/protocol/openid-connect/token")
266+
.then()
267+
.statusCode(200)
268+
.extract()
269+
.path("access_token");
270+
}
164271
}
165272
}

src/test/resources/testloginattribute-realm.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1126,7 +1126,7 @@
11261126
"strictTransportSecurity" : "max-age=31536000; includeSubDomains"
11271127
},
11281128
"smtpServer" : { },
1129-
"eventsEnabled" : false,
1129+
"eventsEnabled" : true,
11301130
"eventsListeners" : [ "jboss-logging" ],
11311131
"enabledEventTypes" : [ ],
11321132
"adminEventsEnabled" : false,

0 commit comments

Comments
 (0)