added delete and cypress test

This commit is contained in:
Erik Jan de Wit 2021-03-05 14:47:59 +01:00
parent 5a8762b3fa
commit 949a0e198b
10 changed files with 176 additions and 58 deletions

View file

@ -6,6 +6,7 @@ import CreateClientPage from "../support/pages/admin_console/manage/clients/Crea
import ModalUtils from "../support/util/ModalUtils"; import ModalUtils from "../support/util/ModalUtils";
import AdvancedTab from "../support/pages/admin_console/manage/clients/AdvancedTab"; import AdvancedTab from "../support/pages/admin_console/manage/clients/AdvancedTab";
import AdminClient from "../support/util/AdminClient"; import AdminClient from "../support/util/AdminClient";
import InitialAccessTokenTab from "../support/pages/admin_console/manage/clients/InitialAccessTokenTab";
let itemId = "client_crud"; let itemId = "client_crud";
const loginPage = new LoginPage(); const loginPage = new LoginPage();
@ -78,6 +79,27 @@ describe("Clients test", function () {
listingPage.itemExist(itemId, false); listingPage.itemExist(itemId, false);
}); });
it("Initial access token", () => {
const initialAccessTokenTab = new InitialAccessTokenTab();
listingPage.goToInitialAccessTokenTab();
initialAccessTokenTab.shouldBeEmpty();
initialAccessTokenTab.createNewToken(1, 1).save();
modalUtils.checkModalTitle("Initial access token details").closeModal();
initialAccessTokenTab.shouldNotBeEmpty();
initialAccessTokenTab.getFistId((id) => {
listingPage.deleteItem(id);
modalUtils
.checkModalTitle("Delete initial access token?")
.confirmModal();
masthead.checkNotificationMessage(
"initial access token created successfully"
);
});
});
}); });
describe("Advanced tab test", () => { describe("Advanced tab test", () => {

View file

@ -7,6 +7,7 @@ export default class ListingPage {
searchBtn: string; searchBtn: string;
createBtn: string; createBtn: string;
importBtn: string; importBtn: string;
initialAccessTokenTab = "initialAccessToken";
constructor() { constructor() {
this.searchInput = '.pf-c-toolbar__item [type="search"]'; this.searchInput = '.pf-c-toolbar__item [type="search"]';
@ -34,15 +35,20 @@ export default class ListingPage {
return this; return this;
} }
goToInitialAccessTokenTab() {
cy.getId(this.initialAccessTokenTab).click();
return this;
}
searchItem(searchValue: string, wait = true) { searchItem(searchValue: string, wait = true) {
if (wait) { if (wait) {
const searchUrl = `/admin/realms/master/*${searchValue}*`; const searchUrl = `/admin/realms/master/*${searchValue}*`;
cy.intercept(searchUrl).as("searchClients"); cy.intercept(searchUrl).as("search");
} }
cy.get(this.searchInput).type(searchValue); cy.get(this.searchInput).type(searchValue);
cy.get(this.searchBtn).click(); cy.get(this.searchBtn).click();
if (wait) { if (wait) {
cy.wait(["@searchClients"]); cy.wait(["@search"]);
} }
return this; return this;
} }

View file

@ -0,0 +1,38 @@
export default class InitialAccessTokenTab {
private emptyAction = "empty-primary-action";
private expirationInput = "expiration";
private countInput = "count";
private saveBtn = "save";
shouldBeEmpty() {
cy.getId(this.emptyAction).should("exist");
return this;
}
shouldNotBeEmpty() {
cy.getId(this.emptyAction).should("not.exist");
return this;
}
getFistId(callback: (id: string) => void) {
cy.get('tbody > tr > [data-label="ID"]')
.invoke("text")
.then((text) => {
callback(text);
});
return this;
}
createNewToken(expiration: number, count: number) {
cy.getId(this.emptyAction).click();
cy.getId(this.expirationInput).type(`${expiration}`);
cy.getId(this.countInput).type(`${count}`);
return this;
}
save() {
cy.getId(this.saveBtn).click();
return this;
}
}

View file

@ -24,7 +24,7 @@
"@patternfly/react-table": "4.23.0", "@patternfly/react-table": "4.23.0",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"i18next": "^19.6.2", "i18next": "^19.6.2",
"keycloak-admin": "1.14.9", "keycloak-admin": "1.14.10",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"moment": "^2.29.1", "moment": "^2.29.1",
"react": "^16.8.5", "react": "^16.8.5",

View file

@ -81,7 +81,11 @@ export const CreateInitialAccessToken = () => {
defaultValue="" defaultValue=""
control={control} control={control}
render={({ onChange, value }) => ( render={({ onChange, value }) => (
<TimeSelector value={value} onChange={onChange} /> <TimeSelector
data-testid="expiration"
value={value}
onChange={onChange}
/>
)} )}
/> />
</FormGroup> </FormGroup>
@ -102,6 +106,7 @@ export const CreateInitialAccessToken = () => {
control={control} control={control}
render={({ onChange, value }) => ( render={({ onChange, value }) => (
<NumberInput <NumberInput
data-testid="count"
inputName="count" inputName="count"
inputAriaLabel={t("count")} inputAriaLabel={t("count")}
min={1} min={1}
@ -116,10 +121,11 @@ export const CreateInitialAccessToken = () => {
/> />
</FormGroup> </FormGroup>
<ActionGroup> <ActionGroup>
<Button variant="primary" type="submit"> <Button variant="primary" type="submit" data-testid="save">
{t("common:save")} {t("common:save")}
</Button> </Button>
<Button <Button
data-testid="cancel"
variant="link" variant="link"
onClick={() => onClick={() =>
history.push(`/${realm}/clients/initialAccessToken`) history.push(`/${realm}/clients/initialAccessToken`)

View file

@ -1,27 +1,56 @@
import React from "react"; import React, { useState } from "react";
import { useHistory, useRouteMatch } from "react-router-dom"; import { useHistory, useRouteMatch } from "react-router-dom";
import moment from "moment"; import moment from "moment";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button } from "@patternfly/react-core"; import { AlertVariant, Button, ButtonVariant } from "@patternfly/react-core";
import ClientInitialAccessPresentation from "keycloak-admin/lib/defs/clientInitialAccessPresentation";
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable"; import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
import { useAdminClient } from "../../context/auth/AdminClient"; import { useAdminClient } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext"; import { useRealm } from "../../context/realm-context/RealmContext";
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState"; import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { useAlerts } from "../../components/alert/Alerts";
export const InitialAccessTokenList = () => { export const InitialAccessTokenList = () => {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const { realm } = useRealm(); const { realm } = useRealm();
const history = useHistory(); const history = useHistory();
const { url } = useRouteMatch(); const { url } = useRouteMatch();
const [token, setToken] = useState<ClientInitialAccessPresentation>();
const loader = async () => const loader = async () =>
await adminClient.realms.getClientsInitialAccess({ realm }); await adminClient.realms.getClientsInitialAccess({ realm });
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "clients:tokenDeleteConfirmTitle",
messageKey: t("tokenDeleteConfirm", token),
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await adminClient.realms.delClientsInitialAccess({
realm,
id: token!.id!,
});
addAlert(t("tokenDeleteSuccess"), AlertVariant.success);
setToken(undefined);
} catch (error) {
addAlert(t("tokenDeleteError", { error }), AlertVariant.danger);
}
},
});
return ( return (
<>
<DeleteConfirm />
<KeycloakDataTable <KeycloakDataTable
key={token?.id}
ariaLabelKey="clients:initialAccessToken" ariaLabelKey="clients:initialAccessToken"
searchPlaceholderKey="clients:searchInitialAccessToken" searchPlaceholderKey="clients:searchInitialAccessToken"
loader={loader} loader={loader}
@ -32,6 +61,15 @@ export const InitialAccessTokenList = () => {
</Button> </Button>
</> </>
} }
actions={[
{
title: t("common:delete"),
onRowClick: (token) => {
setToken(token);
toggleDeleteDialog();
},
},
]}
columns={[ columns={[
{ {
name: "id", name: "id",
@ -66,5 +104,6 @@ export const InitialAccessTokenList = () => {
/> />
} }
/> />
</>
); );
}; };

View file

@ -75,6 +75,10 @@
"clientDeleteConfirm": "If you delete this client, all associated data will be removed.", "clientDeleteConfirm": "If you delete this client, all associated data will be removed.",
"searchInitialAccessToken": "Search token", "searchInitialAccessToken": "Search token",
"createToken": "Create initial access token", "createToken": "Create initial access token",
"tokenDeleteConfirm": "Are you sure you want to permanently delete the initial access token {{id}}",
"tokenDeleteConfirmTitle": "Delete initial access token?",
"tokenDeleteSuccess": "initial access token created successfully",
"tokenDeleteError": "Could not delete initial access token: '{{error}}'",
"id": "ID", "id": "ID",
"timestamp": "Created date", "timestamp": "Created date",
"expires": "Expires", "expires": "Expires",

View file

@ -5,13 +5,14 @@ import {
Split, Split,
SplitItem, SplitItem,
TextInput, TextInput,
TextInputProps,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export type Unit = "seconds" | "minutes" | "hours" | "days"; export type Unit = "seconds" | "minutes" | "hours" | "days";
export type TimeSelectorProps = { export type TimeSelectorProps = TextInputProps & {
value: number; value: number;
units?: Unit[]; units?: Unit[];
onChange: (time: number | string) => void; onChange: (time: number | string) => void;
@ -21,6 +22,7 @@ export const TimeSelector = ({
value, value,
units = ["seconds", "minutes", "hours", "days"], units = ["seconds", "minutes", "hours", "days"],
onChange, onChange,
...rest
}: TimeSelectorProps) => { }: TimeSelectorProps) => {
const { t } = useTranslation("common"); const { t } = useTranslation("common");
@ -73,6 +75,7 @@ export const TimeSelector = ({
<Split hasGutter> <Split hasGutter>
<SplitItem> <SplitItem>
<TextInput <TextInput
{...rest}
type="number" type="number"
id={`kc-time-${new Date().getTime()}`} id={`kc-time-${new Date().getTime()}`}
min="0" min="0"

View file

@ -26,7 +26,7 @@ export const UsersInRoleTab = () => {
name: role.name!, name: role.name!,
first: first!, first: first!,
max: max!, max: max!,
} as any); });
return usersWithRole; return usersWithRole;
}; };

View file

@ -13477,10 +13477,10 @@ junk@^3.1.0:
resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1" resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ== integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==
keycloak-admin@1.14.9: keycloak-admin@1.14.10:
version "1.14.9" version "1.14.10"
resolved "https://registry.yarnpkg.com/keycloak-admin/-/keycloak-admin-1.14.9.tgz#f068e8580714c3b92987901cfe6c83bfba669a2a" resolved "https://registry.yarnpkg.com/keycloak-admin/-/keycloak-admin-1.14.10.tgz#e44903826896262b3655303db46795b84a5f9b08"
integrity sha512-VaS6unTYLWuEkqb+FAY+c0Oo+ta505OxhOntCHLcgysMwRmL5pz6taVK8lUZ1ir/6QVto62adVQjmN5fMkpSWg== integrity sha512-WhEA+FkcPikN/Oqh7L0puVkPU1cm3bB+15VOoPdESZknQ9poS0Ohz3Rg1flRfmMdqoMgcy+prigUPtHy6gOAUg==
dependencies: dependencies:
axios "^0.21.0" axios "^0.21.0"
camelize "^1.0.0" camelize "^1.0.0"