131 lines
5.3 KiB
TypeScript
131 lines
5.3 KiB
TypeScript
// @vitest-environment node
|
|
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { ActionFunctionArgs } from "react-router";
|
|
|
|
vi.mock("@toolctrl/db/analytics.server", () => ({
|
|
deleteAllAnalyticsData: vi.fn(),
|
|
getOrCreateAnalyticsSettings: vi.fn(),
|
|
removeAnalyticsIgnoredPage: vi.fn(),
|
|
updateAnalyticsSettingsPreferences: vi.fn(),
|
|
}));
|
|
vi.mock("@toolctrl/db/impersonation-audit.server", async () => {
|
|
const actual = await vi.importActual<typeof import("@toolctrl/db/impersonation-audit.server")>("@toolctrl/db/impersonation-audit.server");
|
|
return {
|
|
...actual,
|
|
createImpersonationAudit: vi.fn(),
|
|
};
|
|
});
|
|
vi.mock("@toolctrl/db/audit-events.server", () => ({
|
|
createAuditEvent: vi.fn(),
|
|
}));
|
|
vi.mock("~/locale/i18next.server", () => ({
|
|
getTranslations: vi.fn(async () => ({ t: (key: string) => key })),
|
|
}));
|
|
vi.mock("~/utils/helpers/.server/PermissionsService", () => ({
|
|
verifyUserHasPermission: vi.fn(),
|
|
}));
|
|
vi.mock("~/utils/session.server", () => ({
|
|
getUserInfo: vi.fn(),
|
|
validateCSRFToken: vi.fn(),
|
|
}));
|
|
|
|
import { deleteAllAnalyticsData } from "@toolctrl/db/analytics.server";
|
|
import { createImpersonationAudit, IMPERSONATED_DESTRUCTIVE_ACTION_CONFIRMATION } from "@toolctrl/db/impersonation-audit.server";
|
|
import { verifyUserHasPermission } from "~/utils/helpers/.server/PermissionsService";
|
|
import { getUserInfo } from "~/utils/session.server";
|
|
import { action } from "./settings";
|
|
|
|
function deleteAllRequest(body: Record<string, string> = {}) {
|
|
return new Request("https://admin.example.test/analytics/settings?tab=danger", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body: new URLSearchParams({ action: "delete-all", ...body }),
|
|
});
|
|
}
|
|
|
|
async function callDeleteAll(body: Record<string, string> = {}) {
|
|
return action({
|
|
request: deleteAllRequest(body),
|
|
params: {},
|
|
context: {},
|
|
unstable_pattern: "admin/analytics/settings",
|
|
} as ActionFunctionArgs) as Promise<Response>;
|
|
}
|
|
|
|
describe("admin analytics settings route DB boundary", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.mocked(verifyUserHasPermission).mockResolvedValue(undefined as never);
|
|
vi.mocked(getUserInfo).mockResolvedValue({ userId: "user-1", lightOrDarkMode: "light", lng: "en", cookies: [] } as never);
|
|
});
|
|
|
|
it("uses package-owned analytics settings helpers", () => {
|
|
expect(deleteAllAnalyticsData).toBeDefined();
|
|
});
|
|
|
|
it("keeps visible delete confirmation text behind locale keys", () => {
|
|
const source = readFileSync(path.resolve(process.cwd(), "apps/web/app/routes/admin/analytics/settings.tsx"), "utf8");
|
|
const en = JSON.parse(readFileSync(path.resolve(process.cwd(), "apps/web/public/locales/en/translations.json"), "utf8"));
|
|
const de = JSON.parse(readFileSync(path.resolve(process.cwd(), "apps/web/public/locales/de/translations.json"), "utf8"));
|
|
|
|
expect(source).toContain("analytics.danger.confirmText");
|
|
expect(source).not.toContain('"delete all analytics data"');
|
|
expect(source).not.toContain("`delete all analytics data`");
|
|
expect(en.analytics.danger.confirmText).toBe("delete all analytics data");
|
|
expect(de.analytics.danger.confirmText).toBe("alle Analytics-Daten löschen");
|
|
});
|
|
|
|
it("deletes analytics data for normal sessions", async () => {
|
|
const response = await callDeleteAll();
|
|
|
|
await expect(response.json()).resolves.toEqual({ deleteSuccess: true });
|
|
expect(response.status).toBe(200);
|
|
expect(verifyUserHasPermission).toHaveBeenCalledWith(expect.any(Request), "admin.analytics.delete");
|
|
expect(deleteAllAnalyticsData).toHaveBeenCalledTimes(1);
|
|
expect(createImpersonationAudit).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("blocks impersonated deletion without the explicit impersonation phrase", async () => {
|
|
vi.mocked(getUserInfo).mockResolvedValue({
|
|
userId: "target-user-1",
|
|
impersonatingFromUserId: "actor-user-1",
|
|
lightOrDarkMode: "light",
|
|
lng: "en",
|
|
cookies: [],
|
|
} as never);
|
|
|
|
const response = await callDeleteAll({ "impersonation-confirm": "delete all analytics data" });
|
|
|
|
await expect(response.json()).resolves.toEqual({ deleteError: "admin.analytics.settings.errors.confirmImpersonatedDelete" });
|
|
expect(response.status).toBe(400);
|
|
expect(deleteAllAnalyticsData).not.toHaveBeenCalled();
|
|
expect(createImpersonationAudit).toHaveBeenCalledWith({
|
|
actorUserId: "actor-user-1",
|
|
targetUserId: "target-user-1",
|
|
tenantId: null,
|
|
action: "destructive_action_blocked",
|
|
reason: "analytics.delete-all",
|
|
requestPath: "/analytics/settings",
|
|
});
|
|
});
|
|
|
|
it("allows impersonated deletion with the explicit impersonation phrase", async () => {
|
|
vi.mocked(getUserInfo).mockResolvedValue({
|
|
userId: "target-user-1",
|
|
impersonatingFromUserId: "actor-user-1",
|
|
lightOrDarkMode: "light",
|
|
lng: "en",
|
|
cookies: [],
|
|
} as never);
|
|
|
|
const response = await callDeleteAll({ "impersonation-confirm": IMPERSONATED_DESTRUCTIVE_ACTION_CONFIRMATION });
|
|
|
|
await expect(response.json()).resolves.toEqual({ deleteSuccess: true });
|
|
expect(response.status).toBe(200);
|
|
expect(deleteAllAnalyticsData).toHaveBeenCalledTimes(1);
|
|
expect(createImpersonationAudit).not.toHaveBeenCalled();
|
|
});
|
|
});
|