mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Isolate account console tests on a per-realm basis (#41608)
Closes #41606 Signed-off-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
parent
a99149b83a
commit
a3591f670f
2
.github/workflows/js-ci.yml
vendored
2
.github/workflows/js-ci.yml
vendored
@ -11,7 +11,6 @@ on:
|
||||
|
||||
env:
|
||||
MAVEN_ARGS: "-B -nsu -Daether.connector.http.connectionMaxTtl=25"
|
||||
RETRY_COUNT: 3
|
||||
|
||||
concurrency:
|
||||
# Only cancel jobs for PR updates
|
||||
@ -207,6 +206,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
WORKSPACE: "@keycloak/keycloak-admin-ui"
|
||||
RETRY_COUNT: 3
|
||||
strategy:
|
||||
matrix:
|
||||
browser: [chromium, firefox]
|
||||
|
||||
@ -1,12 +1,5 @@
|
||||
import { type ViewportSize, defineConfig, devices } from "@playwright/test";
|
||||
|
||||
import { getAccountUrl } from "./test/utils";
|
||||
|
||||
const retryCount = parseInt(process.env.RETRY_COUNT || "0");
|
||||
console.log("----------------------------");
|
||||
console.log("Playwright retries = " + retryCount);
|
||||
console.log("----------------------------");
|
||||
|
||||
const viewport: ViewportSize = { width: 1920, height: 1080 };
|
||||
|
||||
/**
|
||||
@ -14,33 +7,21 @@ const viewport: ViewportSize = { width: 1920, height: 1080 };
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./test",
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: retryCount,
|
||||
reporter: process.env.CI ? [["github"], ["html"]] : "list",
|
||||
|
||||
use: {
|
||||
baseURL: getAccountUrl(),
|
||||
trace: "retain-on-failure",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "import realms",
|
||||
testMatch: /realm\.setup\.ts/,
|
||||
teardown: "del realms",
|
||||
},
|
||||
{
|
||||
name: "del realms",
|
||||
testMatch: /realm\.teardown\.ts/,
|
||||
},
|
||||
{
|
||||
name: "chromium",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
viewport,
|
||||
},
|
||||
dependencies: ["import realms"],
|
||||
},
|
||||
{
|
||||
name: "firefox",
|
||||
@ -48,7 +29,6 @@ export default defineConfig({
|
||||
...devices["Desktop Firefox"],
|
||||
viewport,
|
||||
},
|
||||
dependencies: ["import realms"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { login } from "./login.ts";
|
||||
|
||||
test("Check page heading", async ({ page }) => {
|
||||
await login(page, "alice", "alice", "user-profile");
|
||||
await page.getByTestId("accountSecurity").click();
|
||||
|
||||
const linkedAccountsNavItem = page.getByTestId(
|
||||
"account-security/linked-accounts",
|
||||
);
|
||||
|
||||
await expect(linkedAccountsNavItem).toBeVisible();
|
||||
await linkedAccountsNavItem.click();
|
||||
await expect(page.getByTestId("page-heading")).toHaveText("Linked accounts");
|
||||
});
|
||||
@ -1,50 +1,43 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { login } from "../login.ts";
|
||||
import { login } from "../support/actions.ts";
|
||||
import { createTestBed } from "../support/testbed.ts";
|
||||
|
||||
test.describe("Sign out test", () => {
|
||||
test("Sign out one device", async ({ browser }) => {
|
||||
const context1 = await browser.newContext({
|
||||
userAgent:
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)",
|
||||
});
|
||||
test.describe("Device activity", () => {
|
||||
test("signs out of a single device session", async ({ browser }) => {
|
||||
const realm = await createTestBed();
|
||||
const context1 = await browser.newContext();
|
||||
const context2 = await browser.newContext();
|
||||
|
||||
try {
|
||||
const page1 = await context1.newPage();
|
||||
const page2 = await context2.newPage();
|
||||
await login(page1, "jdoe", "jdoe", "groups");
|
||||
|
||||
// Log in the first session, and verify it is active.
|
||||
await login(page1, realm);
|
||||
await page1.getByTestId("accountSecurity").click();
|
||||
await expect(
|
||||
page1.getByTestId("account-security/device-activity"),
|
||||
).toBeVisible();
|
||||
await page1.getByTestId("account-security/device-activity").click();
|
||||
await expect(page1.getByTestId("row-0")).toContainText("Current session");
|
||||
|
||||
await login(page2, "jdoe", "jdoe", "groups");
|
||||
// Log in the second session, and verify it is active.
|
||||
await login(page2, realm);
|
||||
await page2.getByTestId("accountSecurity").click();
|
||||
await expect(
|
||||
page2.getByTestId("account-security/device-activity"),
|
||||
).toBeVisible();
|
||||
await page2.getByTestId("account-security/device-activity").click();
|
||||
await expect(page2.getByTestId("row-0")).toContainText("Current session");
|
||||
const count = await page2
|
||||
.locator('[aria-label="device-sessions-content"]')
|
||||
.count();
|
||||
|
||||
for (let i = 0; i < count - 1; ++i) {
|
||||
await page2
|
||||
.getByRole("button", { name: "Sign out", exact: true })
|
||||
.first()
|
||||
.click();
|
||||
await page2.getByRole("button", { name: "Confirm" }).click();
|
||||
await page2.getByText("Signed out").isVisible();
|
||||
await page2.getByTestId("global-alerts").locator("button").click();
|
||||
}
|
||||
// Sign out the first session from the second session.
|
||||
await page2
|
||||
.getByRole("button", { name: "Sign out", exact: true })
|
||||
.click();
|
||||
await page2.getByRole("button", { name: "Confirm", exact: true }).click();
|
||||
|
||||
// reload pages in browsers, one should stay logged in, the other should be logged out
|
||||
// Reload pages and verify the first session is logged out, while the second session remains active.
|
||||
await page1.reload();
|
||||
await page2.reload();
|
||||
await expect(
|
||||
page1.getByRole("heading", { name: "Sign in to your account" }),
|
||||
page1.getByRole("heading", {
|
||||
name: "Sign in to your account",
|
||||
exact: true,
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(page2.getByTestId("accountSecurity")).toBeVisible();
|
||||
} finally {
|
||||
@ -53,37 +46,38 @@ test.describe("Sign out test", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("Sign out all devices", async ({ browser }) => {
|
||||
const context1 = await browser.newContext({
|
||||
userAgent:
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)",
|
||||
});
|
||||
test("signs out of all device sessions", async ({ browser }) => {
|
||||
const realm = await createTestBed();
|
||||
const context1 = await browser.newContext();
|
||||
const context2 = await browser.newContext();
|
||||
|
||||
try {
|
||||
const page1 = await context1.newPage();
|
||||
const page2 = await context2.newPage();
|
||||
await login(page1, "jdoe", "jdoe", "groups");
|
||||
await login(page2, "jdoe", "jdoe", "groups");
|
||||
|
||||
// Log in both sessions, then sign out of all devices from the second session.
|
||||
await login(page1, realm);
|
||||
await login(page2, realm);
|
||||
await page2.getByTestId("accountSecurity").click();
|
||||
await page2.getByTestId("account-security/device-activity").click();
|
||||
|
||||
await page2
|
||||
.getByRole("button", { name: "Sign out all devices", exact: true })
|
||||
.click();
|
||||
await page2.getByRole("button", { name: "Confirm" }).click();
|
||||
await expect(
|
||||
page2.getByRole("heading", { name: "Sign in to your account" }),
|
||||
).toBeVisible();
|
||||
await page2.getByRole("button", { name: "Confirm", exact: true }).click();
|
||||
|
||||
// reload pages in browsers, one should stay logged in, the other should be logged out
|
||||
// Reload only the first page (second page is already logged out), and verify both sessions are logged out.
|
||||
await page1.reload();
|
||||
// Reload in page2 should not be needed, as it should be logged out after clicking the button
|
||||
await expect(
|
||||
page1.getByRole("heading", { name: "Sign in to your account" }),
|
||||
page1.getByRole("heading", {
|
||||
name: "Sign in to your account",
|
||||
exact: true,
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page2.getByRole("heading", { name: "Sign in to your account" }),
|
||||
page2.getByRole("heading", {
|
||||
name: "Sign in to your account",
|
||||
exact: true,
|
||||
}),
|
||||
).toBeVisible();
|
||||
} finally {
|
||||
await context1.close();
|
||||
|
||||
@ -1,40 +1,56 @@
|
||||
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation.js";
|
||||
import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation.js";
|
||||
import { type Page, expect, test } from "@playwright/test";
|
||||
|
||||
import {
|
||||
createClient,
|
||||
createIdentityProvider,
|
||||
createRandomUserWithPassword,
|
||||
deleteClient,
|
||||
deleteIdentityProvider,
|
||||
deleteUser,
|
||||
findClientByClientId,
|
||||
} from "../admin-client.ts";
|
||||
import { SERVER_URL } from "../constants.ts";
|
||||
import groupsIdPClient from "../realms/groups-idp.json" with { type: "json" };
|
||||
import userProfileRealm from "../realms/user-profile-realm.json" with { type: "json" };
|
||||
import { login } from "../support/actions.ts";
|
||||
import { adminClient } from "../support/admin-client.ts";
|
||||
import { DEFAULT_USER, getAccountUrl, SERVER_URL } from "../support/common.ts";
|
||||
import { createTestBed } from "../support/testbed.ts";
|
||||
|
||||
const realm = "groups";
|
||||
const EXTERNAL_USERNAME = "external-user";
|
||||
const EXTERNAL_PASSWORD = "external-user";
|
||||
const EXTERNAL_EMAIL = "external-user@keycloak.org";
|
||||
|
||||
test.describe("Account linking", () => {
|
||||
let groupIdPClientId: string;
|
||||
let user: string;
|
||||
// Tests for keycloak account console, section Account linking in Account security
|
||||
test.beforeAll(async () => {
|
||||
user = await createRandomUserWithPassword(
|
||||
"user-" + crypto.randomUUID(),
|
||||
"pwd",
|
||||
test.describe("Linked accounts", () => {
|
||||
test("shows linked accounts", async ({ page }) => {
|
||||
const realm = await createTestBed(userProfileRealm);
|
||||
|
||||
// Log in and navigate to the linked accounts section.
|
||||
await login(page, realm);
|
||||
await page.getByTestId("accountSecurity").click();
|
||||
await page.getByTestId("account-security/linked-accounts").click();
|
||||
await expect(page.getByTestId("page-heading")).toHaveText(
|
||||
"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({
|
||||
users: [
|
||||
{
|
||||
...DEFAULT_USER,
|
||||
username: EXTERNAL_USERNAME,
|
||||
email: EXTERNAL_EMAIL,
|
||||
credentials: [
|
||||
{
|
||||
type: "password",
|
||||
value: EXTERNAL_PASSWORD,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await adminClient.clients.create({
|
||||
realm: externalRealm,
|
||||
...groupsIdPClient,
|
||||
});
|
||||
|
||||
// Create a realm that links to the external realm as an identity provider.
|
||||
const realm = await createTestBed();
|
||||
|
||||
await adminClient.identityProviders.create({
|
||||
realm,
|
||||
);
|
||||
|
||||
const kcGroupsIdpId = await findClientByClientId("groups-idp");
|
||||
if (kcGroupsIdpId) {
|
||||
await deleteClient(kcGroupsIdpId);
|
||||
}
|
||||
groupIdPClientId = await createClient(
|
||||
groupsIdPClient as ClientRepresentation,
|
||||
);
|
||||
const idp: IdentityProviderRepresentation = {
|
||||
alias: "master-idp",
|
||||
providerId: "oidc",
|
||||
enabled: true,
|
||||
@ -42,40 +58,22 @@ test.describe("Account linking", () => {
|
||||
clientId: "groups-idp",
|
||||
clientSecret: "H0JaTc7VBu3HJR26vrzMxgidfJmgI5Dw",
|
||||
validateSignature: "false",
|
||||
tokenUrl: `${SERVER_URL}/realms/master/protocol/openid-connect/token`,
|
||||
jwksUrl: `${SERVER_URL}/realms/master/protocol/openid-connect/certs`,
|
||||
issuer: `${SERVER_URL}/realms/master`,
|
||||
authorizationUrl: `${SERVER_URL}/realms/master/protocol/openid-connect/auth`,
|
||||
logoutUrl: `${SERVER_URL}/realms/master/protocol/openid-connect/logout`,
|
||||
userInfoUrl: `${SERVER_URL}/realms/master/protocol/openid-connect/userinfo`,
|
||||
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`,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await createIdentityProvider(idp, realm);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await deleteUser(user, realm);
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await deleteClient(groupIdPClientId);
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await deleteIdentityProvider("master-idp", realm);
|
||||
});
|
||||
|
||||
test("Linking", async ({ page }) => {
|
||||
// If refactoring this, consider introduction of helper functions for individual pages - login, update profile etc.
|
||||
await page.goto(`/realms/${realm}/account`);
|
||||
await page.goto(getAccountUrl(realm).toString());
|
||||
|
||||
// Click the login via master-idp provider button
|
||||
await loginWithIdp(page, "master-idp");
|
||||
|
||||
// Now the login at the master-idp should be visible
|
||||
await loginWithUsernamePassword(page, "admin", "admin");
|
||||
|
||||
// Now the update-profile page should be visible
|
||||
await updateProfile(page, "test", "user", "testuser@keycloak.org");
|
||||
await loginWithUsernamePassword(page, EXTERNAL_USERNAME, EXTERNAL_PASSWORD);
|
||||
|
||||
// Now the account console should be visible
|
||||
await page.getByTestId("accountSecurity").click();
|
||||
@ -100,21 +98,6 @@ test.describe("Account linking", () => {
|
||||
});
|
||||
});
|
||||
|
||||
async function updateProfile(
|
||||
page: Page,
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
email: string,
|
||||
) {
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Update Account Information" }),
|
||||
).toBeVisible();
|
||||
await page.getByLabel("Email").fill(email);
|
||||
await page.getByLabel("First name").fill(firstName);
|
||||
await page.getByLabel("Last name").fill(lastName);
|
||||
await page.getByRole("button", { name: "Submit" }).click();
|
||||
}
|
||||
|
||||
async function loginWithUsernamePassword(
|
||||
page: Page,
|
||||
username: string,
|
||||
|
||||
@ -1,25 +1,19 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import {
|
||||
getUserByUsername,
|
||||
getCredentials,
|
||||
deleteCredential,
|
||||
deleteRealm,
|
||||
importRealm,
|
||||
} from "../admin-client.ts";
|
||||
import { login } from "../login.ts";
|
||||
import groupsRealm from "../realms/groups-realm.json" with { type: "json" };
|
||||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation.js";
|
||||
import { login } from "../support/actions.ts";
|
||||
import { adminClient, findUserByUsername } from "../support/admin-client.ts";
|
||||
import { DEFAULT_USER } from "../support/common.ts";
|
||||
import { createTestBed } from "../support/testbed.ts";
|
||||
|
||||
const realm = "groups";
|
||||
test.describe("Signing in", () => {
|
||||
// Tests for keycloak account console, section Signing in in Account security
|
||||
test("Should see only password", async ({ page }) => {
|
||||
await login(page, "jdoe", "jdoe", "groups");
|
||||
test("shows password and OTP credentials", async ({ page }) => {
|
||||
const realm = await createTestBed();
|
||||
|
||||
// Log in and navigate to the signing in section.
|
||||
await login(page, realm);
|
||||
await page.getByTestId("accountSecurity").click();
|
||||
await expect(page.getByTestId("account-security/signing-in")).toBeVisible();
|
||||
await page.getByTestId("account-security/signing-in").click();
|
||||
|
||||
// Verify the password credential is configured, and it is not possible to create a new one.
|
||||
await expect(
|
||||
page.getByTestId("password/credential-list").getByRole("listitem"),
|
||||
).toHaveCount(1);
|
||||
@ -28,54 +22,48 @@ test.describe("Signing in", () => {
|
||||
).toContainText("My password");
|
||||
await expect(page.getByTestId("password/create")).toBeHidden();
|
||||
|
||||
// Verify the OTP credential not configured, and it is possible to create a new one.
|
||||
await expect(
|
||||
page.getByTestId("otp/credential-list").getByRole("listitem"),
|
||||
).toHaveCount(1);
|
||||
await expect(
|
||||
page.getByTestId("otp/credential-list").getByRole("listitem"),
|
||||
).toContainText("not set up");
|
||||
await expect(page.getByTestId("otp/create")).toBeVisible();
|
||||
|
||||
).toContainText("Authenticator application is not set up.");
|
||||
await page.getByTestId("otp/create").click();
|
||||
await expect(page.locator("#kc-page-title")).toContainText(
|
||||
"Mobile Authenticator Setup",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Signing in 2", () => {
|
||||
test.afterAll(async () => {
|
||||
await deleteRealm(realm);
|
||||
await importRealm(groupsRealm as RealmRepresentation);
|
||||
});
|
||||
test("Password removal", async ({ page }) => {
|
||||
const jdoeUser = await getUserByUsername("jdoe", realm);
|
||||
test("allows setting a password credential if none exists", async ({
|
||||
page,
|
||||
}) => {
|
||||
const realm = await createTestBed();
|
||||
const user = await findUserByUsername(realm, DEFAULT_USER.username);
|
||||
|
||||
await login(page, "jdoe", "jdoe", "groups");
|
||||
|
||||
const credentials = await getCredentials(jdoeUser!.id!, realm);
|
||||
await deleteCredential(jdoeUser!.id!, credentials![0].id!, realm);
|
||||
// Log in and delete the password credential of the user.
|
||||
await login(page, realm);
|
||||
const credentials = await adminClient.users.getCredentials({
|
||||
realm,
|
||||
id: user.id as string,
|
||||
});
|
||||
await adminClient.users.deleteCredential({
|
||||
realm,
|
||||
id: user.id as string,
|
||||
credentialId: credentials[0].id as string,
|
||||
});
|
||||
|
||||
// Navigate to the signing in section.
|
||||
await page.getByTestId("accountSecurity").click();
|
||||
await expect(page.getByTestId("account-security/signing-in")).toBeVisible();
|
||||
await page.getByTestId("account-security/signing-in").click();
|
||||
|
||||
// Verify the password credential is not configured, and it is possible to create a new one.
|
||||
await expect(
|
||||
page.getByTestId("password/credential-list").getByRole("listitem"),
|
||||
).toHaveCount(1);
|
||||
await expect(
|
||||
page.getByTestId("password/credential-list").getByRole("listitem"),
|
||||
).toContainText("not set up");
|
||||
await expect(page.getByTestId("password/create")).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByTestId("otp/credential-list").getByRole("listitem"),
|
||||
).toHaveCount(1);
|
||||
await expect(
|
||||
page.getByTestId("otp/credential-list").getByRole("listitem"),
|
||||
).toContainText("not set up");
|
||||
await expect(page.getByTestId("otp/create")).toBeVisible();
|
||||
|
||||
await page.getByTestId("password/create").click();
|
||||
await expect(page.locator("#kc-page-title")).toContainText(
|
||||
"Update password",
|
||||
|
||||
@ -1,174 +0,0 @@
|
||||
import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
|
||||
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation.js";
|
||||
import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation.js";
|
||||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation.js";
|
||||
import type { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata.js";
|
||||
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation.js";
|
||||
|
||||
import { DEFAULT_REALM, SERVER_URL } from "./constants.ts";
|
||||
|
||||
const adminClient = new KeycloakAdminClient({
|
||||
baseUrl: SERVER_URL,
|
||||
realmName: DEFAULT_REALM,
|
||||
});
|
||||
|
||||
await adminClient.auth({
|
||||
username: "admin",
|
||||
password: "admin",
|
||||
grantType: "password",
|
||||
clientId: "admin-cli",
|
||||
});
|
||||
|
||||
export async function useTheme() {
|
||||
const masterRealm = await adminClient.realms.findOne({
|
||||
realm: DEFAULT_REALM,
|
||||
});
|
||||
|
||||
await adminClient.realms.update(
|
||||
{ realm: DEFAULT_REALM },
|
||||
{ ...masterRealm, accountTheme: "keycloak.v3" },
|
||||
);
|
||||
}
|
||||
|
||||
export async function importRealm(realm: RealmRepresentation) {
|
||||
await adminClient.realms.create(realm);
|
||||
}
|
||||
|
||||
export async function deleteRealm(realm: string) {
|
||||
await adminClient.realms.del({ realm });
|
||||
}
|
||||
|
||||
export async function createClient(
|
||||
client: ClientRepresentation,
|
||||
): Promise<string> {
|
||||
return adminClient.clients.create(client).then((client) => client.id);
|
||||
}
|
||||
|
||||
export async function findClientByClientId(clientId: string) {
|
||||
return adminClient.clients
|
||||
.find({ clientId })
|
||||
.then((clientArray) => clientArray[0]?.["id"]);
|
||||
}
|
||||
|
||||
export async function deleteClient(id: string) {
|
||||
await adminClient.clients.del({ id });
|
||||
}
|
||||
|
||||
export async function createIdentityProvider(
|
||||
idp: IdentityProviderRepresentation,
|
||||
realm = DEFAULT_REALM,
|
||||
): Promise<string> {
|
||||
return (await adminClient.identityProviders.create({ ...idp, realm }))["id"];
|
||||
}
|
||||
|
||||
export async function deleteIdentityProvider(
|
||||
alias: string,
|
||||
realm = DEFAULT_REALM,
|
||||
) {
|
||||
await adminClient.identityProviders.del({ alias, realm });
|
||||
}
|
||||
|
||||
export async function importUserProfile(
|
||||
userProfile: UserProfileConfig,
|
||||
realm: string,
|
||||
) {
|
||||
await adminClient.users.updateProfile({ ...userProfile, realm });
|
||||
}
|
||||
|
||||
export async function enableLocalization(realm = DEFAULT_REALM) {
|
||||
const realmRepresentation = await adminClient.realms.findOne({ realm });
|
||||
await adminClient.realms.update(
|
||||
{ realm },
|
||||
{
|
||||
...realmRepresentation,
|
||||
internationalizationEnabled: true,
|
||||
supportedLocales: ["en", "nl", "de"],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function createUser(
|
||||
user: UserRepresentation,
|
||||
realm = DEFAULT_REALM,
|
||||
) {
|
||||
try {
|
||||
await adminClient.users.create({ ...user, realm });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createRandomUserWithPassword(
|
||||
username: string,
|
||||
password: string,
|
||||
realm: string,
|
||||
props?: UserRepresentation,
|
||||
): Promise<string> {
|
||||
await adminClient.auth({
|
||||
username: "admin",
|
||||
password: "admin",
|
||||
grantType: "password",
|
||||
clientId: "admin-cli",
|
||||
});
|
||||
return createUser(
|
||||
{
|
||||
username: username,
|
||||
enabled: true,
|
||||
credentials: [
|
||||
{
|
||||
type: "password",
|
||||
value: password,
|
||||
},
|
||||
],
|
||||
...props,
|
||||
},
|
||||
realm,
|
||||
).then(() => username);
|
||||
}
|
||||
|
||||
export async function getUserByUsername(username: string, realm: string) {
|
||||
const users = await adminClient.users.find({ username, realm, exact: true });
|
||||
return users.length > 0 ? users[0] : undefined;
|
||||
}
|
||||
|
||||
export async function deleteUser(username: string, realm = DEFAULT_REALM) {
|
||||
try {
|
||||
const users = await adminClient.users.find({ username, realm });
|
||||
if (users.length === 0) {
|
||||
console.warn(`User ${username} not found in realm ${realm}`);
|
||||
return;
|
||||
}
|
||||
const { id } = users[0];
|
||||
await adminClient.users.del({ id: id!, realm });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateUser(user: UserRepresentation, realm: string) {
|
||||
try {
|
||||
await adminClient.users.update({ id: user.id!, realm }, user);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCredentials(id: string, realm: string) {
|
||||
try {
|
||||
return await adminClient.users.getCredentials({ id, realm });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCredential(
|
||||
id: string,
|
||||
credentialId: string,
|
||||
realm: string,
|
||||
) {
|
||||
try {
|
||||
await adminClient.users.deleteCredential({ id, credentialId, realm });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
@ -1,84 +1,21 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { login } from "./login.ts";
|
||||
import { getAccountUrl, getAdminUrl, getRootPath } from "./utils.ts";
|
||||
import { login } from "./support/actions.ts";
|
||||
import { createTestBed } from "./support/testbed.ts";
|
||||
|
||||
test.describe("Applications test", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Sign out all devices before each test
|
||||
await login(page);
|
||||
await page.getByTestId("accountSecurity").click();
|
||||
await page.getByTestId("account-security/device-activity").click();
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Sign out all devices", exact: true })
|
||||
.click();
|
||||
await page.getByRole("button", { name: "Confirm" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Sign in to your account" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Single application", async ({ page }) => {
|
||||
await login(page);
|
||||
test.describe("Applications", () => {
|
||||
test("shows a list of applications the user has access to", async ({
|
||||
page,
|
||||
}) => {
|
||||
const realm = await createTestBed();
|
||||
|
||||
// Log in and navigate to the applications page.
|
||||
await login(page, realm);
|
||||
await page.getByTestId("applications").click();
|
||||
|
||||
// Assert that the applications list is displayed and contains the expected application.
|
||||
await expect(page.getByTestId("applications-list-item")).toHaveCount(1);
|
||||
await expect(page.getByTestId("applications-list-item")).toContainText(
|
||||
"Account Console",
|
||||
);
|
||||
});
|
||||
|
||||
test("Single application twice", async ({ browser }) => {
|
||||
const context1 = await browser.newContext({
|
||||
userAgent:
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)",
|
||||
});
|
||||
const context2 = await browser.newContext();
|
||||
try {
|
||||
const page1 = await context1.newPage();
|
||||
const page2 = await context2.newPage();
|
||||
|
||||
await login(page1);
|
||||
await login(page2);
|
||||
|
||||
await page1.getByTestId("applications").click();
|
||||
|
||||
await expect(page1.getByTestId("applications-list-item")).toHaveCount(1);
|
||||
await expect(
|
||||
page1.getByTestId("applications-list-item").nth(0),
|
||||
).toContainText("Account Console");
|
||||
} finally {
|
||||
await context1.close();
|
||||
await context2.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("Two applications", async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
// go to admin console
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveURL(getAdminUrl());
|
||||
await page.waitForURL(getAdminUrl());
|
||||
await expect(page.getByTestId("options-toggle")).toBeVisible();
|
||||
|
||||
await page.goto(getRootPath());
|
||||
await page.waitForURL(getAccountUrl());
|
||||
|
||||
await page.getByTestId("applications").click();
|
||||
|
||||
await expect(page.getByTestId("applications-list-item")).toHaveCount(2);
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("applications-list-item")
|
||||
.filter({ hasText: "Account Console" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("applications-list-item")
|
||||
.filter({ hasText: "security-admin-console" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
export const SERVER_URL = "http://localhost:8080";
|
||||
export const ROOT_PATH = "/realms/:realm/account";
|
||||
export const DEFAULT_REALM = "master";
|
||||
export const ADMIN_USER = "admin";
|
||||
export const ADMIN_PASSWORD = "admin";
|
||||
@ -1,15 +1,21 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { login } from "./login.ts";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import groupsRealm from "./realms/groups-realm.json" with { type: "json" };
|
||||
import { login } from "./support/actions.ts";
|
||||
import { createTestBed } from "./support/testbed.ts";
|
||||
|
||||
test.describe("Groups page", () => {
|
||||
test("List my groups", async ({ page }) => {
|
||||
await login(page, "jdoe", "jdoe", "groups");
|
||||
test.describe("Groups", () => {
|
||||
test("lists groups", async ({ page }) => {
|
||||
const realm = await createTestBed(groupsRealm);
|
||||
|
||||
await login(page, realm);
|
||||
await page.getByTestId("groups").click();
|
||||
await expect(page.getByTestId("group[1].name")).toHaveText("three");
|
||||
});
|
||||
|
||||
test("List direct and indirect groups", async ({ page }) => {
|
||||
await login(page, "alice", "alice", "groups");
|
||||
test("lists direct and indirect groups", async ({ page }) => {
|
||||
const realm = await createTestBed(groupsRealm);
|
||||
|
||||
await login(page, realm, "alice", "alice");
|
||||
await page.getByTestId("groups").click();
|
||||
|
||||
await expect(
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
import { ADMIN_PASSWORD, ADMIN_USER, DEFAULT_REALM } from "./constants.ts";
|
||||
import { getRootPath } from "./utils.ts";
|
||||
|
||||
export const login = async (
|
||||
page: Page,
|
||||
username = ADMIN_USER,
|
||||
password = ADMIN_PASSWORD,
|
||||
realm = DEFAULT_REALM,
|
||||
queryParams?: Record<string, string>,
|
||||
) => {
|
||||
const rootPath =
|
||||
getRootPath(realm) +
|
||||
(queryParams ? "?" + new URLSearchParams(queryParams) : "");
|
||||
|
||||
await page.goto(rootPath);
|
||||
await page.getByLabel("Username").fill(username);
|
||||
await page.getByLabel("Password", { exact: true }).fill(password);
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
};
|
||||
@ -1,20 +0,0 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { login } from "../login.ts";
|
||||
|
||||
const realm = "verifiable-credentials";
|
||||
|
||||
test.describe("Verifiable Credentials page", () => {
|
||||
test.skip("Get offer for test-credential.", async ({ page }) => {
|
||||
await login(page, "test-user", "test", realm);
|
||||
|
||||
await expect(page.getByTestId("qr-code")).toBeHidden();
|
||||
|
||||
await page.getByTestId("oid4vci").click();
|
||||
await page.getByTestId("menu-toggle").click();
|
||||
|
||||
await expect(page.getByTestId("verifiable-credential")).toBeVisible();
|
||||
await expect(page.getByTestId("natural-person")).toBeVisible();
|
||||
await page.getByTestId("natural-person").click();
|
||||
await expect(page.getByTestId("qr-code")).toBeVisible();
|
||||
});
|
||||
});
|
||||
@ -1,26 +1,17 @@
|
||||
import type { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata.js";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import {
|
||||
createRandomUserWithPassword,
|
||||
deleteUser,
|
||||
enableLocalization,
|
||||
importUserProfile,
|
||||
} from "../admin-client.ts";
|
||||
import { login } from "../login.ts";
|
||||
import userProfileConfig from "./user-profile.json" with { type: "json" };
|
||||
|
||||
const realm = "user-profile";
|
||||
|
||||
test.describe("Personal info page", () => {
|
||||
const user = "user-" + crypto.randomUUID();
|
||||
|
||||
test.beforeAll(() => createRandomUserWithPassword(user, "pwd", realm));
|
||||
test.afterAll(async () => deleteUser(user, realm));
|
||||
import { login } from "../support/actions.ts";
|
||||
import { createTestBed } from "../support/testbed.ts";
|
||||
import userProfile from "./user-profile.json" with { type: "json" };
|
||||
import { adminClient } from "../support/admin-client.ts";
|
||||
import userProfileRealm from "../realms/user-profile-realm.json" with { type: "json" };
|
||||
|
||||
test.describe("Personal info", () => {
|
||||
test("sets basic information", async ({ page }) => {
|
||||
await login(page, user, "pwd", realm);
|
||||
const realm = await createTestBed();
|
||||
|
||||
await page.getByTestId("email").fill(`${user}@somewhere.com`);
|
||||
await login(page, realm);
|
||||
|
||||
await page.getByTestId("email").fill("edewit@somewhere.com");
|
||||
await page.getByTestId("firstName").fill("Erik");
|
||||
await page.getByTestId("lastName").fill("de Wit");
|
||||
await page.getByTestId("save").click();
|
||||
@ -30,30 +21,12 @@ test.describe("Personal info page", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Personal info with userprofile enabled", () => {
|
||||
let user: string;
|
||||
test.beforeAll(async () => {
|
||||
await importUserProfile(userProfileConfig as UserProfileConfig, realm);
|
||||
user = await createRandomUserWithPassword(
|
||||
"user-" + crypto.randomUUID(),
|
||||
"jdoe",
|
||||
realm,
|
||||
{
|
||||
email: "jdoe@keycloak.org",
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
realmRoles: [],
|
||||
clientRoles: {
|
||||
account: ["manage-account"],
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
test.describe("Personal info (user profile enabled)", () => {
|
||||
test("renders user profile fields", async ({ page }) => {
|
||||
const realm = await createTestBed(userProfileRealm);
|
||||
|
||||
test.afterAll(() => deleteUser(user, realm));
|
||||
|
||||
test("render user profile fields", async ({ page }) => {
|
||||
await login(page, user, "jdoe", realm);
|
||||
await adminClient.users.updateProfile({ ...userProfile, realm });
|
||||
await login(page, realm);
|
||||
|
||||
await expect(page.locator("#select")).toBeVisible();
|
||||
await expect(page.getByTestId("help-label-select")).toBeVisible();
|
||||
@ -63,8 +36,11 @@ test.describe("Personal info with userprofile enabled", () => {
|
||||
await expect(page.getByText("Español")).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("render long select options as typeahead", async ({ page }) => {
|
||||
await login(page, user, "jdoe", realm);
|
||||
test("renders long select options as typeahead", async ({ page }) => {
|
||||
const realm = await createTestBed(userProfileRealm);
|
||||
|
||||
await adminClient.users.updateProfile({ ...userProfile, realm });
|
||||
await login(page, realm);
|
||||
|
||||
await page.locator("#alternatelang").click();
|
||||
await page.waitForSelector("text=Italiano");
|
||||
@ -77,8 +53,11 @@ test.describe("Personal info with userprofile enabled", () => {
|
||||
await expect(page.getByText('Create "S"')).toBeHidden();
|
||||
});
|
||||
|
||||
test("render long list of locales as typeahead", async ({ page }) => {
|
||||
await login(page, user, "jdoe", realm);
|
||||
test("renders long list of locales as typeahead", async ({ page }) => {
|
||||
const realm = await createTestBed(userProfileRealm);
|
||||
|
||||
await adminClient.users.updateProfile({ ...userProfile, realm });
|
||||
await login(page, realm);
|
||||
|
||||
await page.locator("#attributes\\.locale").click();
|
||||
await page.waitForSelector("text=Italiano");
|
||||
@ -91,8 +70,11 @@ test.describe("Personal info with userprofile enabled", () => {
|
||||
await expect(page.getByText('Create "S"')).toBeHidden();
|
||||
});
|
||||
|
||||
test("save user profile", async ({ page }) => {
|
||||
await login(page, user, "jdoe", realm);
|
||||
test("saves user profile", async ({ page }) => {
|
||||
const realm = await createTestBed(userProfileRealm);
|
||||
|
||||
await adminClient.users.updateProfile({ ...userProfile, realm });
|
||||
await login(page, realm);
|
||||
|
||||
await page.locator("#select").click();
|
||||
await page.getByRole("option", { name: "two" }).click();
|
||||
@ -117,15 +99,13 @@ test.describe("Personal info with userprofile enabled", () => {
|
||||
});
|
||||
|
||||
test.describe("Realm localization", () => {
|
||||
test.beforeAll(() => enableLocalization());
|
||||
test("change locale", async ({ page }) => {
|
||||
const user = await createRandomUserWithPassword(
|
||||
"user-" + crypto.randomUUID(),
|
||||
"pwd",
|
||||
realm,
|
||||
);
|
||||
test("changes locale", async ({ page }) => {
|
||||
const realm = await createTestBed({
|
||||
internationalizationEnabled: true,
|
||||
supportedLocales: ["en", "nl", "de"],
|
||||
});
|
||||
|
||||
await login(page, user, "pwd", realm);
|
||||
await login(page, realm);
|
||||
await page.locator("#attributes\\.locale").click();
|
||||
page.getByRole("option").filter({ hasText: "Deutsch" });
|
||||
await page.getByRole("option", { name: "English" }).click();
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation.js";
|
||||
import { test as setup } from "@playwright/test";
|
||||
|
||||
import { deleteRealm, importRealm } from "./admin-client.ts";
|
||||
import groupsRealm from "./realms/groups-realm.json" with { type: "json" };
|
||||
import resourcesRealm from "./realms/resources-realm.json" with { type: "json" };
|
||||
import userProfileRealm from "./realms/user-profile-realm.json" with { type: "json" };
|
||||
import verifiableCredentialsRealm from "./realms/verifiable-credentials-realm.json" with { type: "json" };
|
||||
|
||||
setup("import realm", async () => {
|
||||
await Promise.allSettled([
|
||||
deleteRealm(groupsRealm.realm),
|
||||
deleteRealm(resourcesRealm.realm),
|
||||
deleteRealm(userProfileRealm.realm),
|
||||
deleteRealm(verifiableCredentialsRealm.realm),
|
||||
]);
|
||||
await Promise.all([
|
||||
importRealm(groupsRealm),
|
||||
importRealm(resourcesRealm as RealmRepresentation),
|
||||
importRealm(userProfileRealm),
|
||||
importRealm(verifiableCredentialsRealm as RealmRepresentation),
|
||||
]);
|
||||
});
|
||||
@ -1,15 +0,0 @@
|
||||
import { test as setup } from "@playwright/test";
|
||||
import { deleteRealm } from "./admin-client.ts";
|
||||
import groupsRealm from "./realms/groups-realm.json" with { type: "json" };
|
||||
import resourcesRealm from "./realms/resources-realm.json" with { type: "json" };
|
||||
import userProfileRealm from "./realms/user-profile-realm.json" with { type: "json" };
|
||||
import verifiableCredentialsRealm from "./realms/verifiable-credentials-realm.json" with { type: "json" };
|
||||
|
||||
setup("delete realm", async () => {
|
||||
await Promise.all([
|
||||
deleteRealm(groupsRealm.realm),
|
||||
deleteRealm(resourcesRealm.realm),
|
||||
deleteRealm(userProfileRealm.realm),
|
||||
deleteRealm(verifiableCredentialsRealm.realm),
|
||||
]);
|
||||
});
|
||||
@ -6,7 +6,7 @@
|
||||
"enabled": true,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"secret": "H0JaTc7VBu3HJR26vrzMxgidfJmgI5Dw",
|
||||
"redirectUris": [ "/realms/groups/*" ],
|
||||
"redirectUris": [ "/realms/*" ],
|
||||
"webOrigins": [ "${authAdminUrl}" ],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
|
||||
@ -49,7 +49,6 @@
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": "2a81d08d-9720-46c3-b66f-b566bcc7bce7",
|
||||
"name": "five",
|
||||
"path": "/five",
|
||||
"attributes": {},
|
||||
@ -58,7 +57,6 @@
|
||||
"subGroups": []
|
||||
},
|
||||
{
|
||||
"id": "68582f84-e8a3-4e74-b4ab-6a75d85c690d",
|
||||
"name": "four",
|
||||
"path": "/four",
|
||||
"attributes": {},
|
||||
@ -67,7 +65,6 @@
|
||||
"subGroups": []
|
||||
},
|
||||
{
|
||||
"id": "2b96c84d-cf09-4bc7-8292-d84a346bcc77",
|
||||
"name": "one",
|
||||
"path": "/one",
|
||||
"attributes": {},
|
||||
@ -75,7 +72,6 @@
|
||||
"clientRoles": {},
|
||||
"subGroups": [
|
||||
{
|
||||
"id": "7722ed31-0d9a-438a-895d-1cb7c1691c38",
|
||||
"name": "subgroup",
|
||||
"path": "/one/subgroup",
|
||||
"attributes": {},
|
||||
@ -86,7 +82,6 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "fd9ec69b-aad3-426a-9412-a9c6819f72ff",
|
||||
"name": "three",
|
||||
"path": "/three",
|
||||
"attributes": {},
|
||||
@ -95,7 +90,6 @@
|
||||
"subGroups": []
|
||||
},
|
||||
{
|
||||
"id": "c2153a9f-b300-4843-9663-fdfb42a973ad",
|
||||
"name": "two",
|
||||
"path": "/two",
|
||||
"attributes": {},
|
||||
|
||||
@ -143,7 +143,6 @@
|
||||
},
|
||||
"ownerManagedAccess": true,
|
||||
"attributes": {},
|
||||
"_id": "3fba8c0d-c388-4177-8808-18f2b1917ec9",
|
||||
"uris": [
|
||||
"/album/20277be2-548b-49dd-9dbe-95fe1fe80830"
|
||||
],
|
||||
@ -164,7 +163,6 @@
|
||||
},
|
||||
"ownerManagedAccess": true,
|
||||
"attributes": {},
|
||||
"_id": "575da73f-cc0c-482f-ac2a-47c9dd70c390",
|
||||
"uris": [
|
||||
"/album/5fc8c73d-40e0-4682-b555-7b9f56ede273"
|
||||
],
|
||||
@ -181,11 +179,9 @@
|
||||
"policies": [],
|
||||
"scopes": [
|
||||
{
|
||||
"id": "0b4de4d2-b173-415f-9071-20f866e879ab",
|
||||
"name": "album:view"
|
||||
},
|
||||
{
|
||||
"id": "3b7cf7ed-46c7-4133-b15b-66d05b1f2afe",
|
||||
"name": "album:delete"
|
||||
}
|
||||
],
|
||||
|
||||
@ -106,32 +106,27 @@
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"username": "alice",
|
||||
"username": "jdoe",
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"email": "jdoe@keycloak.org",
|
||||
"enabled": true,
|
||||
"email": "alice@keycloak.org",
|
||||
"firstName": "Alice",
|
||||
"lastName": "In Chains",
|
||||
"realmRoles": [],
|
||||
"clientRoles": {
|
||||
"account": ["manage-account"]
|
||||
},
|
||||
"credentials": [
|
||||
{
|
||||
"type": "password",
|
||||
"value": "alice"
|
||||
"value": "jdoe"
|
||||
}
|
||||
],
|
||||
"realmRoles": [
|
||||
"user", "uma_authorization"
|
||||
],
|
||||
"clientRoles": {
|
||||
"account": [
|
||||
"manage-account"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"identityProviders": [
|
||||
{
|
||||
"alias": "keycloak-oidc",
|
||||
"displayName": "",
|
||||
"internalId": "566b8743-6b80-4165-9675-ed10a2e9af9c",
|
||||
"providerId": "keycloak-oidc",
|
||||
"enabled": true,
|
||||
"updateProfileFirstLoginMode": "on",
|
||||
|
||||
@ -1,195 +0,0 @@
|
||||
{
|
||||
"id": "verifiable-credentials",
|
||||
"realm": "verifiable-credentials",
|
||||
"displayName": "Keycloak",
|
||||
"displayNameHtml": "<div class=\"kc-logo-text\"><span>Keycloak</span></div>",
|
||||
"enabled": true,
|
||||
"attributes": {
|
||||
"frontendUrl": "http://localhost:8080/",
|
||||
"issuerDid": "did:web:test.org"
|
||||
},
|
||||
"sslRequired": "none",
|
||||
"roles": {
|
||||
"realm": [
|
||||
{
|
||||
"name": "user",
|
||||
"description": "User privileges",
|
||||
"composite": false,
|
||||
"clientRole": false,
|
||||
"containerId": "dome",
|
||||
"attributes": {}
|
||||
}
|
||||
],
|
||||
"client": {
|
||||
"did:web:test-marketplace.org": [
|
||||
{
|
||||
"name": "LEGAL_REPRESENTATIVE",
|
||||
"clientRole": true
|
||||
},
|
||||
{
|
||||
"name": "EMPLOYEE",
|
||||
"clientRole": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"groups": [
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"username": "test-user",
|
||||
"enabled": true,
|
||||
"email": "test@user.org",
|
||||
"firstName": "Test",
|
||||
"lastName": "Employee",
|
||||
"credentials": [
|
||||
{
|
||||
"type": "password",
|
||||
"value": "test"
|
||||
}
|
||||
],
|
||||
"clientRoles": {
|
||||
"did:web:test-marketplace.org": [
|
||||
"EMPLOYEE"
|
||||
],
|
||||
"account": [
|
||||
"view-profile",
|
||||
"manage-account"
|
||||
]
|
||||
},
|
||||
"groups": [
|
||||
]
|
||||
}
|
||||
],
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "did:web:test-marketplace.org",
|
||||
"enabled": true,
|
||||
"description": "Client to connect the marketplace",
|
||||
"surrogateAuthRequired": false,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"defaultRoles": [],
|
||||
"redirectUris": [],
|
||||
"webOrigins": [],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
"standardFlowEnabled": true,
|
||||
"implicitFlowEnabled": false,
|
||||
"directAccessGrantsEnabled": false,
|
||||
"serviceAccountsEnabled": false,
|
||||
"publicClient": false,
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "oid4vc",
|
||||
"attributes": {
|
||||
"client.secret.creation.time": "1675260539",
|
||||
"vc.natural-person.format": "jwt_vc",
|
||||
"vc.natural-person.scope": "NaturalPersonCredential",
|
||||
"vc.verifiable-credential.format": "jwt_vc",
|
||||
"vc.verifiabel-credential.scope": "VerifiableCredential"
|
||||
},
|
||||
"protocolMappers": [
|
||||
{
|
||||
"name": "target-role-mapper",
|
||||
"protocol": "oid4vc",
|
||||
"protocolMapper": "oid4vc-target-role-mapper",
|
||||
"config": {
|
||||
"claim.name": "roles",
|
||||
"clientId": "did:web:test-marketplace.org"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "target-vc-role-mapper",
|
||||
"protocol": "oid4vc",
|
||||
"protocolMapper": "oid4vc-target-role-mapper",
|
||||
"config": {
|
||||
"claim.name": "roles",
|
||||
"clientId": "did:web:test-marketplace.org"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "email-mapper",
|
||||
"protocol": "oid4vc",
|
||||
"protocolMapper": "oid4vc-user-attribute-mapper",
|
||||
"config": {
|
||||
"claim.name": "email",
|
||||
"userAttribute": "email"
|
||||
}
|
||||
}
|
||||
],
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": true,
|
||||
"nodeReRegistrationTimeout": -1,
|
||||
"defaultClientScopes": [],
|
||||
"optionalClientScopes": []
|
||||
}
|
||||
],
|
||||
"clientScopes": [
|
||||
{
|
||||
"name": "roles",
|
||||
"description": "OpenID Connect scope for add user roles to the access token",
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"include.in.token.scope": "false",
|
||||
"display.on.consent.screen": "true",
|
||||
"consent.screen.text": "${rolesScopeConsentText}"
|
||||
},
|
||||
"protocolMappers": [
|
||||
{
|
||||
"name": "audience resolve",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-audience-resolve-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"introspection.token.claim": "true",
|
||||
"access.token.claim": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "realm roles",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-usermodel-realm-role-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"introspection.token.claim": "true",
|
||||
"multivalued": "true",
|
||||
"user.attribute": "foo",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "realm_access.roles",
|
||||
"jsonType.label": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "client roles",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-usermodel-client-role-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"introspection.token.claim": "true",
|
||||
"multivalued": "true",
|
||||
"user.attribute": "foo",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "resource_access.${client_id}.roles",
|
||||
"jsonType.label": "String"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"defaultDefaultClientScopes": [
|
||||
],
|
||||
"defaultOptionalClientScopes": [
|
||||
],
|
||||
"components": {
|
||||
"org.keycloak.protocol.oid4vc.issuance.credentialbuilder.CredentialBuilder": [
|
||||
{
|
||||
"id": "sd-jwt-credential-builder",
|
||||
"name": "credential-builder-dc+sd-jwt",
|
||||
"providerId": "dc+sd-jwt",
|
||||
"subComponents": {},
|
||||
"config": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -1,43 +1,58 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { login } from "./support/actions.ts";
|
||||
import {
|
||||
ADMIN_CLIENT_ID,
|
||||
ADMIN_PASSWORD,
|
||||
ADMIN_USERNAME,
|
||||
DEFAULT_REALM,
|
||||
getAdminUrl,
|
||||
} from "./support/common.ts";
|
||||
|
||||
import { ADMIN_PASSWORD, ADMIN_USER, DEFAULT_REALM } from "./constants.ts";
|
||||
import { login } from "./login.ts";
|
||||
import { getAdminUrl } from "./utils.ts";
|
||||
|
||||
test.describe("Signing in with referrer link", () => {
|
||||
test.describe("Referrer", () => {
|
||||
test("shows a referrer link when a matching client exists", async ({
|
||||
page,
|
||||
}) => {
|
||||
const referrer = "security-admin-console";
|
||||
const referrerUrl = getAdminUrl();
|
||||
const referrerName = "Security Admin Console";
|
||||
const queryParams = new URLSearchParams([
|
||||
["referrer", ADMIN_CLIENT_ID],
|
||||
["referrer_uri", getAdminUrl(DEFAULT_REALM).toString()],
|
||||
]);
|
||||
|
||||
const queryParams = {
|
||||
referrer,
|
||||
referrer_uri: referrerUrl,
|
||||
};
|
||||
// Log in with a referrer to the admin console, and check if the referrer link is displayed.
|
||||
await login(
|
||||
page,
|
||||
DEFAULT_REALM,
|
||||
ADMIN_USERNAME,
|
||||
ADMIN_PASSWORD,
|
||||
queryParams,
|
||||
);
|
||||
await expect(page.getByTestId("referrer-link")).toContainText(
|
||||
"Security Admin Console",
|
||||
);
|
||||
|
||||
await login(page, ADMIN_USER, ADMIN_PASSWORD, DEFAULT_REALM, queryParams);
|
||||
await expect(page.getByTestId("referrer-link")).toContainText(referrerName);
|
||||
|
||||
// Navigate around to ensure the referrer is still shown.
|
||||
// Navigate around and check if the referrer link is still displayed.
|
||||
await page.getByTestId("accountSecurity").click();
|
||||
await expect(page.getByTestId("account-security/signing-in")).toBeVisible();
|
||||
await expect(page.getByTestId("referrer-link")).toContainText(referrerName);
|
||||
await page.getByTestId("account-security/signing-in").click();
|
||||
await expect(page.getByTestId("referrer-link")).toContainText(
|
||||
"Security Admin Console",
|
||||
);
|
||||
});
|
||||
|
||||
test("shows no referrer link when an invalid URL is passed", async ({
|
||||
page,
|
||||
}) => {
|
||||
const referrer = "security-admin-console";
|
||||
const referrerUrl = "http://i-am-not-an-allowed-url.com";
|
||||
const queryParams = new URLSearchParams([
|
||||
["referrer", ADMIN_CLIENT_ID],
|
||||
["referrer_uri", "http://i-am-not-an-allowed-url.com"],
|
||||
]);
|
||||
|
||||
const queryParams = {
|
||||
referrer,
|
||||
referrer_uri: referrerUrl,
|
||||
};
|
||||
|
||||
await login(page, ADMIN_USER, ADMIN_PASSWORD, DEFAULT_REALM, queryParams);
|
||||
// Log in with an invalid referrer URL, and check if the referrer link is not displayed.
|
||||
await login(
|
||||
page,
|
||||
DEFAULT_REALM,
|
||||
ADMIN_USERNAME,
|
||||
ADMIN_PASSWORD,
|
||||
queryParams,
|
||||
);
|
||||
await expect(page.getByText("Manage your basic information")).toBeVisible();
|
||||
await expect(page.getByTestId("referrer-link")).toBeHidden();
|
||||
});
|
||||
|
||||
@ -1,18 +1,27 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { login } from "./login.ts";
|
||||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation.js";
|
||||
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";
|
||||
|
||||
test.describe("My resources page", () => {
|
||||
test.describe("Resources", () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test("List my resources", async ({ page }) => {
|
||||
await login(page, "jdoe", "jdoe", "photoz");
|
||||
let realm: string;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
realm = await createTestBed(resourcesRealm as RealmRepresentation);
|
||||
});
|
||||
|
||||
test("shows the resources owned by the user", async ({ page }) => {
|
||||
await login(page, realm);
|
||||
await page.getByTestId("resources").click();
|
||||
|
||||
await expect(page.getByRole("gridcell", { name: "one" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Nothing is shared with alice", async ({ page }) => {
|
||||
await login(page, "alice", "alice", "photoz");
|
||||
test("shows no resources are shared with another user", async ({ page }) => {
|
||||
await login(page, realm, "alice", "alice");
|
||||
await page.getByTestId("resources").click();
|
||||
|
||||
await page.getByTestId("sharedWithMe").click();
|
||||
@ -20,8 +29,8 @@ test.describe("My resources page", () => {
|
||||
expect(tableData).toBe(0);
|
||||
});
|
||||
|
||||
test("Share one with alice", async ({ page }) => {
|
||||
await login(page, "jdoe", "jdoe", "photoz");
|
||||
test("shares a recourse with another user", async ({ page }) => {
|
||||
await login(page, realm);
|
||||
await page.getByTestId("resources").click();
|
||||
|
||||
await page.getByTestId("expand-one").click();
|
||||
@ -52,8 +61,8 @@ test.describe("My resources page", () => {
|
||||
await expect(page.getByTestId("shared-with-alice")).toBeVisible();
|
||||
});
|
||||
|
||||
test("One is shared with alice", async ({ page }) => {
|
||||
await login(page, "alice", "alice", "photoz");
|
||||
test("shows the resources shared with another user", async ({ page }) => {
|
||||
await login(page, realm, "alice", "alice");
|
||||
await page.getByTestId("resources").click();
|
||||
|
||||
await page.getByTestId("sharedWithMe").click();
|
||||
27
js/apps/account-ui/test/support/actions.ts
Normal file
27
js/apps/account-ui/test/support/actions.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { DEFAULT_PASSWORD, DEFAULT_USERNAME, getAccountUrl } from "./common.ts";
|
||||
|
||||
export async function login(
|
||||
page: Page,
|
||||
realm: string,
|
||||
username = DEFAULT_USERNAME,
|
||||
password = DEFAULT_PASSWORD,
|
||||
queryParams?: URLSearchParams,
|
||||
): Promise<void> {
|
||||
const url = getAccountUrl(realm);
|
||||
|
||||
if (queryParams) {
|
||||
for (const [key, value] of queryParams) {
|
||||
url.searchParams.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
await page.goto(url.toString());
|
||||
await page
|
||||
.getByRole("textbox", { name: "Username or email", exact: true })
|
||||
.fill(username);
|
||||
await page
|
||||
.getByRole("textbox", { name: "Password", exact: true })
|
||||
.fill(password);
|
||||
await page.getByRole("button", { name: "Sign In", exact: true }).click();
|
||||
}
|
||||
28
js/apps/account-ui/test/support/admin-client.ts
Normal file
28
js/apps/account-ui/test/support/admin-client.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import AdminClient from "@keycloak/keycloak-admin-client";
|
||||
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation.js";
|
||||
import { ADMIN_PASSWORD, ADMIN_USERNAME, SERVER_URL } from "./common.ts";
|
||||
|
||||
export const adminClient = new AdminClient({
|
||||
baseUrl: SERVER_URL.toString(),
|
||||
});
|
||||
|
||||
await adminClient.auth({
|
||||
username: ADMIN_USERNAME,
|
||||
password: ADMIN_PASSWORD,
|
||||
grantType: "password",
|
||||
clientId: "admin-cli",
|
||||
});
|
||||
|
||||
export async function findUserByUsername(
|
||||
realm: string,
|
||||
username: string,
|
||||
): Promise<UserRepresentation> {
|
||||
const users = await adminClient.users.find({
|
||||
realm,
|
||||
username,
|
||||
exact: true,
|
||||
max: 1,
|
||||
});
|
||||
|
||||
return users[0];
|
||||
}
|
||||
37
js/apps/account-ui/test/support/common.ts
Normal file
37
js/apps/account-ui/test/support/common.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation.js";
|
||||
import { generatePath } from "react-router-dom";
|
||||
|
||||
export const SERVER_URL = new URL("http://localhost:8080");
|
||||
export const ACCOUNT_ROOT_PATH = "/realms/:realm/account" as const;
|
||||
export const ADMIN_ROOT_PATH = "/admin/:realm/console" as const;
|
||||
export const DEFAULT_REALM = "master";
|
||||
export const ADMIN_CLIENT_ID = "security-admin-console";
|
||||
export const ADMIN_USERNAME = "admin";
|
||||
export const ADMIN_PASSWORD = "admin";
|
||||
export const DEFAULT_USERNAME = "jdoe";
|
||||
export const DEFAULT_PASSWORD = "jdoe";
|
||||
|
||||
export const DEFAULT_USER = {
|
||||
username: DEFAULT_USERNAME,
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
email: "jdoe@keycloak.org",
|
||||
enabled: true,
|
||||
credentials: [
|
||||
{
|
||||
type: "password",
|
||||
value: DEFAULT_PASSWORD,
|
||||
},
|
||||
],
|
||||
clientRoles: {
|
||||
account: ["manage-account"],
|
||||
},
|
||||
} satisfies UserRepresentation;
|
||||
|
||||
export function getAccountUrl(realm: string): URL {
|
||||
return new URL(generatePath(ACCOUNT_ROOT_PATH, { realm }), SERVER_URL);
|
||||
}
|
||||
|
||||
export function getAdminUrl(realm: string): URL {
|
||||
return new URL(generatePath(ADMIN_ROOT_PATH, { realm }), SERVER_URL);
|
||||
}
|
||||
16
js/apps/account-ui/test/support/testbed.ts
Normal file
16
js/apps/account-ui/test/support/testbed.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation.js";
|
||||
import { adminClient } from "./admin-client.ts";
|
||||
import { DEFAULT_USER } from "./common.ts";
|
||||
|
||||
export async function createTestBed(
|
||||
overrides?: RealmRepresentation,
|
||||
): Promise<string> {
|
||||
const { realmName } = await adminClient.realms.create({
|
||||
enabled: true,
|
||||
users: [DEFAULT_USER],
|
||||
...overrides,
|
||||
realm: crypto.randomUUID(),
|
||||
});
|
||||
|
||||
return realmName;
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
import { generatePath } from "react-router-dom";
|
||||
|
||||
import { DEFAULT_REALM, ROOT_PATH, SERVER_URL } from "./constants.ts";
|
||||
|
||||
export function getAccountUrl() {
|
||||
return SERVER_URL + getRootPath();
|
||||
}
|
||||
|
||||
export function getAdminUrl() {
|
||||
return SERVER_URL + "/admin/master/console/";
|
||||
}
|
||||
|
||||
export const getRootPath = (realm = DEFAULT_REALM) =>
|
||||
generatePath(ROOT_PATH, { realm });
|
||||
Loading…
x
Reference in New Issue
Block a user