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.
- checksums.yaml +4 -4
- data/README.md +71 -24
- data/bin/webspicy +30 -14
- data/examples/restful/Gemfile +2 -2
- data/examples/restful/Gemfile.lock +53 -33
- data/examples/restful/Rakefile +0 -1
- data/examples/restful/app.rb +4 -1
- data/examples/restful/webspicy/config.rb +8 -0
- data/examples/restful/webspicy/rack.rb +1 -1
- data/examples/restful/webspicy/real.rb +1 -1
- data/examples/restful/webspicy/schema.fio +2 -2
- data/examples/restful/webspicy/support/must_be_authenticated.rb +2 -2
- data/examples/restful/webspicy/support/todo_removed.rb +18 -0
- data/examples/restful/webspicy/todo/deleteTodo.yml +4 -1
- data/examples/restful/webspicy/todo/getTodoSingleServiceFormat.yml +46 -0
- data/examples/restful/webspicy/todo/options.yml +1 -1
- data/examples/restful/webspicy/todo/patchTodo.yml +3 -0
- data/examples/restful/webspicy/todo/postFile.yml +1 -1
- data/examples/single_spec/spec.yml +59 -0
- data/examples/website/config.rb +2 -0
- data/examples/website/schema.fio +1 -0
- data/examples/website/specification/get-http.yml +30 -0
- data/examples/website/specification/get-https.yml +30 -0
- data/lib/finitio/webspicy/scalars.fio +25 -0
- data/lib/webspicy.rb +48 -17
- data/lib/webspicy/checker.rb +2 -2
- data/lib/webspicy/configuration.rb +70 -14
- data/lib/webspicy/configuration/scope.rb +162 -0
- data/lib/webspicy/configuration/single_url.rb +58 -0
- data/lib/webspicy/configuration/single_yml_file.rb +30 -0
- data/lib/webspicy/formaldoc.fio +23 -8
- data/lib/webspicy/mocker.rb +8 -8
- data/lib/webspicy/mocker/config.ru +5 -0
- data/lib/webspicy/openapi.rb +1 -0
- data/lib/webspicy/openapi/generator.rb +127 -0
- data/lib/webspicy/{resource.rb → specification.rb} +28 -5
- data/lib/webspicy/specification/file_upload.rb +37 -0
- data/lib/webspicy/specification/postcondition.rb +16 -0
- data/lib/webspicy/specification/precondition.rb +19 -0
- data/lib/webspicy/specification/precondition/global_request_headers.rb +35 -0
- data/lib/webspicy/specification/precondition/robust_to_invalid_input.rb +68 -0
- data/lib/webspicy/{resource → specification}/service.rb +11 -6
- data/lib/webspicy/specification/test_case.rb +139 -0
- data/lib/webspicy/support.rb +1 -0
- data/lib/webspicy/support/colorize.rb +28 -0
- data/lib/webspicy/support/status_range.rb +6 -1
- data/lib/webspicy/tester.rb +16 -11
- data/lib/webspicy/tester/asserter.rb +3 -2
- data/lib/webspicy/tester/assertions.rb +5 -1
- data/lib/webspicy/tester/client.rb +63 -0
- data/lib/webspicy/tester/client/http_client.rb +154 -0
- data/lib/webspicy/tester/client/rack_test_client.rb +188 -0
- data/lib/webspicy/tester/client/support.rb +65 -0
- data/lib/webspicy/tester/invocation.rb +218 -0
- data/lib/webspicy/tester/rspec_asserter.rb +108 -0
- data/lib/webspicy/tester/rspec_matchers.rb +104 -0
- data/lib/webspicy/version.rb +2 -2
- data/spec/{unit/spec_helper.rb → spec_helper.rb} +0 -0
- data/spec/unit/configuration/scope/test_each_service.rb +49 -0
- data/spec/unit/configuration/scope/test_each_specification.rb +68 -0
- data/spec/unit/configuration/scope/test_expand_example.rb +65 -0
- data/spec/unit/configuration/scope/test_to_real_url.rb +82 -0
- data/spec/unit/openapi/test_generator.rb +28 -0
- data/spec/unit/specification/precondition/test_global_request_headers.rb +42 -0
- data/spec/unit/{resource → specification}/service/test_dress_params.rb +2 -2
- data/spec/unit/specification/test_case/test_mutate.rb +24 -0
- data/spec/unit/{resource → specification}/test_instantiate_url.rb +5 -5
- data/spec/unit/{resource → specification}/test_url_placeholders.rb +4 -4
- data/spec/unit/test_configuration.rb +24 -7
- data/spec/unit/tester/client/test_around.rb +61 -0
- data/spec/unit/tester/test_asserter.rb +51 -0
- data/spec/unit/tester/test_assertions.rb +4 -4
- data/tasks/test.rake +3 -1
- metadata +88 -45
- data/LICENSE.md +0 -22
- data/lib/webspicy/client.rb +0 -61
- data/lib/webspicy/client/http_client.rb +0 -145
- data/lib/webspicy/client/rack_test_client.rb +0 -181
- data/lib/webspicy/client/support.rb +0 -48
- data/lib/webspicy/file_upload.rb +0 -35
- data/lib/webspicy/postcondition.rb +0 -14
- data/lib/webspicy/precondition.rb +0 -15
- data/lib/webspicy/resource/service/invocation.rb +0 -212
- data/lib/webspicy/resource/service/test_case.rb +0 -132
- data/lib/webspicy/scope.rb +0 -160
- data/spec/unit/client/test_around.rb +0 -59
- data/spec/unit/scope/test_each_resource.rb +0 -66
- data/spec/unit/scope/test_each_service.rb +0 -47
- data/spec/unit/scope/test_expand_example.rb +0 -63
- data/spec/unit/scope/test_to_real_url.rb +0 -80
@@ -0,0 +1,162 @@
|
|
1
|
+
module Webspicy
|
2
|
+
class Configuration
|
3
|
+
class Scope
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
@config = config
|
7
|
+
end
|
8
|
+
attr_reader :config
|
9
|
+
|
10
|
+
def preconditions
|
11
|
+
config.preconditions
|
12
|
+
end
|
13
|
+
|
14
|
+
def postconditions
|
15
|
+
config.postconditions
|
16
|
+
end
|
17
|
+
|
18
|
+
###
|
19
|
+
### Eachers -- Allow navigating the web service definitions
|
20
|
+
###
|
21
|
+
|
22
|
+
# Yields each specification file in the current scope
|
23
|
+
def each_specification_file(&bl)
|
24
|
+
return enum_for(:each_specification_file) unless block_given?
|
25
|
+
_each_specification_file(config, &bl)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Recursive implementation of `each_specification_file` for each
|
29
|
+
# folder in the configuration.
|
30
|
+
def _each_specification_file(config)
|
31
|
+
folder = config.folder
|
32
|
+
folder.glob("**/*.yml").select(&to_filter_proc(config.file_filter)).each do |file|
|
33
|
+
yield file, folder
|
34
|
+
end
|
35
|
+
end
|
36
|
+
private :_each_specification_file
|
37
|
+
|
38
|
+
# Yields each specification in the current scope in turn.
|
39
|
+
def each_specification(&bl)
|
40
|
+
return enum_for(:each_specification) unless block_given?
|
41
|
+
each_specification_file do |file, folder|
|
42
|
+
yield Webspicy.specification(file.load, file, self)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def each_service(specification, &bl)
|
47
|
+
specification.services.select(&to_filter_proc(config.service_filter)).each(&bl)
|
48
|
+
end
|
49
|
+
|
50
|
+
def each_example(service, &bl)
|
51
|
+
service.examples
|
52
|
+
.map{|e| expand_example(service, e) }
|
53
|
+
.select(&to_filter_proc(config.test_case_filter))
|
54
|
+
.each(&bl) if config.run_examples?
|
55
|
+
end
|
56
|
+
|
57
|
+
def each_counterexamples(service, &bl)
|
58
|
+
service.counterexamples
|
59
|
+
.map{|e| expand_example(service, e) }
|
60
|
+
.select(&to_filter_proc(config.test_case_filter))
|
61
|
+
.each(&bl) if config.run_counterexamples?
|
62
|
+
end
|
63
|
+
|
64
|
+
def each_generated_counterexamples(service, &bl)
|
65
|
+
Webspicy.with_scope(self) do
|
66
|
+
service.generated_counterexamples
|
67
|
+
.map{|e| expand_example(service, e) }
|
68
|
+
.select(&to_filter_proc(config.test_case_filter))
|
69
|
+
.each(&bl) if config.run_generated_counterexamples?
|
70
|
+
end if config.run_generated_counterexamples?
|
71
|
+
end
|
72
|
+
|
73
|
+
def each_testcase(service, &bl)
|
74
|
+
each_example(service, &bl)
|
75
|
+
each_counterexamples(service, &bl)
|
76
|
+
each_generated_counterexamples(service, &bl)
|
77
|
+
end
|
78
|
+
|
79
|
+
###
|
80
|
+
### Schemas -- For parsing input and output data schemas found in
|
81
|
+
### web service definitions
|
82
|
+
###
|
83
|
+
|
84
|
+
# Parses a Finitio schema based on the data system.
|
85
|
+
def parse_schema(fio)
|
86
|
+
data_system.parse(fio)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns the Data system to use for parsing schemas
|
90
|
+
def data_system
|
91
|
+
@data_system ||= config.data_system
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
###
|
96
|
+
### Service invocation: abstract the configuration about what client is
|
97
|
+
### used and how to instantiate it
|
98
|
+
###
|
99
|
+
|
100
|
+
# Returns an instance of the client to use to invoke web services
|
101
|
+
def get_client
|
102
|
+
config.client.new(self)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Convert an instantiated URL found in a webservice definition
|
106
|
+
# to a real URL, using the configuration host.
|
107
|
+
#
|
108
|
+
# When no host resolved on the configuration and the url is not
|
109
|
+
# already an absolute URL, yields the block if given, or raise
|
110
|
+
# an exception.
|
111
|
+
def to_real_url(url, test_case = nil, &bl)
|
112
|
+
case config.host
|
113
|
+
when Proc
|
114
|
+
config.host.call(url, test_case)
|
115
|
+
when String
|
116
|
+
url =~ /^http/ ? url : "#{config.host}#{url}"
|
117
|
+
else
|
118
|
+
return url if url =~ /^http/
|
119
|
+
return yield(url) if block_given?
|
120
|
+
raise "Unable to resolve `#{url}` : no host resolver provided\nSee `Configuration#host="
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
###
|
125
|
+
### Private methods
|
126
|
+
###
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def expand_example(service, example)
|
131
|
+
return example unless service.default_example
|
132
|
+
h1 = service.default_example.to_info
|
133
|
+
h2 = example.to_info
|
134
|
+
ex = Specification::TestCase.new(merge_maps(h1, h2))
|
135
|
+
ex.bind(service, example.counterexample?)
|
136
|
+
end
|
137
|
+
|
138
|
+
def merge_maps(h1, h2)
|
139
|
+
h1.merge(h2) do |k,v1,v2|
|
140
|
+
case v1
|
141
|
+
when Hash then merge_maps(v1, v2)
|
142
|
+
when Array then v1 + v2
|
143
|
+
else v2
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Returns a proc that implements file_filter strategy according to the
|
149
|
+
# type of filter installed
|
150
|
+
def to_filter_proc(filter)
|
151
|
+
case ff = filter
|
152
|
+
when NilClass then ->(f){ true }
|
153
|
+
when Proc then ff
|
154
|
+
when Regexp then ->(f){ ff =~ f.to_s }
|
155
|
+
else
|
156
|
+
->(f){ ff === f }
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
end # class Scope
|
161
|
+
end # class Configuration
|
162
|
+
end # module Webspicy
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Webspicy
|
2
|
+
class Configuration
|
3
|
+
class SingleUrl
|
4
|
+
|
5
|
+
class SingleUrlScope < Scope
|
6
|
+
|
7
|
+
def initialize(config, url)
|
8
|
+
super(config)
|
9
|
+
@url = url
|
10
|
+
end
|
11
|
+
attr_reader :url
|
12
|
+
|
13
|
+
def each_specification(&bl)
|
14
|
+
return enum_for(:each_specification) unless block_given?
|
15
|
+
spec = <<~YML
|
16
|
+
---
|
17
|
+
name: |-
|
18
|
+
Default specification
|
19
|
+
url: |-
|
20
|
+
#{url}
|
21
|
+
|
22
|
+
services:
|
23
|
+
- method: |-
|
24
|
+
GET
|
25
|
+
description: |-
|
26
|
+
Getting #{url}
|
27
|
+
|
28
|
+
input_schema: |-
|
29
|
+
Any
|
30
|
+
output_schema: |-
|
31
|
+
Any
|
32
|
+
error_schema: |-
|
33
|
+
Any
|
34
|
+
|
35
|
+
examples:
|
36
|
+
- description: |-
|
37
|
+
it returns a 200
|
38
|
+
params: {}
|
39
|
+
expected:
|
40
|
+
status: 200
|
41
|
+
YML
|
42
|
+
Webspicy.debug(spec)
|
43
|
+
yield Webspicy.specification(spec, nil, self)
|
44
|
+
end
|
45
|
+
|
46
|
+
end # class SingleUrlScope
|
47
|
+
|
48
|
+
def initialize(url)
|
49
|
+
@url = url
|
50
|
+
end
|
51
|
+
|
52
|
+
def call(config)
|
53
|
+
SingleUrlScope.new(config, @url)
|
54
|
+
end
|
55
|
+
|
56
|
+
end # class SingleUrl
|
57
|
+
end # class Configuration
|
58
|
+
end # module Webspicy
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Webspicy
|
2
|
+
class Configuration
|
3
|
+
class SingleYmlFile
|
4
|
+
|
5
|
+
class SingleYmlFileScope < Scope
|
6
|
+
|
7
|
+
def initialize(config, file)
|
8
|
+
super(config)
|
9
|
+
@file = file
|
10
|
+
end
|
11
|
+
attr_reader :file
|
12
|
+
|
13
|
+
def each_specification(&bl)
|
14
|
+
return enum_for(:each_specification) unless block_given?
|
15
|
+
yield Webspicy.specification(file.read, nil, self)
|
16
|
+
end
|
17
|
+
|
18
|
+
end # class SingleYmlFileScope
|
19
|
+
|
20
|
+
def initialize(file)
|
21
|
+
@file = file
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(config)
|
25
|
+
SingleYmlFileScope.new(config, @file)
|
26
|
+
end
|
27
|
+
|
28
|
+
end # class SingleYmlFile
|
29
|
+
end # class Configuration
|
30
|
+
end # module Webspicy
|
data/lib/webspicy/formaldoc.fio
CHANGED
@@ -17,15 +17,30 @@ FileUpload =
|
|
17
17
|
param_name :? String
|
18
18
|
}
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
20
|
+
Specification = .Webspicy::Specification
|
21
|
+
<singleservice> {
|
22
|
+
name :? String
|
23
|
+
url : String
|
24
|
+
method : Method
|
25
|
+
description : String
|
26
|
+
preconditions :? [String]|String
|
27
|
+
postconditions :? [String]|String
|
28
|
+
input_schema : Schema
|
29
|
+
output_schema : Schema
|
30
|
+
error_schema : Schema
|
31
|
+
blackbox :? String
|
32
|
+
default_example :? TestCase
|
33
|
+
examples :? [TestCase]
|
34
|
+
counterexamples :? [TestCase]
|
35
|
+
}
|
36
|
+
<info> {
|
37
|
+
name: String
|
38
|
+
url: String
|
39
|
+
services: [Service]
|
40
|
+
}
|
26
41
|
|
27
42
|
Service =
|
28
|
-
.Webspicy::
|
43
|
+
.Webspicy::Specification::Service <info> {
|
29
44
|
method : Method
|
30
45
|
description : String
|
31
46
|
preconditions :? [String]|String
|
@@ -40,7 +55,7 @@ Service =
|
|
40
55
|
}
|
41
56
|
|
42
57
|
TestCase =
|
43
|
-
.Webspicy::
|
58
|
+
.Webspicy::Specification::TestCase <info> {
|
44
59
|
description :? String
|
45
60
|
dress_params :? Boolean
|
46
61
|
params :? Params
|
data/lib/webspicy/mocker.rb
CHANGED
@@ -29,8 +29,8 @@ module Webspicy
|
|
29
29
|
|
30
30
|
def has_service?(path)
|
31
31
|
config.each_scope do |scope|
|
32
|
-
scope.
|
33
|
-
next unless url_matches?(
|
32
|
+
scope.each_specification do |specification|
|
33
|
+
next unless url_matches?(specification, path)
|
34
34
|
return true
|
35
35
|
end
|
36
36
|
end
|
@@ -39,9 +39,9 @@ module Webspicy
|
|
39
39
|
|
40
40
|
def find_service(method, path)
|
41
41
|
config.each_scope do |scope|
|
42
|
-
scope.
|
43
|
-
next unless url_matches?(
|
44
|
-
scope.each_service(
|
42
|
+
scope.each_specification do |specification|
|
43
|
+
next unless url_matches?(specification, path)
|
44
|
+
scope.each_service(specification) do |service|
|
45
45
|
return service if service.method == method
|
46
46
|
end
|
47
47
|
end
|
@@ -71,8 +71,8 @@ module Webspicy
|
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
74
|
-
def url_matches?(
|
75
|
-
|
74
|
+
def url_matches?(specification, path)
|
75
|
+
specification.url_pattern.match(path)
|
76
76
|
end
|
77
77
|
|
78
78
|
def random_body(service, request)
|
@@ -85,4 +85,4 @@ module Webspicy
|
|
85
85
|
end
|
86
86
|
|
87
87
|
end # class Mocker
|
88
|
-
end # module Webspicy
|
88
|
+
end # module Webspicy
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'openapi/generator'
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require "finitio/generation"
|
2
|
+
require "finitio/json_schema"
|
3
|
+
module Webspicy
|
4
|
+
module Openapi
|
5
|
+
class Generator
|
6
|
+
|
7
|
+
def initialize(config)
|
8
|
+
@config = Configuration.dress(config)
|
9
|
+
@generator = config.generator || Finitio::Generation.new
|
10
|
+
end
|
11
|
+
attr_reader :config, :generator
|
12
|
+
|
13
|
+
def call
|
14
|
+
{
|
15
|
+
openapi: "3.0.2",
|
16
|
+
info: {
|
17
|
+
version: "1.0.0",
|
18
|
+
title: "Hello API"
|
19
|
+
},
|
20
|
+
paths: paths
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def paths
|
27
|
+
config.each_scope.inject({}) do |paths,scope|
|
28
|
+
scope.each_specification.inject(paths) do |paths,specification|
|
29
|
+
paths.merge(path_for(specification)){|k,ps,qs|
|
30
|
+
ps.merge(qs)
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def path_for(specification)
|
37
|
+
{
|
38
|
+
specification.url => {
|
39
|
+
summary: specification.name
|
40
|
+
}.merge(verbs_for(specification))
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def verbs_for(specification)
|
45
|
+
specification.services.inject({}) do |verbs,service|
|
46
|
+
verb = service.method.downcase
|
47
|
+
verb_defn = {
|
48
|
+
description: service.description,
|
49
|
+
responses: responses_for(service)
|
50
|
+
}
|
51
|
+
unless ["get", "options", "delete", "head"].include?(verb)
|
52
|
+
verb_defn[:requestBody] = request_body_for(service)
|
53
|
+
end
|
54
|
+
verbs.merge({ verb => verb_defn })
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def request_body_for(service)
|
59
|
+
schema = actual_input_schema(service)
|
60
|
+
{
|
61
|
+
required: true,
|
62
|
+
content: {
|
63
|
+
"application/json" => {
|
64
|
+
schema: schema.to_json_schema,
|
65
|
+
example: generator.call(schema, {})
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def responses_for(service)
|
72
|
+
result = {}
|
73
|
+
service.examples.each_with_object(result) do |test_case, rs|
|
74
|
+
rs.merge!(response_for(test_case, false)){|k,r1,r2| r1 }
|
75
|
+
end
|
76
|
+
service.counterexamples.each_with_object(result) do |test_case, rs|
|
77
|
+
rs.merge!(response_for(test_case, true)){|k,r1,r2| r1 }
|
78
|
+
end
|
79
|
+
result
|
80
|
+
end
|
81
|
+
|
82
|
+
def response_for(test_case, counterexample)
|
83
|
+
res = {
|
84
|
+
description: test_case.description,
|
85
|
+
}
|
86
|
+
status = (test_case.expected_status && test_case.expected_status.to_int) || 200
|
87
|
+
if test_case.expected_content_type && status != 204
|
88
|
+
content = {
|
89
|
+
schema: schema_for(test_case, counterexample)
|
90
|
+
}
|
91
|
+
unless counterexample
|
92
|
+
content[:example] = example_for(test_case, counterexample)
|
93
|
+
end
|
94
|
+
res[:content] = {
|
95
|
+
test_case.expected_content_type => content
|
96
|
+
}
|
97
|
+
end
|
98
|
+
{
|
99
|
+
status => res
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def schema_for(test_case, counterexample)
|
104
|
+
schema = actual_output_schema(test_case, counterexample)
|
105
|
+
schema.to_json_schema
|
106
|
+
end
|
107
|
+
|
108
|
+
def example_for(test_case, counterexample)
|
109
|
+
schema = actual_output_schema(test_case, counterexample)
|
110
|
+
generator.call(schema, {})
|
111
|
+
end
|
112
|
+
|
113
|
+
def actual_output_schema(test_case, counterexample)
|
114
|
+
if counterexample
|
115
|
+
test_case.service.error_schema['Main']
|
116
|
+
else
|
117
|
+
test_case.service.output_schema['Main']
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def actual_input_schema(service)
|
122
|
+
service.input_schema['Main']
|
123
|
+
end
|
124
|
+
|
125
|
+
end # class Generator
|
126
|
+
end # module Openapi
|
127
|
+
end # module Webspicy
|