Add tests for linked accounts (#27299)

Fixes: #21248

Signed-off-by: Hynek Mlnarik <hmlnarik@redhat.com>
This commit is contained in:
Hynek Mlnařík 2024-02-27 07:15:23 +01:00 committed by GitHub
parent bf89d53134
commit 004805e21d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 252 additions and 24 deletions

View file

@ -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">

View 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();
}

View file

@ -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];

View file

@ -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$/ })

View 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
}

View file

@ -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"
}
}
]
}