146 lines
5.4 KiB
TypeScript
146 lines
5.4 KiB
TypeScript
import { strict as assert } from "node:assert";
|
|
import { describe, it } from "node:test";
|
|
|
|
import { pickLanguage } from "../app/services/invoice/i18n";
|
|
import { reserveWebhook, type DedupeDeps } from "../app/services/webhooks/dedupe.server";
|
|
|
|
describe("pickLanguage", () => {
|
|
it("returns 'de' only for explicit German locales", () => {
|
|
assert.equal(pickLanguage("de"), "de");
|
|
assert.equal(pickLanguage("de-AT"), "de");
|
|
assert.equal(pickLanguage("de-DE"), "de");
|
|
assert.equal(pickLanguage("de_CH"), "de");
|
|
assert.equal(pickLanguage("DE-AT"), "de"); // case-insensitive
|
|
});
|
|
|
|
it("returns 'en' for non-German locales (regression: it/fr/es no longer fall back to de)", () => {
|
|
assert.equal(pickLanguage("en"), "en");
|
|
assert.equal(pickLanguage("en-US"), "en");
|
|
assert.equal(pickLanguage("it"), "en");
|
|
assert.equal(pickLanguage("it-IT"), "en");
|
|
assert.equal(pickLanguage("fr"), "en");
|
|
assert.equal(pickLanguage("fr-FR"), "en");
|
|
assert.equal(pickLanguage("es"), "en");
|
|
assert.equal(pickLanguage("hu-HU"), "en");
|
|
});
|
|
|
|
it("falls back to 'de' for empty/unknown input so the per-shop default chain still works", () => {
|
|
assert.equal(pickLanguage(undefined), "de");
|
|
assert.equal(pickLanguage(null), "de");
|
|
assert.equal(pickLanguage(""), "de");
|
|
});
|
|
});
|
|
|
|
function makeRequest(headers: Record<string, string> = {}): Request {
|
|
return new Request("https://example.com/webhooks/test", {
|
|
method: "POST",
|
|
headers,
|
|
});
|
|
}
|
|
|
|
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 () => {
|
|
if (behaviour === "ok") return {};
|
|
if (behaviour === "p2002") {
|
|
const err = new Error("Unique constraint failed") as Error & { code?: string };
|
|
err.code = "P2002";
|
|
throw err;
|
|
}
|
|
throw new Error("DB unavailable");
|
|
},
|
|
findUnique: async () => existing,
|
|
update: async () => {
|
|
calls.update += 1;
|
|
calls.commit += 1;
|
|
return {};
|
|
},
|
|
delete: async () => {
|
|
calls.release += 1;
|
|
return {};
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("reserveWebhook", () => {
|
|
it("returns a reservation on first delivery (insert succeeds)", async () => {
|
|
const req = makeRequest({ "x-shopify-webhook-id": "abc-123" });
|
|
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 null for an already-processed (done) delivery", async () => {
|
|
const req = makeRequest({ "x-shopify-webhook-id": "abc-123" });
|
|
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 null for a fresh in-flight (processing) delivery", async () => {
|
|
const req = makeRequest({ "x-shopify-webhook-id": "abc-123" });
|
|
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("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();
|
|
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);
|
|
});
|
|
});
|