← back to CelebritySignatures
test/wear-artwork-flow.test.mjs
146 lines
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, readFile, writeFile, copyFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawn, execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { createServer } from 'node:net';
import { once } from 'node:events';
import { buildPayload } from '../scripts/submit-pod-draft-printify.mjs';
// Run real draft producers against synthetic payment/artwork responses in an
// isolated project. No project .env, real orders, or external APIs are used.
const ROOT = fileURLToPath(new URL('..', import.meta.url));
const exec = promisify(execFile);
const artwork = 'https://fixture.invalid/signature.svg';
const recipient = { name: 'Fixture Buyer', address: { line1: '1 Fixture Lane',
city: 'Fixture', state: 'CA', country: 'US', postal_code: '90001' } };
const fixtureOrder = id => ({ id, status: 'pending_payment', mode: 'test', provider: 'printify',
email: 'fixture@example.invalid', stripeSession: `cs_test_${id}`, pod_submitted: false,
items: [{ qid: 'QFixture', signature_name: 'Fixture', garment: 'polo', garment_label: 'Polo',
color: 'black', size: 'M', placement: 'left_chest' }] });
const env = { PATH: process.env.PATH, STRIPE_TEST_SECRET_KEY: 'sk_test_fixture',
WEAR_SALES_LIVE: '0', STRIPE_LIVE_ENABLED: '0', WEAR_STRIPE_LIVE_ENABLED: '0' };
async function freePort() {
const socket = createServer();
socket.listen(0, '127.0.0.1');
await once(socket, 'listening');
const port = socket.address().port;
await new Promise(resolve => socket.close(resolve));
return port;
}
test('black-polo draft artwork survives both producers, cache hits, serving, payload, and retries', { timeout: 60000 }, async t => {
const root = await mkdtemp(join(tmpdir(), 'tk10286-artwork-'));
t.after(() => rm(root, { recursive: true, force: true }));
const template = JSON.parse(await readFile(join(ROOT, 'data/wear-templates.json'), 'utf8'));
for (const producer of ['success', 'reconciler']) {
await t.test(producer, async () => {
const work = join(root, producer);
await mkdir(join(work, 'data'), { recursive: true });
await mkdir(join(work, 'scripts'));
await copyFile(join(ROOT, 'server.js'), join(work, 'server.mjs'));
await copyFile(join(ROOT, 'scripts/reconcile-wear-orders.mjs'), join(work, 'scripts/reconcile-wear-orders.mjs'));
const put = (name, data) => writeFile(join(work, 'data', name), JSON.stringify(data));
await put('wear-templates.json', template);
await put('wear-orders.json', [fixtureOrder(91001), fixtureOrder(91002)]);
await put('celebrity_signatures.json', [{ wikidata: 'https://www.wikidata.org/wiki/QFixture',
full_name: 'Fixture', category: 'Politics', signature_image_url: artwork,
usable_in_commercial_collage: 'yes', risk_level: 'low', death_date: '1800-01-01' }]);
const interceptor = join(work, 'intercept.mjs');
await writeFile(interceptor, `
import { appendFileSync } from 'node:fs';
globalThis.fetch = async (input, options = {}) => {
const url = String(input), method = options.method || 'GET';
if (method !== 'GET') throw new Error('External mutation blocked: ' + method);
appendFileSync(${JSON.stringify(join(work, 'requests.jsonl'))}, JSON.stringify({url,method})+'\\n');
if (url === ${JSON.stringify(artwork)}) return new Response('<svg xmlns="http://www.w3.org/2000/svg" width="20" height="10"><rect x="5" y="3" width="10" height="4" fill="black"/></svg>');
const m = /^https:\\/\\/api\\.stripe\\.com\\/v1\\/checkout\\/sessions\\/cs_test_(91001|91002)$/.exec(url);
if (m) return Response.json({ id: 'cs_test_'+m[1], status:'complete', payment_status:'paid', metadata:{order_id:m[1]}, shipping_details:${JSON.stringify(recipient)} });
throw new Error('External network blocked: ' + url);
};
`);
const reconcile = () => exec(process.execPath, ['--import', interceptor,
join(work, 'scripts/reconcile-wear-orders.mjs'), '--apply'], { env, timeout: 15000 });
if (producer === 'reconciler') await reconcile();
const port = await freePort();
const child = spawn(process.execPath, ['--import', interceptor, join(work, 'server.mjs')],
{ env: { ...env, PORT: String(port) }, stdio: ['ignore', 'pipe', 'pipe'] });
let stderr = '';
child.stderr.on('data', c => { stderr += c; });
try {
await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('server startup timeout: '+stderr)), 10000);
child.stdout.on('data', c => { if (String(c).includes('CelebritySignatures →')) { clearTimeout(timer); resolve(); } });
child.once('error', e => { clearTimeout(timer); reject(e); });
child.once('exit', code => { clearTimeout(timer); reject(new Error('server exited '+code+': '+stderr)); });
});
const base = `http://127.0.0.1:${port}`;
if (producer === 'success') {
for (const id of [91001, 91002]) {
const response = await fetch(`${base}/wear-success?sid=cs_test_${id}`);
assert.equal(response.status, 200);
assert.match(await response.text(), /Order received/);
}
}
const draftPath = join(work, 'data/pod-order-drafts.jsonl');
const before = await readFile(draftPath, 'utf8');
const drafts = before.trim().split('\n').map(JSON.parse);
assert.deepEqual(drafts.map(d => d.orderId), ['91001.0', '91002.0']);
assert.equal(drafts[0].design_image_url, drafts[1].design_image_url, 'cache hit uses same artifact');
for (const draft of drafts) {
assert.equal(draft.provider, 'printify');
const image = new URL(draft.design_image_url);
assert.equal(image.origin, 'https://celebsignatures.com');
assert.match(image.pathname, /^\/assets\/recolored-signatures\/[a-f0-9]{24}\.png$/);
const payload = buildPayload(template, draft);
assert.equal(payload.external_id, `celebsig-wear-${draft.orderId}`);
assert.equal(payload.line_items[0].variant_id, template.garments.find(g => g.id === 'polo').printify.variants['black/M']);
assert.equal(payload.line_items[0].print_areas.left_chest[0].src, image.href);
const served = await fetch(base + image.pathname);
assert.equal(served.status, 200);
assert.match(served.headers.get('content-type'), /image\/png/);
const png = Buffer.from(await served.arrayBuffer());
assert.deepEqual(png, await readFile(join(work, 'public', image.pathname)));
assert.equal(png.subarray(1, 4).toString(), 'PNG');
}
const pixels = await exec('convert', [join(work, 'public', new URL(drafts[0].design_image_url).pathname), 'txt:-']);
// No buyer inkColor on this fixture + dark (black) cloth → white ink,
// matching public/wear.html's inkHexFor() exactly (server.js's
// recolorSignatureInk call site, 18cca456: darkCloth with no explicit
// pick recolors to '#ffffff', not the old fixed '#f5f3ee').
assert.match(pixels.stdout, /10,5:.*#FFFFFFFF/i, 'opaque signature pixel has light ink');
assert.match(pixels.stdout, /0,0:.*#FFFFFF00/i, 'transparent background preserved');
if (producer === 'success') {
for (const id of [91001, 91002]) await fetch(`${base}/wear-success?sid=cs_test_${id}`);
} else await reconcile();
assert.equal(await readFile(draftPath, 'utf8'), before, 'retry must not append duplicate drafts');
const orders = JSON.parse(await readFile(join(work, 'data/wear-orders.json'), 'utf8'));
assert.ok(orders.every(o => o.status === 'paid' && o.pod_submitted === true));
const ordersBefore = await readFile(join(work, 'data/wear-orders.json'), 'utf8');
const blocked = await fetch(`${base}/api/wear-checkout`, { method: 'POST',
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
email: 'fixture@example.invalid', items: [{ qid: 'QFixture', garment: 'polo', color: 'black', size: 'M' }] }) });
assert.equal((await blocked.json()).comingSoon, true, 'disabled sales stop before payment');
const unpaid = await fetch(`${base}/wear-success?sid=invalid`);
assert.match(await unpaid.text(), /Order not completed/);
assert.equal(await readFile(join(work, 'data/wear-orders.json'), 'utf8'), ordersBefore);
assert.equal(await readFile(draftPath, 'utf8'), before, 'negative paths do not change the queue');
const requests = (await readFile(join(work, 'requests.jsonl'), 'utf8')).trim().split('\n').map(JSON.parse);
assert.equal(requests.filter(r => r.url === artwork).length, 1, 'second order uses cached artwork');
assert.ok(requests.every(r => r.method === 'GET'));
assert.doesNotMatch(stderr, /recolorSignatureToLight failed/);
} finally {
if (child.exitCode === null && child.signalCode === null) {
const exited = once(child, 'exit');
child.kill('SIGTERM');
await exited;
}
}
});
}
});