webspicy 0.1.0.pre.rc1

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 (38) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +2 -0
  3. data/LICENSE.md +22 -0
  4. data/README.md +7 -0
  5. data/Rakefile +11 -0
  6. data/examples/restful/Gemfile +5 -0
  7. data/examples/restful/Gemfile.lock +69 -0
  8. data/examples/restful/Rakefile +21 -0
  9. data/examples/restful/app.rb +32 -0
  10. data/examples/restful/webspicy/schema.fio +4 -0
  11. data/examples/restful/webspicy/todo/getTodo.yml +52 -0
  12. data/examples/restful/webspicy/todo/getTodos.yml +39 -0
  13. data/lib/webspicy/checker.rb +26 -0
  14. data/lib/webspicy/client/http_client.rb +70 -0
  15. data/lib/webspicy/client.rb +20 -0
  16. data/lib/webspicy/configuration.rb +168 -0
  17. data/lib/webspicy/formaldoc.fio +47 -0
  18. data/lib/webspicy/resource/service/invocation.rb +158 -0
  19. data/lib/webspicy/resource/service/test_case.rb +74 -0
  20. data/lib/webspicy/resource/service.rb +49 -0
  21. data/lib/webspicy/resource.rb +43 -0
  22. data/lib/webspicy/scope.rb +113 -0
  23. data/lib/webspicy/tester/asserter.rb +94 -0
  24. data/lib/webspicy/tester/assertions.rb +103 -0
  25. data/lib/webspicy/tester.rb +96 -0
  26. data/lib/webspicy/version.rb +8 -0
  27. data/lib/webspicy.rb +112 -0
  28. data/spec/unit/resource/service/test_dress_params.rb +34 -0
  29. data/spec/unit/resource/test_instantiate_url.rb +20 -0
  30. data/spec/unit/resource/test_url_placeholders.rb +16 -0
  31. data/spec/unit/scope/test_each_resource.rb +59 -0
  32. data/spec/unit/scope/test_each_service.rb +51 -0
  33. data/spec/unit/scope/test_to_real_url.rb +75 -0
  34. data/spec/unit/spec_helper.rb +28 -0
  35. data/spec/unit/test_configuration.rb +84 -0
  36. data/spec/unit/tester/test_assertions.rb +108 -0
  37. data/tasks/test.rake +27 -0
  38. metadata +149 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b34d2485758616a81425a1f39fd0e5350fea1799
4
+ data.tar.gz: 8756493dea8e750857c172399dcebe2edcc2a76b
5
+ SHA512:
6
+ metadata.gz: 5ee96045a18f309ca39859a8459cc2cf11b1dd02d3fbc9a45186f7f5ef45af46b5121f5626303e52eca886196b8baa0eb34c17e57702e5408514092a78c089de
7
+ data.tar.gz: 83db0b47519892d12acc7b73ff4bf2ec97642ffebb680d2f0535915c6f11321172656a0dee2ddd62c33317cd9045b0bb8104f28a997f9c7f92c2293fe39a009f
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ # The MIT Licence
2
+
3
+ Copyright (c) 2017 - Enspirit SPRL (Bernard Lambeau)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # Webspicy
2
+
3
+ A description, specification and test framework for black box web services seen as software operations.
4
+
5
+ ## Work in progress
6
+
7
+ Please refer to the examples for now. More documentation coming soon.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ #
2
+ # Install all tasks found in tasks folder
3
+ #
4
+ # See .rake files there for complete documentation.
5
+ #
6
+ Dir["tasks/*.rake"].each do |taskfile|
7
+ load taskfile
8
+ end
9
+
10
+ # We run tests by default
11
+ task :default => :test
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rake", "~> 10"
4
+ gem 'sinatra', "~> 2.0"
5
+ gem "webspicy", path: "../.."
@@ -0,0 +1,69 @@
1
+ PATH
2
+ remote: ../..
3
+ specs:
4
+ webspicy (0.0.1)
5
+ finitio (~> 0.5.2)
6
+ http (~> 0.5)
7
+ path (~> 1.3)
8
+ rspec (~> 3.6)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ addressable (2.5.1)
14
+ public_suffix (~> 2.0, >= 2.0.2)
15
+ citrus (3.0.2)
16
+ diff-lcs (1.3)
17
+ domain_name (0.5.20170404)
18
+ unf (>= 0.0.5, < 1.0.0)
19
+ finitio (0.5.2)
20
+ citrus (>= 2.4, < 4.0)
21
+ http (0.9.9)
22
+ addressable (~> 2.3)
23
+ http-cookie (~> 1.0)
24
+ http-form_data (~> 1.0.1)
25
+ http_parser.rb (~> 0.6.0)
26
+ http-cookie (1.0.3)
27
+ domain_name (~> 0.5)
28
+ http-form_data (1.0.3)
29
+ http_parser.rb (0.6.0)
30
+ mustermann (1.0.0)
31
+ path (1.3.3)
32
+ public_suffix (2.0.5)
33
+ rack (2.0.3)
34
+ rack-protection (2.0.0)
35
+ rack
36
+ rake (10.5.0)
37
+ rspec (3.6.0)
38
+ rspec-core (~> 3.6.0)
39
+ rspec-expectations (~> 3.6.0)
40
+ rspec-mocks (~> 3.6.0)
41
+ rspec-core (3.6.0)
42
+ rspec-support (~> 3.6.0)
43
+ rspec-expectations (3.6.0)
44
+ diff-lcs (>= 1.2.0, < 2.0)
45
+ rspec-support (~> 3.6.0)
46
+ rspec-mocks (3.6.0)
47
+ diff-lcs (>= 1.2.0, < 2.0)
48
+ rspec-support (~> 3.6.0)
49
+ rspec-support (3.6.0)
50
+ sinatra (2.0.0)
51
+ mustermann (~> 1.0)
52
+ rack (~> 2.0)
53
+ rack-protection (= 2.0.0)
54
+ tilt (~> 2.0)
55
+ tilt (2.0.7)
56
+ unf (0.1.4)
57
+ unf_ext
58
+ unf_ext (0.0.7.4)
59
+
60
+ PLATFORMS
61
+ ruby
62
+
63
+ DEPENDENCIES
64
+ rake (~> 10)
65
+ sinatra (~> 2.0)
66
+ webspicy!
67
+
68
+ BUNDLED WITH
69
+ 1.14.6
@@ -0,0 +1,21 @@
1
+ require 'webspicy'
2
+
3
+ namespace :webspicy do
4
+
5
+ config = Webspicy::Configuration.new do |c|
6
+ c.host = "http://127.0.0.1:4567"
7
+ c.add_folder Path.dir/"webspicy"
8
+ end
9
+
10
+ task :check do
11
+ Webspicy::Checker.new(config).call
12
+ end
13
+
14
+ task :test do
15
+ Webspicy::Tester.new(config).call
16
+ end
17
+
18
+ end
19
+
20
+ task :test => :"webspicy:test"
21
+ task :default => :test
@@ -0,0 +1,32 @@
1
+ require 'sinatra'
2
+ require 'json'
3
+
4
+ TODOLIST = [
5
+ {
6
+ id: 1,
7
+ description: "Refactor the framework"
8
+ },
9
+ {
10
+ id: 2,
11
+ description: "Write documentation"
12
+ }
13
+ ]
14
+
15
+ disable :show_exceptions
16
+ enable :raise_errors
17
+
18
+ get '/todo/' do
19
+ content_type :json
20
+ TODOLIST.to_json
21
+ end
22
+
23
+ get '/todo/:id' do |id|
24
+ content_type :json
25
+ todo = TODOLIST.find{|todo| todo[:id] == Integer(id) }
26
+ if todo.nil?
27
+ status 404
28
+ {error: "No such todo"}.to_json
29
+ else
30
+ todo.to_json
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ Todo = {
2
+ id : Integer
3
+ description : String
4
+ }
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: |-
3
+ Todo
4
+
5
+ url: |-
6
+ /todo/{id}
7
+
8
+ services:
9
+ - method: |-
10
+ GET
11
+
12
+ description: |-
13
+ Returns a single todo item
14
+
15
+ preconditions: |-
16
+
17
+ input_schema: |-
18
+ {
19
+ id: Integer
20
+ }
21
+
22
+ output_schema: |-
23
+ Todo
24
+
25
+ error_schema: |-
26
+ {
27
+ error: String
28
+ }
29
+
30
+ examples:
31
+
32
+ - description: |-
33
+ when requested on an existing TODO
34
+ params:
35
+ id: 1
36
+ expected:
37
+ content_type: application/json
38
+ status: 200
39
+ assert:
40
+ - "pathFD('', id: 1)"
41
+
42
+ counterexamples:
43
+
44
+ - description: |-
45
+ when requested on an unexisting TODO
46
+ params:
47
+ id: 999254654
48
+ expected:
49
+ content_type: application/json
50
+ status: 404
51
+ assert:
52
+ - "pathFD('', error: 'No such todo')"
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: |-
3
+ Todo
4
+
5
+ url: |-
6
+ /todo/
7
+
8
+ services:
9
+ - method: |-
10
+ GET
11
+
12
+ description: |-
13
+ Returns the list of todo items
14
+
15
+ preconditions: |-
16
+
17
+ input_schema: |-
18
+ {
19
+ }
20
+
21
+ output_schema: |-
22
+ [Todo]
23
+
24
+ error_schema: |-
25
+ {
26
+ }
27
+
28
+ examples:
29
+
30
+ - description: |-
31
+ when requested
32
+ params: {}
33
+ expected:
34
+ content_type: application/json
35
+ status: 200
36
+ assert:
37
+ - notEmpty
38
+
39
+ counterexamples: []
@@ -0,0 +1,26 @@
1
+ module Webspicy
2
+ class Checker
3
+
4
+ def initialize(config)
5
+ @config = config
6
+ end
7
+ attr_reader :config
8
+
9
+ def call
10
+ Webspicy.with_scope_for(config) do |scope|
11
+ client = scope.get_client
12
+ scope.each_resource_file do |file, folder|
13
+ RSpec.describe file.relative_to(folder).to_s do
14
+
15
+ it 'meets the formal doc data schema' do
16
+ Webspicy.resource(file.load, file)
17
+ end
18
+
19
+ end
20
+ end
21
+ RSpec::Core::Runner.run config.rspec_options
22
+ end
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,70 @@
1
+ module Webspicy
2
+ class HttpClient < Client
3
+
4
+ def call(test_case, service, resource)
5
+ # Instantiate the parameters
6
+ headers = test_case.headers
7
+ params = test_case.dress_params? ? service.dress_params(test_case.params) : test_case.params
8
+
9
+ # Instantiate the url and strip parameters
10
+ url, params = resource.instantiate_url(params)
11
+
12
+ # Globalize the URL if required
13
+ url = scope.to_real_url(url)
14
+
15
+ # Invoke the service now
16
+ api = Api.new
17
+ api.public_send(service.method.to_s.downcase.to_sym, url, params, headers)
18
+
19
+ # Return the result
20
+ Resource::Service::Invocation.new(service, test_case, api.last_response)
21
+ end
22
+
23
+ class Api
24
+
25
+ attr_reader :last_response
26
+
27
+ def get(url, params, headers = nil)
28
+ headers, url = headers_and_url_for(url, params, headers)
29
+
30
+ Webspicy.info("GET #{url} -- #{params.inspect}")
31
+
32
+ @last_response = HTTP[headers].get(url, params: params)
33
+
34
+ Webspicy.debug("Headers: #{@last_response.headers.to_hash}")
35
+ Webspicy.debug("Response: #{@last_response.body}")
36
+ end
37
+
38
+ def post(url, params, headers = nil)
39
+ headers, url = headers_and_url_for(url, params, headers)
40
+
41
+ Webspicy.info("POST #{url} -- #{params.inspect}")
42
+
43
+ @last_response = HTTP[headers].post(url, body: params.to_json)
44
+
45
+ Webspicy.debug("Headers: #{@last_response.headers.to_hash}")
46
+ Webspicy.debug("Response: #{@last_response.body}")
47
+ end
48
+
49
+ def post_form(url, params, headers = nil)
50
+ headers, url = headers_and_url_for(url, params, headers)
51
+
52
+ Webspicy.info("POST #{url} -- #{params.inspect}")
53
+
54
+ @last_response = HTTP[headers].post(url, form: params)
55
+
56
+ Webspicy.debug("Headers: #{@last_response.headers.to_hash}")
57
+ Webspicy.debug("Response: #{@last_response.body}")
58
+ end
59
+
60
+ private
61
+
62
+ def headers_and_url_for(url, params, headers)
63
+ headers = headers || {}
64
+ [ headers, url ]
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,20 @@
1
+ module Webspicy
2
+ class Client
3
+
4
+ def initialize(scope)
5
+ @scope = scope
6
+ end
7
+ attr_reader :scope
8
+
9
+ def config
10
+ scope.config
11
+ end
12
+
13
+ def before(*args, &bl)
14
+ config.before_listeners.each do |beach|
15
+ beach.call(*args, &bl)
16
+ end
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,168 @@
1
+ module Webspicy
2
+ class Configuration
3
+
4
+ def initialize
5
+ @folders = []
6
+ @before_listeners = []
7
+ @rspec_options = default_rspec_options
8
+ @run_counterexamples = default_run_counterexamples
9
+ @file_filter = default_file_filter
10
+ @service_filter = default_service_filter
11
+ @client = HttpClient
12
+ yield(self) if block_given?
13
+ end
14
+
15
+ # Adds a folder to the list of folders where test case definitions are
16
+ # to be found.
17
+ def add_folder(folder)
18
+ folder = Path(folder)
19
+ raise "Folder `#{folder}` does not exists" unless folder.exists? && folder.directory?
20
+ @folders << folder
21
+ end
22
+ attr_reader :folders
23
+
24
+ # Sets whether counter examples have to be ran or not.
25
+ def run_counterexamples=(run_counterexamples)
26
+ @run_counterexamples = run_counterexamples
27
+ end
28
+ attr_reader :run_counterexamples
29
+
30
+ # Whether counter examples must be ran or not.
31
+ def run_counterexamples?
32
+ @run_counterexamples
33
+ end
34
+
35
+ # Returns the defaut value for run_counterexamples
36
+ def default_run_counterexamples
37
+ ENV['ROBUST'].nil? || ENV['ROBUST'] != 'no'
38
+ end
39
+ private :default_run_counterexamples
40
+
41
+ # Installs a host (resolver).
42
+ #
43
+ # The host resolver is responsible from transforming URLs found in
44
+ # .yml test files to an absolute URL invoked by the client. Supported
45
+ # values are:
46
+ #
47
+ # - String: taken as a prefix for all relative URLs. Using this option
48
+ # lets specify all webservices through relative URLs and having the
49
+ # host itself as global configuration variable.
50
+ # - Proc: all URLs are passed to the proc, relative and absolute ones.
51
+ # The result of the proc is used as URL to use in practice.
52
+ #
53
+ # When no host provider is provided, all URLs are expected to be absolute
54
+ # URLs, otherwise an error will be thrown at runtime.
55
+ def host=(host)
56
+ @host = host
57
+ end
58
+ attr_reader :host
59
+
60
+ # Installs a file filter.
61
+ #
62
+ # A file filter can be added to restrict the scope attention only to the
63
+ # files that match the filter installed. Supported values are:
64
+ #
65
+ # - Proc: each file (a Path instance) is passed in turn. Only files for
66
+ # which a truthy value is returned will be considered by the scope.
67
+ # - Regexp: the path of each file is matched against the regexp. Only files
68
+ # that match are considered by the scope.
69
+ # - ===: any instance responding to `===` can be used as a matcher, following
70
+ # Ruby conventions. The match is done on a Path instance.
71
+ #
72
+ def file_filter=(file_filter)
73
+ @file_filter = file_filter
74
+ end
75
+ attr_reader :file_filter
76
+
77
+ # Returns the default file filter to use.
78
+ #
79
+ # By default no file filter is set, unless a RESOURCE environment variable is
80
+ # set. In that case, a file filter is set that matches the file name to the
81
+ # variable value, through a regular expression.
82
+ def default_file_filter
83
+ ENV['RESOURCE'] ? Regexp.compile(ENV['RESOURCE']) : nil
84
+ end
85
+ private :default_file_filter
86
+
87
+ # Installs a service filter.
88
+ #
89
+ # A service filter can be added to restrict the scope attention only to the
90
+ # services that match the filter installed. Supported values are:
91
+ #
92
+ # - Proc: each service is passed in turn. Only services for which a truthy value
93
+ # is returned will be considered by the scope.
94
+ # - ===: any instance responding to `===` can be used as a matcher, following
95
+ # Ruby conventions. The match is done on a Service instance.
96
+ #
97
+ def service_filter=(service_filter)
98
+ @service_filter = service_filter
99
+ end
100
+ attr_reader :service_filter
101
+
102
+ # Returns the default service filters.
103
+ #
104
+ # By default no filter is set unless a METHOD environment variable is set.
105
+ # In that case, a service filter is returned that filters the services whose
106
+ # HTTP method match the variable value.
107
+ def default_service_filter
108
+ ENV['METHOD'] ? ->(s){ s.method.to_s.downcase == ENV['METHOD'].downcase } : nil
109
+ end
110
+ private :default_service_filter
111
+
112
+
113
+ # Installs a client class to use to invoke web services for real.
114
+ #
115
+ # This configuration allows defining a subclass of Client to be used for
116
+ # actually invoking web services. Options are:
117
+ #
118
+ # - HttpClient: Uses the HTTP library to make real HTTP call to a web server.
119
+ #
120
+ # Note that this configuration variable expected a client *class*, not an
121
+ # instance
122
+ def client=(client)
123
+ @client = client
124
+ end
125
+ attr_reader :client
126
+
127
+ # Installs a listener that will be called before each web service invocation.
128
+ #
129
+ # The `listener` must respond to `call`.
130
+ def before_each(&listener)
131
+ raise "Must respond to call" unless listener.respond_to?(:call)
132
+ @before_listeners << listener
133
+ end
134
+
135
+ # Returns the list of listeners that must be called before each web service
136
+ # invocation.
137
+ def before_listeners
138
+ @before_listeners
139
+ end
140
+
141
+ # Allows setting the options passed at RSpec, which is used by both the runner
142
+ # and checker classes.
143
+ #
144
+ # `options` is supposed to be valid RSpec options, to be passed at
145
+ # `RSpec::Core::Runner.run`
146
+ def rspec_options=(options)
147
+ @rspec_options = options
148
+ end
149
+ attr_reader :rspec_options
150
+
151
+ # Returns the default rspec options.
152
+ #
153
+ # By default rspec colors are enabled and the format set to 'documentation'.
154
+ # The following environment variables <-> rspec options are supported:
155
+ #
156
+ # - FAILFAST <-> --fail-fast
157
+ #
158
+ def default_rspec_options
159
+ options = ["--color", "--format=documentation"]
160
+ if ENV['FAILFAST']
161
+ options << (ENV['FAILFAST'] == 'no' ? "--no-fail-fast" : "--fail-fast=#{ENV['FAILFAST']}")
162
+ end
163
+ options
164
+ end
165
+ private :default_rspec_options
166
+
167
+ end
168
+ end
@@ -0,0 +1,47 @@
1
+ Method =
2
+ String( s | s =~ /^(GET|POST|POST_FORM|PUT|DELETE|PATCH)$/ )
3
+
4
+ Schema =
5
+ .Finitio::System <fio> String
6
+ \( s | ::Webspicy.schema(s) )
7
+ \( s | raise "Unsupported" )
8
+
9
+ Resource =
10
+ .Webspicy::Resource <info> {
11
+ name: String
12
+ url: String
13
+ services: [Service]
14
+ }
15
+
16
+ Service =
17
+ .Webspicy::Resource::Service <info> {
18
+ method : Method
19
+ description : String
20
+ preconditions : String
21
+ postconditions :? String
22
+ input_schema : Schema
23
+ output_schema : Schema
24
+ error_schema : Schema
25
+ blackbox :? String
26
+ examples :? [TestCase]
27
+ counterexamples :? [TestCase]
28
+ }
29
+
30
+ TestCase =
31
+ .Webspicy::Resource::Service::TestCase <info> {
32
+ description : String
33
+ dress_params :? Boolean
34
+ params : Params
35
+ headers :? .Hash
36
+ seeds :? String
37
+ requester :? String
38
+ expected: {
39
+ status : Integer
40
+ content_type :? String
41
+ error :? String
42
+ headers :? .Hash
43
+ }
44
+ assert :? [String]
45
+ }
46
+
47
+ Params = .Array|.Hash