mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 16:16:21 -06:00
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com> Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
278 lines
9.6 KiB
TypeScript
278 lines
9.6 KiB
TypeScript
import * as Sentry from "@sentry/nextjs";
|
|
import { Mock, beforeEach, describe, expect, test, vi } from "vitest";
|
|
import { logger } from "@formbricks/logger";
|
|
import { responses } from "./response";
|
|
import { ApiAuditLog } from "./with-api-logging";
|
|
|
|
// Mocks
|
|
// This top-level mock is crucial for the SUT (withApiLogging.ts)
|
|
vi.mock("@/modules/ee/audit-logs/lib/handler", () => ({
|
|
__esModule: true,
|
|
queueAuditEvent: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@sentry/nextjs", () => ({
|
|
captureException: vi.fn(),
|
|
}));
|
|
|
|
// Define these outside the mock factory so they can be referenced in tests and reset by clearAllMocks.
|
|
const mockContextualLoggerError = vi.fn();
|
|
const mockContextualLoggerWarn = vi.fn();
|
|
const mockContextualLoggerInfo = vi.fn();
|
|
|
|
vi.mock("@formbricks/logger", () => {
|
|
const mockWithContextInstance = vi.fn(() => ({
|
|
error: mockContextualLoggerError,
|
|
warn: mockContextualLoggerWarn,
|
|
info: mockContextualLoggerInfo,
|
|
}));
|
|
return {
|
|
logger: {
|
|
withContext: mockWithContextInstance,
|
|
// These are for direct calls like logger.error(), logger.warn()
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
info: vi.fn(),
|
|
},
|
|
};
|
|
});
|
|
|
|
function createMockRequest({ method = "GET", url = "https://api.test/endpoint", headers = new Map() } = {}) {
|
|
return {
|
|
method,
|
|
url,
|
|
headers: {
|
|
get: (key: string) => headers.get(key),
|
|
},
|
|
} as unknown as Request;
|
|
}
|
|
|
|
// Minimal valid ApiAuditLog
|
|
const baseAudit: ApiAuditLog = {
|
|
action: "created",
|
|
targetType: "survey",
|
|
userId: "user-1",
|
|
targetId: "target-1",
|
|
organizationId: "org-1",
|
|
status: "failure",
|
|
userType: "api",
|
|
};
|
|
|
|
describe("withApiLogging", () => {
|
|
beforeEach(() => {
|
|
vi.resetModules(); // Reset SUT and other potentially cached modules
|
|
// vi.doMock for constants if a specific test needs to override it
|
|
// The top-level mocks for audit-logs, sentry, logger should be re-applied implicitly
|
|
// or are already in place due to vi.mock hoisting.
|
|
|
|
// Restore the mock for constants to its default for most tests
|
|
vi.doMock("@/lib/constants", () => ({
|
|
AUDIT_LOG_ENABLED: true,
|
|
IS_PRODUCTION: true,
|
|
SENTRY_DSN: "dsn",
|
|
ENCRYPTION_KEY: "test-key",
|
|
REDIS_URL: "redis://localhost:6379",
|
|
}));
|
|
|
|
vi.clearAllMocks(); // Clear call counts etc. for all vi.fn()
|
|
});
|
|
|
|
test("logs and audits on error response", async () => {
|
|
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
|
|
"@/modules/ee/audit-logs/lib/handler"
|
|
)) as unknown as { queueAuditEvent: Mock };
|
|
const handler = vi.fn().mockImplementation(async (req, _props, auditLog) => {
|
|
if (auditLog) {
|
|
auditLog.action = "created";
|
|
auditLog.targetType = "survey";
|
|
auditLog.userId = "user-1";
|
|
auditLog.targetId = "target-1";
|
|
auditLog.organizationId = "org-1";
|
|
auditLog.userType = "api";
|
|
}
|
|
return {
|
|
response: responses.internalServerErrorResponse("fail"),
|
|
};
|
|
});
|
|
const req = createMockRequest({ headers: new Map([["x-request-id", "abc-123"]]) });
|
|
const { withApiLogging } = await import("./with-api-logging"); // SUT dynamically imported
|
|
const wrapped = withApiLogging(handler, "created", "survey");
|
|
await wrapped(req, undefined);
|
|
expect(logger.withContext).toHaveBeenCalled();
|
|
expect(mockContextualLoggerError).toHaveBeenCalled();
|
|
expect(mockContextualLoggerWarn).not.toHaveBeenCalled();
|
|
expect(mockContextualLoggerInfo).not.toHaveBeenCalled();
|
|
expect(mockedQueueAuditEvent).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
eventId: "abc-123",
|
|
userType: "api",
|
|
apiUrl: req.url,
|
|
action: "created",
|
|
status: "failure",
|
|
targetType: "survey",
|
|
userId: "user-1",
|
|
targetId: "target-1",
|
|
organizationId: "org-1",
|
|
})
|
|
);
|
|
expect(Sentry.captureException).toHaveBeenCalledWith(
|
|
expect.any(Error),
|
|
expect.objectContaining({ extra: expect.objectContaining({ correlationId: "abc-123" }) })
|
|
);
|
|
});
|
|
|
|
test("does not log Sentry if not 500", async () => {
|
|
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
|
|
"@/modules/ee/audit-logs/lib/handler"
|
|
)) as unknown as { queueAuditEvent: Mock };
|
|
const handler = vi.fn().mockImplementation(async (req, _props, auditLog) => {
|
|
if (auditLog) {
|
|
auditLog.action = "created";
|
|
auditLog.targetType = "survey";
|
|
auditLog.userId = "user-1";
|
|
auditLog.targetId = "target-1";
|
|
auditLog.organizationId = "org-1";
|
|
auditLog.userType = "api";
|
|
}
|
|
return {
|
|
response: responses.badRequestResponse("bad req"),
|
|
};
|
|
});
|
|
const req = createMockRequest();
|
|
const { withApiLogging } = await import("./with-api-logging");
|
|
const wrapped = withApiLogging(handler, "created", "survey");
|
|
await wrapped(req, undefined);
|
|
expect(Sentry.captureException).not.toHaveBeenCalled();
|
|
expect(logger.withContext).toHaveBeenCalled();
|
|
expect(mockContextualLoggerError).toHaveBeenCalled();
|
|
expect(mockContextualLoggerWarn).not.toHaveBeenCalled();
|
|
expect(mockContextualLoggerInfo).not.toHaveBeenCalled();
|
|
expect(mockedQueueAuditEvent).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
userType: "api",
|
|
apiUrl: req.url,
|
|
action: "created",
|
|
status: "failure",
|
|
targetType: "survey",
|
|
userId: "user-1",
|
|
targetId: "target-1",
|
|
organizationId: "org-1",
|
|
})
|
|
);
|
|
});
|
|
|
|
test("logs and audits on thrown error", async () => {
|
|
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
|
|
"@/modules/ee/audit-logs/lib/handler"
|
|
)) as unknown as { queueAuditEvent: Mock };
|
|
const handler = vi.fn().mockImplementation(async (req, _props, auditLog) => {
|
|
if (auditLog) {
|
|
auditLog.action = "created";
|
|
auditLog.targetType = "survey";
|
|
auditLog.userId = "user-1";
|
|
auditLog.targetId = "target-1";
|
|
auditLog.organizationId = "org-1";
|
|
auditLog.userType = "api";
|
|
}
|
|
throw new Error("fail!");
|
|
});
|
|
const req = createMockRequest({ headers: new Map([["x-request-id", "err-1"]]) });
|
|
const { withApiLogging } = await import("./with-api-logging");
|
|
const wrapped = withApiLogging(handler, "created", "survey");
|
|
const res = await wrapped(req, undefined);
|
|
expect(res.status).toBe(500);
|
|
const body = await res.json();
|
|
expect(body).toEqual({
|
|
code: "internal_server_error",
|
|
message: "An unexpected error occurred.",
|
|
details: {},
|
|
});
|
|
expect(logger.withContext).toHaveBeenCalled();
|
|
expect(mockContextualLoggerError).toHaveBeenCalled();
|
|
expect(mockContextualLoggerWarn).not.toHaveBeenCalled();
|
|
expect(mockContextualLoggerInfo).not.toHaveBeenCalled();
|
|
expect(mockedQueueAuditEvent).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
eventId: "err-1",
|
|
userType: "api",
|
|
apiUrl: req.url,
|
|
action: "created",
|
|
status: "failure",
|
|
targetType: "survey",
|
|
userId: "user-1",
|
|
targetId: "target-1",
|
|
organizationId: "org-1",
|
|
})
|
|
);
|
|
expect(Sentry.captureException).toHaveBeenCalledWith(
|
|
expect.any(Error),
|
|
expect.objectContaining({ extra: expect.objectContaining({ correlationId: "err-1" }) })
|
|
);
|
|
});
|
|
|
|
test("does not log/audit on success response", async () => {
|
|
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
|
|
"@/modules/ee/audit-logs/lib/handler"
|
|
)) as unknown as { queueAuditEvent: Mock };
|
|
const handler = vi.fn().mockImplementation(async (req, _props, auditLog) => {
|
|
if (auditLog) {
|
|
auditLog.action = "created";
|
|
auditLog.targetType = "survey";
|
|
auditLog.userId = "user-1";
|
|
auditLog.targetId = "target-1";
|
|
auditLog.organizationId = "org-1";
|
|
auditLog.userType = "api";
|
|
}
|
|
return {
|
|
response: responses.successResponse({ ok: true }),
|
|
};
|
|
});
|
|
const req = createMockRequest();
|
|
const { withApiLogging } = await import("./with-api-logging");
|
|
const wrapped = withApiLogging(handler, "created", "survey");
|
|
await wrapped(req, undefined);
|
|
expect(logger.withContext).not.toHaveBeenCalled();
|
|
expect(mockContextualLoggerError).not.toHaveBeenCalled();
|
|
expect(mockContextualLoggerWarn).not.toHaveBeenCalled();
|
|
expect(mockContextualLoggerInfo).not.toHaveBeenCalled();
|
|
expect(mockedQueueAuditEvent).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
userType: "api",
|
|
apiUrl: req.url,
|
|
action: "created",
|
|
status: "success",
|
|
targetType: "survey",
|
|
userId: "user-1",
|
|
targetId: "target-1",
|
|
organizationId: "org-1",
|
|
})
|
|
);
|
|
expect(Sentry.captureException).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("does not call audit if AUDIT_LOG_ENABLED is false", async () => {
|
|
// For this specific test, we override the AUDIT_LOG_ENABLED constant
|
|
vi.doMock("@/lib/constants", () => ({
|
|
AUDIT_LOG_ENABLED: false,
|
|
IS_PRODUCTION: true,
|
|
SENTRY_DSN: "dsn",
|
|
ENCRYPTION_KEY: "test-key",
|
|
REDIS_URL: "redis://localhost:6379",
|
|
}));
|
|
|
|
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
|
|
"@/modules/ee/audit-logs/lib/handler"
|
|
)) as unknown as { queueAuditEvent: Mock };
|
|
const { withApiLogging } = await import("./with-api-logging");
|
|
|
|
const handler = vi.fn().mockResolvedValue({
|
|
response: responses.internalServerErrorResponse("fail"),
|
|
audit: { ...baseAudit },
|
|
});
|
|
const req = createMockRequest();
|
|
const wrapped = withApiLogging(handler, "created", "survey");
|
|
await wrapped(req, undefined);
|
|
expect(mockedQueueAuditEvent).not.toHaveBeenCalled();
|
|
});
|
|
});
|