webspicy 0.24.0 → 0.26.0
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 +4 -4
- data/lib/webspicy/specification/pre.rb +0 -2
- data/lib/webspicy/tester/asserter.rb +12 -0
- data/lib/webspicy/tester/assertions.rb +10 -0
- data/lib/webspicy/version.rb +1 -1
- data/lib/webspicy/web/specification/post/etag_caching_protocol.rb +38 -0
- data/lib/webspicy/web/specification/post/last_modified_caching_protocol.rb +41 -0
- data/lib/webspicy/web/specification/post/semantics_preserved_by_refactoring.rb +67 -0
- data/lib/webspicy/web/specification/post.rb +9 -0
- data/lib/webspicy/web/specification/pre/global_request_headers.rb +38 -0
- data/lib/webspicy/web/specification/pre/robust_to_invalid_input.rb +70 -0
- data/lib/webspicy/web/specification/pre.rb +9 -0
- data/lib/webspicy/web/specification.rb +2 -0
- data/lib/webspicy.rb +1 -0
- data/spec/unit/tester/test_assertions.rb +77 -70
- data/spec/unit/web/specification/pre/test_global_request_headers.rb +50 -0
- metadata +85 -36
- data/lib/webspicy/specification/pre/global_request_headers.rb +0 -35
- data/lib/webspicy/specification/pre/robust_to_invalid_input.rb +0 -68
- data/spec/unit/specification/pre/test_global_request_headers.rb +0 -47
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 49f65b0370d22d9ff81cb309ec916a292c7c852e25d783b11b72de21d5466772
|
4
|
+
data.tar.gz: be4edc16dd8809028b243687b69da34591fec591c520ad72f9a5786a8f324130
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 89f2301b02f83bfe3e48286854e6c51c3b55dfd588d2dc1c4f1f4f343a8cde91eccbf592d3c7516975143f743869aa26719b2d5389b318b95284c5f9239a3761
|
7
|
+
data.tar.gz: f63b1e52e4ae1edebe0b2e9f6aaa7360a4208f8b9fe7bc917ef19a6710d28b003bad13bcea31874935528cf428444e7548d6bc6fac42d46526fc11b84a327dba
|
@@ -108,6 +108,18 @@ module Webspicy
|
|
108
108
|
end
|
109
109
|
end
|
110
110
|
|
111
|
+
def eq(path, expected = NO_ARG)
|
112
|
+
path, expected = '', path if expected == NO_ARG
|
113
|
+
target = @assertions.extract_path(@target, path)
|
114
|
+
Predicate.eq(target, expected).assert!
|
115
|
+
end
|
116
|
+
|
117
|
+
def eql(path, expected = NO_ARG)
|
118
|
+
path, expected = '', path if expected == NO_ARG
|
119
|
+
target = @assertions.extract_path(@target, path)
|
120
|
+
Predicate.eq(target, expected).assert!
|
121
|
+
end
|
122
|
+
|
111
123
|
private
|
112
124
|
|
113
125
|
def DateTime(str)
|
@@ -90,6 +90,16 @@ module Webspicy
|
|
90
90
|
!match(target, path, rx)
|
91
91
|
end
|
92
92
|
|
93
|
+
def eq(target, path, expected)
|
94
|
+
target = extract_path(target, path)
|
95
|
+
target == expected
|
96
|
+
end
|
97
|
+
|
98
|
+
def eql(target, path, expected)
|
99
|
+
target = extract_path(target, path)
|
100
|
+
value_equal(target, expected)
|
101
|
+
end
|
102
|
+
|
93
103
|
public
|
94
104
|
|
95
105
|
def extract_path(target, path = NO_ARG)
|
data/lib/webspicy/version.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
module Webspicy
|
2
|
+
module Web
|
3
|
+
class Specification
|
4
|
+
module Post
|
5
|
+
class ETagCachingProtocol
|
6
|
+
include Webspicy::Specification::Post
|
7
|
+
|
8
|
+
MATCH = /It supports the ETag\/If-None-Match caching protocol/
|
9
|
+
|
10
|
+
def self.match(service, descr)
|
11
|
+
return nil unless descr =~ MATCH
|
12
|
+
ETagCachingProtocol.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def check!
|
16
|
+
res = invocation.response
|
17
|
+
etag = res.headers['ETag']
|
18
|
+
fail!("No ETag response header found") unless etag
|
19
|
+
|
20
|
+
url, _ = test_case.specification.instantiate_url(test_case.params)
|
21
|
+
url = scope.to_real_url(url, test_case){|u,_| u }
|
22
|
+
|
23
|
+
response = client.api.get(url, {}, test_case.headers.merge({
|
24
|
+
'If-None-Match' => etag
|
25
|
+
}))
|
26
|
+
fail!("304 expected") unless response.status == 304
|
27
|
+
|
28
|
+
response = client.api.get(url, {}, test_case.headers.merge({
|
29
|
+
'If-None-Match' => "W/somethingelse"
|
30
|
+
}))
|
31
|
+
fail!("2xx expected") if response.status == 304
|
32
|
+
end
|
33
|
+
|
34
|
+
end # class ETagCachingProtocol
|
35
|
+
end # module Post
|
36
|
+
end # module Webspicy
|
37
|
+
end # module Web
|
38
|
+
end # class Specification
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Webspicy
|
2
|
+
module Web
|
3
|
+
class Specification
|
4
|
+
module Post
|
5
|
+
class LastModifiedCachingProtocol
|
6
|
+
include Webspicy::Specification::Post
|
7
|
+
|
8
|
+
MATCH = /It supports the Last-Modified\/If-Modified-Since caching protocol/
|
9
|
+
|
10
|
+
def self.match(service, descr)
|
11
|
+
return nil unless descr =~ MATCH
|
12
|
+
LastModifiedCachingProtocol.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def check!
|
16
|
+
res = invocation.response
|
17
|
+
last_modified = res.headers['Last-Modified']
|
18
|
+
fail!("No last-modified response header found") unless last_modified
|
19
|
+
|
20
|
+
# check it fits the HTTP-date format or fail
|
21
|
+
Time.httpdate(last_modified) rescue fail!("Not valid Last-Modified response header")
|
22
|
+
|
23
|
+
url, _ = test_case.specification.instantiate_url(test_case.params)
|
24
|
+
url = scope.to_real_url(url, test_case){|u,_| u }
|
25
|
+
|
26
|
+
response = client.api.get(url, {}, test_case.headers.merge({
|
27
|
+
'If-Modified-Since' => last_modified
|
28
|
+
}))
|
29
|
+
fail!("304 expected") unless response.status == 304
|
30
|
+
|
31
|
+
response = client.api.get(url, {}, test_case.headers.merge({
|
32
|
+
'If-Modified-Since' => "Thu, 08 Jun 1970 19:06:27 GMT"
|
33
|
+
}))
|
34
|
+
fail!("2xx expected") if response.status == 304
|
35
|
+
end
|
36
|
+
|
37
|
+
end # class LastModifiedCachingProtocol
|
38
|
+
end # module Post
|
39
|
+
end # module Webspicy
|
40
|
+
end # module Web
|
41
|
+
end # class Specification
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Webspicy
|
2
|
+
module Web
|
3
|
+
class Specification
|
4
|
+
module Post
|
5
|
+
class SemanticsPreservedByRefactoring
|
6
|
+
include ::Webspicy::Specification::Post
|
7
|
+
|
8
|
+
MATCH = /The data output semantics is preserved by the refactoring/
|
9
|
+
|
10
|
+
def self.match(service, descr)
|
11
|
+
return nil unless descr =~ MATCH
|
12
|
+
SemanticsPreservedByRefactoring.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def instrument
|
16
|
+
end
|
17
|
+
|
18
|
+
def check!
|
19
|
+
test_id = {
|
20
|
+
description: test_case.description,
|
21
|
+
seeds: test_case.seeds,
|
22
|
+
url: test_case.service.specification.url,
|
23
|
+
method: test_case.service.method,
|
24
|
+
params: test_case.params,
|
25
|
+
headers: test_case.headers.reject{|k| k == 'Authorization' },
|
26
|
+
metadata: test_case.metadata,
|
27
|
+
}
|
28
|
+
sha1 = Digest::SHA1.hexdigest(test_id.to_json)
|
29
|
+
|
30
|
+
record_file_path = config.folder/".morpheus/#{sha1}.key.json"
|
31
|
+
record_file_path.parent.mkdir_p
|
32
|
+
record_file_path.write(JSON.pretty_generate(test_id))
|
33
|
+
|
34
|
+
response = invocation.response
|
35
|
+
test_data = {
|
36
|
+
status: response.status,
|
37
|
+
headers: response.headers,
|
38
|
+
body: JSON.parse(response.body),
|
39
|
+
}
|
40
|
+
|
41
|
+
case ENV['MORPHEUS'].upcase
|
42
|
+
when 'RECORD'
|
43
|
+
expected_file_path = config.folder/".morpheus/#{sha1}.expected.json"
|
44
|
+
expected_file_path.write(JSON.pretty_generate(test_data))
|
45
|
+
when 'CHECK'
|
46
|
+
expected_file_path = config.folder/".morpheus/#{sha1}.expected.json"
|
47
|
+
expected = expected_file_path.load
|
48
|
+
|
49
|
+
actual_file_path = config.folder/".morpheus/#{sha1}.actual.json"
|
50
|
+
actual_file_path.write(JSON.pretty_generate(test_data))
|
51
|
+
actual = actual_file_path.load
|
52
|
+
|
53
|
+
fail!("Semantics has changed.") unless values_equal?(actual, expected)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def values_equal?(a, b)
|
60
|
+
Tester::Asserter.new(a).eql('', b)
|
61
|
+
end
|
62
|
+
|
63
|
+
end # SemanticsPreservedByRefactoring
|
64
|
+
end # module Post
|
65
|
+
end # module Webspicy
|
66
|
+
end # module Web
|
67
|
+
end # class Specification
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Webspicy
|
2
|
+
module Web
|
3
|
+
class Specification
|
4
|
+
module Pre
|
5
|
+
class GlobalRequestHeaders
|
6
|
+
include Pre
|
7
|
+
|
8
|
+
DEFAULT_OPTIONS = {}
|
9
|
+
|
10
|
+
def initialize(headers, options = {}, &bl)
|
11
|
+
@headers = headers
|
12
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
13
|
+
@matcher = bl
|
14
|
+
end
|
15
|
+
attr_reader :headers, :matcher
|
16
|
+
|
17
|
+
def match(service, pre)
|
18
|
+
if matcher
|
19
|
+
return self if matcher.call(service)
|
20
|
+
nil
|
21
|
+
else
|
22
|
+
self
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def instrument
|
27
|
+
extra = headers.reject{|k|
|
28
|
+
test_case.headers.has_key?(k)
|
29
|
+
}
|
30
|
+
puts "Instrumenting #{test_case.object_id}"
|
31
|
+
test_case.headers.merge!(extra)
|
32
|
+
end
|
33
|
+
|
34
|
+
end # class GlobalRequestHeaders
|
35
|
+
end # module Pre
|
36
|
+
end # class Specification
|
37
|
+
end # module Web
|
38
|
+
end # module Webspicy
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Webspicy
|
2
|
+
module Web
|
3
|
+
class Specification
|
4
|
+
module Pre
|
5
|
+
class RobustToInvalidInput
|
6
|
+
include Pre
|
7
|
+
|
8
|
+
def self.match(service, pre)
|
9
|
+
self.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def match(service, pre)
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def counterexamples(service)
|
17
|
+
spec = service.specification
|
18
|
+
first = service.examples.first
|
19
|
+
cexamples = []
|
20
|
+
cexamples += url_randomness_counterexamples(service, first) if first
|
21
|
+
cexamples += empty_input_counterexamples(service, first) if first
|
22
|
+
cexamples
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def url_randomness_counterexamples(service, first)
|
28
|
+
service.specification.url_placeholders.map{|p|
|
29
|
+
first.mutate({
|
30
|
+
:description => "it is robust to URL randomness on param `#{p}` (RobustToInvalidInput)",
|
31
|
+
:dress_params => false,
|
32
|
+
:params => first.params.merge(p => (SecureRandom.random_number * 100000000).to_i),
|
33
|
+
:expected => {
|
34
|
+
status: Support::StatusRange.str("4xx")
|
35
|
+
},
|
36
|
+
:assert => []
|
37
|
+
})
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def empty_input_counterexamples(service, first)
|
42
|
+
placeholders = service.specification.url_placeholders
|
43
|
+
empty_input = first.params.reject{|k| !placeholders.include?(k) }
|
44
|
+
if invalid_input?(service, empty_input)
|
45
|
+
[first.mutate({
|
46
|
+
:description => "it is robust to an invalid empty input (RobustToInvalidInput)",
|
47
|
+
:dress_params => false,
|
48
|
+
:params => empty_input,
|
49
|
+
:expected => {
|
50
|
+
status: Support::StatusRange.str("4xx")
|
51
|
+
},
|
52
|
+
:assert => []
|
53
|
+
})]
|
54
|
+
else
|
55
|
+
[]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def invalid_input?(service, empty_input)
|
60
|
+
service.input_schema.dress(empty_input)
|
61
|
+
false
|
62
|
+
rescue Finitio::Error
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
end # class RobustToInvalidInput
|
67
|
+
end # module Pre
|
68
|
+
end # class Specification
|
69
|
+
end # module Web
|
70
|
+
end # module Webspicy
|
@@ -63,6 +63,8 @@ module Webspicy
|
|
63
63
|
end # class Specification
|
64
64
|
end # module Web
|
65
65
|
end # module Webspicy
|
66
|
+
require_relative 'specification/pre'
|
67
|
+
require_relative 'specification/post'
|
66
68
|
require_relative 'specification/service'
|
67
69
|
require_relative 'specification/test_case'
|
68
70
|
require_relative 'specification/file_upload'
|
data/lib/webspicy.rb
CHANGED
@@ -3,92 +3,99 @@ require 'spec_helper'
|
|
3
3
|
module Webspicy
|
4
4
|
class Tester
|
5
5
|
describe Assertions do
|
6
|
-
include Assertions
|
7
6
|
|
8
|
-
|
7
|
+
class A
|
8
|
+
include Assertions
|
9
|
+
|
10
|
+
public :extract_path
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:a) do
|
14
|
+
A.new
|
15
|
+
end
|
9
16
|
|
10
17
|
it 'has an extract_path helper' do
|
11
18
|
target = { foo: "Hello", bar: { foo: "Hello" }, baz: [{ foo: "world" }] }
|
12
|
-
expect(extract_path(target)).to be(target)
|
13
|
-
expect(extract_path(target, nil)).to be(target)
|
14
|
-
expect(extract_path(target, '')).to be(target)
|
15
|
-
expect(extract_path(target, 'foo')).to eql("Hello")
|
16
|
-
expect(extract_path(target, 'bar/foo')).to eql("Hello")
|
17
|
-
expect(extract_path(target, 'baz/0')).to eql({ foo: "world" })
|
18
|
-
expect(extract_path(target, 'baz/0/foo')).to eql("world")
|
19
|
+
expect(a.extract_path(target)).to be(target)
|
20
|
+
expect(a.extract_path(target, nil)).to be(target)
|
21
|
+
expect(a.extract_path(target, '')).to be(target)
|
22
|
+
expect(a.extract_path(target, 'foo')).to eql("Hello")
|
23
|
+
expect(a.extract_path(target, 'bar/foo')).to eql("Hello")
|
24
|
+
expect(a.extract_path(target, 'baz/0')).to eql({ foo: "world" })
|
25
|
+
expect(a.extract_path(target, 'baz/0/foo')).to eql("world")
|
19
26
|
end
|
20
27
|
|
21
28
|
it 'has an includes() assertion' do
|
22
|
-
expect(includes [], 1).to be(false)
|
23
|
-
expect(includes [5, 1], 1).to be(true)
|
29
|
+
expect(a.includes [], 1).to be(false)
|
30
|
+
expect(a.includes [5, 1], 1).to be(true)
|
24
31
|
end
|
25
32
|
|
26
33
|
it 'has a notIncludes() assertion' do
|
27
|
-
expect(notIncludes [], 1).to be(true)
|
28
|
-
expect(notIncludes [5, 1], 1).to be(false)
|
34
|
+
expect(a.notIncludes [], 1).to be(true)
|
35
|
+
expect(a.notIncludes [5, 1], 1).to be(false)
|
29
36
|
end
|
30
37
|
|
31
38
|
it 'has an exists() assertion' do
|
32
|
-
expect(exists nil).to be(false)
|
33
|
-
expect(exists []).to be(true)
|
34
|
-
expect(exists [1]).to be(true)
|
35
|
-
expect(exists({ foo: [] }, 'foo')).to be(true)
|
36
|
-
expect(exists({ foo: {} }, 'foo')).to be(true)
|
37
|
-
expect(exists({ foo: {} }, 'foo/bar')).to be(false)
|
39
|
+
expect(a.exists nil).to be(false)
|
40
|
+
expect(a.exists []).to be(true)
|
41
|
+
expect(a.exists [1]).to be(true)
|
42
|
+
expect(a.exists({ foo: [] }, 'foo')).to be(true)
|
43
|
+
expect(a.exists({ foo: {} }, 'foo')).to be(true)
|
44
|
+
expect(a.exists({ foo: {} }, 'foo/bar')).to be(false)
|
38
45
|
end
|
39
46
|
|
40
47
|
it 'has a notExists() assertion' do
|
41
|
-
expect(notExists nil).to be(true)
|
42
|
-
expect(notExists []).to be(false)
|
43
|
-
expect(notExists [1]).to be(false)
|
44
|
-
expect(notExists({ foo: [] }, 'foo')).to be(false)
|
45
|
-
expect(notExists({ foo: {} }, 'foo')).to be(false)
|
46
|
-
expect(notExists({ foo: {} }, 'foo/bar')).to be(true)
|
48
|
+
expect(a.notExists nil).to be(true)
|
49
|
+
expect(a.notExists []).to be(false)
|
50
|
+
expect(a.notExists [1]).to be(false)
|
51
|
+
expect(a.notExists({ foo: [] }, 'foo')).to be(false)
|
52
|
+
expect(a.notExists({ foo: {} }, 'foo')).to be(false)
|
53
|
+
expect(a.notExists({ foo: {} }, 'foo/bar')).to be(true)
|
47
54
|
end
|
48
55
|
|
49
56
|
it 'has an empty() assertion' do
|
50
|
-
expect(empty []).to be(true)
|
51
|
-
expect(empty [1]).to be(false)
|
52
|
-
expect(empty({ foo: [] }, 'foo')).to be(true)
|
53
|
-
expect(empty({ foo: [1] }, 'foo')).to be(false)
|
57
|
+
expect(a.empty []).to be(true)
|
58
|
+
expect(a.empty [1]).to be(false)
|
59
|
+
expect(a.empty({ foo: [] }, 'foo')).to be(true)
|
60
|
+
expect(a.empty({ foo: [1] }, 'foo')).to be(false)
|
54
61
|
end
|
55
62
|
|
56
63
|
it 'has a notEmpty() assertion' do
|
57
|
-
expect(notEmpty []).to be(false)
|
58
|
-
expect(notEmpty [1]).to be(true)
|
59
|
-
expect(notEmpty({ foo: [] }, 'foo')).to be(false)
|
60
|
-
expect(notEmpty({ foo: [1] }, 'foo')).to be(true)
|
64
|
+
expect(a.notEmpty []).to be(false)
|
65
|
+
expect(a.notEmpty [1]).to be(true)
|
66
|
+
expect(a.notEmpty({ foo: [] }, 'foo')).to be(false)
|
67
|
+
expect(a.notEmpty({ foo: [1] }, 'foo')).to be(true)
|
61
68
|
end
|
62
69
|
|
63
70
|
it 'has a size() assertion' do
|
64
|
-
expect(size [], 0).to be(true)
|
65
|
-
expect(size [], 1).to be(false)
|
66
|
-
expect(size [12], 1).to be(true)
|
67
|
-
expect(size({ foo: [] }, 'foo', 0)).to be(true)
|
68
|
-
expect(size({ foo: [] }, 'foo', 1)).to be(false)
|
69
|
-
expect(size({ foo: ['bar'] }, 'foo', 1)).to be(true)
|
71
|
+
expect(a.size [], 0).to be(true)
|
72
|
+
expect(a.size [], 1).to be(false)
|
73
|
+
expect(a.size [12], 1).to be(true)
|
74
|
+
expect(a.size({ foo: [] }, 'foo', 0)).to be(true)
|
75
|
+
expect(a.size({ foo: [] }, 'foo', 1)).to be(false)
|
76
|
+
expect(a.size({ foo: ['bar'] }, 'foo', 1)).to be(true)
|
70
77
|
end
|
71
78
|
|
72
79
|
it 'has an idIn assertion' do
|
73
|
-
expect(idIn [{id: 1}, {id: 2}], [1, 2]).to be(true)
|
74
|
-
expect(idIn [{id: 1}, {id: 2}], [2, 1]).to be(true)
|
75
|
-
expect(idIn [{id: 1}, {id: 2}], [1, 3]).to be(false)
|
76
|
-
expect(idIn [{id: 1}, {id: 2}], [1]).to be(false)
|
77
|
-
expect(idIn({ foo: [{id: 1}, {id: 2}] }, 'foo', [1, 2])).to be(true)
|
78
|
-
|
79
|
-
expect(idIn({id: 1}, [1])).to be(true)
|
80
|
-
expect(idIn({id: 1}, [2])).to be(false)
|
80
|
+
expect(a.idIn [{id: 1}, {id: 2}], [1, 2]).to be(true)
|
81
|
+
expect(a.idIn [{id: 1}, {id: 2}], [2, 1]).to be(true)
|
82
|
+
expect(a.idIn [{id: 1}, {id: 2}], [1, 3]).to be(false)
|
83
|
+
expect(a.idIn [{id: 1}, {id: 2}], [1]).to be(false)
|
84
|
+
expect(a.idIn({ foo: [{id: 1}, {id: 2}] }, 'foo', [1, 2])).to be(true)
|
85
|
+
|
86
|
+
expect(a.idIn({id: 1}, [1])).to be(true)
|
87
|
+
expect(a.idIn({id: 1}, [2])).to be(false)
|
81
88
|
end
|
82
89
|
|
83
90
|
it 'has an idNotIn assertion' do
|
84
|
-
expect(idNotIn [{id: 1}, {id: 2}], [3]).to be(true)
|
85
|
-
expect(idNotIn [{id: 1}, {id: 2}], [3, 4]).to be(true)
|
86
|
-
expect(idNotIn [{id: 1}, {id: 2}], [1]).to be(false)
|
87
|
-
expect(idNotIn({ foo: [{id: 1}, {id: 2}] }, 'foo', [1])).to be(false)
|
88
|
-
expect(idNotIn({ foo: [{id: 1}, {id: 2}] }, 'foo', [3])).to be(true)
|
89
|
-
|
90
|
-
expect(idNotIn({id: 1}, [3])).to be(true)
|
91
|
-
expect(idNotIn({id: 1}, [1])).to be(false)
|
91
|
+
expect(a.idNotIn [{id: 1}, {id: 2}], [3]).to be(true)
|
92
|
+
expect(a.idNotIn [{id: 1}, {id: 2}], [3, 4]).to be(true)
|
93
|
+
expect(a.idNotIn [{id: 1}, {id: 2}], [1]).to be(false)
|
94
|
+
expect(a.idNotIn({ foo: [{id: 1}, {id: 2}] }, 'foo', [1])).to be(false)
|
95
|
+
expect(a.idNotIn({ foo: [{id: 1}, {id: 2}] }, 'foo', [3])).to be(true)
|
96
|
+
|
97
|
+
expect(a.idNotIn({id: 1}, [3])).to be(true)
|
98
|
+
expect(a.idNotIn({id: 1}, [1])).to be(false)
|
92
99
|
end
|
93
100
|
|
94
101
|
it 'has an idFD assertion' do
|
@@ -96,35 +103,35 @@ module Webspicy
|
|
96
103
|
{ id: 1, bar: "bar" },
|
97
104
|
{ id: 2, bar: "baz" }
|
98
105
|
] }
|
99
|
-
element = element_with_id(target, 'foo', 1)
|
100
|
-
expect(idFD(element, bar: "bar")).to be(true)
|
101
|
-
expect(idFD(element, bar: "baz")).to be(false)
|
102
|
-
expect(idFD(element, baz: "boz")).to be(false)
|
106
|
+
element = a.element_with_id(target, 'foo', 1)
|
107
|
+
expect(a.idFD(element, bar: "bar")).to be(true)
|
108
|
+
expect(a.idFD(element, bar: "baz")).to be(false)
|
109
|
+
expect(a.idFD(element, baz: "boz")).to be(false)
|
103
110
|
|
104
111
|
target = { foo: { id: 1, bar: "bar" } }
|
105
|
-
element = element_with_id(target, 'foo', 1)
|
106
|
-
expect(idFD(element, bar: "bar")).to be(true)
|
107
|
-
expect(idFD(element, bar: "baz")).to be(false)
|
108
|
-
expect(idFD(element, baz: "boz")).to be(false)
|
112
|
+
element = a.element_with_id(target, 'foo', 1)
|
113
|
+
expect(a.idFD(element, bar: "bar")).to be(true)
|
114
|
+
expect(a.idFD(element, bar: "baz")).to be(false)
|
115
|
+
expect(a.idFD(element, baz: "boz")).to be(false)
|
109
116
|
end
|
110
117
|
|
111
118
|
it 'has a pathFD assertion' do
|
112
119
|
target = { foo: { bar: "baz"} }
|
113
|
-
expect(pathFD(target, 'foo', bar: "baz")).to be(true)
|
114
|
-
expect(pathFD(target, 'foo', bar: "boz")).to be(false)
|
115
|
-
expect(pathFD(target, 'foo', boz: "biz")).to be(false)
|
120
|
+
expect(a.pathFD(target, 'foo', bar: "baz")).to be(true)
|
121
|
+
expect(a.pathFD(target, 'foo', bar: "boz")).to be(false)
|
122
|
+
expect(a.pathFD(target, 'foo', boz: "biz")).to be(false)
|
116
123
|
end
|
117
124
|
|
118
125
|
it 'has a match assertion' do
|
119
126
|
target = "hello world"
|
120
|
-
expect(match(target, '', /world/)).to be(true)
|
121
|
-
expect(match(target, '', /foobar/)).to be(false)
|
127
|
+
expect(a.match(target, '', /world/)).to be(true)
|
128
|
+
expect(a.match(target, '', /foobar/)).to be(false)
|
122
129
|
end
|
123
130
|
|
124
131
|
it 'has a notMatch assertion' do
|
125
132
|
target = "hello world"
|
126
|
-
expect(notMatch(target, '', /world/)).to be(false)
|
127
|
-
expect(notMatch(target, '', /foobar/)).to be(true)
|
133
|
+
expect(a.notMatch(target, '', /world/)).to be(false)
|
134
|
+
expect(a.notMatch(target, '', /foobar/)).to be(true)
|
128
135
|
end
|
129
136
|
|
130
137
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'webspicy/web/specification/pre/global_request_headers'
|
3
|
+
|
4
|
+
module Webspicy
|
5
|
+
module Web
|
6
|
+
class Specification
|
7
|
+
module Pre
|
8
|
+
describe GlobalRequestHeaders do
|
9
|
+
let(:gbr){
|
10
|
+
GlobalRequestHeaders.new('Accept' => 'application/json')
|
11
|
+
}
|
12
|
+
|
13
|
+
def instrument(tc)
|
14
|
+
t = OpenStruct.new(test_case: tc)
|
15
|
+
gbr.bind(t).instrument
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "instrument" do
|
19
|
+
it 'injects the headers' do
|
20
|
+
tc = Web::Specification::TestCase.new({})
|
21
|
+
instrument(tc)
|
22
|
+
expect(tc.headers['Accept']).to eql("application/json")
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'keeps original headers unchanged' do
|
26
|
+
tc = Web::Specification::TestCase.new({
|
27
|
+
headers: {
|
28
|
+
'Content-Type' => 'text/plain'
|
29
|
+
}
|
30
|
+
})
|
31
|
+
instrument(tc)
|
32
|
+
expect(tc.headers['Content-Type']).to eql("text/plain")
|
33
|
+
expect(tc.headers['Accept']).to eql("application/json")
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'has low precedence' do
|
37
|
+
tc = Web::Specification::TestCase.new({
|
38
|
+
headers: {
|
39
|
+
'Accept' => 'text/plain'
|
40
|
+
}
|
41
|
+
})
|
42
|
+
instrument(tc)
|
43
|
+
expect(tc.headers['Accept']).to eql("text/plain")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: webspicy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.26.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bernard Lambeau
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-06-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -28,9 +28,9 @@ dependencies:
|
|
28
28
|
name: sinatra
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: '3.0'
|
34
34
|
- - "<"
|
35
35
|
- !ruby/object:Gem::Version
|
36
36
|
version: '4.0'
|
@@ -38,9 +38,9 @@ dependencies:
|
|
38
38
|
prerelease: false
|
39
39
|
version_requirements: !ruby/object:Gem::Requirement
|
40
40
|
requirements:
|
41
|
-
- - "
|
41
|
+
- - ">"
|
42
42
|
- !ruby/object:Gem::Version
|
43
|
-
version:
|
43
|
+
version: '3.0'
|
44
44
|
- - "<"
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: '4.0'
|
@@ -62,30 +62,42 @@ dependencies:
|
|
62
62
|
name: rspec_junit_formatter
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
64
64
|
requirements:
|
65
|
-
- - "
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0.6'
|
68
|
+
- - "<"
|
66
69
|
- !ruby/object:Gem::Version
|
67
|
-
version: 0.
|
70
|
+
version: '0.7'
|
68
71
|
type: :development
|
69
72
|
prerelease: false
|
70
73
|
version_requirements: !ruby/object:Gem::Requirement
|
71
74
|
requirements:
|
72
|
-
- - "
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0.6'
|
78
|
+
- - "<"
|
73
79
|
- !ruby/object:Gem::Version
|
74
|
-
version: 0.
|
80
|
+
version: '0.7'
|
75
81
|
- !ruby/object:Gem::Dependency
|
76
82
|
name: rack-test
|
77
83
|
requirement: !ruby/object:Gem::Requirement
|
78
84
|
requirements:
|
79
|
-
- - "
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '2.0'
|
88
|
+
- - "<"
|
80
89
|
- !ruby/object:Gem::Version
|
81
|
-
version: 0
|
90
|
+
version: '3.0'
|
82
91
|
type: :runtime
|
83
92
|
prerelease: false
|
84
93
|
version_requirements: !ruby/object:Gem::Requirement
|
85
94
|
requirements:
|
86
|
-
- - "
|
95
|
+
- - ">="
|
87
96
|
- !ruby/object:Gem::Version
|
88
|
-
version: 0
|
97
|
+
version: '2.0'
|
98
|
+
- - "<"
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '3.0'
|
89
101
|
- !ruby/object:Gem::Dependency
|
90
102
|
name: finitio
|
91
103
|
requirement: !ruby/object:Gem::Requirement
|
@@ -110,16 +122,22 @@ dependencies:
|
|
110
122
|
name: http
|
111
123
|
requirement: !ruby/object:Gem::Requirement
|
112
124
|
requirements:
|
113
|
-
- - "
|
125
|
+
- - ">="
|
114
126
|
- !ruby/object:Gem::Version
|
115
|
-
version:
|
127
|
+
version: '5.0'
|
128
|
+
- - "<"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '6.0'
|
116
131
|
type: :runtime
|
117
132
|
prerelease: false
|
118
133
|
version_requirements: !ruby/object:Gem::Requirement
|
119
134
|
requirements:
|
120
|
-
- - "
|
135
|
+
- - ">="
|
121
136
|
- !ruby/object:Gem::Version
|
122
|
-
version:
|
137
|
+
version: '5.0'
|
138
|
+
- - "<"
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '6.0'
|
123
141
|
- !ruby/object:Gem::Dependency
|
124
142
|
name: path
|
125
143
|
requirement: !ruby/object:Gem::Requirement
|
@@ -138,22 +156,22 @@ dependencies:
|
|
138
156
|
name: rack-robustness
|
139
157
|
requirement: !ruby/object:Gem::Requirement
|
140
158
|
requirements:
|
141
|
-
- - "~>"
|
142
|
-
- !ruby/object:Gem::Version
|
143
|
-
version: '1.1'
|
144
159
|
- - ">="
|
145
160
|
- !ruby/object:Gem::Version
|
146
|
-
version: 1.
|
161
|
+
version: '1.2'
|
162
|
+
- - "<"
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: '2.0'
|
147
165
|
type: :runtime
|
148
166
|
prerelease: false
|
149
167
|
version_requirements: !ruby/object:Gem::Requirement
|
150
168
|
requirements:
|
151
|
-
- - "~>"
|
152
|
-
- !ruby/object:Gem::Version
|
153
|
-
version: '1.1'
|
154
169
|
- - ">="
|
155
170
|
- !ruby/object:Gem::Version
|
156
|
-
version: 1.
|
171
|
+
version: '1.2'
|
172
|
+
- - "<"
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '2.0'
|
157
175
|
- !ruby/object:Gem::Dependency
|
158
176
|
name: mustermann
|
159
177
|
requirement: !ruby/object:Gem::Requirement
|
@@ -200,16 +218,22 @@ dependencies:
|
|
200
218
|
name: openapi3_parser
|
201
219
|
requirement: !ruby/object:Gem::Requirement
|
202
220
|
requirements:
|
203
|
-
- - "
|
221
|
+
- - ">="
|
222
|
+
- !ruby/object:Gem::Version
|
223
|
+
version: '0.9'
|
224
|
+
- - "<"
|
204
225
|
- !ruby/object:Gem::Version
|
205
|
-
version: 0.
|
226
|
+
version: '0.10'
|
206
227
|
type: :runtime
|
207
228
|
prerelease: false
|
208
229
|
version_requirements: !ruby/object:Gem::Requirement
|
209
230
|
requirements:
|
210
|
-
- - "
|
231
|
+
- - ">="
|
232
|
+
- !ruby/object:Gem::Version
|
233
|
+
version: '0.9'
|
234
|
+
- - "<"
|
211
235
|
- !ruby/object:Gem::Version
|
212
|
-
version: 0.
|
236
|
+
version: '0.10'
|
213
237
|
- !ruby/object:Gem::Dependency
|
214
238
|
name: mustache
|
215
239
|
requirement: !ruby/object:Gem::Requirement
|
@@ -266,6 +290,26 @@ dependencies:
|
|
266
290
|
- - "~>"
|
267
291
|
- !ruby/object:Gem::Version
|
268
292
|
version: '3.7'
|
293
|
+
- !ruby/object:Gem::Dependency
|
294
|
+
name: predicate
|
295
|
+
requirement: !ruby/object:Gem::Requirement
|
296
|
+
requirements:
|
297
|
+
- - ">="
|
298
|
+
- !ruby/object:Gem::Version
|
299
|
+
version: '2.8'
|
300
|
+
- - "<"
|
301
|
+
- !ruby/object:Gem::Version
|
302
|
+
version: '3.0'
|
303
|
+
type: :runtime
|
304
|
+
prerelease: false
|
305
|
+
version_requirements: !ruby/object:Gem::Requirement
|
306
|
+
requirements:
|
307
|
+
- - ">="
|
308
|
+
- !ruby/object:Gem::Version
|
309
|
+
version: '2.8'
|
310
|
+
- - "<"
|
311
|
+
- !ruby/object:Gem::Version
|
312
|
+
version: '3.0'
|
269
313
|
description: Webspicy helps testing web services as software operation black boxes
|
270
314
|
email: blambeau@gmail.com
|
271
315
|
executables:
|
@@ -300,8 +344,6 @@ files:
|
|
300
344
|
- lib/webspicy/specification/post/missing_condition_impl.rb
|
301
345
|
- lib/webspicy/specification/post/unexpected_condition_impl.rb
|
302
346
|
- lib/webspicy/specification/pre.rb
|
303
|
-
- lib/webspicy/specification/pre/global_request_headers.rb
|
304
|
-
- lib/webspicy/specification/pre/robust_to_invalid_input.rb
|
305
347
|
- lib/webspicy/specification/service.rb
|
306
348
|
- lib/webspicy/specification/test_case.rb
|
307
349
|
- lib/webspicy/support.rb
|
@@ -361,6 +403,13 @@ files:
|
|
361
403
|
- lib/webspicy/web/openapi/generator.rb
|
362
404
|
- lib/webspicy/web/specification.rb
|
363
405
|
- lib/webspicy/web/specification/file_upload.rb
|
406
|
+
- lib/webspicy/web/specification/post.rb
|
407
|
+
- lib/webspicy/web/specification/post/etag_caching_protocol.rb
|
408
|
+
- lib/webspicy/web/specification/post/last_modified_caching_protocol.rb
|
409
|
+
- lib/webspicy/web/specification/post/semantics_preserved_by_refactoring.rb
|
410
|
+
- lib/webspicy/web/specification/pre.rb
|
411
|
+
- lib/webspicy/web/specification/pre/global_request_headers.rb
|
412
|
+
- lib/webspicy/web/specification/pre/robust_to_invalid_input.rb
|
364
413
|
- lib/webspicy/web/specification/service.rb
|
365
414
|
- lib/webspicy/web/specification/test_case.rb
|
366
415
|
- spec/spec_helper.rb
|
@@ -369,7 +418,6 @@ files:
|
|
369
418
|
- spec/unit/configuration/scope/test_each_specification.rb
|
370
419
|
- spec/unit/configuration/scope/test_expand_example.rb
|
371
420
|
- spec/unit/configuration/scope/test_to_real_url.rb
|
372
|
-
- spec/unit/specification/pre/test_global_request_headers.rb
|
373
421
|
- spec/unit/specification/service/test_dress_params.rb
|
374
422
|
- spec/unit/specification/test_case/test_mutate.rb
|
375
423
|
- spec/unit/specification/test_condition.rb
|
@@ -392,6 +440,7 @@ files:
|
|
392
440
|
- spec/unit/web/inferer/test_inferer.rb
|
393
441
|
- spec/unit/web/mocker/test_mocker.rb
|
394
442
|
- spec/unit/web/openapi/test_generator.rb
|
443
|
+
- spec/unit/web/specification/pre/test_global_request_headers.rb
|
395
444
|
- spec/unit/web/specification/test_instantiate_url.rb
|
396
445
|
- spec/unit/web/specification/test_url_placeholders.rb
|
397
446
|
- tasks/gem.rake
|
@@ -400,7 +449,7 @@ homepage: http://github.com/enspirit/webspicy
|
|
400
449
|
licenses:
|
401
450
|
- MIT
|
402
451
|
metadata: {}
|
403
|
-
post_install_message:
|
452
|
+
post_install_message:
|
404
453
|
rdoc_options: []
|
405
454
|
require_paths:
|
406
455
|
- lib
|
@@ -416,7 +465,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
416
465
|
version: '0'
|
417
466
|
requirements: []
|
418
467
|
rubygems_version: 3.3.26
|
419
|
-
signing_key:
|
468
|
+
signing_key:
|
420
469
|
specification_version: 4
|
421
470
|
summary: Webspicy helps testing web services as software operation black boxes!
|
422
471
|
test_files: []
|
@@ -1,35 +0,0 @@
|
|
1
|
-
module Webspicy
|
2
|
-
class Specification
|
3
|
-
module Pre
|
4
|
-
class GlobalRequestHeaders
|
5
|
-
include Pre
|
6
|
-
|
7
|
-
DEFAULT_OPTIONS = {}
|
8
|
-
|
9
|
-
def initialize(headers, options = {}, &bl)
|
10
|
-
@headers = headers
|
11
|
-
@options = DEFAULT_OPTIONS.merge(options)
|
12
|
-
@matcher = bl
|
13
|
-
end
|
14
|
-
attr_reader :headers, :matcher
|
15
|
-
|
16
|
-
def match(service, pre)
|
17
|
-
if matcher
|
18
|
-
return self if matcher.call(service)
|
19
|
-
nil
|
20
|
-
else
|
21
|
-
self
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def instrument
|
26
|
-
extra = headers.reject{|k|
|
27
|
-
test_case.headers.has_key?(k)
|
28
|
-
}
|
29
|
-
test_case.headers.merge!(extra)
|
30
|
-
end
|
31
|
-
|
32
|
-
end # class GlobalRequestHeaders
|
33
|
-
end # module Pre
|
34
|
-
end # class Specification
|
35
|
-
end # module Webspicy
|
@@ -1,68 +0,0 @@
|
|
1
|
-
module Webspicy
|
2
|
-
class Specification
|
3
|
-
module Pre
|
4
|
-
class RobustToInvalidInput
|
5
|
-
include Pre
|
6
|
-
|
7
|
-
def self.match(service, pre)
|
8
|
-
self.new
|
9
|
-
end
|
10
|
-
|
11
|
-
def match(service, pre)
|
12
|
-
self
|
13
|
-
end
|
14
|
-
|
15
|
-
def counterexamples(service)
|
16
|
-
spec = service.specification
|
17
|
-
first = service.examples.first
|
18
|
-
cexamples = []
|
19
|
-
cexamples += url_randomness_counterexamples(service, first) if first
|
20
|
-
cexamples += empty_input_counterexamples(service, first) if first
|
21
|
-
cexamples
|
22
|
-
end
|
23
|
-
|
24
|
-
protected
|
25
|
-
|
26
|
-
def url_randomness_counterexamples(service, first)
|
27
|
-
service.specification.url_placeholders.map{|p|
|
28
|
-
first.mutate({
|
29
|
-
:description => "it is robust to URL randomness on param `#{p}` (RobustToInvalidInput)",
|
30
|
-
:dress_params => false,
|
31
|
-
:params => first.params.merge(p => (SecureRandom.random_number * 100000000).to_i),
|
32
|
-
:expected => {
|
33
|
-
status: Support::StatusRange.str("4xx")
|
34
|
-
},
|
35
|
-
:assert => []
|
36
|
-
})
|
37
|
-
}
|
38
|
-
end
|
39
|
-
|
40
|
-
def empty_input_counterexamples(service, first)
|
41
|
-
placeholders = service.specification.url_placeholders
|
42
|
-
empty_input = first.params.reject{|k| !placeholders.include?(k) }
|
43
|
-
if invalid_input?(service, empty_input)
|
44
|
-
[first.mutate({
|
45
|
-
:description => "it is robust to an invalid empty input (RobustToInvalidInput)",
|
46
|
-
:dress_params => false,
|
47
|
-
:params => empty_input,
|
48
|
-
:expected => {
|
49
|
-
status: Support::StatusRange.str("4xx")
|
50
|
-
},
|
51
|
-
:assert => []
|
52
|
-
})]
|
53
|
-
else
|
54
|
-
[]
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def invalid_input?(service, empty_input)
|
59
|
-
service.input_schema.dress(empty_input)
|
60
|
-
false
|
61
|
-
rescue Finitio::Error
|
62
|
-
true
|
63
|
-
end
|
64
|
-
|
65
|
-
end # class RobustToInvalidInput
|
66
|
-
end # module Pre
|
67
|
-
end # class Specification
|
68
|
-
end # module Webspicy
|
@@ -1,47 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module Webspicy
|
4
|
-
class Specification
|
5
|
-
module Pre
|
6
|
-
describe GlobalRequestHeaders do
|
7
|
-
let(:gbr){
|
8
|
-
GlobalRequestHeaders.new('Accept' => 'application/json')
|
9
|
-
}
|
10
|
-
|
11
|
-
def instrument(tc)
|
12
|
-
t = OpenStruct.new(test_case: tc)
|
13
|
-
gbr.bind(t).instrument
|
14
|
-
end
|
15
|
-
|
16
|
-
describe "instrument" do
|
17
|
-
it 'injects the headers' do
|
18
|
-
tc = Web::Specification::TestCase.new({})
|
19
|
-
instrument(tc)
|
20
|
-
expect(tc.headers['Accept']).to eql("application/json")
|
21
|
-
end
|
22
|
-
|
23
|
-
it 'keeps original headers unchanged' do
|
24
|
-
tc = Web::Specification::TestCase.new({
|
25
|
-
headers: {
|
26
|
-
'Content-Type' => 'text/plain'
|
27
|
-
}
|
28
|
-
})
|
29
|
-
instrument(tc)
|
30
|
-
expect(tc.headers['Content-Type']).to eql("text/plain")
|
31
|
-
expect(tc.headers['Accept']).to eql("application/json")
|
32
|
-
end
|
33
|
-
|
34
|
-
it 'has low precedence' do
|
35
|
-
tc = Web::Specification::TestCase.new({
|
36
|
-
headers: {
|
37
|
-
'Accept' => 'text/plain'
|
38
|
-
}
|
39
|
-
})
|
40
|
-
instrument(tc)
|
41
|
-
expect(tc.headers['Accept']).to eql("text/plain")
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|