Files
ten31-database/backend/test_grid_contact_link.py
Keysat 300041a7ec Unification polish: LinkedIn in the grid inline contact editor (v0.1.0:54)
The fundraising grid's per-contact editor now has a LinkedIn URL field next to
name, email, title, and location. It threads through the grid contact object and
sanitize (which preserves contact-object fields), and _upsert_contact_from_fundraising
now reads and persists linkedin_url on both the update and insert paths — so a
LinkedIn entered in the grid lands on the linked contact record.

Test: test_grid_contact_link.py extended to assert LinkedIn entered in the grid
persists to the contact (idempotent). Frontend html.parser clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 15:24:50 -05:00

80 lines
3.0 KiB
Python

#!/usr/bin/env python3
"""Integration test for the grid→contact id-link wiring (migration 0004 + sync).
Imports the real server module against a throwaway DB, runs init_db (which applies
the 0004 migration adding fundraising_contacts.contact_id), then drives a grid
sync and asserts the grid contact is linked to the contacts-table row the app
created for it. Verifies the end-to-end backend wiring the entity resolver relies on.
Run: cd backend && python3 test_grid_contact_link.py
"""
import os
import sqlite3
import sys
import tempfile
_tmp = tempfile.mkdtemp()
os.environ["CRM_DATA_DIR"] = _tmp
os.environ["CRM_DB_PATH"] = os.path.join(_tmp, "crm.db")
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import server # noqa: E402
FAILS = []
def check(cond, msg):
print((" PASS " if cond else " FAIL ") + msg)
if not cond:
FAILS.append(msg)
def main():
server.init_db()
conn = sqlite3.connect(server.DB_PATH)
conn.row_factory = sqlite3.Row
cols = {r[1] for r in conn.execute("PRAGMA table_info(fundraising_contacts)")}
check("contact_id" in cols, "migration 0004 added fundraising_contacts.contact_id")
grid = {
"columns": [
{"id": "investor_name", "label": "Investor Name", "type": "text"},
{"id": "contacts", "label": "Contacts", "type": "contacts"},
],
"rows": [
{"id": "row-test-1", "investor_name": "Testco Capital",
"contacts": [{"name": "Jane Doe", "email": "jane@testco.com", "title": "Partner",
"linkedin_url": "https://linkedin.com/in/janedoe"}]},
],
}
server.sync_fundraising_relational(conn, server.sanitize_fundraising_grid(grid), [])
conn.commit()
fc = conn.execute("SELECT full_name, contact_id FROM fundraising_contacts WHERE full_name='Jane Doe'").fetchone()
check(bool(fc and fc["contact_id"]), f"grid contact_id populated by sync (got {dict(fc) if fc else None})")
if fc and fc["contact_id"]:
ct = conn.execute("SELECT id, email, linkedin_url FROM contacts WHERE id=?", (fc["contact_id"],)).fetchone()
check(bool(ct and ct["email"] == "jane@testco.com"),
f"link points to the correct contacts row (got {dict(ct) if ct else None})")
check(bool(ct and ct["linkedin_url"] == "https://linkedin.com/in/janedoe"),
f"LinkedIn entered in the grid persists to the contact (got {ct['linkedin_url'] if ct else None})")
# Re-sync is idempotent: still exactly one linked contact for Jane.
server.sync_fundraising_relational(conn, server.sanitize_fundraising_grid(grid), [])
conn.commit()
n = conn.execute("SELECT COUNT(*) FROM contacts WHERE lower(email)='jane@testco.com'").fetchone()[0]
check(n == 1, f"re-sync does not duplicate the contact (got {n})")
conn.close()
print()
if FAILS:
print(f"FAILED ({len(FAILS)})")
sys.exit(1)
print("ALL PASS (grid contact_id link wiring)")
if __name__ == "__main__":
main()