security hardening

This commit is contained in:
Gerhard Scheikl
2026-05-31 09:35:31 +02:00
parent d7d437a871
commit 01b4734477
31 changed files with 1234 additions and 238 deletions
+77 -11
View File
@@ -2,7 +2,7 @@ import { strict as assert } from "node:assert";
import { describe, it } from "node:test";
import { pickLanguage } from "../app/services/invoice/i18n";
import { isDuplicateWebhook, type DedupeDeps } from "../app/services/webhooks/dedupe.server";
import { reserveWebhook, type DedupeDeps } from "../app/services/webhooks/dedupe.server";
describe("pickLanguage", () => {
it("returns 'de' only for explicit German locales", () => {
@@ -38,8 +38,21 @@ function makeRequest(headers: Record<string, string> = {}): Request {
});
}
function makeDeps(behaviour: "ok" | "p2002" | "boom"): DedupeDeps {
type ExistingRow = { webhookId: string; status: string; receivedAt: Date } | null;
/**
* Build a DedupeDeps stub.
* - "ok" : create() succeeds (fresh reservation).
* - "p2002" : create() conflicts; findUnique() returns `existing`.
* - "boom" : create() throws a non-P2002 error (fail-open).
*/
function makeDeps(
behaviour: "ok" | "p2002" | "boom",
existing: ExistingRow = null,
): DedupeDeps & { calls: { commit: number; release: number; update: number } } {
const calls = { commit: 0, release: 0, update: 0 };
return {
calls,
db: {
processedWebhook: {
create: async () => {
@@ -51,29 +64,82 @@ function makeDeps(behaviour: "ok" | "p2002" | "boom"): DedupeDeps {
}
throw new Error("DB unavailable");
},
findUnique: async () => existing,
update: async () => {
calls.update += 1;
calls.commit += 1;
return {};
},
delete: async () => {
calls.release += 1;
return {};
},
},
},
};
}
describe("isDuplicateWebhook", () => {
it("returns false on first delivery (insert succeeds)", async () => {
describe("reserveWebhook", () => {
it("returns a reservation on first delivery (insert succeeds)", async () => {
const req = makeRequest({ "x-shopify-webhook-id": "abc-123" });
assert.equal(await isDuplicateWebhook(req, "shop.myshopify.com", "ORDERS_CREATE", makeDeps("ok")), false);
const res = await reserveWebhook(req, "shop.myshopify.com", "ORDERS_CREATE", makeDeps("ok"));
assert.ok(res, "expected a reservation");
assert.equal(res!.webhookId, "abc-123");
});
it("returns true on a retried delivery (P2002)", async () => {
it("returns null for an already-processed (done) delivery", async () => {
const req = makeRequest({ "x-shopify-webhook-id": "abc-123" });
assert.equal(await isDuplicateWebhook(req, "shop.myshopify.com", "ORDERS_CREATE", makeDeps("p2002")), true);
const deps = makeDeps("p2002", {
webhookId: "abc-123",
status: "done",
receivedAt: new Date(),
});
assert.equal(await reserveWebhook(req, "shop.myshopify.com", "ORDERS_CREATE", deps), null);
});
it("returns false (and proceeds) when the dedupe table itself errors \u2014 fail-open, never drop a webhook silently", async () => {
it("returns null for a fresh in-flight (processing) delivery", async () => {
const req = makeRequest({ "x-shopify-webhook-id": "abc-123" });
assert.equal(await isDuplicateWebhook(req, "shop.myshopify.com", "ORDERS_CREATE", makeDeps("boom")), false);
const deps = makeDeps("p2002", {
webhookId: "abc-123",
status: "processing",
receivedAt: new Date(), // fresh lease
});
assert.equal(await reserveWebhook(req, "shop.myshopify.com", "ORDERS_CREATE", deps), null);
});
it("returns false when the X-Shopify-Webhook-Id header is missing (test harness / non-Shopify caller)", async () => {
it("reclaims a stale (crashed) processing reservation", async () => {
const req = makeRequest({ "x-shopify-webhook-id": "abc-123" });
const deps = makeDeps("p2002", {
webhookId: "abc-123",
status: "processing",
receivedAt: new Date(Date.now() - 10 * 60 * 1000), // 10 min ago > 5 min lease
});
const res = await reserveWebhook(req, "shop.myshopify.com", "ORDERS_CREATE", deps);
assert.ok(res, "expected to reclaim the stale reservation");
assert.equal(deps.calls.update, 1, "stale reclaim should renew the lease via update()");
});
it("commit() flips the row to done; release() deletes it", async () => {
const req = makeRequest({ "x-shopify-webhook-id": "abc-123" });
const deps = makeDeps("ok");
const res = await reserveWebhook(req, "shop.myshopify.com", "ORDERS_CREATE", deps);
await res!.commit();
assert.equal(deps.calls.commit, 1);
await res!.release();
assert.equal(deps.calls.release, 1);
});
it("fails open (returns a no-op reservation) when the dedupe table errors", async () => {
const req = makeRequest({ "x-shopify-webhook-id": "abc-123" });
const res = await reserveWebhook(req, "shop.myshopify.com", "ORDERS_CREATE", makeDeps("boom"));
assert.ok(res, "fail-open must still process — never silently drop a webhook");
assert.equal(res!.webhookId, "abc-123");
});
it("returns a no-op reservation when the X-Shopify-Webhook-Id header is missing", async () => {
const req = makeRequest();
assert.equal(await isDuplicateWebhook(req, "shop.myshopify.com", "ORDERS_CREATE", makeDeps("ok")), false);
const res = await reserveWebhook(req, "shop.myshopify.com", "ORDERS_CREATE", makeDeps("ok"));
assert.ok(res, "missing id => process without dedupe");
assert.equal(res!.webhookId, null);
});
});