import os import uuid import csv import io import pytest from fastapi.testclient import TestClient # Ensure required env vars for app import/config os.environ.setdefault("SECRET_KEY", "x" * 32) os.environ.setdefault("DATABASE_URL", "sqlite:////tmp/delphi_test.sqlite") from app.main import app # noqa: E402 from app.auth.security import get_current_user # noqa: E402 @pytest.fixture(scope="module") def client(): # Override auth to bypass JWT for these tests class _User: def __init__(self): self.id = "test" self.username = "tester" self.is_admin = True self.is_active = True app.dependency_overrides[get_current_user] = lambda: _User() try: yield TestClient(app) finally: app.dependency_overrides.pop(get_current_user, None) @pytest.fixture(scope="module") def phone_book_data(client: TestClient): gid1 = f"PBGRP1-{uuid.uuid4().hex[:6]}" gid2 = f"PBGRP2-{uuid.uuid4().hex[:6]}" def _create_customer(cid: str, last: str, first: str, group: str): payload = { "id": cid, "last": last, "first": first, "group": group, "email": f"{cid.lower()}@example.com", } resp = client.post("/api/customers/", json=payload) assert resp.status_code == 200, resp.text def _add_phone(cid: str, location: str, number: str): resp = client.post(f"/api/customers/{cid}/phones", json={"location": location, "phone": number}) assert resp.status_code == 200, resp.text # Create entries for letters A, B and non-alpha '#' cid_a1 = f"PB-{uuid.uuid4().hex[:6]}-A1" cid_a2 = f"PB-{uuid.uuid4().hex[:6]}-A2" cid_b1 = f"PB-{uuid.uuid4().hex[:6]}-B1" cid_hash = f"PB-{uuid.uuid4().hex[:6]}-H" _create_customer(cid_a1, last="Alpha", first="Alice", group=gid1) _add_phone(cid_a1, "Office", "111-111-1111") _add_phone(cid_a1, "Mobile", "111-111-2222") _create_customer(cid_a2, last="Able", first="Andy", group=gid1) _add_phone(cid_a2, "Office", "222-222-2222") _create_customer(cid_b1, last="Beta", first="Bob", group=gid2) _add_phone(cid_b1, "Home", "333-333-3333") _create_customer(cid_hash, last="123Company", first="NA", group=gid1) _add_phone(cid_hash, "Main", "444-444-4444") try: yield { "gid1": gid1, "gid2": gid2, "ids": [cid_a1, cid_a2, cid_b1, cid_hash], } finally: # Cleanup for cid in [cid_a1, cid_a2, cid_b1, cid_hash]: client.delete(f"/api/customers/{cid}") def _parse_csv(text: str): reader = csv.reader(io.StringIO(text)) rows = list(reader) return rows[0], rows[1:] def test_phone_book_csv_letter_column_when_grouped_by_letter(client: TestClient, phone_book_data): # Only include our test group gid1 to avoid interference gid = phone_book_data["gid1"] resp = client.get( "/api/customers/phone-book", params={ "format": "csv", "mode": "numbers", "grouping": "letter", "groups": gid, }, ) assert resp.status_code == 200 assert "text/csv" in resp.headers.get("content-type", "") header, rows = _parse_csv(resp.text) assert "Letter" in header # Ensure letters include 'A' and '#' letters = {r[header.index("Letter")] for r in rows} assert "A" in letters and "#" in letters def test_phone_book_html_sections_by_letter_with_page_break(client: TestClient, phone_book_data): gid = phone_book_data["gid1"] resp = client.get( "/api/customers/phone-book", params={ "format": "html", "mode": "numbers", "grouping": "letter", "page_break": "1", "groups": gid, }, ) assert resp.status_code == 200 html = resp.text # Snapshot-style checks: section headers and page-break class assert "Letter: A" in html assert "Letter: #" in html assert "class=\"section-title\"" in html assert "page-break" in html # later sections should have page-break class def test_phone_book_html_group_then_letter_sections(client: TestClient, phone_book_data): gid1 = phone_book_data["gid1"] gid2 = phone_book_data["gid2"] # Include both groups to verify group and nested letter sections resp = client.get( "/api/customers/phone-book", params=[ ("format", "html"), ("mode", "addresses"), ("grouping", "group_letter"), ("groups", gid1), ("groups", gid2), ], ) assert resp.status_code == 200 html = resp.text assert f"Group: {gid1}" in html assert f"Group: {gid2}" in html assert "Letter: A" in html or "Letter: #" in html def test_phone_book_csv_no_letter_for_grouping_none(client: TestClient, phone_book_data): gid = phone_book_data["gid1"] resp = client.get( "/api/customers/phone-book", params={ "format": "csv", "mode": "numbers", "grouping": "none", "groups": gid, }, ) assert resp.status_code == 200 header, rows = _parse_csv(resp.text) assert "Letter" not in header # Basic sanity: names and phones are present assert any("Alpha" in ",".join(row) or "Able" in ",".join(row) for row in rows) def test_phone_book_respects_group_filter(client: TestClient, phone_book_data): gid2 = phone_book_data["gid2"] resp = client.get( "/api/customers/phone-book", params={ "format": "csv", "mode": "numbers", "grouping": "letter", "groups": gid2, }, ) assert resp.status_code == 200 header, rows = _parse_csv(resp.text) # Only Beta/Bob (gid2) should be present all_text = "\n".join([",".join(r) for r in rows]) assert "Beta" in all_text or "Bob" in all_text # Ensure gid1 names are not present assert "Alpha" not in all_text and "Able" not in all_text