feat: T4 wave 11 – bottling form with article values and confirmation, Telegram in the app
- Bottling form: jar size and best-before from the product with source, differs-hints before saving, prefill status, water-content confirmation naming the measured value, idempotency key renewed on body change; product form gets shelf life (months) and whole-gram jar weight. - Telegram frontend: link card with step-up dialog, t.me-only deep link, status polling, unlink; system Telegram card with receiver/sender state and confirmed retry; StepUpDialog shared with the backup import. - Reauth calls never refresh-and-retry on 401 (no double lockout count); German texts for expired session and 429; service worker never caches Telegram, notification settings and system responses. - Telegram backend round 2: shared link removal on every disconnect path, lease released on shutdown, connection errors not counted as poison. - ARCHITECTURE.md §2, §6 (Annahmen 56–58), §10, §11.12, §11.13; TASKS.md T4. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
fab7f7e12f
commit
9760ce154f
73 files changed
+3633
-269
No files matched your search
@@ -0,0 +1,119 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { StepUpDialog } from "./step-up-dialog";
|
||||
import { ApiError } from "@/lib/auth/api-client";
|
||||
|
||||
function renderDialog(overrides: Partial<Parameters<typeof StepUpDialog>[0]> = {}) {
|
||||
const onConfirm = vi.fn().mockResolvedValue(undefined);
|
||||
const onOpenChange = vi.fn();
|
||||
render(
|
||||
<StepUpDialog
|
||||
open
|
||||
title="Telegram verbinden?"
|
||||
confirmLabel="Verbinden"
|
||||
needsMfa={false}
|
||||
onConfirm={onConfirm}
|
||||
onOpenChange={onOpenChange}
|
||||
{...overrides}
|
||||
/>,
|
||||
);
|
||||
return { onConfirm, onOpenChange };
|
||||
}
|
||||
|
||||
describe("StepUpDialog", () => {
|
||||
it("asks for the password only without MFA and confirms with it", async () => {
|
||||
const { onConfirm, onOpenChange } = renderDialog();
|
||||
|
||||
expect(screen.queryByLabelText("Code aus der Authenticator-App")).not.toBeInTheDocument();
|
||||
const confirm = screen.getByRole("button", { name: "Verbinden" });
|
||||
expect(confirm).toBeDisabled();
|
||||
|
||||
await userEvent.type(screen.getByLabelText("Ihr Passwort"), "geheim");
|
||||
await userEvent.click(confirm);
|
||||
|
||||
await waitFor(() => expect(onConfirm).toHaveBeenCalledWith({ password: "geheim" }));
|
||||
await waitFor(() => expect(onOpenChange).toHaveBeenCalledWith(false));
|
||||
});
|
||||
|
||||
it("asks for the six-digit code with MFA and sends both", async () => {
|
||||
const { onConfirm } = renderDialog({ needsMfa: true });
|
||||
|
||||
await userEvent.type(screen.getByLabelText("Ihr Passwort"), "geheim");
|
||||
const confirm = screen.getByRole("button", { name: "Verbinden" });
|
||||
await userEvent.type(screen.getByLabelText("Code aus der Authenticator-App"), "12ab34");
|
||||
expect(confirm).toBeDisabled();
|
||||
await userEvent.type(screen.getByLabelText("Code aus der Authenticator-App"), "56");
|
||||
expect(confirm).toBeEnabled();
|
||||
await userEvent.click(confirm);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(onConfirm).toHaveBeenCalledWith({ password: "geheim", mfaCode: "123456" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("stays open and shows the server's German reason when the proof is refused", async () => {
|
||||
const onConfirm = vi.fn().mockRejectedValue(new ApiError(401, "Das Passwort ist falsch."));
|
||||
const { onOpenChange } = renderDialog({ onConfirm });
|
||||
|
||||
await userEvent.type(screen.getByLabelText("Ihr Passwort"), "falsch");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Verbinden" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole("alert")).toHaveTextContent("Das Passwort ist falsch."),
|
||||
);
|
||||
expect(onOpenChange).not.toHaveBeenCalledWith(false);
|
||||
expect(screen.getByLabelText("Ihr Passwort")).toHaveValue("");
|
||||
expect(screen.getByLabelText("Ihr Passwort")).toHaveFocus();
|
||||
});
|
||||
|
||||
it("shows the German too-many-attempts text on 429", async () => {
|
||||
renderDialog({ onConfirm: vi.fn().mockRejectedValue(new ApiError(429, "ThrottlerException")) });
|
||||
|
||||
await userEvent.type(screen.getByLabelText("Ihr Passwort"), "x");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Verbinden" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole("alert")).toHaveTextContent(
|
||||
"Zu viele Versuche. Bitte in einer Minute noch einmal.",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("says the session expired on a bare 401", async () => {
|
||||
renderDialog({ onConfirm: vi.fn().mockRejectedValue(new ApiError(401, "Unauthorized")) });
|
||||
|
||||
await userEvent.type(screen.getByLabelText("Ihr Passwort"), "x");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Verbinden" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole("alert")).toHaveTextContent(
|
||||
"Sitzung abgelaufen. Bitte die Seite neu laden und erneut anmelden.",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("makes the inputs read-only while the action runs", async () => {
|
||||
renderDialog({ onConfirm: vi.fn(() => new Promise<void>(() => {})) });
|
||||
|
||||
await userEvent.type(screen.getByLabelText("Ihr Passwort"), "geheim");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Verbinden" }));
|
||||
|
||||
await waitFor(() => expect(screen.getByLabelText("Ihr Passwort")).toHaveAttribute("readonly"));
|
||||
});
|
||||
|
||||
it("cannot be closed with Escape while the action runs, and shows the pending label", async () => {
|
||||
const onConfirm = vi.fn(() => new Promise<void>(() => {}));
|
||||
const { onOpenChange } = renderDialog({ onConfirm, pendingLabel: "Wird eingespielt…" });
|
||||
|
||||
await userEvent.type(screen.getByLabelText("Ihr Passwort"), "geheim");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Verbinden" }));
|
||||
expect(await screen.findByRole("button", { name: "Wird eingespielt…" })).toBeDisabled();
|
||||
|
||||
await userEvent.keyboard("{Escape}");
|
||||
await userEvent.click(screen.getByRole("button", { name: /Close/ }));
|
||||
|
||||
expect(onOpenChange).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user