fix: harden week-two ordering, artwork and operations

This commit is contained in:
Cauê Faleiros
2026-09-23 10:40:18 -03:00
parent ccc25a2d5d
commit 24013458c9
43 changed files with 1064 additions and 228 deletions

View File

@@ -13,6 +13,30 @@ const html=await readFile('web/index.html','utf8');
const hashes=[...html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi)]
.filter(m=>! /\bsrc\s*=/i.test(m[1]))
.map(m=>"'sha256-"+createHash('sha256').update(m[2]).digest('base64')+"'");
function pdfFixture({pages=1,media='[0 0 720 360]',crop='',rotate=0,unit=1}={}){
const objects=[
'<< /Type /Catalog /Pages 2 0 R >>',
'<< /Type /Pages /Kids ['+Array.from({length:pages},(_,i)=>i+3+' 0 R').join(' ')+
'] /Count '+pages+' /MediaBox '+media+' >>',
...Array.from({length:pages},()=> '<< /Type /Page /Parent 2 0 R'+
(crop?' /CropBox '+crop:'')+(rotate?' /Rotate '+rotate:'')+
(unit!==1?' /UserUnit '+unit:'')+' >>')
];
const chunks=['%PDF-1.6\n%\xE2\xE3\xCF\xD3\n'], offsets=[0];
let length=Buffer.byteLength(chunks[0],'latin1');
for(let i=0;i<objects.length;i++){
offsets.push(length);
const part=(i+1)+' 0 obj\n'+objects[i]+'\nendobj\n';
chunks.push(part);length+=Buffer.byteLength(part,'latin1');
}
const xref=length;
chunks.push('xref\n0 '+(objects.length+1)+'\n0000000000 65535 f \n');
for(const offset of offsets.slice(1))chunks.push(String(offset).padStart(10,'0')+' 00000 n \n');
chunks.push('trailer\n<< /Size '+(objects.length+1)+
' /Root 1 0 R >>\nstartxref\n'+xref+'\n%%EOF\n');
return Buffer.from(chunks.join(''),'latin1');
}
const pdfData=options=>JSON.stringify(pdfFixture(options).toString('base64'));
const server=createServer(async(req,res)=>{
if(req.url.startsWith('/api/')){
res.setHeader('Content-Type','application/json');
@@ -27,7 +51,7 @@ const server=createServer(async(req,res)=>{
// Serve any script the page asks for, resolved inside web/, rather than
// a hardcoded list: the Site's behaviour is split across several files and a
// list would silently 404 the next one added.
if(/^\/[\w.-]+\.js$/.test(req.url)){
if(/^\/[\w.-]+\.js$/.test(req.url) || /^\/vendor\/pdf\.(worker\.)?min\.js$/.test(req.url)){
try{
const body=await readFile(resolve('web'+req.url));
res.setHeader('Content-Type','text/javascript');res.end(body);return;
@@ -39,6 +63,7 @@ await new Promise(r=>server.listen(0,'127.0.0.1',r));
const profile=await mkdtemp(tmpdir()+'/dtf-artwork-');
const chrome=spawn(process.env.CHROME_BIN||'/usr/bin/google-chrome-stable',[
'--headless=new','--disable-gpu','--no-first-run','--no-default-browser-check',
...(process.env.CHROME_NO_SANDBOX==='1'?['--no-sandbox']:[]),
'--remote-debugging-port=0','--user-data-dir='+profile,'about:blank'
],{stdio:['ignore','ignore','pipe']});
let stderr='',ws,next=0;
@@ -128,6 +153,9 @@ try{
assert.deepEqual(await evaluate(`({shown:!$('tipoEnvio').hidden,
on:[...document.querySelectorAll('#tipoEnvio .cam.on')].map(b=>b.dataset.tipo)})`),
{shown:true,on:['folha']});
await evaluate(`(()=>{const f=new File(['x'],'oversize.cdr');Object.defineProperty(f,'size',{value:128*1048576+1});sel([f]);})()`);
assert.equal(await evaluate(`folhas.length===0 && $('recusa').textContent.includes('128 MB')`),true,
'files beyond the scanner limit are rejected before browser analysis');
await evaluate(`pickTipo('avulsa')`);
assert.equal(await evaluate('modo'),'avulsa');
await evaluate('sendImage()');
@@ -135,6 +163,14 @@ try{
assert.deepEqual(await evaluate(`({mode:modo,sheets:folhas.length,width:artes[0].cm,quantity:artes[0].q})`),{mode:'avulsa',sheets:0,width:0,quantity:1});
await fill('[data-cm]',20);await fill('[data-q]',6);
let layout=await packed(6);
await waitFor(()=>evaluate(`itemAtual?.production?.sources[0]?.copies===6`),'per-file production record');
assert.deepEqual(await evaluate(`({version:itemAtual.production.version,source:itemAtual.production.sources[0]})`),
{version:2,source:{kind:'artwork',width_cm:20,length_cm:40,copies:6,
rotation_degrees:0,mirrored:false,measurement:'file'}});
assert.equal(await evaluate('itemAtual.production.placements.length'),6);
assert.equal(await evaluate('itemAtual.production.height_cm'),121);
assert.deepEqual(await evaluate('itemAtual.production.placements.map(p=>[p.source_index,p.copy_index,p.x_cm,p.y_cm])'),
[[0,0,0,0],[0,1,20.5,0],[0,2,0,40.5],[0,3,20.5,40.5],[0,4,0,81],[0,5,20.5,81]]);
assert.equal(layout.heading,'Montagem ao vivo');assert.equal(layout.height,121);
assert.equal(layout.billed,1.3);
assert.deepEqual(layout.pos.map(p=>[p.x,p.y]),[[0,0],[20.5,0],[0,40.5],[20.5,40.5],[0,81],[20.5,81]]);
@@ -149,6 +185,32 @@ try{
await click('[data-esp]');
await waitFor(()=>evaluate(`$('vArea').querySelector('canvas').toDataURL()!==${JSON.stringify(beforeMirror)}`),'mirror updates pixels');
assert.equal(await evaluate('metros'),0.235);
await waitFor(()=>evaluate(`itemAtual?.production?.sources[0]?.mirrored===true`),'transforms in production record');
assert.deepEqual(await evaluate(`({rotation:itemAtual.production.sources[0].rotation_degrees,copies:itemAtual.production.sources[0].copies})`),
{rotation:90,copies:9});
await waitFor(()=>evaluate(`itemAtual?.qualityStatus==='rejected'`),'low resolution refusal');
assert.equal(await evaluate('cartPodeEnviar()'),false);
await fill('[data-cm]',3);
await waitFor(()=>evaluate(`itemAtual?.qualityStatus==='ok'`),'rotated DPI uses image height');
assert.equal(await evaluate('Math.round(dpiDe(artes[0]))'),339);
await click('[data-giro]');
await waitFor(()=>evaluate(`itemAtual?.qualityStatus==='warning'`),'unrotated resolution warning');
assert.equal(await evaluate('Math.round(dpiDe(artes[0]))'),169);
assert.equal(await evaluate('cartPodeEnviar()'),false);
await click('#cienteOk');
assert.equal(await evaluate('cartPodeEnviar()'),true);
await fill('[data-q]',8);
assert.equal(await evaluate('itemAtual===null'),true,'editing invalidates the old cart immediately');
await waitFor(()=>evaluate(`itemAtual?.qualityStatus==='warning'`),'edited warning');
assert.equal(await evaluate('cartPodeEnviar()'),false,'acknowledgement does not survive an edit');
await fill('[data-cm]',58);
await waitFor(()=>evaluate(`itemAtual===null && artes[0].cm===58`),'oversized width rejected');
assert.equal(await evaluate('cartPodeEnviar()'),false);
assert.equal(await evaluate(`(()=>{try{encaixar([{w:58,h:10,img:null}],57);return false}catch(error){return error instanceof RangeError}})()`),true,
'packing engine must not shrink an oversized source');
await click('[data-rm]');
assert.equal(await evaluate(`artes.length===0 && itemAtual===null && !cartPodeEnviar()`),true,
'removed artwork cannot remain in the cart');
// A transparent asymmetric image exposes masks that ignore user transforms.
// Its top-left quarter becomes bottom-right after a mirror and 90° turn.
assert.equal(await evaluate(`(()=>{
@@ -169,6 +231,8 @@ try{
await evaluate(`(()=>{const el=$('lista').querySelector('[data-repf]');el.value=2;el.dispatchEvent(new Event('change',{bubbles:true}));})()`);
await waitFor(()=>evaluate('metros===0.684'),'ready-sheet repetitions');
assert.equal(await evaluate(`document.querySelectorAll('.folhaPrevia').length`),1);
await evaluate(`(()=>{folhas.push({f:new File(['manual'],'manual.cdr'),med:null,rep:1,m:1,an:null});pintaFolha();})()`);
await waitFor(()=>evaluate(`itemAtual?.nota===0 && itemAtual?.unit===TABELA[modo]`),'mixed analyzed and manual sheets use table pricing');
// An image too small to span the film is refused, never silently repriced.
await click('#bVoltar');await click('[data-modo="file"]');
await evaluate('sendImage()');
@@ -182,6 +246,33 @@ try{
await waitFor(()=>evaluate(`!!$('lista').querySelector('[data-comp]')`),'manual ready sheet');
await evaluate(`(()=>{const el=$('lista').querySelector('[data-comp]');el.value=1.01;el.dispatchEvent(new Event('change',{bubbles:true}));})()`);
await waitFor(()=>evaluate('itemAtual?.total===21.89'),'unchanged manual pricing');
await evaluate(`folhas[0].semAnalise='<img src=x onerror=alert(1)>';pintaFolha()`);
assert.equal(await evaluate(`!$('lista').querySelector('img') && $('lista').textContent.includes('<img src=x')`),true,
'PDF parser errors are text, never executable markup');
// Parsed geometry honors inherited MediaBox, CropBox, rotation and UserUnit.
// Extra pages and unreadable PDFs must never fall through to manual pricing.
const pdfFile=(data,name='sheet.pdf')=>`new File([Uint8Array.from(atob(${data}),c=>c.charCodeAt(0))],${JSON.stringify(name)},{type:'application/pdf'})`;
const rotated=await evaluate(`medirFolha(${pdfFile(pdfData({media:'[0 0 720 1440]',crop:'[0 0 360 720]',rotate:90,unit:2}))})`);
assert.deepEqual(rotated,{larg:50.8,alt:25.4,fonte:'página do PDF · UserUnit 2'});
const rendered=await evaluate(`rasterizarPdf(${pdfFile(pdfData({media:'[0 0 720 1440]',crop:'[0 0 360 720]',rotate:90,unit:2}))},50.8,25.4).then(r=>({ok:!!r.tela,error:r.erro||null}))`);
assert.deepEqual(rendered,{ok:true,error:null});
const multi=await evaluate(`medirFolha(${pdfFile(pdfData({pages:2}))})`);
assert.equal(multi.rejected,true);assert.match(multi.reason,/2 páginas/);
const beyondSpec=await evaluate(`medirFolha(${pdfFile(pdfData({media:'[0 0 20000 720]'}))})`);
assert.equal(beyondSpec.rejected,true);assert.match(beyondSpec.reason,/508 cm/);
const broken=await evaluate(`medirFolha(new File(['broken'],'broken.pdf',{type:'application/pdf'}))`);
assert.equal(broken.rejected,true);
await click('#bVoltar');await click('[data-modo="file"]');
await evaluate(`sel([${pdfFile(pdfData({pages:2}),'two-pages.pdf')}])`);
await waitFor(()=>evaluate(`folhas.length===1 && !!folhas[0].measurementError`),'multipage PDF refusal');
assert.equal(await evaluate(`avaliar().pronto`),false);
assert.equal(await evaluate(`cartPodeEnviar()`),false);
assert.match(await evaluate(`$('lista').textContent`),/não pode ser orçado/);
await click('#bVoltar');await click('[data-modo="avulsa"]');
await evaluate(`sel([new File(['not an image'],'broken.png',{type:'image/png'})])`);
await waitFor(()=>evaluate('artes.length===1 && artes[0].decodeError===true'),'failed image decode');
await fill('[data-cm]',20);
await waitFor(()=>evaluate('itemAtual===null && !cartPodeEnviar()'),'undecodable image cannot be quoted');
// PNG drag/drop follows exactly the same artwork path; UV stays UV. Reaching it
// from a by-metre product is one declared click, and it is reversible.
for(const [mode,expected] of [['file','avulsa'],['avulsa','avulsa'],['uvfile','uv'],['uv','uv']]){
@@ -201,6 +292,21 @@ try{
await fill('[data-q]',7);
await evaluate(`carregarImagem=realLoad;delayed[0]()`);
await packed(7);
const timeoutCleanup=await evaluate(`(async()=>{
const realLoader=carregarPdfJs, realTimer=setTimeout, realWorker=temWorker;
let destroyed=false, fail;
carregarPdfJs=async()=>({getDocument:()=>({
promise:new Promise((_,reject)=>{fail=reject}),
destroy:()=>{destroyed=true;fail(new Error('cancelled'));return Promise.resolve()}
})});
temWorker=true;
window.setTimeout=(fn,ms)=>realTimer(fn,ms===30000?1:ms);
try{
const result=await rasterizarPdf({arrayBuffer:async()=>new ArrayBuffer(1)},10,10);
return {timedOut:result.erro==='demorou demais neste navegador',destroyed};
}finally{carregarPdfJs=realLoader;window.setTimeout=realTimer;temWorker=realWorker;}
})()`);
assert.deepEqual(timeoutCleanup,{timedOut:true,destroyed:true});
// Inspect the supplied local artwork, when requested, using the real file input.
if(process.env.ARTWORK_FILE){
await click('#bVoltar');await click('[data-modo="file"]');

View File

@@ -14,6 +14,7 @@ try {
const profile=await mkdtemp(tmpdir()+'/dtf-browser-');
const chrome=spawn(process.env.CHROME_BIN||'/usr/bin/google-chrome-stable',[
'--headless=new','--disable-gpu','--no-first-run','--no-default-browser-check',
...(process.env.CHROME_NO_SANDBOX==='1'?['--no-sandbox']:[]),
'--remote-debugging-port=0','--user-data-dir='+profile,'about:blank'
],{stdio:['ignore','ignore','pipe']});
const pause=ms=>new Promise(r=>setTimeout(r,ms));
@@ -41,7 +42,9 @@ try{
await p.call('Emulation.setDeviceMetricsOverride',{width:1440,height:1000,deviceScaleFactor:1,mobile:false});
await waitFor(()=>p.eval('document.readyState === "complete"'),'page load');return p;
}
const site=await page('http://localhost:'+(process.env.SITE_PORT||8080));
const siteOrigin=process.env.SITE_BROWSER_ORIGIN||'http://localhost:'+(process.env.SITE_PORT||8080);
const kanbanOrigin=process.env.KANBAN_BROWSER_ORIGIN||'http://localhost:'+(process.env.KANBAN_PORT||8081);
const site=await page(siteOrigin);
await waitFor(()=>site.eval('typeof window.dtfCheckout === "function"'),'checkout bridge');
// Prove escaping itself, independently of the CSP's second line of defense.
await site.call('Page.setBypassCSP',{enabled:true});
@@ -74,9 +77,14 @@ try{
assert.equal(await site.eval('pedido[0].total'),21.89);
assert.equal(await site.eval('document.getElementById("bPagar").disabled'),false);
await site.click('#bPagar');
await waitFor(async()=> (await site.text()).includes('Arquivos enviados.'),'browser upload and quote',45000);
try{
await waitFor(async()=> (await site.text()).includes('Arquivos enviados.'),'browser upload and quote',45000);
}catch(error){
console.error('Checkout status:',await site.eval('document.getElementById("checkoutStatus")?.textContent'));
throw error;
}
const qid=await site.eval('localStorage.getItem("dtf-quote")');
const kanban=await page('http://localhost:'+(process.env.KANBAN_PORT||8081));
const kanban=await page(kanbanOrigin);
await kanban.fill('#email',process.env.OPERATOR_EMAIL||'operator@example.test');
await kanban.fill('#password',process.env.OPERATOR_PASSWORD||'local-operator-only');
await kanban.eval('document.getElementById("login").requestSubmit()');
@@ -85,6 +93,14 @@ try{
assert.equal(await kanban.eval('document.getElementById("password").value'),'');
await kanban.eval(`(()=>{const card=[...document.querySelectorAll('.review')].find(x=>x.textContent.includes(${JSON.stringify(qid.slice(0,8))}));card.querySelector('[type=checkbox]').click();card.querySelector('form').requestSubmit();})()`);
await waitFor(async()=> (await kanban.text()).includes('Aprovada:'),'quote approval');
assert.equal(await site.eval('pedido[0].production.sources[0].copies'),1);
await site.eval('pedido[0].production.sources[0].copies=2;pintaPedido()');
await waitFor(async()=> (await site.text()).includes('O carrinho mudou'),'same-price production edit invalidates quote');
await site.eval('pedido[0].production.sources[0].copies=1;pintaPedido()');
await site.fill('#fMail','changed-browser@example.test');
await waitFor(async()=> (await site.text()).includes('O carrinho mudou'),'quote invalidated by cart edit');
assert.equal(await site.eval('[...document.querySelectorAll("button")].some(x=>x.textContent==="Criar pedido de teste")'),false);
await site.fill('#fMail','local-browser@example.test');
await site.eval('window.dtfCheckout()');
await waitFor(async()=> (await site.text()).includes('Total validado no servidor:'),'approved quote displayed');
await site.eval('[...document.querySelectorAll("button")].find(x=>x.textContent==="Criar pedido de teste").click()');
@@ -92,6 +108,8 @@ try{
await kanban.click('#refresh');
await waitFor(()=>kanban.eval(`board.orders.some(o=>o.quote_id===${JSON.stringify(qid)})`),'paid card');
const oid=await kanban.eval(`board.orders.find(o=>o.quote_id===${JSON.stringify(qid)}).id`);
assert.deepEqual(await kanban.eval(`(()=>{const spec=board.orders.find(o=>o.id===${JSON.stringify(oid)}).snapshot.items[0].production;const source=spec.sources[0];return {kind:source.kind,copies:source.copies,length:Number(source.length_cm),height:Number(spec.height_cm),placed:spec.placements.length}})()`),
{kind:'sheet',copies:1,length:101,height:101,placed:1});
// Click real transition buttons, including rerender after each move.
for(const [title,state] of [['Arte tratada','tra'],['Fila de impressão','fil'],['Imprimindo','imp'],['Finalizado','fin']]){
if(state==='fil'){
@@ -115,7 +133,7 @@ try{
await waitFor(()=>site.eval('window.scrollY===0'),'screenshot scroll position');
await site.screenshot('output/local/site.png');
await kanban.screenshot('output/local/kanban.png');
const portal=await page('http://localhost:'+(process.env.SITE_PORT||8080)+'/portal.html?order='+oid);
const portal=await page(siteOrigin+'/portal.html?order='+oid);
await waitFor(async()=> (await portal.text()).includes('Finalizado'),'customer order tracking');
await portal.fill('#cnpj','11222333000181');await portal.fill('#phone','11999999999');
await portal.fill('#register-email','browser-'+Date.now()+'@example.test');
@@ -127,7 +145,7 @@ try{
// A logout must clear draft file blobs and metadata, including other open Site tabs.
await portal.eval(`(async()=>{const db=await new Promise((resolve,reject)=>{const r=indexedDB.open('dtf-local-cart',1);r.onsuccess=()=>resolve(r.result);r.onerror=reject;});await new Promise((resolve,reject)=>{const tx=db.transaction('cart','readwrite');tx.objectStore('cart').put({items:[new File(['private'],'private.cdr')],expires:Date.now()+86400000},'security-fixture');tx.oncomplete=resolve;tx.onerror=reject;});db.close();})()`);
await portal.click('#logout');
await waitFor(()=>portal.eval('document.getElementById("logout").hidden'),'customer logout');
await waitFor(()=>portal.eval('document.getElementById("logout")?.hidden===true'),'customer logout');
let stored;
await waitFor(async()=>{stored=await portal.eval(`(async()=>{try{const db=await new Promise((resolve,reject)=>{const r=indexedDB.open('dtf-local-cart',1);r.onupgradeneeded=()=>r.result.createObjectStore('cart');r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error);});const n=await new Promise((resolve,reject)=>{const r=db.transaction('cart').objectStore('cart').count();r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error);});db.close();return n;}catch{return -1;}})()`);return stored>=0;},'IndexedDB available after Clear-Site-Data');
assert.equal(stored,0);

View File

@@ -12,7 +12,7 @@ from urllib.error import HTTPError
from urllib.request import Request, urlopen
from uuid import uuid4
from tests.smoke_test import BASE, Client, upload_bytes, with_host
from tests.smoke_test import BASE, Client, upload_bytes, item_spec, with_host
SECRET = os.environ.get('PAYMENT_WEBHOOK_SECRET', 'local-webhook-secret').encode()
@@ -37,7 +37,7 @@ def reviewed_quote():
customer = Client()
customer.call('/session')
uid = upload_bytes(customer, b'PAYMENT WEBHOOK TEST')
item = {'mode': 'file', 'metres': '1.01', 'grade': 0, 'uploads': [uid]}
item = item_spec('file', '1.01', 0, uid)
profile = {'cnpj': '11222333000181', 'zap': '11999999999',
'mail': 'payment-' + uuid4().hex[:8] + '@example.test'}
quote = customer.call('/quotes', {'request_key': str(uuid4()), 'customer': profile,
@@ -62,7 +62,13 @@ def run():
deliver({'event_id': 'short-' + uuid4().hex, 'reference': quote_id,
'status': 'approved', 'amount_cents': total - 100})
assert not customer.call('/quotes/' + quote_id)['order'], 'underpayment created an order'
print('PASS: an amount that disagrees with the reviewed quote is refused')
deliver({'event_id': 'missing-amount-' + uuid4().hex, 'reference': quote_id,
'status': 'approved'})
assert not customer.call('/quotes/' + quote_id)['order'], 'missing paid amount created an order'
deliver({'event_id': 'invalid-amount-' + uuid4().hex, 'reference': quote_id,
'status': 'approved', 'amount_cents': str(total)})
assert not customer.call('/quotes/' + quote_id)['order'], 'non-integer paid amount created an order'
print('PASS: a missing, invalid or mismatched paid amount is refused')
# The real thing, then the same delivery again, and a second event for the
# same quote: a provider does all three.
@@ -81,6 +87,12 @@ def run():
assert customer.call('/quotes/' + quote_id)['order']['id'] == order['id'], 'a second order appeared'
print('PASS: one order from a repeated and re-sent approval')
assert customer.call('/orders/dev-paid', {'quote_id': quote_id})['id'] == order['id']
other = Client()
other.call('/session')
other.call('/orders/dev-paid', {'quote_id': quote_id}, expected=404)
print('PASS: another customer cannot retrieve the paid order by quote id')
# The customer is told once, not once per delivery.
board = Client()
events = board.call('/operator/board', operator=True)['events']

View File

@@ -0,0 +1,60 @@
"""The 101st pending quote and older approved quotes remain reachable on the board."""
from uuid import uuid4
from urllib.parse import urlencode
from psycopg.types.json import Jsonb
from app.core import db
from tests.smoke_test import Client
def run():
owner = uuid4()
pending_ids = [uuid4() for _ in range(105)]
approved_ids = [uuid4() for _ in range(22)]
created = pending_ids + approved_ids
draft = {'customer': {'mail': 'pagination-fixture@example.test'},
'items': [], 'freight': {'service': 'pickup'}}
try:
with db.connect() as c:
for uid in pending_ids:
c.execute('''INSERT INTO dtf_local.quotes
(id,owner,request_key,request_hash,draft,created_at)
VALUES(%s,%s,%s,%s,%s,now()+interval '1 hour')''',
(uid, owner, uuid4(), 'pagination-fixture', Jsonb(draft)))
for uid in approved_ids:
c.execute('''INSERT INTO dtf_local.quotes
(id,owner,request_key,request_hash,draft,approved,approved_at,created_at)
VALUES(%s,%s,%s,%s,%s,%s,now(),now()+interval '1 hour')''',
(uid, owner, uuid4(), 'pagination-fixture', Jsonb(draft),
Jsonb({'items': [], 'total_cents': 0})))
client = Client()
board = client.call('/operator/board', operator=True)
assert board['pending_total'] >= 105
assert board['approved_total'] >= 22
for kind, fixture_ids in [('pending', pending_ids), ('approved', approved_ids)]:
first = [q for q in board['quotes'] if (q['approved'] is None) == (kind == 'pending')]
seen = {q['id'] for q in first}
assert len(first) == (100 if kind == 'pending' else 20)
last = first[-1]
for _ in range(5):
path = '/operator/quotes?' + urlencode({
'kind': kind, 'limit': 50,
'before_created_at': last['created_at'], 'before_id': last['id']})
page = client.call(path, operator=True)
assert page['quotes'], 'An older quote page disappeared'
assert not seen.intersection(q['id'] for q in page['quotes']), 'Quote page repeated rows'
seen.update(q['id'] for q in page['quotes'])
if set(map(str, fixture_ids)) <= seen:
break
last = page['quotes'][-1]
assert set(map(str, fixture_ids)) <= seen, f'{kind} quotes were hidden by the board limit'
print('PASS: 105 pending and 22 approved quotes remain reachable across board pages')
finally:
with db.connect() as c:
c.execute('DELETE FROM dtf_local.quotes WHERE id=ANY(%s)', (created,))
if __name__ == '__main__':
run()

View File

@@ -1,6 +1,6 @@
"""Harmless EICAR anti-malware test and blocked download/quote regressions."""
from uuid import uuid4
from tests.smoke_test import Client, upload_bytes
from tests.smoke_test import Client, upload_bytes, item_spec
def run():
customer=Client();customer.call('/session')
@@ -9,7 +9,7 @@ def run():
uid=upload_bytes(customer,marker,name='SECURITY-EICAR.cdr',expected_scan='rejected')
customer.call('/operator/uploads/'+uid+'/download',operator=True,expected=409)
customer.call('/quotes',{'request_key':str(uuid4()),'customer':{'cnpj':'11222333000181','zap':'11999999999','mail':'security@example.test'},
'items':[{'mode':'file','metres':'1','grade':0,'uploads':[uid]}],'freight':{'service':'pickup'}},expected=409)
'items':[item_spec('file','1',0,uid)],'freight':{'service':'pickup'}},expected=409)
clean=upload_bytes(customer,b'Harmless local artwork fixture',name='SECURITY-CLEAN.cdr')
customer.call('/operator/uploads/'+clean+'/download',operator=True)
print('PASS: real ClamAV detects EICAR; rejected artwork cannot be downloaded or quoted; clean artwork is released.')

View File

@@ -37,14 +37,15 @@ class Client:
self.jar=http.cookiejar.CookieJar()
self.opener=build_opener(HTTPCookieProcessor(self.jar))
self.operator_client=None
def call(self,path,body=None,operator=False,expected=200):
def call(self,path,body=None,operator=False,expected=200,method=None):
headers=with_host({'Content-Type':'application/json'})
if operator:
if self.operator_client is None:
self.operator_client=Client()
self.operator_client.call('/operator/login',{'email':os.environ.get('OPERATOR_EMAIL','operator@example.test'),'password':os.environ.get('OPERATOR_PASSWORD','local-operator-only')})
return self.operator_client.call(path,body,expected=expected)
request=Request(BASE+'/api'+path,data=None if body is None else json.dumps(body).encode(),headers=headers)
return self.operator_client.call(path,body,expected=expected,method=method)
request=Request(BASE+'/api'+path,data=None if body is None else json.dumps(body).encode(),
headers=headers,method=method)
try:
with self.opener.open(request,timeout=30) as response:
assert response.status==expected,(path,response.status,expected)
@@ -78,10 +79,32 @@ def upload_bytes(client, content, name='LOCAL-TEST.cdr', order_id=None, expected
wait_scan(client,uid,operator,expected_scan)
return uid
def item_spec(mode, metres, grade, uid):
film_width=28.5 if mode in ('uvfile','uv') else 57
length_cm=float(metres)*100
return {'mode':mode,'metres':str(metres),'grade':grade,'uploads':[uid],
'production':{'version':2,'film_width_cm':film_width,'height_cm':length_cm,
'sources':[{'upload_id':uid,
'kind':'sheet' if mode in ('file','uvfile') else 'artwork',
'width_cm':film_width,'length_cm':length_cm,'copies':1,
'rotation_degrees':0,'mirrored':False,'measurement':'customer'}],
'placements':[{'source_index':0,'copy_index':0,'x_cm':0,'y_cm':0,
'width_cm':film_width,'length_cm':length_cm,
'rotation_degrees':0,'mirrored':False}]},
'quality_status':'unverified' if grade==0 else 'ok',
'quality_acknowledged':False}
def run():
client=Client();other=Client()
config=client.call('/session');other.call('/session')
assert client.call('/health')['integrations']=='fake'
assert 0 < config['max_upload_bytes'] <= 128 * 1024 * 1024
client.call('/uploads',{'name':'too-large.cdr',
'size':config['max_upload_bytes']+1},expected=413)
cancelled=client.call('/uploads',{'name':'CANCELLED-PART.cdr','size':3})['id']
other.call('/uploads/'+cancelled,expected=404,method='DELETE')
assert client.call('/uploads/'+cancelled,method='DELETE')['cancelled']
client.call('/uploads/'+cancelled,expected=410)
client.call('/operator/board',expected=401)
block=config['part_bytes'];content=b'DTF local multipart test\n'+b'x'*block
uid=client.call('/uploads',{'name':'LOCAL-SMOKE-ONLY.cdr','size':len(content)})['id']
@@ -104,10 +127,15 @@ def run():
except HTTPError as exc:assert exc.code==403
print('PASS: multipart resume, incomplete rejection, immutable completion, ownership, private/downloaded bytes')
items=[{'mode':m,'metres':'2.75','grade':90,'uploads':[uid]} for m in ('file','avulsa','uvfile','uv')]
items=[item_spec(m,'2.75',90,uid) for m in ('file','avulsa','uvfile','uv')]
draft={'request_key':str(uuid4()),'customer':{'cnpj':'11222333000181','zap':'11999999999','mail':'local-smoke@example.test'},
'items':items,'freight':{'service':'mock-standard','postal_code':'14400000'}}
client.call('/quotes',{**draft,'total_cents':1},expected=422)
client.call('/quotes',{**draft,'items':[{k:v for k,v in items[0].items() if k!='production'}]},expected=422)
outside={**items[0],'production':{**items[0]['production'],
'placements':[{**items[0]['production']['placements'][0],'x_cm':1}]}}
client.call('/quotes',{**draft,'items':[outside]},expected=422)
client.call('/quotes',{**draft,'items':[{**items[0],'metres':'1.00'}]},expected=422)
client.call('/quotes',{**draft,'customer':{**draft['customer'],'cnpj':'11111111111111'}},expected=422)
quote=client.call('/quotes',draft)
assert client.call('/quotes',draft)['id']==quote['id']
@@ -116,6 +144,9 @@ def run():
other.call('/quotes/'+qid,expected=404)
client.call('/orders/dev-paid',{'quote_id':qid},expected=409)
client.call('/operator/quotes/'+qid+'/approve',{'items':items},expected=401)
altered={**items[0],'production':{**items[0]['production'],
'sources':[{**items[0]['production']['sources'][0],'measurement':'file'}]}}
client.call('/operator/quotes/'+qid+'/approve',{'items':[altered,*items[1:]]},operator=True,expected=422)
# Reviewer corrects a browser-supplied grade and length. Browser values are proposals.
corrected=[{**items[0],'metres':'1.01','grade':0},*items[1:]]
approved=client.call('/operator/quotes/'+qid+'/approve',{'items':corrected},operator=True)

View File

@@ -1,12 +1,12 @@
"""Customer identity, correction and final-file trust boundaries against local stack."""
from uuid import uuid4
from urllib.request import urlopen
from tests.smoke_test import Client, upload_bytes
from tests.smoke_test import Client, upload_bytes, item_spec
def run():
customer=Client();other=Client();customer.call('/session');other.call('/session')
uid=upload_bytes(customer,b'LOCAL ORIGINAL ONLY')
item={'mode':'file','metres':'1.01','grade':0,'uploads':[uid]}
item=item_spec('file','1.01',0,uid)
profile={'cnpj':'11222333000181','zap':'11999999999','mail':'workflow-'+uuid4().hex[:8]+'@example.test'}
q=customer.call('/quotes',{'request_key':str(uuid4()),'customer':profile,'items':[item],'freight':{'service':'pickup'}})
customer.call('/operator/quotes/'+q['id']+'/approve',{'items':[item]},operator=True)
@@ -47,6 +47,10 @@ def run():
guest=Client();guest.call('/session');guest.call('/customer/orders/'+oid+'/files/'+final['id']+'/download',expected=404)
version=move('fil',version);version=move('imp',version);version=move('cor',version)
customer.call('/customer/orders/'+oid+'/files/'+final['id']+'/download',expected=404)
premature_final=upload_bytes(customer,b'FINAL BEFORE CUSTOMER CORRECTION',order_id=oid)
version=customer.call('/operator/orders/'+oid+'/final-files',
{'version':version,'files':[{'item_index':0,'upload_id':premature_final}],
'note':'Prepared before customer sent the new correction'},operator=True)['version']
correction_id=upload_bytes(customer,b'LOCAL CORRECTED ORIGINAL')
payload={'version':version,'files':[{'item_index':0,'upload_id':correction_id}],'note':'Replaced the artwork as requested'}
guest.call('/customer/orders/'+oid+'/corrections',payload,expected=404)
@@ -54,6 +58,7 @@ def run():
version=customer.call('/customer/orders/'+oid+'/corrections',payload)['version']
files=customer.call('/operator/orders/'+oid+'/files',operator=True)
assert any(f['kind']=='correction' and f['active'] and f['upload_id']==correction_id for f in files)
assert not any(f['kind']=='final' and f['active'] for f in files), 'new correction kept a stale final active'
version=move('tra',version)
customer.call('/operator/orders/'+oid+'/move',{'state':'fil','version':version},operator=True,expected=409)
new_final=upload_bytes(customer,b'LOCAL FINAL VERSION TWO',order_id=oid)