webspicy 0.1.0.pre.rc1

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