|
3 | 3 | ``build_erasure_mailer`` / ``build_posthog_eraser`` pick the real |
4 | 4 | integration when its settings are configured and the Noop otherwise — |
5 | 5 | AppSettings is env-only, so both branches are probed via env vars. |
| 6 | +``build_account_erasure_service`` (the worker-side composition of the |
| 7 | +same cascade) is probed the same way: env decides which side-effect |
| 8 | +clients get wired, primitives are fakes. |
6 | 9 | """ |
7 | 10 |
|
8 | 11 | from unittest.mock import MagicMock |
9 | 12 |
|
10 | 13 | import pytest |
11 | 14 |
|
12 | 15 | from config import AppSettings |
13 | | -from dependencies.wiring import build_erasure_mailer, build_posthog_eraser |
| 16 | +from dependencies.wiring import ( |
| 17 | + build_account_erasure_service, |
| 18 | + build_erasure_mailer, |
| 19 | + build_posthog_eraser, |
| 20 | +) |
| 21 | +from infrastructure.cloudflare_kv import CloudflareKVClient |
14 | 22 | from infrastructure.email.zeptomail import ZeptoMailProvider |
15 | 23 | from infrastructure.posthog_erasure import HttpPostHogEraser |
| 24 | +from infrastructure.storage.r2 import R2StorageClient |
16 | 25 | from services.account_erasure_service import ( |
| 26 | + AccountErasureService, |
17 | 27 | NoopErasureMailer, |
18 | 28 | NoopPostHogEraser, |
19 | 29 | ) |
| 30 | +from services.cf_saas_backend import CfSaasBackend |
| 31 | +from services.edge_cache.og_writethrough import OgEdgeWritethrough |
| 32 | +from services.mock_dcv_backend import MockDcvBackend |
| 33 | + |
| 34 | +_SIDE_EFFECT_ENV = ( |
| 35 | + "ZEPTO_API_TOKEN", |
| 36 | + "POSTHOG_ERASURE_API_KEY", |
| 37 | + "POSTHOG_ERASURE_PROJECT_ID", |
| 38 | + "POSTHOG_ERASURE_HOST", |
| 39 | + "EDGE_CACHE_CF_ACCOUNT_ID", |
| 40 | + "EDGE_CACHE_CF_API_TOKEN", |
| 41 | + "EDGE_CACHE_KV_NAMESPACE_ID", |
| 42 | + "R2_ACCOUNT_ID", |
| 43 | + "R2_ACCESS_KEY_ID", |
| 44 | + "R2_SECRET_ACCESS_KEY", |
| 45 | + "R2_BUCKET", |
| 46 | + "R2_PUBLIC_BASE_URL", |
| 47 | + "CUSTOM_DOMAINS_MOCK_DCV", |
| 48 | +) |
20 | 49 |
|
21 | 50 |
|
22 | 51 | @pytest.fixture |
23 | 52 | def base_env(monkeypatch): |
24 | 53 | monkeypatch.setenv("MONGODB_URI", "mongodb://localhost:27017/") |
25 | | - monkeypatch.delenv("ZEPTO_API_TOKEN", raising=False) |
26 | | - monkeypatch.delenv("POSTHOG_ERASURE_API_KEY", raising=False) |
27 | | - monkeypatch.delenv("POSTHOG_ERASURE_PROJECT_ID", raising=False) |
28 | | - monkeypatch.delenv("POSTHOG_ERASURE_HOST", raising=False) |
| 54 | + for var in _SIDE_EFFECT_ENV: |
| 55 | + monkeypatch.delenv(var, raising=False) |
29 | 56 | return monkeypatch |
30 | 57 |
|
31 | 58 |
|
@@ -81,3 +108,54 @@ def test_host_override(self, base_env): |
81 | 108 | eraser = build_posthog_eraser(AppSettings(), MagicMock()) |
82 | 109 | assert isinstance(eraser, HttpPostHogEraser) |
83 | 110 | assert eraser._host == "https://us.posthog.com" |
| 111 | + |
| 112 | + |
| 113 | +class TestBuildAccountErasureService: |
| 114 | + """Worker-side composition root: same env gates as the app wiring.""" |
| 115 | + |
| 116 | + def _build(self): |
| 117 | + # Mock db mapping, inert http client, redis None (cache |
| 118 | + # invalidation degrades to no-ops, as in workers without Redis). |
| 119 | + return build_account_erasure_service( |
| 120 | + MagicMock(), AppSettings(), MagicMock(), None |
| 121 | + ) |
| 122 | + |
| 123 | + def test_minimal_env_wires_disabled_side_effects(self, base_env): |
| 124 | + service = self._build() |
| 125 | + assert isinstance(service, AccountErasureService) |
| 126 | + # Edge cache + R2 unconfigured ⇒ no KV purge, no og write-through, |
| 127 | + # no R2 sweep. |
| 128 | + assert service._r2_storage is None |
| 129 | + assert service._url_service._edge_kv is None |
| 130 | + assert service._url_service._og_writethrough is None |
| 131 | + assert service._url_service._r2_storage is None |
| 132 | + # Unconfigured mail/PostHog degrade to Noops, as in the app. |
| 133 | + assert isinstance(service._mailer, NoopErasureMailer) |
| 134 | + assert isinstance(service._posthog, NoopPostHogEraser) |
| 135 | + |
| 136 | + def test_default_dcv_selects_cf_saas_backend(self, base_env): |
| 137 | + service = self._build() |
| 138 | + assert isinstance(service._domain_service._edge, CfSaasBackend) |
| 139 | + |
| 140 | + def test_mock_dcv_selects_mock_backend(self, base_env): |
| 141 | + base_env.setenv("CUSTOM_DOMAINS_MOCK_DCV", "true") |
| 142 | + service = self._build() |
| 143 | + assert isinstance(service._domain_service._edge, MockDcvBackend) |
| 144 | + |
| 145 | + def test_configured_edge_and_r2_wire_real_clients(self, base_env): |
| 146 | + base_env.setenv("EDGE_CACHE_CF_ACCOUNT_ID", "acct") |
| 147 | + base_env.setenv("EDGE_CACHE_CF_API_TOKEN", "kv-token") |
| 148 | + base_env.setenv("EDGE_CACHE_KV_NAMESPACE_ID", "ns") |
| 149 | + base_env.setenv("R2_ACCOUNT_ID", "acct") |
| 150 | + base_env.setenv("R2_ACCESS_KEY_ID", "key") |
| 151 | + base_env.setenv("R2_SECRET_ACCESS_KEY", "secret") |
| 152 | + base_env.setenv("R2_BUCKET", "og-images") |
| 153 | + base_env.setenv("R2_PUBLIC_BASE_URL", "https://og.spoo.me") |
| 154 | + service = self._build() |
| 155 | + url_service = service._url_service |
| 156 | + assert isinstance(url_service._edge_kv, CloudflareKVClient) |
| 157 | + assert isinstance(url_service._og_writethrough, OgEdgeWritethrough) |
| 158 | + assert isinstance(url_service._r2_storage, R2StorageClient) |
| 159 | + # The erasure R2 sweep and the URL service delete path must share |
| 160 | + # ONE client — a split here would orphan uploaded og:images. |
| 161 | + assert service._r2_storage is url_service._r2_storage |
0 commit comments