feat: Kanban shows each order's artwork, filters quotes and searches orders and quotes
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
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
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>
This commit is contained in:
126
tests/kanban_test.py
Normal file
126
tests/kanban_test.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user