Skip to content

Commit 39f68f0

Browse files
authored
chore: Backport Python InMemoryPushNotificationConfigStore tests (#295)
1 parent 4eb55b2 commit 39f68f0

File tree

1 file changed

+333
-0
lines changed

1 file changed

+333
-0
lines changed
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
package io.a2a.server.tasks;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotNull;
5+
import static org.junit.jupiter.api.Assertions.assertNull;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
import static org.mockito.ArgumentMatchers.any;
8+
import static org.mockito.Mockito.never;
9+
import static org.mockito.Mockito.verify;
10+
import static org.mockito.Mockito.when;
11+
12+
import org.mockito.ArgumentCaptor;
13+
14+
import java.util.List;
15+
16+
import org.junit.jupiter.api.BeforeEach;
17+
import org.junit.jupiter.api.Disabled;
18+
import org.junit.jupiter.api.Test;
19+
import org.mockito.Mock;
20+
import org.mockito.MockitoAnnotations;
21+
22+
import io.a2a.client.http.A2AHttpClient;
23+
import io.a2a.client.http.A2AHttpResponse;
24+
import io.a2a.spec.PushNotificationConfig;
25+
import io.a2a.spec.Task;
26+
import io.a2a.spec.TaskState;
27+
import io.a2a.spec.TaskStatus;
28+
29+
class InMemoryPushNotificationConfigStoreTest {
30+
31+
private InMemoryPushNotificationConfigStore configStore;
32+
private BasePushNotificationSender notificationSender;
33+
34+
@Mock
35+
private A2AHttpClient mockHttpClient;
36+
37+
@Mock
38+
private A2AHttpClient.PostBuilder mockPostBuilder;
39+
40+
@Mock
41+
private A2AHttpResponse mockHttpResponse;
42+
43+
@BeforeEach
44+
public void setUp() {
45+
MockitoAnnotations.openMocks(this);
46+
configStore = new InMemoryPushNotificationConfigStore();
47+
notificationSender = new BasePushNotificationSender(configStore, mockHttpClient);
48+
}
49+
50+
private Task createSampleTask(String taskId, TaskState state) {
51+
return new Task.Builder()
52+
.id(taskId)
53+
.contextId("ctx456")
54+
.status(new TaskStatus(state))
55+
.build();
56+
}
57+
58+
private PushNotificationConfig createSamplePushConfig(String url, String configId, String token) {
59+
return new PushNotificationConfig.Builder()
60+
.url(url)
61+
.id(configId)
62+
.token(token)
63+
.build();
64+
}
65+
66+
@Test
67+
public void testSetInfoAddsNewConfig() {
68+
String taskId = "task_new";
69+
PushNotificationConfig config = createSamplePushConfig("http://new.url/callback", "cfg1", null);
70+
71+
PushNotificationConfig result = configStore.setInfo(taskId, config);
72+
73+
assertNotNull(result);
74+
assertEquals(config.url(), result.url());
75+
assertEquals(config.id(), result.id());
76+
77+
List<PushNotificationConfig> configs = configStore.getInfo(taskId);
78+
assertNotNull(configs);
79+
assertEquals(1, configs.size());
80+
assertEquals(config.url(), configs.get(0).url());
81+
assertEquals(config.id(), configs.get(0).id());
82+
}
83+
84+
@Test
85+
public void testSetInfoAppendsToExistingConfig() {
86+
String taskId = "task_update";
87+
PushNotificationConfig initialConfig = createSamplePushConfig(
88+
"http://initial.url/callback", "cfg_initial", null);
89+
configStore.setInfo(taskId, initialConfig);
90+
91+
PushNotificationConfig updatedConfig = createSamplePushConfig(
92+
"http://updated.url/callback", "cfg_updated", null);
93+
configStore.setInfo(taskId, updatedConfig);
94+
95+
List<PushNotificationConfig> configs = configStore.getInfo(taskId);
96+
assertNotNull(configs);
97+
assertEquals(2, configs.size());
98+
99+
// Find the configs by ID since order might vary
100+
PushNotificationConfig foundInitial = configs.stream()
101+
.filter(c -> "cfg_initial".equals(c.id()))
102+
.findFirst()
103+
.orElse(null);
104+
PushNotificationConfig foundUpdated = configs.stream()
105+
.filter(c -> "cfg_updated".equals(c.id()))
106+
.findFirst()
107+
.orElse(null);
108+
109+
assertNotNull(foundInitial);
110+
assertNotNull(foundUpdated);
111+
assertEquals(initialConfig.url(), foundInitial.url());
112+
assertEquals(updatedConfig.url(), foundUpdated.url());
113+
}
114+
115+
@Test
116+
public void testSetInfoWithoutConfigId() {
117+
String taskId = "task1";
118+
PushNotificationConfig initialConfig = new PushNotificationConfig.Builder()
119+
.url("http://initial.url/callback")
120+
.build(); // No ID set
121+
122+
PushNotificationConfig result = configStore.setInfo(taskId, initialConfig);
123+
assertEquals(taskId, result.id(), "Config ID should default to taskId when not provided");
124+
125+
List<PushNotificationConfig> configs = configStore.getInfo(taskId);
126+
assertEquals(1, configs.size());
127+
assertEquals(taskId, configs.get(0).id());
128+
129+
PushNotificationConfig updatedConfig = new PushNotificationConfig.Builder()
130+
.url("http://initial.url/callback_new")
131+
.build(); // No ID set
132+
133+
PushNotificationConfig updatedResult = configStore.setInfo(taskId, updatedConfig);
134+
assertEquals(taskId, updatedResult.id());
135+
136+
configs = configStore.getInfo(taskId);
137+
assertEquals(1, configs.size(), "Should replace existing config with same ID rather than adding new one");
138+
assertEquals(updatedConfig.url(), configs.get(0).url());
139+
}
140+
141+
@Test
142+
public void testGetInfoExistingConfig() {
143+
String taskId = "task_get_exist";
144+
PushNotificationConfig config = createSamplePushConfig("http://get.this/callback", "cfg1", null);
145+
configStore.setInfo(taskId, config);
146+
147+
List<PushNotificationConfig> retrievedConfigs = configStore.getInfo(taskId);
148+
assertNotNull(retrievedConfigs);
149+
assertEquals(1, retrievedConfigs.size());
150+
assertEquals(config.url(), retrievedConfigs.get(0).url());
151+
assertEquals(config.id(), retrievedConfigs.get(0).id());
152+
}
153+
154+
@Test
155+
public void testGetInfoNonExistentConfig() {
156+
String taskId = "task_get_non_exist";
157+
List<PushNotificationConfig> retrievedConfigs = configStore.getInfo(taskId);
158+
assertNull(retrievedConfigs, "Should return null for non-existent task ID");
159+
}
160+
161+
@Test
162+
public void testDeleteInfoExistingConfig() {
163+
String taskId = "task_delete_exist";
164+
PushNotificationConfig config = createSamplePushConfig("http://delete.this/callback", "cfg1", null);
165+
configStore.setInfo(taskId, config);
166+
167+
List<PushNotificationConfig> configs = configStore.getInfo(taskId);
168+
assertNotNull(configs);
169+
assertEquals(1, configs.size());
170+
171+
configStore.deleteInfo(taskId, config.id());
172+
173+
List<PushNotificationConfig> configsAfterDelete = configStore.getInfo(taskId);
174+
assertNull(configsAfterDelete, "Should return null when no configs remain after deletion");
175+
}
176+
177+
@Test
178+
public void testDeleteInfoNonExistentConfig() {
179+
String taskId = "task_delete_non_exist";
180+
// Should not throw an error
181+
configStore.deleteInfo(taskId, "non_existent_id");
182+
183+
List<PushNotificationConfig> configs = configStore.getInfo(taskId);
184+
assertNull(configs, "Should still return null for non-existent task ID");
185+
}
186+
187+
@Test
188+
public void testDeleteInfoWithNullConfigId() {
189+
String taskId = "task_delete_null_config";
190+
PushNotificationConfig config = new PushNotificationConfig.Builder()
191+
.url("http://delete.this/callback")
192+
.build(); // No ID set, will use taskId
193+
configStore.setInfo(taskId, config);
194+
195+
// Delete with null configId should use taskId
196+
configStore.deleteInfo(taskId, null);
197+
198+
List<PushNotificationConfig> configs = configStore.getInfo(taskId);
199+
assertNull(configs, "Should return null after deletion when using taskId as configId");
200+
}
201+
202+
@Test
203+
public void testSendNotificationSuccess() throws Exception {
204+
String taskId = "task_send_success";
205+
Task task = createSampleTask(taskId, TaskState.COMPLETED);
206+
PushNotificationConfig config = createSamplePushConfig("http://notify.me/here", "cfg1", null);
207+
configStore.setInfo(taskId, config);
208+
209+
// Mock successful HTTP response
210+
when(mockHttpClient.createPost()).thenReturn(mockPostBuilder);
211+
when(mockPostBuilder.url(any(String.class))).thenReturn(mockPostBuilder);
212+
when(mockPostBuilder.body(any(String.class))).thenReturn(mockPostBuilder);
213+
when(mockPostBuilder.post()).thenReturn(mockHttpResponse);
214+
when(mockHttpResponse.success()).thenReturn(true);
215+
216+
notificationSender.sendNotification(task);
217+
218+
// Verify HTTP client was called
219+
ArgumentCaptor<String> bodyCaptor = ArgumentCaptor.forClass(String.class);
220+
verify(mockHttpClient).createPost();
221+
verify(mockPostBuilder).url(config.url());
222+
verify(mockPostBuilder).body(bodyCaptor.capture());
223+
verify(mockPostBuilder).post();
224+
225+
// Verify the request body contains the task data
226+
String sentBody = bodyCaptor.getValue();
227+
assertTrue(sentBody.contains(task.getId()));
228+
assertTrue(sentBody.contains(task.getStatus().state().asString()));
229+
}
230+
231+
@Test
232+
@Disabled("Token authentication is not yet implemented in BasePushNotificationSender (TODO auth)")
233+
public void testSendNotificationWithToken() throws Exception {
234+
String taskId = "task_send_with_token";
235+
Task task = createSampleTask(taskId, TaskState.COMPLETED);
236+
PushNotificationConfig config = createSamplePushConfig("http://notify.me/here", "cfg1", "unique_token");
237+
configStore.setInfo(taskId, config);
238+
239+
// Mock successful HTTP response
240+
when(mockHttpClient.createPost()).thenReturn(mockPostBuilder);
241+
when(mockPostBuilder.url(any(String.class))).thenReturn(mockPostBuilder);
242+
when(mockPostBuilder.body(any(String.class))).thenReturn(mockPostBuilder);
243+
when(mockPostBuilder.post()).thenReturn(mockHttpResponse);
244+
when(mockHttpResponse.success()).thenReturn(true);
245+
246+
notificationSender.sendNotification(task);
247+
248+
// TODO: Once token authentication is implemented, verify that:
249+
// 1. The token is included in request headers (e.g., X-A2A-Notification-Token)
250+
// 2. The HTTP client is called with proper authentication
251+
// 3. The token from the config is actually used
252+
253+
// For now, just verify basic HTTP client interaction
254+
ArgumentCaptor<String> bodyCaptor = ArgumentCaptor.forClass(String.class);
255+
verify(mockHttpClient).createPost();
256+
verify(mockPostBuilder).url(config.url());
257+
verify(mockPostBuilder).body(bodyCaptor.capture());
258+
verify(mockPostBuilder).post();
259+
260+
// Verify the request body contains the task data
261+
String sentBody = bodyCaptor.getValue();
262+
assertTrue(sentBody.contains(task.getId()));
263+
assertTrue(sentBody.contains(task.getStatus().state().asString()));
264+
}
265+
266+
@Test
267+
public void testSendNotificationNoConfig() throws Exception {
268+
String taskId = "task_send_no_config";
269+
Task task = createSampleTask(taskId, TaskState.COMPLETED);
270+
271+
notificationSender.sendNotification(task);
272+
273+
// Verify HTTP client was never called
274+
verify(mockHttpClient, never()).createPost();
275+
}
276+
277+
@Test
278+
public void testMultipleConfigsForSameTask() {
279+
String taskId = "task_multiple";
280+
PushNotificationConfig config1 = createSamplePushConfig("http://url1.com/callback", "cfg1", null);
281+
PushNotificationConfig config2 = createSamplePushConfig("http://url2.com/callback", "cfg2", null);
282+
283+
configStore.setInfo(taskId, config1);
284+
configStore.setInfo(taskId, config2);
285+
286+
List<PushNotificationConfig> configs = configStore.getInfo(taskId);
287+
assertNotNull(configs);
288+
assertEquals(2, configs.size());
289+
290+
// Verify both configs are present
291+
assertTrue(configs.stream().anyMatch(c -> "cfg1".equals(c.id())));
292+
assertTrue(configs.stream().anyMatch(c -> "cfg2".equals(c.id())));
293+
}
294+
295+
@Test
296+
public void testDeleteSpecificConfigFromMultiple() {
297+
String taskId = "task_delete_specific";
298+
PushNotificationConfig config1 = createSamplePushConfig("http://url1.com/callback", "cfg1", null);
299+
PushNotificationConfig config2 = createSamplePushConfig("http://url2.com/callback", "cfg2", null);
300+
301+
configStore.setInfo(taskId, config1);
302+
configStore.setInfo(taskId, config2);
303+
304+
// Delete only config1
305+
configStore.deleteInfo(taskId, "cfg1");
306+
307+
List<PushNotificationConfig> configs = configStore.getInfo(taskId);
308+
assertNotNull(configs);
309+
assertEquals(1, configs.size());
310+
assertEquals("cfg2", configs.get(0).id());
311+
}
312+
313+
@Test
314+
public void testConfigStoreIntegration() {
315+
String taskId = "integration_test";
316+
PushNotificationConfig config = createSamplePushConfig("http://example.com", "test_id", "test_token");
317+
318+
// Test that we can store and retrieve configurations
319+
PushNotificationConfig storedConfig = configStore.setInfo(taskId, config);
320+
assertEquals(config.url(), storedConfig.url());
321+
assertEquals(config.token(), storedConfig.token());
322+
323+
List<PushNotificationConfig> retrievedConfigs = configStore.getInfo(taskId);
324+
assertEquals(1, retrievedConfigs.size());
325+
assertEquals(config.url(), retrievedConfigs.get(0).url());
326+
327+
// Test deletion
328+
configStore.deleteInfo(taskId, storedConfig.id());
329+
List<PushNotificationConfig> afterDeletion = configStore.getInfo(taskId);
330+
assertNull(afterDeletion);
331+
}
332+
333+
}

0 commit comments

Comments
 (0)