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.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/LICENSE.md +22 -0
- data/README.md +7 -0
- data/Rakefile +11 -0
- data/examples/restful/Gemfile +5 -0
- data/examples/restful/Gemfile.lock +69 -0
- data/examples/restful/Rakefile +21 -0
- data/examples/restful/app.rb +32 -0
- data/examples/restful/webspicy/schema.fio +4 -0
- data/examples/restful/webspicy/todo/getTodo.yml +52 -0
- data/examples/restful/webspicy/todo/getTodos.yml +39 -0
- data/lib/webspicy/checker.rb +26 -0
- data/lib/webspicy/client/http_client.rb +70 -0
- data/lib/webspicy/client.rb +20 -0
- data/lib/webspicy/configuration.rb +168 -0
- data/lib/webspicy/formaldoc.fio +47 -0
- data/lib/webspicy/resource/service/invocation.rb +158 -0
- data/lib/webspicy/resource/service/test_case.rb +74 -0
- data/lib/webspicy/resource/service.rb +49 -0
- data/lib/webspicy/resource.rb +43 -0
- data/lib/webspicy/scope.rb +113 -0
- data/lib/webspicy/tester/asserter.rb +94 -0
- data/lib/webspicy/tester/assertions.rb +103 -0
- data/lib/webspicy/tester.rb +96 -0
- data/lib/webspicy/version.rb +8 -0
- data/lib/webspicy.rb +112 -0
- data/spec/unit/resource/service/test_dress_params.rb +34 -0
- data/spec/unit/resource/test_instantiate_url.rb +20 -0
- data/spec/unit/resource/test_url_placeholders.rb +16 -0
- data/spec/unit/scope/test_each_resource.rb +59 -0
- data/spec/unit/scope/test_each_service.rb +51 -0
- data/spec/unit/scope/test_to_real_url.rb +75 -0
- data/spec/unit/spec_helper.rb +28 -0
- data/spec/unit/test_configuration.rb +84 -0
- data/spec/unit/tester/test_assertions.rb +108 -0
- data/tasks/test.rake +27 -0
- 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
|