minitest-rack 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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