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.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user