Files

165 lines
6.7 KiB
Python
Raw Permalink Normal View History

"""G4 -- the seed data is large, skewed, and contains the intended poison rows."""
import os
import subprocess
from pathlib import Path
import psycopg
import pytest
from harness.config import POSTGRES_DSN, REPO_ROOT
pytestmark = pytest.mark.seed
TARGET_COUNTS = {
"accounts": 800,
"transactions": 250_000,
"merchant_categories": 45,
"sanctions_entries": 12_000,
}
@pytest.fixture
def db_cursor(dsn, seeded):
with psycopg.connect(dsn, autocommit=True) as conn:
with conn.cursor() as cur:
yield cur
def _count(cur, table):
cur.execute(f"SELECT count(*) FROM {table}")
return cur.fetchone()[0]
def test_row_counts_match_manifest_and_targets(manifest, db_cursor):
"""G4: row counts match the manifest and the spec's targets within +-2%.
`transactions` is compared with >= rather than == : `make test-contract`
runs contract tests (which legitimately create new transactions) ahead of
this one in the same session (alphabetical file collection order), so its
count can only grow past the seed-time manifest snapshot, never shrink or
diverge below it. The other three tables are never mutated by any test,
so they stay an exact match.
"""
for table, target in TARGET_COUNTS.items():
actual = _count(db_cursor, table)
manifest_count = manifest["counts"][table]
if table == "transactions":
assert actual >= manifest_count, f"{table}: db={actual} manifest={manifest_count}"
else:
assert actual == manifest_count, f"{table}: db={actual} manifest={manifest_count}"
tolerance = target * 0.02
assert abs(actual - target) <= tolerance, f"{table}: {actual} not within 2% of {target}"
def test_exactly_five_hot_accounts_matching_manifest(manifest, db_cursor):
"""G4: exactly 5 accounts have is_hot = true, and they match hot_account_ids."""
db_cursor.execute("SELECT id FROM accounts WHERE is_hot = true")
hot_ids = {str(r[0]) for r in db_cursor.fetchall()}
assert len(hot_ids) == 5, hot_ids
assert hot_ids == set(manifest["hot_account_ids"])
def test_hot_accounts_own_20_to_40_percent_of_transactions(manifest, db_cursor):
"""G4: the 5 hot accounts own between 20% and 40% of all transactions."""
hot_ids = list(manifest["hot_account_ids"])
db_cursor.execute("SELECT count(*) FROM transactions WHERE account_id = ANY(%s)", (hot_ids,))
hot_count = db_cursor.fetchone()[0]
total = _count(db_cursor, "transactions")
share = hot_count / total
assert 0.20 <= share <= 0.40, f"hot accounts own {share:.1%} of transactions"
def test_transactions_span_at_least_110_distinct_days(db_cursor):
"""G4: transactions span >= 110 distinct days."""
db_cursor.execute("SELECT count(DISTINCT created_at::date) FROM transactions")
(days,) = db_cursor.fetchone()
assert days >= 110, days
def test_currency_distribution_roughly_70_20_10(db_cursor):
"""G4: currency distribution is roughly 70/20/10 EUR/USD/GBP (+-10 points each)."""
db_cursor.execute("SELECT currency, count(*) FROM transactions GROUP BY currency")
rows = dict(db_cursor.fetchall())
total = sum(rows.values())
expected = {"EUR": 0.70, "USD": 0.20, "GBP": 0.10}
for currency, target in expected.items():
actual = rows.get(currency, 0) / total
assert abs(actual - target) <= 0.10, f"{currency}: {actual:.1%} vs target {target:.1%}"
def test_status_distribution_roughly_92_5_3(db_cursor):
"""G4: status distribution is roughly 92/5/3 settled/rejected/pending (+-5 points)."""
db_cursor.execute("SELECT status, count(*) FROM transactions GROUP BY status")
rows = dict(db_cursor.fetchall())
total = sum(rows.values())
expected = {"settled": 0.92, "rejected": 0.05, "pending": 0.03}
for status, target in expected.items():
actual = rows.get(status, 0) / total
assert abs(actual - target) <= 0.05, f"{status}: {actual:.1%} vs target {target:.1%}"
def test_malformed_rows_cover_all_five_varieties(db_cursor):
"""G4: malformed rows exist in all five described varieties, 1%-4% overall."""
total = _count(db_cursor, "transactions")
db_cursor.execute("SELECT count(*) FROM transactions WHERE currency IS NULL")
null_currency = db_cursor.fetchone()[0]
assert null_currency >= 1, "no null-currency rows found"
db_cursor.execute("SELECT count(*) FROM transactions WHERE kind = 'deposit' AND amount_minor < 0")
negative_deposit = db_cursor.fetchone()[0]
assert negative_deposit >= 1, "no negative-amount deposit rows found"
db_cursor.execute(r"SELECT count(*) FROM transactions WHERE merchant_name ~ '[^\x00-\x7F]'")
non_ascii = db_cursor.fetchone()[0]
assert non_ascii >= 1, "no non-ASCII merchant_name rows found"
db_cursor.execute("SELECT count(*) FROM transactions WHERE created_at > now()")
future = db_cursor.fetchone()[0]
assert future >= 1, "no future-dated rows found"
db_cursor.execute(
"""
SELECT count(*) FROM transactions t
WHERE t.category_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM merchant_categories c WHERE c.id = t.category_id)
"""
)
orphan_category = db_cursor.fetchone()[0]
assert orphan_category >= 1, "no orphan category_id rows found"
malformed_total = null_currency + negative_deposit + non_ascii + future + orphan_category
share = malformed_total / total
assert 0.01 <= share <= 0.04, f"malformed rows are {share:.1%} of total (want 1%-4%)"
def test_sanctions_entries_match_seeded_holders(db_cursor):
"""G4: at least 30 sanctions entries match names of seeded account holders."""
db_cursor.execute(
"""
SELECT count(DISTINCT s.id) FROM sanctions_entries s
JOIN accounts a ON a.holder_name = s.normalized_name
"""
)
(matches,) = db_cursor.fetchone()
assert matches >= 30, matches
def test_manifest_records_the_configured_seed(manifest):
"""G4 (determinism proxy): the manifest records the SEED value used to
generate it. A full re-seed-and-diff is a manual check -- see
tests/README.md, "Seed determinism" -- because it takes up to 60s and would
blow the <20s seed-marker / <60s test-contract budgets on every run."""
expected_seed = int(os.environ.get("SEED", "42"))
assert manifest["seed"] == expected_seed
def test_no_missing_indexes_present_after_fresh_seed(db_cursor):
"""G4: neither transactions(account_id, created_at) nor
transactions(category_id) exists in pg_indexes after a fresh seed."""
db_cursor.execute("SELECT indexdef FROM pg_indexes WHERE tablename = 'transactions'")
defs = [row[0].lower() for row in db_cursor.fetchall()]
for d in defs:
assert not ("account_id" in d and "created_at" in d), d
assert "(category_id)" not in d.replace(" ", ""), d