Add tests for linked accounts (#27299)
Fixes: #21248 Signed-off-by: Hynek Mlnarik <hmlnarik@redhat.com>
This commit is contained in:
parent
bf89d53134
commit
004805e21d
6 changed files with 252 additions and 24 deletions
|
@ -51,7 +51,10 @@ export const AccountRow = ({ account, isLinked = false }: AccountRowProps) => {
|
||||||
key={account.providerName}
|
key={account.providerName}
|
||||||
aria-label={t("linkedAccounts")}
|
aria-label={t("linkedAccounts")}
|
||||||
>
|
>
|
||||||
<DataListItemRow key={account.providerName}>
|
<DataListItemRow
|
||||||
|
key={account.providerName}
|
||||||
|
data-testid={`linked-accounts/${account.providerName}`}
|
||||||
|
>
|
||||||
<DataListItemCells
|
<DataListItemCells
|
||||||
dataListCells={[
|
dataListCells={[
|
||||||
<DataListCell key="idp">
|
<DataListCell key="idp">
|
||||||
|
|
118
js/apps/account-ui/test/account-security/linked-accounts.spec.ts
Normal file
118
js/apps/account-ui/test/account-security/linked-accounts.spec.ts
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import {
|
||||||
|
createIdentityProvider,
|
||||||
|
deleteIdentityProvider,
|
||||||
|
createClient,
|
||||||
|
deleteClient,
|
||||||
|
inRealm,
|
||||||
|
findClientByClientId,
|
||||||
|
createRandomUserWithPassword,
|
||||||
|
deleteUser,
|
||||||
|
} from "../admin-client";
|
||||||
|
import groupsIdPClient from "../realms/groups-idp.json" assert { type: "json" };
|
||||||
|
import ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
||||||
|
import IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
|
const realm = "groups";
|
||||||
|
|
||||||
|
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-" + randomUUID(), "pwd");
|
||||||
|
|
||||||
|
const kcGroupsIdpId = await findClientByClientId("groups-idp");
|
||||||
|
if (kcGroupsIdpId) {
|
||||||
|
await deleteClient(kcGroupsIdpId);
|
||||||
|
}
|
||||||
|
groupIdPClientId = await createClient(
|
||||||
|
groupsIdPClient as ClientRepresentation,
|
||||||
|
);
|
||||||
|
const kc = process.env.KEYCLOAK_SERVER || "http://localhost:8080";
|
||||||
|
const idp: IdentityProviderRepresentation = {
|
||||||
|
alias: "master-idp",
|
||||||
|
providerId: "oidc",
|
||||||
|
enabled: true,
|
||||||
|
config: {
|
||||||
|
clientId: "groups-idp",
|
||||||
|
clientSecret: "H0JaTc7VBu3HJR26vrzMxgidfJmgI5Dw",
|
||||||
|
validateSignature: "false",
|
||||||
|
tokenUrl: `${kc}/realms/master/protocol/openid-connect/token`,
|
||||||
|
jwksUrl: `${kc}/realms/master/protocol/openid-connect/certs`,
|
||||||
|
issuer: `${kc}/realms/master`,
|
||||||
|
authorizationUrl: `${kc}/realms/master/protocol/openid-connect/auth`,
|
||||||
|
logoutUrl: `${kc}/realms/master/protocol/openid-connect/logout`,
|
||||||
|
userInfoUrl: `${kc}/realms/master/protocol/openid-connect/userinfo`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await inRealm(realm, () => createIdentityProvider(idp));
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await deleteUser(user);
|
||||||
|
});
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await deleteClient(groupIdPClientId);
|
||||||
|
});
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await inRealm(realm, () => deleteIdentityProvider("master-idp"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Linking", async ({ page }) => {
|
||||||
|
// If refactoring this, consider introduction of helper functions for individual pages - login, update profile etc.
|
||||||
|
await page.goto(
|
||||||
|
process.env.CI ? `/realms/${realm}/account` : `/?realm=${realm}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
// Now the account console should be visible
|
||||||
|
await page.getByTestId("accountSecurity").click();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("account-security/linked-accounts"),
|
||||||
|
).toBeVisible();
|
||||||
|
await page.getByTestId("account-security/linked-accounts").click();
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.getByTestId("linked-accounts/master-idp")
|
||||||
|
.getByRole("button", { name: "Unlink account" }),
|
||||||
|
).toBeVisible();
|
||||||
|
await page
|
||||||
|
.getByTestId("linked-accounts/master-idp")
|
||||||
|
.getByRole("button", { name: "Unlink account" })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Expect an error shown that the account cannot be unlinked
|
||||||
|
await expect(page.getByLabel("Danger Alert")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateProfile(page, firstName, lastName, email) {
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: "Update Account Information" }),
|
||||||
|
).toBeVisible();
|
||||||
|
await page.getByLabel("Email", { exact: true }).fill(email);
|
||||||
|
await page.getByLabel("First name", { exact: true }).fill(firstName);
|
||||||
|
await page.getByLabel("Last name", { exact: true }).fill(lastName);
|
||||||
|
await page.getByRole("button", { name: "Submit" }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginWithUsernamePassword(page, username, password) {
|
||||||
|
await page.getByLabel("Username").fill(username);
|
||||||
|
await page.getByLabel("Password", { exact: true }).fill(password);
|
||||||
|
await page.getByRole("button", { name: "Sign In" }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginWithIdp(page, idpAlias: string) {
|
||||||
|
await page.getByRole("link", { name: idpAlias }).click();
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
|
import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
|
||||||
|
import ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
||||||
|
import IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
|
||||||
import RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
import RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
|
||||||
import type { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
|
import type { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
|
||||||
import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
||||||
|
|
||||||
const adminClient = new KeycloakAdminClient({
|
const adminClient = new KeycloakAdminClient({
|
||||||
baseUrl: process.env.KEYCLOAK_SERVER || "http://127.0.0.1:8180",
|
baseUrl: process.env.KEYCLOAK_SERVER || "http://127.0.0.1:8080",
|
||||||
realmName: "master",
|
realmName: "master",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -23,6 +25,16 @@ export async function useTheme() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function inRealm<T>(realm: string, fn: () => Promise<T>) {
|
||||||
|
const prevRealm = adminClient.realmName;
|
||||||
|
adminClient.realmName = realm;
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} finally {
|
||||||
|
adminClient.realmName = prevRealm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function importRealm(realm: RealmRepresentation) {
|
export async function importRealm(realm: RealmRepresentation) {
|
||||||
await adminClient.realms.create(realm);
|
await adminClient.realms.create(realm);
|
||||||
}
|
}
|
||||||
|
@ -31,6 +43,32 @@ export async function deleteRealm(realm: string) {
|
||||||
await adminClient.realms.del({ realm });
|
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,
|
||||||
|
): Promise<string> {
|
||||||
|
return adminClient.identityProviders.create(idp)["id"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteIdentityProvider(alias: string) {
|
||||||
|
await adminClient.identityProviders.del({ alias });
|
||||||
|
}
|
||||||
|
|
||||||
export async function importUserProfile(
|
export async function importUserProfile(
|
||||||
userProfile: UserProfileConfig,
|
userProfile: UserProfileConfig,
|
||||||
realm: string,
|
realm: string,
|
||||||
|
@ -50,20 +88,38 @@ export async function enableLocalization(realm: string) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createUser(user: UserRepresentation, realm: string) {
|
export async function createUser(user: UserRepresentation) {
|
||||||
try {
|
try {
|
||||||
await adminClient.users.create({ ...user, realm });
|
await adminClient.users.create(user);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createRandomUserWithPassword(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
props?: UserRepresentation,
|
||||||
|
): Promise<string> {
|
||||||
|
return createUser({
|
||||||
|
username: username,
|
||||||
|
enabled: true,
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
type: "password",
|
||||||
|
value: password,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
...props,
|
||||||
|
}).then(() => username);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUserByUsername(username: string, realm: string) {
|
export async function getUserByUsername(username: string, realm: string) {
|
||||||
const users = await adminClient.users.find({ username, realm, exact: true });
|
const users = await adminClient.users.find({ username, realm, exact: true });
|
||||||
return users.length > 0 ? users[0] : undefined;
|
return users.length > 0 ? users[0] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteUser(username: string, realm: string) {
|
export async function deleteUser(username: string) {
|
||||||
try {
|
try {
|
||||||
const users = await adminClient.users.find({ username, realm });
|
const users = await adminClient.users.find({ username, realm });
|
||||||
const { id } = users[0];
|
const { id } = users[0];
|
||||||
|
|
|
@ -1,21 +1,26 @@
|
||||||
import type { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
|
import type { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
|
||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
import {
|
import {
|
||||||
createUser,
|
createRandomUserWithPassword,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
enableLocalization,
|
enableLocalization,
|
||||||
importUserProfile,
|
importUserProfile,
|
||||||
|
inRealm,
|
||||||
} from "../admin-client";
|
} from "../admin-client";
|
||||||
import { login } from "../login";
|
import { login } from "../login";
|
||||||
import userProfileConfig from "./user-profile.json" assert { type: "json" };
|
import userProfileConfig from "./user-profile.json" assert { type: "json" };
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
const realm = "user-profile";
|
const realm = "user-profile";
|
||||||
|
|
||||||
test.describe("Personal info page", () => {
|
test.describe("Personal info page", () => {
|
||||||
|
let user: string;
|
||||||
test("sets basic information", async ({ page }) => {
|
test("sets basic information", async ({ page }) => {
|
||||||
await login(page, "admin", "admin", "master");
|
user = await createRandomUserWithPassword("user-" + randomUUID(), "pwd");
|
||||||
|
|
||||||
await page.getByTestId("email").fill("edewit@somewhere.com");
|
await login(page, user, "pwd", "master");
|
||||||
|
|
||||||
|
await page.getByTestId("email").fill(`${user}@somewhere.com`);
|
||||||
await page.getByTestId("firstName").fill("Erik");
|
await page.getByTestId("firstName").fill("Erik");
|
||||||
await page.getByTestId("lastName").fill("de Wit");
|
await page.getByTestId("lastName").fill("de Wit");
|
||||||
await page.getByTestId("save").click();
|
await page.getByTestId("save").click();
|
||||||
|
@ -26,34 +31,26 @@ test.describe("Personal info page", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Personal info with userprofile enabled", async () => {
|
test.describe("Personal info with userprofile enabled", async () => {
|
||||||
|
let user: string;
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
await importUserProfile(userProfileConfig as UserProfileConfig, realm);
|
await importUserProfile(userProfileConfig as UserProfileConfig, realm);
|
||||||
await createUser(
|
user = await inRealm(realm, () =>
|
||||||
{
|
createRandomUserWithPassword("user-" + randomUUID(), "jdoe", {
|
||||||
username: "jdoe",
|
|
||||||
enabled: true,
|
|
||||||
email: "jdoe@keycloak.org",
|
email: "jdoe@keycloak.org",
|
||||||
firstName: "John",
|
firstName: "John",
|
||||||
lastName: "Doe",
|
lastName: "Doe",
|
||||||
credentials: [
|
|
||||||
{
|
|
||||||
type: "password",
|
|
||||||
value: "jdoe",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
realmRoles: [],
|
realmRoles: [],
|
||||||
clientRoles: {
|
clientRoles: {
|
||||||
account: ["manage-account"],
|
account: ["manage-account"],
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
realm,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterAll(async () => await deleteUser("jdoe", realm));
|
test.afterAll(async () => await inRealm(realm, () => deleteUser(user)));
|
||||||
|
|
||||||
test("render user profile fields", async ({ page }) => {
|
test("render user profile fields", async ({ page }) => {
|
||||||
await login(page, "jdoe", "jdoe", realm);
|
await login(page, user, "jdoe", realm);
|
||||||
|
|
||||||
await expect(page.locator("#select")).toBeVisible();
|
await expect(page.locator("#select")).toBeVisible();
|
||||||
await expect(page.getByTestId("help-label-select")).toBeVisible();
|
await expect(page.getByTestId("help-label-select")).toBeVisible();
|
||||||
|
@ -61,7 +58,7 @@ test.describe("Personal info with userprofile enabled", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("save user profile", async ({ page }) => {
|
test("save user profile", async ({ page }) => {
|
||||||
await login(page, "jdoe", "jdoe", realm);
|
await login(page, user, "jdoe", realm);
|
||||||
|
|
||||||
await page.locator("#select").click();
|
await page.locator("#select").click();
|
||||||
await page.getByRole("option", { name: "two" }).click();
|
await page.getByRole("option", { name: "two" }).click();
|
||||||
|
@ -90,7 +87,12 @@ test.describe.skip("Realm localization", async () => {
|
||||||
test.beforeAll(() => enableLocalization("master"));
|
test.beforeAll(() => enableLocalization("master"));
|
||||||
|
|
||||||
test("change locale", async ({ page }) => {
|
test("change locale", async ({ page }) => {
|
||||||
await login(page, "admin", "admin", "master");
|
const user = await createRandomUserWithPassword(
|
||||||
|
"user-" + randomUUID(),
|
||||||
|
"pwd",
|
||||||
|
);
|
||||||
|
|
||||||
|
await login(page, user, "pwd", "master");
|
||||||
await page
|
await page
|
||||||
.locator("div")
|
.locator("div")
|
||||||
.filter({ hasText: /^Deutsch$/ })
|
.filter({ hasText: /^Deutsch$/ })
|
||||||
|
|
19
js/apps/account-ui/test/realms/groups-idp.json
Normal file
19
js/apps/account-ui/test/realms/groups-idp.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"clientId": "groups-idp",
|
||||||
|
"name": "groups-idp",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"rootUrl": "${authAdminUrl}",
|
||||||
|
"enabled": true,
|
||||||
|
"clientAuthenticatorType": "client-secret",
|
||||||
|
"secret": "H0JaTc7VBu3HJR26vrzMxgidfJmgI5Dw",
|
||||||
|
"redirectUris": [ "/realms/groups/*" ],
|
||||||
|
"webOrigins": [ "${authAdminUrl}" ],
|
||||||
|
"notBefore": 0,
|
||||||
|
"bearerOnly": false,
|
||||||
|
"consentRequired": false,
|
||||||
|
"standardFlowEnabled": true,
|
||||||
|
"implicitFlowEnabled": false,
|
||||||
|
"directAccessGrantsEnabled": false,
|
||||||
|
"serviceAccountsEnabled": false,
|
||||||
|
"publicClient": false
|
||||||
|
}
|
|
@ -152,5 +152,35 @@
|
||||||
"manage": true
|
"manage": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"identityProviders": [
|
||||||
|
{
|
||||||
|
"alias": "oidc",
|
||||||
|
"displayName": "",
|
||||||
|
"providerId": "oidc",
|
||||||
|
"enabled": true,
|
||||||
|
"updateProfileFirstLoginMode": "on",
|
||||||
|
"trustEmail": false,
|
||||||
|
"storeToken": false,
|
||||||
|
"addReadTokenRoleOnCreate": false,
|
||||||
|
"authenticateByDefault": false,
|
||||||
|
"linkOnly": false,
|
||||||
|
"firstBrokerLoginFlowAlias": "first broker login",
|
||||||
|
"config": {
|
||||||
|
"clientId": "groups-idp",
|
||||||
|
"clientSecret": "H0JaTc7VBu3HJR26vrzMxgidfJmgI5Dw",
|
||||||
|
"userInfoUrl": "http://localhost:8180/realms/master/protocol/openid-connect/userinfo",
|
||||||
|
"validateSignature": "true",
|
||||||
|
"tokenUrl": "http://localhost:8180/realms/master/protocol/openid-connect/token",
|
||||||
|
"jwksUrl": "http://localhost:8180/realms/master/protocol/openid-connect/certs",
|
||||||
|
"issuer": "http://localhost:8180/realms/master",
|
||||||
|
"useJwksUrl": "true",
|
||||||
|
"pkceEnabled": "false",
|
||||||
|
"metadataDescriptorUrl": "http://localhost:8180/realms/master/.well-known/openid-configuration",
|
||||||
|
"authorizationUrl": "http://localhost:8180/realms/master/protocol/openid-connect/auth",
|
||||||
|
"clientAuthMethod": "client_secret_post",
|
||||||
|
"logoutUrl": "http://localhost:8180/realms/master/protocol/openid-connect/logout"
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue