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

102 lines
4.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@toolctrl/db/logs.server", () => ({
createLegacyEntityWebhookLog: vi.fn(async (data) => data),
createLegacyRowLog: vi.fn(async () => ({ id: "log_1" })),
getLegacyEntityWebhooksByAction: vi.fn(),
}));
vi.mock("~/utils/url.server", () => ({
getBaseURL: vi.fn((request: Request) => new URL(request.url).origin),
}));
vi.mock("~/utils/helpers/ApiHelper", () => ({
default: {
getApiFormat: vi.fn(() => ({ formatted: true })),
},
}));
import { createLegacyEntityWebhookLog, createLegacyRowLog, getLegacyEntityWebhooksByAction } from "@toolctrl/db/logs.server";
import { createApiRowLog } from "./RowLogService.server";
describe("RowLogService webhook delivery", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal("fetch", vi.fn(async () => new Response(null, { status: 202 })));
});
it("resolves relative webhook URLs against the request base before validation", async () => {
vi.mocked(getLegacyEntityWebhooksByAction).mockResolvedValueOnce([{ id: "webhook_relative", method: "POST", endpoint: "/webhooks/row" }]);
await createApiRowLog(new Request("https://tenant.example.com/api/rows"), {
tenantId: "tenant_1",
action: "created",
entity: { id: "entity_1" },
item: { id: "row_1" },
});
expect(fetch).toHaveBeenCalledWith(
"https://tenant.example.com/webhooks/row",
expect.objectContaining({ method: "POST", redirect: "manual", signal: expect.any(AbortSignal) })
);
expect(createLegacyEntityWebhookLog).toHaveBeenCalledWith({ webhookId: "webhook_relative", logId: "log_1", status: 202 });
});
it("follows safe redirects after validating the redirect target", async () => {
vi.stubGlobal(
"fetch",
vi
.fn()
.mockResolvedValueOnce(new Response(null, { status: 307, headers: { location: "https://hooks.example.com/row-next" } }))
.mockResolvedValueOnce(new Response(null, { status: 202 }))
);
vi.mocked(getLegacyEntityWebhooksByAction).mockResolvedValueOnce([{ id: "webhook_redirect", method: "POST", endpoint: "https://hooks.example.com/row" }]);
await createApiRowLog(new Request("https://tenant.example.com/api/rows"), {
tenantId: "tenant_1",
action: "created",
entity: { id: "entity_1" },
item: { id: "row_1" },
});
expect(fetch).toHaveBeenNthCalledWith(1, "https://hooks.example.com/row", expect.objectContaining({ method: "POST", redirect: "manual" }));
expect(fetch).toHaveBeenNthCalledWith(2, "https://hooks.example.com/row-next", expect.objectContaining({ method: "POST", redirect: "manual" }));
expect(createLegacyEntityWebhookLog).toHaveBeenCalledWith({ webhookId: "webhook_redirect", logId: "log_1", status: 202 });
});
it("blocks unsafe redirect targets without following them", async () => {
vi.stubGlobal("fetch", vi.fn(async () => new Response(null, { status: 302, headers: { location: "http://127.0.0.1/webhook" } })));
vi.mocked(getLegacyEntityWebhooksByAction).mockResolvedValueOnce([{ id: "webhook_redirect_blocked", method: "POST", endpoint: "https://hooks.example.com/row" }]);
await createApiRowLog(new Request("https://tenant.example.com/api/rows"), {
tenantId: "tenant_1",
action: "created",
entity: { id: "entity_1" },
item: { id: "row_1" },
});
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith("https://hooks.example.com/row", expect.objectContaining({ redirect: "manual" }));
expect(createLegacyEntityWebhookLog).toHaveBeenCalledWith({ webhookId: "webhook_redirect_blocked", logId: "log_1", status: 0 });
});
it("blocks unsafe webhook URLs and records failed delivery status without fetching", async () => {
vi.mocked(getLegacyEntityWebhooksByAction).mockResolvedValueOnce([
{ id: "webhook_loopback", method: "POST", endpoint: "http://127.0.0.1/webhook" },
{ id: "webhook_relative_localhost", method: "POST", endpoint: "/webhooks/row" },
]);
await createApiRowLog(new Request("https://localhost/api/rows"), {
tenantId: "tenant_1",
action: "created",
entity: { id: "entity_1" },
item: { id: "row_1" },
});
expect(fetch).not.toHaveBeenCalled();
expect(createLegacyRowLog).toHaveBeenCalledTimes(1);
expect(createLegacyEntityWebhookLog).toHaveBeenCalledWith({ webhookId: "webhook_loopback", logId: "log_1", status: 0 });
expect(createLegacyEntityWebhookLog).toHaveBeenCalledWith({ webhookId: "webhook_relative_localhost", logId: "log_1", status: 0 });
});
});