← back to CelebritySignatures
test/wear-accessories.test.mjs
129 lines
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, readFile, writeFile, copyFile, rm, realpath } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
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';
const root = new URL('..', import.meta.url);
const templates = JSON.parse(await readFile(new URL('data/wear-templates.json', root), 'utf8'));
const catalog = JSON.parse(await readFile(new URL('verification/hat-sock-catalog.json', root), 'utf8'));
const ids = ['dad-cap', 'trucker-cap', 'bucket-hat', 'crew-socks'];
const draft = { orderId: 'fixture.0', provider: 'printify', status: 'DRAFT_UNSENT',
signature_name: 'Fixture', design_image_url: 'https://fixture.invalid/signature.png',
recipient_email: 'fixture@example.invalid', recipient: { name: 'Fixture Buyer',
address1: '1 Fixture Lane', city: 'Fixture', state_code: 'CA', country_code: 'US', zip: '90001' } };
const item = id => { const g = templates.garments.find(g => g.id === id);
return { qid: 'QFixture', garment: id, color: g.colors[0].id, size: g.sizes[0] }; };
const env = { PATH: process.env.PATH, WEAR_SALES_LIVE: '1', WEAR_POD_PROVIDER: 'printify',
PRINTIFY_API_TOKEN: 'fixture-token', PRINTIFY_SHOP_ID: 'fixture-shop', PRINTIFY_MANUAL_APPROVAL_CONFIRMED: '1',
STRIPE_TEST_SECRET_KEY: 'sk_test_fixture', WEAR_STRIPE_LIVE_ENABLED: '0', STRIPE_LIVE_ENABLED: '0' };
test('all four accessory payloads match real catalog variants and raster print areas', () => {
for (const id of ids) {
const g = templates.garments.find(g => g.id === id);
const source = catalog.products.find(p => p.id === id);
const payload = buildPayload(templates, { ...draft, ...item(id) }).line_items[0];
assert.equal(payload.blueprint_id, source.blueprint.id);
assert.equal(payload.print_provider_id, source.catalog.id);
const variant = source.catalog.variants.find(v => v.id === payload.variant_id);
assert.ok(variant, 'variant exists in provider response');
assert.equal(variant.options.size, g.sizes[0]);
assert.equal(variant.options.color, g.colors[0].label.split(' / ')[0], 'printer color matches the selected color');
for (const [position, images] of Object.entries(payload.print_areas)) {
const supported = variant.placeholders.find(p => p.position === position);
assert.ok(supported, 'printer supports '+position);
assert.ok(['dtf', 'dtg'].includes(supported.decoration_method), 'raster decoration method');
assert.equal(images[0].src, draft.design_image_url);
}
assert.equal(g.saleEnabled, false, 'requires cost/mockup approval');
assert.equal(g.priceUsd, null, 'no invented price');
}
const socks = buildPayload(templates, { ...draft, ...item('crew-socks') }).line_items[0];
assert.deepEqual(Object.keys(socks.print_areas).sort(), ['left_leg', 'right_leg']);
assert.deepEqual(socks.print_areas.left_leg, socks.print_areas.right_leg);
});
async function fixture(t) {
const work = await realpath(await mkdtemp(join(tmpdir(), 'tk10286-accessories-')));
t.after(() => rm(work, { recursive: true, force: true }));
await mkdir(join(work, 'data'));
await mkdir(join(work, 'scripts'));
const put = (name, data) => writeFile(join(work, 'data', name), JSON.stringify(data));
await put('wear-templates.json', templates);
await put('wear-orders.json', []);
await put('celebrity_signatures.json', [{ wikidata: 'https://www.wikidata.org/wiki/QFixture',
full_name: 'Fixture', category: 'Politics', signature_image_url: draft.design_image_url,
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 = {}) => {
appendFileSync(${JSON.stringify(join(work, 'requests.jsonl'))}, JSON.stringify({url:String(input),method:options.method,body:options.body})+'\\n');
if (String(input) === 'https://api.stripe.com/v1/checkout/sessions' && options.method === 'POST')
return Response.json({id:'cs_test_fixture',url:'https://fixture.invalid/checkout'});
throw new Error('External request blocked by fixture');
};
`);
return { work, put, interceptor };
}
test('direct and mixed-cart requests cannot buy accessories; original shirts still checkout', { timeout: 20000 }, async t => {
const { work, put, interceptor } = await fixture(t);
// A held item without a mapping must not disable the three existing shirts.
const local = structuredClone(templates);
local.garments.filter(g => g.saleEnabled === false).forEach(g => { delete g.printify; });
await put('wear-templates.json', local);
await copyFile(new URL('server.js', root), join(work, 'server.mjs'));
const socket = createServer().listen(0, '127.0.0.1');
await once(socket, 'listening');
const port = socket.address().port;
await new Promise(resolve => socket.close(resolve));
const child = spawn(process.execPath, ['--import', interceptor, join(work, 'server.mjs')],
{ env: { ...env, PORT: String(port) }, stdio: ['ignore', 'pipe', 'pipe'] });
t.after(async () => { if (child.exitCode === null) { const stopped = once(child, 'exit'); child.kill(); await stopped; } });
await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('startup timeout')), 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', c => { clearTimeout(timer); reject(new Error('server exited '+c)); });
});
const buy = items => fetch(`http://127.0.0.1:${port}/api/wear-checkout`, { method: 'POST',
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'fixture@example.invalid', items }) });
for (const id of ids) {
for (const items of [[item(id)], [item('classic-tee'), item(id)]]) {
const res = await buy(items);
assert.equal(res.status, 503);
assert.match((await res.json()).error, /coming soon/);
}
}
assert.deepEqual(JSON.parse(await readFile(join(work, 'data/wear-orders.json'), 'utf8')), []);
await assert.rejects(readFile(join(work, 'requests.jsonl')), { code: 'ENOENT' });
const res = await buy([item('classic-tee')]);
assert.equal(res.status, 200);
assert.equal((await res.json()).url, 'https://fixture.invalid/checkout');
const requests = (await readFile(join(work, 'requests.jsonl'), 'utf8')).trim().split('\n').map(JSON.parse);
assert.equal(requests.length, 1);
assert.equal(requests[0].url, 'https://api.stripe.com/v1/checkout/sessions');
const order = JSON.parse(await readFile(join(work, 'data/wear-orders.json'), 'utf8'))[0];
assert.equal(order.mode, 'test');
assert.equal(order.items[0].garment, 'classic-tee');
});
test('sender holds accessories even with --apply and all account gates set', async t => {
const { work, interceptor } = await fixture(t);
const script = join(work, 'scripts/submit-pod-draft-printify.mjs');
await copyFile(new URL('scripts/submit-pod-draft-printify.mjs', root), script);
const queue = ids.map((id, i) => JSON.stringify({ ...draft, ...item(id), orderId: `fixture.${i}` })).join('\n')+'\n';
const file = join(work, 'data/pod-order-drafts.jsonl');
await writeFile(file, queue);
const { stdout } = await promisify(execFile)(process.execPath, ['--import', interceptor, script, '--apply'], { env, timeout: 10000 });
assert.equal((stdout.match(/HELD:/g) || []).length, 4, stdout);
assert.equal(await readFile(file, 'utf8'), queue);
await assert.rejects(readFile(join(work, 'requests.jsonl')), { code: 'ENOENT' });
});