-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_collateral.py
More file actions
166 lines (147 loc) · 6.09 KB
/
Copy pathtest_collateral.py
File metadata and controls
166 lines (147 loc) · 6.09 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
"""Tests for CollateralService — LTV tracking and liquidation.
Tests verify behavior through public interfaces only:
- get() method for querying collateral state
- Emitted events (COLLATERAL_MARKED, COLLATERAL_LIQUIDATED)
- Edge cases: missing fields, unknown borrower, zero principal
"""
from __future__ import annotations
import pytest
from underwrite.__bus__ import LocalBus
from underwrite.__events__ import Event, EventType
from underwrite.services.collateral.service import CollateralService
def collateral(bus=None) -> CollateralService:
return CollateralService(service_id="collateral", bus=bus)
class TestCollateralService:
def test_tracks_collateral_on_origination(self) -> None:
svc = collateral()
svc.handle(
Event(
event_type=EventType.LOAN_ORIGINATED,
source="test",
payload={"borrower": "alice", "principal": 10000},
)
)
col = svc.get("alice")
assert col is not None
assert col["principal"] == 10000
assert col["required"] == 7500.0
assert col["ltv"] == 0.75
def test_emits_marked_event_with_correct_values(self) -> None:
bus = LocalBus()
received: list[Event] = []
bus.subscribe(EventType.COLLATERAL_MARKED, lambda e: received.append(e))
svc = collateral(bus=bus)
bus.start()
svc.handle(
Event(
event_type=EventType.LOAN_ORIGINATED,
source="test",
payload={"borrower": "bob", "principal": 50000},
)
)
assert len(received) == 1
assert received[0].payload["borrower"] == "bob"
assert received[0].payload["required"] == 37500.0
assert received[0].payload["ltv_ratio"] == 0.75
def test_liquidates_on_default_and_emits(self) -> None:
bus = LocalBus()
received: list[Event] = []
bus.subscribe(EventType.COLLATERAL_LIQUIDATED, lambda e: received.append(e))
svc = collateral(bus=bus)
bus.start()
svc.handle(
Event(
event_type=EventType.LOAN_ORIGINATED,
source="test",
payload={"borrower": "carol", "principal": 20000},
)
)
svc.handle(
Event(
event_type=EventType.DEFAULT_OCCURRED,
source="test",
payload={"borrower": "carol", "principal": 20000},
)
)
assert len(received) == 1
assert received[0].payload["borrower"] == "carol"
assert received[0].payload["principal"] == 20000.0
assert svc.get("carol") is None
def test_default_without_collateral_does_nothing(self) -> None:
bus = LocalBus()
received: list[Event] = []
bus.subscribe(EventType.COLLATERAL_LIQUIDATED, lambda e: received.append(e))
svc = collateral(bus=bus)
bus.start()
svc.handle(
Event(
event_type=EventType.DEFAULT_OCCURRED,
source="test",
payload={"borrower": "nobody"},
)
)
assert len(received) == 0
def test_get_unknown_borrower_returns_none(self) -> None:
svc = collateral()
assert svc.get("ghost") is None
def test_multiple_borrowers_independent(self) -> None:
svc = collateral()
svc.handle(
Event(event_type=EventType.LOAN_ORIGINATED, source="test", payload={"borrower": "a", "principal": 100})
)
svc.handle(
Event(event_type=EventType.LOAN_ORIGINATED, source="test", payload={"borrower": "b", "principal": 200})
)
col_a = svc.get("a")
assert col_a is not None
assert col_a["principal"] == 100
col_b = svc.get("b")
assert col_b is not None
assert col_b["principal"] == 200
assert col_a["required"] == 75.0
assert col_b["required"] == 150.0
def test_ignores_unrelated_event_types(self) -> None:
bus = LocalBus()
received: list[Event] = []
bus.subscribe(EventType.COLLATERAL_MARKED, lambda e: received.append(e))
svc = collateral(bus=bus)
bus.start()
svc.handle(Event(event_type="seed.added", source="test", payload={}))
svc.handle(Event(event_type="user.added", source="test", payload={}))
assert len(received) == 0
assert svc.get("anyone") is None
def test_origination_without_borrower_is_rejected(self) -> None:
svc = collateral()
from underwrite.__exceptions__ import ProtocolError
with pytest.raises(ProtocolError):
svc.handle(Event(event_type=EventType.LOAN_ORIGINATED, source="test", payload={}))
assert svc.get("") is None
def test_origination_without_principal_defaults_zero(self) -> None:
svc = collateral()
svc.handle(Event(event_type=EventType.LOAN_ORIGINATED, source="test", payload={"borrower": "x"}))
col_x = svc.get("x")
assert col_x is not None
assert col_x["required"] == 0.0
def test_liquidation_removes_borrower(self) -> None:
svc = collateral()
svc.handle(
Event(event_type=EventType.LOAN_ORIGINATED, source="test", payload={"borrower": "y", "principal": 5000})
)
assert svc.get("y") is not None
svc.handle(Event(event_type=EventType.DEFAULT_OCCURRED, source="test", payload={"borrower": "y"}))
assert svc.get("y") is None
def test_emit_both_marked_and_liquidated_on_lifecycle(self) -> None:
bus = LocalBus()
all_events: list[Event] = []
bus.subscribe("*", lambda e: all_events.append(e))
svc = collateral(bus=bus)
bus.start()
svc.handle(
Event(event_type=EventType.LOAN_ORIGINATED, source="test", payload={"borrower": "z", "principal": 10000})
)
svc.handle(
Event(event_type=EventType.DEFAULT_OCCURRED, source="test", payload={"borrower": "z", "principal": 10000})
)
types = [e.event_type for e in all_events]
assert EventType.COLLATERAL_MARKED in types
assert EventType.COLLATERAL_LIQUIDATED in types