-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_tenancy_services.py
More file actions
181 lines (133 loc) · 6.48 KB
/
Copy pathtest_tenancy_services.py
File metadata and controls
181 lines (133 loc) · 6.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
"""Per-user settings, budget ledger, and vault services on the tenancy primitives."""
from __future__ import annotations
import pytest
from openjobradar.config.errors import ConfigError
from openjobradar.tenancy import (
InMemoryStore,
InvalidTenantError,
SettingsService,
SettingsVersionConflict,
TenantContext,
TenantRepository,
)
from openjobradar.tenancy.budget import BudgetLedger, UserLimits
from openjobradar.tenancy.vault import InsecureTestCipher, VaultService
ALICE = TenantContext.from_sub("alice-sub-0000001")
PLAN = {"budget": {"daily_cap_usd": 2.0}}
def _repo() -> TenantRepository:
return TenantRepository(InMemoryStore())
# --- Settings service ------------------------------------------------------
def test_settings_resolve_through_full_layer_chain() -> None:
svc = SettingsService(_repo(), plan_defaults=PLAN)
config = svc.get(ALICE)
assert config.get("alerts.alert_threshold") == 78 # platform default
assert config.get("budget.daily_cap_usd") == 2.0 # plan layer
assert config.source_of("budget") == "plan"
def test_save_validates_fail_closed_and_layers_win() -> None:
svc = SettingsService(_repo(), plan_defaults=PLAN)
with pytest.raises(ConfigError):
svc.save(ALICE, {"alerts": {"alert_threshold": "high"}})
version = svc.save(ALICE, {"alerts": {"alert_threshold": 82}})
assert version == 1
config = svc.get(ALICE)
assert config.get("alerts.alert_threshold") == 82
assert config.get("dry_run") is True
assert config.source_of("alerts") == "user"
def test_optimistic_versioning_blocks_lost_updates() -> None:
svc = SettingsService(_repo())
v1 = svc.save(ALICE, {"dry_run": True})
with pytest.raises(SettingsVersionConflict):
svc.save(ALICE, {"dry_run": False}, expected_version=v1 - 1)
assert svc.save(ALICE, {"dry_run": False}, expected_version=v1) == 2
def test_reset_returns_to_platform_defaults() -> None:
svc = SettingsService(_repo())
svc.save(ALICE, {"alerts": {"alert_threshold": 90}})
svc.reset(ALICE)
assert svc.get(ALICE).get("alerts.alert_threshold") == 78
def test_settings_are_isolated_per_user() -> None:
BOB = TenantContext.from_sub("bob-sub-00000002")
svc = SettingsService(_repo())
svc.save(ALICE, {"alerts": {"alert_threshold": 95}})
assert svc.get(BOB).get("alerts.alert_threshold") == 78
def test_session_override_wins_but_never_persists() -> None:
svc = SettingsService(_repo())
override = svc.get(ALICE, overrides={"alerts": {"alert_threshold": 99}})
assert override.get("alerts.alert_threshold") == 99
assert svc.get(ALICE).get("alerts.alert_threshold") == 78
# --- Budget ledger ---------------------------------------------------------
def _ledger(platform_spent: float = 0.0) -> tuple[BudgetLedger, dict]:
state = {"platform": platform_spent}
ledger = BudgetLedger(
_repo(),
limits_for=lambda ctx: UserLimits(daily_cap_usd=5.0, monthly_cap_usd=20.0),
platform_daily_cap_usd=100.0,
platform_spent_today=lambda day: state["platform"],
)
return ledger, state
def test_spend_within_caps_records_to_both_rows() -> None:
ledger, _ = _ledger()
decision = ledger.record(ALICE, 1.5, "2026-08-23", "2026-08")
assert decision.allowed
usage = ledger.usage(ALICE, "2026-08-23", "2026-08")
assert usage == {"daily": 1.5, "monthly": 1.5}
def test_daily_cap_blocks_before_invocation() -> None:
ledger, _ = _ledger()
assert ledger.record(ALICE, 4.0, "2026-08-23", "2026-08").allowed
blocked = ledger.can_spend(ALICE, 2.0, "2026-08-23", "2026-08")
assert not blocked.allowed and "daily cap" in blocked.reason
def test_monthly_cap_blocks_when_days_accumulate() -> None:
ledger, _ = _ledger()
for day in ("2026-08-01", "2026-08-02", "2026-08-03", "2026-08-04"):
ledger.record(ALICE, 4.9, day, "2026-08")
blocked = ledger.can_spend(ALICE, 1.5, "2026-08-05", "2026-08")
assert not blocked.allowed and "monthly cap" in blocked.reason
def test_platform_ceiling_blocks_everyone() -> None:
ledger, state = _ledger(platform_spent=99.5)
blocked = ledger.can_spend(ALICE, 1.0, "2026-08-23", "2026-08")
assert not blocked.allowed and "platform-wide" in blocked.reason
state["platform"] = 10.0
assert ledger.can_spend(ALICE, 1.0, "2026-08-23", "2026-08").allowed
def test_malformed_period_keys_rejected() -> None:
ledger, _ = _ledger()
with pytest.raises(ValueError):
ledger.can_spend(ALICE, 1.0, "08/23/2026", "2026-08")
def test_ledgers_are_isolated_per_user() -> None:
BOB = TenantContext.from_sub("bob-sub-00000002")
ledger, _ = _ledger()
ledger.record(ALICE, 4.9, "2026-08-23", "2026-08")
assert ledger.can_spend(BOB, 1.0, "2026-08-23", "2026-08").allowed
# --- Vault -----------------------------------------------------------------
def test_vault_roundtrip_and_masked_views() -> None:
vault = VaultService(_repo(), InsecureTestCipher())
view = vault.add(ALICE, "adzuna", "super-secret-key-9999", today="2026-08-01")
assert view.masked_tail == "****9999"
assert "secret-key" not in str(view)
assert vault.reveal(ALICE, "adzuna") == "super-secret-key-9999"
ids = vault.list_ids(ALICE)
assert ids == ["adzuna"]
def test_vault_rotation_bookkeeping() -> None:
vault = VaultService(_repo(), InsecureTestCipher())
vault.add(ALICE, "hunter", "old-secret-1234", today="2026-06-01", rotation_days=30)
stale = vault.describe(ALICE, "hunter", today="2026-08-01")
assert stale.needs_rotation
vault.rotate(ALICE, "hunter", "new-secret-5678", today="2026-08-01")
fresh = vault.describe(ALICE, "hunter", today="2026-08-01")
assert not fresh.needs_rotation
assert fresh.last_rotated == "2026-08-01"
assert vault.reveal(ALICE, "hunter") == "new-secret-5678"
def test_vault_secrets_never_cross_tenants() -> None:
BOB = TenantContext.from_sub("bob-sub-00000002")
vault = VaultService(_repo(), InsecureTestCipher())
vault.add(ALICE, "sam", "alice-only-8888", today="2026-08-01")
with pytest.raises(KeyError):
vault.reveal(BOB, "sam")
assert vault.list_ids(BOB) == []
def test_vault_rejects_empty_secret() -> None:
vault = VaultService(_repo(), InsecureTestCipher())
with pytest.raises(ValueError):
vault.add(ALICE, "empty", "", today="2026-08-01")
def test_invalid_tenant_context_fails_everywhere() -> None:
repo = _repo()
svc = SettingsService(repo)
with pytest.raises(InvalidTenantError):
svc.save(None, {}) # type: ignore[arg-type]