minitest-rack 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.
@@ -0,0 +1,298 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/assertions'
4
+ require 'minitest/spec'
5
+
6
+ # reopening to add validations functionality
7
+ module Minitest
8
+ # add support for Assert syntax
9
+ module Assertions
10
+ # Test if a specific response header has an expected value
11
+ # Essentially, a shortcut for testing the `last_response.header` value
12
+ #
13
+ # @param [String] header The name of the HTTP header to check
14
+ # @param [String] contents The expected value of the header
15
+ #
16
+ # @return [Boolean] True if header matches expected value
17
+ #
18
+ # Example:
19
+ # assert_header('Accept', 'text/plain')
20
+ #
21
+ def assert_header(header, contents)
22
+ msg = "Expected response header '#{header}' to be '#{contents}', "
23
+ msg << "but was '#{last_response.headers[header]}'"
24
+
25
+ assert_equal(contents, last_response.headers[header], msg)
26
+ end
27
+
28
+ # Tests that a header contains the Accept content-type
29
+ # Accept indicates which content-types the client can process
30
+ #
31
+ # @param [String] type The content-type to check for
32
+ #
33
+ # @return [Boolean] True if Accept header matches the specified type
34
+ #
35
+ # Example:
36
+ # assert_header_accept("text/plain")
37
+ #
38
+ def assert_header_accept(type)
39
+ assert_header('Accept', type)
40
+ end
41
+
42
+ # Test for the `application/` type header via `Content-Type`
43
+ # Valid application types include pdf, json, xml, etc.
44
+ #
45
+ # @param [String] type The application content-type suffix (e.g. "pdf", "json")
46
+ #
47
+ # @return [Boolean] True if Content-Type header matches "application/type"
48
+ #
49
+ # Example:
50
+ # assert_header_application_type('pdf')
51
+ # # Tests Content-Type equals 'application/pdf'
52
+ #
53
+ def assert_header_application_type(type)
54
+ assert_header('Content-Type', "application/#{type}")
55
+ end
56
+
57
+ # Test if Content-Encoding header matches expected value
58
+ # Content-Encoding specifies what encodings have been applied to the payload.
59
+ # Common encoding types include gzip, compress, deflate, br
60
+ #
61
+ # @param [String] encoding_str The expected content encoding value
62
+ #
63
+ # @return [Boolean] True if Content-Encoding matches expected value
64
+ #
65
+ # Example:
66
+ # assert_header_content_encoding('gzip')
67
+ #
68
+ # assert_header_encoding('gzip')
69
+ #
70
+ def assert_header_content_encoding(encoding_str)
71
+ assert_header('Content-Encoding', encoding_str)
72
+ end
73
+ alias assert_header_encoding assert_header_content_encoding
74
+
75
+ # Tests that the Content-Language header matches an expected value
76
+ # Content-Language indicates the language of the content returned by the server
77
+ #
78
+ # @param [String] lang The expected language value
79
+ #
80
+ # @return [Boolean] True if Content-Language matches the expected value
81
+ #
82
+ # Example:
83
+ # assert_header_content_language('en')
84
+ # assert_header_content_language('fr')
85
+ #
86
+ def assert_header_content_language(lang)
87
+ assert_header('Content-Language', lang)
88
+ end
89
+ alias assert_header_language assert_header_content_language
90
+
91
+ # Tests that the Content-Length header matches the expected value
92
+ # Content-Length specifies the size of the response body in bytes
93
+ #
94
+ # @param [String, Integer] length The expected content length value
95
+ #
96
+ # @return [Boolean] True if Content-Length matches the expected value
97
+ #
98
+ # Example:
99
+ # assert_header_content_length(348)
100
+ # assert_header_content_length("348")
101
+ #
102
+ def assert_header_content_length(length)
103
+ assert_header('Content-Length', length.to_s)
104
+ end
105
+
106
+ # Test if Content-Location header matches expected value
107
+ # Content-Location indicates an alternate location for the returned data
108
+ #
109
+ # @param [String] url The expected alternate URL value
110
+ #
111
+ # @return [Boolean] True if Content-Location matches expected value
112
+ #
113
+ # Example:
114
+ # assert_header_content_location('/index.htm')
115
+ #
116
+ def assert_header_content_location(url)
117
+ assert_header('Content-Location', url.to_s)
118
+ end
119
+
120
+ # Tests that the Content-Type header matches an expected value
121
+ # Content-Type specifies the MIME type of the content being sent
122
+ #
123
+ # @param [String] type The expected MIME type
124
+ #
125
+ # @return [Boolean] True if Content-Type matches expected value
126
+ #
127
+ # Example:
128
+ # assert_header_content_type('text/html; charset=utf-8')
129
+ #
130
+ def assert_header_content_type(type)
131
+ assert_header('Content-Type', type)
132
+ end
133
+
134
+ # Tests that the ETag header matches an expected value
135
+ # ETag is a unique identifier for a specific version of a resource, often a hash
136
+ #
137
+ # @param [String] tag The expected ETag value
138
+ #
139
+ # @return [Boolean] True if ETag matches expected value
140
+ #
141
+ # Example:
142
+ # assert_header_etag('"737060cd8c284d8af7ad3082f209582d"')
143
+ #
144
+ def assert_header_etag(tag)
145
+ assert_header('ETag', tag)
146
+ end
147
+
148
+ # Tests that the Expires header matches an expected timestamp
149
+ # Expires defines when the response should be considered stale
150
+ #
151
+ # @param [String] timestamp The expected expiration date in HTTP-date format
152
+ #
153
+ # @return [Boolean] True if Expires matches expected timestamp
154
+ #
155
+ # Example:
156
+ # assert_header_expires('Thu, 01 Dec 1994 16:00:00 GMT')
157
+ #
158
+ def assert_header_expires(timestamp)
159
+ assert_header('Expires', timestamp)
160
+ end
161
+
162
+ # Tests that the Content-Type header matches an image MIME type
163
+ # Validates that content is an image with the specified format
164
+ #
165
+ # @param [String, Symbol] type The expected image format (bmp, gif, jpg, jpeg, png, tiff)
166
+ #
167
+ # @return [Boolean] True if Content-Type matches "image/type"
168
+ #
169
+ # Example:
170
+ # assert_header_image_type('png')
171
+ # # Tests Content-Type equals 'image/png'
172
+ #
173
+ def assert_header_image_type(type)
174
+ assert_header('Content-Type', "image/#{type}")
175
+ end
176
+
177
+ # Tests that the Last-Modified header matches an expected timestamp
178
+ # Last-Modified indicates when the resource was last changed
179
+ #
180
+ # @param [String] timestamp The expected last modified date in HTTP-date format
181
+ #
182
+ # @return [Boolean] True if Last-Modified matches expected timestamp
183
+ #
184
+ # Example:
185
+ # assert_header_last_modified('Tue, 15 Nov 1994 12:45:26 GMT')
186
+ #
187
+ def assert_header_last_modified(timestamp)
188
+ assert_header('Last-Modified', timestamp)
189
+ end
190
+
191
+ # Tests that the Server header matches an expected value
192
+ # Server specifies information about the software used by the origin server
193
+ #
194
+ # @param [String] server_str The expected server identification string
195
+ #
196
+ # @return [Boolean] True if Server header matches expected value
197
+ #
198
+ # Example:
199
+ # assert_header_server('Apache/2.4.1 (Unix)')
200
+ #
201
+ def assert_header_server(server_str)
202
+ assert_header('Server', server_str)
203
+ end
204
+
205
+ # Test for the `text/` type header via `Content-Type`
206
+ # Tests that the Content-Type header matches a text/* MIME type
207
+ #
208
+ # @param [String] type The text content-type suffix (e.g. "plain", "html")
209
+ #
210
+ # @return [Boolean] True if Content-Type header matches "text/type"
211
+ #
212
+ # Example:
213
+ # assert_header_text_type('plain')
214
+ # # Tests Content-Type equals 'text/plain'
215
+ #
216
+ def assert_header_text_type(type)
217
+ assert_header('Content-Type', "text/#{type}")
218
+ end
219
+
220
+ # Tests that the WWW-Authenticate header matches an expected value
221
+ # WWW-Authenticate indicates the authentication scheme that should be used
222
+ # to access the requested resource
223
+ #
224
+ # @param [String] auth_str The expected authentication scheme value
225
+ #
226
+ # @return [Boolean] True if WWW-Authenticate matches expected value
227
+ #
228
+ # Example:
229
+ # assert_header_www_authenticate('Basic')
230
+ # assert_header_www_authenticate('Bearer realm="example"')
231
+ #
232
+ def assert_header_www_authenticate(auth_str)
233
+ assert_header('WWW-Authenticate', auth_str)
234
+ end
235
+
236
+ # Tests that the Content-Disposition header indicates an attachment download
237
+ # Content-Disposition suggests whether content should be displayed inline or downloaded
238
+ # as an attachment with an optional filename
239
+ #
240
+ # Raise a "File Download" dialogue box for a known MIME type with binary format or suggest
241
+ # a filename for dynamic content. Quotes are necessary with special characters
242
+ #
243
+ # @param [String] filename The suggested filename for the attachment
244
+ #
245
+ # @return [Boolean] True if Content-Disposition matches expected attachment format
246
+ #
247
+ # Example:
248
+ # assert_header_attachment('document.pdf')
249
+ # # Tests Content-Disposition equals 'attachment; filename="document.pdf"'
250
+ #
251
+ def assert_header_attachment(filename)
252
+ assert_header('Content-Disposition', "attachment; filename=\"#{filename}\"")
253
+ end
254
+
255
+ # Tests that the Content-Type header is set to "application/json"
256
+ # Validates that the response is formatted as JSON data
257
+ #
258
+ # @return [Boolean] True if Content-Type matches "application/json"
259
+ #
260
+ # Example:
261
+ # assert_header_type_is_json
262
+ # # Tests Content-Type equals 'application/json'
263
+ #
264
+ def assert_header_type_is_json
265
+ assert_header('Content-Type', 'application/json')
266
+ end
267
+ end
268
+ # /module Assertions
269
+
270
+ # add support for Spec syntax
271
+ module Expectations
272
+ infect_an_assertion :assert_header_accept, :must_have_header_accept, :reverse
273
+ infect_an_assertion :assert_header_application_type, :must_have_header_application_type,
274
+ :reverse
275
+ infect_an_assertion :assert_header_content_encoding, :must_have_header_content_encoding,
276
+ :reverse
277
+ infect_an_assertion :assert_header_content_language, :must_have_header_content_language,
278
+ :reverse
279
+ infect_an_assertion :assert_header_content_length, :must_have_header_content_length,
280
+ :reverse
281
+ infect_an_assertion :assert_header_content_location, :must_have_header_content_location,
282
+ :reverse
283
+ infect_an_assertion :assert_header_content_type, :must_have_header_content_type,
284
+ :reverse
285
+ infect_an_assertion :assert_header_etag, :must_have_header_etag, :reverse
286
+ infect_an_assertion :assert_header_expires, :must_have_header_expires, :reverse
287
+ infect_an_assertion :assert_header_image_type, :must_have_header_image_type,
288
+ :reverse
289
+ infect_an_assertion :assert_header_last_modified, :must_have_header_last_modified,
290
+ :reverse
291
+ infect_an_assertion :assert_header_server, :must_have_header_server, :reverse
292
+ infect_an_assertion :assert_header_text_type, :must_have_header_text_type, :reverse
293
+ infect_an_assertion :assert_header_www_authenticate, :must_have_header_www_authenticate,
294
+ :reverse
295
+ infect_an_assertion :assert_header_attachment, :must_have_header_attachment, :reverse
296
+ infect_an_assertion :assert_header_type_is_json, :must_have_header_type_as_json, :reverse
297
+ end
298
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'rack/test'
4
+ require 'json'
5
+ require 'minitest/assertions'
6
+ require 'minitest/spec'
7
+
8
+ # reopening to add validations functionality
9
+ module Minitest
10
+ # add support for Assert syntax
11
+ module Assertions
12
+ # Parse the response body of the last response as JSON using the native Ruby
13
+ # JSON parser. This method helps in quickly grabbing the JSON data from the
14
+ # response to verify in assertions.
15
+ #
16
+ # @return [Hash] parsed JSON data from the response body
17
+ #
18
+ def json_data
19
+ ::JSON.parse(last_response.body)
20
+ end
21
+
22
+ # Asserts against the presence of specific key/value pairs in the response JSON data. When
23
+ # testing endpoint responses for JSON data conformity, it ensures the response matches the
24
+ # expected data exactly. Takes a hash of expected data and validates it against the parsed
25
+ # JSON response.
26
+ #
27
+ # @param res [Hash] hash containing the expected key/value pairs that should be
28
+ # present in the JSON response data
29
+ #
30
+ # @return [Boolean] true when response data matches expected data
31
+ #
32
+ # @example
33
+ # # Response body contains: {"id": 1, "name": "test"}
34
+ # assert_json_data({id: 1, name: "test"}) # => true
35
+ #
36
+ def assert_json_data(res)
37
+ data = json_data
38
+
39
+ msg = "Expected response JSON data to be '#{res}', but was '#{data}'"
40
+
41
+ assert_equal(res, data, msg)
42
+ end
43
+
44
+ # Verify if the response JSON data contains a success attribute with specified truth value.
45
+ # This method specifically checks for the presence of a "success" key and validates its
46
+ # value against the expected boolean. By default, it expects true unless otherwise specified.
47
+ #
48
+ # @param bool [Boolean] the expected value of success attribute (defaults to true)
49
+ #
50
+ # @return [Boolean] true when the success value matches the expected boolean
51
+ #
52
+ def assert_json_success(bool: true)
53
+ data = json_data
54
+
55
+ msg = "Expected response JSON data to include '\"success\": #{bool}', "
56
+ msg << "but was '#{data.inspect}'"
57
+
58
+ assert_equal(bool, data['success'], msg)
59
+ end
60
+
61
+ # Asserts that the response JSON data contains an 'error' attribute with the specified
62
+ # error code and validates that its value matches the expected error code string.
63
+ # Default error code is "404" if none specified.
64
+ #
65
+ # @param errno [String] the expected error code value (defaults to "404")
66
+ #
67
+ # @return [Boolean] true when the error value matches the expected error code
68
+ #
69
+ def assert_json_error(errno = '404')
70
+ data = json_data
71
+
72
+ msg = "Expected response JSON data to include '\"error\": #{errno}', "
73
+ msg << "but was '#{data.inspect}'"
74
+
75
+ assert_equal(errno.to_s, data['error'].to_s, msg)
76
+ end
77
+
78
+ # Verifies that the response JSON data contains a message attribute with the
79
+ # specified message string.
80
+ # This method checks for the presence of a "message" key and validates that its
81
+ # value matches the expected message text.
82
+ #
83
+ # @param msg [String] the expected message value to check for in the response
84
+ #
85
+ # @return [Boolean] true when the message value matches the expected message string
86
+ #
87
+ def assert_json_message(message)
88
+ data = json_data
89
+
90
+ msg = "Expected response JSON data to include '\"message\": #{message}', "
91
+ msg << "but was '#{data.inspect}'"
92
+
93
+ assert_equal(message, data['message'], msg)
94
+ end
95
+
96
+ # Verifies that the response JSON data contains a specific model attribute with
97
+ # the expected model data.
98
+ # This method checks if the response contains a key matching the provided model name and
99
+ # validates that its JSON representation matches the expected model object.
100
+ #
101
+ # @param key [String, Symbol] the model key to check for in the response
102
+ # @param model [Object] the model object whose JSON representation matches the response data
103
+ #
104
+ # @return [Boolean] true when the model JSON matches the response data for the given key
105
+ #
106
+ # @example
107
+ # # Response body contains: {"user": {"id": 1, "name": "Bob"}}
108
+ # user = User.new(id: 1, name: "Bob")
109
+ # assert_json_model('user', user) # => true
110
+ #
111
+ def assert_json_model(key, model)
112
+ data = json_data
113
+ key = key.to_s
114
+
115
+ msg = 'Expected response JSON data to include '
116
+
117
+ # handle wrong key value being passed
118
+ if data.key?(key)
119
+ msg << "'#{key}: #{model.to_json}', but was '#{data[key].to_json}'"
120
+
121
+ assert_equal(model.values.to_json, data[key].to_json, msg)
122
+ else
123
+ msg << "key: '#{key}', but JSON is: '#{data}'"
124
+
125
+ assert_has_key data, key, msg
126
+ end
127
+ end
128
+
129
+ # Verifies that the response JSON data contains the specified key with a non-empty value.
130
+ # This method checks for the presence of a given key in the response and validates that
131
+ # its value is not empty, ensuring the expected data field exists and has content.
132
+ #
133
+ # @param key [String, Symbol] the key to check for in the response
134
+ #
135
+ # @return [Boolean] true when the key exists and has a non-empty value
136
+ #
137
+ def assert_json_key(key)
138
+ data = json_data
139
+ key = key.to_s
140
+
141
+ msg = 'Expected response JSON data to include '
142
+ msg << "key: '#{key}', but JSON is '#{data}'"
143
+
144
+ # handle the model being present
145
+ if data.key?(key)
146
+ refute_empty(data[key], msg)
147
+ else
148
+ assert_has_key data, key, msg
149
+ end
150
+ end
151
+
152
+ # Verifies that the response JSON data contains a nested key within a model attribute
153
+ # and that the key's value is not empty.
154
+ # This method checks if the response contains a model key and a nested key within it,
155
+ # validating that the nested key's value exists and is not empty.
156
+ #
157
+ # @param model [String, Symbol] the model key to check for in the response
158
+ # @param key [String, Symbol] the nested key to check within the model object
159
+ #
160
+ # @return [Boolean] true when the nested key exists and has a non-empty value
161
+ #
162
+ # rubocop:disable Metrics/MethodLength
163
+ def assert_json_model_key(model, key)
164
+ data = json_data
165
+ model = model.to_s
166
+ key = key.to_s
167
+
168
+ msg = 'Expected response JSON data to include '
169
+
170
+ # handle the model being present
171
+ if data.key?(model)
172
+ if data[model].key?(key)
173
+ msg = 'life is great'
174
+
175
+ refute_empty(data[model][key], msg)
176
+ else
177
+ msg << "model.key: '#{model}.#{key}', but it did not"
178
+
179
+ assert_has_key data, "#{model}.#{key}", msg
180
+ end
181
+ else
182
+ msg << "model: '#{model}', but it did not"
183
+
184
+ assert_has_key data, model, msg
185
+ end
186
+ end
187
+ # rubocop:enable Metrics/MethodLength
188
+
189
+ # Shortcut for sending GET requests as JSON
190
+ #
191
+ # get_json("/api/users")
192
+ #
193
+ def get_json(path, params = {}, headers = {})
194
+ json_request(:get, path, params, headers)
195
+ end
196
+
197
+ # Shortcut for sending POST requests as JSON
198
+ #
199
+ # post_json("/api/users", {name: "Joe"})
200
+ #
201
+ def post_json(path, params = {}, headers = {})
202
+ json_request(:post, path, params, headers)
203
+ end
204
+
205
+ # Shortcut for sending PUT requests as JSON
206
+ #
207
+ # put_json("/api/users/1234", {id: 1, name: "Joe"})
208
+ #
209
+ def put_json(path, params = {}, headers = {})
210
+ json_request(:put, path, params, headers)
211
+ end
212
+
213
+ # Shortcut for sending DELETE requests as JSON
214
+ #
215
+ # delete_json("/api/users/1234")
216
+ #
217
+ def delete_json(path, params = {}, headers = {})
218
+ json_request(:delete, path, params, headers)
219
+ end
220
+
221
+ private
222
+
223
+ # Makes an HTTP request with JSON data. This helper method is used by the HTTP verb shortcuts
224
+ # (get_json, post_json, etc) to standardize JSON request handling. It converts the params
225
+ # to JSON and sets the appropriate content type header.
226
+ #
227
+ # @param verb [Symbol] the HTTP verb to use (:get, :post, :put, :delete)
228
+ # @param path [String] the URL path for the request
229
+ # @param params [Hash] parameters to be converted to JSON and sent with request (default: {})
230
+ # @param headers [Hash] HTTP headers to include with request (default: {})
231
+ #
232
+ # @return [Response] the response from the HTTP request
233
+ #
234
+ def json_request(verb, path, params = {}, headers = {})
235
+ send(verb, path, params.to_json, headers.merge({ 'Content-Type' => 'application/json' }))
236
+ end
237
+ end
238
+ # /module Assertions
239
+
240
+ # add support for Spec syntax
241
+ module Expectations
242
+ infect_an_assertion :assert_json_data, :must_have_json_data, :reverse
243
+ infect_an_assertion :assert_json_success, :must_have_json_success, :reverse
244
+ infect_an_assertion :assert_json_error, :must_have_json_error, :reverse
245
+ infect_an_assertion :assert_json_message, :must_have_json_message, :reverse
246
+ infect_an_assertion :assert_json_key, :must_have_json_key, :reverse
247
+ infect_an_assertion :assert_json_model, :must_have_json_model, :reverse
248
+ infect_an_assertion :assert_json_model_key, :must_have_json_model_key, :reverse
249
+ end
250
+ end