airborne-rails 0.99.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 +7 -0
- data/lib/airborne/expectations.rb +249 -0
- data/lib/airborne/helpers.rb +26 -0
- data/lib/airborne/path_matcher.rb +117 -0
- data/lib/airborne/version.rb +5 -0
- data/lib/airborne.rb +26 -0
- metadata +73 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c1a75ecbbfe8a614852a6d113d03254eed84d26d0e0715e70d051335090610bd
|
4
|
+
data.tar.gz: cbf2103b4072492d434988e02dba5ed3bda47cc515a77b16867f2c1037be0975
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b88e1bdcc2daa43b7386695480a35da4a31573c367b4ca4b910f1f74b8346a4bd6b2232ce66e1352a0caf5b7067dd71a6ce01392b4040bd931cde088fc12ce4a
|
7
|
+
data.tar.gz: e31ba52e1d369cf0f19087dc7b1efa87b3b963627ef8dabdcb491a804e19318ed031d796f3fbb661a14471ef922411da304bcb8ee9d00f474caea2e653cdf36c
|
@@ -0,0 +1,249 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Airborne
|
4
|
+
# rubocop:todo Metrics/ModuleLength
|
5
|
+
module Expectations
|
6
|
+
def expect_json_types(*args)
|
7
|
+
call_with_path(args) do |param, body|
|
8
|
+
expect_json_types_impl(param, body)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def expect_json(*args)
|
13
|
+
call_with_path(args) do |param, body|
|
14
|
+
expect_json_impl(param, body)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def expect_json_keys(*args)
|
19
|
+
call_with_path(args) do |param, body|
|
20
|
+
expect(body.keys).to include(*param)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def expect_json_sizes(*args)
|
25
|
+
args.push(convert_expectations_for_json_sizes(args.pop))
|
26
|
+
|
27
|
+
expect_json_types(*args)
|
28
|
+
end
|
29
|
+
|
30
|
+
def expect_status(code)
|
31
|
+
code = Rack::Utils::SYMBOL_TO_STATUS_CODE[code] if code.is_a?(Symbol)
|
32
|
+
|
33
|
+
expect(response.code.to_s).to eq(code.to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
def expect_header(key, content)
|
37
|
+
expect_header_impl(key, content)
|
38
|
+
end
|
39
|
+
|
40
|
+
def expect_header_contains(key, content)
|
41
|
+
expect_header_impl(key, content, true)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def get_by_path(...)
|
47
|
+
PathMatcher.call(...)
|
48
|
+
end
|
49
|
+
|
50
|
+
def expect_header_impl(key, content, contains = nil)
|
51
|
+
header = headers[key]
|
52
|
+
raise RSpec::Expectations::ExpectationNotMetError, "Header #{key} not present in the HTTP response" unless header
|
53
|
+
|
54
|
+
if contains
|
55
|
+
expect(header.downcase).to include(content.downcase)
|
56
|
+
else
|
57
|
+
expect(header.downcase).to eq(content.downcase)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# rubocop:todo Metrics/AbcSize
|
62
|
+
# rubocop:todo Metrics/CyclomaticComplexity
|
63
|
+
# rubocop:todo Metrics/MethodLength
|
64
|
+
# rubocop:todo Metrics/PerceivedComplexity
|
65
|
+
def expect_json_impl(expected, actual)
|
66
|
+
actual = actual.to_s if expected.is_a?(Regexp)
|
67
|
+
|
68
|
+
return expect(actual).to match(expected) if property?(expected)
|
69
|
+
|
70
|
+
keys = []
|
71
|
+
|
72
|
+
keys << expected.keys if match_expected?
|
73
|
+
keys << actual.keys if match_actual?
|
74
|
+
keys = expected.keys & actual.keys if match_none?
|
75
|
+
|
76
|
+
keys.flatten.uniq.each do |prop|
|
77
|
+
expected_value = extract_expected_value(expected, prop)
|
78
|
+
actual_value = extract_actual(actual, prop)
|
79
|
+
|
80
|
+
next expect_json_impl(expected_value, actual_value) if hash?(expected_value) && hash?(actual_value)
|
81
|
+
next expected_value.call(actual_value) if expected_value.is_a?(Proc)
|
82
|
+
next expect(actual_value.to_s).to match(expected_value) if expected_value.is_a?(Regexp)
|
83
|
+
|
84
|
+
expect(actual_value).to eq(expected_value)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def expect_json_types_impl(expected, actual)
|
89
|
+
@mapper ||= get_mapper
|
90
|
+
|
91
|
+
actual = convert_to_date(actual) if (expected == :date) || (expected == :date_or_null)
|
92
|
+
|
93
|
+
return expect_type(expected, actual) if expected.is_a?(Symbol)
|
94
|
+
return expected.call(actual) if expected.is_a?(Proc)
|
95
|
+
|
96
|
+
keys = []
|
97
|
+
|
98
|
+
keys << expected.keys if match_expected?
|
99
|
+
keys << actual.keys if match_actual?
|
100
|
+
keys = expected.keys & actual.keys if match_none?
|
101
|
+
|
102
|
+
keys.flatten.uniq.each do |prop|
|
103
|
+
type = extract_expected_type(expected, prop)
|
104
|
+
value = extract_actual(actual, prop)
|
105
|
+
value = convert_to_date(value) if (type == :date) || (type == :date_or_null)
|
106
|
+
|
107
|
+
next expect_json_types_impl(type, value) if hash?(type)
|
108
|
+
next type.call(value) if type.is_a?(Proc)
|
109
|
+
|
110
|
+
type_string = type.to_s
|
111
|
+
|
112
|
+
if type_string.include?("array_of") && !(type_string.include?("or_null") && value.nil?)
|
113
|
+
check_array_types(value, prop, type)
|
114
|
+
else
|
115
|
+
expect_type(type, value, prop)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
# rubocop:enable Metrics/ModuleLength
|
120
|
+
# rubocop:enable Metrics/AbcSize
|
121
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
122
|
+
# rubocop:enable Metrics/MethodLength
|
123
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
124
|
+
|
125
|
+
def call_with_path(args)
|
126
|
+
if args.length == 2
|
127
|
+
get_by_path(args[0], json_body) do |json_chunk|
|
128
|
+
yield(args[1], json_chunk)
|
129
|
+
end
|
130
|
+
else
|
131
|
+
yield(args[0], json_body)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def extract_expected_value(expected, prop)
|
136
|
+
raise unless expected.key?(prop)
|
137
|
+
|
138
|
+
expected[prop]
|
139
|
+
rescue StandardError
|
140
|
+
raise ExpectationError, "Expectation is expected to contain property: #{prop}"
|
141
|
+
end
|
142
|
+
|
143
|
+
def extract_expected_type(expected, prop)
|
144
|
+
type = expected[prop]
|
145
|
+
type.nil? ? raise : type
|
146
|
+
rescue StandardError
|
147
|
+
raise ExpectationError, "Expectation is expected to contain property: #{prop}"
|
148
|
+
end
|
149
|
+
|
150
|
+
def extract_actual(actual, prop)
|
151
|
+
actual[prop]
|
152
|
+
rescue StandardError
|
153
|
+
raise ExpectationError, "Expected #{actual.class} #{actual}\nto be an object with property #{prop}"
|
154
|
+
end
|
155
|
+
|
156
|
+
def expect_type(expected_type, value, prop_name = nil)
|
157
|
+
raise ExpectationError, "Expected type #{expected_type}\nis an invalid type" if @mapper[expected_type].nil?
|
158
|
+
|
159
|
+
insert = prop_name.nil? ? "" : "#{prop_name} to be of type"
|
160
|
+
message = "Expected #{insert} #{expected_type}\n got #{value.class} instead"
|
161
|
+
|
162
|
+
expect(@mapper[expected_type].any? { |type| value.is_a?(type) }).to eq(true), message
|
163
|
+
end
|
164
|
+
|
165
|
+
def convert_to_date(value)
|
166
|
+
DateTime.parse(value)
|
167
|
+
rescue StandardError
|
168
|
+
nil
|
169
|
+
end
|
170
|
+
|
171
|
+
def check_array_types(value, prop_name, expected_type)
|
172
|
+
expect_array(value, prop_name, expected_type)
|
173
|
+
value.each do |val|
|
174
|
+
expect_type(expected_type, val, prop_name)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def hash?(hash)
|
179
|
+
hash.is_a?(Hash)
|
180
|
+
end
|
181
|
+
|
182
|
+
def expect_array(value, prop_name, expected_type)
|
183
|
+
expect(value.class).to eq(Array),
|
184
|
+
"Expected #{prop_name}\n to be of type #{expected_type}\n got #{value.class} instead"
|
185
|
+
end
|
186
|
+
|
187
|
+
def convert_expectations_for_json_sizes(old_expectations)
|
188
|
+
return convert_expectation_for_json_sizes(old_expectations) unless old_expectations.is_a?(Hash)
|
189
|
+
|
190
|
+
old_expectations.each_with_object({}) do |(prop_name, expected_size), memo|
|
191
|
+
new_value = if expected_size.is_a?(Hash)
|
192
|
+
convert_expectations_for_json_sizes(expected_size)
|
193
|
+
else
|
194
|
+
convert_expectation_for_json_sizes(expected_size)
|
195
|
+
end
|
196
|
+
memo[prop_name] = new_value
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def convert_expectation_for_json_sizes(expected_size)
|
201
|
+
->(data) { expect(data.size).to eq(expected_size) }
|
202
|
+
end
|
203
|
+
|
204
|
+
def property?(expectation)
|
205
|
+
[String, Regexp, Float, Integer, TrueClass, FalseClass, NilClass, Array].any? { |type| expectation.is_a?(type) }
|
206
|
+
end
|
207
|
+
|
208
|
+
def get_mapper # rubocop:todo Metrics/MethodLength
|
209
|
+
base_mapper = {
|
210
|
+
integer: [Integer],
|
211
|
+
array_of_integers: [Integer],
|
212
|
+
int: [Integer],
|
213
|
+
array_of_ints: [Integer],
|
214
|
+
float: [Float, Integer],
|
215
|
+
array_of_floats: [Float, Integer],
|
216
|
+
string: [String],
|
217
|
+
array_of_strings: [String],
|
218
|
+
boolean: [TrueClass, FalseClass],
|
219
|
+
array_of_booleans: [TrueClass, FalseClass],
|
220
|
+
bool: [TrueClass, FalseClass],
|
221
|
+
array_of_bools: [TrueClass, FalseClass],
|
222
|
+
object: [Hash],
|
223
|
+
array_of_objects: [Hash],
|
224
|
+
array: [Array],
|
225
|
+
array_of_arrays: [Array],
|
226
|
+
date: [DateTime],
|
227
|
+
null: [NilClass]
|
228
|
+
}
|
229
|
+
|
230
|
+
mapper = base_mapper.clone
|
231
|
+
base_mapper.each do |key, value|
|
232
|
+
mapper[:"#{key}_or_null"] = value + [NilClass]
|
233
|
+
end
|
234
|
+
mapper
|
235
|
+
end
|
236
|
+
|
237
|
+
def match_none?
|
238
|
+
!match_actual? && !match_expected?
|
239
|
+
end
|
240
|
+
|
241
|
+
def match_actual?
|
242
|
+
RSpec.configuration.airborne_match_actual?
|
243
|
+
end
|
244
|
+
|
245
|
+
def match_expected?
|
246
|
+
RSpec.configuration.airborne_match_expected?
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Airborne
|
4
|
+
module Helpers
|
5
|
+
def response
|
6
|
+
@response
|
7
|
+
end
|
8
|
+
|
9
|
+
def headers
|
10
|
+
response
|
11
|
+
.headers
|
12
|
+
.transform_keys { |k| k.to_s.underscore.to_sym }
|
13
|
+
.with_indifferent_access
|
14
|
+
end
|
15
|
+
|
16
|
+
def body
|
17
|
+
response.body
|
18
|
+
end
|
19
|
+
|
20
|
+
def json_body
|
21
|
+
JSON.parse(response.body, symbolize_names: true)
|
22
|
+
rescue StandardError
|
23
|
+
raise InvalidJsonError, "API request returned invalid json"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Airborne
|
4
|
+
class PathMatcher
|
5
|
+
ALL = "?"
|
6
|
+
ANY = "*"
|
7
|
+
|
8
|
+
def self.call(...)
|
9
|
+
new.call(...)
|
10
|
+
end
|
11
|
+
|
12
|
+
# rubocop:todo Metrics/AbcSize,Metrics/MethodLength
|
13
|
+
def call(path, json, &)
|
14
|
+
raise PathError, "Invalid Path, contains '..'" if path.include?("..")
|
15
|
+
|
16
|
+
type = false
|
17
|
+
parts = path.split(".")
|
18
|
+
parts.each_with_index do |part, index|
|
19
|
+
if [ALL, ANY].include?(part)
|
20
|
+
ensure_array(path, json)
|
21
|
+
type = part
|
22
|
+
|
23
|
+
if index < parts.length.pred
|
24
|
+
walk_with_path(type, index, path, parts, json, &)
|
25
|
+
return # rubocop:todo Lint/NonLocalExitFromIterator
|
26
|
+
end
|
27
|
+
|
28
|
+
next
|
29
|
+
end
|
30
|
+
|
31
|
+
json = process_json(part, json)
|
32
|
+
end
|
33
|
+
|
34
|
+
if type == ANY
|
35
|
+
expect_all(json, &)
|
36
|
+
elsif type == ALL
|
37
|
+
expect_one(path, json, &)
|
38
|
+
else
|
39
|
+
yield json
|
40
|
+
end
|
41
|
+
end
|
42
|
+
# rubocop:enable Metrics/AbcSize,Metrics/MethodLength
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# rubocop:todo Metrics/MethodLength
|
47
|
+
def walk_with_path(type, index, path, parts, json, &)
|
48
|
+
last_error = nil
|
49
|
+
item_count = json.length
|
50
|
+
error_count = 0
|
51
|
+
json.each do |element|
|
52
|
+
begin
|
53
|
+
sub_path = parts[(index.next)...(parts.length)].join(".")
|
54
|
+
self.class.call(sub_path, element, &)
|
55
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
56
|
+
last_error = e
|
57
|
+
error_count += 1
|
58
|
+
end
|
59
|
+
|
60
|
+
ensure_match_all(last_error) if type == "*"
|
61
|
+
ensure_match_one(path, item_count, error_count) if type == "?"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
# rubocop:enable Metrics/MethodLength
|
65
|
+
|
66
|
+
def process_json(part, json)
|
67
|
+
if index?(part) && json.is_a?(Array)
|
68
|
+
json[part.to_i]
|
69
|
+
else
|
70
|
+
json[part.to_sym]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def index?(part)
|
75
|
+
part =~ /^\d+$/
|
76
|
+
end
|
77
|
+
|
78
|
+
def expect_one(path, json)
|
79
|
+
item_count = json.length
|
80
|
+
error_count = 0
|
81
|
+
json.each do |part|
|
82
|
+
yield part
|
83
|
+
rescue RSpec::Expectations::ExpectationNotMetError
|
84
|
+
error_count += 1
|
85
|
+
ensure_match_one(path, item_count, error_count)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def expect_all(json, &)
|
90
|
+
last_error = nil
|
91
|
+
begin
|
92
|
+
json.each(&)
|
93
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
94
|
+
last_error = e
|
95
|
+
end
|
96
|
+
ensure_match_all(last_error)
|
97
|
+
end
|
98
|
+
|
99
|
+
def ensure_match_one(path, item_count, error_count)
|
100
|
+
return unless item_count == error_count
|
101
|
+
|
102
|
+
raise RSpec::Expectations::ExpectationNotMetError,
|
103
|
+
"Expected one object in path #{path} to match provided JSON values"
|
104
|
+
end
|
105
|
+
|
106
|
+
def ensure_match_all(error)
|
107
|
+
raise error unless error.nil?
|
108
|
+
end
|
109
|
+
|
110
|
+
def ensure_array(path, json)
|
111
|
+
return if json.is_a?(Array)
|
112
|
+
|
113
|
+
raise RSpec::Expectations::ExpectationNotMetError,
|
114
|
+
"Expected #{path} to be array got #{json.class} from JSON response"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/airborne.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "airborne/path_matcher"
|
4
|
+
require "airborne/helpers"
|
5
|
+
require "airborne/expectations"
|
6
|
+
|
7
|
+
module Airborne
|
8
|
+
class InvalidJsonError < StandardError; end
|
9
|
+
class PathError < StandardError; end
|
10
|
+
class ExpectationError < StandardError; end
|
11
|
+
end
|
12
|
+
|
13
|
+
RSpec.configure do |config|
|
14
|
+
config.add_setting :airborne_match_expected
|
15
|
+
config.add_setting :airborne_match_actual
|
16
|
+
|
17
|
+
config.before do |e|
|
18
|
+
config.airborne_match_expected =
|
19
|
+
e.metadata[:airborne_match_expected].nil? ? true : e.metadata[:airborne_match_expected]
|
20
|
+
config.airborne_match_actual =
|
21
|
+
e.metadata[:airborne_match_actual].nil? ? false : e.metadata[:airborne_match_actual]
|
22
|
+
end
|
23
|
+
|
24
|
+
config.include Airborne::Helpers
|
25
|
+
config.include Airborne::Expectations
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: airborne-rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.99.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Placewise Devs
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-03-26 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rspec
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: rspec-rails
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
email:
|
41
|
+
- mpc.dev@placewise.com
|
42
|
+
executables: []
|
43
|
+
extensions: []
|
44
|
+
extra_rdoc_files: []
|
45
|
+
files:
|
46
|
+
- lib/airborne.rb
|
47
|
+
- lib/airborne/expectations.rb
|
48
|
+
- lib/airborne/helpers.rb
|
49
|
+
- lib/airborne/path_matcher.rb
|
50
|
+
- lib/airborne/version.rb
|
51
|
+
homepage: https://github.com/Placewise/airborne-rails
|
52
|
+
licenses:
|
53
|
+
- MIT
|
54
|
+
metadata: {}
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 3.0.0
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubygems_version: 3.6.2
|
70
|
+
specification_version: 4
|
71
|
+
summary: RSpec helpers and expectations for Rails-based JSON APIs - extracted from
|
72
|
+
airborne
|
73
|
+
test_files: []
|