update cypress tests, update zod validation error messaging to UI

This commit is contained in:
Francis Cao 2025-03-07 13:06:38 -08:00
parent 72ac97c5d9
commit b1901c7278
18 changed files with 221 additions and 41 deletions

View file

@ -15,7 +15,7 @@ export function UserAddButton({ onSave }: { onSave?: () => void }) {
return (
<ModalTrigger>
<Button variant="primary">
<Button data-test="button-create-user" variant="primary">
<Icon>
<Icons.Plus />
</Icon>

View file

@ -12,6 +12,7 @@ import {
} from 'react-basics';
import { useApi, useMessages } from '@/components/hooks';
import { ROLES } from '@/lib/constants';
import { messages } from '@/components/messages';
export function UserAddForm({ onSave, onClose }) {
const { post, useMutation } = useApi();
@ -44,26 +45,43 @@ export function UserAddForm({ onSave, onClose }) {
return (
<Form onSubmit={handleSubmit} error={error}>
<FormRow label={formatMessage(labels.username)}>
<FormInput name="username" rules={{ required: formatMessage(labels.required) }}>
<FormInput
data-test="input-username"
name="username"
rules={{ required: formatMessage(labels.required) }}
>
<TextField autoComplete="new-username" />
</FormInput>
</FormRow>
<FormRow label={formatMessage(labels.password)}>
<FormInput name="password" rules={{ required: formatMessage(labels.required) }}>
<FormInput
data-test="input-password"
name="password"
rules={{
required: formatMessage(labels.required),
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
}}
>
<PasswordField autoComplete="new-password" />
</FormInput>
</FormRow>
<FormRow label={formatMessage(labels.role)}>
<FormInput name="role" rules={{ required: formatMessage(labels.required) }}>
<Dropdown renderValue={renderValue}>
<Item key={ROLES.viewOnly}>{formatMessage(labels.viewOnly)}</Item>
<Item key={ROLES.user}>{formatMessage(labels.user)}</Item>
<Item key={ROLES.admin}>{formatMessage(labels.admin)}</Item>
<Dropdown data-test="dropdown-role" renderValue={renderValue}>
<Item data-test="dropdown-item-viewOnly" key={ROLES.viewOnly}>
{formatMessage(labels.viewOnly)}
</Item>
<Item data-test="dropdown-item-user" key={ROLES.user}>
{formatMessage(labels.user)}
</Item>
<Item data-test="dropdown-item-admin" key={ROLES.admin}>
{formatMessage(labels.admin)}
</Item>
</Dropdown>
</FormInput>
</FormRow>
<FormButtons flex>
<SubmitButton variant="primary" disabled={false}>
<SubmitButton data-test="button-submit" variant="primary" disabled={false}>
{formatMessage(labels.save)}
</SubmitButton>
<Button disabled={isPending} onClick={onClose}>

View file

@ -16,7 +16,7 @@ export function UserDeleteButton({
return (
<ModalTrigger disabled={userId === user?.id}>
<Button disabled={userId === user?.id} variant="quiet">
<Button data-test="button-delete" disabled={userId === user?.id} variant="quiet">
<Icon>
<Icons.Trash />
</Icon>

View file

@ -44,7 +44,7 @@ export function UsersTable({
<>
<UserDeleteButton userId={id} username={username} />
<LinkButton href={`/settings/users/${id}`}>
<Icon>
<Icon data-test="link-button-edit">
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>

View file

@ -62,6 +62,7 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
</FormRow>
<FormRow label={formatMessage(labels.password)}>
<FormInput
data-test="input-password"
name="password"
rules={{
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
@ -73,16 +74,24 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
{user.id !== login.id && (
<FormRow label={formatMessage(labels.role)}>
<FormInput name="role" rules={{ required: formatMessage(labels.required) }}>
<Dropdown renderValue={renderValue}>
<Item key={ROLES.viewOnly}>{formatMessage(labels.viewOnly)}</Item>
<Item key={ROLES.user}>{formatMessage(labels.user)}</Item>
<Item key={ROLES.admin}>{formatMessage(labels.admin)}</Item>
<Dropdown data-test="dropdown-role" renderValue={renderValue}>
<Item data-test="dropdown-item-viewOnly" key={ROLES.viewOnly}>
{formatMessage(labels.viewOnly)}
</Item>
<Item data-test="dropdown-item-user" key={ROLES.user}>
{formatMessage(labels.user)}
</Item>
<Item data-test="dropdown-item-admin" key={ROLES.admin}>
{formatMessage(labels.admin)}
</Item>
</Dropdown>
</FormInput>
</FormRow>
)}
<FormButtons>
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton>
<SubmitButton data-test="button-submit" variant="primary">
{formatMessage(labels.save)}
</SubmitButton>
</FormButtons>
</Form>
);

View file

@ -26,7 +26,7 @@ export async function POST(request: Request) {
const user = await getUserByUsername(username, { includePassword: true });
if (!user || !checkPassword(password, user.password)) {
return unauthorized();
return unauthorized('message.incorrect-username-password');
}
const { id, role, createdAt } = user;

View file

@ -1,9 +1,8 @@
import { z } from 'zod';
import { canUpdateUser, canViewUser, canDeleteUser } from '@/lib/auth';
import { getUser, getUserByUsername, updateUser, deleteUser } from '@/queries';
import { json, unauthorized, badRequest, ok } from '@/lib/response';
import { hashPassword } from '@/lib/auth';
import { canDeleteUser, canUpdateUser, canViewUser, hashPassword } from '@/lib/auth';
import { parseRequest } from '@/lib/request';
import { badRequest, json, ok, unauthorized } from '@/lib/response';
import { deleteUser, getUser, getUserByUsername, updateUser } from '@/queries';
import { z } from 'zod';
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
const { auth, error } = await parseRequest(request);
@ -26,7 +25,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ user
export async function POST(request: Request, { params }: { params: Promise<{ userId: string }> }) {
const schema = z.object({
username: z.string().max(255),
password: z.string().max(255),
password: z.string().max(255).optional(),
role: z.string().regex(/admin|user|view-only/i),
});

View file

@ -16,7 +16,7 @@ import Logo from '@/assets/logo.svg';
import styles from './LoginForm.module.css';
export function LoginForm() {
const { formatMessage, labels } = useMessages();
const { formatMessage, labels, getMessage } = useMessages();
const router = useRouter();
const { post, useMutation } = useApi();
const { mutate, error, isPending } = useMutation({
@ -40,7 +40,7 @@ export function LoginForm() {
<Logo />
</Icon>
<div className={styles.title}>umami</div>
<Form className={styles.form} onSubmit={handleSubmit} error={error}>
<Form className={styles.form} onSubmit={handleSubmit} error={getMessage(error)}>
<FormRow label={formatMessage(labels.username)}>
<FormInput
data-test="input-username"

View file

@ -27,7 +27,12 @@ export function ConfirmationForm({
<Form error={error}>
<p>{message}</p>
<FormButtons flex>
<LoadingButton isLoading={isLoading} onClick={onConfirm} variant={buttonVariant}>
<LoadingButton
data-test="button-confirm"
isLoading={isLoading}
onClick={onConfirm}
variant={buttonVariant}
>
{buttonLabel || formatMessage(labels.ok)}
</LoadingButton>
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>

View file

@ -1,4 +1,4 @@
import { ZodSchema } from 'zod';
import { z, ZodSchema } from 'zod';
import { FILTER_COLUMNS } from '@/lib/constants';
import { badRequest, unauthorized } from '@/lib/response';
import { getAllowedUnits, getMinimumUnit } from '@/lib/date';
@ -24,12 +24,21 @@ export async function parseRequest(
let error: () => void | undefined;
let auth = null;
const getErrorMessages = (error: z.ZodError) => {
return Object.entries(error.format())
.map(([key, value]) => {
const messages = (value as any)._errors;
return messages ? `${key}: ${messages.join(', ')}` : null;
})
.filter(Boolean);
};
if (schema) {
const isGet = request.method === 'GET';
const result = schema.safeParse(isGet ? query : body);
if (!result.success) {
error = () => badRequest(result.error);
error = () => badRequest(getErrorMessages(result.error));
} else if (isGet) {
query = result.data;
} else {