|
| 1 | +"""Tests for MessagesResource.""" |
| 2 | + |
| 3 | +import pytest |
| 4 | +from unittest.mock import MagicMock |
| 5 | + |
| 6 | +from cueapi.resources.messages import MessagesResource |
| 7 | + |
| 8 | + |
| 9 | +class TestSend: |
| 10 | + def test_minimal_body_and_from_header(self): |
| 11 | + # Pin: --from goes in X-Cueapi-From-Agent HEADER, NOT in body. |
| 12 | + # The server's MessageCreate is extra="forbid" and would 400 on |
| 13 | + # `{"from": "..."}` in the body, but we want this caught at unit |
| 14 | + # test time, not silently at integration. |
| 15 | + mock_client = MagicMock() |
| 16 | + mock_client._post.return_value = { |
| 17 | + "id": "msg_x", "delivery_state": "queued", "thread_id": "thr_x", |
| 18 | + } |
| 19 | + r = MessagesResource(mock_client) |
| 20 | + |
| 21 | + r.send(from_agent="sender@x", to="recipient@y", body="hi") |
| 22 | + |
| 23 | + mock_client._post.assert_called_once_with( |
| 24 | + "/v1/messages", |
| 25 | + json={"to": "recipient@y", "body": "hi"}, |
| 26 | + headers={"X-Cueapi-From-Agent": "sender@x"}, |
| 27 | + ) |
| 28 | + |
| 29 | + def test_with_all_optionals(self): |
| 30 | + mock_client = MagicMock() |
| 31 | + mock_client._post.return_value = {"id": "msg_x", "delivery_state": "queued"} |
| 32 | + r = MessagesResource(mock_client) |
| 33 | + |
| 34 | + r.send( |
| 35 | + from_agent="sender@x", |
| 36 | + to="recipient@y", |
| 37 | + body="hi", |
| 38 | + subject="re: hello", |
| 39 | + reply_to="msg_abcdef123456", |
| 40 | + priority=5, |
| 41 | + expects_reply=True, |
| 42 | + reply_to_agent="alt@x", |
| 43 | + metadata={"k": "v"}, |
| 44 | + idempotency_key="idemp-key-1", |
| 45 | + ) |
| 46 | + |
| 47 | + call = mock_client._post.call_args |
| 48 | + assert call.args == ("/v1/messages",) |
| 49 | + assert call.kwargs["json"] == { |
| 50 | + "to": "recipient@y", |
| 51 | + "body": "hi", |
| 52 | + "subject": "re: hello", |
| 53 | + "reply_to": "msg_abcdef123456", |
| 54 | + "priority": 5, |
| 55 | + "expects_reply": True, |
| 56 | + "reply_to_agent": "alt@x", |
| 57 | + "metadata": {"k": "v"}, |
| 58 | + } |
| 59 | + assert call.kwargs["headers"] == { |
| 60 | + "X-Cueapi-From-Agent": "sender@x", |
| 61 | + "Idempotency-Key": "idemp-key-1", |
| 62 | + } |
| 63 | + |
| 64 | + def test_omits_expects_reply_when_default(self): |
| 65 | + # Pin: default False MUST NOT appear in body. Server's Pydantic |
| 66 | + # default is False; sending `expects_reply: false` is no-op + adds |
| 67 | + # noise. Refactor that always-sends would slip past the typed |
| 68 | + # server schema but be caught here. |
| 69 | + mock_client = MagicMock() |
| 70 | + mock_client._post.return_value = {"id": "msg_x"} |
| 71 | + r = MessagesResource(mock_client) |
| 72 | + |
| 73 | + r.send(from_agent="x", to="y", body="hi") |
| 74 | + |
| 75 | + body = mock_client._post.call_args.kwargs["json"] |
| 76 | + assert "expects_reply" not in body |
| 77 | + |
| 78 | + def test_idempotency_key_too_long_raises_client_side(self): |
| 79 | + mock_client = MagicMock() |
| 80 | + r = MessagesResource(mock_client) |
| 81 | + |
| 82 | + with pytest.raises(ValueError, match="255"): |
| 83 | + r.send( |
| 84 | + from_agent="x", to="y", body="hi", |
| 85 | + idempotency_key="x" * 256, |
| 86 | + ) |
| 87 | + # Crucially: must NOT have hit the wire. |
| 88 | + mock_client._post.assert_not_called() |
| 89 | + |
| 90 | + def test_omits_idempotency_key_header_when_unset(self): |
| 91 | + # Headers should ONLY contain X-Cueapi-From-Agent when no |
| 92 | + # idempotency_key is passed. Pin so a refactor can't silently |
| 93 | + # start adding `Idempotency-Key: None` (httpx would coerce). |
| 94 | + mock_client = MagicMock() |
| 95 | + mock_client._post.return_value = {"id": "msg_x"} |
| 96 | + r = MessagesResource(mock_client) |
| 97 | + |
| 98 | + r.send(from_agent="x", to="y", body="hi") |
| 99 | + |
| 100 | + headers = mock_client._post.call_args.kwargs["headers"] |
| 101 | + assert headers == {"X-Cueapi-From-Agent": "x"} |
| 102 | + assert "Idempotency-Key" not in headers |
| 103 | + |
| 104 | + |
| 105 | +class TestGet: |
| 106 | + def test_get(self): |
| 107 | + mock_client = MagicMock() |
| 108 | + mock_client._get.return_value = {"id": "msg_x"} |
| 109 | + r = MessagesResource(mock_client) |
| 110 | + |
| 111 | + r.get("msg_x") |
| 112 | + |
| 113 | + mock_client._get.assert_called_once_with("/v1/messages/msg_x") |
| 114 | + |
| 115 | + |
| 116 | +class TestMarkRead: |
| 117 | + def test_mark_read(self): |
| 118 | + mock_client = MagicMock() |
| 119 | + mock_client._post.return_value = {"delivery_state": "read"} |
| 120 | + r = MessagesResource(mock_client) |
| 121 | + |
| 122 | + r.mark_read("msg_x") |
| 123 | + |
| 124 | + mock_client._post.assert_called_once_with( |
| 125 | + "/v1/messages/msg_x/read", json={}, |
| 126 | + ) |
| 127 | + |
| 128 | + |
| 129 | +class TestAck: |
| 130 | + def test_ack(self): |
| 131 | + mock_client = MagicMock() |
| 132 | + mock_client._post.return_value = {"delivery_state": "acked"} |
| 133 | + r = MessagesResource(mock_client) |
| 134 | + |
| 135 | + r.ack("msg_x") |
| 136 | + |
| 137 | + mock_client._post.assert_called_once_with( |
| 138 | + "/v1/messages/msg_x/ack", json={}, |
| 139 | + ) |
0 commit comments