100 lines
3.5 KiB
TypeScript
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();
|
|
});
|
|
});
|