Skip to content

Commit 9c03288

Browse files
authored
Merge pull request #1848 from elementary-data/messaging-integration-changes
Changes around messaging integrations
2 parents 7277349 + e41796a commit 9c03288

File tree

7 files changed

+249
-8
lines changed

7 files changed

+249
-8
lines changed

elementary/messages/messaging_integrations/base_messaging_integration.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818

1919
class MessageSendResult(BaseModel, Generic[T]):
2020
timestamp: datetime
21+
message_format: str
2122
message_context: Optional[T] = None
2223

2324

2425
DestinationType = TypeVar("DestinationType")
25-
MessageContextType = TypeVar("MessageContextType")
26+
MessageContextType = TypeVar("MessageContextType", bound=BaseModel)
2627

2728

2829
class BaseMessagingIntegration(ABC, Generic[DestinationType, MessageContextType]):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from pydantic import BaseModel
2+
3+
4+
class EmptyMessageContext(BaseModel):
5+
pass
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from typing import Dict
2+
3+
from elementary.messages.message_body import MessageBody
4+
from elementary.messages.messaging_integrations.base_messaging_integration import (
5+
BaseMessagingIntegration,
6+
MessageContextType,
7+
MessageSendResult,
8+
)
9+
from elementary.messages.messaging_integrations.exceptions import (
10+
MessagingIntegrationError,
11+
)
12+
13+
14+
class MappedMessagingIntegration(BaseMessagingIntegration[str, MessageContextType]):
15+
def __init__(
16+
self, mapping: Dict[str, BaseMessagingIntegration[None, MessageContextType]]
17+
):
18+
self._mapping = mapping
19+
20+
def send_message(
21+
self, destination: str, body: MessageBody
22+
) -> MessageSendResult[MessageContextType]:
23+
if destination not in self._mapping:
24+
raise MessagingIntegrationError(f"Invalid destination: {destination}")
25+
return self._mapping[destination].send_message(None, body)
26+
27+
def supports_reply(self) -> bool:
28+
return all(
29+
integration.supports_reply() for integration in self._mapping.values()
30+
)
31+
32+
def supports_actions(self) -> bool:
33+
return all(
34+
integration.supports_actions() for integration in self._mapping.values()
35+
)
36+
37+
def reply_to_message(
38+
self, destination: str, message_context: MessageContextType, body: MessageBody
39+
) -> MessageSendResult[MessageContextType]:
40+
if destination not in self._mapping:
41+
raise MessagingIntegrationError(f"Invalid destination: {destination}")
42+
return self._mapping[destination].reply_to_message(None, message_context, body)

elementary/messages/messaging_integrations/slack_web.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def _send_message(
100100
id=response["ts"], channel=response["channel"]
101101
),
102102
timestamp=response["ts"],
103+
message_format="block_kit",
103104
)
104105

105106
def _handle_send_err(self, err: SlackApiError, channel_name: str):

elementary/messages/messaging_integrations/slack_webhook.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
BaseMessagingIntegration,
1616
MessageSendResult,
1717
)
18+
from elementary.messages.messaging_integrations.empty_message_context import (
19+
EmptyMessageContext,
20+
)
1821
from elementary.messages.messaging_integrations.exceptions import (
1922
MessagingIntegrationError,
2023
)
@@ -23,7 +26,9 @@
2326
ONE_SECOND = 1
2427

2528

26-
class SlackWebhookMessagingIntegration(BaseMessagingIntegration[None, None]):
29+
class SlackWebhookMessagingIntegration(
30+
BaseMessagingIntegration[None, EmptyMessageContext]
31+
):
2732
def __init__(
2833
self, client: WebhookClient, tracking: Optional[Tracking] = None
2934
) -> None:
@@ -52,12 +57,13 @@ def _send_message(self, formatted_message: FormattedBlockKitMessage) -> None:
5257

5358
def send_message(
5459
self, destination: None, body: MessageBody
55-
) -> MessageSendResult[None]:
60+
) -> MessageSendResult[EmptyMessageContext]:
5661
formatted_message = format_block_kit(body)
5762
self._send_message(formatted_message)
5863
return MessageSendResult(
59-
message_context=destination,
64+
message_context=EmptyMessageContext(),
6065
timestamp=datetime.utcnow(),
66+
message_format="block_kit",
6167
)
6268

6369
def supports_reply(self) -> bool:

elementary/messages/messaging_integrations/teams_webhook.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
BaseMessagingIntegration,
1111
MessageSendResult,
1212
)
13+
from elementary.messages.messaging_integrations.empty_message_context import (
14+
EmptyMessageContext,
15+
)
1316
from elementary.messages.messaging_integrations.exceptions import (
1417
MessagingIntegrationError,
1518
)
@@ -44,21 +47,24 @@ def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response:
4447
return response
4548

4649

47-
class TeamsWebhookMessagingIntegration(BaseMessagingIntegration[Channel, Channel]):
50+
class TeamsWebhookMessagingIntegration(
51+
BaseMessagingIntegration[None, EmptyMessageContext]
52+
):
4853
def __init__(self, url: str) -> None:
4954
self.url = url
5055

5156
def send_message(
5257
self,
53-
destination: Channel,
58+
destination: None,
5459
body: MessageBody,
55-
) -> MessageSendResult[Channel]:
60+
) -> MessageSendResult[EmptyMessageContext]:
5661
card = format_adaptive_card(body)
5762
try:
5863
send_adaptive_card(self.url, card)
5964
return MessageSendResult(
60-
message_context=destination,
65+
message_context=EmptyMessageContext(),
6166
timestamp=datetime.utcnow(),
67+
message_format="adaptive_cards",
6268
)
6369
except requests.RequestException as e:
6470
raise MessagingIntegrationError(
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
from datetime import datetime
2+
from typing import Dict, List
3+
from unittest.mock import MagicMock
4+
5+
import pytest
6+
from pydantic import BaseModel
7+
8+
from elementary.messages.blocks import HeaderBlock
9+
from elementary.messages.message_body import MessageBody
10+
from elementary.messages.messaging_integrations.base_messaging_integration import (
11+
BaseMessagingIntegration,
12+
MessageSendResult,
13+
)
14+
from elementary.messages.messaging_integrations.exceptions import (
15+
MessagingIntegrationError,
16+
)
17+
from elementary.messages.messaging_integrations.mapped import MappedMessagingIntegration
18+
19+
20+
class MockMessageContext(BaseModel):
21+
id: str
22+
23+
24+
class MockMessagingIntegration(BaseMessagingIntegration[None, MockMessageContext]):
25+
def __init__(self, supports_reply: bool = True, supports_actions: bool = False):
26+
self.supports_reply_value = supports_reply
27+
self.supports_actions_value = supports_actions
28+
self.send_message_mock = MagicMock()
29+
self.send_message_mock.return_value = MessageSendResult(
30+
timestamp=datetime.now(),
31+
message_format="test_format",
32+
message_context=MockMessageContext(id="test_id"),
33+
)
34+
self.reply_to_message_mock = MagicMock()
35+
self.reply_to_message_mock.return_value = MessageSendResult(
36+
timestamp=datetime.now(),
37+
message_format="test_format",
38+
message_context=MockMessageContext(id="test_id"),
39+
)
40+
41+
def send_message(
42+
self, destination: None, body: MessageBody
43+
) -> MessageSendResult[MockMessageContext]:
44+
return self.send_message_mock(destination, body)
45+
46+
def supports_reply(self) -> bool:
47+
return self.supports_reply_value
48+
49+
def supports_actions(self) -> bool:
50+
return self.supports_actions_value
51+
52+
def reply_to_message(
53+
self,
54+
destination: None,
55+
message_context: MockMessageContext,
56+
body: MessageBody,
57+
) -> MessageSendResult[MockMessageContext]:
58+
return self.reply_to_message_mock(destination, message_context, body)
59+
60+
61+
@pytest.fixture
62+
def mock_integration() -> MockMessagingIntegration:
63+
return MockMessagingIntegration()
64+
65+
66+
@pytest.fixture
67+
def mapped_integration(
68+
mock_integration: MockMessagingIntegration,
69+
) -> MappedMessagingIntegration:
70+
return MappedMessagingIntegration({"test_destination": mock_integration})
71+
72+
73+
def test_send_message_success(
74+
mapped_integration: MappedMessagingIntegration,
75+
mock_integration: MockMessagingIntegration,
76+
) -> None:
77+
destination = "test_destination"
78+
body = MessageBody(blocks=[HeaderBlock(text="test message")])
79+
expected_result: MessageSendResult[MockMessageContext] = MessageSendResult(
80+
timestamp=datetime.now(),
81+
message_format="test_format",
82+
message_context=None,
83+
)
84+
mock_integration.send_message_mock.return_value = expected_result
85+
86+
result = mapped_integration.send_message(destination, body)
87+
88+
assert result == expected_result
89+
mock_integration.send_message_mock.assert_called_once_with(None, body)
90+
91+
92+
def test_send_message_invalid_destination(
93+
mapped_integration: MappedMessagingIntegration,
94+
) -> None:
95+
destination = "invalid_destination"
96+
body = MessageBody(blocks=[HeaderBlock(text="test message")])
97+
98+
with pytest.raises(MessagingIntegrationError) as exc_info:
99+
mapped_integration.send_message(destination, body)
100+
assert str(exc_info.value) == "Invalid destination: invalid_destination"
101+
102+
103+
@pytest.mark.parametrize(
104+
"integrations_support_reply,expected_support",
105+
[
106+
([True, True], True),
107+
([True, False], False),
108+
([False, True], False),
109+
([False, False], False),
110+
],
111+
)
112+
def test_supports_reply(
113+
integrations_support_reply: List[bool], expected_support: bool
114+
) -> None:
115+
integrations: Dict[str, BaseMessagingIntegration[None, MockMessageContext]] = {
116+
f"dest_{i}": MockMessagingIntegration(supports_reply=supports_reply)
117+
for i, supports_reply in enumerate(integrations_support_reply)
118+
}
119+
mapped_integration = MappedMessagingIntegration(integrations)
120+
121+
result = mapped_integration.supports_reply()
122+
123+
assert result == expected_support
124+
125+
126+
@pytest.mark.parametrize(
127+
"integrations_support_actions,expected_support",
128+
[
129+
([True, True], True),
130+
([True, False], False),
131+
([False, True], False),
132+
([False, False], False),
133+
],
134+
)
135+
def test_supports_actions(
136+
integrations_support_actions: List[bool], expected_support: bool
137+
) -> None:
138+
integrations: Dict[str, BaseMessagingIntegration[None, MockMessageContext]] = {
139+
f"dest_{i}": MockMessagingIntegration(supports_actions=supports_actions)
140+
for i, supports_actions in enumerate(integrations_support_actions)
141+
}
142+
mapped_integration = MappedMessagingIntegration(integrations)
143+
144+
result = mapped_integration.supports_actions()
145+
146+
assert result == expected_support
147+
148+
149+
def test_reply_to_message_success(
150+
mapped_integration: MappedMessagingIntegration,
151+
mock_integration: MockMessagingIntegration,
152+
) -> None:
153+
destination = "test_destination"
154+
message_context = MagicMock()
155+
body = MessageBody(blocks=[HeaderBlock(text="test reply")])
156+
expected_result: MessageSendResult[MockMessageContext] = MessageSendResult(
157+
timestamp=datetime.now(),
158+
message_format="test_format",
159+
message_context=message_context,
160+
)
161+
mock_integration.reply_to_message_mock.return_value = expected_result
162+
163+
result = mapped_integration.reply_to_message(destination, message_context, body)
164+
165+
assert result == expected_result
166+
mock_integration.reply_to_message_mock.assert_called_once_with(
167+
None, message_context, body
168+
)
169+
170+
171+
def test_reply_to_message_invalid_destination(
172+
mapped_integration: MappedMessagingIntegration,
173+
) -> None:
174+
destination = "invalid_destination"
175+
message_context = MagicMock()
176+
body = MessageBody(blocks=[HeaderBlock(text="test reply")])
177+
178+
with pytest.raises(MessagingIntegrationError) as exc_info:
179+
mapped_integration.reply_to_message(destination, message_context, body)
180+
assert str(exc_info.value) == "Invalid destination: invalid_destination"

0 commit comments

Comments
 (0)