Files
dtf-system/tests/kanban_test.py
Cauê Faleiros 352590e63a
All checks were successful
Build and deploy / Validate source (push) Successful in 1m25s
Build and deploy / Integration suite on a real stack (push) Successful in 3m16s
Build and deploy / Secret scan and release gate (push) Successful in 13s
Build and deploy / Publish images (push) Successful in 1m29s
feat: Kanban shows each order's artwork, filters quotes and searches orders and quotes
The customer's browser sends the small picture it already makes of each file
(at most 300 KB, WebP/JPEG/PNG read from the bytes, own uploads only); it goes
when the file's bytes go. Board cards, quote rows, the order panel and the
quote detail show it, with the layout drawing as a second view and as the
fallback for files without a picture.

Cotações gets a customer filter (e-mail, CNPJ, WhatsApp) and product, layout
and resolution-warning chips, with the page bar always shown. The board adds
Entrega, Retirada and stalled-order chips. The top search now covers orders in
any stage and unpaid quotes; CNPJ and phone match by digits only when nothing
but digits and punctuation was typed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:46:33 -03:00

127 lines
6.8 KiB
Python

"""Kanban artwork pictures, quote filters and the search over orders and quotes.
Run against the local stack: python3 -m tests.kanban_test
"""
import base64
import secrets
from urllib.error import HTTPError
from urllib.parse import urlencode
from urllib.request import Request
from uuid import uuid4
from psycopg.types.json import Jsonb
from app.core import db
from tests.smoke_test import BASE, Client, with_host
# A 1 x 1 PNG: the endpoint reads the type from the bytes.
PNG = base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==')
def raw(client, method, path, data=None, content_type=None):
"""A request whose body is not JSON; returns (status, headers, body)."""
headers = with_host({'Content-Type': content_type} if content_type else {})
request = Request(BASE + '/api' + path, data=data, headers=headers, method=method)
try:
with client.opener.open(request, timeout=30) as response:
return response.status, response.headers, response.read()
except HTTPError as exc:
return exc.code, exc.headers, exc.read()
def session_upload(client, name):
"""A completed, clean upload owned by this client's session. The bytes are
not the subject here, so none are stored."""
sid = next(c.value for c in client.jar if c.name == 'dtf_session')
uid = uuid4()
with db.connect() as c:
owner = c.execute('SELECT owner FROM dtf_local.sessions WHERE id=%s', (sid,)).fetchone()['owner']
c.execute('''INSERT INTO dtf_local.uploads(id,owner,name,size,object_key,multipart_id,complete,expires_at,scan_state)
VALUES(%s,%s,%s,1,%s,'none',true,now()+interval '1 day','clean')''', (uid, owner, name, f'originals/{uid}'))
return str(uid)
def run():
tag = secrets.token_hex(4)
mail = f'kanban-{tag}@example.test'
cnpj = '11.222.333/0001-81'
customer = {'mail': mail, 'cnpj': cnpj, 'zap': '(16) 98765-' + str(4000 + int(tag[:3], 16) % 1000)}
client = Client()
client.call('/session')
uid = session_upload(client, f'kanban-{tag}.cdr')
# The customer's browser sends the picture of its own upload; nothing else.
assert raw(client, 'PUT', f'/uploads/{uid}/thumbnail', PNG, 'image/png')[0] == 200
assert raw(client, 'PUT', f'/uploads/{uid}/thumbnail', b'<svg onload=alert(1)>', 'image/png')[0] == 415
assert raw(client, 'PUT', f'/uploads/{uid}/thumbnail', PNG + b'\0' * 300_001, 'image/png')[0] == 413
stranger = Client()
stranger.call('/session')
assert raw(stranger, 'PUT', f'/uploads/{uid}/thumbnail', PNG, 'image/png')[0] == 404
# Only an operator reads it, as an image the browser may keep for a while.
assert raw(client, 'GET', f'/operator/uploads/{uid}/thumbnail')[0] == 401
client.call('/operator/board', operator=True)
status, headers, body = raw(client.operator_client, 'GET', f'/operator/uploads/{uid}/thumbnail')
assert status == 200 and body == PNG, status
assert headers['Content-Type'] == 'image/png' and 'private' in headers['Cache-Control']
assert headers['X-Content-Type-Options'] == 'nosniff'
owner = uuid4()
pending, approved, paid, order_id = uuid4(), uuid4(), uuid4(), uuid4()
item = lambda mode, warning=False: {'mode': mode, 'uploads': [uid], 'metres': '1.00', 'billed_metres': '1.00',
'grade': 100, 'quality_status': 'warning' if warning else 'ok'}
draft = lambda *items: {'customer': customer, 'items': list(items), 'freight': {'service': 'pickup'}}
try:
with db.connect() as c:
for qid, content, accepted in [(pending, draft(item('avulsa', warning=True)), None),
(approved, draft(item('uvfile')), {'items': [], 'total_cents': 6990}),
(paid, draft(item('file')), {'items': [], 'total_cents': 1490})]:
c.execute('''INSERT INTO dtf_local.quotes(id,owner,request_key,request_hash,draft,approved)
VALUES(%s,%s,%s,%s,%s,%s)''', (qid, owner, uuid4(), 'kanban-fixture', Jsonb(content),
Jsonb(accepted) if accepted else None))
number = c.execute('''INSERT INTO dtf_local.orders(id,quote_id,owner,snapshot,payment)
VALUES(%s,%s,%s,%s,%s) RETURNING number''',
(order_id, paid, owner, Jsonb({**draft(item('file')), 'total_cents': 1490}),
Jsonb({'provider': 'teste', 'id': f'kanban-{tag}'}))).fetchone()['number']
def quotes(**params):
page = client.call('/operator/quotes?' + urlencode({'q': mail, **params}), operator=True)
return page, {q['id'] for q in page['quotes']}
page, ids = quotes(kind='pending')
assert ids == {str(pending)} and page['total'] == 1, ids
assert str(uid) in page['thumbnails']
assert quotes(kind='pending', layout='avulsa', flag='ressalva')[1] == {str(pending)}
assert quotes(kind='pending', product='uv')[1] == set()
assert quotes(kind='approved', product='uv', layout='folha')[1] == {str(approved)}
assert quotes(kind='approved', layout='avulsa')[1] == set()
# CNPJ and WhatsApp match by digits, whatever the punctuation typed.
assert client.call('/operator/quotes?' + urlencode({'kind': 'approved', 'q': '11222333'}),
operator=True)['total'] >= 1
# LIKE's own wildcards are literal: "%%" is not "everything".
assert str(pending) not in {q['id'] for q in client.call(
'/operator/quotes?' + urlencode({'kind': 'pending', 'q': '%%'}), operator=True)['quotes']}
# One search: the paid order (by customer and by number) and the two unpaid quotes.
found = client.call('/operator/search?' + urlencode({'q': mail}), operator=True)
assert {o['id'] for o in found['orders']} == {str(order_id)}
assert {q['id'] for q in found['quotes']} == {str(pending), str(approved)}
assert str(uid) in found['thumbnails']
by_number = client.call('/operator/search?' + urlencode({'q': f'#{number}'}), operator=True)
assert str(order_id) in {o['id'] for o in by_number['orders']}
assert client.call('/operator/search?q=a', operator=True, expected=422)
board = client.call('/operator/board', operator=True)
assert str(uid) in board['thumbnails']
print('PASS: artwork pictures (owner only, images only), quote filters and the order/quote search')
finally:
with db.connect() as c:
c.execute('DELETE FROM dtf_local.orders WHERE id=%s', (order_id,))
c.execute('DELETE FROM dtf_local.quotes WHERE id=ANY(%s)', ([pending, approved, paid],))
c.execute('DELETE FROM dtf_local.upload_thumbnails WHERE upload_id=%s', (uid,))
c.execute('DELETE FROM dtf_local.uploads WHERE id=%s', (uid,))
if __name__ == '__main__':
run()