Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Submission API smoke-tests. Covers the three documented anti-spam layers
|
||||
* (origin allowlist, honeypot silent-drop, per-IP rate-limit) plus the
|
||||
* happy-path persistence call. Heavy collaborators (Prisma + SharedStore +
|
||||
* feature-flags + Turnstile fetch) are mocked at the module boundary so the
|
||||
* tests stay hermetic + fast.
|
||||
*/
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { HONEYPOT_FIELD_KEY } from "@toolctrl/form-widget";
|
||||
|
||||
const formRow = {
|
||||
id: "form_1",
|
||||
tenantId: "t_1",
|
||||
isEnabled: true,
|
||||
definition: {
|
||||
title: "Contact",
|
||||
submitLabel: "Send",
|
||||
successMessage: "Thanks",
|
||||
fields: [
|
||||
{ key: "email", type: "email", label: "Email", required: true },
|
||||
{ key: HONEYPOT_FIELD_KEY, type: "honeypot", label: "Leave empty" },
|
||||
],
|
||||
},
|
||||
allowedOrigins: ["https://allowed.example.com"],
|
||||
targetType: "generic",
|
||||
targetEntityId: null,
|
||||
fieldMapping: {},
|
||||
config: {},
|
||||
webhookUrl: null,
|
||||
notifyEmails: ["ops@example.com"],
|
||||
};
|
||||
|
||||
const createSubmission = vi.fn(async () => ({ id: "sub_1" }));
|
||||
const publishOutboxEventMock = vi.fn(async () => ({ id: "ob_1" }));
|
||||
|
||||
vi.mock("@toolctrl/db/outbox", () => ({
|
||||
publishOutboxEvent: (...args: unknown[]) => publishOutboxEventMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("~/utils/db.server", () => {
|
||||
const tx = {
|
||||
formSubmission: { create: createSubmission },
|
||||
};
|
||||
return {
|
||||
db: {
|
||||
form: {
|
||||
findUnique: vi.fn(async ({ where }: { where: { key: string } }) =>
|
||||
where.key === "key_missing" ? null : { ...formRow },
|
||||
),
|
||||
},
|
||||
formSubmission: { create: createSubmission },
|
||||
$transaction: vi.fn(async (fn: (tx: typeof tx) => Promise<unknown>) => fn(tx)),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("~/utils/logger.server", () => ({
|
||||
createScopedLogger: () => ({
|
||||
info: () => undefined,
|
||||
warn: () => undefined,
|
||||
error: () => undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("~/utils/services/.server/featureFlagEvaluator.server", () => ({
|
||||
evaluateFlagForContext: vi.fn(async () => ({ enabled: false, reason: "disabled" })),
|
||||
}));
|
||||
|
||||
vi.mock("~/shared-store.server", () => {
|
||||
const memory = new Map<string, number>();
|
||||
return {
|
||||
getSharedStore: () => ({
|
||||
// The route only calls helpers via createSlidingWindowRateLimiter; we
|
||||
// hand the limiter a real MemoryAdapter-backed store from shared-store.
|
||||
// For tests we stub eval to no-op via override below.
|
||||
}),
|
||||
__memory: memory,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@toolctrl/shared-store", async (orig) => {
|
||||
const real = (await orig()) as Record<string, unknown>;
|
||||
return {
|
||||
...real,
|
||||
createSlidingWindowRateLimiter: () => ({
|
||||
consume: async () => ({ allowed: true, remaining: 9, oldestAt: null }),
|
||||
reset: async () => undefined,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
async function loadAction() {
|
||||
const mod = await import("./api.embed.forms.$formKey.submit");
|
||||
return mod.action;
|
||||
}
|
||||
|
||||
function makeRequest(body: unknown, origin: string | null): Request {
|
||||
const params = new URLSearchParams();
|
||||
params.set("__payload", typeof body === "string" ? body : JSON.stringify(body));
|
||||
return new Request("https://forms.example.com/api/embed/forms/key_1/submit", {
|
||||
method: "POST",
|
||||
body: params,
|
||||
headers: {
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
...(origin ? { origin } : {}),
|
||||
"x-forwarded-for": "1.2.3.4",
|
||||
"user-agent": "vitest",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("api.embed.forms.$formKey.submit action", () => {
|
||||
beforeEach(() => {
|
||||
createSubmission.mockClear();
|
||||
publishOutboxEventMock.mockClear();
|
||||
});
|
||||
|
||||
it("returns 404 for an unknown form key", async () => {
|
||||
const action = await loadAction();
|
||||
const res = await action({
|
||||
params: { formKey: "key_missing" },
|
||||
request: makeRequest({ values: { email: "a@b.co" } }, "https://allowed.example.com"),
|
||||
});
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it("returns 403 when origin is not allowlisted", async () => {
|
||||
const action = await loadAction();
|
||||
const res = await action({
|
||||
params: { formKey: "key_1" },
|
||||
request: makeRequest({ values: { email: "a@b.co" } }, "https://evil.example.com"),
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
expect(createSubmission).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("silently drops (204) when honeypot is filled", async () => {
|
||||
const action = await loadAction();
|
||||
const res = await action({
|
||||
params: { formKey: "key_1" },
|
||||
request: makeRequest(
|
||||
{ values: { email: "a@b.co", [HONEYPOT_FIELD_KEY]: "bot" } },
|
||||
"https://allowed.example.com",
|
||||
),
|
||||
});
|
||||
expect(res.status).toBe(204);
|
||||
expect(createSubmission).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects missing required fields with 400", async () => {
|
||||
const action = await loadAction();
|
||||
const res = await action({
|
||||
params: { formKey: "key_1" },
|
||||
request: makeRequest({ values: { email: "" } }, "https://allowed.example.com"),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
expect(createSubmission).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects malformed email with 400", async () => {
|
||||
const action = await loadAction();
|
||||
const res = await action({
|
||||
params: { formKey: "key_1" },
|
||||
request: makeRequest({ values: { email: "not-an-email" } }, "https://allowed.example.com"),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("persists submission on the happy path", async () => {
|
||||
const action = await loadAction();
|
||||
const res = await action({
|
||||
params: { formKey: "key_1" },
|
||||
request: makeRequest({ values: { email: "alice@example.com" } }, "https://allowed.example.com"),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const json = (await res.json()) as { ok: boolean };
|
||||
expect(json.ok).toBe(true);
|
||||
expect(createSubmission).toHaveBeenCalledTimes(1);
|
||||
const args = createSubmission.mock.calls[0]![0] as { data: { tenantId: string; status: string } };
|
||||
expect(args.data.tenantId).toBe("t_1");
|
||||
expect(args.data.status).toBe("received");
|
||||
});
|
||||
|
||||
it("publishes a form.submission.process outbox event with the submission payload", async () => {
|
||||
const action = await loadAction();
|
||||
const res = await action({
|
||||
params: { formKey: "key_1" },
|
||||
request: makeRequest({ values: { email: "alice@example.com" } }, "https://allowed.example.com"),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(publishOutboxEventMock).toHaveBeenCalledTimes(1);
|
||||
const call = publishOutboxEventMock.mock.calls[0]!;
|
||||
const args = call[1] as {
|
||||
tenantId: string;
|
||||
type: string;
|
||||
dispatchKey: string;
|
||||
payload: {
|
||||
submissionId: string;
|
||||
formId: string;
|
||||
targetType: string;
|
||||
notifyEmails: string[];
|
||||
values: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
expect(args.type).toBe("form.submission.process");
|
||||
expect(args.tenantId).toBe("t_1");
|
||||
expect(args.dispatchKey).toBe("form-submission:sub_1");
|
||||
expect(args.payload.submissionId).toBe("sub_1");
|
||||
expect(args.payload.formId).toBe("form_1");
|
||||
expect(args.payload.targetType).toBe("generic");
|
||||
expect(args.payload.notifyEmails).toEqual(["ops@example.com"]);
|
||||
expect(args.payload.values).toEqual({ email: "alice@example.com" });
|
||||
});
|
||||
|
||||
it("returns 405 on GET", async () => {
|
||||
const action = await loadAction();
|
||||
const req = new Request("https://forms.example.com/api/embed/forms/key_1/submit", { method: "GET" });
|
||||
const res = await action({ params: { formKey: "key_1" }, request: req });
|
||||
expect(res.status).toBe(405);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user