mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 13:26:48 +00:00
267 lines
8.2 KiB
Python
267 lines
8.2 KiB
Python
"""Tests for SendBlue iMessage/SMS channel.
|
|
|
|
Covers: init, env-var fallback, connect, send (mocked httpx), webhook
|
|
handler, event emission, and registry registration.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from openjarvis.channels._stubs import ChannelStatus
|
|
from openjarvis.channels.sendblue import SendBlueChannel
|
|
from openjarvis.core.events import EventBus, EventType
|
|
from openjarvis.core.registry import ChannelRegistry
|
|
from tests.channels.channel_test_helpers import make_common_channel_tests
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _register_sendblue():
|
|
if not ChannelRegistry.contains("sendblue"):
|
|
ChannelRegistry.register_value("sendblue", SendBlueChannel)
|
|
|
|
|
|
TestCommonChannel = make_common_channel_tests(
|
|
SendBlueChannel,
|
|
"sendblue",
|
|
constructor_kwargs={
|
|
"api_key_id": "test_key",
|
|
"api_secret_key": "test_secret",
|
|
"from_number": "+15551234567",
|
|
},
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_channel(**overrides):
|
|
defaults = {
|
|
"api_key_id": "test_key",
|
|
"api_secret_key": "test_secret",
|
|
"from_number": "+15551234567",
|
|
}
|
|
defaults.update(overrides)
|
|
return SendBlueChannel(**defaults)
|
|
|
|
|
|
def _mock_httpx_post(status=200, body=None):
|
|
"""Return a mock httpx response."""
|
|
resp = MagicMock()
|
|
resp.status_code = status
|
|
resp.text = json.dumps(body or {})
|
|
resp.json.return_value = body or {}
|
|
return resp
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Init / env vars
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestInit:
|
|
def test_from_params(self):
|
|
ch = _make_channel()
|
|
assert ch.channel_id == "sendblue"
|
|
assert ch._api_key_id == "test_key"
|
|
assert ch._api_secret_key == "test_secret"
|
|
assert ch._from_number == "+15551234567"
|
|
assert ch.status() == ChannelStatus.DISCONNECTED
|
|
|
|
def test_from_env_vars(self, monkeypatch):
|
|
monkeypatch.setenv("SENDBLUE_API_KEY_ID", "env_key")
|
|
monkeypatch.setenv("SENDBLUE_API_SECRET_KEY", "env_secret")
|
|
monkeypatch.setenv("SENDBLUE_FROM_NUMBER", "+19998887777")
|
|
|
|
ch = SendBlueChannel()
|
|
assert ch._api_key_id == "env_key"
|
|
assert ch._api_secret_key == "env_secret"
|
|
assert ch._from_number == "+19998887777"
|
|
|
|
def test_no_credentials(self):
|
|
ch = SendBlueChannel()
|
|
ch.connect()
|
|
assert ch.status() == ChannelStatus.ERROR
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Connect / disconnect
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestConnect:
|
|
def test_connect_with_creds(self):
|
|
ch = _make_channel()
|
|
ch.connect()
|
|
assert ch.status() == ChannelStatus.CONNECTED
|
|
|
|
def test_disconnect(self):
|
|
ch = _make_channel()
|
|
ch.connect()
|
|
ch.disconnect()
|
|
assert ch.status() == ChannelStatus.DISCONNECTED
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Send
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSend:
|
|
def test_send_success(self):
|
|
ch = _make_channel()
|
|
ch.connect()
|
|
with patch("httpx.post", return_value=_mock_httpx_post(200)):
|
|
result = ch.send("+19998887777", "Hello!")
|
|
assert result is True
|
|
|
|
def test_send_includes_from_number(self):
|
|
ch = _make_channel(from_number="+15559876543")
|
|
ch.connect()
|
|
with patch("httpx.post", return_value=_mock_httpx_post(200)) as mock:
|
|
ch.send("+19998887777", "Hi!")
|
|
call_kwargs = mock.call_args
|
|
payload = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json")
|
|
assert payload["number"] == "+19998887777"
|
|
assert payload["content"] == "Hi!"
|
|
assert payload["from_number"] == "+15559876543"
|
|
|
|
def test_send_api_error_returns_false(self):
|
|
ch = _make_channel()
|
|
ch.connect()
|
|
with patch("httpx.post", return_value=_mock_httpx_post(401)):
|
|
result = ch.send("+19998887777", "Hello!")
|
|
assert result is False
|
|
|
|
def test_send_network_error_returns_false(self):
|
|
ch = _make_channel()
|
|
ch.connect()
|
|
with patch("httpx.post", side_effect=Exception("Network error")):
|
|
result = ch.send("+19998887777", "Hello!")
|
|
assert result is False
|
|
|
|
def test_send_no_credentials_returns_false(self):
|
|
ch = SendBlueChannel()
|
|
result = ch.send("+19998887777", "Hello!")
|
|
assert result is False
|
|
|
|
def test_send_publishes_event(self):
|
|
bus = EventBus(record_history=True)
|
|
ch = _make_channel(bus=bus)
|
|
ch.connect()
|
|
with patch("httpx.post", return_value=_mock_httpx_post(200)):
|
|
ch.send("+19998887777", "Hello!")
|
|
event_types = [e.event_type for e in bus.history]
|
|
assert EventType.CHANNEL_MESSAGE_SENT in event_types
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Webhook handler
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestWebhookHandler:
|
|
def test_incoming_message_triggers_handlers(self):
|
|
ch = _make_channel()
|
|
received = []
|
|
ch.on_message(lambda msg: received.append(msg))
|
|
|
|
ch.handle_webhook(
|
|
{
|
|
"from_number": "+19127130720",
|
|
"to_number": "+15551234567",
|
|
"content": "Hello Jarvis",
|
|
"message_handle": "msg-001",
|
|
"is_outbound": False,
|
|
"status": "RECEIVED",
|
|
"service": "iMessage",
|
|
}
|
|
)
|
|
|
|
assert len(received) == 1
|
|
assert received[0].sender == "+19127130720"
|
|
assert received[0].content == "Hello Jarvis"
|
|
assert received[0].channel == "sendblue"
|
|
|
|
def test_outbound_messages_ignored(self):
|
|
ch = _make_channel()
|
|
received = []
|
|
ch.on_message(lambda msg: received.append(msg))
|
|
|
|
ch.handle_webhook(
|
|
{
|
|
"from_number": "+15551234567",
|
|
"content": "Outbound message",
|
|
"is_outbound": True,
|
|
}
|
|
)
|
|
|
|
assert len(received) == 0
|
|
|
|
def test_empty_content_ignored(self):
|
|
ch = _make_channel()
|
|
received = []
|
|
ch.on_message(lambda msg: received.append(msg))
|
|
|
|
ch.handle_webhook(
|
|
{
|
|
"from_number": "+19127130720",
|
|
"content": "",
|
|
"is_outbound": False,
|
|
}
|
|
)
|
|
|
|
assert len(received) == 0
|
|
|
|
def test_incoming_publishes_event(self):
|
|
bus = EventBus(record_history=True)
|
|
ch = _make_channel(bus=bus)
|
|
|
|
ch.handle_webhook(
|
|
{
|
|
"from_number": "+19127130720",
|
|
"content": "Test",
|
|
"message_handle": "msg-002",
|
|
"is_outbound": False,
|
|
"service": "iMessage",
|
|
}
|
|
)
|
|
|
|
event_types = [e.event_type for e in bus.history]
|
|
assert EventType.CHANNEL_MESSAGE_RECEIVED in event_types
|
|
event = [
|
|
e for e in bus.history if e.event_type == EventType.CHANNEL_MESSAGE_RECEIVED
|
|
][0]
|
|
assert event.data["sender"] == "+19127130720"
|
|
assert event.data["service"] == "iMessage"
|
|
|
|
def test_handler_exception_does_not_crash(self):
|
|
ch = _make_channel()
|
|
ch.on_message(lambda msg: 1 / 0) # Will raise ZeroDivisionError
|
|
|
|
# Should not raise
|
|
ch.handle_webhook(
|
|
{
|
|
"from_number": "+19127130720",
|
|
"content": "Test",
|
|
"message_handle": "msg-003",
|
|
"is_outbound": False,
|
|
}
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Properties
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestProperties:
|
|
def test_from_number(self):
|
|
ch = _make_channel(from_number="+15559876543")
|
|
assert ch.from_number == "+15559876543"
|