brine-dsl 0.5.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 +7 -0
- data/.gitignore +4 -0
- data/.ruby-version +1 -0
- data/.travis.yml +11 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +123 -0
- data/Guardfile +12 -0
- data/LICENSE +21 -0
- data/README.md +137 -0
- data/Rakefile +32 -0
- data/brine-dsl.gemspec +32 -0
- data/config/cucumber.yml +2 -0
- data/docs/build.gradle +19 -0
- data/docs/cookbook.html +567 -0
- data/docs/gradle/wrapper/gradle-wrapper.jar +0 -0
- data/docs/gradle/wrapper/gradle-wrapper.properties +6 -0
- data/docs/gradlew +172 -0
- data/docs/gradlew.bat +84 -0
- data/docs/guide.html +1149 -0
- data/docs/index.html +472 -0
- data/docs/specs.html +1672 -0
- data/docs/src/cookbook.adoc +87 -0
- data/docs/src/guide.adoc +427 -0
- data/docs/src/index.adoc +16 -0
- data/docs/src/spec.erb +121 -0
- data/docs/src/specs.adoc +24 -0
- data/features/argument_transforms/boolean.feature +37 -0
- data/features/argument_transforms/datetime.feature +45 -0
- data/features/argument_transforms/integer.feature +41 -0
- data/features/argument_transforms/list.feature +46 -0
- data/features/argument_transforms/object.feature +66 -0
- data/features/argument_transforms/quoted.feature +41 -0
- data/features/argument_transforms/regex.feature +40 -0
- data/features/argument_transforms/template.feature +46 -0
- data/features/argument_transforms/whitespace.feature +51 -0
- data/features/assertions/is_a_valid.feature +184 -0
- data/features/assertions/is_equal_to.feature +60 -0
- data/features/assertions/is_including.feature +29 -0
- data/features/assertions/is_matching.feature +35 -0
- data/features/deprecations/replaced_with.feature +35 -0
- data/features/request_construction/basic.feature +29 -0
- data/features/request_construction/body.feature +26 -0
- data/features/request_construction/clearing.feature +46 -0
- data/features/request_construction/headers.feature +94 -0
- data/features/request_construction/params.feature +60 -0
- data/features/resource_cleanup/cleanup.feature +86 -0
- data/features/selectors/all.feature +55 -0
- data/features/selectors/any.feature +48 -0
- data/features/step_definitions/test_steps.rb +5 -0
- data/features/support/env.rb +10 -0
- data/lib/brine/cleaner_upper.rb +62 -0
- data/lib/brine/coercer.rb +18 -0
- data/lib/brine/hooks.rb +4 -0
- data/lib/brine/mustache_binder.rb +25 -0
- data/lib/brine/requester.rb +125 -0
- data/lib/brine/rest_steps.rb +138 -0
- data/lib/brine/selector.rb +66 -0
- data/lib/brine/step_definitions/assertions.rb +37 -0
- data/lib/brine/step_definitions/assignment.rb +13 -0
- data/lib/brine/step_definitions/cleanup.rb +4 -0
- data/lib/brine/step_definitions/request_construction.rb +19 -0
- data/lib/brine/step_definitions/selection.rb +37 -0
- data/lib/brine/test_steps.rb +138 -0
- data/lib/brine/transforms.rb +81 -0
- data/lib/brine/type_checks.rb +35 -0
- data/lib/brine/util.rb +35 -0
- data/lib/brine.rb +39 -0
- data/tutorial/missing.feature +5 -0
- data/tutorial/post_matching.feature +12 -0
- data/tutorial/post_status.feature +10 -0
- data/tutorial/support/env.rb +2 -0
- metadata +306 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
# requester.rb - Provide request construction and response storage
|
2
|
+
|
3
|
+
require 'oauth2'
|
4
|
+
require 'faraday_middleware'
|
5
|
+
|
6
|
+
# Parameter object used to configure OAuth2 middleware
|
7
|
+
# Also used to provide basic DSL for configuration
|
8
|
+
class OAuth2Params
|
9
|
+
attr_accessor :token, :token_type
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@token_type = 'bearer'
|
13
|
+
end
|
14
|
+
|
15
|
+
def fetch_from(id, secret, opts)
|
16
|
+
@token = OAuth2::Client.new(id, secret, opts)
|
17
|
+
.client_credentials.get_token.token
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Construct a Faraday client to be used to send built requests
|
22
|
+
module ClientBuilding
|
23
|
+
|
24
|
+
# authenticate using provided info and save token for use in later requests
|
25
|
+
def use_oauth2_token(&block)
|
26
|
+
@oauth2 = OAuth2Params.new
|
27
|
+
@oauth2.instance_eval(&block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def with_oauth2_token(&block)
|
31
|
+
use_oauth2_token(&block)
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def client_for_host(host, logging: ENV['BRINE_LOG_HTTP'])
|
36
|
+
Faraday.new(host) do |conn|
|
37
|
+
conn.request :json
|
38
|
+
|
39
|
+
if @oauth2
|
40
|
+
conn.request :oauth2, @oauth2.token, :token_type => @oauth2.token_type
|
41
|
+
end
|
42
|
+
|
43
|
+
if logging
|
44
|
+
conn.response :logger, nil, :bodies => (logging.casecmp('DEBUG') == 0)
|
45
|
+
end
|
46
|
+
|
47
|
+
conn.response :json, :content_type => /\bjson$/
|
48
|
+
|
49
|
+
conn.adapter Faraday.default_adapter
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class ClientBuilder
|
55
|
+
include ClientBuilding
|
56
|
+
end
|
57
|
+
|
58
|
+
# Module in charge of constructing requests and saving responses
|
59
|
+
module Requesting
|
60
|
+
include ClientBuilding
|
61
|
+
|
62
|
+
# Utility Methods
|
63
|
+
#
|
64
|
+
# Normalize an HTTP method passed from a specification into a form
|
65
|
+
# expected by the HTTP client library (lowercased symbol)
|
66
|
+
def parse_method(method)
|
67
|
+
method.downcase.to_sym
|
68
|
+
end
|
69
|
+
|
70
|
+
def set_client(client)
|
71
|
+
@client = client
|
72
|
+
end
|
73
|
+
|
74
|
+
# return Faraday client object so that it could be used directly
|
75
|
+
# or passed to another object
|
76
|
+
def client
|
77
|
+
@client ||= client_for_host((ENV['ROOT_URL'] || 'http://localhost:8080'),
|
78
|
+
logging: ENV['BRINE_LOG_HTTP'])
|
79
|
+
end
|
80
|
+
|
81
|
+
# clear out any previously built request state and set defaults
|
82
|
+
def reset_request
|
83
|
+
@params = @headers = @body = nil
|
84
|
+
end
|
85
|
+
|
86
|
+
# store the provided body in the request options being built
|
87
|
+
# overriding any previously provided object
|
88
|
+
def set_request_body(obj)
|
89
|
+
@body = obj
|
90
|
+
end
|
91
|
+
|
92
|
+
# send a request using method to url using whatever options
|
93
|
+
# have been built for the present request
|
94
|
+
def send_request(method, url)
|
95
|
+
@response = client.run_request(method, url, @body, headers) do |req|
|
96
|
+
req.params = params
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# getter for the latest response returned
|
101
|
+
def response
|
102
|
+
@response
|
103
|
+
end
|
104
|
+
|
105
|
+
def headers
|
106
|
+
@headers ||= {content_type: 'application/json'}
|
107
|
+
end
|
108
|
+
|
109
|
+
def add_header(k, v)
|
110
|
+
headers[k] = v
|
111
|
+
end
|
112
|
+
|
113
|
+
def params
|
114
|
+
@params ||= {}
|
115
|
+
end
|
116
|
+
|
117
|
+
def add_request_param(k, v)
|
118
|
+
params[k] = v
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
class Requester
|
124
|
+
include Requesting
|
125
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'brine/util'
|
3
|
+
require 'brine/selector'
|
4
|
+
require 'jsonpath'
|
5
|
+
|
6
|
+
# Chopping Block
|
7
|
+
When(/^the request parameter `([^`]*)` is set to `([^`]*)`$/) do |param, value|
|
8
|
+
replaced_with('When', "the request query paramter `#{param}` is assigned `#{value}`", '0.6')
|
9
|
+
end
|
10
|
+
Then(/^the response body is the list:$/) do |table|
|
11
|
+
replaced_with('Then', "the value of the response body is equal to:\n\"\"\"#{table.hashes.to_json}\"\"\"", '0.6')
|
12
|
+
end
|
13
|
+
Then(/^the raw response body is:$/) do |text|
|
14
|
+
warn 'DEPRECATION: This step will be removed in version 0.6'
|
15
|
+
expect(response.body).to eq text
|
16
|
+
end
|
17
|
+
Then(/^the response body has `([^`]*)` with a value equal to `([^`]*)`$/) do |child, value|
|
18
|
+
replaced_with('Then', "the value of the response body child `#{child}` is equal to `#{value}`", '0.6')
|
19
|
+
end
|
20
|
+
|
21
|
+
Then(/^the response #{RESPONSE_ATTRIBUTES} has `([^`]*)` with a value including `([^`]*)`$/) do
|
22
|
+
|attribute, member, value|
|
23
|
+
replaced_with('Then', "the value of the response body child `#{child}` is including `#{value}`", '0.6')
|
24
|
+
end
|
25
|
+
|
26
|
+
# This file is legacy or unsorted steps which will be deprecated or moved into
|
27
|
+
# more appropriate homes
|
28
|
+
|
29
|
+
def not_if(val) val ? :not_to : :to end
|
30
|
+
|
31
|
+
# Return a table that that is a key value pair in a format ready for consumption
|
32
|
+
def kv_table(table)
|
33
|
+
transform_table!(table).rows_hash
|
34
|
+
end
|
35
|
+
|
36
|
+
Then(/^the response body is a list which all are (\w+)$/) do |matcher|
|
37
|
+
pass_it = method(matcher.to_sym).call
|
38
|
+
expect(response_body_child.first).to all(pass_it)
|
39
|
+
end
|
40
|
+
|
41
|
+
#TODO: The binding environment should be able to be accessed directly
|
42
|
+
# without requiring a custom step
|
43
|
+
When(/^`([^`]*)` is bound to `([^`]*)` from the response body$/) do |name, path|
|
44
|
+
binding[name] = response_body_child(path).first
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Response attribute (non-body) assertions
|
49
|
+
#
|
50
|
+
Then(/^the response #{RESPONSE_ATTRIBUTES} has `([^`]*)` with a value that is not empty$/) do
|
51
|
+
|attribute, member|
|
52
|
+
expect(response).to have_attributes(attribute.to_sym => include(member.to_sym => be_not_empty))
|
53
|
+
end
|
54
|
+
|
55
|
+
Then(/^the response #{RESPONSE_ATTRIBUTES} includes? the entries:$/) do |attribute, table|
|
56
|
+
expect(response).to have_attributes(attribute.to_sym => include(kv_table(table)))
|
57
|
+
end
|
58
|
+
|
59
|
+
Then(/^the response #{RESPONSE_ATTRIBUTES} contains? null fields:$/) do |attribute, table|
|
60
|
+
expect(response)
|
61
|
+
.to have_attributes(attribute.to_sym =>
|
62
|
+
include(table.raw.flatten.collect{|v| [v, be_nil]}.to_h))
|
63
|
+
end
|
64
|
+
|
65
|
+
Then(/^the response #{RESPONSE_ATTRIBUTES} contains? non null fields:$/) do |attribute, table|
|
66
|
+
expect(response)
|
67
|
+
.to have_attributes(attribute.to_sym =>
|
68
|
+
include(table.raw.flatten.collect{|v| [v, be_not_nil]}.to_h))
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Response body assertions
|
73
|
+
#
|
74
|
+
Then(/^the response body does not contain fields:$/) do |table|
|
75
|
+
expect(response_body_child.first.keys).to_not include(*table.raw.flatten)
|
76
|
+
end
|
77
|
+
|
78
|
+
Then(/^the response body has `([^`]*)` which (in|ex)cludes? the entries:$/) do
|
79
|
+
|child, in_or_ex, table|
|
80
|
+
expect(response_body_child(child).first)
|
81
|
+
.send(not_if(in_or_ex=='ex'),
|
82
|
+
include(kv_table(table)))
|
83
|
+
end
|
84
|
+
|
85
|
+
Then(/^the response body is a list of length (\d+)$/) do |length|
|
86
|
+
expect(response_body_child.first).to have_attributes(length: length)
|
87
|
+
end
|
88
|
+
|
89
|
+
#TODO: Maybe worth optimizing these 2 to O(n) after tests are in place
|
90
|
+
Then(/^the response body is a list sorted by `([^`]*)` ascending$/) do |path|
|
91
|
+
values = response_body_child(path)
|
92
|
+
expect(values).to eq values.sort{|a,b| a.to_s.downcase <=> b.to_s.downcase}
|
93
|
+
end
|
94
|
+
|
95
|
+
Then(/^the response body is a list sorted by `([^`]*)` descending$/) do |path|
|
96
|
+
values = response_body_child(path)
|
97
|
+
expect(values).to eq values.sort{|a,b| b.to_s.downcase <=> a.to_s.downcase}
|
98
|
+
end
|
99
|
+
|
100
|
+
Then(/^the response body is a list (with|without) an entry containing:$/) do |with, data|
|
101
|
+
expect(response_body_child.first)
|
102
|
+
.send(not_if(with == 'without'),
|
103
|
+
include(include(kv_table(data))))
|
104
|
+
end
|
105
|
+
|
106
|
+
Then(/^the response body is (\w+)$/) do |matcher|
|
107
|
+
pass_it = method(matcher.to_sym).call
|
108
|
+
expect(response_body_child.first).to pass_it
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Polling assertions
|
113
|
+
#
|
114
|
+
Then(/^a non\-empty list is eventually returned at `([^`]*)`$/) do |path|
|
115
|
+
retry_for(120, 3) do
|
116
|
+
send_request(parse_method('GET'), path)
|
117
|
+
expect(response_body_child.first).to_not be_empty
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
Then(/^the resource is eventually available at `([^`]*)`$/) do |path|
|
122
|
+
retry_for(120, 3) do
|
123
|
+
send_request(parse_method('GET'), path)
|
124
|
+
expect(response).to have_attributes(:status => 200)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# TODO: Parameterize polling length
|
129
|
+
Then(/^the property `([^`]*)` is eventually `([^`]*)` at `([^`]*)`$/) do |field, value, path|
|
130
|
+
retry_for(180) do
|
131
|
+
send_request(parse_method('GET'), path)
|
132
|
+
expect(response_body_child.first).to include(field => value)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def response_body_child(path="")
|
137
|
+
JsonPath.new("$.#{path}").on(response.body.to_json)
|
138
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'brine/coercer'
|
2
|
+
require 'rspec/expectations'
|
3
|
+
require 'jsonpath'
|
4
|
+
|
5
|
+
# Selectors here are small wrappers around RSpec
|
6
|
+
# expectation behavior to encapsulate variations in
|
7
|
+
# some expecation associated behavior in classes.
|
8
|
+
class Selector
|
9
|
+
include RSpec::Matchers
|
10
|
+
attr_accessor :coercer
|
11
|
+
|
12
|
+
def initialize(target, negated)
|
13
|
+
@target = target
|
14
|
+
@message = negated ? :to_not : :to
|
15
|
+
end
|
16
|
+
|
17
|
+
def filter_matcher(matcher)
|
18
|
+
matcher
|
19
|
+
end
|
20
|
+
|
21
|
+
def assert_that(value)
|
22
|
+
target, value = coercer.coerce(@target, value)
|
23
|
+
matcher = filter_matcher(yield(value))
|
24
|
+
expect(target).send(@message, matcher)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class AnySelector < Selector
|
29
|
+
def filter_matcher(matcher)
|
30
|
+
include(matcher)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class AllSelector < Selector
|
35
|
+
def filter_matcher(matcher)
|
36
|
+
all(matcher)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Module
|
42
|
+
#
|
43
|
+
module Selection
|
44
|
+
include Coercion
|
45
|
+
attr_reader :selector
|
46
|
+
|
47
|
+
def use_selector(selector)
|
48
|
+
selector.coercer = coercer
|
49
|
+
@selector = selector
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Steps
|
55
|
+
#
|
56
|
+
RESPONSE_ATTRIBUTES='(status|headers|body)'
|
57
|
+
Then(/^the value of `([^`]*)` is( not)? (.*)$/) do |value, negated, assertion|
|
58
|
+
use_selector(Selector.new(value, (!negated.nil?)))
|
59
|
+
step "it is #{assertion}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def dig_from_response(attribute, path=nil, plural=false)
|
63
|
+
root = response.send(attribute.to_sym)
|
64
|
+
return root if !path
|
65
|
+
JsonPath.new("$.#{path}").send(plural ? :on : :first, root)
|
66
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# assertions.rb - General assertions to be used with a Selector
|
2
|
+
|
3
|
+
Then(/^it is equal to `([^`]*)`$/) do |value|
|
4
|
+
selector.assert_that(value) {|v| eq v}
|
5
|
+
end
|
6
|
+
Then(/^it is equal to:$/) do |value|
|
7
|
+
selector.assert_that(value) {|v| eq v}
|
8
|
+
end
|
9
|
+
Then(/^it is matching `([^`]*)`$/) do |value|
|
10
|
+
selector.assert_that(value) {|v| match v}
|
11
|
+
end
|
12
|
+
Then (/^it is matching:$/) do |value|
|
13
|
+
selector.assert_that(value) {|v| match v}
|
14
|
+
end
|
15
|
+
Then(/^it is greater than `([^`]*)`$/) do |value|
|
16
|
+
selector.assert_that(value) {|v| be > v}
|
17
|
+
end
|
18
|
+
Then(/^it is greater than or equal to `([^`]*)`$/) do |value|
|
19
|
+
selector.assert_that(value) {|v| be >= v}
|
20
|
+
end
|
21
|
+
Then(/^it is less than `([^`]*)`$/) do |value|
|
22
|
+
selector.assert_that(value) {|v| be < v}
|
23
|
+
end
|
24
|
+
Then(/^it is less than or equal to `([^`]*)`$/) do |value|
|
25
|
+
selector.assert_that(value) {|v| be <= v}
|
26
|
+
end
|
27
|
+
|
28
|
+
Then(/^it is including `([^`]*)`$/) do |value|
|
29
|
+
selector.assert_that(value) {|v| include v }
|
30
|
+
end
|
31
|
+
Then(/^it is including:$/) do |value|
|
32
|
+
selector.assert_that(value) {|v| include v }
|
33
|
+
end
|
34
|
+
|
35
|
+
Then(/^it is a valid `([^`]*)`$/) do |type|
|
36
|
+
selector.assert_that(type) {|t| type_check_for(t) }
|
37
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# assignment.rb -- assignment related steps
|
2
|
+
|
3
|
+
When(/^`([^`]*)` is assigned a random string$/) do |name|
|
4
|
+
bind(name, SecureRandom.uuid)
|
5
|
+
end
|
6
|
+
|
7
|
+
When(/^`([^`]*)` is assigned `([^`]*)`$/) do |name, value|
|
8
|
+
bind(name, value)
|
9
|
+
end
|
10
|
+
|
11
|
+
When(/^`([^`]*)` is assigned a timestamp$/) do |name|
|
12
|
+
bind(name, DateTime.now)
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# request_construction.rb - Build and send requests
|
2
|
+
|
3
|
+
When(/^the request body is assigned:$/) do |input|
|
4
|
+
set_request_body(input)
|
5
|
+
end
|
6
|
+
|
7
|
+
When(/^the request query parameter `([^`]*)` is assigned `([^`]*)`$/) do |param, value|
|
8
|
+
add_request_param(param, value)
|
9
|
+
end
|
10
|
+
|
11
|
+
When(/^the request header `([^`]*)` is assigned `([^`]*)`$/) do |header, value|
|
12
|
+
add_header(header, value)
|
13
|
+
end
|
14
|
+
|
15
|
+
When(/^an? (GET|POST|PATCH|PUT|DELETE|HEAD|OPTIONS) is sent to `([^`]*)`$/) do |method, url|
|
16
|
+
send_request(parse_method(method), URI.escape(url))
|
17
|
+
bind('response', response)
|
18
|
+
reset_request
|
19
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#RESPONSE_ATTRIBUTES='(status|headers|body)'
|
2
|
+
Then(/^the value of the response #{RESPONSE_ATTRIBUTES}(?: child(ren)? `([^`]*)`)? is( not)? (.*)(?<!:)$/) do
|
3
|
+
|attribute, plural, path, negated, assertion|
|
4
|
+
use_selector(Selector.new(dig_from_response(attribute, path, !plural.nil?), (!negated.nil?)))
|
5
|
+
step "it is #{assertion}"
|
6
|
+
end
|
7
|
+
|
8
|
+
Then(/^the value of the response #{RESPONSE_ATTRIBUTES}(?: child(ren)? `([^`]*)`)? is( not)? (.*)(?<=:)$/) do
|
9
|
+
|attribute, plural, path, negated, assertion, multi|
|
10
|
+
use_selector(Selector.new(dig_from_response(attribute, path, !plural.nil?), (!negated.nil?)))
|
11
|
+
step "it is #{assertion}", multi.to_json
|
12
|
+
end
|
13
|
+
|
14
|
+
Then(/^the value of the response #{RESPONSE_ATTRIBUTES}(?: child(ren)? `([^`]*)`)? does( not)? have any element that is (.*)(?<!:)$/) do
|
15
|
+
|attribute, plural, path, negated, assertion|
|
16
|
+
use_selector(AnySelector.new(dig_from_response(attribute, path, !plural.nil?), (!negated.nil?)))
|
17
|
+
step "it is #{assertion}"
|
18
|
+
end
|
19
|
+
|
20
|
+
Then(/^the value of the response #{RESPONSE_ATTRIBUTES}(?: child(ren)? `([^`]*)`)? does( not)? have any element that is (.*)(?<=:)$/) do
|
21
|
+
|attribute, plural, path, negated, assertion, multi|
|
22
|
+
use_selector(AnySelector.new(dig_from_response(attribute, path, !plural.nil?), (!negated.nil?)))
|
23
|
+
step "it is #{assertion}", multi.to_json
|
24
|
+
end
|
25
|
+
|
26
|
+
#Would be negated with `not all' which would be equivalent to any(not ) but that's not readily supported
|
27
|
+
Then(/^the value of the response #{RESPONSE_ATTRIBUTES}(?: child(ren)? `([^`]*)`)? has elements which are all (.*)(?<!:)$/) do
|
28
|
+
|attribute, plural, path, assertion|
|
29
|
+
use_selector(AllSelector.new(dig_from_response(attribute, path, !plural.nil?), false))
|
30
|
+
step "it is #{assertion}"
|
31
|
+
end
|
32
|
+
|
33
|
+
Then(/^the value of the response #{RESPONSE_ATTRIBUTES}(?: child(ren)? `([^`]*)`)? has elements which are all (.*)(?<=:)$/) do
|
34
|
+
|attribute, plural, path, assertion, multi|
|
35
|
+
use_selector(AllSelector.new(dig_from_response(attribute, path, !plural.nil?), false))
|
36
|
+
step "it is #{assertion}", multi.to_json
|
37
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
#
|
3
|
+
# Steps used to test this library
|
4
|
+
# Not loaded by default (except in the tests)
|
5
|
+
#
|
6
|
+
HTTP_METHOD='GET|POST|PATCH|PUT|DELETE|HEAD|OPTIONS'
|
7
|
+
|
8
|
+
class StubResponse
|
9
|
+
attr_accessor :body, :status
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@body = ''
|
13
|
+
@status = 200
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class StubRequest
|
18
|
+
attr_accessor :method, :path, :headers, :body
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@headers = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def method=(value)
|
25
|
+
@method=value.downcase.to_sym
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class StubBuilder
|
30
|
+
attr_reader :request, :response
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
@request = StubRequest.new
|
34
|
+
@response = StubResponse.new
|
35
|
+
end
|
36
|
+
|
37
|
+
def make_response()
|
38
|
+
[@response.status, {}, @response.body]
|
39
|
+
end
|
40
|
+
|
41
|
+
def build(stubs)
|
42
|
+
# Currently the Faraday stub code provides one method per HTTP method (which then
|
43
|
+
# calls a generalized protected method), so this block grabs the method to use
|
44
|
+
# and passes the right args based on the signature. The last arg is normally a block
|
45
|
+
# but we're using the make_response method to avoid duplication and allow overriding.
|
46
|
+
m = stubs.method(@request.method)
|
47
|
+
case m.parameters.length
|
48
|
+
when 3
|
49
|
+
m.call(@request.path, @request.headers, &method(:make_response))
|
50
|
+
when 4
|
51
|
+
m.call(@request.path, @request.body, @request.headers, &method(:make_response))
|
52
|
+
else
|
53
|
+
raise "I don't know how to call #{m}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class ResponseStatusSequenceStubBuilder < StubBuilder
|
59
|
+
def initialize(stub, seq)
|
60
|
+
@request = stub.request
|
61
|
+
@response = stub.response
|
62
|
+
@enum = seq.to_enum
|
63
|
+
end
|
64
|
+
|
65
|
+
def make_response()
|
66
|
+
begin
|
67
|
+
@val = @enum.next
|
68
|
+
end
|
69
|
+
[@val, {}, @response.body]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def stub
|
74
|
+
@stub ||= StubBuilder.new
|
75
|
+
end
|
76
|
+
|
77
|
+
def build_stub
|
78
|
+
stub.build($stubs)
|
79
|
+
@stub = nil
|
80
|
+
end
|
81
|
+
|
82
|
+
Before do
|
83
|
+
$stubs = Faraday::Adapter::Test::Stubs.new
|
84
|
+
@client = Faraday.new(url: ENV['ROOT_URL'] ||
|
85
|
+
'http://localhost:8080') do |conn|
|
86
|
+
conn.response :logger, nil
|
87
|
+
conn.adapter :test, $stubs
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
Given(/^expected response status of `([^`]*)`$/) do |status|
|
92
|
+
stub.response.status = status
|
93
|
+
end
|
94
|
+
|
95
|
+
Given(/^expected response status sequence of `([^`]*)`$/) do |seq|
|
96
|
+
@stub = ResponseStatusSequenceStubBuilder.new(stub, seq)
|
97
|
+
end
|
98
|
+
|
99
|
+
Given(/^expected request body:$/) do |body|
|
100
|
+
stub.request.body = body
|
101
|
+
end
|
102
|
+
|
103
|
+
Given(/^expected request headers:$/) do |headers|
|
104
|
+
stub.request.headers = headers
|
105
|
+
end
|
106
|
+
|
107
|
+
Given(/^expected (#{HTTP_METHOD}) sent to `([^`]*)`/) do |method, path|
|
108
|
+
stub.request.method = method
|
109
|
+
stub.request.path = path
|
110
|
+
build_stub
|
111
|
+
end
|
112
|
+
|
113
|
+
When(/^the response body is assigned:$/) do |input|
|
114
|
+
@response ||= StubResponse.new
|
115
|
+
@response.body = input
|
116
|
+
end
|
117
|
+
|
118
|
+
When(/^the response body is assigned `([^`]*)`/) do |input|
|
119
|
+
@response ||= StubResponse.new
|
120
|
+
@response.body = input
|
121
|
+
end
|
122
|
+
|
123
|
+
When(/^the response body is:$/) do |input|
|
124
|
+
replaced_with('When', 'the response body is assigned:', '1.0.0', input.to_json)
|
125
|
+
end
|
126
|
+
|
127
|
+
When /^the response status is assigned `([^`]*)`$/ do |status|
|
128
|
+
@response ||= StubResponse.new
|
129
|
+
@response.status = status.to_i # this coercion isn't needed but is a guarantee
|
130
|
+
end
|
131
|
+
|
132
|
+
Then(/^the response body as JSON is:$/) do |text|
|
133
|
+
expect(response.body.to_json).to eq text
|
134
|
+
end
|
135
|
+
|
136
|
+
Then(/^expected calls are verified$/) do
|
137
|
+
$stubs.verify_stubbed_calls
|
138
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# = transformers.rb:: Argument Transforms for Brine
|
2
|
+
#
|
3
|
+
# The Transforms that convert provided inputs to support richer
|
4
|
+
# functionaliy than the simple strings which Cucumber provides.
|
5
|
+
|
6
|
+
# == Scalar transforms
|
7
|
+
# Convert inputs into basic Ruby data types which represent a single value
|
8
|
+
|
9
|
+
# Integers
|
10
|
+
Transform /\A(-?\d+)\z/ do |number|
|
11
|
+
number.to_i
|
12
|
+
end
|
13
|
+
|
14
|
+
# Booleans
|
15
|
+
Transform /\A(?:true|false)\z/ do |boolean|
|
16
|
+
boolean.to_s == "true"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Regexp
|
20
|
+
# This presently does not support flags after the closing slash, support for these should be added as needed
|
21
|
+
Transform /\A\/(.*)\/\z/ do |pattern|
|
22
|
+
Regexp.new(pattern)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Temporal
|
26
|
+
DATE='\d{4}-\d{2}-\d{2}'
|
27
|
+
TIME='\d{2}:\d{2}:\d{2}'
|
28
|
+
MILLIS='(?:\.\d{3})?'
|
29
|
+
TZ='(?:Z|(?:[+-]\d{2}:\d{2}))'
|
30
|
+
Transform /^#{DATE}T#{TIME}#{MILLIS}#{TZ}$/ do |date|
|
31
|
+
Time.parse(date)
|
32
|
+
end
|
33
|
+
|
34
|
+
# == Structure transforms
|
35
|
+
# Converts inputs to general data structures
|
36
|
+
|
37
|
+
# Lists
|
38
|
+
Transform /\A\[.*\]\z/m do |input|
|
39
|
+
JSON.parse(input)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Objects
|
43
|
+
# Rely on templates being registered later and therefore given higher priority.
|
44
|
+
# Lookarounds could avoid the ambiguity but are a nastier pattern.
|
45
|
+
Transform /\A{.*}\z$/m do |input|
|
46
|
+
JSON.parse(input)
|
47
|
+
end
|
48
|
+
|
49
|
+
# == Atypical transforms
|
50
|
+
# Transforms for which data type is not the primary focus
|
51
|
+
|
52
|
+
# Whitespace removal transforms
|
53
|
+
# Handle stripping leading and trailing whitespace.
|
54
|
+
# These are split out from the transforms to consolidate the behavior.
|
55
|
+
# They call transform on the stripped value so that subsequent transforms no longer
|
56
|
+
# have to deal with such whitespace.
|
57
|
+
#
|
58
|
+
# Note that these need to deal with multiline string arguments which require
|
59
|
+
# the multiline flag and \A/\z anchors to properly operate on the full string rather than
|
60
|
+
# being line oriented. The calls to +#strip+ are also not likely to properly clean up
|
61
|
+
# multiline strings but is just meant as a (potentially ineffective) optimization over
|
62
|
+
# recursive calls and capturing.
|
63
|
+
Transform /\A\s+(.*)\z/m do |input|
|
64
|
+
Transform(input.strip)
|
65
|
+
end
|
66
|
+
Transform /\A(.*)\s+\z/m do |input|
|
67
|
+
Transform(input.strip)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Quotes
|
71
|
+
Transform /\A".*"\z/ do |quoted|
|
72
|
+
quoted[1..-2]
|
73
|
+
end
|
74
|
+
Transform /\A'.*'\z/ do |quoted|
|
75
|
+
quoted[1..-2]
|
76
|
+
end
|
77
|
+
|
78
|
+
# Template Expansion
|
79
|
+
Transform /.*{{.*}}.*/ do |template|
|
80
|
+
Transform(shave_value(template))
|
81
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# type_checks.rb -- checks whether provided is a valid instance of a specified type
|
2
|
+
#
|
3
|
+
# Provides validation for standard JSON type
|
4
|
+
|
5
|
+
require 'rspec/expectations'
|
6
|
+
|
7
|
+
# This will be made extensible so it can replace current domain specific check logic
|
8
|
+
class TypeChecks
|
9
|
+
include RSpec::Matchers
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@map = {
|
13
|
+
Object: be_a_kind_of(Hash),
|
14
|
+
String: be_a_kind_of(String),
|
15
|
+
Number: be_a_kind_of(Numeric),
|
16
|
+
Integer: be_a_kind_of(Integer),
|
17
|
+
Array: be_a_kind_of(Array),
|
18
|
+
Boolean: satisfy{|it| it == true || it == false }
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def for_type(type)
|
23
|
+
@map[type.to_sym] || raise("Unsupported type #{type}")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module TypeChecking
|
28
|
+
def type_checks
|
29
|
+
@type_check ||= TypeChecks.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def type_check_for(type)
|
33
|
+
type_checks.for_type(type)
|
34
|
+
end
|
35
|
+
end
|