This commit is contained in:
2026-04-05 00:43:23 +05:30
commit 8be37d3e92
425 changed files with 101853 additions and 0 deletions

View File

@@ -0,0 +1,281 @@
"""
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())