213 lines
7.8 KiB
TypeScript
213 lines
7.8 KiB
TypeScript
import "@testing-library/jest-dom";
|
|
import { render, screen } from "@testing-library/react";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { MemoryRouter } from "react-router";
|
|
import AdminSettingsAnalytics, { action } from "./analytics";
|
|
import { getOrCreateAppConfiguration, updateAppConfiguration } from "~/utils/db/appConfiguration.db.server";
|
|
import { createAdminLog } from "~/utils/db/logs.db.server";
|
|
import { verifyUserHasPermission } from "~/utils/helpers/.server/PermissionsService";
|
|
|
|
// EU-Sterilisation (R-P1a): Google-Analytics-Feld aus den Admin-Analytics-Settings entfernt.
|
|
// Verbleibend sind die datenschutzfreundlichen EU-Tracker SimpleAnalytics + Plausible.
|
|
let routeLoaderData = {
|
|
title: "Analytics Settings",
|
|
appConfiguration: {
|
|
analytics: {
|
|
enabled: true,
|
|
simpleAnalytics: false,
|
|
plausibleAnalytics: true,
|
|
},
|
|
},
|
|
};
|
|
let routeActionData: { success?: string; error?: string; fieldErrors?: Record<string, string[]> } | undefined;
|
|
let canUpdate = true;
|
|
|
|
vi.mock("react-router", async () => {
|
|
const actual = await vi.importActual<typeof import("react-router")>("react-router");
|
|
return {
|
|
...actual,
|
|
Form: ({ children, ...props }: React.ComponentProps<"form">) => <form {...props}>{children}</form>,
|
|
useLoaderData: () => routeLoaderData,
|
|
useActionData: () => routeActionData,
|
|
};
|
|
});
|
|
|
|
vi.mock("react-i18next", () => ({
|
|
useTranslation: () => ({ t: (key: string) => key }),
|
|
}));
|
|
|
|
vi.mock("~/locale/i18next.server", () => ({
|
|
getTranslations: vi.fn(async () => ({ t: (key: string) => key })),
|
|
}));
|
|
|
|
vi.mock("~/utils/data/useAdminData", () => ({
|
|
useAdminData: () => ({ permissions: canUpdate ? ["admin.settings.analytics.update"] : [] }),
|
|
}));
|
|
|
|
vi.mock("~/utils/helpers/PermissionsHelper", () => ({
|
|
getUserHasPermission: vi.fn(() => canUpdate),
|
|
}));
|
|
|
|
vi.mock("~/utils/helpers/.server/PermissionsService", () => ({
|
|
verifyUserHasPermission: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("~/utils/session.server", () => ({
|
|
validateCSRFToken: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("~/utils/data/useRootData", () => ({
|
|
useRootData: () => ({ csrf: "csrf-test-token" }),
|
|
}));
|
|
|
|
vi.mock("~/utils/db/appConfiguration.db.server", () => ({
|
|
getAppConfiguration: vi.fn(),
|
|
getOrCreateAppConfiguration: vi.fn(),
|
|
updateAppConfiguration: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("~/utils/db/logs.db.server", () => ({
|
|
createAdminLog: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("sonner", () => ({
|
|
toast: { success: vi.fn(), error: vi.fn() },
|
|
}));
|
|
|
|
vi.mock("~/components/ui/layouts/EditPageLayout", () => ({
|
|
default: ({ children }: { children: React.ReactNode }) => <main>{children}</main>,
|
|
}));
|
|
|
|
vi.mock("~/components/ui/buttons/BackButtonWithTitle", () => ({
|
|
default: ({ children }: { children: React.ReactNode }) => <h1>{children}</h1>,
|
|
}));
|
|
|
|
vi.mock("~/components/ui/sections/SettingSection", () => ({
|
|
default: ({ title, children }: { title: React.ReactNode; children: React.ReactNode }) => (
|
|
<section>
|
|
{title}
|
|
{children}
|
|
</section>
|
|
),
|
|
}));
|
|
|
|
vi.mock("~/components/ui/input/InputCheckboxWithDescription", () => ({
|
|
default: ({ name, title, value, disabled }: { name: string; title: string; value?: boolean; disabled?: boolean }) => (
|
|
<label>
|
|
{title}
|
|
<input name={name} type="checkbox" defaultChecked={value} disabled={disabled} />
|
|
</label>
|
|
),
|
|
}));
|
|
|
|
vi.mock("~/components/ui/button", () => ({
|
|
Button: ({ children, ...props }: React.ComponentProps<"button">) => <button {...props}>{children}</button>,
|
|
}));
|
|
|
|
const verifyUserHasPermissionMock = vi.mocked(verifyUserHasPermission);
|
|
const getOrCreateAppConfigurationMock = vi.mocked(getOrCreateAppConfiguration);
|
|
const updateAppConfigurationMock = vi.mocked(updateAppConfiguration);
|
|
const createAdminLogMock = vi.mocked(createAdminLog);
|
|
|
|
function postRequest(form: FormData) {
|
|
return new Request("https://admin.example.com/settings/analytics?token=secret", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body: new URLSearchParams(form as unknown as Record<string, string>),
|
|
});
|
|
}
|
|
|
|
function validForm() {
|
|
const form = new FormData();
|
|
form.set("action", "update");
|
|
form.set("enabled", "on");
|
|
form.set("simpleAnalytics", "on");
|
|
return form;
|
|
}
|
|
|
|
describe("admin analytics settings route", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
routeActionData = undefined;
|
|
canUpdate = true;
|
|
verifyUserHasPermissionMock.mockResolvedValue(undefined as never);
|
|
getOrCreateAppConfigurationMock.mockResolvedValue(undefined as never);
|
|
updateAppConfigurationMock.mockResolvedValue(undefined as never);
|
|
createAdminLogMock.mockResolvedValue(undefined as never);
|
|
});
|
|
|
|
it("rejects invalid actions before permission, database, or audit side effects", async () => {
|
|
const form = validForm();
|
|
form.set("action", "delete");
|
|
|
|
const response = await action({ request: postRequest(form), params: {}, context: {}, unstable_pattern: "admin/settings/analytics" });
|
|
const body = await (response as Response).json();
|
|
|
|
expect((response as Response).status).toBe(400);
|
|
expect(body.fieldErrors.action).toEqual(["Invalid action."]);
|
|
expect(verifyUserHasPermissionMock).not.toHaveBeenCalled();
|
|
expect(getOrCreateAppConfigurationMock).not.toHaveBeenCalled();
|
|
expect(updateAppConfigurationMock).not.toHaveBeenCalled();
|
|
expect(createAdminLogMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("propagates permission rejection for valid mutations without database or audit side effects", async () => {
|
|
const rejection = new Response("Forbidden", { status: 403 });
|
|
verifyUserHasPermissionMock.mockRejectedValue(rejection);
|
|
|
|
await expect(action({ request: postRequest(validForm()), params: {}, context: {}, unstable_pattern: "admin/settings/analytics" })).rejects.toBe(rejection);
|
|
|
|
expect(verifyUserHasPermissionMock).toHaveBeenCalledWith(expect.any(Request), "admin.settings.analytics.update");
|
|
expect(getOrCreateAppConfigurationMock).not.toHaveBeenCalled();
|
|
expect(updateAppConfigurationMock).not.toHaveBeenCalled();
|
|
expect(createAdminLogMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("updates valid settings and writes an audit payload", async () => {
|
|
const response = await action({ request: postRequest(validForm()), params: {}, context: {}, unstable_pattern: "admin/settings/analytics" });
|
|
const body = await (response as Response).json();
|
|
|
|
expect((response as Response).status).toBe(200);
|
|
expect(body.success).toBe("shared.updated");
|
|
expect(updateAppConfigurationMock).toHaveBeenCalledWith({
|
|
analyticsEnabled: true,
|
|
analyticsSimpleAnalytics: true,
|
|
analyticsPlausibleAnalytics: false,
|
|
});
|
|
expect(createAdminLogMock).toHaveBeenCalledWith(expect.any(Request), "admin.settings.updated", expect.any(String));
|
|
const auditPayload = createAdminLogMock.mock.calls[0][2];
|
|
expect(auditPayload).toContain("analytics");
|
|
expect(auditPayload).toContain("analyticsEnabled");
|
|
expect(auditPayload).toContain('"analyticsEnabled":true');
|
|
expect(auditPayload).not.toContain("token=secret");
|
|
});
|
|
|
|
it("renders accessible action and field errors", () => {
|
|
routeActionData = { error: "shared.invalidForm", fieldErrors: { action: ["Invalid action."] } };
|
|
|
|
render(
|
|
<MemoryRouter>
|
|
<AdminSettingsAnalytics />
|
|
</MemoryRouter>
|
|
);
|
|
|
|
const alert = screen.getByRole("alert");
|
|
expect(alert).toHaveTextContent("shared.invalidForm");
|
|
expect(alert).toHaveTextContent("action: Invalid action.");
|
|
});
|
|
|
|
it("preserves save button and checkbox disabled state when the admin lacks update permission", () => {
|
|
canUpdate = false;
|
|
|
|
render(
|
|
<MemoryRouter>
|
|
<AdminSettingsAnalytics />
|
|
</MemoryRouter>
|
|
);
|
|
|
|
expect(screen.getByLabelText("shared.enabled")).toBeDisabled();
|
|
expect(screen.getByLabelText("analytics.configuration.simpleAnalyticsEnabled")).toBeDisabled();
|
|
expect(screen.getByRole("button", { name: "shared.save" })).toBeDisabled();
|
|
});
|
|
});
|