Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions playwright/tests/root_password.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,13 @@ test.describe('The user section', () => {
// See https://playwright.dev/docs/api/class-locator

// initial expectation - the root password is not configured yet
await expect(page.getByText("None authentication method defined for root user")).toBeVisible();
await expect(page.getByText("No root authentication method defined")).toBeVisible();

// click the "Users" header
await page.locator("a[href='#/users']").click();

// display the actions menu for the root password
await page.locator("#actions-for-root-password").click();

// click the "Set" item
await page.getByRole("menuitem", { name: "Set" }).click();
// click on the "Set a password" button
await page.getByRole("button", { name: "Set a password" }).click();

// fill a new password
await page.locator('#password').fill('agama');
Expand Down
8 changes: 4 additions & 4 deletions playwright/tests/take_screenshots.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ test.describe("The Installer", () => {
// check for multiple texts in parallel, avoid waiting for timeouts
let action = await Promise.any([
page.getByText("Product selection").waitFor().then(() => actions.setProduct),
page.getByText("None authentication method").waitFor().then(() => actions.setPassword),
page.getByText("No root authentication method").waitFor().then(() => actions.setPassword),
page.getByText("Root authentication set").waitFor().then(() => actions.done),
]);

Expand All @@ -52,7 +52,7 @@ test.describe("The Installer", () => {

// update the action for the next step
action = await Promise.any([
page.getByText("None authentication method").waitFor().then(() => actions.setPassword),
page.getByText("No root authentication method").waitFor().then(() => actions.setPassword),
page.getByText("Root authentication set").waitFor().then(() => actions.done),
]);
}
Expand All @@ -63,8 +63,8 @@ test.describe("The Installer", () => {
await page.locator("a[href='#/users']").click();
// Create users page screenshot
await page.screenshot({ path: "screenshots/users-page.png" });
await page.locator("#actions-for-root-password").click();
await page.getByRole("menuitem", { name: "Set" }).click();
// click on the "Set a password" button
await page.getByRole("button", { name: "Set a password" }).click();
await page.locator("#password").fill("linux");
await page.locator("#passwordConfirmation").fill("linux");
await page.locator('button[type="submit"]').click();
Expand Down
6 changes: 6 additions & 0 deletions web/package/cockpit-agama.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-------------------------------------------------------------------
Mon Jun 12 11:13:41 UTC 2023 - David Diaz <dgonzalez@suse.com>

- UI: Makes more evident when there is none authentication method
defined for the root user (gh#openSUSE/agama#615).

-------------------------------------------------------------------
Fri Jun 9 09:38:05 UTC 2023 - David Diaz <dgonzalez@suse.com>

Expand Down
2 changes: 1 addition & 1 deletion web/src/components/overview/UsersSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export default function UsersSection({ showErrors }) {
return (
<div>
<If condition={both} then={<>Root authentication set for using both, password and public SSH Key</>} />
<If condition={none} then={<>None authentication method defined for root user</>} />
<If condition={none} then={<>No root authentication method defined</>} />
<If condition={onlyPassword} then={<>Root authentication set for using password</>} />
<If condition={onlySSHKey} then={<>Root authentication set for using public SSH Key</>} />
</div>
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/overview/UsersSection.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,6 @@ describe("when none root auth method is set", () => {
it("renders information about it", async () => {
installerRender(<UsersSection />);

await screen.findByText(/none authentication method defined for root/i);
await screen.findByText("No root authentication method defined");
});
});
39 changes: 31 additions & 8 deletions web/src/components/users/RootAuthMethods.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,26 @@
*/

import React, { useState, useEffect } from "react";
import { Skeleton, Truncate } from "@patternfly/react-core";
import { Button, Skeleton, Truncate } from "@patternfly/react-core";
import { TableComposable, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
import { Em, RowActions } from '~/components/core';
import { RootPasswordPopup, RootSSHKeyPopup } from '~/components/users';

import { useCancellablePromise } from "~/utils";
import { useInstallerClient } from "~/context/installer";

const MethodsNotDefined = ({ setPassword, setSSHKey }) => {
return (
<div className="stack">
<div className="bold">No root authentication method defined yet</div>
<div>Please, define at least one authentication method for logging into the system as root.</div>
<div className="split">
<Button variant="primary" onClick={setPassword}>Set a password</Button>
<Button variant="secondary" onClick={setSSHKey}>Upload a SSH Public Key</Button>
</div>
</div>
);
};
export default function RootAuthMethods() {
const { users: client } = useInstallerClient();
const { cancellablePromise } = useCancellablePromise();
Expand Down Expand Up @@ -65,10 +77,15 @@ export default function RootAuthMethods() {

const isSSHKeyDefined = sshKey !== "";

const openPasswordForm = () => setIsPasswordFormOpen(true);
const openSSHKeyForm = () => setIsSSHKeyFormOpen(true);
const closePasswordForm = () => setIsPasswordFormOpen(false);
const closeSSHKeyForm = () => setIsSSHKeyFormOpen(false);

const passwordActions = [
{
title: isPasswordDefined ? "Change" : "Set",
onClick: () => setIsPasswordFormOpen(true),
onClick: openPasswordForm

},
isPasswordDefined && {
Expand All @@ -81,7 +98,7 @@ export default function RootAuthMethods() {
const sshKeyActions = [
{
title: isSSHKeyDefined ? "Change" : "Set",
onClick: () => setIsSSHKeyFormOpen(true),
onClick: openSSHKeyForm
},
sshKey && {
title: "Discard",
Expand All @@ -100,9 +117,6 @@ export default function RootAuthMethods() {
);
}

const closePasswordForm = () => setIsPasswordFormOpen(false);
const closeSSHKeyForm = () => setIsSSHKeyFormOpen(false);

const PasswordLabel = () => {
return isPasswordDefined
? "Already set"
Expand All @@ -121,8 +135,12 @@ export default function RootAuthMethods() {
);
};

return (
<>
const Content = () => {
if (!isPasswordDefined && !isSSHKeyDefined) {
return <MethodsNotDefined setPassword={openPasswordForm} setSSHKey={openSSHKeyForm} />;
}

return (
<TableComposable variant="compact" gridBreakPoint="grid-md">
<Thead>
<Tr>
Expand All @@ -148,7 +166,12 @@ export default function RootAuthMethods() {
</Tr>
</Tbody>
</TableComposable>
);
};

return (
<>
<Content />
{ isPasswordFormOpen &&
<RootPasswordPopup
isOpen
Expand Down
57 changes: 52 additions & 5 deletions web/src/components/users/RootAuthMethods.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,50 @@ describe("when loading initial data", () => {
});

describe("when ready", () => {
it("renders a table holding available methods", async () => {
installerRender(<RootAuthMethods />);
describe("and no method is defined", () => {
it("renders a text inviting the user to define at least one", async () => {
installerRender(<RootAuthMethods />);

await screen.findByText("No root authentication method defined yet");
screen.getByText(/at least one/);
});

it("renders buttons for setting either, a password or a SSH Public Key", async () => {
installerRender(<RootAuthMethods />);

await screen.findByRole("button", { name: "Set a password" });
screen.getByRole("button", { name: "Upload a SSH Public Key" });
});

it("allows setting the password", async () => {
const { user } = installerRender(<RootAuthMethods />);

const button = await screen.findByRole("button", { name: "Set a password" });
await user.click(button);

screen.getByRole("dialog", { name: "Set a root password" });
});

it("allows setting the SSH Public Key", async () => {
const { user } = installerRender(<RootAuthMethods />);

const table = await screen.findByRole("grid");
within(table).getByText("Password");
within(table).getByText("SSH Key");
const button = await screen.findByRole("button", { name: "Upload a SSH Public Key" });
await user.click(button);

screen.getByRole("dialog", { name: "Add a SSH Public Key for root" });
});
});

describe("and at least one method is already defined", () => {
beforeEach(() => isRootPasswordSetFn.mockResolvedValue(true));

it("renders a table with available methods", async () => {
installerRender(<RootAuthMethods />);

const table = await screen.findByRole("grid");
within(table).getByText("Password");
within(table).getByText("SSH Key");
});
});

describe("and the password has been set", () => {
Expand Down Expand Up @@ -132,6 +170,9 @@ describe("when ready", () => {
});

describe("but the password is not set yet", () => {
// Mock another auth method for reaching the table
beforeEach(() => getRootSSHKeyFn.mockResolvedValue("Fake"));

it("renders the 'Not set' status", async () => {
installerRender(<RootAuthMethods />);

Expand Down Expand Up @@ -226,6 +267,9 @@ describe("when ready", () => {
});

describe("but the SSH Key is not set yet", () => {
// Mock another auth method for reaching the table
beforeEach(() => isRootPasswordSetFn.mockResolvedValue(true));

it("renders the 'Not set' status", async () => {
installerRender(<RootAuthMethods />);

Expand Down Expand Up @@ -266,6 +310,9 @@ describe("when ready", () => {
});

describe("and user settings changes", () => {
// Mock an auth method for reaching the table
beforeEach(() => isRootPasswordSetFn.mockResolvedValue(true));

it("updates the UI accordingly", async () => {
const [mockFunction, callbacks] = createCallbackMock();
onUsersChangeFn = mockFunction;
Expand Down