Automatically dispose of realms created by createTestBed() (#43299)

Closes #43298

Signed-off-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Jon Koops 2025-10-10 16:22:21 +02:00 committed by GitHub
parent 934ac48a54
commit 5cbba8f984
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 152 additions and 101 deletions

View File

@ -5,7 +5,7 @@ inputs:
node-version:
description: Node.js version
required: false
default: "22"
default: "24"
runs:
using: composite

View File

@ -4,7 +4,7 @@ import { createTestBed } from "../support/testbed.ts";
test.describe("Device activity", () => {
test("signs out of a single device session", async ({ browser }) => {
const realm = await createTestBed();
await using testBed = await createTestBed();
const context1 = await browser.newContext();
const context2 = await browser.newContext();
@ -13,13 +13,13 @@ test.describe("Device activity", () => {
const page2 = await context2.newPage();
// Log in the first session, and verify it is active.
await login(page1, realm);
await login(page1, testBed.realm);
await page1.getByTestId("accountSecurity").click();
await page1.getByTestId("account-security/device-activity").click();
await expect(page1.getByTestId("row-0")).toContainText("Current session");
// Log in the second session, and verify it is active.
await login(page2, realm);
await login(page2, testBed.realm);
await page2.getByTestId("accountSecurity").click();
await page2.getByTestId("account-security/device-activity").click();
await expect(page2.getByTestId("row-0")).toContainText("Current session");
@ -48,7 +48,7 @@ test.describe("Device activity", () => {
});
test("signs out of all device sessions", async ({ browser }) => {
const realm = await createTestBed();
await using testBed = await createTestBed();
const context1 = await browser.newContext();
const context2 = await browser.newContext();
@ -57,8 +57,8 @@ test.describe("Device activity", () => {
const page2 = await context2.newPage();
// Log in both sessions, then sign out of all devices from the second session.
await login(page1, realm);
await login(page2, realm);
await login(page1, testBed.realm);
await login(page2, testBed.realm);
await page2.getByTestId("accountSecurity").click();
await page2.getByTestId("account-security/device-activity").click();
await page2

View File

@ -12,10 +12,10 @@ const EXTERNAL_EMAIL = "external-user@keycloak.org";
test.describe("Linked accounts", () => {
test("shows linked accounts", async ({ page }) => {
const realm = await createTestBed(userProfileRealm);
await using testBed = await createTestBed(userProfileRealm);
// Log in and navigate to the linked accounts section.
await login(page, realm);
await login(page, testBed.realm);
await page.getByTestId("accountSecurity").click();
await page.getByTestId("account-security/linked-accounts").click();
await expect(page.getByTestId("page-heading")).toHaveText(
@ -25,7 +25,7 @@ test.describe("Linked accounts", () => {
test("cannot remove the last federated identity", async ({ page }) => {
// Create an 'external' realm with a user that will be used for linking.
const externalRealm = await createTestBed({
await using externalTestBed = await createTestBed({
users: [
{
...DEFAULT_USER,
@ -42,15 +42,15 @@ test.describe("Linked accounts", () => {
});
await adminClient.clients.create({
realm: externalRealm,
realm: externalTestBed.realm,
...groupsIdPClient,
});
// Create a realm that links to the external realm as an identity provider.
const realm = await createTestBed();
await using testBed = await createTestBed();
await adminClient.identityProviders.create({
realm,
realm: testBed.realm,
alias: "master-idp",
providerId: "oidc",
enabled: true,
@ -58,16 +58,16 @@ test.describe("Linked accounts", () => {
clientId: "groups-idp",
clientSecret: "H0JaTc7VBu3HJR26vrzMxgidfJmgI5Dw",
validateSignature: "false",
tokenUrl: `${SERVER_URL}realms/${externalRealm}/protocol/openid-connect/token`,
jwksUrl: `${SERVER_URL}realms/${externalRealm}/protocol/openid-connect/certs`,
issuer: `${SERVER_URL}realms/${externalRealm}`,
authorizationUrl: `${SERVER_URL}realms/${externalRealm}/protocol/openid-connect/auth`,
logoutUrl: `${SERVER_URL}realms/${externalRealm}/protocol/openid-connect/logout`,
userInfoUrl: `${SERVER_URL}realms/${externalRealm}/protocol/openid-connect/userinfo`,
tokenUrl: `${SERVER_URL}realms/${externalTestBed.realm}/protocol/openid-connect/token`,
jwksUrl: `${SERVER_URL}realms/${externalTestBed.realm}/protocol/openid-connect/certs`,
issuer: `${SERVER_URL}realms/${externalTestBed.realm}`,
authorizationUrl: `${SERVER_URL}realms/${externalTestBed.realm}/protocol/openid-connect/auth`,
logoutUrl: `${SERVER_URL}realms/${externalTestBed.realm}/protocol/openid-connect/logout`,
userInfoUrl: `${SERVER_URL}realms/${externalTestBed.realm}/protocol/openid-connect/userinfo`,
},
});
await page.goto(getAccountUrl(realm).toString());
await page.goto(getAccountUrl(testBed.realm).toString());
// Click the login via master-idp provider button
await loginWithIdp(page, "master-idp");

View File

@ -6,10 +6,10 @@ import { createTestBed } from "../support/testbed.ts";
test.describe("Signing in", () => {
test("shows password and OTP credentials", async ({ page }) => {
const realm = await createTestBed();
await using testBed = await createTestBed();
// Log in and navigate to the signing in section.
await login(page, realm);
await login(page, testBed.realm);
await page.getByTestId("accountSecurity").click();
await page.getByTestId("account-security/signing-in").click();
@ -38,17 +38,17 @@ test.describe("Signing in", () => {
test("allows setting a password credential if none exists", async ({
page,
}) => {
const realm = await createTestBed();
const user = await findUserByUsername(realm, DEFAULT_USER.username);
await using testBed = await createTestBed();
const user = await findUserByUsername(testBed.realm, DEFAULT_USER.username);
// Log in and delete the password credential of the user.
await login(page, realm);
await login(page, testBed.realm);
const credentials = await adminClient.users.getCredentials({
realm,
realm: testBed.realm,
id: user.id as string,
});
await adminClient.users.deleteCredential({
realm,
realm: testBed.realm,
id: user.id as string,
credentialId: credentials[0].id as string,
});

View File

@ -6,10 +6,10 @@ test.describe("Applications", () => {
test("shows a list of applications the user has access to", async ({
page,
}) => {
const realm = await createTestBed();
await using testBed = await createTestBed();
// Log in and navigate to the applications page.
await login(page, realm);
await login(page, testBed.realm);
await page.getByTestId("applications").click();
// Assert that the applications list is displayed and contains the expected application.

View File

@ -5,17 +5,17 @@ import { createTestBed } from "./support/testbed.ts";
test.describe("Groups", () => {
test("lists groups", async ({ page }) => {
const realm = await createTestBed(groupsRealm);
await using testBed = await createTestBed(groupsRealm);
await login(page, realm);
await login(page, testBed.realm);
await page.getByTestId("groups").click();
await expect(page.getByTestId("group[1].name")).toHaveText("three");
});
test("lists direct and indirect groups", async ({ page }) => {
const realm = await createTestBed(groupsRealm);
await using testBed = await createTestBed(groupsRealm);
await login(page, realm, "alice", "alice");
await login(page, testBed.realm, "alice", "alice");
await page.getByTestId("groups").click();
await expect(

View File

@ -7,9 +7,9 @@ import userProfileRealm from "../realms/user-profile-realm.json" with { type: "j
test.describe("Personal info", () => {
test("sets basic information", async ({ page }) => {
const realm = await createTestBed();
await using testBed = await createTestBed();
await login(page, realm);
await login(page, testBed.realm);
await page.getByTestId("email").fill("edewit@somewhere.com");
await page.getByTestId("firstName").fill("Erik");
@ -22,10 +22,13 @@ test.describe("Personal info", () => {
test.describe("Personal info (user profile enabled)", () => {
test("renders user profile fields", async ({ page }) => {
const realm = await createTestBed(userProfileRealm);
await using testBed = await createTestBed(userProfileRealm);
await adminClient.users.updateProfile({ ...userProfile, realm });
await login(page, realm);
await adminClient.users.updateProfile({
...userProfile,
realm: testBed.realm,
});
await login(page, testBed.realm);
await expect(page.locator("#select")).toBeVisible();
await expect(page.getByTestId("help-label-select")).toBeVisible();
@ -36,10 +39,13 @@ test.describe("Personal info (user profile enabled)", () => {
});
test("renders long select options as typeahead", async ({ page }) => {
const realm = await createTestBed(userProfileRealm);
await using testBed = await createTestBed(userProfileRealm);
await adminClient.users.updateProfile({ ...userProfile, realm });
await login(page, realm);
await adminClient.users.updateProfile({
...userProfile,
realm: testBed.realm,
});
await login(page, testBed.realm);
await page.locator("#alternatelang").click();
await page.waitForSelector("text=Italiano");
@ -53,10 +59,13 @@ test.describe("Personal info (user profile enabled)", () => {
});
test("renders long list of locales as typeahead", async ({ page }) => {
const realm = await createTestBed(userProfileRealm);
await using testBed = await createTestBed(userProfileRealm);
await adminClient.users.updateProfile({ ...userProfile, realm });
await login(page, realm);
await adminClient.users.updateProfile({
...userProfile,
realm: testBed.realm,
});
await login(page, testBed.realm);
await page.locator("#attributes\\.locale").click();
await page.waitForSelector("text=Italiano");
@ -70,10 +79,13 @@ test.describe("Personal info (user profile enabled)", () => {
});
test("saves user profile", async ({ page }) => {
const realm = await createTestBed(userProfileRealm);
await using testBed = await createTestBed(userProfileRealm);
await adminClient.users.updateProfile({ ...userProfile, realm });
await login(page, realm);
await adminClient.users.updateProfile({
...userProfile,
realm: testBed.realm,
});
await login(page, testBed.realm);
await page.locator("#select").click();
await page.getByRole("option", { name: "two" }).click();
@ -101,12 +113,12 @@ test.describe("Personal info (user profile enabled)", () => {
test.describe("Realm localization", () => {
test("changes locale", async ({ page }) => {
const realm = await createTestBed({
await using testBed = await createTestBed({
internationalizationEnabled: true,
supportedLocales: ["en", "nl", "de"],
});
await login(page, realm);
await login(page, testBed.realm);
await page.locator("#attributes\\.locale").click();
page.getByRole("option").filter({ hasText: "Deutsch" });
await page.getByRole("option", { name: "English" }).click();

View File

@ -2,26 +2,33 @@ import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/r
import { expect, test } from "@playwright/test";
import resourcesRealm from "./realms/resources-realm.json" with { type: "json" };
import { login } from "./support/actions.ts";
import { createTestBed } from "./support/testbed.ts";
import { createTestBed, type TestBed } from "./support/testbed.ts";
test.describe("Resources", () => {
// The test cases in this suite depend on state created in previous tests.
// Therefore, we run them in serial mode.
// TODO: Refactor tests to be independent and run in parallel.
test.describe.configure({ mode: "serial" });
let realm: string;
let testBed: TestBed;
test.beforeAll(async () => {
realm = await createTestBed(resourcesRealm as RealmRepresentation);
testBed = await createTestBed(resourcesRealm as RealmRepresentation);
});
test.afterAll(async () => {
await testBed[Symbol.asyncDispose]();
});
test("shows the resources owned by the user", async ({ page }) => {
await login(page, realm);
await login(page, testBed.realm);
await page.getByTestId("resources").click();
await expect(page.getByRole("gridcell", { name: "one" })).toBeVisible();
});
test("shows no resources are shared with another user", async ({ page }) => {
await login(page, realm, "alice", "alice");
await login(page, testBed.realm, "alice", "alice");
await page.getByTestId("resources").click();
await page.getByTestId("sharedWithMe").click();
@ -30,7 +37,7 @@ test.describe("Resources", () => {
});
test("shares a recourse with another user", async ({ page }) => {
await login(page, realm);
await login(page, testBed.realm);
await page.getByTestId("resources").click();
await page.getByTestId("expand-one").click();
@ -62,7 +69,7 @@ test.describe("Resources", () => {
});
test("shows the resources shared with another user", async ({ page }) => {
await login(page, realm, "alice", "alice");
await login(page, testBed.realm, "alice", "alice");
await page.getByTestId("resources").click();
await page.getByTestId("sharedWithMe").click();

View File

@ -2,15 +2,24 @@ import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/r
import { adminClient } from "./admin-client.ts";
import { DEFAULT_USER } from "./common.ts";
export interface TestBed extends AsyncDisposable {
realm: string;
}
export async function createTestBed(
overrides?: RealmRepresentation,
): Promise<string> {
const { realmName } = await adminClient.realms.create({
): Promise<TestBed> {
const { realmName: realm } = await adminClient.realms.create({
enabled: true,
users: [DEFAULT_USER],
...overrides,
realm: crypto.randomUUID(),
});
return realmName;
const deleteRealm = () => adminClient.realms.del({ realm });
return {
realm,
[Symbol.asyncDispose]: deleteRealm,
};
}

View File

@ -6,7 +6,7 @@ This project is the next generation of the Keycloak Administration UI. It is wri
### Prerequisites
Make sure that you have Node.js version 18 (or later) installed on your system. If you do not have Node.js installed we recommend using [Node Version Manager](https://github.com/nvm-sh/nvm) to install it.
Make sure that you have Node.js version 24 (or later) [installed on your system](https://nodejs.org/en/download).
You can find out which version of Node.js you are using by running the following command:

View File

@ -9,8 +9,8 @@ import { login } from "../utils/login.js";
test("OID4VCI section visibility and jump link in Tokens tab", async ({
page,
}) => {
const realm = await createTestBed();
await login(page, { to: toRealmSettings({ realm }) });
await using testBed = await createTestBed();
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
const tokensTab = page.getByTestId("rs-tokens-tab");
await tokensTab.click();
@ -28,8 +28,8 @@ test("OID4VCI section visibility and jump link in Tokens tab", async ({
test("should render fields and save values with correct attribute keys", async ({
page,
}) => {
const realm = await createTestBed();
await login(page, { to: toRealmSettings({ realm }) });
await using testBed = await createTestBed();
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
const tokensTab = page.getByTestId("rs-tokens-tab");
await tokensTab.click();
@ -54,7 +54,7 @@ test("should render fields and save values with correct attribute keys", async (
page.getByText("Realm successfully updated").first(),
).toBeVisible();
const realmData = await adminClient.getRealm(realm);
const realmData = await adminClient.getRealm(testBed.realm);
expect(realmData).toBeDefined();
// TimeSelector converts values based on selected unit (60 minutes = 3600 seconds, 120 seconds = 120 seconds)
expect(realmData?.attributes?.["vc.c-nonce-lifetime-seconds"]).toBe("3600");
@ -62,8 +62,8 @@ test("should render fields and save values with correct attribute keys", async (
});
test("should persist values after page refresh", async ({ page }) => {
const realm = await createTestBed();
await login(page, { to: toRealmSettings({ realm }) });
await using testBed = await createTestBed();
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
const tokensTab = page.getByTestId("rs-tokens-tab");
await tokensTab.click();
@ -89,12 +89,15 @@ test("should persist values after page refresh", async ({ page }) => {
await page.reload();
// Navigate back to realm settings using the same pattern as login
const url = new URL(generatePath(ROOT_PATH, { realm }), SERVER_URL);
url.hash = toRealmSettings({ realm }).pathname!;
const url = new URL(
generatePath(ROOT_PATH, { realm: testBed.realm }),
SERVER_URL,
);
url.hash = toRealmSettings({ realm: testBed.realm }).pathname!;
await page.goto(url.toString());
// The TimeSelector component converts values based on units, so we need to check the actual saved values
const realmData = await adminClient.getRealm(realm);
const realmData = await adminClient.getRealm(testBed.realm);
expect(realmData?.attributes?.["vc.c-nonce-lifetime-seconds"]).toBeDefined();
expect(realmData?.attributes?.["preAuthorizedCodeLifespanS"]).toBeDefined();
@ -111,8 +114,8 @@ test("should persist values after page refresh", async ({ page }) => {
});
test("should validate form fields and save valid values", async ({ page }) => {
const realm = await createTestBed();
await login(page, { to: toRealmSettings({ realm }) });
await using testBed = await createTestBed();
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
const tokensTab = page.getByTestId("rs-tokens-tab");
await tokensTab.click();
@ -150,7 +153,7 @@ test("should validate form fields and save valid values", async ({ page }) => {
).toBeVisible();
// Verify the values were saved correctly
const realmData = await adminClient.getRealm(realm);
const realmData = await adminClient.getRealm(testBed.realm);
expect(realmData?.attributes?.["vc.c-nonce-lifetime-seconds"]).toBeDefined();
expect(realmData?.attributes?.["preAuthorizedCodeLifespanS"]).toBeDefined();
@ -169,8 +172,8 @@ test("should validate form fields and save valid values", async ({ page }) => {
test("should show validation error for values below minimum threshold", async ({
page,
}) => {
const realm = await createTestBed();
await login(page, { to: toRealmSettings({ realm }) });
await using testBed = await createTestBed();
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
const tokensTab = page.getByTestId("rs-tokens-tab");
await tokensTab.click();

View File

@ -1,12 +1,22 @@
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation.js";
import adminClient from "../utils/AdminClient.ts";
export interface TestBed extends AsyncDisposable {
realm: string;
}
export async function createTestBed(
overrides?: RealmRepresentation,
): Promise<string> {
const { realmName } = await adminClient.createRealm(
): Promise<TestBed> {
const { realmName: realm } = await adminClient.createRealm(
crypto.randomUUID(),
overrides,
);
return realmName;
const deleteRealm = () => adminClient.deleteRealm(realm);
return {
realm,
[Symbol.asyncDispose]: deleteRealm,
};
}

View File

@ -34,9 +34,9 @@ import {
test.describe("User creation", () => {
test("navigates to the create user page", async ({ page }) => {
const realm = await createTestBed();
await using testBed = await createTestBed();
await login(page, { to: toUsers({ realm }) });
await login(page, { to: toUsers({ realm: testBed.realm }) });
await clickAddUserButton(page);
await expect(page).toHaveURL(/.*users\/add-user/);
@ -46,9 +46,9 @@ test.describe("User creation", () => {
});
test("creates a new user", async ({ page }) => {
const realm = await createTestBed();
await using testBed = await createTestBed();
await login(page, { to: toAddUser({ realm }) });
await login(page, { to: toAddUser({ realm: testBed.realm }) });
await fillUserForm(page, {
username: "test-user",
@ -72,11 +72,11 @@ test.describe("User creation", () => {
});
test("creates a user that joins a group", async ({ page }) => {
const realm = await createTestBed({
await using testBed = await createTestBed({
groups: [{ name: "test-group" }],
});
await login(page, { to: toAddUser({ realm }) });
await login(page, { to: toAddUser({ realm: testBed.realm }) });
await fillUserForm(page, { username: "test-user" });
await joinGroup(page, ["test-group"]);
@ -85,9 +85,9 @@ test.describe("User creation", () => {
});
test("creates a user with a password credential", async ({ page }) => {
const realm = await createTestBed();
await using testBed = await createTestBed();
await login(page, { to: toAddUser({ realm }) });
await login(page, { to: toAddUser({ realm: testBed.realm }) });
await fillUserForm(page, {
username: "test-user",
@ -116,28 +116,33 @@ test.describe("Existing users", () => {
};
test("searches for an existing user", async ({ page }) => {
const realm = await createTestBed(overrides);
await using testBed = await createTestBed(overrides);
await login(page, { to: toUsers({ realm }) });
await login(page, { to: toUsers({ realm: testBed.realm }) });
await searchItem(page, placeHolder, existingUserName);
await assertRowExists(page, existingUserName);
});
test("searches for a non-existing user", async ({ page }) => {
const realm = await createTestBed(overrides);
await using testBed = await createTestBed(overrides);
await login(page, { to: toUsers({ realm }) });
await login(page, { to: toUsers({ realm: testBed.realm }) });
await searchItem(page, "Search", "non-existing-user");
await assertNoResults(page);
});
test("edits a user", async ({ page }) => {
const realm = await createTestBed(overrides);
const user = await adminClient.findUserByUsername(realm, existingUserName);
await using testBed = await createTestBed(overrides);
const user = await adminClient.findUserByUsername(
testBed.realm,
existingUserName,
);
await login(page, { to: toUser({ realm, id: user.id!, tab: "settings" }) });
await login(page, {
to: toUser({ realm: testBed.realm, id: user.id!, tab: "settings" }),
});
await fillUserForm(page, {
email: "test-user@example.com",
@ -151,9 +156,9 @@ test.describe("Existing users", () => {
const attributesName = "unmanagedAttributes";
test("adds unmanaged attributes to a user", async ({ page }) => {
const realm = await createTestBed(overrides);
await using testBed = await createTestBed(overrides);
await login(page, { to: toRealmSettings({ realm }) });
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
await selectItem(page, "#unmanagedAttributePolicy", "Enabled");
await page.getByTestId("realmSettingsGeneralTab-save").click();
@ -185,9 +190,9 @@ test.describe("Existing users", () => {
test("adds unmanaged attributes with multiple values to a user", async ({
page,
}) => {
const realm = await createTestBed(overrides);
await using testBed = await createTestBed(overrides);
await login(page, { to: toRealmSettings({ realm }) });
await login(page, { to: toRealmSettings({ realm: testBed.realm }) });
await selectItem(page, "#unmanagedAttributePolicy", "Enabled");
await page.getByTestId("realmSettingsGeneralTab-save").click();
@ -204,13 +209,18 @@ test.describe("Existing users", () => {
});
test("adds a user to a group", async ({ page }) => {
const realm = await createTestBed({
await using testBed = await createTestBed({
...overrides,
groups: [{ name: "test-group" }],
});
const user = await adminClient.findUserByUsername(realm, existingUserName);
const user = await adminClient.findUserByUsername(
testBed.realm,
existingUserName,
);
await login(page, { to: toUser({ realm, id: user.id!, tab: "groups" }) });
await login(page, {
to: toUser({ realm: testBed.realm, id: user.id!, tab: "groups" }),
});
await joinGroup(page, ["test-group"], true);
await assertNotificationMessage(page, "Added group membership");

View File

@ -24,7 +24,7 @@
</modules>
<properties>
<node.version>v22.18.0</node.version>
<node.version>v24.9.0</node.version>
<pnpm.version>10.14.0</pnpm.version>
<!-- The JavaScript projects use non-standard folders for their sources, therefore, name it here explicitly -->
<maven.build.cache.input.1>src</maven.build.cache.input.1>