From cd85fe5060868f0bcb854089e5143255a63af2be Mon Sep 17 00:00:00 2001 From: miclon Date: Thu, 17 Jul 2025 16:37:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(bark):=20=E6=94=AF=E6=8C=81=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89base=5Furl=E5=B9=B6=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=8F=91=E9=80=81=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加对自定义base_url的支持,允许配置不同的Bark服务器地址 - 将消息发送方式从GET改为POST,支持JSON格式的请求体 - 新增_prepare_payload方法处理消息参数,支持更多可选配置项 - 添加响应状态检查确保消息发送成功 - 添加测试用例验证功能正确性 --- src/use_notify/channels/bark.py | 35 +++++-- tests/test_bark.py | 162 ++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+), 6 deletions(-) create mode 100644 tests/test_bark.py diff --git a/src/use_notify/channels/bark.py b/src/use_notify/channels/bark.py index 64c423a..5f9e347 100644 --- a/src/use_notify/channels/bark.py +++ b/src/use_notify/channels/bark.py @@ -13,20 +13,43 @@ class Bark(BaseChannel): @property def api_url(self): - return f"https://api.day.app/{self.config.token}/{{title}}/{{content}}" + # Check if base_url exists in config, otherwise use default + if self.config.base_url: + base_url = self.config.base_url.rstrip("/") + else: + base_url = "https://api.day.app" + + return f"{base_url}/{self.config.token}" @property def headers(self): - return {"Content-Type": "application/x-www-form-urlencoded"} + return {"Content-Type": "application/json; charset=utf-8"} + + def _prepare_payload(self, content, title=None): + payload = { + "body": content, + } + + if title: + payload["title"] = title + + # Optional parameters from config + for param in ["badge", "sound", "icon", "group", "url"]: + if hasattr(self.config, param) and getattr(self.config, param) is not None: + payload[param] = getattr(self.config, param) + + return payload def send(self, content, title=None): - api_url = self.api_url.format_map({"content": content, "title": title}) + payload = self._prepare_payload(content, title) with httpx.Client() as client: - client.get(api_url, headers=self.headers) + response = client.post(self.api_url, headers=self.headers, json=payload) + response.raise_for_status() logger.debug("`bark` send successfully") async def send_async(self, content, title=None): - api_url = self.api_url.format_map({"content": content, "title": title}) + payload = self._prepare_payload(content, title) async with httpx.AsyncClient() as client: - await client.get(api_url, headers=self.headers) + response = await client.post(self.api_url, headers=self.headers, json=payload) + response.raise_for_status() logger.debug("`bark` send successfully") diff --git a/tests/test_bark.py b/tests/test_bark.py new file mode 100644 index 0000000..886f475 --- /dev/null +++ b/tests/test_bark.py @@ -0,0 +1,162 @@ +import pytest +from unittest.mock import patch, MagicMock + +from use_notify.channels.bark import Bark + + +@pytest.fixture +def bark_config(): + return { + "token": "your_key", + "sound": "minuet", + "icon": "https://day.app/assets/images/avatar.jpg", + "group": "test", + "badge": 1, + "url": "https://mritd.com" + } + + +def test_bark_send(bark_config): + # Create a mock for httpx.Client + mock_client = MagicMock() + mock_response = MagicMock() + mock_client.return_value.__enter__.return_value.post.return_value = mock_response + + # Create Bark instance + bark = Bark(bark_config) + + # Mock the httpx.Client + with patch('httpx.Client', mock_client): + bark.send("Test Bark Server", "Test Title") + + # Verify the request was made correctly + mock_client.return_value.__enter__.return_value.post.assert_called_once() + + # Get the call arguments + call_args = mock_client.return_value.__enter__.return_value.post.call_args + url = call_args[0][0] + kwargs = call_args[1] + + # Verify URL and headers + assert url == "https://api.day.app/your_key" + assert kwargs["headers"]["Content-Type"] == "application/json; charset=utf-8" + + # Verify payload + payload = kwargs["json"] + assert payload["body"] == "Test Bark Server" + assert payload["title"] == "Test Title" + assert payload["badge"] == 1 + assert payload["sound"] == "minuet" + assert payload["icon"] == "https://day.app/assets/images/avatar.jpg" + assert payload["group"] == "test" + assert payload["url"] == "https://mritd.com" + + # Verify response handling + mock_response.raise_for_status.assert_called_once() + + +@pytest.mark.asyncio +async def test_bark_send_async(bark_config): + # Create a mock for httpx.AsyncClient + mock_client = MagicMock() + mock_response = MagicMock() + mock_client.return_value.__aenter__.return_value.post.return_value = mock_response + + # Create Bark instance + bark = Bark(bark_config) + + # Mock the httpx.AsyncClient + with patch('httpx.AsyncClient', mock_client): + await bark.send_async("Test Bark Server", "Test Title") + + # Verify the request was made correctly + mock_client.return_value.__aenter__.return_value.post.assert_called_once() + + # Get the call arguments + call_args = mock_client.return_value.__aenter__.return_value.post.call_args + url = call_args[0][0] + kwargs = call_args[1] + + # Verify URL and headers + assert url == "https://api.day.app/your_key" + assert kwargs["headers"]["Content-Type"] == "application/json; charset=utf-8" + + # Verify payload + payload = kwargs["json"] + assert payload["body"] == "Test Bark Server" + assert payload["title"] == "Test Title" + assert payload["badge"] == 1 + assert payload["sound"] == "minuet" + assert payload["icon"] == "https://day.app/assets/images/avatar.jpg" + assert payload["group"] == "test" + assert payload["url"] == "https://mritd.com" + + # Verify response handling + mock_response.raise_for_status.assert_called_once() + + +@pytest.fixture +def custom_bark_config(): + return { + "token": "your_key", + "base_url": "https://bark.example.com", + "sound": "minuet", + "icon": "https://day.app/assets/images/avatar.jpg", + "group": "test", + "badge": 1, + "url": "https://mritd.com" + } + + +def test_bark_custom_url(custom_bark_config): + # Create a mock for httpx.Client + mock_client = MagicMock() + mock_response = MagicMock() + mock_client.return_value.__enter__.return_value.post.return_value = mock_response + + # Create Bark instance with custom base URL + bark = Bark(custom_bark_config) + + # Mock the httpx.Client + with patch('httpx.Client', mock_client): + bark.send("Test Bark Server", "Test Title") + + # Verify the request was made correctly + mock_client.return_value.__enter__.return_value.post.assert_called_once() + + # Get the call arguments + call_args = mock_client.return_value.__enter__.return_value.post.call_args + url = call_args[0][0] + + # Verify custom URL is used + assert url == "https://bark.example.com/your_key" + + # Verify response handling + mock_response.raise_for_status.assert_called_once() + + +def test_bark_url_with_trailing_slash(): + # Config with trailing slash in base_url + config = { + "token": "your_key", + "base_url": "https://bark.example.com/" + } + + # Create a mock for httpx.Client + mock_client = MagicMock() + mock_response = MagicMock() + mock_client.return_value.__enter__.return_value.post.return_value = mock_response + + # Create Bark instance + bark = Bark(config) + + # Mock the httpx.Client + with patch('httpx.Client', mock_client): + bark.send("Test Content") + + # Get the call arguments + call_args = mock_client.return_value.__enter__.return_value.post.call_args + url = call_args[0][0] + + # Verify URL doesn't have double slashes + assert url == "https://bark.example.com/your_key" \ No newline at end of file