|
5 | 5 | from gooddata_eval.core.agentic.alert_skill import ( |
6 | 6 | AlertEvaluation, |
7 | 7 | _check_filters, |
| 8 | + _check_recipients, |
8 | 9 | _check_trigger, |
9 | 10 | _deep_subset, |
10 | 11 | _normalize_expected_output, |
@@ -141,6 +142,83 @@ def test_normalize_expected_filters_treats_prose_filters_column_as_unspecified() |
141 | 142 | assert _check_filters(expected, {"filters": [_ATTR_FILTER]}) is True |
142 | 143 |
|
143 | 144 |
|
| 145 | +def test_check_recipients_matches_external_recipients_without_sdk(): |
| 146 | + # The common path never needs a network call at all -- confirms adding the |
| 147 | + # internal_recipients fallback doesn't force a lookup when it isn't needed. |
| 148 | + expected = _normalize_expected_output({"Recipients": ["user@example.com"]}) |
| 149 | + mock_sdk = MagicMock() |
| 150 | + assert _check_recipients(expected, {"recipients": ["user@example.com"]}, sdk=mock_sdk) is True |
| 151 | + mock_sdk._client.entities_api.get_all_entities_users.assert_not_called() |
| 152 | + |
| 153 | + |
| 154 | +def test_check_recipients_matches_internal_recipients_via_resolved_user_id(): |
| 155 | + # Some notification channels are workspace-restricted to internal users -- |
| 156 | + # create_metric_alert then addresses the alert by internal user id via |
| 157 | + # `internal_recipients`, never by email, so the plain email/external-recipients |
| 158 | + # comparison alone can never match this delivery path. |
| 159 | + expected = _normalize_expected_output({"Recipients": ["user@example.com"]}) |
| 160 | + mock_sdk = MagicMock() |
| 161 | + mock_sdk._client.entities_api.get_all_entities_users.return_value.data = [ |
| 162 | + MagicMock(id="user.abc123"), |
| 163 | + ] |
| 164 | + assert _check_recipients(expected, {"internal_recipients": ["user.abc123"]}, sdk=mock_sdk) is True |
| 165 | + mock_sdk._client.entities_api.get_all_entities_users.assert_called_once_with(filter="email=in=('user@example.com')") |
| 166 | + |
| 167 | + |
| 168 | +def test_check_recipients_escapes_apostrophe_in_email_for_rsql_filter(): |
| 169 | + # o'hara@example.com must not break the RSQL filter string -- the apostrophe |
| 170 | + # has to be escaped before interpolation, same as the query engine requires. |
| 171 | + expected = _normalize_expected_output({"Recipients": ["o'hara@example.com"]}) |
| 172 | + mock_sdk = MagicMock() |
| 173 | + mock_sdk._client.entities_api.get_all_entities_users.return_value.data = [ |
| 174 | + MagicMock(id="user.abc123"), |
| 175 | + ] |
| 176 | + assert _check_recipients(expected, {"internal_recipients": ["user.abc123"]}, sdk=mock_sdk) is True |
| 177 | + mock_sdk._client.entities_api.get_all_entities_users.assert_called_once_with( |
| 178 | + filter="email=in=('o\\'hara@example.com')" |
| 179 | + ) |
| 180 | + |
| 181 | + |
| 182 | +def test_check_recipients_resolves_multiple_emails_in_a_single_bulk_request(): |
| 183 | + # N expected recipients must cost one request, not N -- confirmed against the |
| 184 | + # live Users entities API that RSQL `=in=(...)` returns only the matching subset. |
| 185 | + expected = _normalize_expected_output({"Recipients": ["a@example.com", "b@example.com"]}) |
| 186 | + mock_sdk = MagicMock() |
| 187 | + mock_sdk._client.entities_api.get_all_entities_users.return_value.data = [ |
| 188 | + MagicMock(id="user.a"), |
| 189 | + MagicMock(id="user.b"), |
| 190 | + ] |
| 191 | + assert _check_recipients(expected, {"internal_recipients": ["user.a", "user.b"]}, sdk=mock_sdk) is True |
| 192 | + mock_sdk._client.entities_api.get_all_entities_users.assert_called_once_with( |
| 193 | + filter="email=in=('a@example.com','b@example.com')" |
| 194 | + ) |
| 195 | + |
| 196 | + |
| 197 | +def test_check_recipients_internal_recipients_mismatch_still_fails(): |
| 198 | + expected = _normalize_expected_output({"Recipients": ["user@example.com"]}) |
| 199 | + mock_sdk = MagicMock() |
| 200 | + mock_sdk._client.entities_api.get_all_entities_users.return_value.data = [ |
| 201 | + MagicMock(id="someone.else"), |
| 202 | + ] |
| 203 | + assert _check_recipients(expected, {"internal_recipients": ["user.abc123"]}, sdk=mock_sdk) is False |
| 204 | + |
| 205 | + |
| 206 | +def test_check_recipients_internal_recipients_without_sdk_fails_gracefully(): |
| 207 | + # No sdk available to resolve the email -> no crash, just no match (the plain |
| 208 | + # external-recipients comparison already ran and failed by this point). |
| 209 | + expected = _normalize_expected_output({"Recipients": ["user@example.com"]}) |
| 210 | + assert _check_recipients(expected, {"internal_recipients": ["user.abc123"]}, sdk=None) is False |
| 211 | + |
| 212 | + |
| 213 | +def test_check_recipients_resolution_failure_fails_gracefully(): |
| 214 | + # A lookup error (permissions, network) must not crash the evaluation -- |
| 215 | + # it just means this comparison path can't match, same as no sdk at all. |
| 216 | + expected = _normalize_expected_output({"Recipients": ["user@example.com"]}) |
| 217 | + mock_sdk = MagicMock() |
| 218 | + mock_sdk._client.entities_api.get_all_entities_users.side_effect = RuntimeError("boom") |
| 219 | + assert _check_recipients(expected, {"internal_recipients": ["user.abc123"]}, sdk=mock_sdk) is False |
| 220 | + |
| 221 | + |
144 | 222 | def test_alert_evaluation_strict_pass(): |
145 | 223 | ev = AlertEvaluation( |
146 | 224 | alert_created=True, |
|
0 commit comments