refactor: lay the repository out by role
All checks were successful
Build and deploy / Validate source (push) Successful in 7s
Build and deploy / Integration suite on a real stack (push) Successful in 1m25s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images and notify Portainer (push) Successful in 1m31s

local/ held six unrelated things under a name that stopped being true once it
became the production runtime: the service, the frontend, the tests, the ops
commands, the container definitions and the dependency lock, 65 files with
nothing to tell them apart.

  app/      the service: api/ routers, core/ for identity, database, models,
            prices and secret loading, and the worker, bootstrap and schema
  tests/    the twelve suites, no longer inside the shipped package
  ops/      backup, readiness, dependency audit, security summary
  infra/    Dockerfiles, gateway templates, ClamAV and storage configuration,
            the requirements and their hash lock
  web/      the Site, Kanban and portal pages with their scripts

deploy/Dockerfile.api now copies app/ alone, so the tests stop shipping to
production; the local image still carries them, because the suites run inside
the stack's network.

Five kinds of reference had to follow, and each was found by something different
rather than by reading. Imports of the form "from . import db" survived a rewrite
that only matched "from .db import". Tests kept relative imports of modules that
had left the package. A mock.patch target names its module in a string, where no
import rewriting can see it. The browser test resolves a fixture by path. And the
release gate's markers pointed at local/runtime.py and local/worker.py, which is
the decay its new marker test exists to catch — it caught it.

Verified from docker compose down -v: the stack starts, all six integration
suites, both browser suites and the twenty-nine unit tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-21 17:40:57 -03:00
parent c9f8122600
commit b329f76378
69 changed files with 146 additions and 139 deletions

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Test suites. Not shipped in the production image."""

View File

@@ -0,0 +1,220 @@
// Isolated upload/editor regression: no API, database, storage, or Docker services.
// Run: node local/artwork_browser_test.mjs
// Optional: ARTWORK_FILE=/absolute/path/to/image.jpg to also inspect a real image.
import assert from 'node:assert/strict';
import {spawn} from 'node:child_process';
import {createServer} from 'node:http';
import {createHash} from 'node:crypto';
import {mkdtemp, readFile, writeFile, mkdir} from 'node:fs/promises';
import {tmpdir} from 'node:os';
import {resolve} from 'node:path';
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')+"'");
const server=createServer(async(req,res)=>{
if(req.url.startsWith('/api/')){
res.setHeader('Content-Type','application/json');
res.end(JSON.stringify(req.url==='/api/session'?{cart_scope:'isolated-artwork-test'}:{customer:null}));
return;
}
if(req.url==='/'){
res.setHeader('Content-Type','text/html');
res.setHeader('Content-Security-Policy',`default-src 'self'; script-src 'self' ${hashes.join(' ')}; script-src-attr 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; object-src 'none'`);
res.end(html);return;
}
// 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)){
try{
const body=await readFile(resolve('web'+req.url));
res.setHeader('Content-Type','text/javascript');res.end(body);return;
}catch{ res.writeHead(404);res.end();return; }
}
res.writeHead(404);res.end();
});
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',
'--remote-debugging-port=0','--user-data-dir='+profile,'about:blank'
],{stdio:['ignore','ignore','pipe']});
let stderr='',ws,next=0;
chrome.stderr.on('data',b=>stderr+=b);
const pending=new Map(),errors=[];
const pause=ms=>new Promise(r=>setTimeout(r,ms));
async function waitFor(fn,label){
const end=Date.now()+15000;
while(Date.now()<end){if(await fn())return;await pause(50);}
throw new Error('Timeout: '+label);
}
function call(method,params={}){
return new Promise((resolve,reject)=>{const id=++next;pending.set(id,{resolve,reject});ws.send(JSON.stringify({id,method,params}));});
}
async function evaluate(expression){
const r=await call('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});
if(r.exceptionDetails)throw new Error(JSON.stringify(r.exceptionDetails));
return r.result.value;
}
const click=selector=>evaluate(`document.querySelector(${JSON.stringify(selector)}).click()`);
async function fill(selector,value){
await evaluate(`(()=>{const el=document.querySelector(${JSON.stringify(selector)});el.value=${JSON.stringify(String(value))};el.dispatchEvent(new Event('input',{bubbles:true}));})()`);
}
async function packed(count){
await waitFor(()=>evaluate(`window.lastLayout?.pos.length===${count} && !!document.querySelector('#vArea canvas') && Math.abs(metros-window.lastLayout.altura/100)<0.0001`),'packed canvas and matching metres');
return evaluate(`({height:lastLayout.altura, pos:lastLayout.pos, metres:metros, billed:cobrar(metros), mode:modo, heading:$('montcabTit').textContent})`);
}
async function screenshot(name){
// Finish editing as a customer does when leaving the field, refreshing the
// item's quality/details as well as the already-live canvas.
await evaluate(`$('lista').querySelector('[data-cm]')?.dispatchEvent(new Event('change',{bubbles:true}))`);
await waitFor(()=>evaluate(`!!$('vArea').querySelector('canvas') && Math.abs(metros-montagemCm/100)<0.0001`),'screenshot layout');
await evaluate(`$('foco').scrollIntoView({behavior:'instant',block:'start'})`);
const r=await call('Page.captureScreenshot',{format:'png'});
await mkdir('output/local',{recursive:true});
await writeFile('output/local/'+name+'.png',Buffer.from(r.data,'base64'));
}
try{
let port;
await waitFor(async()=>{try{port=Number((await readFile(profile+'/DevToolsActivePort','utf8')).split('\n')[0]);return true;}catch{return false;}},'Chrome startup');
const tab=await(await fetch(`http://127.0.0.1:${port}/json/new?about:blank`,{method:'PUT'})).json();
ws=new WebSocket(tab.webSocketDebuggerUrl);
await new Promise((r,j)=>{ws.onopen=r;ws.onerror=j;});
ws.onmessage=event=>{
const m=JSON.parse(event.data);
if(m.id){const p=pending.get(m.id);pending.delete(m.id);m.error?p.reject(m.error):p.resolve(m.result);}
else if(m.method==='Runtime.exceptionThrown')errors.push(m.params.exceptionDetails);
};
await call('Runtime.enable');await call('Page.enable');
await call('Emulation.setDeviceMetricsOverride',{width:1440,height:1100,deviceScaleFactor:1,mobile:false});
await call('Page.navigate',{url:`http://127.0.0.1:${server.address().port}/`});
await waitFor(()=>evaluate(`typeof sel==='function' && typeof AUTO!=='undefined'`),'Site scripts');
// Observe the actual engine, without replacing its placement or rendering.
await evaluate(`(()=>{
const original=encaixar;
encaixar=(pieces,width)=>{const result=original(pieces,width);
if(pieces.some(p=>p.a))window.lastLayout={altura:result.altura,pos:result.pos.map(p=>({x:p.x,y:p.y,w:p.w,h:p.h,rot:p.rot}))};
return result;};
window.sendImage=async(type='image/jpeg',drop=false)=>{
const c=document.createElement('canvas');c.width=200;c.height=400;
const ctx=c.getContext('2d');ctx.fillStyle='white';ctx.fillRect(0,0,200,400);
ctx.fillStyle='#222';ctx.fillRect(85,20,20,360);ctx.fillRect(25,100,140,20);
ctx.fillStyle='#e65c00';ctx.fillRect(130,280,45,80);
const blob=await new Promise(r=>c.toBlob(r,type));
const file=new File([blob],type==='image/jpeg'?'artwork.jpg':'artwork.png',{type});
const dt=new DataTransfer();dt.items.add(file);
if(drop)$('zona').dispatchEvent(new DragEvent('drop',{dataTransfer:dt,bubbles:true,cancelable:true}));
else{$('inp').files=dt.files;$('inp').dispatchEvent(new Event('change',{bubbles:true}));}
};
// A genuine mounted sheet spans the film width with printable resolution.
window.sendSheet=async()=>{
const cm=larguraFilme(), px=Math.ceil(cm/2.54*150);
const c=document.createElement('canvas');c.width=px;c.height=Math.round(px*0.6);
const ctx=c.getContext('2d');ctx.fillStyle='white';ctx.fillRect(0,0,c.width,c.height);
ctx.fillStyle='#222';ctx.fillRect(40,40,px/4,px/4);
const blob=await new Promise(r=>c.toBlob(r,'image/png'));
const dt=new DataTransfer();dt.items.add(new File([blob],'folha-montada.png',{type:'image/png'}));
$('inp').files=dt.files;$('inp').dispatchEvent(new Event('change',{bubbles:true}));
};
window.pickTipo=t=>document.querySelector('#tipoEnvio .cam[data-tipo="'+t+'"]').click();
})()`);
// Reproduce the reported entry path, with no navigation workaround.
// Navigation links must reach the chooser, never preselect a product.
assert.equal(await evaluate(`document.querySelectorAll('[data-modo-cta]').length`),0);
await click('[data-modo="file"]');
// By-metre opens declaring a mounted sheet; switching is explicit and priced.
assert.deepEqual(await evaluate(`({shown:!$('tipoEnvio').hidden,
on:[...document.querySelectorAll('#tipoEnvio .cam.on')].map(b=>b.dataset.tipo)})`),
{shown:true,on:['folha']});
await evaluate(`pickTipo('avulsa')`);
assert.equal(await evaluate('modo'),'avulsa');
await evaluate('sendImage()');
await waitFor(()=>evaluate(`artes.length===1 && !!artes[0].src`),'JPG artwork model');
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);
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]]);
assert.ok(layout.pos.every(p=>p.h/p.w===2 && p.x+p.w<=57));
await screenshot('artwork-packed');
await fill('[data-cm]',15);
await waitFor(()=>evaluate('lastLayout?.altura===60.5 && metros===0.605'),'width updates without blur');
await fill('[data-q]',9);layout=await packed(9);assert.equal(layout.height,91);
await click('[data-giro]');
await waitFor(()=>evaluate('lastLayout?.altura===23.5 && metros===0.235'),'rotation updates packing');
const beforeMirror=await evaluate(`$('vArea').querySelector('canvas').toDataURL()`);
await click('[data-esp]');
await waitFor(()=>evaluate(`$('vArea').querySelector('canvas').toDataURL()!==${JSON.stringify(beforeMirror)}`),'mirror updates pixels');
assert.equal(await evaluate('metros'),0.235);
// 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(`(()=>{
const image=document.createElement('canvas');image.width=100;image.height=200;
image.getContext('2d').fillRect(0,0,50,100);
const result=encaixar([{w:20,h:10,img:image,a:{giro:90,esp:true},travado:true}],57);
const m=result.pos[0].m;
return !m.bits[2*m.cw+2] && !m.bits[2*m.cw+30] && !!m.bits[15*m.cw+30];
})()`),true);
// A real finished sheet keeps its own dimensions and by-metre price.
await click('#bVoltar');await click('[data-modo="file"]');
await evaluate('sendSheet()');
await waitFor(()=>evaluate('folhas.length===1 && !!folhas[0].previewSrc && metros===0.342'),'ready-sheet image');
assert.deepEqual(await evaluate(`({mode:modo,art:artes.length,title:$('montcabTit').textContent,rate:precoBase(100)})`),{mode:'file',art:0,title:'Sua folha',rate:14.9});
assert.equal(await evaluate(`document.querySelectorAll('[data-cm]').length`),0);
// With a sheet loaded the declaration is locked; it cannot flip under the customer.
assert.equal(await evaluate(`document.querySelector('#tipoEnvio .cam[data-tipo="avulsa"]').disabled`),true);
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);
// An image too small to span the film is refused, never silently repriced.
await click('#bVoltar');await click('[data-modo="file"]');
await evaluate('sendImage()');
await waitFor(()=>evaluate(`!!document.getElementById('usarAvulsa')`),'ready-sheet refusal');
assert.deepEqual(await evaluate(`({mode:modo,sheets:folhas.length,art:artes.length})`),{mode:'file',sheets:0,art:0});
await click('#usarAvulsa');
await waitFor(()=>evaluate(`modo==='avulsa' && artes.length===1`),'one-click recovery into loose artwork');
// Keep manual ready-sheet uploads intact.
await click('#bVoltar');await click('[data-modo="file"]');
await evaluate(`sel([new File(['sheet'],'sheet.cdr')])`);
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');
// 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']]){
await click('#bVoltar');await click('[data-modo="'+mode+'"]');
if(await evaluate(`ehFolha()`))await evaluate(`pickTipo('avulsa')`);
await evaluate(`sendImage('image/png',true)`);
await waitFor(()=>evaluate('artes.length===1 && !!artes[0].src'),'PNG drop');
assert.equal(await evaluate('modo'),expected);
await fill('[data-cm]',10);await fill('[data-q]',4);await packed(4);
}
// An older image load must not overwrite a newer edit, even before debounce.
await evaluate(`(()=>{window.realLoad=carregarImagem;window.delayed=[];
carregarImagem=f=>new Promise(resolve=>delayed.push(()=>realLoad(f).then(resolve)));
})()`);
await fill('[data-q]',5);
await waitFor(()=>evaluate('delayed.length===1'),'delayed preview');
await fill('[data-q]',7);
await evaluate(`carregarImagem=realLoad;delayed[0]()`);
await packed(7);
// 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"]');
const doc=await call('DOM.getDocument');
const input=await call('DOM.querySelector',{nodeId:doc.root.nodeId,selector:'#inp'});
await call('DOM.setFileInputFiles',{nodeId:input.nodeId,files:[resolve(process.env.ARTWORK_FILE)]});
await waitFor(()=>evaluate('artes.length===1 && !!artes[0].src'),'supplied artwork');
await fill('[data-cm]',20);await fill('[data-q]',6);layout=await packed(6);
assert.ok(new Set(layout.pos.map(p=>p.x)).size>1);
assert.ok(new Set(layout.pos.map(p=>p.y)).size>1);
await screenshot('artwork-supplied');
console.log('Supplied image:',JSON.stringify(layout));
}
assert.deepEqual(errors,[]);
console.log('PASS: JPG/PNG upload and drop, packed copies, 5 mm gaps, aspect ratio, live width/quantity/rotation/mirror, metres, ready-sheet pricing/repeats, UV, stale renders, strict inline CSP.');
}catch(error){console.error(error);if(stderr)console.error(stderr.slice(-1200));process.exitCode=1;}
finally{ws?.close();chrome.kill();server.closeAllConnections();await new Promise(r=>server.close(r));}

140
tests/browser_test.mjs Normal file
View File

@@ -0,0 +1,140 @@
// Dependency-free Chrome DevTools smoke test. Node 22+ and Chrome required.
import {spawn} from 'node:child_process';
import {mkdtemp,readFile,writeFile,mkdir} from 'node:fs/promises';
import {tmpdir} from 'node:os';
import {resolve} from 'node:path';
import assert from 'node:assert/strict';
try {
for(const line of (await readFile('.env','utf8')).split('\n')) {
if(!line.startsWith('#') && line.includes('=')) {
const i=line.indexOf('=');process.env[line.slice(0,i)]??=line.slice(i+1);
}
}
}catch(e){if(e.code!=='ENOENT')throw e;}
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',
'--remote-debugging-port=0','--user-data-dir='+profile,'about:blank'
],{stdio:['ignore','ignore','pipe']});
const pause=ms=>new Promise(r=>setTimeout(r,ms));
let stderr='';chrome.stderr.on('data',data=>stderr+=data.toString());
const clients=[];
async function waitFor(fn,description,timeout=30000){const end=Date.now()+timeout;while(Date.now()<end){if(await fn())return;await pause(200);}throw new Error('Timeout: '+description);}
class Page {
constructor(ws){this.ws=ws;this.next=0;this.pending=new Map();this.errors=[];
ws.onmessage=event=>{const d=JSON.parse(event.data);if(d.id){const p=this.pending.get(d.id);if(p){this.pending.delete(d.id);d.error?p.reject(d.error):p.resolve(d.result);}}else if(d.method==='Runtime.exceptionThrown')this.errors.push(d.params.exceptionDetails);};
}
call(method,params={}){return new Promise((resolve,reject)=>{const id=++this.next;this.pending.set(id,{resolve,reject});this.ws.send(JSON.stringify({id,method,params}));});}
async eval(expression){const result=await this.call('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});if(result.exceptionDetails)throw new Error(JSON.stringify(result.exceptionDetails));return result.result.value;}
async click(selector){await this.eval(`document.querySelector(${JSON.stringify(selector)}).click()`);}
async fill(selector,value,event='input'){await this.eval(`(()=>{const el=document.querySelector(${JSON.stringify(selector)});el.value=${JSON.stringify(value)};el.dispatchEvent(new Event(${JSON.stringify(event)},{bubbles:true}));})()`);}
async text(){return this.eval('document.body.innerText');}
async screenshot(path){const result=await this.call('Page.captureScreenshot',{format:'png',captureBeyondViewport:false});await writeFile(path,Buffer.from(result.data,'base64'));}
}
try{
let port;
await waitFor(async()=>{try{port=Number((await readFile(profile+'/DevToolsActivePort','utf8')).split('\n')[0]);return true;}catch{return false;}},'Chrome startup');
async function page(url){
const tab=await (await fetch('http://localhost:'+port+'/json/new?'+encodeURIComponent(url),{method:'PUT'})).json();
const ws=new WebSocket(tab.webSocketDebuggerUrl);await new Promise((r,j)=>{ws.onopen=r;ws.onerror=j;});
const p=new Page(ws);clients.push(p);await p.call('Runtime.enable');await p.call('Page.enable');
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));
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});
await site.eval(`(()=>{window.xssProbe=0;abrir('file');sel([new File(['test'],'<img src=x onerror=window.xssProbe=1>.cdr')]);})()`);
await pause(500);
assert.equal(await site.eval('window.xssProbe'),0);
assert.equal(await site.eval('document.querySelectorAll("#lista img[onerror]").length'),0);
assert.ok((await site.text()).includes('<img src=x onerror=window.xssProbe=1>.cdr'));
await site.eval(`(()=>{recusa([new File(['test'],'bad.<img src=x onerror=window.xssProbe=1>')]);})()`);
await pause(200);assert.equal(await site.eval('window.xssProbe'),0);
await site.call('Page.setBypassCSP',{enabled:false});
await site.call('Page.reload');
await waitFor(()=>site.eval('typeof window.dtfCheckout === "function"'),'reload after security probe');
await site.click('[data-modo="file"]');
await site.click('[data-cam="tabela"]');
const root=await site.call('DOM.getDocument');
const input=await site.call('DOM.querySelector',{nodeId:root.root.nodeId,selector:'#inp'});
await site.call('DOM.setFileInputFiles',{nodeId:input.nodeId,files:[resolve('tests/fixtures/local-test.cdr')]});
await waitFor(()=>site.eval('!!document.querySelector("[data-comp]")'),'manual length field');
await site.fill('[data-comp]','1.01','change');
await waitFor(()=>site.eval('!!itemAtual'),'cart calculation');
assert.equal(await site.eval('itemAtual.total'),21.89);
await site.fill('#fCnpj','11222333000181');
await site.fill('#fZap','11999999999');
await site.fill('#fMail','local-browser@example.test');
await pause(800);
await site.call('Page.reload');
await waitFor(()=>site.eval('typeof pedido!=="undefined" && pedido.length===1'),'persistent cart recovery');
assert.equal(await site.eval('pedido[0].localFiles[0].name'),'local-test.cdr');
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);
const qid=await site.eval('localStorage.getItem("dtf-quote")');
const kanban=await page('http://localhost:'+(process.env.KANBAN_PORT||8081));
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()');
await waitFor(async()=> (await kanban.text()).includes(qid.slice(0,8)),'quote on Kanban');
assert.equal(await kanban.eval('sessionStorage.getItem("dtf-operator")'),null);
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');
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()');
await waitFor(async()=> (await site.text()).includes('Pedido #'),'test payment');
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`);
// 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'){
await kanban.eval(`(()=>{const card=document.querySelector('[data-order="${oid}"]');[...card.querySelectorAll('button')].find(x=>x.textContent==='Arquivos de produção').click();})()`);
await waitFor(()=>kanban.eval(`!!document.querySelector('[data-order="${oid}"] [data-final-item]')`),'final upload controls');
const doc=await kanban.call('DOM.getDocument');
const input=await kanban.call('DOM.querySelector',{nodeId:doc.root.nodeId,selector:`[data-order="${oid}"] [data-final-item]`});
await kanban.call('DOM.setFileInputFiles',{nodeId:input.nodeId,files:[resolve('tests/fixtures/local-test.cdr')]});
await kanban.fill(`[data-order="${oid}"] input[placeholder="Nota da revisão"]`,'Browser test final file');
await kanban.eval(`(()=>{const form=document.querySelector('[data-order="${oid}"] form');form.querySelector('[type=checkbox]').click();form.requestSubmit();})()`);
await waitFor(()=>kanban.eval(`board.orders.find(o=>o.id==='${oid}').version===2`),'final file approval');
}
await kanban.eval(`(()=>{const card=document.querySelector('[data-order="${oid}"]');[...card.querySelectorAll('button')].find(x=>x.textContent===${JSON.stringify('→ '+title)}).click();})()`);
await waitFor(()=>kanban.eval(`board.orders.find(o=>o.id==='${oid}').state==='${state}'`),'transition '+state);
}
// Reload proves the board is persisted on the backend.
await kanban.call('Page.reload');
await waitFor(()=>kanban.eval(`typeof board!=='undefined' && !!board && board.orders.some(o=>o.id==='${oid}'&&o.state==='fin')`),'persisted board');
await mkdir('output/local',{recursive:true});
await site.eval('window.scrollTo({top:0,behavior:"instant"})');
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);
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');
await portal.fill('#register-password','local-browser-password-123');
await portal.eval('document.getElementById("register").requestSubmit()');
await waitFor(()=>portal.eval('document.getElementById("auth").hidden'),'customer registration');
assert.ok((await portal.text()).includes('Finalizado'));
await portal.screenshot('output/local/portal.png');
// 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');
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);
assert.deepEqual(portal.errors,[]);
assert.deepEqual(site.errors,[]);assert.deepEqual(kanban.errors,[]);
console.log('PASS: browser Site upload → operator quote → local paid order → all main Kanban states → reload persistence. Order '+oid);
console.log('Screenshots: output/local/site.png and output/local/kanban.png');
console.log('PASS: filename XSS escaping with CSP bypassed, no stored operator password, logout clears browser file blobs.');
}catch(error){console.error(error);if(stderr)console.error(stderr.slice(-1500));process.exitCode=1;}
finally{for(const p of clients)p.ws.close();chrome.kill();}

4
tests/fixtures/local-test.cdr vendored Normal file
View File

@@ -0,0 +1,4 @@
DTF LOCAL UPLOAD TEST ONLY
This is a harmless text fixture with a .cdr extension, not printable artwork.
Use the manual-length path: 1.01 metres, grade 0, pickup = BRL 21.89.
It intentionally requires no artwork parsing or automatic pre-flight.

34
tests/retention_test.py Normal file
View File

@@ -0,0 +1,34 @@
"""Run inside the API container. Creates only synthetic retention fixtures."""
from datetime import datetime, timedelta, timezone
from uuid import uuid4
from botocore.exceptions import ClientError
from app.adapters import LocalS3Storage
from app.core.db import connect
from app.worker import cleanup
def run():
storage=LocalS3Storage()
ids=[]
for expired in (True,False):
uid=uuid4();ids.append(uid);key=f'originals/{uid}'
upload=storage.begin(key)
result=storage.client.upload_part(Bucket=storage.bucket,Key=key,UploadId=upload,PartNumber=1,Body=b'LOCAL RETENTION TEST')
storage.complete(key,upload,[{'PartNumber':1,'ETag':result['ETag']}])
expiry=datetime.now(timezone.utc)+timedelta(days=-1 if expired else 1)
with connect() as c:
c.execute('INSERT INTO dtf_local.uploads(id,owner,name,size,object_key,multipart_id,complete,expires_at) VALUES(%s,%s,%s,%s,%s,%s,true,%s)',
(uid,uuid4(),'LOCAL-RETENTION-TEST.txt',len(b'LOCAL RETENTION TEST'),key,upload,expiry))
cleanup()
with connect() as c:
assert c.execute('SELECT purged_at FROM dtf_local.uploads WHERE id=%s',(ids[0],)).fetchone()['purged_at']
assert not c.execute('SELECT purged_at FROM dtf_local.uploads WHERE id=%s',(ids[1],)).fetchone()['purged_at']
try:storage.size(f'originals/{ids[0]}');raise AssertionError('Expired bytes still exist')
except ClientError as exc:assert exc.response['ResponseMetadata']['HTTPStatusCode']==404
assert storage.size(f'originals/{ids[1]}')==len(b'LOCAL RETENTION TEST')
# Clean only the other fixture just created above, leaving metadata intact.
with connect() as c:
c.execute("UPDATE dtf_local.uploads SET expires_at=now()-interval '1 second' WHERE id=%s",(ids[1],))
cleanup()
print('PASS: expired bytes removed, unexpired bytes preserved, upload metadata retained. Synthetic fixture bytes cleaned up.')
if __name__=='__main__':run()

View File

@@ -0,0 +1,87 @@
"""Run inside API container: permissions, hashing and fail-closed scanner unit checks."""
import hashlib
from unittest.mock import patch
from botocore.exceptions import ClientError
from app.core.db import connect
from app.adapters import LocalS3Storage
from app.core.auth import client_ip, password_hash, password_matches
from app.scanning import ClamAV, require_clean
from fastapi import HTTPException
class FakeRequest:
def __init__(self, headers=None, peer='10.0.0.2'):
self.headers=headers or {}
self.client=type('C',(),{'host':peer})() if peer else None
def check_client_ip():
"""The gateway hands one address; a direct peer falls back to its own."""
# Gateway-set header wins over the connection peer, which is the gateway.
assert client_ip(FakeRequest({'x-forwarded-for':'198.51.100.9'}))=='198.51.100.9'
# Only the first entry is used, and it is length-capped.
assert client_ip(FakeRequest({'x-forwarded-for':'198.51.100.9, 10.0.0.2'}))=='198.51.100.9'
assert len(client_ip(FakeRequest({'x-forwarded-for':'a'*500})))<=64
# No header: the peer address, never a constant shared by every request.
assert client_ip(FakeRequest(peer='192.0.2.5'))=='192.0.2.5'
assert client_ip(FakeRequest({'x-forwarded-for':' '},peer='192.0.2.5'))=='192.0.2.5'
assert client_ip(FakeRequest(peer=None))=='unknown'
print('PASS: client address resolution for rate-limit buckets and audit events')
def check_operator_accounts():
"""Accounts are per person, and disabling one ends its access at once."""
from uuid import uuid4
from app.core.auth import password_hash, password_matches
from app.operators import seed_from_environment, set_active
email='runtime-check-'+uuid4().hex[:8]+'@example.test'
with connect() as c:
c.execute('INSERT INTO dtf_local.operators(id,email,name,password_hash) VALUES(%s,%s,%s,%s)',
(uuid4(), email, 'Runtime Check', password_hash('runtime-check-password')))
row=c.execute('SELECT * FROM dtf_local.operators WHERE email=%s',(email,)).fetchone()
assert row['active'] and password_matches('runtime-check-password', row['password_hash'])
assert not password_matches('wrong', row['password_hash'])
# Seeding is once-only: a password changed here must survive a redeploy.
c.execute("INSERT INTO dtf_local.operator_sessions(token_hash,username) VALUES(%s,%s)",
('runtime-check-'+uuid4().hex, email))
set_active(email, False)
with connect() as c:
row=c.execute('SELECT active FROM dtf_local.operators WHERE email=%s',(email,)).fetchone()
sessions=c.execute('SELECT count(*) AS n FROM dtf_local.operator_sessions WHERE username=%s',
(email,)).fetchone()['n']
assert not row['active'], 'disable did not deactivate'
assert sessions==0, 'disable left an open session behind'
c.execute('DELETE FROM dtf_local.operators WHERE email=%s',(email,))
print('PASS: per-operator accounts, password verification, immediate revocation on disable')
def run():
check_client_ip()
check_operator_accounts()
with connect() as c:
role=c.execute('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user').fetchone()
assert not any(role.values()),role
assert not c.execute("SELECT has_schema_privilege(current_user,'dtf_local','CREATE') AS allowed").fetchone()['allowed']
storage=LocalS3Storage()
storage.health()
visible={bucket['Name'] for bucket in storage.client.list_buckets()['Buckets']}
assert visible=={storage.bucket},visible
for call in (lambda:storage.client.get_bucket_policy(Bucket=storage.bucket),
lambda:storage.client.get_object(Bucket=storage.bucket,Key='outside-runtime-prefix/test.cdr')):
try:call();raise AssertionError('Runtime storage credentials have excessive privilege')
except ClientError as error:assert error.response['ResponseMetadata']['HTTPStatusCode']==403
password='test-password-for-hash'
salt='00'*16
old='scrypt$'+salt+'$'+hashlib.scrypt(password.encode(),salt=bytes.fromhex(salt),n=16384,r=8,p=1).hex()
assert password_matches(password,old)
new=password_hash(password)
assert new.startswith('scrypt-v2$') and password_matches(password,new)
assert not password_matches('wrong',new)
for state in ('pending','error','rejected'):
try:require_clean({'complete':True,'scan_state':state});raise AssertionError('Unscanned file released')
except HTTPException as error:assert error.status_code==409
require_clean({'complete':True,'scan_state':'clean'})
assert ClamAV().ping() and ClamAV().version().startswith('ClamAV ')
assert ClamAV().scan(None,134217729)[0]=='rejected'
with patch('app.scanning.socket.create_connection',side_effect=OSError('offline')):
try:ClamAV().scan(None,1);raise AssertionError('Offline scanner returned success')
except OSError:pass
print('PASS: runtime DB/S3 least privilege, legacy/current password hashes, quarantine states and scanner size/offline behavior')
if __name__=='__main__':run()

17
tests/scanning_test.py Normal file
View File

@@ -0,0 +1,17 @@
"""Harmless EICAR anti-malware test and blocked download/quote regressions."""
from uuid import uuid4
from tests.smoke_test import Client, upload_bytes
def run():
customer=Client();customer.call('/session')
# Standard antivirus test string, not executable malware.
marker=b'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*'
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)
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.')
if __name__=='__main__':run()

84
tests/security_test.py Normal file
View File

@@ -0,0 +1,84 @@
"""Non-destructive localhost security regressions. Leaves tiny test upload reservations."""
import base64
import os
from urllib.error import HTTPError
from urllib.request import Request, urlopen
from urllib.parse import urlparse, parse_qs
from uuid import uuid4
from tests.smoke_test import Client, BASE, with_host
def raw(path, expected, headers=None, body=None):
request=Request(BASE+path, data=body, headers=with_host(headers))
try:
with urlopen(request,timeout=10) as response:
assert response.status==expected
return response.headers
except HTTPError as error:
assert error.code==expected,(path,error.code,expected)
return error.headers
def run():
headers=raw('/',200)
policy=headers['Content-Security-Policy']
assert "script-src-attr 'none'" in policy and "frame-ancestors 'none'" in policy
# The Site has no inline script, so the policy needs no hash allowlist at all.
# Assert the property that matters rather than the mechanism: nothing inline
# executes, and any hash that does appear was added deliberately at build time.
script_src = next(d.strip() for d in policy.split(';') if d.strip().startswith('script-src '))
assert "'unsafe-inline'" not in script_src and "'unsafe-eval'" not in script_src, script_src
assert script_src == "script-src 'self'", script_src
assert "object-src 'none'" in policy
assert 'cdnjs' not in policy, 'pdf.js is vendored; no CDN belongs in the policy'
raw('/api/health',400,{'Host':'attacker.invalid'})
raw('/api/account/logout',403,{'Content-Type':'application/json','Origin':'https://attacker.invalid'},b'{}')
raw('/api/account/logout',403,{'Content-Type':'application/json','Origin':'http://localhost:9999'},b'{}')
print('PASS: CSP, frame protection, Host and cross-origin rejection')
operator=Client()
credentials={'email':os.environ.get('OPERATOR_EMAIL','operator@example.test'),'password':os.environ.get('OPERATOR_PASSWORD','local-operator-only')}
encoded=base64.b64encode((credentials['email']+':'+credentials['password']).encode()).decode()
raw('/api/operator/board',401,{'Authorization':'Basic '+encoded})
operator.call('/operator/login',credentials)
token=next(c for c in operator.jar if c.name=='dtf_operator')
assert token.has_nonstandard_attr('HttpOnly') and token.get_nonstandard_attr('SameSite')=='strict'
assert token.path=='/api/operator'
operator.call('/operator/board')
replay=Client();replay.jar.set_cookie(token)
operator.call('/operator/logout',{})
replay.call('/operator/board',expected=401)
print('PASS: Basic rejected; HttpOnly scoped operator session; server-side logout revocation')
client=Client();client.call('/session')
client.call('/uploads',{'name':'payload.html','size':1},expected=422)
uid=client.call('/uploads',{'name':'SECURITY-PART.cdr','size':3})['id']
url=client.call('/uploads/'+uid+'/parts/1',{})['url']
assert 'content-length' in parse_qs(urlparse(url).query)['X-Amz-SignedHeaders'][0]
try:
urlopen(Request(url,data=b'toolong',method='PUT'),timeout=10)
raise AssertionError('Signed part accepted wrong length')
except HTTPError as error:assert error.code==403,error.code
with urlopen(Request(url,data=b'abc',method='PUT'),timeout=10) as response:assert response.status==200
client.call('/uploads/'+uid+'/complete',{})
count=int(os.environ.get('MAX_PENDING_UPLOADS','10'))
for i in range(count):client.call('/uploads',{'name':'SECURITY-PENDING.cdr','size':1})
client.call('/uploads',{'name':'SECURITY-OVER-LIMIT.cdr','size':1},expected=429)
print('PASS: extension allowlist, exact multipart Content-Length signature, pending upload quota')
# Unique identity avoids locking out the real local operator.
attacker=Client();email='test-'+uuid4().hex+'@example.test'
for _ in range(10):attacker.call('/operator/login',{'email':email,'password':'invalid'},expected=401)
attacker.call('/operator/login',{'email':email,'password':'invalid'},expected=429)
print('PASS: operator login throttling (only synthetic account bucket exhausted)')
# Guest sessions are limited per source, not once for the whole deployment.
# Keyed on the environment name this was a single global bucket of 120 per
# 15 minutes, which the suites above would already have eaten into.
for _ in range(25):
Client().call('/session')
# A forged forwarded address must not let a client pick another bucket: the
# gateway overwrites the header, so these count against the real source too.
for _ in range(5):
raw('/api/session',200,{'X-Forwarded-For':'203.0.113.7'})
print('PASS: guest sessions limited per source, forwarded address not client-controlled')
if __name__=='__main__':run()

159
tests/smoke_test.py Normal file
View File

@@ -0,0 +1,159 @@
"""Local stack integration checks; creates and retains clearly named test orders.
Run: python3 -m tests.smoke_test. Standard library only. Honors .env/environment.
"""
from concurrent.futures import ThreadPoolExecutor
import hashlib
import http.cookiejar
import json
import os
from pathlib import Path
import time
from urllib.error import HTTPError
from urllib.request import build_opener, HTTPCookieProcessor, Request, urlopen
from uuid import uuid4
if Path('.env').exists():
for line in Path('.env').read_text().splitlines():
if line.strip() and not line.startswith('#') and '=' in line:
key,value=line.split('=',1)
os.environ.setdefault(key,value)
# CI runs these from a container on the stack's own network, because a runner
# container cannot reach ports published on the host's loopback. SITE_BASE_URL
# points at the gateway by service name; SITE_HOST_HEADER keeps the Host the
# gateway and TrustedHostMiddleware expect, so the security configuration under
# test stays identical to a developer's localhost run.
BASE=os.environ.get('SITE_BASE_URL') or 'http://localhost:'+os.environ.get('SITE_PORT','8080')
HOST_HEADER=os.environ.get('SITE_HOST_HEADER')
def with_host(headers=None):
headers=dict(headers or {})
if HOST_HEADER and not any(k.lower()=='host' for k in headers):
headers['Host']=HOST_HEADER
return headers
class Client:
def __init__(self):
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):
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)
try:
with self.opener.open(request,timeout=30) as response:
assert response.status==expected,(path,response.status,expected)
return json.load(response)
except HTTPError as exc:
if exc.code!=expected:raise AssertionError((path,exc.code,exc.read().decode())) from exc
return json.load(exc)
def wait_scan(client, uid, operator=False, expected='clean'):
prefix='/operator/uploads' if operator else '/uploads'
deadline=time.monotonic()+150
while time.monotonic()<deadline:
state=client.call(prefix+'/'+uid,operator=operator)
if state['scan_state'] in ('clean','rejected','error'):
assert state['scan_state']==expected,state
return state
time.sleep(0.5)
raise AssertionError('Malware scan did not finish')
def upload_bytes(client, content, name='LOCAL-TEST.cdr', order_id=None, expected_scan='clean'):
operator=order_id is not None
prefix='/operator/uploads' if operator else '/uploads'
start='/operator/orders/'+order_id+'/uploads' if operator else prefix
data=client.call(start,{'name':name,'size':len(content)},operator=operator)
uid=data['id'];size=data['part_bytes']
for offset in range(0,len(content),size):
url=client.call(prefix+'/'+uid+'/parts/'+str(offset//size+1),{},operator=operator)['url']
with urlopen(Request(url,data=content[offset:offset+size],method='PUT'),timeout=30) as response:
assert response.status==200
client.call(prefix+'/'+uid+'/complete',{},operator=operator)
wait_scan(client,uid,operator,expected_scan)
return uid
def run():
client=Client();other=Client()
config=client.call('/session');other.call('/session')
assert client.call('/health')['integrations']=='fake'
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']
other.call('/uploads/'+uid,expected=404)
other.call('/uploads/'+uid+'/parts/1',{},expected=404)
signed=client.call('/uploads/'+uid+'/parts/1',{})['url']
with urlopen(Request(signed,data=content[:block],method='PUT'),timeout=30) as response:assert response.status==200
assert client.call('/uploads/'+uid)['parts']==[1]
client.call('/uploads/'+uid+'/complete',{},expected=409)
signed=client.call('/uploads/'+uid+'/parts/2',{})['url']
with urlopen(Request(signed,data=content[block:],method='PUT'),timeout=30) as response:assert response.status==200
client.call('/uploads/'+uid+'/complete',{})
client.call('/uploads/'+uid+'/complete',{})
wait_scan(client,uid)
client.call('/uploads/'+uid+'/parts/1',{},expected=409)
download=client.call('/operator/uploads/'+uid+'/download',operator=True)
with urlopen(download['url'],timeout=30) as response:assert hashlib.sha256(response.read()).digest()==hashlib.sha256(content).digest()
unsigned=download['url'].split('?')[0]
try:urlopen(unsigned,timeout=10);raise AssertionError('Bucket must be private')
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')]
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,'customer':{**draft['customer'],'cnpj':'11111111111111'}},expected=422)
quote=client.call('/quotes',draft)
assert client.call('/quotes',draft)['id']==quote['id']
client.call('/quotes',{**draft,'freight':{'service':'pickup'}},expected=409)
qid=quote['id']
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)
# 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)
assert approved['items'][0]['total_cents']==2189
assert approved['total_cents']==2189+6972+19572+23492+int(os.environ.get('MOCK_FREIGHT_CENTS','1500'))
client.call('/operator/quotes/'+qid+'/approve',{'items':items},operator=True,expected=409)
client.call('/orders/dev-paid',{'quote_id':qid,'total_cents':1},expected=422)
other.call('/orders/dev-paid',{'quote_id':qid},expected=404)
# Concurrent retries must produce precisely one payment/order/outbox pair.
with ThreadPoolExecutor(max_workers=4) as executor:
paid=list(executor.map(lambda _:client.call('/orders/dev-paid',{'quote_id':qid}),range(4)))
assert len({p['id'] for p in paid})==1
order=paid[0];oid=order['id']
assert order['payment']['status']=='paid' and order['snapshot']==approved
board=client.call('/operator/board',operator=True)
assert len([o for o in board['orders'] if o['quote_id']==qid])==1
client.call('/operator/orders/'+oid+'/move',{'state':'fin','version':0},operator=True,expected=409)
client.call('/operator/orders/'+oid+'/move',{'state':'cor','version':0},operator=True,expected=422)
version=0
for state in ('cor','rec','tra','fil','imp','fin'):
if state=='fil':
client.call('/operator/orders/'+oid+'/move',{'state':'fil','version':version},operator=True,expected=409)
files=[{'item_index':i,'upload_id':upload_bytes(client,b'LOCAL FINAL FIXTURE '+str(i).encode(),order_id=oid)} for i in range(4)]
result=client.call('/operator/orders/'+oid+'/final-files',{'version':version,'files':files,'note':'Local test manual final-file approval'},operator=True)
version=result['version']
moved=client.call('/operator/orders/'+oid+'/move',{'state':state,'version':version,'reason':'Local test correction' if state=='cor' else ''},operator=True)
version+=1;assert moved['version']==version
client.call('/operator/orders/'+oid+'/move',{'state':'rec','version':0},operator=True,expected=409)
assert len(client.call('/operator/orders/'+oid+'/history',operator=True))==6
print('PASS: all modes, authoritative review/prices/freight, tamper rejection, concurrent payment idempotency, transitions and history')
deadline=time.monotonic()+30
while time.monotonic()<deadline:
events=[e for e in client.call('/operator/board',operator=True)['events'] if e['payload']['order_id']==oid]
if len(events)==8 and all(e['delivered_at'] and e['receipt'] for e in events):break
time.sleep(1)
else:raise AssertionError('Mock outbox did not drain')
assert len({e['event_key'] for e in events})==8
print(f"PASS: 8 durable fake receipts. Local test order #{order['number']} retained in Finalizado.")
return oid
if __name__=='__main__':run()

View File

@@ -0,0 +1,46 @@
import re
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
NAME_VERSION = re.compile(r'^([A-Za-z0-9_.-]+)==([^\s\\]+)', re.MULTILINE)
def normalized(name):
return re.sub(r'[-_.]+', '-', name).lower()
class DependencyLockTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.direct_text = (ROOT / 'infra/requirements.txt').read_text()
cls.lock_text = (ROOT / 'infra/requirements.lock').read_text()
def test_every_direct_pin_matches_lock(self):
direct = {normalized(name): version for name, version in
NAME_VERSION.findall(self.direct_text)}
locked = {normalized(name): version for name, version in
NAME_VERSION.findall(self.lock_text)}
self.assertTrue(direct)
self.assertEqual({name: locked.get(name) for name in direct}, direct)
def test_every_locked_package_has_sha256_hash(self):
matches = list(NAME_VERSION.finditer(self.lock_text))
self.assertTrue(matches)
for index, match in enumerate(matches):
end = matches[index + 1].start() if index + 1 < len(matches) else len(self.lock_text)
block = self.lock_text[match.start():end]
hashes = re.findall(r'--hash=sha256:([0-9a-f]{64})(?:\s|\\)', block)
self.assertTrue(hashes, f'{match.group(1)} has no SHA-256 artifact hash')
def test_image_build_requires_the_lock_and_hashes(self):
dockerfile = (ROOT / 'infra/Dockerfile').read_text()
self.assertIn('requirements.lock', dockerfile)
self.assertIn('--require-hashes -r infra/requirements.lock', dockerfile)
def test_reproducible_generator_is_recorded(self):
self.assertIn('./local/lock_dependencies.sh', self.lock_text[:300])
if __name__ == '__main__':
unittest.main()

42
tests/test_pricing.py Normal file
View File

@@ -0,0 +1,42 @@
"""Run from repository root: python3 -m unittest tests.test_pricing -v."""
import json
from pathlib import Path
import subprocess
import unittest
from app.core.pricing import price, TIERS
class PricingTests(unittest.TestCase):
def test_every_grade_against_actual_site_javascript(self):
# The ladders live with the rest of the Site's behaviour; this test exists
# to prove the server agrees with whatever the customer is shown.
source = Path('web/site-config.js').read_text()
tiers = source.split('const FAIXAS=')[1].split('\n};',1)[0]+'\n}'
calculator = source.split('const cobrar=')[1].split(';',1)[0]
js = f'const FAIXAS={tiers};const MINIMO_M=1;const cobrar={calculator};'
js += '''const results=[];for(const mode of Object.keys(FAIXAS))for(let grade=0;grade<=100;grade++)
for(const metres of [0.01,0.99,1,1.01,1.1,2.75,2.8,2.8000000000000003,19.99,60,12000]){
const unit=FAIXAS[mode].find(x=>grade>=x[0])[1];
results.push([mode,String(metres),grade,cobrar(metres),Math.round(unit*100),Math.round(cobrar(metres)*unit*100)]);
}process.stdout.write(JSON.stringify(results));'''
cases = json.loads(subprocess.check_output(['node','-e',js],text=True))
for mode,metres,grade,billed,unit,total in cases:
with self.subTest(mode=mode,metres=metres,grade=grade):
result=price(mode,metres,grade)
self.assertEqual(float(result['billed_metres']),billed)
self.assertEqual(result['unit_cents'],unit)
self.assertEqual(result['total_cents'],total)
def test_assembly_is_included_at_every_tier(self):
for grade in range(101):
self.assertEqual(price('avulsa','1',grade)['total_cents']-price('file','1',grade)['total_cents'],1000)
self.assertEqual(price('uv','1',grade)['total_cents']-price('uvfile','1',grade)['total_cents'],1400)
def test_invalid_inputs(self):
for value in ('0','-1','NaN','Infinity','12001'):
with self.assertRaises(ValueError):price('file',value,90)
for grade in (-1,101,True,89.5):
with self.assertRaises(ValueError):price('file','1',grade)
with self.assertRaises(ValueError):price('other','1',90)
if __name__ == '__main__':
unittest.main()

93
tests/test_secrets.py Normal file
View File

@@ -0,0 +1,93 @@
"""Docker secret-file resolution. Standard library only; no stack required."""
import tempfile
import unittest
from pathlib import Path
from app.core.secrets import SECRET_FILE_SETTINGS, load, read_secret
class SecretFileTests(unittest.TestCase):
def setUp(self):
self.dir = tempfile.TemporaryDirectory()
self.addCleanup(self.dir.cleanup)
def secret(self, content, name='secret'):
path = Path(self.dir.name) / name
path.write_text(content, encoding='utf-8')
return str(path)
def test_resolves_file_into_plain_setting(self):
env = {'DATABASE_URL_FILE': self.secret('postgresql://u:p@db:5432/dtf\n')}
self.assertEqual(load(env), ['DATABASE_URL'])
self.assertEqual(env['DATABASE_URL'], 'postgresql://u:p@db:5432/dtf')
def test_strips_only_one_trailing_newline(self):
# A generated password may legitimately end in whitespace, so only the
# newline `docker secret` or an editor appends may be removed.
self.assertEqual(read_secret(self.secret('p@ss \n')), 'p@ss ')
self.assertEqual(read_secret(self.secret('p@ss\n\n')), 'p@ss\n')
self.assertEqual(read_secret(self.secret('p@ss\r\n')), 'p@ss')
self.assertEqual(read_secret(self.secret('p@ss')), 'p@ss')
def test_preserves_characters_that_would_break_a_url(self):
value = 'p@ss:w/rd?#[]&=+$ ,%'
env = {'OPERATOR_PASSWORD_FILE': self.secret(value + '\n')}
load(env)
self.assertEqual(env['OPERATOR_PASSWORD'], value)
def test_resolves_every_documented_production_setting(self):
env = {f'{name}_FILE': self.secret(f'value-for-{name}', name)
for name in SECRET_FILE_SETTINGS}
load(env)
for name in SECRET_FILE_SETTINGS:
self.assertEqual(env[name], f'value-for-{name}')
def test_missing_file_fails_closed(self):
env = {'DATABASE_URL_FILE': str(Path(self.dir.name) / 'absent')}
with self.assertRaises(RuntimeError) as caught:
load(env)
self.assertIn('DATABASE_URL_FILE', str(caught.exception))
self.assertNotIn('DATABASE_URL', env)
def test_empty_secret_fails_closed(self):
with self.assertRaises(RuntimeError):
load({'OPERATOR_PASSWORD_FILE': self.secret('\n')})
def test_empty_path_fails_closed(self):
with self.assertRaises(RuntimeError):
load({'OPERATOR_PASSWORD_FILE': ' '})
def test_value_and_file_together_is_ambiguous(self):
env = {'OPERATOR_PASSWORD': 'inline',
'OPERATOR_PASSWORD_FILE': self.secret('from-file')}
with self.assertRaises(RuntimeError):
load(env)
self.assertEqual(env['OPERATOR_PASSWORD'], 'inline')
def test_never_puts_a_secret_in_the_error_text(self):
value = 'super-secret-value'
env = {'TINY_TOKEN': value, 'TINY_TOKEN_FILE': self.secret(value)}
with self.assertRaises(RuntimeError) as caught:
load(env)
self.assertNotIn(value, str(caught.exception))
def test_ignores_a_bare_suffix_and_leaves_other_settings_alone(self):
env = {'_FILE': '/nowhere', 'APP_ENV': 'production'}
self.assertEqual(load(env), [])
self.assertEqual(env['APP_ENV'], 'production')
def test_documented_list_matches_the_production_stack(self):
stack = Path(__file__).resolve().parent.parent / 'deploy' / 'stack.yaml'
if not stack.exists():
self.skipTest('production stack definition not present')
text = stack.read_text()
# POSTGRES_PASSWORD_FILE is consumed by the database image, not by us.
used = {line.split(':')[0].strip() for line in text.splitlines()
if '_FILE:' in line and 'POSTGRES_PASSWORD_FILE' not in line}
for key in used:
self.assertIn(key[:-len('_FILE')], SECRET_FILE_SETTINGS,
f'{key} is passed by the stack but undocumented in secrets.py')
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,56 @@
import tempfile
import unittest
from pathlib import Path
from ops.staging_readiness import read_config, validate
VALID = '''
APP_ENV=staging
STAGING_APPROVED_BY=business-and-technical-owners
STAGING_PUBLIC_ORIGIN=https://staging.example.invalid
STAGING_S3_ENDPOINT=https://example.r2.cloudflarestorage.com
STAGING_S3_BUCKET=dtf-staging-artwork
STAGING_DATABASE_MODE=dedicated-container
STAGING_SECRET_SOURCE=portainer-secrets
STAGING_BACKUP_DESTINATION=separate-encrypted-r2-bucket
STAGING_FREIGHT_PROVIDER=provider-sandbox
STAGING_PAYMENT_PROVIDER=mercado-pago-sandbox
STAGING_ERP_PROVIDER=tiny-olist-sandbox
STAGING_WHATSAPP_PROVIDER=provider-sandbox
STAGING_ALERT_OWNER=operations-team
STAGING_ROLLBACK_OWNER=technical-team
'''
class StagingReadinessTests(unittest.TestCase):
def parse(self, text):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / 'staging.env'
path.write_text(text)
return read_config(path)
def test_complete_non_secret_metadata_passes(self):
self.assertEqual(validate(self.parse(VALID)), [])
def test_local_endpoint_and_fake_provider_are_blocked(self):
values = self.parse(VALID.replace(
'https://example.r2.cloudflarestorage.com', 'http://localhost:9000'
).replace('provider-sandbox', 'fake', 1))
errors = validate(values)
self.assertTrue(any('STAGING_S3_ENDPOINT' in error for error in errors))
self.assertTrue(any('STAGING_FREIGHT_PROVIDER' in error for error in errors))
def test_secret_named_setting_is_rejected(self):
with self.assertRaisesRegex(ValueError, 'secrets must not be stored'):
self.parse(VALID + 'MERCADO_PAGO_ACCESS_TOKEN=do-not-store-this\n')
def test_undecided_values_are_blocked(self):
errors = validate(self.parse(VALID.replace(
'STAGING_APPROVED_BY=business-and-technical-owners',
'STAGING_APPROVED_BY=TBD')))
self.assertIn('STAGING_APPROVED_BY is not decided', errors)
if __name__ == '__main__':
unittest.main()

73
tests/workflow_test.py Normal file
View File

@@ -0,0 +1,73 @@
"""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
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]}
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)
order=customer.call('/orders/dev-paid',{'quote_id':q['id']});oid=order['id']
before=list(customer.jar)[0].value
password='local-test-password-'+uuid4().hex
customer.call('/account/register',{'customer':profile,'password':password})
assert customer.call('/account/me')['customer']['mail']==profile['mail']
assert customer.call('/customer/orders')['orders'][0]['id']==oid
# Email/CNPJ do not grant ownership; only current guest session is migrated.
other.call('/customer/orders/'+oid,expected=404)
other.call('/account/login',{'email':profile['mail'],'password':'wrong-password'},expected=401)
revoked=Client()
import http.cookiejar
cookie=http.cookiejar.Cookie(0,'dtf_session',before,None,False,'localhost.local',False,False,'/',True,False,None,True,None,None,{},False)
revoked.jar.set_cookie(cookie)
revoked.call('/customer/orders',expected=401)
account_scope=customer.call('/session')['cart_scope']
cookie.value=account_scope;revoked.jar.set_cookie(cookie)
revoked.call('/customer/orders',expected=401)
other.call('/account/login',{'email':profile['mail'],'password':password})
assert other.call('/customer/orders/'+oid)['id']==oid
print('PASS: registration claims only current guest records, cross-session account login, revoked sessions, owner UUID is not a credential')
def move(state,version):
return customer.call('/operator/orders/'+oid+'/move',{'state':state,'version':version,'reason':'Please replace the artwork' if state=='cor' else ''},operator=True)['version']
version=move('tra',0)
customer.call('/operator/orders/'+oid+'/move',{'state':'fil','version':version},operator=True,expected=409)
final_id=upload_bytes(customer,b'LOCAL FINAL VERSION ONE',order_id=oid)
customer.call('/uploads/'+final_id,expected=404)
body={'version':version,'files':[{'item_index':0,'upload_id':final_id}],'note':'Manually checked final'}
customer.call('/operator/orders/'+oid+'/final-files',body,expected=401)
version=customer.call('/operator/orders/'+oid+'/final-files',body,operator=True)['version']
detail=customer.call('/customer/orders/'+oid)
final=detail['files'][0]
link=customer.call('/customer/orders/'+oid+'/files/'+final['id']+'/download')
assert urlopen(link['url']).read()==b'LOCAL FINAL VERSION ONE'
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)
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)
customer.call('/customer/orders/'+oid+'/corrections',{**payload,'version':0},expected=409)
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)
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)
version=customer.call('/operator/orders/'+oid+'/final-files',{'version':version,'files':[{'item_index':0,'upload_id':new_final}],'note':'Checked corrected final'},operator=True)['version']
for state in ('fil','imp','fin'):version=move(state,version)
detail=other.call('/customer/orders/'+oid)
assert detail['state']=='fin'
assert sum(f['active'] and f['kind']=='final' for f in detail['files'])==1
assert any(h['reason']=='Please replace the artwork' for h in detail['history'])
print('PASS: final-file gate, final revisions, secure customer downloads, correction history/uploads, old final invalidation and reapproval')
old_cookie=list(other.jar)[0].value
other.call('/account/logout',{})
cookie.value=old_cookie;revoked.jar.set_cookie(cookie);revoked.call('/customer/orders',expected=401)
other.call('/session');assert other.call('/customer/orders')['orders']==[]
print('PASS: logout revokes server session and signed-out visitors cannot see account orders')
if __name__=='__main__':run()