● Zion Boggan
repos/Docket/services/ingest/tests/test_categorize.py
zionboggan.com ↗
405 lines · python
History for this file →
1
"""
2
Tests for ingest.categorize  -  unified taxonomy module.
3
 
4
Covers:
5
  - agency_to_category: deterministic mapping, prefix match, unknown agencies
6
  - POLICY_AREAS: completeness and slug format
7
  - _parse_subjects: XML parsing from a synthetic BILLSTATUS snippet
8
  - Schema helpers: ensure tables and columns are created idempotently
9
  - _seed_categories: idempotent seeding, extra bill areas picked up
10
  - _backfill_awards: correct category assignment, NULL handling
11
  - category_counts: returns expected shape
12
"""
13
 
14
import sqlite3
15
import textwrap
16
import xml.etree.ElementTree as ET
17
 
18
import pytest
19
 
20
from ingest.categorize import (
21
    AGENCY_CATEGORY_MAP,
22
    POLICY_AREAS,
23
    agency_to_category,
24
    _parse_subjects,
25
    _ensure_schema,
26
    _seed_categories,
27
    _backfill_awards,
28
    _slug_from_label,
29
    category_counts,
30
)
31
from ingest.db import init_schema
32
 
33
 
34
 
35
@pytest.fixture
36
def conn():
37
    """In-memory SQLite connection with base schema initialised."""
38
    c = sqlite3.connect(":memory:")
39
    c.row_factory = sqlite3.Row
40
    c.execute("PRAGMA foreign_keys=ON")
41
    init_schema(c)
42
    _ensure_schema(c)
43
    yield c
44
    c.close()
45
 
46
 
47
def _insert_award(conn, agency: str, category: str | None = None) -> int:
48
    conn.execute(
49
        """
50
        INSERT INTO awards (source, external_id, agency, recipient, amount,
51
                            action_date, program, source_url, category)
52
        VALUES ('usaspending', ?, ?, 'Test Recipient', 1000000,
53
                '2026-01-01', 'Test Program',
54
                'https://www.usaspending.gov/award/TEST', ?)
55
        """,
56
        (f"award-{agency[:20]}", agency, category),
57
    )
58
    conn.commit()
59
    return conn.execute("SELECT last_insert_rowid()").fetchone()[0]
60
 
61
 
62
def _insert_bill(conn, policy_area: str = "", external_id: str = "119-hr-1") -> int:
63
    conn.execute(
64
        """
65
        INSERT INTO bills (source, external_id, congress, number, title,
66
                           sponsor_id, status, policy_area, updated_at, source_url)
67
        VALUES ('govinfo_billstatus', ?, 119, 'H.R. 1', 'Test Bill',
68
                '', '', ?, '2026-01-01', 'https://www.congress.gov/bill/119th-congress/house-bill/1')
69
        """,
70
        (external_id, policy_area),
71
    )
72
    conn.commit()
73
    return conn.execute("SELECT last_insert_rowid()").fetchone()[0]
74
 
75
 
76
 
77
class TestAgencyToCategory:
78
    """Deterministic agency→policy-area mapping."""
79
 
80
    def test_exact_match_defense(self):
81
        assert agency_to_category("Department of Defense") == "Armed Forces and National Security"
82
 
83
    def test_exact_match_health(self):
84
        assert agency_to_category("Department of Health and Human Services") == "Health"
85
 
86
    def test_exact_match_transportation(self):
87
        assert agency_to_category("Department of Transportation") == "Transportation and Public Works"
88
 
89
    def test_exact_match_education(self):
90
        assert agency_to_category("Department of Education") == "Education"
91
 
92
    def test_exact_match_energy(self):
93
        assert agency_to_category("Department of Energy") == "Energy"
94
 
95
    def test_exact_match_agriculture(self):
96
        assert agency_to_category("Department of Agriculture") == "Agriculture and Food"
97
 
98
    def test_exact_match_labor(self):
99
        assert agency_to_category("Department of Labor") == "Labor and Employment"
100
 
101
    def test_exact_match_epa(self):
102
        assert agency_to_category("Environmental Protection Agency") == "Environmental Protection"
103
 
104
    def test_exact_match_doj(self):
105
        assert agency_to_category("Department of Justice") == "Crime and Law Enforcement"
106
 
107
    def test_exact_match_state(self):
108
        assert agency_to_category("Department of State") == "International Affairs"
109
 
110
    def test_exact_match_usaid(self):
111
        assert agency_to_category("Agency for International Development") == "International Affairs"
112
 
113
    def test_exact_match_commerce(self):
114
        assert agency_to_category("Department of Commerce") == "Commerce"
115
 
116
    def test_exact_match_treasury(self):
117
        assert agency_to_category("Department of the Treasury") == "Economics and Public Finance"
118
 
119
    def test_exact_match_va(self):
120
        assert agency_to_category("Department of Veterans Affairs") == "Armed Forces and National Security"
121
 
122
    def test_exact_match_dhs(self):
123
        assert agency_to_category("Department of Homeland Security") == "Emergency Management"
124
 
125
    def test_exact_match_interior(self):
126
        assert agency_to_category("Department of the Interior") == "Public Lands and Natural Resources"
127
 
128
    def test_exact_match_hud(self):
129
        assert agency_to_category("Department of Housing and Urban Development") == "Housing and Community Development"
130
 
131
    def test_exact_match_ssa(self):
132
        assert agency_to_category("Social Security Administration") == "Social Welfare"
133
 
134
    def test_exact_match_nasa(self):
135
        assert agency_to_category("National Aeronautics and Space Administration") == "Science, Technology, Communications"
136
 
137
    def test_exact_match_nsf(self):
138
        assert agency_to_category("National Science Foundation") == "Science, Technology, Communications"
139
 
140
    def test_exact_match_gsa(self):
141
        assert agency_to_category("General Services Administration") == "Government Operations and Politics"
142
 
143
    def test_prefix_match_dod_suborg(self):
144
        """Sub-agency starting with 'Department of Defense' should map correctly."""
145
        result = agency_to_category("Department of Defense - Defense Advanced Research Projects Agency")
146
        assert result == "Armed Forces and National Security"
147
 
148
    def test_unknown_agency_returns_none(self):
149
        assert agency_to_category("Some Completely Unknown Agency XYZ") is None
150
 
151
    def test_empty_string_returns_none(self):
152
        assert agency_to_category("") is None
153
 
154
    def test_none_returns_none(self):
155
        assert agency_to_category(None) is None
156
 
157
    def test_all_map_keys_are_in_policy_area_labels(self):
158
        """Every agency map value must correspond to a canonical policy-area label."""
159
        valid_labels = {label for _, label in POLICY_AREAS}
160
        for agency, category in AGENCY_CATEGORY_MAP.items():
161
            assert category in valid_labels, (
162
                f"Agency '{agency}' maps to '{category}' which is not a canonical policy area"
163
            )
164
 
165
 
166
 
167
class TestPolicyAreas:
168
    def test_has_expected_count(self):
169
        assert 28 <= len(POLICY_AREAS) <= 45, f"Unexpected POLICY_AREAS count: {len(POLICY_AREAS)}"
170
 
171
    def test_no_duplicate_slugs(self):
172
        slugs = [slug for slug, _ in POLICY_AREAS]
173
        assert len(slugs) == len(set(slugs)), "Duplicate slugs in POLICY_AREAS"
174
 
175
    def test_no_duplicate_labels(self):
176
        labels = [label for _, label in POLICY_AREAS]
177
        assert len(labels) == len(set(labels)), "Duplicate labels in POLICY_AREAS"
178
 
179
    def test_slugs_are_lowercase_hyphenated(self):
180
        import re
181
        for slug, _ in POLICY_AREAS:
182
            assert re.match(r"^[a-z0-9-]+$", slug), f"Slug '{slug}' has invalid characters"
183
 
184
    def test_key_areas_present(self):
185
        labels = {label for _, label in POLICY_AREAS}
186
        required = {
187
            "Health",
188
            "Armed Forces and National Security",
189
            "Education",
190
            "Energy",
191
            "Taxation",
192
            "Transportation and Public Works",
193
            "Environmental Protection",
194
            "Agriculture and Food",
195
            "Crime and Law Enforcement",
196
            "International Affairs",
197
        }
198
        missing = required - labels
199
        assert not missing, f"Missing required policy areas: {missing}"
200
 
201
    def test_slug_matches_label(self):
202
        """Generated slug from label should equal the declared slug (or be a close match)."""
203
        for declared_slug, label in POLICY_AREAS:
204
            generated = _slug_from_label(label)
205
            assert declared_slug == generated or declared_slug.replace("-", "") == generated.replace("-", ""), (
206
                f"Slug mismatch: declared='{declared_slug}' generated='{generated}' for label='{label}'"
207
            )
208
 
209
 
210
 
211
_BILLSTATUS_TEMPLATE = """\
212
<?xml version="1.0" encoding="UTF-8"?>
213
<billStatus>
214
  <bill>
215
    <subjects>
216
      <legislativeSubjects>
217
        {items}
218
      </legislativeSubjects>
219
    </subjects>
220
  </bill>
221
</billStatus>"""
222
 
223
def _make_billstatus(subjects: list[str]) -> bytes:
224
    items = "".join(f"<item><name>{s}</name></item>" for s in subjects)
225
    return _BILLSTATUS_TEMPLATE.format(items=items).encode()
226
 
227
 
228
class TestParseSubjects:
229
    def test_parses_single_subject(self):
230
        xml_bytes = _make_billstatus(["Health care reform"])
231
        assert _parse_subjects(xml_bytes) == ["Health care reform"]
232
 
233
    def test_parses_multiple_subjects(self):
234
        subjects = ["Tax exemptions", "Income tax", "Small business"]
235
        xml_bytes = _make_billstatus(subjects)
236
        assert _parse_subjects(xml_bytes) == subjects
237
 
238
    def test_empty_legislative_subjects(self):
239
        xml_bytes = _make_billstatus([])
240
        assert _parse_subjects(xml_bytes) == []
241
 
242
    def test_missing_subjects_element(self):
243
        xml_bytes = b"<billStatus><bill><title>Test</title></bill></billStatus>"
244
        assert _parse_subjects(xml_bytes) == []
245
 
246
    def test_malformed_xml_returns_empty(self):
247
        assert _parse_subjects(b"not valid xml<<<") == []
248
 
249
    def test_strips_whitespace_from_subject_names(self):
250
        xml_bytes = _make_billstatus(["  Defense contracts  "])
251
        result = _parse_subjects(xml_bytes)
252
        assert result == ["Defense contracts"]
253
 
254
    def test_bill_root_without_billstatus_wrapper(self):
255
        """Some GovInfo responses have <bill> as the root element."""
256
        xml_bytes = b"""<bill>
257
          <subjects>
258
            <legislativeSubjects>
259
              <item><name>Veterans</name></item>
260
            </legislativeSubjects>
261
          </subjects>
262
        </bill>"""
263
        assert _parse_subjects(xml_bytes) == ["Veterans"]
264
 
265
 
266
 
267
class TestEnsureSchema:
268
    def test_creates_categories_table(self, conn):
269
        tables = {r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")}
270
        assert "categories" in tables
271
 
272
    def test_creates_bill_subjects_table(self, conn):
273
        tables = {r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")}
274
        assert "bill_subjects" in tables
275
 
276
    def test_adds_category_column_to_awards(self, conn):
277
        cols = {r[1] for r in conn.execute("PRAGMA table_info(awards)")}
278
        assert "category" in cols
279
 
280
    def test_idempotent_second_call(self, conn):
281
        """Calling _ensure_schema twice must not raise."""
282
        _ensure_schema(conn)
283
        cols = {r[1] for r in conn.execute("PRAGMA table_info(awards)")}
284
        assert "category" in cols
285
 
286
 
287
 
288
class TestSeedCategories:
289
    def test_seeds_all_policy_areas(self, conn):
290
        _seed_categories(conn)
291
        count = conn.execute("SELECT COUNT(*) FROM categories").fetchone()[0]
292
        assert count >= len(POLICY_AREAS)
293
 
294
    def test_idempotent_double_seed(self, conn):
295
        _seed_categories(conn)
296
        count1 = conn.execute("SELECT COUNT(*) FROM categories").fetchone()[0]
297
        _seed_categories(conn)
298
        count2 = conn.execute("SELECT COUNT(*) FROM categories").fetchone()[0]
299
        assert count1 == count2
300
 
301
    def test_picks_up_extra_bill_policy_area(self, conn):
302
        """A policy area present in bills but not in POLICY_AREAS is added."""
303
        conn.execute("""
304
            INSERT INTO bills (source, external_id, congress, number, title,
305
                               sponsor_id, status, policy_area, updated_at, source_url)
306
            VALUES ('govinfo_billstatus', 'test-1', 119, 'H.R. 999', 'Test',
307
                    '', '', 'Futuristic Technologies', '2026-01-01',
308
                    'https://www.congress.gov/bill/119th-congress/house-bill/999')
309
        """)
310
        conn.commit()
311
        _seed_categories(conn)
312
        row = conn.execute(
313
            "SELECT * FROM categories WHERE label = 'Futuristic Technologies'"
314
        ).fetchone()
315
        assert row is not None
316
 
317
    def test_health_area_in_categories(self, conn):
318
        _seed_categories(conn)
319
        row = conn.execute("SELECT * FROM categories WHERE label = 'Health'").fetchone()
320
        assert row is not None
321
        assert row["kind"] == "policy_area"
322
 
323
 
324
 
325
class TestBackfillAwards:
326
    def test_categorizes_known_agency(self, conn):
327
        _insert_award(conn, "Department of Defense")
328
        _backfill_awards(conn)
329
        row = conn.execute("SELECT category FROM awards WHERE agency = 'Department of Defense'").fetchone()
330
        assert row["category"] == "Armed Forces and National Security"
331
 
332
    def test_leaves_unknown_agency_null(self, conn):
333
        _insert_award(conn, "Unknown Mystery Agency")
334
        _backfill_awards(conn)
335
        row = conn.execute("SELECT category FROM awards WHERE agency = 'Unknown Mystery Agency'").fetchone()
336
        assert row["category"] is None
337
 
338
    def test_does_not_overwrite_existing_category(self, conn):
339
        """Awards that already have a category should keep it if the agency is unknown."""
340
        _insert_award(conn, "Unknown Mystery Agency", category="Existing Category")
341
        _backfill_awards(conn)
342
        row = conn.execute("SELECT category FROM awards WHERE agency = 'Unknown Mystery Agency'").fetchone()
343
        assert row["category"] == "Existing Category"
344
 
345
    def test_multiple_agencies(self, conn):
346
        agencies = [
347
            ("Department of Health and Human Services", "Health"),
348
            ("Department of Transportation",            "Transportation and Public Works"),
349
            ("National Science Foundation",             "Science, Technology, Communications"),
350
            ("Social Security Administration",          "Social Welfare"),
351
        ]
352
        for agency, _ in agencies:
353
            _insert_award(conn, agency)
354
        _backfill_awards(conn)
355
        for agency, expected_cat in agencies:
356
            row = conn.execute(
357
                "SELECT category FROM awards WHERE agency = ?", (agency,)
358
            ).fetchone()
359
            assert row["category"] == expected_cat, f"Wrong category for {agency}"
360
 
361
    def test_returns_count_of_updated(self, conn):
362
        _insert_award(conn, "Department of Energy")
363
        _insert_award(conn, "Department of Labor")
364
        updated = _backfill_awards(conn)
365
        assert updated == 2
366
 
367
    def test_idempotent(self, conn):
368
        _insert_award(conn, "Department of Defense")
369
        _backfill_awards(conn)
370
        updated2 = _backfill_awards(conn)
371
        assert updated2 == 0
372
 
373
 
374
 
375
class TestCategoryCounts:
376
    def test_returns_list(self, conn):
377
        _seed_categories(conn)
378
        result = category_counts(conn)
379
        assert isinstance(result, list)
380
 
381
    def test_each_item_has_expected_keys(self, conn):
382
        _seed_categories(conn)
383
        result = category_counts(conn)
384
        assert len(result) > 0
385
        for item in result:
386
            assert "label" in item
387
            assert "bills" in item
388
            assert "awards" in item
389
 
390
    def test_bill_counted_in_correct_category(self, conn):
391
        _seed_categories(conn)
392
        _insert_bill(conn, policy_area="Health")
393
        result = category_counts(conn)
394
        health_row = next((r for r in result if r["label"] == "Health"), None)
395
        assert health_row is not None
396
        assert health_row["bills"] == 1
397
 
398
    def test_award_counted_in_correct_category(self, conn):
399
        _seed_categories(conn)
400
        _insert_award(conn, "Department of Defense")
401
        _backfill_awards(conn)
402
        result = category_counts(conn)
403
        row = next((r for r in result if r["label"] == "Armed Forces and National Security"), None)
404
        assert row is not None
405
        assert row["awards"] == 1