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}
|
||||
aria-label={t("linkedAccounts")}
|
||||
>
|
||||
<DataListItemRow key={account.providerName}>
|
||||
<DataListItemRow
|
||||
key={account.providerName}
|
||||
data-testid={`linked-accounts/${account.providerName}`}
|
||||
>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<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 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 type { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
|
||||
import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
||||
|
||||
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",
|
||||
});
|
||||
|
||||
|
@ -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) {
|
||||
await adminClient.realms.create(realm);
|
||||
}
|
||||
|
@ -31,6 +43,32 @@ 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,
|
||||
): Promise<string> {
|
||||
return adminClient.identityProviders.create(idp)["id"];
|
||||
}
|
||||
|
||||
export async function deleteIdentityProvider(alias: string) {
|
||||
await adminClient.identityProviders.del({ alias });
|
||||
}
|
||||
|
||||
export async function importUserProfile(
|
||||
userProfile: UserProfileConfig,
|
||||
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 {
|
||||
await adminClient.users.create({ ...user, realm });
|
||||
await adminClient.users.create(user);
|
||||
} catch (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) {
|
||||
const users = await adminClient.users.find({ username, realm, exact: true });
|
||||
return users.length > 0 ? users[0] : undefined;
|
||||
}
|
||||
|
||||
export async function deleteUser(username: string, realm: string) {
|
||||
export async function deleteUser(username: string) {
|
||||
try {
|
||||
const users = await adminClient.users.find({ username, realm });
|
||||
const { id } = users[0];
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
import type { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import {
|
||||
createUser,
|
||||
createRandomUserWithPassword,
|
||||
deleteUser,
|
||||
enableLocalization,
|
||||
importUserProfile,
|
||||
inRealm,
|
||||
} from "../admin-client";
|
||||
import { login } from "../login";
|
||||
import userProfileConfig from "./user-profile.json" assert { type: "json" };
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
const realm = "user-profile";
|
||||
|
||||
test.describe("Personal info page", () => {
|
||||
let user: string;
|
||||
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("lastName").fill("de Wit");
|
||||
await page.getByTestId("save").click();
|
||||
|
@ -26,34 +31,26 @@ test.describe("Personal info page", () => {
|
|||
});
|
||||
|
||||
test.describe("Personal info with userprofile enabled", async () => {
|
||||
let user: string;
|
||||
test.beforeAll(async () => {
|
||||
await importUserProfile(userProfileConfig as UserProfileConfig, realm);
|
||||
await createUser(
|
||||
{
|
||||
username: "jdoe",
|
||||
enabled: true,
|
||||
user = await inRealm(realm, () =>
|
||||
createRandomUserWithPassword("user-" + randomUUID(), "jdoe", {
|
||||
email: "jdoe@keycloak.org",
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
credentials: [
|
||||
{
|
||||
type: "password",
|
||||
value: "jdoe",
|
||||
},
|
||||
],
|
||||
realmRoles: [],
|
||||
clientRoles: {
|
||||
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 }) => {
|
||||
await login(page, "jdoe", "jdoe", realm);
|
||||
await login(page, user, "jdoe", realm);
|
||||
|
||||
await expect(page.locator("#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 }) => {
|
||||
await login(page, "jdoe", "jdoe", realm);
|
||||
await login(page, user, "jdoe", realm);
|
||||
|
||||
await page.locator("#select").click();
|
||||
await page.getByRole("option", { name: "two" }).click();
|
||||
|
@ -90,7 +87,12 @@ test.describe.skip("Realm localization", async () => {
|
|||
test.beforeAll(() => enableLocalization("master"));
|
||||
|
||||
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
|
||||
.locator("div")
|
||||
.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
|
||||
}
|
||||
}
|
||||
],
|
||||
"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