import type { TFunction } from "i18next"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { db } from "~/utils/db.server"; import { createFeedback } from "../db/feedback.db.server"; import NotificationService from "~/modules/notifications/services/.server/NotificationService"; import { feedbackRedactionMarkers } from "./FeedbackRedaction.server"; vi.mock("~/utils/db.server", () => ({ db: { feedback: { count: vi.fn(), }, }, })); vi.mock("../db/feedback.db.server", () => ({ createFeedback: vi.fn(), })); vi.mock("~/modules/notifications/services/.server/NotificationService", () => ({ default: { sendToRoles: vi.fn(), }, })); const countFeedbackMock = vi.mocked(db.feedback.count); const createFeedbackMock = vi.mocked(createFeedback); const sendToRolesMock = vi.mocked(NotificationService.sendToRoles); const t = ((key: string) => key) as TFunction; describe("FeedbackServer.send", () => { beforeEach(() => { vi.clearAllMocks(); countFeedbackMock.mockResolvedValue(0); createFeedbackMock.mockResolvedValue({ id: "feedback_1" } as Awaited>); }); it("redacts sensitive message and URL values before notification and persistence", async () => { const rawEmail = "owner@example.com"; const rawBearer = "secret-token-value"; const rawHexSecret = "abcdefabcdefabcdefabcdefabcdefabcdefabcd"; const rawQueryToken = "raw-token-123"; const rawClientSecret = "super-secret-value"; const message = `Login failed for ${rawEmail} using Bearer ${rawBearer}; trace ${rawHexSecret}.`; const fromUrl = `https://app.example.com/feedback?token=${rawQueryToken}&next=/home&client_secret=${rawClientSecret}`; const { default: FeedbackServer } = await import("./Feedback.server"); await FeedbackServer.send({ t, tenantId: "tenant_1", userId: "user_1", message, fromUrl, }); expect(sendToRolesMock).toHaveBeenCalledWith( expect.objectContaining({ notification: expect.objectContaining({ message: expect.stringContaining(feedbackRedactionMarkers.email), }), }), ); expect(createFeedbackMock).toHaveBeenCalledWith({ tenantId: "tenant_1", userId: "user_1", message: expect.stringContaining(feedbackRedactionMarkers.secret), fromUrl: expect.stringContaining(`token=${feedbackRedactionMarkers.token}`), }); const notifiedPayload = JSON.stringify(sendToRolesMock.mock.calls[0]); const createdPayload = JSON.stringify(createFeedbackMock.mock.calls[0]); for (const rawSecret of [rawEmail, rawBearer, rawHexSecret, rawQueryToken, rawClientSecret]) { expect(notifiedPayload).not.toContain(rawSecret); expect(createdPayload).not.toContain(rawSecret); } expect(notifiedPayload).toContain(`Bearer ${feedbackRedactionMarkers.token}`); expect(createdPayload).toContain(`client_secret=${feedbackRedactionMarkers.token}`); expect(createdPayload).toContain("next=/home"); }); it("rejects feedback when the redacted trimmed message is empty", async () => { const { default: FeedbackServer } = await import("./Feedback.server"); await expect( FeedbackServer.send({ t, tenantId: "tenant_1", userId: "user_1", message: " ", fromUrl: "/acme?token=raw-token-123", }), ).rejects.toThrow("Message is required"); expect(sendToRolesMock).not.toHaveBeenCalled(); expect(createFeedbackMock).not.toHaveBeenCalled(); expect(countFeedbackMock).not.toHaveBeenCalled(); }); });