Files
zwitschi 5d81f617f0 feat(deploy): conflict-free 'git pull' update flow (D001/D004/D005)
- deploy.sh: clean-tree guard + git fetch && git pull --ff-only + optional
  dep reinstall + app.db --migrate + restart.
- restart-service.sh: supervisor-agnostic restart (systemd or fallback), waits
  for an in-flight scan.
- check_runtime_git.sh: proves no scan writes git-tracked runtime data.
- ids.service: systemd unit template.
- tests/test_deployment.py: stdlib unittest invariant tests for the deployment
  runtime-isolation guarantee.
2026-10-24 19:50:45 +02:00

58 lines
2.1 KiB
Python

#!/usr/bin/env python3
"""Deployment invariant tests (D001).
Proves that running a scan never creates or modifies git-tracked runtime data,
which is what makes ``git pull`` conflict-free on a deployed server.
Run with:
python -m unittest discover -s tests -p 'test_*.py'
or (if pytest is installed):
python -m pytest tests/
"""
from __future__ import annotations
import os
import subprocess
import sys
import unittest
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# The paloalto scanner-ips.txt seed is deliberately tracked (read-only); every
# other file under the runtime dirs must remain untracked.
ALLOWED_TRACKED_RUNTIME = {os.path.join("app", "data", "paloalto",
"scanner-ips.txt")}
def _git(*args: str) -> str:
return subprocess.run(["git", *args], cwd=ROOT, check=True,
capture_output=True, text=True).stdout
class RuntimeGitTest(unittest.TestCase):
def test_no_runtime_data_tracked(self):
"""No runtime file (other than the read-only paloalto seed) is tracked."""
tracked = _git("ls-files", "app/data", "app/reports").splitlines()
unexpected = [f for f in tracked
if f not in ALLOWED_TRACKED_RUNTIME]
self.assertEqual(
unexpected, [],
"git-tracked runtime files would make `git pull` conflict; "
"add them to .gitignore: %s" % unexpected)
def test_runtime_write_paths_ignored(self):
"""Every runtime write path is ignored by git (won't dirty the tree)."""
stats = subprocess.run(
["git", "check-ignore", "-v",
"app/data/scan_state.json", "app/data/scan_jobs.json",
"app/data/whois_cache.json", "app/data/ids.db",
"app/data/ids.db-wal", "app/reports/app/data/blocklist_ipv4.txt",
], cwd=ROOT, capture_output=True, text=True).stdout
self.assertIn("scan_state.json", stats)
self.assertIn("ids.db", stats)
self.assertIn("blocklist_ipv4.txt", stats)
if __name__ == "__main__":
unittest.main()