conftest.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. """Shared test fixtures.
  2. Isolation is the top priority: no test may touch the real config.toml or start a worker
  3. thread. A session-scoped autouse fixture points PPSQ_CONFIG at a temp copy of
  4. config.example.toml BEFORE any app module is imported, and ConfigStore itself refuses to
  5. open the default path under pytest.
  6. """
  7. from __future__ import annotations
  8. import os
  9. import shutil
  10. from pathlib import Path
  11. import pytest
  12. ROOT = Path(__file__).resolve().parents[1]
  13. EXAMPLE = ROOT / "config.example.toml"
  14. # http base_url in the example would fail https validation; allow it for tests.
  15. os.environ.setdefault("PPSQ_ALLOW_INSECURE", "1")
  16. def _write_config(dest: Path) -> Path:
  17. """Copy the example config and make it bootable for tests: strong secret, dev URL,
  18. static auth with a known dev account."""
  19. text = EXAMPLE.read_text()
  20. text = text.replace(
  21. 'secret_key = "change-me-to-a-random-string"',
  22. 'secret_key = "test-secret-key-that-is-definitely-long-enough-xx"',
  23. )
  24. text = text.replace(
  25. 'base_url = "https://pps.example.com:10000"',
  26. 'base_url = "http://pps.example.invalid:10000"',
  27. )
  28. text = text.replace('mode = "oidc"', 'mode = "static"')
  29. # Known dev credentials for the static-login tests.
  30. text = text.replace('username = "admin"\npassword = "admin"',
  31. 'username = "dev"\npassword = "devpass"')
  32. text = text.replace('email = "dev-admin@example.invalid"',
  33. 'email = "dev@example.invalid"')
  34. dest.write_text(text)
  35. return dest
  36. @pytest.fixture(scope="session", autouse=True)
  37. def _isolate_config(tmp_path_factory):
  38. cfg = tmp_path_factory.mktemp("cfg") / "config.toml"
  39. _write_config(cfg)
  40. os.environ["PPSQ_CONFIG"] = str(cfg)
  41. yield
  42. @pytest.fixture
  43. def config_path(tmp_path):
  44. """A fresh, writable config file per test."""
  45. return _write_config(tmp_path / "config.toml")
  46. @pytest.fixture
  47. def store(config_path):
  48. from config_store import ConfigStore
  49. return ConfigStore(config_path)
  50. class FakePPS:
  51. """Records every call; returns canned data for search/get_raw."""
  52. def __init__(self, records=None):
  53. self.calls = []
  54. self._records = records or []
  55. def act(self, action, folder, localguids, *, targetfolder=None, deletedfolder=None, scan=False):
  56. self.calls.append(
  57. {"m": "act", "action": action, "folder": folder,
  58. "localguids": list(localguids), "targetfolder": targetfolder,
  59. "deletedfolder": deletedfolder, "scan": scan}
  60. )
  61. return {"status": "ok"}
  62. def search(self, folder, query, limit=200, days_back=7, enddate=None):
  63. self.calls.append({"m": "search", "folder": folder, "enddate": enddate})
  64. return list(self._records)
  65. def get_raw(self, guid):
  66. self.calls.append({"m": "get_raw", "guid": guid})
  67. return b"From: x@y\r\nSubject: t\r\n\r\nbody"
  68. def searches(self):
  69. return [c for c in self.calls if c["m"] == "search"]
  70. def acts(self):
  71. return [c for c in self.calls if c["m"] == "act"]
  72. @pytest.fixture
  73. def fake_pps():
  74. return FakePPS()
  75. @pytest.fixture
  76. def make_queue(tmp_path):
  77. """Factory: a JobQueue wired to a FakePPS and a live cfg provider."""
  78. from worker import JobQueue
  79. def _make(pps, cfg):
  80. return JobQueue(
  81. str(tmp_path / "jobs.db"),
  82. pps_provider=lambda: pps,
  83. cfg_provider=lambda: cfg,
  84. ops_log_path=str(tmp_path / "worker.log"),
  85. )
  86. return _make
  87. @pytest.fixture
  88. def app_client(store, fake_pps, tmp_path, monkeypatch):
  89. """A Flask test client. No worker thread — tests drive queue._process directly."""
  90. from app import create_app
  91. from prefs import PrefStore
  92. from worker import JobQueue
  93. # Point the store's PPS client at the fake.
  94. monkeypatch.setattr(store, "pps", lambda: fake_pps)
  95. queue = JobQueue(
  96. str(tmp_path / "jobs.db"),
  97. pps_provider=lambda: fake_pps,
  98. cfg_provider=lambda: store.snapshot().quarantine,
  99. ops_log_path=str(tmp_path / "worker.log"),
  100. )
  101. prefs = PrefStore(str(tmp_path / "jobs.db"))
  102. app = create_app(store, queue, prefs)
  103. app.config["TESTING"] = True
  104. client = app.test_client()
  105. client._store = store
  106. client._queue = queue
  107. client._pps = fake_pps
  108. return client
  109. def login_static(client, username="dev", password="devpass"):
  110. """Log in via static mode and return the CSRF token."""
  111. client.post("/login", data={"username": username, "password": password})
  112. return client.get("/api/config").get_json()["csrf_token"]