webspicy 0.15.7 → 0.16.3

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +71 -24
  3. data/bin/webspicy +30 -14
  4. data/examples/restful/Gemfile +2 -2
  5. data/examples/restful/Gemfile.lock +53 -33
  6. data/examples/restful/Rakefile +0 -1
  7. data/examples/restful/app.rb +4 -1
  8. data/examples/restful/webspicy/config.rb +8 -0
  9. data/examples/restful/webspicy/rack.rb +1 -1
  10. data/examples/restful/webspicy/real.rb +1 -1
  11. data/examples/restful/webspicy/schema.fio +2 -2
  12. data/examples/restful/webspicy/support/must_be_authenticated.rb +2 -2
  13. data/examples/restful/webspicy/support/todo_removed.rb +18 -0
  14. data/examples/restful/webspicy/todo/deleteTodo.yml +4 -1
  15. data/examples/restful/webspicy/todo/getTodoSingleServiceFormat.yml +46 -0
  16. data/examples/restful/webspicy/todo/options.yml +1 -1
  17. data/examples/restful/webspicy/todo/patchTodo.yml +3 -0
  18. data/examples/restful/webspicy/todo/postFile.yml +1 -1
  19. data/examples/single_spec/spec.yml +59 -0
  20. data/examples/website/config.rb +2 -0
  21. data/examples/website/schema.fio +1 -0
  22. data/examples/website/specification/get-http.yml +30 -0
  23. data/examples/website/specification/get-https.yml +30 -0
  24. data/lib/finitio/webspicy/scalars.fio +25 -0
  25. data/lib/webspicy.rb +48 -17
  26. data/lib/webspicy/checker.rb +2 -2
  27. data/lib/webspicy/configuration.rb +70 -14
  28. data/lib/webspicy/configuration/scope.rb +162 -0
  29. data/lib/webspicy/configuration/single_url.rb +58 -0
  30. data/lib/webspicy/configuration/single_yml_file.rb +30 -0
  31. data/lib/webspicy/formaldoc.fio +23 -8
  32. data/lib/webspicy/mocker.rb +8 -8
  33. data/lib/webspicy/mocker/config.ru +5 -0
  34. data/lib/webspicy/openapi.rb +1 -0
  35. data/lib/webspicy/openapi/generator.rb +127 -0
  36. data/lib/webspicy/{resource.rb → specification.rb} +28 -5
  37. data/lib/webspicy/specification/file_upload.rb +37 -0
  38. data/lib/webspicy/specification/postcondition.rb +16 -0
  39. data/lib/webspicy/specification/precondition.rb +19 -0
  40. data/lib/webspicy/specification/precondition/global_request_headers.rb +35 -0
  41. data/lib/webspicy/specification/precondition/robust_to_invalid_input.rb +68 -0
  42. data/lib/webspicy/{resource → specification}/service.rb +11 -6
  43. data/lib/webspicy/specification/test_case.rb +139 -0
  44. data/lib/webspicy/support.rb +1 -0
  45. data/lib/webspicy/support/colorize.rb +28 -0
  46. data/lib/webspicy/support/status_range.rb +6 -1
  47. data/lib/webspicy/tester.rb +16 -11
  48. data/lib/webspicy/tester/asserter.rb +3 -2
  49. data/lib/webspicy/tester/assertions.rb +5 -1
  50. data/lib/webspicy/tester/client.rb +63 -0
  51. data/lib/webspicy/tester/client/http_client.rb +154 -0
  52. data/lib/webspicy/tester/client/rack_test_client.rb +188 -0
  53. data/lib/webspicy/tester/client/support.rb +65 -0
  54. data/lib/webspicy/tester/invocation.rb +218 -0
  55. data/lib/webspicy/tester/rspec_asserter.rb +108 -0
  56. data/lib/webspicy/tester/rspec_matchers.rb +104 -0
  57. data/lib/webspicy/version.rb +2 -2
  58. data/spec/{unit/spec_helper.rb → spec_helper.rb} +0 -0
  59. data/spec/unit/configuration/scope/test_each_service.rb +49 -0
  60. data/spec/unit/configuration/scope/test_each_specification.rb +68 -0
  61. data/spec/unit/configuration/scope/test_expand_example.rb +65 -0
  62. data/spec/unit/configuration/scope/test_to_real_url.rb +82 -0
  63. data/spec/unit/openapi/test_generator.rb +28 -0
  64. data/spec/unit/specification/precondition/test_global_request_headers.rb +42 -0
  65. data/spec/unit/{resource → specification}/service/test_dress_params.rb +2 -2
  66. data/spec/unit/specification/test_case/test_mutate.rb +24 -0
  67. data/spec/unit/{resource → specification}/test_instantiate_url.rb +5 -5
  68. data/spec/unit/{resource → specification}/test_url_placeholders.rb +4 -4
  69. data/spec/unit/test_configuration.rb +24 -7
  70. data/spec/unit/tester/client/test_around.rb +61 -0
  71. data/spec/unit/tester/test_asserter.rb +51 -0
  72. data/spec/unit/tester/test_assertions.rb +4 -4
  73. data/tasks/test.rake +3 -1
  74. metadata +88 -45
  75. data/LICENSE.md +0 -22
  76. data/lib/webspicy/client.rb +0 -61
  77. data/lib/webspicy/client/http_client.rb +0 -145
  78. data/lib/webspicy/client/rack_test_client.rb +0 -181
  79. data/lib/webspicy/client/support.rb +0 -48
  80. data/lib/webspicy/file_upload.rb +0 -35
  81. data/lib/webspicy/postcondition.rb +0 -14
  82. data/lib/webspicy/precondition.rb +0 -15
  83. data/lib/webspicy/resource/service/invocation.rb +0 -212
  84. data/lib/webspicy/resource/service/test_case.rb +0 -132
  85. data/lib/webspicy/scope.rb +0 -160
  86. data/spec/unit/client/test_around.rb +0 -59
  87. data/spec/unit/scope/test_each_resource.rb +0 -66
  88. data/spec/unit/scope/test_each_service.rb +0 -47
  89. data/spec/unit/scope/test_expand_example.rb +0 -63
  90. data/spec/unit/scope/test_to_real_url.rb +0 -80
@@ -0,0 +1,65 @@
1
+ module Webspicy
2
+ class Tester
3
+ class Client
4
+ module Support
5
+ include Webspicy::Support::Colorize
6
+
7
+ NONE = Object.new
8
+
9
+ def querystring_params(params)
10
+ Hash[params.each_pair.map{|k,v| [k.to_s,v.to_s] }]
11
+ end
12
+
13
+ def info_request(kind, url, params, headers, body)
14
+ Webspicy.info(colorize_highlight("~> #{kind} #{url}"))
15
+ debug(" Req params", json_pretty(params)) if params
16
+ debug(" Req headers", json_pretty(headers)) if headers
17
+ debug(" Req body", request_body_to_s(body)) if body
18
+ end
19
+
20
+ def debug_response(response)
21
+ debug(colorize_highlight("."))
22
+ debug(" Res status", status_to_s(@last_response.status))
23
+ debug(" Res headers", json_pretty(last_response.headers.to_h))
24
+ debug(" Res body", response_body_to_s(last_response))
25
+ Webspicy.debug("")
26
+ end
27
+
28
+ def debug(what, value = NONE)
29
+ return Webspicy.debug(" #{what}") if value == NONE
30
+ Webspicy.debug(" #{what}: " + value_to_s(value))
31
+ end
32
+
33
+ def request_body_to_s(body)
34
+ body = body.to_info if body.is_a?(Webspicy::FileUpload)
35
+ json_pretty(body)
36
+ end
37
+
38
+ def response_body_to_s(response)
39
+ case response.content_type.to_s
40
+ when /json/
41
+ json_pretty(JSON.load(response.body))
42
+ else
43
+ response.body.to_s
44
+ end
45
+ end
46
+
47
+ def value_to_s(value)
48
+ value.to_s.gsub(/\n/, "\n ")
49
+ end
50
+
51
+ def status_to_s(status)
52
+ case status
53
+ when 0 ... 400 then colorize_success(status.to_s)
54
+ else colorize_error(status.to_s)
55
+ end
56
+ end
57
+
58
+ def json_pretty(s)
59
+ JSON.pretty_generate(s)
60
+ end
61
+
62
+ end # module Support
63
+ end # class Client
64
+ end # class Tester
65
+ end # module Webspicy
@@ -0,0 +1,218 @@
1
+ module Webspicy
2
+ class Tester
3
+ class Invocation
4
+
5
+ def initialize(test_case, response, client)
6
+ @test_case = test_case
7
+ @response = response
8
+ @client = client
9
+ end
10
+
11
+ attr_reader :test_case, :response, :client
12
+
13
+ def service
14
+ test_case.service
15
+ end
16
+
17
+ def rspec_assert!(rspec)
18
+ RSpecAsserter.new(rspec, self).assert!
19
+ end
20
+
21
+ def errors
22
+ @errors ||= begin
23
+ errs = [
24
+ [:expected_status_unmet, true],
25
+ [:expected_content_type_unmet, !test_case.is_expected_status?(204)],
26
+ [:expected_headers_unmet, test_case.has_expected_headers?],
27
+ [:expected_schema_unmet, !test_case.is_expected_status?(204)],
28
+ [:assertions_unmet, test_case.has_assertions?],
29
+ [:postconditions_unmet, test_case.service.has_postconditions? && !test_case.counterexample?],
30
+ [:expected_error_unmet, test_case.has_expected_error?]
31
+ ].map do |(expectation,only_if)|
32
+ next unless only_if
33
+ begin
34
+ self.send(expectation)
35
+ rescue => ex
36
+ ex.message
37
+ end
38
+ end
39
+ errs.compact
40
+ end
41
+ end
42
+
43
+ def has_error?
44
+ !errors.empty?
45
+ end
46
+
47
+ ### Getters on response
48
+
49
+ def response_code
50
+ code = response.status
51
+ code = code.code unless code.is_a?(Integer)
52
+ code
53
+ end
54
+
55
+ ### Query methods
56
+
57
+ def done?
58
+ !response.nil?
59
+ end
60
+
61
+ def is_expected_success?
62
+ test_case.expected_status.to_i >= 200 && test_case.expected_status.to_i < 300
63
+ end
64
+
65
+ def is_success?
66
+ response_code >= 200 && response_code < 300
67
+ end
68
+
69
+ def is_empty_response?
70
+ response_code == 204
71
+ end
72
+
73
+ def is_redirect?
74
+ response_code >= 300 && response_code < 400
75
+ end
76
+
77
+ ### Check of HTTP status
78
+
79
+ def expected_status_unmet
80
+ expected = test_case.expected_status
81
+ got = response.status
82
+ expected === got ? nil : "[status] #{expected} !== #{got}"
83
+ end
84
+
85
+ def meets_expected_status?
86
+ expected_status_unmet.nil?
87
+ end
88
+
89
+ ### Check of the expected output type
90
+
91
+ def expected_content_type_unmet
92
+ ect = test_case.expected_content_type
93
+ return nil unless ect
94
+ got = response.content_type
95
+ got = got.mime_type if got.respond_to?(:mime_type)
96
+ if ect.nil?
97
+ got.nil? ? nil : "[content type] #{ect} != #{got}"
98
+ else
99
+ got.to_s.start_with?(ect.to_s) ? nil : "[content type] #{ect} != #{got}"
100
+ end
101
+ end
102
+
103
+ def meets_expected_content_type?
104
+ expected_content_type_unmet.nil?
105
+ end
106
+
107
+ ### Check of output schema
108
+
109
+ def expected_schema_unmet
110
+ if is_empty_response?
111
+ body = response.body.to_s.strip
112
+ body.empty? ? nil : "[body] empty vs. #{body}"
113
+ elsif is_redirect?
114
+ else
115
+ case dressed_body
116
+ when Finitio::TypeError
117
+ rc = dressed_body.root_cause
118
+ "#{rc.message} (#{rc.location ? rc.location : 'unknown location'})"
119
+ when StandardError
120
+ dressed_body.message
121
+ else nil
122
+ end
123
+ end
124
+ end
125
+
126
+ def meets_expected_schema?
127
+ expected_schema_unmet.nil?
128
+ end
129
+
130
+ ### Check of assertions
131
+
132
+ def assertions_unmet
133
+ unmet = []
134
+ asserter = Tester::Asserter.new(dressed_body)
135
+ test_case.assert.each do |assert|
136
+ begin
137
+ asserter.instance_eval(assert)
138
+ rescue => ex
139
+ unmet << ex.message
140
+ end
141
+ end
142
+ unmet.empty? ? nil : unmet.join("\n")
143
+ end
144
+
145
+ def value_equal(exp, got)
146
+ case exp
147
+ when Hash
148
+ exp.all?{|(k,v)|
149
+ got[k] == v
150
+ }
151
+ else
152
+ exp == got
153
+ end
154
+ end
155
+
156
+ ### Check of expected error message
157
+
158
+ def expected_error_unmet
159
+ expected = test_case.expected_error
160
+ case test_case.expected_content_type
161
+ when %r{json}
162
+ got = meets_expected_schema? ? dressed_body[:description] : response.body
163
+ expected == got ? nil : "[error message] `#{expected}` vs. `#{got}`"
164
+ else
165
+ dressed_body.include?(expected) ? nil : "[error message] `#{expected}` not found" unless expected.nil?
166
+ end
167
+ end
168
+
169
+ ### Check of expected headers
170
+
171
+ def expected_headers_unmet
172
+ unmet = []
173
+ expected = test_case.expected_headers
174
+ expected.each_pair do |k,v|
175
+ got = response.headers[k]
176
+ unmet << "[headers] #{v} expected for #{k}, got #{got}" unless (got == v)
177
+ end
178
+ unmet.empty? ? nil : unmet.join("\n")
179
+ end
180
+
181
+ ### Check of postconditions
182
+
183
+ def postconditions_unmet
184
+ failures = service.postconditions.map{|post|
185
+ post.check(self)
186
+ }.compact
187
+ failures.empty? ? nil : failures.join("\n")
188
+ end
189
+
190
+ def loaded_body
191
+ case test_case.expected_content_type
192
+ when %r{json}
193
+ raise "Body empty while expected" if response.body.to_s.empty?
194
+ @loaded_body ||= ::JSON.parse(response.body)
195
+ else
196
+ response.body.to_s
197
+ end
198
+ end
199
+
200
+ def dressed_body
201
+ @dressed_body ||= case test_case.expected_content_type
202
+ when %r{json}
203
+ schema = is_expected_success? ? service.output_schema : service.error_schema
204
+ begin
205
+ schema.dress(loaded_body)
206
+ rescue Finitio::TypeError => ex
207
+ ex
208
+ end
209
+ else
210
+ loaded_body
211
+ end
212
+ end
213
+
214
+ end # class Invocation
215
+ end # class Tester
216
+ end # module Webspicy
217
+ require_relative 'rspec_matchers'
218
+ require_relative 'rspec_asserter'
@@ -0,0 +1,108 @@
1
+ module Webspicy
2
+ class Tester
3
+ class RSpecAsserter
4
+
5
+ def initialize(rspec, invocation)
6
+ @rspec = rspec
7
+ @invocation = invocation
8
+ end
9
+ attr_reader :rspec, :invocation
10
+
11
+ def response
12
+ invocation.response
13
+ end
14
+
15
+ def test_case
16
+ invocation.test_case
17
+ end
18
+
19
+ def service
20
+ test_case.service
21
+ end
22
+
23
+ def assert!
24
+ assert_status_met
25
+ assert_content_type_met
26
+ assert_expected_headers
27
+ assert_output_schema_met
28
+ assert_assertions_met
29
+ assert_postconditions_met
30
+
31
+ assert_no_other_errors
32
+ end
33
+
34
+ def assert_status_met
35
+ got = response.status
36
+ expected = test_case.expected_status
37
+ rspec.expect(got).to rspec.match_response_status(expected)
38
+ end
39
+
40
+ def assert_content_type_met
41
+ return if test_case.is_expected_status?(204)
42
+ return unless ect = test_case.expected_content_type
43
+ got = response.content_type
44
+ got = got.mime_type if got.respond_to?(:mime_type)
45
+ if ect.nil?
46
+ rspec.expect(ect).to rspec.have_no_response_type
47
+ else
48
+ rspec.expect(got).to rspec.match_content_type(ect)
49
+ end
50
+ end
51
+
52
+ def assert_expected_headers
53
+ return unless test_case.has_expected_headers?
54
+ test_case.expected_headers.each_pair do |k,v|
55
+ got = response.headers[k]
56
+ if got.nil?
57
+ rspec.expect(got).to rspec.be_in_response_headers(k)
58
+ else
59
+ rspec.expect(got).to rspec.match_response_header(k, v)
60
+ end
61
+ end
62
+ end
63
+
64
+ def assert_output_schema_met
65
+ return if test_case.is_expected_status?(204)
66
+ return if invocation.is_redirect?
67
+ if invocation.is_empty_response?
68
+ body = response.body.to_s.strip
69
+ rspec.expect(body).to rspec.be_an_empty_response_body
70
+ else
71
+ b = invocation.dressed_body
72
+ if invocation.is_expected_success?
73
+ rspec.expect(b).to rspec.meet_output_schema
74
+ else
75
+ rspec.expect(b).to rspec.meet_error_schema
76
+ end
77
+ end
78
+ end
79
+
80
+ def assert_assertions_met
81
+ return unless test_case.has_assertions?
82
+ asserter = Tester::Asserter.new(invocation.dressed_body)
83
+ test_case.assert.each do |assert|
84
+ begin
85
+ asserter.instance_eval(assert)
86
+ rescue => ex
87
+ rspec.expect(ex).to rspec.meet_assertion(assert)
88
+ end
89
+ end
90
+ end
91
+
92
+ def assert_postconditions_met
93
+ return unless service.has_postconditions?
94
+ return if test_case.counterexample?
95
+ service.postconditions.each do |post|
96
+ msg = post.check(invocation)
97
+ rspec.expect(msg).to rspec.meet_postcondition(post)
98
+ end
99
+ end
100
+
101
+ def assert_no_other_errors
102
+ errors = invocation.errors
103
+ rspec.expect(errors).to rspec.be_an_empty_errors_array
104
+ end
105
+
106
+ end # class RSpecAsserter
107
+ end # class Tester
108
+ end # module Webspicy
@@ -0,0 +1,104 @@
1
+ require 'rspec/expectations'
2
+
3
+ RSpec::Matchers.define :match_response_status do |expected|
4
+ match do |actual|
5
+ expected === actual
6
+ end
7
+ failure_message_for_should do |actual|
8
+ "expected response status #{actual} to be #{expected}"
9
+ end
10
+ end
11
+
12
+ RSpec::Matchers.define :have_no_response_type do
13
+ match do |actual|
14
+ actual.nil?
15
+ end
16
+ failure_message_for_should do |actual|
17
+ "expected Content-Type not to be present"
18
+ end
19
+ end
20
+
21
+ RSpec::Matchers.define :match_content_type do |expected|
22
+ match do |actual|
23
+ actual.to_s.start_with?(expected.to_s)
24
+ end
25
+ failure_message_for_should do |actual|
26
+ "expected Content-Type to be `#{expected}`, got `#{actual}`"
27
+ end
28
+ end
29
+
30
+ RSpec::Matchers.define :be_in_response_headers do |header_name|
31
+ match do |actual|
32
+ !actual.nil?
33
+ end
34
+ failure_message_for_should do |actual|
35
+ "expected response header `#{header_name}` to be set"
36
+ end
37
+ end
38
+
39
+ RSpec::Matchers.define :match_response_header do |header_name, expected|
40
+ match do |actual|
41
+ expected == actual
42
+ end
43
+ failure_message_for_should do |actual|
44
+ "expected response header `#{header_name}` to be `#{expected}`, got `#{actual}`"
45
+ end
46
+ end
47
+
48
+ RSpec::Matchers.define :be_an_empty_response_body do
49
+ match do |actual|
50
+ actual.empty?
51
+ end
52
+ failure_message_for_should do |actual|
53
+ "expected response body to be empty, started with `#{actual[0..20]}`"
54
+ end
55
+ end
56
+
57
+ RSpec::Matchers.define :meet_output_schema do
58
+ match do |actual|
59
+ !actual.is_a?(Exception)
60
+ end
61
+ failure_message_for_should do |actual|
62
+ "expected response body to meet output schema, got following error:\n" + \
63
+ " #{actual.message}"
64
+ end
65
+ end
66
+
67
+ RSpec::Matchers.define :meet_error_schema do
68
+ match do |actual|
69
+ !actual.is_a?(Exception)
70
+ end
71
+ failure_message_for_should do |actual|
72
+ "expected response body to meet error schema, got following error:\n" + \
73
+ " #{actual.message}"
74
+ end
75
+ end
76
+
77
+ RSpec::Matchers.define :meet_assertion do |assert|
78
+ match do |actual|
79
+ actual.nil?
80
+ end
81
+ failure_message_for_should do |actual|
82
+ "expected assertion `#{assert}` to be met, got following error:\n" + \
83
+ " #{actual.message}"
84
+ end
85
+ end
86
+
87
+ RSpec::Matchers.define :meet_postcondition do |post|
88
+ match do |actual|
89
+ actual.nil?
90
+ end
91
+ failure_message_for_should do |actual|
92
+ "expected postcondition `#{post.class.name}` to be met, got following error:\n" + \
93
+ " #{actual}"
94
+ end
95
+ end
96
+
97
+ RSpec::Matchers.define :be_an_empty_errors_array do
98
+ match do |actual|
99
+ actual.empty?
100
+ end
101
+ failure_message_for_should do |actual|
102
+ "expected no webspicy error, got the following ones:\n" + actual.map{|a| " #{a}" }.join("\n")
103
+ end
104
+ end