Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/helpDesk/services/Feedback.server.test.ts
T

100 lines
3.5 KiB
TypeScript

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<ReturnType<typeof createFeedback>>);
});
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();
});
});