Files
ten31-database/backend/run_tests.py
T
Keysat 7285bb0e52 Add regression tests for v74 fixes; close soft-delete leak in list-view aggregates
Lock in the three v0.1.0:74 security/privacy fixes with regression tests, and
fix a same-class soft-delete leak surfaced while writing them.

- backend/test_assets_traversal.py: boots the real server, proves /assets/
  path-traversal vectors (incl. a real decoy file and the live crm.db, plain
  and URL-encoded) 404 and leak nothing, while a legit asset still serves 200.
- backend/test_soft_delete_reads.py: get-by-id 404s soft-deleted rows and
  nested + list-view aggregates exclude soft-deleted children.
- backend/mcp/test_outreach_redaction.py: an unknown free-prose name is
  tokenized away from the Claude payload but re-hydrated locally, and the path
  fails closed (no Claude call) when the local NER model is down.
- backend/run_tests.py: aggregate runner (each backend/**/test_*.py in its own
  subprocess); replaces the manual for-loop. 16/16 green.

A reviewer pass on the tests confirmed the soft-delete filter was missing from
list-view aggregate sub-selects: org contact_count/total_funded and contacts
comm_count/last_contact_date counted soft-deleted rows. Add `deleted_at IS NULL`
to those four (server.py) and regression-cover them.

The reports subsystem (dashboard/pipeline/LP-breakdown, ~16 aggregate queries)
has the same leak and is logged as P2 for a dedicated pass. Not yet built or
deployed — bump the package version before the next s9pk build.
2026-06-13 00:26:22 -05:00

68 lines
2.1 KiB
Python

#!/usr/bin/env python3
"""Aggregate test runner for the backend suite.
The backend tests are standalone scripts (each with `if __name__ == "__main__"`, no
pytest). This discovers every backend/**/test_*.py and runs each in its OWN subprocess
(tests set os.environ and import `server` with different configs, so isolation matters),
prints a one-line PASS/FAIL per test, dumps output only for failures, and exits non-zero
if any test fails.
Run: python3 backend/run_tests.py (from the repo root)
or: cd backend && python3 run_tests.py
Filter: python3 backend/run_tests.py soft_delete redaction # substring match on path
"""
import os
import subprocess
import sys
import time
BACKEND = os.path.dirname(os.path.abspath(__file__))
def discover(filters):
found = []
for root, dirs, files in os.walk(BACKEND):
dirs[:] = [d for d in dirs if d != "__pycache__"]
for f in files:
if f.startswith("test_") and f.endswith(".py"):
path = os.path.join(root, f)
rel = os.path.relpath(path, BACKEND)
if not filters or any(flt in rel for flt in filters):
found.append(path)
return sorted(found)
def main():
filters = sys.argv[1:]
tests = discover(filters)
if not tests:
print("No tests matched.")
sys.exit(1)
print(f"Running {len(tests)} backend test(s)\n")
passed, failed = [], []
t0 = time.time()
for path in tests:
rel = os.path.relpath(path, BACKEND)
proc = subprocess.run([sys.executable, path], cwd=BACKEND,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if proc.returncode == 0:
passed.append(rel)
print(f" PASS {rel}")
else:
failed.append(rel)
print(f" FAIL {rel}")
sys.stdout.write(proc.stdout.decode("utf-8", "replace").rstrip() + "\n")
print(f"\n{len(passed)}/{len(tests)} passed in {time.time() - t0:.1f}s")
if failed:
print("FAILED:")
for f in failed:
print(f" - {f}")
sys.exit(1)
print("ALL PASS")
if __name__ == "__main__":
main()