Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save user to backend #1027

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions frontend/app/component/navbar/NavBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ describe("NavBar", () => {
test.each([
{ pathname: "/elections", hash: "#administratorcoordinator" },
{ pathname: "/users", hash: "#administratorcoordinator" },
{ pathname: "/users/create", hash: "#administratorcoordinator" },
{ pathname: "/users/create/details", hash: "#administratorcoordinator" },
{ pathname: "/workstations", hash: "#administratorcoordinator" },
{ pathname: "/logs", hash: "#administratorcoordinator" },
{ pathname: "/elections/1", hash: "#administratorcoordinator" },
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/component/navbar/NavBarLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function NavBarLinks({ location }: NavBarLinksProps) {

if (
(location.pathname.match(/^\/elections(\/\d+)?$/) && (isAdministrator || isCoordinator)) ||
location.pathname === "/users" ||
location.pathname.startsWith("/users") ||
location.pathname === "/workstations" ||
location.pathname === "/logs"
) {
Expand Down
13 changes: 11 additions & 2 deletions frontend/app/module/users/UserListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { useUserListRequest } from "app/module/users/useUserListRequest";

import { t } from "@kiesraad/i18n";
import { IconPlus } from "@kiesraad/icon";
import { Button, Loader, PageTitle, Table, Toolbar } from "@kiesraad/ui";
import { formatDateTime } from "@kiesraad/util";
import { Alert, Button, Loader, PageTitle, Table, Toolbar } from "@kiesraad/ui";
import { formatDateTime, useQueryParam } from "@kiesraad/util";

export function UserListPage() {
const { requestState } = useUserListRequest();
const [createdMessage, clearCreatedMessage] = useQueryParam("created");

if (requestState.status === "loading") {
return <Loader />;
Expand All @@ -26,6 +27,14 @@ export function UserListPage() {
<h1>{t("users.management")}</h1>
</section>
</header>

{createdMessage && (
<Alert type="success" onClose={clearCreatedMessage}>
<h2>{t("users.user_created")}</h2>
<p>{createdMessage}</p>
</Alert>
)}

<main>
<article>
<Toolbar>
Expand Down
145 changes: 145 additions & 0 deletions frontend/app/module/users/create/UserCreate.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { within } from "@testing-library/dom";
import { render as rtlRender, screen, waitFor } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { beforeEach, describe, expect, test } from "vitest";

import { routes } from "app/routes";

import { ElectionListRequestHandler, UserListRequestHandler } from "@kiesraad/api-mocks";
import { overrideOnce, Providers, server, setupTestRouter } from "@kiesraad/test";

function renderWithRouter() {
const router = setupTestRouter(routes);
rtlRender(<Providers router={router} />);
return router;
}

const rolePage = {
radioGroup: () => screen.findByRole("group", { name: "Welke rol krijgt de nieuwe gebruiker?" }),
administrator: () => screen.getByLabelText(/Beheerder/),
coordinator: () => screen.getByLabelText(/Coördinator/),
typist: () => screen.getByLabelText(/Invoerder/),
continue: () => screen.getByRole("button", { name: "Verder" }),
};

const typePage = {
radioGroup: () => screen.findByRole("group", { name: "Type account" }),
withName: () => screen.getByLabelText(/Op naam/),
anonymous: () => screen.getByLabelText(/Anonieme gebruikersnaam/),
continue: () => screen.getByRole("button", { name: "Verder" }),
};

const detailsPage = {
title: () => screen.findByRole("heading", { name: "Details van het account" }),
username: () => screen.getByLabelText("Gebruikersnaam"),
fullname: () => screen.getByLabelText("Volledige naam"),
fullnameQuery: () => screen.queryByLabelText("Volledige naam"),
password: () => screen.getByLabelText("Tijdelijk wachtwoord"),
save: () => screen.getByRole("button", { name: "Opslaan" }),
};

describe("User create pages integration test", () => {
beforeEach(() => {
server.use(ElectionListRequestHandler);
});

describe("Navigation and fullname presence", () => {
test.each(["administrator", "coordinator"] as const)("For %s", async (role) => {
const router = renderWithRouter();
const user = userEvent.setup();

await router.navigate("/users/create");

await waitFor(rolePage.radioGroup);
await user.click(rolePage[role]());
await user.click(rolePage.continue());

await waitFor(detailsPage.title);
expect(detailsPage.fullname()).toBeInTheDocument();
});

test("For typist", async () => {
const router = renderWithRouter();
const user = userEvent.setup();

await router.navigate("/users/create");

await waitFor(rolePage.radioGroup);
await user.click(rolePage.typist());
await user.click(rolePage.continue());

await waitFor(typePage.radioGroup);
expect(typePage.withName()).toBeChecked();
await user.click(typePage.continue());

await waitFor(detailsPage.title);
expect(detailsPage.fullname()).toBeInTheDocument();
await router.navigate(-1);

await waitFor(typePage.radioGroup);
await user.click(typePage.anonymous());
await user.click(typePage.continue());

await waitFor(detailsPage.title);
expect(detailsPage.fullnameQuery()).not.toBeInTheDocument();
});
});

describe("Save user", () => {
test("Successfully", async () => {
server.use(UserListRequestHandler);
overrideOnce("post", "/api/user", 201, {
id: 10,
username: "GuusGeluk",
fullname: "Guus Geluk",
role: "administrator",
});

const router = renderWithRouter();
const user = userEvent.setup();

await router.navigate("/users/create");

await waitFor(rolePage.radioGroup);
await user.click(rolePage.administrator());
await user.click(rolePage.continue());

await waitFor(detailsPage.title);
await user.type(detailsPage.username(), "GuusGeluk");
await user.type(detailsPage.fullname(), "Guus Geluk");
await user.type(detailsPage.password(), "Geluksdubbeltje10");
await user.click(detailsPage.save());

expect(await screen.findByRole("heading", { name: "Gebruikersbeheer" })).toBeInTheDocument();
const alert = screen.getByRole("alert");
expect(within(alert).getByText("GuusGeluk is toegevoegd met de rol Beheerder")).toBeInTheDocument();
});

test("Showing a unique username error", async () => {
overrideOnce("post", "/api/user", 409, {
error: "Item is not unique",
fatal: false,
reference: "EntryNotUnique",
});

const router = renderWithRouter();
const user = userEvent.setup();

await router.navigate("/users/create");

await waitFor(rolePage.radioGroup);
await user.click(rolePage.administrator());
await user.click(rolePage.continue());

await waitFor(detailsPage.title);
await user.type(detailsPage.username(), "GuusGeluk");
await user.type(detailsPage.fullname(), "Guus Geluk");
await user.type(detailsPage.password(), "Geluksdubbeltje10");
await user.click(detailsPage.save());

const alert = screen.getByRole("alert");
expect(within(alert).getByText("Er bestaat al een gebruiker met gebruikersnaam GuusGeluk")).toBeInTheDocument();
expect(await detailsPage.title()).toBeInTheDocument();
});
});
});
16 changes: 7 additions & 9 deletions frontend/app/module/users/create/UserCreateContext.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import { createContext } from "react";

import { Role } from "@kiesraad/api";
import { ApiError, ApiResult, CreateUserRequest, Role, User } from "@kiesraad/api";

export type UserType = "fullname" | "anonymous";
export type UserDetails = Omit<CreateUserRequest, "role">;

export interface CreateUser {
export interface IUserCreateContext {
role?: Role;
setRole: (role: Role) => void;
type?: UserType;
setType: (type: UserType) => void;
createUser: (user: UserDetails) => Promise<ApiResult<User>>;
username?: string;
fullname?: string;
password?: string;
}

export interface IUserCreateContext {
user: CreateUser;
updateUser: (user: CreateUser) => void;
apiError: ApiError | null;
}

export const UserCreateContext = createContext<IUserCreateContext | undefined>(undefined);
24 changes: 18 additions & 6 deletions frontend/app/module/users/create/UserCreateContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
import * as React from "react";
import { useState } from "react";

import { User } from "@kiesraad/api";
import { ApiResult, CreateUserRequest, Role, useCrud, User, USER_CREATE_REQUEST_PATH } from "@kiesraad/api";

import { IUserCreateContext, UserCreateContext } from "./UserCreateContext";
import { IUserCreateContext, UserCreateContext, UserDetails, UserType } from "./UserCreateContext";

export function UserCreateContextProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<Partial<User>>({});
const [role, setRole] = useState<Role | undefined>(undefined);
const [type, setType] = useState<UserType | undefined>(undefined);
const [username, setUsername] = useState<string | undefined>(undefined);

function updateUser(update: Partial<User>) {
setUser((user) => ({ ...user, ...update }));
const url: USER_CREATE_REQUEST_PATH = "/api/user";
const userApi = useCrud<User>(url);

async function createUser(userDetails: UserDetails): Promise<ApiResult<User>> {
if (!role) {
throw new Error("Role was not set");
}

Check warning on line 19 in frontend/app/module/users/create/UserCreateContextProvider.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/app/module/users/create/UserCreateContextProvider.tsx#L18-L19

Added lines #L18 - L19 were not covered by tests

setUsername(userDetails.username);
return userApi.create({ role, ...userDetails } satisfies CreateUserRequest);
}

const context: IUserCreateContext = { user, updateUser };
const apiError = userApi.requestState.status === "api-error" ? userApi.requestState.error : null;

const context: IUserCreateContext = { role, setRole, type, setType, createUser, username, apiError };
return <UserCreateContext.Provider value={context}>{children}</UserCreateContext.Provider>;
}
Loading
Loading