← 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
A tests/marketplace/textures.test.js
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 →