brine-dsl 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|