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
@@ -0,0 +1,158 @@
1
+ module Webspicy
2
+ class Resource
3
+ class Service
4
+ class Invocation
5
+
6
+ def initialize(service, test_case, response)
7
+ @service = service
8
+ @test_case = test_case
9
+ @response = response
10
+ end
11
+
12
+ attr_reader :service, :test_case, :response
13
+
14
+ def done?
15
+ !response.nil?
16
+ end
17
+
18
+ def is_expected_success?
19
+ test_case.expected_status >= 200 && test_case.expected_status < 300
20
+ end
21
+
22
+ def is_success?
23
+ response.status.code >= 200 && response.status.code < 300
24
+ end
25
+
26
+ def is_empty_response?
27
+ response.status.code == 204
28
+ end
29
+
30
+ def is_redirect?
31
+ response.status.code >= 300 && response.status.code < 400
32
+ end
33
+
34
+ ### Check of HTTP status
35
+
36
+ def expected_status_unmet
37
+ expected = test_case.expected_status
38
+ got = response.status
39
+ expected == got ? nil : "#{expected} != #{got}"
40
+ end
41
+
42
+ def meets_expected_status?
43
+ expected_status_unmet.nil?
44
+ end
45
+
46
+ ### Check of the expected output type
47
+
48
+ def expected_content_type_unmet
49
+ ect = test_case.expected_content_type
50
+ got = response.content_type.mime_type.to_s
51
+ ect == got ? nil : "#{ect} != #{got}"
52
+ end
53
+
54
+ def meets_expected_content_type?
55
+ expected_content_type_unmet.nil?
56
+ end
57
+
58
+ ### Check of output schema
59
+
60
+ def expected_schema_unmet
61
+ if is_empty_response?
62
+ body = response.body.to_s.strip
63
+ body.empty? ? nil : "Expected empty body, got #{body}"
64
+ elsif is_redirect?
65
+ else
66
+ case dressed_body
67
+ when Finitio::TypeError then dressed_body.root_cause.message
68
+ when StandardError then dressed_body.message
69
+ else nil
70
+ end
71
+ end
72
+ end
73
+
74
+ def meets_expected_schema?
75
+ expected_schema_unmet.nil?
76
+ end
77
+
78
+ ### Check of assertions
79
+
80
+ def assertions_unmet
81
+ unmet = []
82
+ asserter = Tester::Asserter.new(dressed_body)
83
+ test_case.assert.each do |assert|
84
+ begin
85
+ asserter.instance_eval(assert)
86
+ rescue => ex
87
+ unmet << ex.message
88
+ end
89
+ end
90
+ unmet.empty? ? nil : unmet.join("\n")
91
+ end
92
+
93
+ def value_equal(exp, got)
94
+ case exp
95
+ when Hash
96
+ exp.all?{|(k,v)|
97
+ got[k] == v
98
+ }
99
+ else
100
+ exp == got
101
+ end
102
+ end
103
+
104
+ ### Check of expected error message
105
+
106
+ def expected_error_unmet
107
+ expected = test_case.expected_error
108
+ case test_case.expected_content_type
109
+ when %r{json}
110
+ got = meets_expected_schema? ? dressed_body[:description] : response.body
111
+ expected == got ? nil : "`#{expected}` vs. `#{got}`"
112
+ else
113
+ dressed_body.include?(expected) ? nil : "#{expected} not found" unless expected.nil?
114
+ end
115
+ end
116
+
117
+ ### Check of expected headers
118
+
119
+ def expected_headers_unmet
120
+ unmet = []
121
+ expected = test_case.expected_headers
122
+ expected.each_pair do |k,v|
123
+ got = response.headers[k]
124
+ unmet << "#{v} expected for #{k}, got #{got}" unless (got == v)
125
+ end
126
+ unmet.empty? ? nil : unmet.join("\n")
127
+ end
128
+
129
+ private
130
+
131
+ def loaded_body
132
+ case test_case.expected_content_type
133
+ when %r{json}
134
+ raise "Body empty while expected" if response.body.to_s.empty?
135
+ @loaded_body ||= ::JSON.parse(response.body)
136
+ else
137
+ response.body.to_s
138
+ end
139
+ end
140
+
141
+ def dressed_body
142
+ @dressed_body ||= case test_case.expected_content_type
143
+ when %r{json}
144
+ schema = is_expected_success? ? service.output_schema : service.error_schema
145
+ begin
146
+ schema.dress(loaded_body)
147
+ rescue Finitio::TypeError => ex
148
+ ex
149
+ end
150
+ else
151
+ loaded_body
152
+ end
153
+ end
154
+
155
+ end # class Invocation
156
+ end # class Service
157
+ end # class Resource
158
+ end # module Webspicy
@@ -0,0 +1,74 @@
1
+ module Webspicy
2
+ class Resource
3
+ class Service
4
+ class TestCase
5
+
6
+ def initialize(raw)
7
+ @raw = raw
8
+ end
9
+
10
+ def self.info(raw)
11
+ new(raw)
12
+ end
13
+
14
+ def description
15
+ @raw[:description]
16
+ end
17
+
18
+ def seeds
19
+ @raw[:seeds]
20
+ end
21
+
22
+ def headers
23
+ @raw[:headers] || {}
24
+ end
25
+
26
+ def dress_params
27
+ @raw.fetch(:dress_params){ true }
28
+ end
29
+ alias :dress_params? :dress_params
30
+
31
+ def params
32
+ @raw[:params]
33
+ end
34
+
35
+ def expected_content_type
36
+ @raw[:expected][:content_type] || 'application/json'
37
+ end
38
+
39
+ def expected_status
40
+ @raw[:expected][:status]
41
+ end
42
+
43
+ def expected_error
44
+ @raw[:expected][:error]
45
+ end
46
+
47
+ def has_expected_error?
48
+ !expected_error.nil?
49
+ end
50
+
51
+ def expected_headers
52
+ @raw[:expected][:headers] || {}
53
+ end
54
+
55
+ def has_expected_headers?
56
+ !expected_headers.empty?
57
+ end
58
+
59
+ def assert
60
+ @raw[:assert] || []
61
+ end
62
+
63
+ def has_assertions?
64
+ !assert.empty?
65
+ end
66
+
67
+ def to_info
68
+ @raw
69
+ end
70
+
71
+ end # class TestCase
72
+ end # class Service
73
+ end # class Resource
74
+ end # module Webspicy
@@ -0,0 +1,49 @@
1
+ module Webspicy
2
+ class Resource
3
+ class Service
4
+
5
+ def initialize(raw)
6
+ @raw = raw
7
+ end
8
+
9
+ def self.info(raw)
10
+ new(raw)
11
+ end
12
+
13
+ def method
14
+ @raw[:method]
15
+ end
16
+
17
+ def examples
18
+ @raw[:examples]
19
+ end
20
+
21
+ def counterexamples
22
+ @raw[:counterexamples]
23
+ end
24
+
25
+ def input_schema
26
+ @raw[:input_schema]
27
+ end
28
+
29
+ def output_schema
30
+ @raw[:output_schema]
31
+ end
32
+
33
+ def error_schema
34
+ @raw[:error_schema]
35
+ end
36
+
37
+ def dress_params(params)
38
+ input_schema.dress(params)
39
+ end
40
+
41
+ def to_info
42
+ @raw
43
+ end
44
+
45
+ end # class Service
46
+ end # class Resource
47
+ end # module Webspicy
48
+ require_relative 'service/test_case'
49
+ require_relative 'service/invocation'
@@ -0,0 +1,43 @@
1
+ module Webspicy
2
+ class Resource
3
+
4
+ def initialize(raw)
5
+ @raw = raw
6
+ end
7
+
8
+ def self.info(raw)
9
+ new(raw)
10
+ end
11
+
12
+ def url
13
+ @raw[:url]
14
+ end
15
+
16
+ def services
17
+ @raw[:services]
18
+ end
19
+
20
+ def url_placeholders
21
+ url.scan(/\{([a-zA-Z]+)\}/).map{|x| x.first.to_sym }
22
+ end
23
+
24
+ def instantiate_url(params)
25
+ url, rest = self.url, params.dup
26
+ url_placeholders.each do |placeholder|
27
+ if (params.has_key?(placeholder))
28
+ url = url.gsub("{#{placeholder}}", params[placeholder].to_s)
29
+ rest.delete(placeholder)
30
+ else
31
+ raise "Missing URL parameter `#{placeholder}`\n\t(#{params.inspect})"
32
+ end
33
+ end
34
+ [ url, rest ]
35
+ end
36
+
37
+ def to_info
38
+ @raw
39
+ end
40
+
41
+ end
42
+ end
43
+ require_relative 'resource/service'
@@ -0,0 +1,113 @@
1
+ module Webspicy
2
+ class Scope
3
+
4
+ def initialize(config)
5
+ @config = config
6
+ end
7
+ attr_reader :config
8
+
9
+ ###
10
+ ### Eachers -- Allow navigating the web service definitions
11
+ ###
12
+
13
+ # Yields each resource file in the current scope
14
+ def each_resource_file(&bl)
15
+ return enum_for(:each_resource_file) unless block_given?
16
+ config.folders.each do |folder|
17
+ _each_resource_file(folder, &bl)
18
+ end
19
+ end
20
+
21
+ # Recursive implementation of `each_resource_file` for each
22
+ # folder in the configuration.
23
+ def _each_resource_file(folder)
24
+ folder.glob("**/*.yml").select(&to_filter_proc(config.file_filter)).each do |file|
25
+ yield file, folder
26
+ end
27
+ end
28
+ private :_each_resource_file
29
+
30
+ # Yields each resource in the current scope in turn.
31
+ def each_resource(&bl)
32
+ return enum_for(:each_resource) unless block_given?
33
+ each_resource_file do |file, folder|
34
+ yield Webspicy.resource(file.load, file)
35
+ end
36
+ end
37
+
38
+ def each_service(resource, &bl)
39
+ resource.services.select(&to_filter_proc(config.service_filter)).each(&bl)
40
+ end
41
+
42
+ def each_example(service, &bl)
43
+ service.examples.each(&bl)
44
+ end
45
+
46
+ def each_counterexamples(service, &bl)
47
+ service.counterexamples.each(&bl) if config.run_counterexamples?
48
+ end
49
+
50
+
51
+ ###
52
+ ### Schemas -- For parsing input and output data schemas found in
53
+ ### web service definitions
54
+ ###
55
+
56
+ # Parses a Finitio schema based on the data system.
57
+ def parse_schema(fio)
58
+ data_system.parse(fio)
59
+ end
60
+
61
+ # Returns the Data system to use for parsing schemas
62
+ def data_system
63
+ @data_system ||= begin
64
+ root = config.folders.find{|f| (f/"schema.fio").file? }
65
+ root ? Finitio::DEFAULT_SYSTEM.parse((root/"schema.fio").read) : Finitio::DEFAULT_SYSTEM
66
+ end
67
+ end
68
+
69
+
70
+ ###
71
+ ### Service invocation: abstract the configuration about what client is
72
+ ### used and how to instantiate it
73
+ ###
74
+
75
+ # Returns an instance of the client to use to invoke web services
76
+ def get_client
77
+ config.client.new(self)
78
+ end
79
+
80
+ # Convert an instantiated URL found in a webservice definition
81
+ # to a real URL, using the configuration host
82
+ def to_real_url(url)
83
+ case config.host
84
+ when Proc
85
+ config.host.call(url)
86
+ when String
87
+ url =~ /^http/ ? url : "#{config.host}#{url}"
88
+ else
89
+ return url if url =~ /^http/
90
+ raise "Unable to resolve `#{url}` : no host resolver provided\nSee `Configuration#host="
91
+ end
92
+ end
93
+
94
+ ###
95
+ ### Private methods
96
+ ###
97
+
98
+ private
99
+
100
+ # Returns a proc that implements file_filter strategy according to the
101
+ # type of filter installed
102
+ def to_filter_proc(filter)
103
+ case ff = filter
104
+ when NilClass then ->(f){ true }
105
+ when Proc then ff
106
+ when Regexp then ->(f){ ff =~ f.to_s }
107
+ else
108
+ ->(f){ ff === f }
109
+ end
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,94 @@
1
+ module Webspicy
2
+ class Tester
3
+ class Asserter
4
+
5
+ NO_ARG = Object.new
6
+
7
+ class AssertionsClass
8
+ include Assertions
9
+ end
10
+
11
+ def initialize(target)
12
+ @target = target
13
+ @assertions = AssertionsClass.new
14
+ end
15
+
16
+ def exists(path = '')
17
+ unless @assertions.exists(@target, path)
18
+ _! "Expected #{_s(@target)} to exists"
19
+ end
20
+ end
21
+
22
+ def notExists(path = '')
23
+ unless @assertions.notExists(@target, path)
24
+ _! "Expected #{_s(@target)} not to exists"
25
+ end
26
+ end
27
+
28
+ def empty(path = '')
29
+ unless @assertions.empty(@target, path)
30
+ _! "Expected #{_s(@target)} to be empty"
31
+ end
32
+ end
33
+
34
+ def notEmpty(path = '')
35
+ unless @assertions.notEmpty(@target, path)
36
+ _! "Expected #{_s(@target)} to be non empty"
37
+ end
38
+ end
39
+
40
+ def size(path, expected = NO_ARG)
41
+ path, expected = '', path if expected == NO_ARG
42
+ unless @assertions.size(@target, path, expected)
43
+ _! "Expected #{_s(@target)} to have a size of #{expected}"
44
+ end
45
+ end
46
+
47
+ def idIn(path, *expected)
48
+ path, expected = '', [path]+expected unless path.is_a?(String)
49
+ unless @assertions.idIn(@target, path, expected)
50
+ _! "Expected #{_s(@target)} to have ids #{expected.join(',')}"
51
+ end
52
+ end
53
+
54
+ def idNotIn(path, *expected)
55
+ path, expected = '', [path]+expected unless path.is_a?(String)
56
+ unless @assertions.idNotIn(@target, path, expected)
57
+ _! "Expected #{_s(@target)} to not have ids #{expected.join(',')}"
58
+ end
59
+ end
60
+
61
+ def idFD(path, id, expected = NO_ARG)
62
+ if expected == NO_ARG
63
+ expected = id
64
+ id, path = path, ''
65
+ end
66
+ unless @assertions.idFD(@target, path, id, expected)
67
+ _! "Expected #{_s(@target)} to meet FD #{expected.inspect}"
68
+ end
69
+ end
70
+
71
+ def pathFD(path, expected)
72
+ unless @assertions.pathFD(@target, path, expected)
73
+ _! "Expected #{_s(@target)} to meet FD #{expected.inspect}"
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def DateTime(str)
80
+ DateTime.parse(str)
81
+ end
82
+
83
+ def _s(target)
84
+ target.inspect[0..25]
85
+ end
86
+
87
+ def _!(msg)
88
+ raise msg
89
+ end
90
+
91
+ end # class Asserter
92
+ end # class Tester
93
+ end # module Webspicy
94
+
@@ -0,0 +1,103 @@
1
+ module Webspicy
2
+ class Tester
3
+ module Assertions
4
+
5
+ class InvalidArgError < StandardError; end
6
+
7
+ NO_ARG = Object.new
8
+
9
+ def exists(target, path = NO_ARG)
10
+ target = extract_path(target, path)
11
+ not target.nil?
12
+ end
13
+
14
+ def notExists(target, path = NO_ARG)
15
+ target = extract_path(target, path)
16
+ target.nil?
17
+ end
18
+
19
+ def empty(target, path = NO_ARG)
20
+ target = extract_path(target, path)
21
+ respond_to!(target, :empty?).empty?
22
+ end
23
+
24
+ def notEmpty(target, path = NO_ARG)
25
+ not empty(target, path)
26
+ end
27
+
28
+ def size(target, path, expected = NO_ARG)
29
+ path, expected = '', path if expected == NO_ARG
30
+ target = extract_path(target, path)
31
+ respond_to!(target, :size).size == expected
32
+ end
33
+
34
+ def idIn(target, path, expected = NO_ARG)
35
+ path, expected = '', path if expected == NO_ARG
36
+ target = extract_path(target, path)
37
+ ids = an_array(target).map do |tuple|
38
+ respond_to!(tuple, :[])[:id]
39
+ end
40
+ ids.to_set == expected.to_set
41
+ end
42
+
43
+ def idNotIn(target, path, expected = NO_ARG)
44
+ path, expected = '', path if expected == NO_ARG
45
+ target = extract_path(target, path)
46
+ ids = an_array(target).map do |tuple|
47
+ respond_to!(tuple, :[])[:id]
48
+ end
49
+ (ids.to_set & expected.to_set).empty?
50
+ end
51
+
52
+ def idFD(target, path, id, expected = NO_ARG)
53
+ if expected == NO_ARG
54
+ expected = id
55
+ id, path = path, ''
56
+ end
57
+ target = extract_path(target, path)
58
+ found = an_array(target).find{|t| t[:id] == id }
59
+ expected.keys.all?{|k|
60
+ value_equal(expected[k], found[k])
61
+ }
62
+ end
63
+
64
+ def pathFD(target, path, expected)
65
+ target = extract_path(target, path)
66
+ expected.keys.all?{|k|
67
+ value_equal(expected[k], target[k])
68
+ }
69
+ end
70
+
71
+ private
72
+
73
+ def extract_path(target, path = NO_ARG)
74
+ return target if path.nil? or path==NO_ARG or path.empty?
75
+ return nil unless target.is_a?(Hash)
76
+ path.split('/').inject(target) do |memo,key|
77
+ memo && (memo.is_a?(Array) ? memo[key.to_i] : memo[key.to_sym])
78
+ end
79
+ end
80
+
81
+ def respond_to!(target, method)
82
+ unless target.respond_to?(method)
83
+ raise InvalidArgError, "Expecting instance responding to #{method}"
84
+ end
85
+ target
86
+ end
87
+
88
+ def an_array(target)
89
+ target.is_a?(Array) ? target : [target]
90
+ end
91
+
92
+ def value_equal(exp, got)
93
+ case exp
94
+ when Hash
95
+ exp.all?{|(k,v)| got[k] == v }
96
+ else
97
+ exp == got
98
+ end
99
+ end
100
+
101
+ end # module Assertions
102
+ end # class Tester
103
+ end # module Webspicy