""" test_pdf_generator.py — Tests for negoT8 Deal Agreement PDF generation Run from the project root: cd /path/to/negot8 python test/test_pdf_generator.py What this tests: 1. Freelance deal PDF — rich details (budget, scope, timeline, payment) 2. Marketplace (buy/sell) PDF — item, price, delivery 3. Minimal data — all optional fields absent; should still produce a valid PDF 4. Blockchain proof attached — real-looking proof dict 5. Mock blockchain proof — mock=True path 6. File is actually written to /tmp and is non-empty 7. Temp-file cleanup helper works """ import asyncio import os import sys # ── Make sure backend/ is on the path ──────────────────────────────────────── BACKEND = os.path.join(os.path.dirname(__file__), "..", "backend") sys.path.insert(0, os.path.abspath(BACKEND)) from tools.pdf_generator import generate_deal_pdf # ───────────────────────────────────────────────────────────────────────────── # Shared fixtures # ───────────────────────────────────────────────────────────────────────────── USER_A = {"id": 111111111, "name": "Alice Sharma", "username": "alice_s"} USER_B = {"id": 222222222, "name": "Bob Chatterjee", "username": "bobchat"} REAL_PROOF = { "success": True, "mock": False, "tx_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", "block_number": 87654321, "agreement_hash": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "explorer_url": "https://amoy.polygonscan.com/tx/0xabcdef1234567890", "gas_used": 42000, } MOCK_PROOF = { "success": True, "mock": True, "tx_hash": "0xMOCKTX1234567890", "block_number": 0, "agreement_hash": "0xMOCKHASH1234567890", "explorer_url": "", "gas_used": 0, } # ───────────────────────────────────────────────────────────────────────────── # Test helpers # ───────────────────────────────────────────────────────────────────────────── def _check_pdf(path: str, label: str): """Assert the file exists, is non-empty, and starts with the PDF magic bytes.""" assert os.path.exists(path), f"[{label}] PDF file not found at {path}" size = os.path.getsize(path) assert size > 500, f"[{label}] PDF suspiciously small: {size} bytes" with open(path, "rb") as f: magic = f.read(4) assert magic == b"%PDF", f"[{label}] File does not start with PDF magic bytes: {magic}" print(f" ✅ [{label}] PDF OK — {size:,} bytes → {path}") def _cleanup(path: str): try: os.remove(path) except OSError: pass # ───────────────────────────────────────────────────────────────────────────── # Individual tests # ───────────────────────────────────────────────────────────────────────────── async def test_freelance_full(): """Freelance deal with full details + real blockchain proof.""" final_proposal = { "summary": "Alice will build the dashboard in 3 weeks for Rs. 45,000 with 50% upfront.", "details": { "budget": "45000", "timeline": "3 weeks", "scope": ["Admin dashboard", "REST API integration", "Mobile responsive UI"], "payment_schedule": "50% upfront (Rs. 22,500) + 50% on delivery", "upfront": "22500", "ip_ownership": "Full transfer to client on final payment", }, } preferences_a = { "goal": "Build a dashboard", "raw_details": { "role": "freelancer", "skill": "React + FastAPI", "rate": "1500", "hours": "30", "upfront_minimum": "50", }, } preferences_b = { "goal": "Hire a developer", "raw_details": { "role": "client", "budget": "45000", "required_features": ["Admin dashboard", "API", "Mobile UI"], }, } path = await generate_deal_pdf( negotiation_id = "freelance_test_001", feature_type = "freelance", final_proposal = final_proposal, user_a = USER_A, user_b = USER_B, rounds_taken = 4, sat_a = 85.0, sat_b = 78.0, preferences_a = preferences_a, preferences_b = preferences_b, blockchain_proof = REAL_PROOF, ) _check_pdf(path, "freelance_full") _cleanup(path) async def test_marketplace_full(): """Buy/sell deal with full details + mock blockchain proof.""" final_proposal = { "summary": "iPhone 14 sold for Rs. 52,000. Pickup at Andheri station on Saturday.", "details": { "agreed_price": "52000", "delivery": "Pickup — Andheri West Metro station, Saturday 4 PM", "condition": "Used — 6 months old, no scratches", "market_price": "55000", }, } preferences_a = { "goal": "Sell iPhone 14", "raw_details": { "role": "seller", "item": "iPhone 14 128GB Black", "asking_price": "55000", "minimum_price": "50000", }, } preferences_b = { "goal": "Buy iPhone 14", "raw_details": { "role": "buyer", "item": "iPhone 14", "max_budget": "54000", "offer_price": "49000", }, } path = await generate_deal_pdf( negotiation_id = "marketplace_test_001", feature_type = "marketplace", final_proposal = final_proposal, user_a = USER_A, user_b = USER_B, rounds_taken = 3, sat_a = 72.0, sat_b = 88.0, preferences_a = preferences_a, preferences_b = preferences_b, blockchain_proof = MOCK_PROOF, ) _check_pdf(path, "marketplace_full") _cleanup(path) async def test_minimal_data(): """Both feature_type is unknown and all optional fields are empty — should not crash.""" path = await generate_deal_pdf( negotiation_id = "minimal_test_001", feature_type = "generic", final_proposal = {"summary": "Parties agreed to share expenses equally."}, user_a = {"id": 1, "name": "", "username": "userA"}, user_b = {"id": 2, "name": "", "username": "userB"}, rounds_taken = 1, sat_a = 60.0, sat_b = 60.0, blockchain_proof = None, ) _check_pdf(path, "minimal_data") _cleanup(path) async def test_no_blockchain_proof(): """Freelance deal where blockchain proof hasn't been registered yet.""" final_proposal = { "summary": "React Native app, Rs. 80,000, 6 weeks.", "details": { "budget": "80000", "timeline": "6 weeks", "scope": ["React Native app", "Backend API"], }, } path = await generate_deal_pdf( negotiation_id = "noproof_test_001", feature_type = "freelance", final_proposal = final_proposal, user_a = USER_A, user_b = USER_B, rounds_taken = 5, sat_a = 90.0, sat_b = 70.0, blockchain_proof = None, ) _check_pdf(path, "no_blockchain_proof") _cleanup(path) async def test_unicode_safe(): """ Ensure the PDF builder doesn't crash on characters outside Latin-1 (Rs. symbol ₹, em-dashes, etc.). """ final_proposal = { "summary": "₹45,000 deal — React dashboard — agreed ✓", "details": { "budget": "₹45,000", "timeline": "3 weeks – confirmed", "scope": ["Dashboard • Admin panel", "API • REST"], }, } path = await generate_deal_pdf( negotiation_id = "unicode_test_001", feature_type = "freelance", final_proposal = final_proposal, user_a = {"id": 1, "name": "Anirbán Bāsak", "username": "anirban"}, user_b = {"id": 2, "name": "Rāhul Gupta", "username": "rahul"}, rounds_taken = 2, sat_a = 88.0, sat_b = 82.0, blockchain_proof = MOCK_PROOF, ) _check_pdf(path, "unicode_safe") _cleanup(path) # ───────────────────────────────────────────────────────────────────────────── # Runner # ───────────────────────────────────────────────────────────────────────────── TESTS = [ ("Freelance full details + real blockchain proof", test_freelance_full), ("Marketplace (buy/sell) + mock proof", test_marketplace_full), ("Minimal / sparse data — no crash", test_minimal_data), ("No blockchain proof yet", test_no_blockchain_proof), ("Unicode / special chars — Latin-1 safety", test_unicode_safe), ] async def main(): print("\n🧪 negoT8 — PDF Generator Tests") print("=" * 52) passed = 0 failed = 0 for name, fn in TESTS: print(f"\n▶ {name}") try: await fn() passed += 1 except Exception as exc: import traceback print(f" ❌ FAILED: {exc}") traceback.print_exc() failed += 1 print("\n" + "=" * 52) print(f"Results: {passed} passed | {failed} failed | {len(TESTS)} total") if failed == 0: print("✅ All PDF tests passed!\n") else: print("❌ Some tests failed — see output above.\n") sys.exit(1) if __name__ == "__main__": asyncio.run(main())