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
@@ -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