[object Object]

← back to Wallco Ai

marketplace: tests for /api/marketplace/textures (9/9 pass live)

d6036b878013d01b618e4a96967097fef73c8a3e · 2026-05-13 15:12:43 -0700 · SteveStudio2

- groups[] shape + 11-category whitelist + per-category cap (≤30)
- absolute https image_url validation
- single-SKU lookup (200 real + 404 bogus)
- colorways route accepts textureSku + textureImageUrl (mock mode, no Gemini burn)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Files touched

Diff

commit d6036b878013d01b618e4a96967097fef73c8a3e
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 13 15:12:43 2026 -0700

    marketplace: tests for /api/marketplace/textures (9/9 pass live)
    
    - groups[] shape + 11-category whitelist + per-category cap (≤30)
    - absolute https image_url validation
    - single-SKU lookup (200 real + 404 bogus)
    - colorways route accepts textureSku + textureImageUrl (mock mode, no Gemini burn)
    
    Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---
 tests/marketplace/textures.test.js | 153 +++++++++++++++++++++++++++++++++++++
 1 file changed, 153 insertions(+)

diff --git a/tests/marketplace/textures.test.js b/tests/marketplace/textures.test.js
new file mode 100644
index 0000000..6426643
--- /dev/null
+++ b/tests/marketplace/textures.test.js
@@ -0,0 +1,153 @@
+// Tests for /api/marketplace/textures + textureSku grounding on colorways.
+//
+//   node tests/marketplace/textures.test.js
+//
+// Only the cheap paths run by default. The actual Gemini-with-texture call is
+// covered by ai-colorways.test.js under WALLCO_TEST_REAL=1.
+//
+// What this verifies:
+//   1. /api/marketplace/textures returns the 11-category grouping with real
+//      shopify_products rows.
+//   2. /api/marketplace/textures/:sku resolves a real SKU and 404s on a bogus one.
+//   3. The colorways endpoint accepts textureSku + textureImageUrl in mock mode
+//      without 4xx-ing (route plumbing check).
+
+'use strict';
+const assert = require('assert');
+const http = require('http');
+
+const HOST = process.env.WALLCO_HOST || '127.0.0.1';
+const PORT = process.env.WALLCO_PORT || 9792;
+
+function req(method, urlPath, body) {
+  return new Promise((resolve, reject) => {
+    const opts = { hostname: HOST, port: PORT, path: urlPath, method, headers: { 'content-type': 'application/json' }, timeout: 60_000 };
+    const r = http.request(opts, (res) => {
+      let buf = '';
+      res.on('data', (c) => (buf += c));
+      res.on('end', () => resolve({ status: res.statusCode, body: buf, json: tryJson(buf) }));
+    });
+    r.on('error', reject);
+    r.on('timeout', () => { r.destroy(new Error('timeout')); });
+    if (body) r.write(JSON.stringify(body));
+    r.end();
+  });
+}
+function tryJson(s) { try { return JSON.parse(s); } catch (_) { return null; } }
+
+const tests = [];
+function t(name, fn) { tests.push({ name, fn }); }
+
+const EXPECTED_KEYS = ['glass-bead','flocked','cork','raffia','silk','sisal','jute','seagrass','paperweave','linen','grasscloth'];
+
+// ── /api/marketplace/textures ────────────────────────────────────────────────
+let firstSku = null;
+
+t('GET /api/marketplace/textures → 200 with groups[]', async () => {
+  const r = await req('GET', '/api/marketplace/textures');
+  assert.strictEqual(r.status, 200);
+  assert.strictEqual(r.json.ok, true);
+  assert.ok(Number.isInteger(r.json.total) && r.json.total > 0, 'total should be positive int');
+  assert.ok(Array.isArray(r.json.groups) && r.json.groups.length > 0, 'groups[] expected');
+});
+
+t('groups use the canonical 11-category key set', async () => {
+  const r = await req('GET', '/api/marketplace/textures');
+  const keys = r.json.groups.map(g => g.key);
+  for (const k of keys) {
+    assert.ok(EXPECTED_KEYS.includes(k), `unknown category key: ${k}`);
+  }
+  // We expect at least a few of these to actually have products.
+  assert.ok(keys.length >= 3, `only ${keys.length} categories returned`);
+});
+
+t('every group has key, label, blurb, count, textures[] shape', async () => {
+  const r = await req('GET', '/api/marketplace/textures');
+  for (const g of r.json.groups) {
+    assert.ok(g.key && typeof g.key === 'string', 'group.key required');
+    assert.ok(g.label && typeof g.label === 'string', 'group.label required');
+    assert.ok(g.blurb && typeof g.blurb === 'string', 'group.blurb required');
+    assert.ok(Number.isInteger(g.count) && g.count === g.textures.length, `count mismatch on ${g.key}`);
+    assert.ok(Array.isArray(g.textures), `${g.key}.textures must be array`);
+    for (const tex of g.textures) {
+      assert.ok(tex.sku, 'texture.sku required');
+      assert.ok(tex.title, 'texture.title required');
+      assert.ok(tex.image_url, 'texture.image_url required');
+      assert.strictEqual(tex.category, g.key, `texture.category should match group.key (${g.key})`);
+      if (!firstSku) firstSku = tex.sku;
+    }
+  }
+});
+
+t('count is capped per category (default 30)', async () => {
+  const r = await req('GET', '/api/marketplace/textures');
+  for (const g of r.json.groups) {
+    assert.ok(g.count <= 30, `${g.key}: count ${g.count} exceeds cap`);
+  }
+});
+
+t('every texture image_url is an absolute https URL', async () => {
+  const r = await req('GET', '/api/marketplace/textures');
+  for (const g of r.json.groups) {
+    for (const tex of g.textures) {
+      assert.ok(/^https?:\/\//.test(tex.image_url), `bad image_url on ${tex.sku}: ${tex.image_url}`);
+    }
+  }
+});
+
+// ── /api/marketplace/textures/:sku ───────────────────────────────────────────
+t('GET /api/marketplace/textures/:sku → 200 for a real SKU', async () => {
+  // Wait until firstSku was captured by the earlier test.
+  assert.ok(firstSku, 'first SKU was not captured from groups[]');
+  const r = await req('GET', `/api/marketplace/textures/${encodeURIComponent(firstSku)}`);
+  assert.strictEqual(r.status, 200, `expected 200, got ${r.status}: ${r.body.slice(0, 200)}`);
+  assert.ok(r.json.texture, 'texture object expected');
+  assert.strictEqual(r.json.texture.sku, firstSku);
+  assert.ok(r.json.texture.image_url);
+  assert.ok(r.json.texture.category);
+});
+
+t('GET /api/marketplace/textures/:sku → 404 for a bogus SKU', async () => {
+  const r = await req('GET', '/api/marketplace/textures/DOES-NOT-EXIST-xyzzy-999');
+  assert.strictEqual(r.status, 404);
+  assert.ok(r.json && r.json.error, 'error message expected');
+});
+
+// ── Colorways endpoint accepts textureSku + textureImageUrl (mock mode) ──────
+t('POST mock with textureSku → 200 (route accepts the param)', async () => {
+  const r = await req('POST', '/api/marketplace/ai/generate-colorways', {
+    patternSlug: 'wildflower-reverie-astrid-mauve',
+    mock: true,
+    textureSku: firstSku || 'any-sku',
+  });
+  assert.strictEqual(r.status, 200, `expected 200, got ${r.status}: ${r.body.slice(0, 200)}`);
+  assert.strictEqual(r.json.status, 'mock');
+  assert.ok(Array.isArray(r.json.colorways) && r.json.colorways.length === 6);
+});
+
+t('POST mock with textureImageUrl → 200 (route accepts the param)', async () => {
+  const r = await req('POST', '/api/marketplace/ai/generate-colorways', {
+    patternSlug: 'wildflower-reverie-astrid-mauve',
+    mock: true,
+    textureImageUrl: 'https://example.com/test.jpg',
+  });
+  assert.strictEqual(r.status, 200);
+  assert.strictEqual(r.json.status, 'mock');
+});
+
+(async () => {
+  console.log(`Running ${tests.length} textures test(s) against ${HOST}:${PORT}\n`);
+  let pass = 0, fail = 0;
+  for (const test of tests) {
+    try {
+      await test.fn();
+      console.log('✓', test.name);
+      pass++;
+    } catch (err) {
+      console.error('✗', test.name, '—', err.message);
+      fail++;
+    }
+  }
+  console.log(`\n${pass}/${tests.length} textures tests passed`);
+  if (fail) process.exit(1);
+})();

← ebf7abd marketplace: real-product texture picker + stripe/plaid gene  ·  back to Wallco Ai  ·  marketplace YOLO loop · overnight milestone 3 — all 8 parall 9ff4a1a →