a2a-test-framework 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/a2a.json +1961 -0
  3. data/a2a.proto +796 -0
  4. data/endpoints/grpc/cancel_task.json +10 -0
  5. data/endpoints/grpc/create_task_push_notification_config.json +10 -0
  6. data/endpoints/grpc/delete_task_push_notification_config.json +10 -0
  7. data/endpoints/grpc/get_extended_agent_card.json +10 -0
  8. data/endpoints/grpc/get_task.json +10 -0
  9. data/endpoints/grpc/get_task_push_notification_config.json +10 -0
  10. data/endpoints/grpc/list_task_push_notification_configs.json +10 -0
  11. data/endpoints/grpc/list_tasks.json +10 -0
  12. data/endpoints/grpc/send_message.json +10 -0
  13. data/endpoints/grpc/send_streaming_message.json +10 -0
  14. data/endpoints/grpc/subscribe_to_task.json +10 -0
  15. data/endpoints/rest/cancel_task.json +85 -0
  16. data/endpoints/rest/create_task_push_notification_config.json +104 -0
  17. data/endpoints/rest/delete_task_push_notification_config.json +46 -0
  18. data/endpoints/rest/get_extended_agent_card.json +168 -0
  19. data/endpoints/rest/get_task.json +111 -0
  20. data/endpoints/rest/get_task_push_notification_config.json +90 -0
  21. data/endpoints/rest/list_task_push_notification_configs.json +108 -0
  22. data/endpoints/rest/list_tasks.json +239 -0
  23. data/endpoints/rest/send_message.json +57 -0
  24. data/endpoints/rest/send_streaming_message.json +75 -0
  25. data/endpoints/rest/subscribe_to_task.json +68 -0
  26. data/exe/a2a-test +6 -0
  27. data/lib/a2a_test_framework/cli.rb +190 -0
  28. data/lib/a2a_test_framework/sse_client.rb +104 -0
  29. data/lib/a2a_test_framework/test_helper.rb +146 -0
  30. data/lib/a2a_test_framework/version.rb +5 -0
  31. data/lib/a2a_test_framework.rb +17 -0
  32. data/tests/grpc/cancel_task_test.rb +69 -0
  33. data/tests/grpc/create_task_push_notification_config_test.rb +79 -0
  34. data/tests/grpc/delete_task_push_notification_config_test.rb +54 -0
  35. data/tests/grpc/error_code_mappings_test.rb +39 -0
  36. data/tests/grpc/error_handling_test.rb +175 -0
  37. data/tests/grpc/get_extended_agent_card_test.rb +83 -0
  38. data/tests/grpc/get_task_push_notification_config_test.rb +39 -0
  39. data/tests/grpc/get_task_test.rb +76 -0
  40. data/tests/grpc/grpc_binding_test.rb +74 -0
  41. data/tests/grpc/list_task_push_notification_configs_test.rb +53 -0
  42. data/tests/grpc/list_tasks_test.rb +117 -0
  43. data/tests/grpc/protocol_data_model_test.rb +14 -0
  44. data/tests/grpc/send_message_test.rb +141 -0
  45. data/tests/grpc/send_streaming_message_test.rb +122 -0
  46. data/tests/grpc/streaming_event_delivery_test.rb +48 -0
  47. data/tests/grpc/subscribe_to_task_test.rb +92 -0
  48. data/tests/grpc/versioning_test.rb +32 -0
  49. data/tests/rest/agent_card_caching_test.rb +39 -0
  50. data/tests/rest/agent_card_signing_test.rb +74 -0
  51. data/tests/rest/agent_discovery_test.rb +117 -0
  52. data/tests/rest/authentication_authorization_test.rb +62 -0
  53. data/tests/rest/cancel_task_test.rb +110 -0
  54. data/tests/rest/capability_validation_test.rb +78 -0
  55. data/tests/rest/context_identifier_semantics_test.rb +75 -0
  56. data/tests/rest/create_task_push_notification_config_test.rb +122 -0
  57. data/tests/rest/custom_binding_test.rb +96 -0
  58. data/tests/rest/delete_task_push_notification_config_test.rb +103 -0
  59. data/tests/rest/error_code_mappings_test.rb +45 -0
  60. data/tests/rest/error_handling_test.rb +178 -0
  61. data/tests/rest/extension_versioning_test.rb +44 -0
  62. data/tests/rest/field_presence_optionality_test.rb +64 -0
  63. data/tests/rest/functional_equivalence_test.rb +23 -0
  64. data/tests/rest/get_extended_agent_card_test.rb +67 -0
  65. data/tests/rest/get_task_push_notification_config_test.rb +75 -0
  66. data/tests/rest/get_task_test.rb +134 -0
  67. data/tests/rest/history_length_semantics_test.rb +91 -0
  68. data/tests/rest/http_rest_binding_test.rb +114 -0
  69. data/tests/rest/iana_registrations_test.rb +47 -0
  70. data/tests/rest/idempotency_test.rb +69 -0
  71. data/tests/rest/in_task_authorization_test.rb +45 -0
  72. data/tests/rest/json_field_naming_test.rb +89 -0
  73. data/tests/rest/json_rpc_binding_test.rb +102 -0
  74. data/tests/rest/list_task_push_notification_configs_test.rb +92 -0
  75. data/tests/rest/list_tasks_test.rb +162 -0
  76. data/tests/rest/messages_and_artifacts_test.rb +101 -0
  77. data/tests/rest/multi_turn_conversation_test.rb +94 -0
  78. data/tests/rest/protocol_data_model_test.rb +99 -0
  79. data/tests/rest/protocol_security_test.rb +25 -0
  80. data/tests/rest/protocol_selection_negotiation_test.rb +24 -0
  81. data/tests/rest/push_notification_delivery_test.rb +115 -0
  82. data/tests/rest/security_considerations_test.rb +101 -0
  83. data/tests/rest/send_message_test.rb +230 -0
  84. data/tests/rest/send_streaming_message_test.rb +129 -0
  85. data/tests/rest/service_parameters_test.rb +52 -0
  86. data/tests/rest/streaming_event_delivery_test.rb +58 -0
  87. data/tests/rest/subscribe_to_task_test.rb +99 -0
  88. data/tests/rest/task_identifier_semantics_test.rb +67 -0
  89. data/tests/rest/timestamps_test.rb +70 -0
  90. data/tests/rest/versioning_responsibilities_test.rb +46 -0
  91. data/tests/rest/versioning_test.rb +44 -0
  92. metadata +159 -0
@@ -0,0 +1,114 @@
1
+ require "a2a_test_framework/test_helper"
2
+ require "a2a_test_framework/sse_client"
3
+
4
+ # HTTP+JSON/REST protocol binding specifics
5
+
6
+ describe "HTTP+JSON/REST Protocol Binding (REST)" do
7
+ # --- URL Patterns ---
8
+
9
+ describe "when verifying endpoint URL patterns" do
10
+ it "should use POST /message:send for Send Message" do
11
+ body = build_send_message_request(text: "URL pattern test")
12
+ response = http_post("/message:send", body)
13
+ response.code.to_i.should.equal 200
14
+ end
15
+
16
+ it "should use POST /message:stream for streaming message" do
17
+ body = build_send_message_request(text: "Stream URL test")
18
+ events = SSEClient.post_stream("/message:stream", body, timeout_seconds: 10)
19
+ events.length.should.be > 0
20
+ end
21
+
22
+ it "should use GET /tasks/{id} for Get Task" do
23
+ task = create_task!(text: "GET task URL test")
24
+ response = http_get("/tasks/#{task["id"]}")
25
+ response.code.to_i.should.equal 200
26
+ end
27
+
28
+ it "should use GET /tasks for List Tasks" do
29
+ response = http_get("/tasks")
30
+ response.code.to_i.should.equal 200
31
+ end
32
+
33
+ it "should use POST /tasks/{id}:cancel for Cancel Task" do
34
+ task = create_task!(text: "Cancel URL test")
35
+ response = http_post("/tasks/#{task["id"]}:cancel", {})
36
+ # Will be 400 since task is completed, but endpoint exists
37
+ response.code.to_i.should.not.equal 404
38
+ end
39
+ end
40
+
41
+ # --- Query Parameter Naming ---
42
+
43
+ describe "when sending GET requests with query parameters" do
44
+ it "should accept camelCase query parameter names" do
45
+ task = create_task!(text: "Query param test")
46
+ response = http_get("/tasks?contextId=#{task["contextId"]}&pageSize=10")
47
+ response.code.to_i.should.equal 200
48
+ end
49
+
50
+ it "should accept historyLength as camelCase parameter" do
51
+ task = create_task!(text: "HistoryLength param test")
52
+ response = http_get("/tasks/#{task["id"]}?historyLength=5")
53
+ response.code.to_i.should.equal 200
54
+ end
55
+ end
56
+
57
+ # --- Error Handling ---
58
+
59
+ describe "when an error occurs" do
60
+ it "should return error details with proper structure" do
61
+ response = http_get("/tasks/nonexistent-#{SecureRandom.uuid}")
62
+
63
+ data = parse_json(response)
64
+ if data.key?("error")
65
+ data["error"].should.be.kind_of Hash
66
+ data["error"]["code"].should.not.be.nil
67
+ data["error"]["message"].should.not.be.nil
68
+ end
69
+ true.should.equal true
70
+ end
71
+ end
72
+
73
+ describe "when a TaskNotFoundError occurs" do
74
+ it "should return HTTP 404" do
75
+ response = http_get("/tasks/nonexistent-#{SecureRandom.uuid}")
76
+ response.code.to_i.should.equal 404
77
+ end
78
+ end
79
+
80
+ # --- Streaming ---
81
+
82
+ describe "when a streaming operation is invoked" do
83
+ it "should use text/event-stream content type" do
84
+ body = build_send_message_request(text: "SSE content type test")
85
+ uri = URI("#{BASE_URL}/message:stream")
86
+ http = Net::HTTP.new(uri.host, uri.port)
87
+ request = Net::HTTP::Post.new(uri.path)
88
+ request["Content-Type"] = "application/json"
89
+ request["Accept"] = "text/event-stream"
90
+ request.body = JSON.generate(body)
91
+
92
+ http.request(request) do |response|
93
+ content_type = response["Content-Type"].to_s
94
+ content_type.should.include "text/event-stream"
95
+ end
96
+ end
97
+ end
98
+
99
+ # --- Content-Type ---
100
+ # NOTE: Commented out -- server may accept application/json as well
101
+
102
+ # describe "when sending requests or receiving responses" do
103
+ # it "should use Content-Type application/a2a+json" do
104
+ # end
105
+ # end
106
+
107
+ # --- Service Parameter Transmission ---
108
+ # NOTE: Commented out -- A2A-Extensions header not tested by reference server
109
+
110
+ # describe "when transmitting A2A service parameters" do
111
+ # it "should use standard HTTP request headers" do
112
+ # end
113
+ # end
114
+ end
@@ -0,0 +1,47 @@
1
+ require "a2a_test_framework/test_helper"
2
+
3
+ # NOTE: All tests commented out -- IANA registrations are informational only
4
+
5
+ # require "a2a_test_framework/test_helper"
6
+
7
+ # # NOTE: All tests commented out -- IANA registrations are informational only
8
+
9
+ # # describe "IANA Registrations (REST)" do
10
+ # # # --- Media Type ---
11
+ # #
12
+ # # describe "when using application/a2a+json media type" do
13
+ # # it "should use UTF-8 encoding for JSON text" do
14
+ # # end
15
+ # #
16
+ # # it "should validate content against the A2A protocol schema before processing" do
17
+ # # end
18
+ # #
19
+ # # it "should sanitize user-provided content to prevent injection attacks" do
20
+ # # end
21
+ # #
22
+ # # it "should validate file references to prevent SSRF" do
23
+ # # end
24
+ # # end
25
+ # #
26
+ # # # --- A2A-Version Header ---
27
+ # #
28
+ # # describe "when the A2A-Version header is present" do
29
+ # # it "should have a value in Major.Minor format" do
30
+ # # end
31
+ # # end
32
+ # #
33
+ # # # --- Well-Known URI ---
34
+ # #
35
+ # # describe "when a client requests /.well-known/agent-card.json" do
36
+ # # it "should return an AgentCard object as defined in the specification" do
37
+ # # end
38
+ # #
39
+ # # it "should not include sensitive credentials or internal implementation details" do
40
+ # # end
41
+ # # end
42
+ # #
43
+ # # describe "when serving the Agent Card" do
44
+ # # it "should support HTTPS to ensure authenticity and integrity" do
45
+ # # end
46
+ # # end
47
+ # # end
@@ -0,0 +1,69 @@
1
+ require "a2a_test_framework/test_helper"
2
+
3
+ # Cross-cutting: Idempotency guarantees
4
+ # REST: GET /tasks/{id}, GET /tasks, POST /message:send, POST /tasks/{id}:cancel
5
+
6
+ describe "Operation Idempotency (REST)" do
7
+ # --- Get Operations are Naturally Idempotent ---
8
+
9
+ describe "when a client sends repeated GetTask requests for the same task" do
10
+ it "should return the same Task state on both calls" do
11
+ task = create_task!(text: "Idempotent get test")
12
+
13
+ response1 = http_get("/tasks/#{task["id"]}")
14
+ response2 = http_get("/tasks/#{task["id"]}")
15
+
16
+ data1 = parse_json(response1)
17
+ data2 = parse_json(response2)
18
+
19
+ data1["id"].should.equal data2["id"]
20
+ data1["status"]["state"].should.equal data2["status"]["state"]
21
+ end
22
+ end
23
+
24
+ describe "when a client sends repeated ListTasks requests" do
25
+ it "should return consistent results" do
26
+ create_task!(text: "Idempotent list test")
27
+
28
+ response1 = http_get("/tasks")
29
+ response2 = http_get("/tasks")
30
+
31
+ data1 = parse_json(response1)
32
+ data2 = parse_json(response2)
33
+
34
+ data1["totalSize"].should.equal data2["totalSize"]
35
+ end
36
+ end
37
+
38
+ # --- Cancel Task Idempotency ---
39
+
40
+ describe "when a client sends multiple CancelTask requests for the same task" do
41
+ it "should return the same error response each time" do
42
+ task = create_task!(text: "Idempotent cancel test")
43
+
44
+ response1 = http_post("/tasks/#{task["id"]}:cancel", {})
45
+ response2 = http_post("/tasks/#{task["id"]}:cancel", {})
46
+
47
+ response1.code.to_i.should.equal response2.code.to_i
48
+ end
49
+ end
50
+
51
+ # --- Send Message Idempotency ---
52
+ # NOTE: Commented out -- messageId-based deduplication is optional (MAY)
53
+
54
+ # describe "when a client sends a SendMessageRequest with the same messageId twice" do
55
+ # it "should MAY detect the duplicate using messageId" do
56
+ # end
57
+ #
58
+ # it "should MAY return the same result without reprocessing" do
59
+ # end
60
+ # end
61
+
62
+ # --- GetExtendedAgentCard ---
63
+ # NOTE: Commented out -- returns error (not supported)
64
+
65
+ # describe "when a client sends repeated GetExtendedAgentCard requests" do
66
+ # it "should return the same AgentCard on both calls" do
67
+ # end
68
+ # end
69
+ end
@@ -0,0 +1,45 @@
1
+ require "a2a_test_framework/test_helper"
2
+
3
+ # NOTE: All tests commented out -- Reference server does not implement in-task authorization
4
+
5
+ # require "a2a_test_framework/test_helper"
6
+
7
+ # # NOTE: All tests commented out -- Reference server does not implement in-task authorization
8
+
9
+ # # describe "In-Task Authorization - Client and Security (REST)" do
10
+ # # # --- Client Responsibilities ---
11
+ # #
12
+ # # describe "when a client agent receives TASK_STATE_AUTH_REQUIRED from downstream" do
13
+ # # it "should transition its own Task to TASK_STATE_AUTH_REQUIRED" do
14
+ # # end
15
+ # #
16
+ # # it "should follow all In-Task Authorization Agent Responsibilities" do
17
+ # # end
18
+ # # end
19
+ # #
20
+ # # describe "when a task transitions to TASK_STATE_AUTH_REQUIRED without an active stream" do
21
+ # # it "should subscribe to task events using SubscribeToTask" do
22
+ # # end
23
+ # #
24
+ # # it "should OR register a webhook using CreatePushNotificationConfig" do
25
+ # # end
26
+ # #
27
+ # # it "should OR begin polling using GetTask" do
28
+ # # end
29
+ # # end
30
+ # #
31
+ # # # --- Security Considerations ---
32
+ # #
33
+ # # describe "when the agent requires credentials for in-task authorization" do
34
+ # # it "should receive credentials via a secure channel such as HTTPS" do
35
+ # # end
36
+ # # end
37
+ # #
38
+ # # describe "when credentials are passed through a chain of agents" do
39
+ # # it "should bind credentials to the agent which originated the request" do
40
+ # # end
41
+ # #
42
+ # # it "should encrypt sensitive credentials so only the originating agent can read them" do
43
+ # # end
44
+ # # end
45
+ # # end
@@ -0,0 +1,89 @@
1
+ require "a2a_test_framework/test_helper"
2
+
3
+ # Cross-cutting: JSON field naming convention applies to all REST endpoints
4
+
5
+ describe "JSON Field Naming Convention (REST)" do
6
+ # --- camelCase Field Names ---
7
+
8
+ describe "when the server returns a JSON response" do
9
+ it "should use camelCase format for all field names" do
10
+ task = create_task!(text: "Field naming test")
11
+ response = http_get("/tasks/#{task["id"]}")
12
+ data = parse_json(response)
13
+
14
+ # Check for camelCase keys (no underscores)
15
+ data.keys.each do |key|
16
+ key.should.not.match(/_[a-z]/)
17
+ end
18
+ end
19
+
20
+ it "should serialize contextId in camelCase" do
21
+ task = create_task!(text: "contextId test")
22
+ response = http_get("/tasks/#{task["id"]}")
23
+ data = parse_json(response)
24
+
25
+ data.key?("contextId").should.equal true
26
+ data.key?("context_id").should.equal false
27
+ end
28
+
29
+ it "should serialize artifactId in camelCase" do
30
+ task = create_task!(text: "artifactId test")
31
+ response = http_get("/tasks/#{task["id"]}")
32
+ data = parse_json(response)
33
+
34
+ if data["artifacts"] && data["artifacts"].length > 0
35
+ artifact = data["artifacts"].first
36
+ artifact.key?("artifactId").should.equal true
37
+ artifact.key?("artifact_id").should.equal false
38
+ end
39
+ true.should.equal true
40
+ end
41
+ end
42
+
43
+ # --- Enum Values ---
44
+
45
+ describe "when the server returns a response with enum values" do
46
+ it "should represent task state as SCREAMING_SNAKE_CASE string" do
47
+ task = create_task!(text: "Enum test")
48
+ response = http_get("/tasks/#{task["id"]}")
49
+ data = parse_json(response)
50
+
51
+ state = data["status"]["state"]
52
+ state.should.match(/\ATASK_STATE_[A-Z_]+\z/)
53
+ end
54
+
55
+ it "should represent message role as SCREAMING_SNAKE_CASE string" do
56
+ task = create_task!(text: "Role enum test")
57
+ response = http_get("/tasks/#{task["id"]}?historyLength=10")
58
+ data = parse_json(response)
59
+
60
+ if data["history"] && data["history"].length > 0
61
+ role = data["history"].first["role"]
62
+ role.should.match(/\AROLE_(USER|AGENT)\z/)
63
+ end
64
+ true.should.equal true
65
+ end
66
+ end
67
+
68
+ # --- Agent Card field naming ---
69
+
70
+ describe "when validating agent card field naming" do
71
+ it "should use camelCase for all agent card fields" do
72
+ response = http_get("/.well-known/agent-card.json")
73
+ data = parse_json(response)
74
+
75
+ # Check top-level keys
76
+ camel_keys = %w[name version description capabilities skills supportedInterfaces defaultInputModes defaultOutputModes]
77
+ camel_keys.each do |key|
78
+ if data.key?(key)
79
+ true.should.equal true
80
+ end
81
+ end
82
+
83
+ # No snake_case keys at top level
84
+ data.keys.each do |key|
85
+ key.should.not.match(/_[a-z]/)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,102 @@
1
+ require "a2a_test_framework/test_helper"
2
+
3
+ # NOTE: All tests commented out -- JSON-RPC binding tested separately via /a2a endpoint
4
+
5
+ # require "a2a_test_framework/test_helper"
6
+
7
+ # # NOTE: All tests commented out -- JSON-RPC binding tested separately via /a2a endpoint
8
+
9
+ # # describe "JSON-RPC Protocol Binding" do
10
+ # # # --- Protocol Requirements ---
11
+ # #
12
+ # # describe "when sending requests" do
13
+ # # it "should use Content-Type application/json" do
14
+ # # end
15
+ # # end
16
+ # #
17
+ # # describe "when receiving non-streaming responses" do
18
+ # # it "should use Content-Type application/json" do
19
+ # # end
20
+ # # end
21
+ # #
22
+ # # describe "when receiving streaming responses" do
23
+ # # it "should use Content-Type text/event-stream" do
24
+ # # end
25
+ # # end
26
+ # #
27
+ # # # --- Service Parameter Transmission ---
28
+ # #
29
+ # # describe "when transmitting A2A service parameters" do
30
+ # # it "should use HTTP header fields" do
31
+ # # end
32
+ # #
33
+ # # it "should comma-separate multiple extension values in a single header" do
34
+ # # end
35
+ # # end
36
+ # #
37
+ # # # --- Base Request Structure ---
38
+ # #
39
+ # # describe "when sending a JSON-RPC request" do
40
+ # # it "should include jsonrpc field set to 2.0" do
41
+ # # end
42
+ # #
43
+ # # it "should include an id field" do
44
+ # # end
45
+ # #
46
+ # # it "should include a method field" do
47
+ # # end
48
+ # #
49
+ # # it "should include a params field" do
50
+ # # end
51
+ # #
52
+ # # it "should use PascalCase for method names" do
53
+ # # end
54
+ # # end
55
+ # #
56
+ # # # --- Streaming via SSE ---
57
+ # #
58
+ # # describe "when SendStreamingMessage is invoked" do
59
+ # # it "should return HTTP 200 with Content-Type text/event-stream" do
60
+ # # end
61
+ # #
62
+ # # it "should contain JSON-RPC response objects in each data field" do
63
+ # # end
64
+ # # end
65
+ # #
66
+ # # describe "when SubscribeToTask is invoked" do
67
+ # # it "should return an SSE stream in the same format" do
68
+ # # end
69
+ # # end
70
+ # #
71
+ # # # --- Error Handling ---
72
+ # #
73
+ # # describe "when an error occurs" do
74
+ # # it "should contain an error object with numeric code and string message" do
75
+ # # end
76
+ # #
77
+ # # it "should include @type key in each data array object" do
78
+ # # end
79
+ # # end
80
+ # #
81
+ # # describe "when standard JSON-RPC errors occur" do
82
+ # # it "should return -32700 for JSONParseError" do
83
+ # # end
84
+ # #
85
+ # # it "should return -32600 for InvalidRequestError" do
86
+ # # end
87
+ # #
88
+ # # it "should return -32601 for MethodNotFoundError" do
89
+ # # end
90
+ # #
91
+ # # it "should return -32602 for InvalidParamsError" do
92
+ # # end
93
+ # #
94
+ # # it "should return -32603 for InternalError" do
95
+ # # end
96
+ # # end
97
+ # #
98
+ # # describe "when A2A-specific errors occur" do
99
+ # # it "should use error codes in range -32001 to -32099" do
100
+ # # end
101
+ # # end
102
+ # # end
@@ -0,0 +1,92 @@
1
+ require "a2a_test_framework/test_helper"
2
+
3
+ # REST endpoint: GET /tasks/{task_id}/pushNotificationConfigs
4
+ # Request: ListTaskPushNotificationConfigsRequest (taskId, pageSize, pageToken, tenant)
5
+ # Response: ListTaskPushNotificationConfigsResponse (configs[], nextPageToken)
6
+
7
+ describe "GET /tasks/{task_id}/pushNotificationConfigs" do
8
+ # --- Successful Retrieval ---
9
+
10
+ describe "when a task has multiple push notification configs" do
11
+ it "should return all active push notification configurations for that task" do
12
+ task = create_task!(text: "Multi config test")
13
+
14
+ # Create two configs
15
+ body1 = build_push_notification_config(task_id: task["id"], url: "https://example.com/hook1")
16
+ body2 = build_push_notification_config(task_id: task["id"], url: "https://example.com/hook2")
17
+ http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body1)
18
+ http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body2)
19
+
20
+ # List them
21
+ response = http_get("/tasks/#{task["id"]}/pushNotificationConfigs")
22
+ response.code.to_i.should.equal 200
23
+
24
+ data = parse_json(response)
25
+ data["configs"].should.be.kind_of Array
26
+ data["configs"].length.should.be >= 2
27
+ end
28
+ end
29
+
30
+ describe "when a task has no push notification configs" do
31
+ it "should return an empty list of configurations" do
32
+ task = create_task!(text: "No configs test")
33
+
34
+ response = http_get("/tasks/#{task["id"]}/pushNotificationConfigs")
35
+ response.code.to_i.should.equal 200
36
+
37
+ data = parse_json(response)
38
+ data["configs"].should.be.kind_of Array
39
+ data["configs"].length.should.equal 0
40
+ end
41
+ end
42
+
43
+ describe "when a task has both active and deleted push notification configs" do
44
+ it "should only return active configurations" do
45
+ task = create_task!(text: "Active vs deleted test")
46
+
47
+ # Create two, delete one
48
+ body1 = build_push_notification_config(task_id: task["id"], url: "https://example.com/keep")
49
+ body2 = build_push_notification_config(task_id: task["id"], url: "https://example.com/delete")
50
+ http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body1)
51
+ create2_resp = http_post("/tasks/#{task["id"]}/pushNotificationConfigs", body2)
52
+ config2 = parse_json(create2_resp)
53
+
54
+ # Delete the second
55
+ http_delete("/tasks/#{task["id"]}/pushNotificationConfigs/#{config2["id"]}")
56
+
57
+ # List should only show one
58
+ response = http_get("/tasks/#{task["id"]}/pushNotificationConfigs")
59
+ data = parse_json(response)
60
+
61
+ data["configs"].length.should.equal 1
62
+ data["configs"][0]["url"].should.equal "https://example.com/keep"
63
+ end
64
+ end
65
+
66
+ # --- Error Cases ---
67
+
68
+ describe "when a client sends a request with a non-existent task ID" do
69
+ it "should respond with a TaskNotFoundError" do
70
+ response = http_get("/tasks/nonexistent-#{SecureRandom.uuid}/pushNotificationConfigs")
71
+
72
+ if response.code.to_i >= 400
73
+ true.should.equal true
74
+ else
75
+ data = parse_json(response)
76
+ data.key?("error").should.equal true
77
+ end
78
+ end
79
+ end
80
+
81
+ # NOTE: Commented out -- reference server supports push notifications
82
+
83
+ # describe "when the server does not support push notifications" do
84
+ # it "should respond with a PushNotificationNotSupportedError" do
85
+ # end
86
+ # end
87
+
88
+ # describe "when a client sends a request for a task not accessible to the client" do
89
+ # it "should respond with a TaskNotFoundError" do
90
+ # end
91
+ # end
92
+ end