rspec-sse-matchers 0.1.1 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +45 -0
- data/examples/custom_matchers_example.rb +134 -0
- data/lib/rspec/sse/matchers/version.rb +1 -1
- data/lib/rspec/sse/matchers.rb +56 -6
- data/sig/generated/rspec/sse/matchers.rbs +16 -0
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e06b785e8c91c1cc5fa04b08dfaf81a9a33d3e750bf9b52bd6b417db55e01ab
|
4
|
+
data.tar.gz: e16dde236261e02ff67d80e4deb1fc8ec1847dd5e432b193918c186f10487c59
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f14b23281a644ca36dd8a636ed43a594954e4a7095a1f30debd81c82e4d718a1792ed77711e71ea9147f2e41b409e002a132e70f9b29471d42836bdb350a45ff
|
7
|
+
data.tar.gz: bc1ff76aa4c587d292cc329686d0c61e04d7e854a50c966f6d315af2166e8da572f0d84788f3899dbd3a52bf794a49960705034fc884bde168326cdf3d052999
|
data/README.md
CHANGED
@@ -127,6 +127,51 @@ expect(response).to be_sse_events([
|
|
127
127
|
|
128
128
|
When the `json: true` option is enabled, the matcher attempts to parse each event's `data` as JSON. If parsing fails (the data is not valid JSON), it raises an error.
|
129
129
|
|
130
|
+
### Using Custom RSpec Matchers
|
131
|
+
|
132
|
+
The SSE matchers now support RSpec custom matchers like `hash_including`, `a_kind_of`, `be_an`, etc. This allows for more flexible matching when testing SSE events with complex data structures:
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
# With hash_including for partial matching
|
136
|
+
expect(response).to be_sse_events([
|
137
|
+
{type: "in_progress", data: {"event" => "in_progress", "data" => {}}, id: "1", retry: 250},
|
138
|
+
hash_including(
|
139
|
+
type: "finished",
|
140
|
+
data: hash_including(
|
141
|
+
"event" => "finished",
|
142
|
+
"data" => hash_including(
|
143
|
+
"object_id" => a_kind_of(String), # or a_string_starting_with("prefix_")
|
144
|
+
"results" => be_an(Array)
|
145
|
+
)
|
146
|
+
),
|
147
|
+
id: "2",
|
148
|
+
retry: 250
|
149
|
+
)
|
150
|
+
], json: true)
|
151
|
+
|
152
|
+
# With type checking matchers
|
153
|
+
expect(response).to be_sse_event_data([
|
154
|
+
hash_including("id" => a_kind_of(Integer), "name" => a_kind_of(String)),
|
155
|
+
hash_including("status" => match(/active|pending/))
|
156
|
+
], json: true)
|
157
|
+
|
158
|
+
# Works with all matcher types
|
159
|
+
expect(response).to contain_exactly_sse_events([
|
160
|
+
hash_including(data: hash_including("status" => "pending")),
|
161
|
+
hash_including(data: hash_including("status" => "active"))
|
162
|
+
], json: true)
|
163
|
+
|
164
|
+
expect(response).to have_sse_events([
|
165
|
+
hash_including(data: hash_including("important_field" => "value"))
|
166
|
+
], json: true)
|
167
|
+
```
|
168
|
+
|
169
|
+
Custom matchers are particularly useful when:
|
170
|
+
- You only care about specific fields in the event data
|
171
|
+
- You want to match patterns or types rather than exact values
|
172
|
+
- You need to test complex nested structures
|
173
|
+
- You want to ignore irrelevant fields
|
174
|
+
|
130
175
|
## Examples
|
131
176
|
|
132
177
|
### Testing Event Types
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rspec"
|
4
|
+
require "rspec/sse/matchers"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
# Mock response for demonstration
|
8
|
+
class MockResponse
|
9
|
+
attr_reader :body, :headers, :status
|
10
|
+
def initialize(body:, status: 200, headers: {"content-type" => "text/event-stream", "cache-control" => "no-store"})
|
11
|
+
@body = body
|
12
|
+
@status = status
|
13
|
+
@headers = headers
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_sse_body(events)
|
18
|
+
body = ""
|
19
|
+
events.each do |event|
|
20
|
+
body += "id: #{event[:id]}\n" if event[:id]
|
21
|
+
body += "event: #{event[:type]}\n" if event[:type]
|
22
|
+
body += "data: #{event[:data]}\n" if event[:data]
|
23
|
+
body += "retry: #{event[:retry]}\n" if event[:retry]
|
24
|
+
body += "\n"
|
25
|
+
end
|
26
|
+
body
|
27
|
+
end
|
28
|
+
|
29
|
+
RSpec.describe "SSE with custom matchers" do
|
30
|
+
include RSpec::Matchers
|
31
|
+
|
32
|
+
describe "example from the original request" do
|
33
|
+
let(:event1) { {type: "in_progress", data: '{"event":"in_progress","data":{}}', id: "", retry: 250} }
|
34
|
+
let(:event2) { {type: "finished", data: '{"event":"finished","data":{"object_id":"special_prefix_12345","results":[1,2,3]}}', id: "", retry: 250} }
|
35
|
+
let(:response) { MockResponse.new(body: create_sse_body([event1, event2])) }
|
36
|
+
|
37
|
+
it "matches SSE events with custom matchers" do
|
38
|
+
expect(response).to be_sse_events([
|
39
|
+
{type: "in_progress", data: {"event" => "in_progress", "data" => {}}, id: "", retry: 250},
|
40
|
+
hash_including(
|
41
|
+
type: "finished",
|
42
|
+
data: hash_including(
|
43
|
+
"event" => "finished",
|
44
|
+
"data" => hash_including(
|
45
|
+
"object_id" => a_kind_of(String), # or a_string_starting_with("special_prefix_")
|
46
|
+
"results" => be_an(Array)
|
47
|
+
)
|
48
|
+
),
|
49
|
+
id: "",
|
50
|
+
retry: 250
|
51
|
+
)
|
52
|
+
], json: true)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "other examples" do
|
57
|
+
context "partial matching with hash_including" do
|
58
|
+
let(:events) {
|
59
|
+
[
|
60
|
+
{type: "user_update", data: '{"user_id":123,"name":"John","email":"john@example.com","status":"active"}', id: "1", retry: 1000}
|
61
|
+
]
|
62
|
+
}
|
63
|
+
let(:response) { MockResponse.new(body: create_sse_body(events)) }
|
64
|
+
|
65
|
+
it "matches only specific fields" do
|
66
|
+
expect(response).to be_sse_events([
|
67
|
+
hash_including(
|
68
|
+
type: "user_update",
|
69
|
+
data: hash_including(
|
70
|
+
"user_id" => 123,
|
71
|
+
"status" => "active"
|
72
|
+
# Other fields are ignored
|
73
|
+
)
|
74
|
+
)
|
75
|
+
], json: true)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context "type checking with RSpec matchers" do
|
80
|
+
let(:events) {
|
81
|
+
[
|
82
|
+
{type: "data_point", data: '{"value":42.5,"timestamp":"2023-01-01T10:00:00Z","tags":["sensor","temperature"]}', id: "2", retry: 500}
|
83
|
+
]
|
84
|
+
}
|
85
|
+
let(:response) { MockResponse.new(body: create_sse_body(events)) }
|
86
|
+
|
87
|
+
it "validates data types" do
|
88
|
+
expect(response).to be_sse_events([
|
89
|
+
hash_including(
|
90
|
+
type: "data_point",
|
91
|
+
data: hash_including(
|
92
|
+
"value" => a_kind_of(Numeric),
|
93
|
+
"timestamp" => match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/),
|
94
|
+
"tags" => include("sensor")
|
95
|
+
)
|
96
|
+
)
|
97
|
+
], json: true)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context "array matching" do
|
102
|
+
let(:events) {
|
103
|
+
[
|
104
|
+
{type: "batch", data: '{"items":[{"id":1,"status":"ok"},{"id":2,"status":"error"},{"id":3,"status":"ok"}]}', id: "3", retry: 1000}
|
105
|
+
]
|
106
|
+
}
|
107
|
+
let(:response) { MockResponse.new(body: create_sse_body(events)) }
|
108
|
+
|
109
|
+
it "matches arrays with custom expectations" do
|
110
|
+
expect(response).to be_sse_events([
|
111
|
+
hash_including(
|
112
|
+
type: "batch",
|
113
|
+
data: hash_including(
|
114
|
+
"items" => include(
|
115
|
+
hash_including("id" => 2, "status" => "error")
|
116
|
+
)
|
117
|
+
)
|
118
|
+
)
|
119
|
+
], json: true)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "matches array properties" do
|
123
|
+
expect(response).to be_sse_events([
|
124
|
+
hash_including(
|
125
|
+
type: "batch",
|
126
|
+
data: hash_including(
|
127
|
+
"items" => have_attributes(size: 3)
|
128
|
+
)
|
129
|
+
)
|
130
|
+
], json: true)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
data/lib/rspec/sse/matchers.rb
CHANGED
@@ -187,6 +187,40 @@ module RSpec
|
|
187
187
|
@json = json
|
188
188
|
end
|
189
189
|
|
190
|
+
# Check if individual items match
|
191
|
+
# Supports RSpec matchers as expected values
|
192
|
+
#
|
193
|
+
# @rbs actual_item: Object
|
194
|
+
# @rbs expected_item: Object
|
195
|
+
# @rbs return: bool
|
196
|
+
def match_items(actual_item, expected_item)
|
197
|
+
if expected_item.respond_to?(:===)
|
198
|
+
# It's an RSpec argument matcher (hash_including, etc.)
|
199
|
+
expected_item === actual_item
|
200
|
+
elsif expected_item.respond_to?(:matches?)
|
201
|
+
# It's an RSpec regular matcher
|
202
|
+
expected_item.matches?(actual_item)
|
203
|
+
else
|
204
|
+
actual_item == expected_item
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Check if two arrays match element by element
|
209
|
+
# Supports RSpec matchers as expected values
|
210
|
+
#
|
211
|
+
# @rbs actual_array: Array[Object]
|
212
|
+
# @rbs expected_array: Array[Object]
|
213
|
+
# @rbs return: bool
|
214
|
+
def match_arrays(actual_array, expected_array)
|
215
|
+
return false unless actual_array.size == expected_array.size
|
216
|
+
|
217
|
+
actual_array.each_with_index do |actual_item, index|
|
218
|
+
expected_item = expected_array[index]
|
219
|
+
return false unless match_items(actual_item, expected_item)
|
220
|
+
end
|
221
|
+
true
|
222
|
+
end
|
223
|
+
|
190
224
|
# @rbs actual: Object
|
191
225
|
# @rbs return: bool
|
192
226
|
def matches?(actual)
|
@@ -278,18 +312,17 @@ module RSpec
|
|
278
312
|
@actual = actual
|
279
313
|
@actual.headers["content-type"] == "text/event-stream" \
|
280
314
|
&& @actual.headers["cache-control"]&.match(/no-(?:store|cache)/) \
|
281
|
-
&& @actual.headers["content-length"].nil? \
|
282
315
|
&& @actual.status == 200
|
283
316
|
end
|
284
317
|
|
285
318
|
# @rbs return: String
|
286
319
|
def failure_message
|
287
|
-
"Expected response header of `content-type` is `text/event-stream`, `cache-control` contains `no-store
|
320
|
+
"Expected response header of `content-type` is `text/event-stream`, `cache-control` contains `no-store` or `no-cache`, and status code is `2xx`"
|
288
321
|
end
|
289
322
|
|
290
323
|
# @rbs return: String
|
291
324
|
def failure_message_when_negated
|
292
|
-
"Expected response header of `content-type` is not `text/event-stream`, `cache-control` does not contain `no-store
|
325
|
+
"Expected response header of `content-type` is not `text/event-stream`, `cache-control` does not contain `no-store` or `no-cache`, and/or status code is not `2xx`"
|
293
326
|
end
|
294
327
|
|
295
328
|
# @rbs return: String
|
@@ -303,7 +336,7 @@ module RSpec
|
|
303
336
|
#
|
304
337
|
# @rbs return: bool
|
305
338
|
def match_condition
|
306
|
-
extract_actual
|
339
|
+
match_arrays(extract_actual, @expected)
|
307
340
|
end
|
308
341
|
|
309
342
|
# @rbs return: String
|
@@ -318,7 +351,22 @@ module RSpec
|
|
318
351
|
#
|
319
352
|
# @rbs return: bool
|
320
353
|
def match_condition
|
321
|
-
|
354
|
+
return false unless extract_actual.size == @expected.size
|
355
|
+
|
356
|
+
matched_indices = {}
|
357
|
+
@expected.each do |expected_item|
|
358
|
+
matched = false
|
359
|
+
extract_actual.each_with_index do |actual_item, index|
|
360
|
+
next if matched_indices[index]
|
361
|
+
if match_items(actual_item, expected_item)
|
362
|
+
matched_indices[index] = true
|
363
|
+
matched = true
|
364
|
+
break
|
365
|
+
end
|
366
|
+
end
|
367
|
+
return false unless matched
|
368
|
+
end
|
369
|
+
true
|
322
370
|
end
|
323
371
|
|
324
372
|
# @rbs return: String
|
@@ -333,7 +381,9 @@ module RSpec
|
|
333
381
|
#
|
334
382
|
# @rbs return: bool
|
335
383
|
def match_condition
|
336
|
-
@expected.all?
|
384
|
+
@expected.all? do |expected_item|
|
385
|
+
extract_actual.any? { |actual_item| match_items(actual_item, expected_item) }
|
386
|
+
end
|
337
387
|
end
|
338
388
|
|
339
389
|
# @rbs return: String
|
@@ -133,6 +133,22 @@ module RSpec
|
|
133
133
|
# @rbs expected: Array[Object]
|
134
134
|
def initialize: (Array[Object] expected, ?json: untyped) -> untyped
|
135
135
|
|
136
|
+
# Check if individual items match
|
137
|
+
# Supports RSpec matchers as expected values
|
138
|
+
#
|
139
|
+
# @rbs actual_item: Object
|
140
|
+
# @rbs expected_item: Object
|
141
|
+
# @rbs return: bool
|
142
|
+
def match_items: (Object actual_item, Object expected_item) -> bool
|
143
|
+
|
144
|
+
# Check if two arrays match element by element
|
145
|
+
# Supports RSpec matchers as expected values
|
146
|
+
#
|
147
|
+
# @rbs actual_array: Array[Object]
|
148
|
+
# @rbs expected_array: Array[Object]
|
149
|
+
# @rbs return: bool
|
150
|
+
def match_arrays: (Array[Object] actual_array, Array[Object] expected_array) -> bool
|
151
|
+
|
136
152
|
# @rbs actual: Object
|
137
153
|
# @rbs return: bool
|
138
154
|
def matches?: (Object actual) -> bool
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec-sse-matchers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- moznion
|
@@ -36,6 +36,7 @@ files:
|
|
36
36
|
- LICENSE
|
37
37
|
- README.md
|
38
38
|
- Rakefile
|
39
|
+
- examples/custom_matchers_example.rb
|
39
40
|
- examples/sse_spec.rb
|
40
41
|
- lib/rspec/sse/matchers.rb
|
41
42
|
- lib/rspec/sse/matchers/version.rb
|