← back to Dw Photo Capture
test-allshopify.js
114 lines
#!/usr/bin/env node
/**
* End-to-end test for the "All Shopify" search + keep/replace photo paths.
* Self-contained: creates a throwaway DRAFT product (never published — no price
* means the activation gate can't pass it), exercises the real server endpoints,
* verifies image counts, then DELETES the test product. No real catalog product
* is ever touched. Run: node test-allshopify.js
*/
const https = require('https');
const fs = require('fs');
const path = require('path');
const BASE = 'http://127.0.0.1:9890';
const AUTH = 'Basic ' + Buffer.from('admin:DW2024!').toString('base64');
const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const API = '2024-10';
let TOKEN = process.env.SHOPIFY_ADMIN_TOKEN || '';
if (!TOKEN) for (const l of fs.readFileSync(path.join(process.env.HOME, 'Projects/secrets-manager/.env'), 'utf8').split('\n'))
if (l.startsWith('SHOPIFY_ADMIN_TOKEN=')) { TOKEN = l.split('=').slice(1).join('=').trim(); break; }
// 1x1 JPEG — content is irrelevant; we only assert image COUNTS change.
const JPG = '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAj/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=';
const DATAURL = 'data:image/jpeg;base64,' + JPG;
function shopify(method, p, payload) {
return new Promise((resolve) => {
const data = payload ? JSON.stringify(payload) : null;
const req = https.request({ host: SHOP, path: `/admin/api/${API}${p}`, method,
headers: Object.assign({ 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
data ? { 'Content-Length': Buffer.byteLength(data) } : {}) }, (res) => {
let d = ''; res.on('data', c => d += c); res.on('end', () => { let b = null; try { b = JSON.parse(d); } catch (e) {} resolve({ status: res.statusCode, body: b }); });
});
req.on('error', e => resolve({ status: 0, err: e.message }));
if (data) req.write(data); req.end();
});
}
function srv(method, p, payload) {
return new Promise((resolve) => {
const http = require('http'); const data = payload ? JSON.stringify(payload) : null;
const req = http.request(BASE + p, { method, headers: Object.assign({ Authorization: AUTH, 'Content-Type': 'application/json' }, data ? { 'Content-Length': Buffer.byteLength(data) } : {}) }, (res) => {
let d = ''; res.on('data', c => d += c); res.on('end', () => { let b = null; try { b = JSON.parse(d); } catch (e) {} resolve({ status: res.statusCode, body: b }); });
});
req.on('error', e => resolve({ status: 0, err: e.message }));
if (data) req.write(data); req.end();
});
}
const imgCount = async (pid) => ((await shopify('GET', `/products/${pid}/images.json`)).body.images || []).length;
let pass = 0, fail = 0;
const ok = (c, m) => { (c ? pass++ : fail++); console.log(` ${c ? '✓' : '✗ FAIL'} ${m}`); };
(async () => {
const TAG = 'ZZTEST-PHOTOCAP-' + process.pid;
console.log(`\n── Setup: throwaway DRAFT product (${TAG}) ──`);
const created = await shopify('POST', '/products.json', { product: {
title: 'ZZ TEST PhotoCapture DELETE ME', vendor: 'ZZTESTVENDOR', status: 'draft', published: false,
variants: [{ sku: TAG, price: '0.00' }], // price 0 → activation gate cannot pass → never goes live
images: [{ attachment: JPG }, { attachment: JPG }] // two pre-existing "professional" images
} });
const pid = created.body && created.body.product && created.body.product.id;
if (!pid) { console.log(' could not create test product:', JSON.stringify(created.body)); process.exit(1); }
ok(await imgCount(pid) === 2, `seeded with 2 existing images`);
try {
console.log(`\n── Test 1: live store-wide search finds it (retry for index lag) ──`);
// Shopify product-search is eventually consistent — a just-created product needs
// a few seconds to enter the search index. Retry with backoff before asserting.
let s, hit, waited = 0;
for (let i = 0; i < 8; i++) {
s = await srv('GET', '/api/shopify-search?q=' + encodeURIComponent(TAG));
hit = (s.body.items || []).find(x => x.dw_sku === TAG);
if (hit) { console.log(` (indexed after ~${waited}s)`); break; }
await new Promise(r => setTimeout(r, 5000)); waited += 5;
}
ok(!!hit, `/api/shopify-search returns the product by SKU`);
ok(hit && hit.keep_images === true, `result is stamped keep_images:true (non-destructive default)`);
ok(hit && hit.scope === 'shopify', `result scope = "shopify"`);
console.log(`\n── Test 2: KEEP path (default) preserves existing imagery ──`);
const k = await srv('POST', '/api/photo', { dw_sku: TAG, product_id: pid, dataUrl: DATAURL, keep_images: true });
ok(k.body && k.body.ok, `/api/photo (keep) returned ok`);
ok(await imgCount(pid) === 3, `image count 2 → 3 (new photo ADDED, originals kept)`);
ok(k.body && k.body.live === false, `draft did NOT auto-activate (no price) — gate held`);
console.log(`\n── Test 3: REPLACE path (armed toggle) wipes to one ──`);
const r = await srv('POST', '/api/photo', { dw_sku: TAG, product_id: pid, dataUrl: DATAURL, keep_images: false });
ok(r.body && r.body.ok, `/api/photo (replace) returned ok`);
ok(await imgCount(pid) === 1, `image count 3 → 1 (replace-all wiped siblings)`);
console.log(`\n── Test 4: MULTI add (batch /api/photos) appends, keeps featured ──`);
const feat0 = ((await shopify('GET', `/products/${pid}/images.json`)).body.images.find(i => i.position === 1) || {}).id;
const m = await srv('POST', '/api/photos', { dw_sku: TAG, product_id: pid, dataUrls: [DATAURL, DATAURL, DATAURL], meta: { title: 'ZZ TEST', product_id: pid } });
ok(m.body && m.body.ok && m.body.added === 3, `/api/photos added 3 of 3 (got ${m.body && m.body.added})`);
ok(await imgCount(pid) === 4, `image count 1 → 4 (3 appended, original kept)`);
const feat1 = ((await shopify('GET', `/products/${pid}/images.json`)).body.images.find(i => i.position === 1) || {}).id;
ok(feat0 && feat0 === feat1, `featured image UNCHANGED by batch add (append, not re-feature)`);
} finally {
console.log(`\n── Teardown: delete the throwaway product + local residue ──`);
const del = await shopify('DELETE', `/products/${pid}.json`);
ok(del.status >= 200 && del.status < 300, `test product deleted (HTTP ${del.status})`);
// scrub local photo files; the progress.json key is owned by the LIVE server's
// in-memory state, so an external file edit gets re-flushed until the server
// reloads — the harmless ZZTEST key clears on the next dwphoto restart.
try {
const PF = path.join(__dirname, 'data/progress.json'); const pr = JSON.parse(fs.readFileSync(PF, 'utf8'));
if (pr[TAG]) { delete pr[TAG]; fs.writeFileSync(PF, JSON.stringify(pr, null, 2)); }
for (const f of fs.readdirSync(path.join(__dirname, 'photos'))) if (f.startsWith(TAG)) fs.unlinkSync(path.join(__dirname, 'photos', f));
ok(true, `photos scrubbed (progress key clears on next server restart — harmless, not in worklist)`);
} catch (e) { ok(false, `cleanup error: ${e.message}`); }
}
console.log(`\n══ ${pass} passed, ${fail} failed ══\n`);
process.exit(fail ? 1 : 0);
})();