CI (SIAX Cloud) / quality (push) Successful in 51s
Security Scan (SIAX Cloud) / security (push) Successful in 26s
CI (SIAX Cloud) / security (push) Successful in 28s
CI (SIAX Cloud) / integration (push) Successful in 1m1s
CI (SIAX Cloud) / integration (pull_request) Successful in 30s
CI (SIAX Cloud) / security (pull_request) Successful in 24s
CI (SIAX Cloud) / quality (pull_request) Successful in 48s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 22s
Tenant isolation (security-critical): every app/api/0pt/** route reading
tenant_id from client-controlled query/body never cross-checked it against
the token middleware.ts already verified (x-0pt-verified-tenant). Adds a
shared assertTenantMatchesVerified guard and wires it into all 20+ affected
routes. Also closes two full-leak-by-omission bugs found in this pass
(documents/capture and lakehouse/mappings GETs returned every tenant's rows
when tenant_id was simply omitted) and two "no tenant check at all" reads
(rate-policies/[id], lakehouse/mappings/[id]) reachable by ID alone.
Duplicate-delivery idempotency (correctness-critical): POST /api/orders now
honors Idempotency-Key (sql/006, replay-or-409-on-conflict, Postgres +
in-memory); the Python orchestrator's POST /orders/{id}/run — the actual
delivery trigger — is now idempotent under an asyncio lock, closing a real
concurrent-request race, not just the sequential-retry case.
Engine-duplication resolution: adaptive_router.py's docstring wrongly
invited a bypass ("see b0ts for scraping") contradicting the product's own
compliance stance; corrected, and given real (additive, not escalating)
block-signal tracking so a rate-limited/blocked source is excluded from the
plan instead of hammered. Also fixes a previously-undiscovered production
bug: the Python engine's only call into the TS quality bridge
(X-Internal-Token) was unconditionally 401'd by middleware.ts, which never
checked that credential — the advisory quality bridge was unreachable in any
environment enforcing auth.
Restore drill: executed for real against throwaway Docker Postgres
containers (not simulated). Found and fixed two real blockers along the way:
sql/004 referenced opt_datasets, which no migration ever created (chain was
never actually runnable end to end); lib/db.ts hardcoded ssl:"require",
making any non-TLS Postgres (incl. compose.dev.yaml's own local-dev
container) unreachable. After both fixes: full migration chain applies
cleanly, pg_dump/pg_restore round-trip is byte-identical (row counts +
content checksums), and real application code (lib/orders-store.ts,
SourceRegistryService's Postgres repository) reads the restored data back
correctly, including cross-tenant isolation still holding.
82 new tests (55 TS + 27 Python), all passing; full existing suite (291 TS +
55 Python) still green; typecheck clean on every changed/added file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
85 lines
4.1 KiB
TypeScript
85 lines
4.1 KiB
TypeScript
/**
|
|
* P014: lib/orders-store.ts previously had ZERO test coverage and no
|
|
* idempotency mechanism -- a retried POST /api/orders created a second,
|
|
* indistinguishable row every time (recon: "Duplicate delivery idempotent:
|
|
* NOT MET"). These tests exercise the in-memory fallback path directly
|
|
* (no DATABASE_URL is set in this test environment, matching every other
|
|
* test in this repo) and prove: a retry under the same Idempotency-Key
|
|
* replays the original order; a reused key with a different submission is
|
|
* refused as a conflict, not silently merged; and omitting the key
|
|
* preserves the exact pre-P014 "always create a new order" behavior.
|
|
*/
|
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
import {
|
|
createOrder,
|
|
findOrderByIdempotencyKey,
|
|
IdempotencyKeyConflictError,
|
|
type OrderConfig,
|
|
} from "@/lib/orders-store";
|
|
|
|
const CONFIG_A: OrderConfig = { markets: ["SE"], type: "Bolag", fieldIds: ["identity"], volume: 1000 };
|
|
const CONFIG_B: OrderConfig = { markets: ["NO"], type: "Bolag", fieldIds: ["identity"], volume: 5000 };
|
|
|
|
describe("createOrder idempotency (in-memory fallback)", () => {
|
|
it("no Idempotency-Key: every call creates a distinct order (unchanged pre-P014 behavior)", async () => {
|
|
const first = await createOrder("custom", CONFIG_A);
|
|
const second = await createOrder("custom", CONFIG_A);
|
|
expect(first.order.order_id).not.toBe(second.order.order_id);
|
|
expect(first.replayed).toBe(false);
|
|
expect(second.replayed).toBe(false);
|
|
});
|
|
|
|
it("same Idempotency-Key + same (flow, config): the second call replays the first order, no duplicate row", async () => {
|
|
const key = `idem_${crypto.randomUUID()}`;
|
|
const first = await createOrder("custom", CONFIG_A, key);
|
|
const second = await createOrder("custom", CONFIG_A, key);
|
|
|
|
expect(first.replayed).toBe(false);
|
|
expect(second.replayed).toBe(true);
|
|
expect(second.order.order_id).toBe(first.order.order_id);
|
|
expect(second.order.created_at).toBe(first.order.created_at);
|
|
});
|
|
|
|
it("retrying the SAME key three times still yields exactly one underlying order", async () => {
|
|
const key = `idem_${crypto.randomUUID()}`;
|
|
const a = await createOrder("custom", CONFIG_A, key);
|
|
const b = await createOrder("custom", CONFIG_A, key);
|
|
const c = await createOrder("custom", CONFIG_A, key);
|
|
const ids = new Set([a.order.order_id, b.order.order_id, c.order.order_id]);
|
|
expect(ids.size).toBe(1);
|
|
});
|
|
|
|
it("same Idempotency-Key reused with a DIFFERENT config: refused as a conflict, not silently merged", async () => {
|
|
const key = `idem_${crypto.randomUUID()}`;
|
|
await createOrder("custom", CONFIG_A, key);
|
|
await expect(createOrder("custom", CONFIG_B, key)).rejects.toBeInstanceOf(IdempotencyKeyConflictError);
|
|
});
|
|
|
|
it("same Idempotency-Key reused with a DIFFERENT flow: refused as a conflict", async () => {
|
|
const key = `idem_${crypto.randomUUID()}`;
|
|
await createOrder("custom", CONFIG_A, key);
|
|
await expect(createOrder("subscription", CONFIG_A, key)).rejects.toBeInstanceOf(IdempotencyKeyConflictError);
|
|
});
|
|
|
|
it("a conflicting retry does not mutate or replace the original order", async () => {
|
|
const key = `idem_${crypto.randomUUID()}`;
|
|
const original = await createOrder("custom", CONFIG_A, key);
|
|
await expect(createOrder("custom", CONFIG_B, key)).rejects.toThrow();
|
|
const stillThere = await findOrderByIdempotencyKey(key);
|
|
expect(stillThere?.order_id).toBe(original.order.order_id);
|
|
expect(stillThere?.config).toEqual(CONFIG_A);
|
|
});
|
|
|
|
it("different Idempotency-Keys for the same config create independent orders", async () => {
|
|
const a = await createOrder("custom", CONFIG_A, `idem_${crypto.randomUUID()}`);
|
|
const b = await createOrder("custom", CONFIG_A, `idem_${crypto.randomUUID()}`);
|
|
expect(a.order.order_id).not.toBe(b.order.order_id);
|
|
});
|
|
|
|
it("an empty-string Idempotency-Key is treated as absent (no dedup, no crash)", async () => {
|
|
const a = await createOrder("custom", CONFIG_A, "");
|
|
const b = await createOrder("custom", CONFIG_A, " ");
|
|
expect(a.order.order_id).not.toBe(b.order.order_id);
|
|
});
|
|
});
|