Selaa lähdekoodia

Require an exact match when falling back to order search

WooCommerce's ?search= is fuzzy, so looking up a non-existent order number
returned an unrelated order, which the ownership check then reported as an
e-mail mismatch. The agent would tell the customer their address does not match
an order that never existed. Accept a search hit only when its number or id is
the one that was requested, otherwise ORDER_NOT_FOUND.

Adds WooClient tests covering the fallback, hidden products and unpublished
products.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvzjhjUY7tr5Naj1Co4N6B
Maciek 4 viikkoa sitten
vanhempi
commit
f36fc3d2e0
2 muutettua tiedostoa jossa 112 lisäystä ja 2 poistoa
  1. 12 2
      src/clients/wooClient.ts
  2. 100 0
      tests/wooClient.test.ts

+ 12 - 2
src/clients/wooClient.ts

@@ -73,10 +73,20 @@ export class WooClient {
         // Custom order-number plugins break the direct fetch — fall back to search.
         const results = await this.get<WooOrder[]>('orders', {
           search: opts.orderNumber,
-          per_page: 1,
+          per_page: 5,
           ...currencyParam,
         });
-        if (Array.isArray(results) && results.length > 0) return results[0] as WooOrder;
+        // Woo's `search` is fuzzy and happily returns unrelated orders, so only
+        // accept a hit whose number/id really is the one that was asked for.
+        // Otherwise a bogus order number would come back as somebody else's
+        // order and be reported as an e-mail mismatch instead of "not found".
+        const wanted = String(opts.orderNumber).trim().toLowerCase();
+        const exact = (Array.isArray(results) ? results : []).find(
+          (o) =>
+            String(o.number ?? '').trim().toLowerCase() === wanted ||
+            String(o.id ?? '').trim().toLowerCase() === wanted,
+        );
+        if (exact) return exact;
         throw notFound('ORDER_NOT_FOUND', `Order #${opts.orderNumber} not found.`);
       }
     }

+ 100 - 0
tests/wooClient.test.ts

@@ -0,0 +1,100 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { applyTestConfig } from './helpers/testConfig.js';
+import { WooClient } from '../src/clients/wooClient.js';
+import { RelayError } from '../src/errors.js';
+
+/**
+ * Exercises the order-number fallback path against a stubbed fetch, because the
+ * fuzzy behaviour of Woo's `search` is exactly what used to make a bogus order
+ * number look like somebody else's order.
+ */
+function stubFetch(handler: (url: string) => { status: number; body: unknown }) {
+  const original = globalThis.fetch;
+  globalThis.fetch = (async (input: string | URL | Request) => {
+    const url = typeof input === 'string' ? input : input.toString();
+    const { status, body } = handler(url);
+    return new Response(JSON.stringify(body), {
+      status,
+      headers: { 'Content-Type': 'application/json' },
+    });
+  }) as typeof globalThis.fetch;
+  return () => {
+    globalThis.fetch = original;
+  };
+}
+
+test('a direct order fetch is returned as-is', async () => {
+  applyTestConfig();
+  const restore = stubFetch(() => ({ status: 200, body: { id: 555, number: '555' } }));
+  try {
+    const order = await new WooClient().getOrder({ orderNumber: '555' });
+    assert.equal(order.id, 555);
+  } finally {
+    restore();
+  }
+});
+
+test('the search fallback accepts only an exact order-number match', async () => {
+  applyTestConfig();
+  const restore = stubFetch((url) => {
+    if (url.includes('/orders/12345')) return { status: 404, body: { message: 'nope' } };
+    // Woo's fuzzy search returns an unrelated order.
+    return { status: 200, body: [{ id: 987, number: '987' }] };
+  });
+  try {
+    await new WooClient().getOrder({ orderNumber: '12345' });
+    assert.fail('expected ORDER_NOT_FOUND');
+  } catch (err) {
+    assert.ok(err instanceof RelayError);
+    assert.equal(err.code, 'ORDER_NOT_FOUND');
+  } finally {
+    restore();
+  }
+});
+
+test('the search fallback returns a genuine custom-order-number match', async () => {
+  applyTestConfig();
+  const restore = stubFetch((url) => {
+    if (url.includes('/orders/EK-2026-77')) return { status: 404, body: {} };
+    return { status: 200, body: [{ id: 42, number: 'EK-2026-77' }, { id: 43, number: 'EK-2026-78' }] };
+  });
+  try {
+    const order = await new WooClient().getOrder({ orderNumber: 'EK-2026-77' });
+    assert.equal(order.id, 42);
+  } finally {
+    restore();
+  }
+});
+
+test('a hidden product is filtered out of search results', async () => {
+  applyTestConfig();
+  const restore = stubFetch(() => ({
+    status: 200,
+    body: [
+      { id: 1, name: 'Widoczny', catalog_visibility: 'visible' },
+      { id: 2, name: 'Ukryty', catalog_visibility: 'hidden' },
+    ],
+  }));
+  try {
+    const products = await new WooClient().searchProducts('gaz', 5, 'PLN', 'pl');
+    assert.equal(products.length, 1);
+    assert.equal(products[0]?.id, 1);
+  } finally {
+    restore();
+  }
+});
+
+test('an unpublished product is not returned by id', async () => {
+  applyTestConfig();
+  const restore = stubFetch(() => ({ status: 200, body: { id: 9, status: 'draft' } }));
+  try {
+    await new WooClient().getProduct({ productId: 9 });
+    assert.fail('expected PRODUCT_NOT_FOUND');
+  } catch (err) {
+    assert.ok(err instanceof RelayError);
+    assert.equal(err.code, 'PRODUCT_NOT_FOUND');
+  } finally {
+    restore();
+  }
+});