"""The Tiny v3 adapter against a fake HTTP transport: payload and idempotency. This proves the documented contract only; the client's account must still confirm products, contacts and order fields. Runs where httpx is installed. The OAuth connection against the real database is tests/tiny_oauth_test.py. """ import json import os import unittest from datetime import date from unittest import mock import httpx from app.tiny import TinyError, TinyOrders, contact_payload, order_payload PAID = {'order_id': 'b6f1c0de-0000-4000-8000-000000000001', 'number': 42, 'event': 'payment_approved', 'order': {'customer': {'cnpj': '11222333000181', 'zap': '16999999999', 'mail': 'loja@example.test'}, 'items': [{'mode': 'avulsa', 'grade': 90, 'billed_metres': '2.8', 'unit_cents': 2490}], 'freight': {'service': 'mock-standard', 'total_cents': 1500}, 'destination': {'recipient': 'Loja Teste', 'street': 'Rua A', 'number': '10', 'complement': '', 'district': 'Centro', 'city': 'Franca', 'state': 'SP', 'postal_code': '14400000'}, 'total_cents': 8472}} PRODUCTS = {'TINY_PRODUCT_TEXTIL_FOLHA': '101', 'TINY_PRODUCT_TEXTIL_AVULSA': '102', 'TINY_PRODUCT_UV_FOLHA': '103', 'TINY_PRODUCT_UV_AVULSA': '104'} class FakeAuth: def access_token(self): return 'access-1' @mock.patch.dict(os.environ, PRODUCTS) class TinyTests(unittest.TestCase): def setUp(self): self.contacts = [] self.orders = [] self.calls = [] def handler(request): path = request.url.path.removeprefix('/public-api/v3') self.calls.append((request.method, path)) assert request.headers['authorization'] == 'Bearer access-1' if request.method == 'GET' and path == '/contatos': cnpj = request.url.params.get('cpfCnpj') return httpx.Response(200, json={'itens': [c for c in self.contacts if cnpj in (None, c['cpfCnpj'])]}) if request.method == 'POST' and path == '/contatos': contact = {**json.loads(request.content), 'id': 500 + len(self.contacts)} self.contacts.append(contact) return httpx.Response(200, json={'id': contact['id']}) if request.method == 'GET' and path == '/pedidos': return httpx.Response(200, json={'itens': [{'id': o['id']} for o in self.orders]}) if request.method == 'GET' and path.startswith('/pedidos/'): wanted = int(path.rsplit('/', 1)[-1]) return httpx.Response(200, json=next(o for o in self.orders if o['id'] == wanted)) if request.method == 'POST' and path == '/pedidos': order = {**json.loads(request.content), 'id': 9000 + len(self.orders), 'numeroPedido': str(100 + len(self.orders))} self.orders.append(order) return httpx.Response(200, json={'id': order['id'], 'numeroPedido': order['numeroPedido']}) return httpx.Response(404) self.handler = handler self.tiny = TinyOrders(auth=FakeAuth(), transport=httpx.MockTransport(handler), today=date(2026, 9, 24)) def test_payload_carries_contact_products_address_and_freight(self): pedido = order_payload(PAID, 777, date(2026, 9, 24)) self.assertEqual((pedido['idContato'], pedido['numeroOrdemCompra'], pedido['data']), (777, 'DTF-42', '2026-09-24')) item = pedido['itens'][0] self.assertEqual((item['produto'], item['quantidade'], item['valorUnitario']), ({'id': 102}, 2.8, 24.9)) self.assertEqual(pedido['valorFrete'], 15.0) self.assertEqual((pedido['enderecoEntrega']['cep'], pedido['enderecoEntrega']['enderecoNro'], pedido['enderecoEntrega']['nomeDestinatario']), ('14400000', '10', 'Loja Teste')) contact = contact_payload(PAID['order']) self.assertEqual((contact['tipoPessoa'], contact['cpfCnpj'], contact['endereco']['uf']), ('J', '11222333000181', 'SP')) pickup = order_payload({**PAID, 'order': {**PAID['order'], 'destination': None, 'freight': {'service': 'pickup', 'total_cents': 0}}}, 1) self.assertNotIn('enderecoEntrega', pickup) self.assertIn('retirada', pickup['observacoes']) def test_second_delivery_finds_the_first_order(self): first = self.tiny.deliver('k1', PAID) second = self.tiny.deliver('k1', PAID) self.assertEqual((first['status'], second['status']), ('created', 'already-created')) self.assertEqual(first['tiny_id'], second['tiny_id']) self.assertEqual(self.calls.count(('POST', '/pedidos')), 1) self.assertEqual(self.calls.count(('POST', '/contatos')), 1) def test_existing_contact_is_reused(self): self.contacts.append({'id': 321, 'cpfCnpj': '11222333000181'}) self.tiny.deliver('k1', PAID) self.assertNotIn(('POST', '/contatos'), self.calls) self.assertEqual(self.orders[0]['idContato'], 321) def test_production_events_are_not_sent(self): self.assertEqual(self.tiny.deliver('k2', {**PAID, 'event': 'ready'})['status'], 'not-applicable') self.assertEqual(self.calls, []) def test_missing_product_mapping_fails_rather_than_guessing(self): with mock.patch.dict(os.environ, {'TINY_PRODUCT_TEXTIL_AVULSA': ''}): with self.assertRaises(TinyError): self.tiny.deliver('k3', PAID) self.assertNotIn(('POST', '/pedidos'), self.calls) def test_connection_check_only_reads(self): from app.tiny import check results = check(auth=FakeAuth(), transport=httpx.MockTransport(self.handler)) self.assertEqual(results, {'pedidos': 'ok', 'contatos': 'ok'}) self.assertTrue(all(method == 'GET' for method, _ in self.calls)) denied = check(auth=FakeAuth(), transport=httpx.MockTransport(lambda r: httpx.Response(403, text='forbidden'))) self.assertTrue(denied['pedidos'].startswith('GET /pedidos: 403')) def test_rate_limit_is_a_retryable_failure(self): tiny = TinyOrders(auth=FakeAuth(), transport=httpx.MockTransport(lambda r: httpx.Response(429))) with self.assertRaisesRegex(TinyError, 'rate limit'): tiny.deliver('k4', PAID) if __name__ == '__main__': unittest.main()