-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_notification.py
More file actions
163 lines (135 loc) · 6.16 KB
/
Copy pathtest_notification.py
File metadata and controls
163 lines (135 loc) · 6.16 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
"""Tests for NotificationService — dispatch alerts to configured channels.
Tests verify behavior through emitted NOTIFICATION_SENT events and
direct dispatch of the background notification logic.
"""
from __future__ import annotations
from unittest.mock import patch
from underwrite.__bus__ import LocalBus
from underwrite.__events__ import Event, EventType
from underwrite.services.notification.service import NotificationService
def notify(bus=None) -> NotificationService:
return NotificationService(service_id="notify", bus=bus)
class TestNotificationService:
def __assert_forwards(self, event_type: str, payload: dict) -> None:
bus = LocalBus()
received: list[Event] = []
bus.subscribe(EventType.NOTIFICATION_SENT, lambda e: received.append(e))
svc = notify(bus=bus)
bus.start()
svc.handle(Event(event_type=event_type, source="test", payload=payload))
assert len(received) == 1
assert received[0].payload["original_event"] == event_type
assert received[0].payload["payload"] == payload
def test_forwards_fraud_alert(self) -> None:
self.__assert_forwards(EventType.FRAUD_ALERT, {"borrower": "alice"})
def test_forwards_wash_flag(self) -> None:
self.__assert_forwards(EventType.WASH_FLAG, {"borrower": "bob", "cycles": 5})
def test_forwards_velocity_flag(self) -> None:
self.__assert_forwards(EventType.VELOCITY_FLAG, {"borrower": "carol"})
def test_forwards_early_warning(self) -> None:
self.__assert_forwards(EventType.RISK_EARLY_WARNING, {"borrower": "dave", "dp": 0.35})
def test_forwards_npa_bucket_changed(self) -> None:
self.__assert_forwards(EventType.NPA_BUCKET_CHANGED, {"borrower": "eve", "bucket": "substandard"})
def test_forwards_dlg_triggered(self) -> None:
self.__assert_forwards(EventType.DLG_TRIGGERED, {"loan_id": "frank", "amount": 10000})
def test_ignores_non_alert_events(self) -> None:
bus = LocalBus()
received: list[Event] = []
bus.subscribe(EventType.NOTIFICATION_SENT, lambda e: received.append(e))
svc = notify(bus=bus)
bus.start()
for et in [
EventType.SEED_ADDED,
EventType.USER_ADDED,
EventType.QUOTE_CALCULATED,
EventType.LOAN_ORIGINATED,
EventType.REPAID,
EventType.GOVERNANCE_EXECUTED,
]:
svc.handle(Event(event_type=et, source="test", payload={}))
assert len(received) == 0
def test_captures_all_alert_types(self) -> None:
alert_types = [
EventType.FRAUD_ALERT,
EventType.WASH_FLAG,
EventType.VELOCITY_FLAG,
EventType.RISK_EARLY_WARNING,
EventType.NPA_BUCKET_CHANGED,
EventType.DLG_TRIGGERED,
]
for at in alert_types:
bus = LocalBus()
received: list[Event] = []
def record(e: Event, _r: list[Event] = received) -> None:
_r.append(e)
bus.subscribe(EventType.NOTIFICATION_SENT, record)
svc = notify(bus=bus)
bus.start()
svc.handle(Event(event_type=at, source="test", payload={"k": "v"}))
assert len(received) == 1, f"Failed to forward {at}"
# ------------------------------------------------------------------ #
# Dispatch notification tests (log-only mode, no email/SMS) #
# ------------------------------------------------------------------ #
def test_dispatch_notification_logs_borrower_recipient(self) -> None:
svc = notify()
event = Event(event_type=EventType.FRAUD_ALERT, source="test", payload={"borrower": "alice"})
with patch.object(
svc._executor,
"submit",
) as mock_submit:
svc.handle(event)
assert mock_submit.call_count == 1
def test_dispatch_falls_back_to_user_recipient(self) -> None:
svc = notify()
event = Event(event_type=EventType.FRAUD_ALERT, source="test", payload={"user": "bob"})
with patch.object(
svc._executor,
"submit",
) as mock_submit:
svc.handle(event)
assert mock_submit.call_count == 1
def test_dispatch_logs_info_in_log_only_mode(self) -> None:
svc = notify()
event = Event(event_type=EventType.WASH_FLAG, source="test", payload={"borrower": "carol", "cycles": 5})
with patch.object(
svc._executor,
"submit",
) as mock_submit:
svc.handle(event)
assert mock_submit.call_count == 1
def test_notification_sent_before_dispatch_completes(self) -> None:
bus = LocalBus()
received: list[Event] = []
bus.subscribe(EventType.NOTIFICATION_SENT, lambda e: received.append(e))
svc = notify(bus=bus)
bus.start()
dispatched: list[bool] = []
assert svc._executor is not None
original_submit = svc._executor.submit
def delayed_submit(fn, *args, **kwargs):
result = original_submit(fn, *args, **kwargs)
dispatched.append(True)
return result
svc._executor.submit = delayed_submit # type: ignore[assignment]
svc.handle(Event(event_type=EventType.DLG_TRIGGERED, source="test", payload={"loan_id": "L1", "amount": 5000}))
assert len(received) == 1
import time
time.sleep(0.05)
assert len(dispatched) == 1
def test_stop_shuts_down_executor(self) -> None:
svc = notify()
executor = svc._executor
assert executor is not None
svc.stop()
assert svc._executor is None
def test_handle_passes_payload_to_notification_sent(self) -> None:
bus = LocalBus()
received: list[Event] = []
bus.subscribe(EventType.NOTIFICATION_SENT, lambda e: received.append(e))
svc = notify(bus=bus)
bus.start()
pl = {"borrower": "dave", "dp": 0.45}
svc.handle(Event(event_type=EventType.RISK_EARLY_WARNING, source="test", payload=pl))
assert len(received) == 1
assert received[0].payload["original_event"] == EventType.RISK_EARLY_WARNING
assert received[0].payload["payload"] == pl