Skip to content

Commit 75aa4ed

Browse files
feat: add support for notification tokens in PushNotificationSender (#266)
# Description Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [x] Follow the [`CONTRIBUTING` Guide](https://github.com/a2aproject/a2a-python/blob/main/CONTRIBUTING.md). - [x] Make your Pull Request title in the <https://www.conventionalcommits.org/> specification. - Important Prefixes for [release-please](https://github.com/googleapis/release-please): - `fix:` which represents bug fixes, and correlates to a [SemVer](https://semver.org/) patch. - `feat:` represents a new feature, and correlates to a SemVer minor. - `feat!:`, or `fix!:`, `refactor!:`, etc., which represent a breaking change (indicated by the `!`) and will result in a SemVer major. - [x] Ensure the tests and linter pass (Run `nox -s format` from the repository root to format) - [ ] Appropriate docs were updated (if necessary) Fixes #284 --------- Co-authored-by: kthota-g <kcthota@google.com>
1 parent 9d6cb68 commit 75aa4ed

File tree

4 files changed

+68
-5
lines changed

4 files changed

+68
-5
lines changed

src/a2a/server/tasks/base_push_notification_sender.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,13 @@ async def _dispatch_notification(
5252
) -> bool:
5353
url = push_info.url
5454
try:
55+
headers = None
56+
if push_info.token:
57+
headers = {'X-A2A-Notification-Token': push_info.token}
5558
response = await self._client.post(
56-
url, json=task.model_dump(mode='json', exclude_none=True)
59+
url,
60+
json=task.model_dump(mode='json', exclude_none=True),
61+
headers=headers
5762
)
5863
response.raise_for_status()
5964
logger.info(

tests/server/request_handlers/test_jsonrpc_handler.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,7 @@ async def streaming_coro():
585585
'kind': 'task',
586586
'status': {'state': 'submitted'},
587587
},
588+
headers=None
588589
),
589590
call(
590591
'http://example.com',
@@ -605,6 +606,7 @@ async def streaming_coro():
605606
'kind': 'task',
606607
'status': {'state': 'submitted'},
607608
},
609+
headers=None
608610
),
609611
call(
610612
'http://example.com',
@@ -625,6 +627,7 @@ async def streaming_coro():
625627
'kind': 'task',
626628
'status': {'state': 'completed'},
627629
},
630+
headers=None
628631
),
629632
]
630633
mock_httpx_client.post.assert_has_calls(calls)

tests/server/tasks/test_inmemory_push_notifications.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ def create_sample_task(task_id='task123', status_state=TaskState.completed):
2626

2727

2828
def create_sample_push_config(
29-
url='http://example.com/callback', config_id='cfg1'
29+
url='http://example.com/callback', config_id='cfg1', token=None
3030
):
31-
return PushNotificationConfig(id=config_id, url=url)
31+
return PushNotificationConfig(id=config_id, url=url, token=token)
3232

3333

3434
class TestInMemoryPushNotifier(unittest.IsolatedAsyncioTestCase):
@@ -158,6 +158,35 @@ async def test_send_notification_success(self):
158158
) # auth is not passed by current implementation
159159
mock_response.raise_for_status.assert_called_once()
160160

161+
async def test_send_notification_with_token_success(self):
162+
task_id = 'task_send_success'
163+
task_data = create_sample_task(task_id=task_id)
164+
config = create_sample_push_config(url='http://notify.me/here', token='unique_token')
165+
await self.config_store.set_info(task_id, config)
166+
167+
# Mock the post call to simulate success
168+
mock_response = AsyncMock(spec=httpx.Response)
169+
mock_response.status_code = 200
170+
self.mock_httpx_client.post.return_value = mock_response
171+
172+
await self.notifier.send_notification(task_data) # Pass only task_data
173+
174+
self.mock_httpx_client.post.assert_awaited_once()
175+
called_args, called_kwargs = self.mock_httpx_client.post.call_args
176+
self.assertEqual(called_args[0], config.url)
177+
self.assertEqual(
178+
called_kwargs['json'],
179+
task_data.model_dump(mode='json', exclude_none=True),
180+
)
181+
self.assertEqual(
182+
called_kwargs['headers'],
183+
{"X-A2A-Notification-Token": "unique_token"},
184+
)
185+
self.assertNotIn(
186+
'auth', called_kwargs
187+
) # auth is not passed by current implementation
188+
mock_response.raise_for_status.assert_called_once()
189+
161190
async def test_send_notification_no_config(self):
162191
task_id = 'task_send_no_config'
163192
task_data = create_sample_task(task_id=task_id)

tests/server/tasks/test_push_notification_sender.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ def create_sample_task(task_id='task123', status_state=TaskState.completed):
2525

2626

2727
def create_sample_push_config(
28-
url='http://example.com/callback', config_id='cfg1'
28+
url='http://example.com/callback', config_id='cfg1', token=None
2929
):
30-
return PushNotificationConfig(id=config_id, url=url)
30+
return PushNotificationConfig(id=config_id, url=url, token=token)
3131

3232

3333
class TestBasePushNotificationSender(unittest.IsolatedAsyncioTestCase):
@@ -61,6 +61,29 @@ async def test_send_notification_success(self):
6161
self.mock_httpx_client.post.assert_awaited_once_with(
6262
config.url,
6363
json=task_data.model_dump(mode='json', exclude_none=True),
64+
headers=None
65+
)
66+
mock_response.raise_for_status.assert_called_once()
67+
68+
async def test_send_notification_with_token_success(self):
69+
task_id = 'task_send_success'
70+
task_data = create_sample_task(task_id=task_id)
71+
config = create_sample_push_config(url='http://notify.me/here', token='unique_token')
72+
self.mock_config_store.get_info.return_value = [config]
73+
74+
mock_response = AsyncMock(spec=httpx.Response)
75+
mock_response.status_code = 200
76+
self.mock_httpx_client.post.return_value = mock_response
77+
78+
await self.sender.send_notification(task_data)
79+
80+
self.mock_config_store.get_info.assert_awaited_once_with
81+
82+
# assert httpx_client post method got invoked with right parameters
83+
self.mock_httpx_client.post.assert_awaited_once_with(
84+
config.url,
85+
json=task_data.model_dump(mode='json', exclude_none=True),
86+
headers={'X-A2A-Notification-Token': 'unique_token'}
6487
)
6588
mock_response.raise_for_status.assert_called_once()
6689

@@ -97,6 +120,7 @@ async def test_send_notification_http_status_error(
97120
self.mock_httpx_client.post.assert_awaited_once_with(
98121
config.url,
99122
json=task_data.model_dump(mode='json', exclude_none=True),
123+
headers=None
100124
)
101125
mock_logger.error.assert_called_once()
102126

@@ -124,10 +148,12 @@ async def test_send_notification_multiple_configs(self):
124148
self.mock_httpx_client.post.assert_any_call(
125149
config1.url,
126150
json=task_data.model_dump(mode='json', exclude_none=True),
151+
headers=None
127152
)
128153
# Check calls for config2
129154
self.mock_httpx_client.post.assert_any_call(
130155
config2.url,
131156
json=task_data.model_dump(mode='json', exclude_none=True),
157+
headers=None
132158
)
133159
mock_response.raise_for_status.call_count = 2

0 commit comments

Comments
 (0)