brine-dsl 0.7.0 → 0.8.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.
@@ -0,0 +1,20 @@
1
+ Feature: A Parameter
2
+ An identifier can be assigned the value of the provided parameter.
3
+
4
+ Scenario: Parameter assignment.
5
+ Given a file named "features/parameter.feature" with:
6
+ """
7
+
8
+ Feature: Parameter Assignment.
9
+ Scenario: Simple assignment.
10
+ Given `foo` is assigned `bar`
11
+ When the response body is assigned `{{ foo }}`
12
+ Then the value of the response body is equal to `bar`
13
+
14
+ """
15
+ When I run `cucumber --strict features/parameter.feature`
16
+ Then the output should contain:
17
+ """
18
+ 1 passed
19
+ """
20
+ And it should pass
@@ -0,0 +1,25 @@
1
+ Feature: A Random String
2
+ An identifier can be assigned a random string with decent entropy.
3
+
4
+ Scenario: Random string assignment.
5
+ Given a file named "features/random.feature" with:
6
+ """
7
+
8
+ Feature: Random Assignment.
9
+ Scenario: Several unique variables.
10
+ Given `v1` is assigned a random string
11
+ And `v2` is assigned a random string
12
+ And `v3` is assigned a random string
13
+ When the response body is assigned `[ "{{ v1 }}","{{ v2 }}","{{ v3 }}" ]`
14
+ Then the value of the response body does not have any element that is empty
15
+ And the value of the response body child `.[0]` is equal to `{{ v1 }}`
16
+ And the value of the response body children `.[1:2]` does not have any element that is equal to `{{ v1 }}`
17
+ And the value of the response body child `.[2]` is not equal to `{{ v2 }}`
18
+
19
+ """
20
+ When I run `cucumber --strict features/random.feature`
21
+ Then the output should contain:
22
+ """
23
+ 8 passed
24
+ """
25
+ And it should pass
@@ -0,0 +1,33 @@
1
+ Feature: A Value From A Response Attribute.
2
+ An identifier can be assigned a value extracted from a response attribute.
3
+
4
+ Scenario: Assorted attribute extractions.
5
+ Given a file named "features/response_attribute.feature" with:
6
+ """
7
+
8
+ Feature: Reponse Attribute Path Assignment.
9
+ Scenario: Response body.
10
+ Given the response body is assigned `foo`
11
+ When `myVar` is assigned the response body
12
+ And the response body is assigned `{{ myVar }}`
13
+ Then the value of the response body is equal to `foo`
14
+
15
+ Scenario: Response body child.
16
+ Given the response body is assigned `{"response": "foo"}`
17
+ When `myVar` is assigned the response body child `.response`
18
+ And the response body is assigned `{{ myVar }}`
19
+ Then the value of the response body is equal to `foo`
20
+
21
+ Scenario: Response body children.
22
+ Given the response body is assigned `{"response": "foo"}`
23
+ When `myVar` is assigned the response body children `.response`
24
+ And the response body is assigned `{{{ myVar }}}`
25
+ Then the value of the response body is equal to `["foo"]`
26
+
27
+ """
28
+ When I run `cucumber --strict features/response_attribute.feature`
29
+ Then the output should contain:
30
+ """
31
+ 3 passed
32
+ """
33
+ And it should pass
@@ -0,0 +1,33 @@
1
+ Feature: A Timestamp
2
+ An identifier can be assigned a current timestamp.
3
+
4
+ Scenario: Timestamp assignment.
5
+ Given a file named "features/timestamp.feature" with:
6
+ """
7
+
8
+ Feature: Timestamp Assignment.
9
+ Scenario: Newer than some old date.
10
+ Given `v1` is assigned a timestamp
11
+ When the response body is assigned `{{ v1 }}`
12
+ Then value of the response body is greater than `2018-06-17T12:00:00Z`
13
+
14
+ Scenario: Values increase.
15
+ Given `v1` is assigned a timestamp
16
+ When the response body is assigned `{{ v1 }}`
17
+ Then the value of the response body is not empty
18
+
19
+ When `v2` is assigned a timestamp
20
+ And the response body is assigned `{{ v2 }}`
21
+ Then the value of the response body is greater than or equal to `{{ v1 }}`
22
+
23
+ When `v3` is assigned a timestamp
24
+ And the response body is assigned `{{ v3 }}`
25
+ Then the value of the response body is greater than or equal to `{{ v1 }}`
26
+ And the value of the response body is greater than or equal to `{{ v2 }}`
27
+
28
+ """
29
+ When I run `cucumber --strict features/timestamp.feature`
30
+ Then the output should contain:
31
+ """
32
+ 2 passed
33
+ """
@@ -1,23 +1,37 @@
1
- # cleaner_upper.rb -- clean up resources created during test run
1
+ ##
2
+ # @file cleaner_upper.rb
3
+ # Clean up resources created during test run.
2
4
  #
3
- # Will issue DELETE call for all tracked URLs which will normally be triggered in a hook.
5
+ # Will issue DELETE call for all tracked URLs which will normally be triggered
6
+ # in a hook.
4
7
  #
5
- # The present approach for this is to explicitly track created resources to which
6
- # DELETE calls will be sent. Cleaning up of resources will be given some further attention
7
- # in the future, but this functionality should be preserved.
8
+ # The present approach for this is to explicitly track created resources to
9
+ # which DELETE calls will be sent. Cleaning up of resources will be given some
10
+ # further attention in the future, but this functionality should be preserved.
8
11
 
12
+ ##
13
+ # A command object for the delete which will be executed as part of cleaning up.
9
14
  class DeleteCommand
10
- attr_accessor :client, :path
11
15
 
12
- def initialize(client, path,
13
- oks:[200,204],
14
- attempts: 3)
16
+ ##
17
+ # Construct a command with the required paramters to perform the delete.
18
+ #
19
+ # @param client The Faraday client which will send the delete message.
20
+ # @param path The path of the resource to be deleted.
21
+ # @param oks The response status codes which will be considered successful.
22
+ # @param attempts The number of times this command should be tried,
23
+ # retrying if an unsuccessful status code is received.
24
+ def initialize(client, path, oks: [200,204], attempts: 3)
15
25
  @client = client
16
26
  @path = path
17
27
  @oks = oks
18
28
  @attempts = attempts
19
29
  end
20
30
 
31
+ ##
32
+ # Issue the delete based on the parameters provided during construction.
33
+ #
34
+ # @returns true if a successful response is obtained, otherwise false.
21
35
  def cleanup
22
36
  while @attempts > 0
23
37
  begin
@@ -33,29 +47,54 @@ class DeleteCommand
33
47
  end
34
48
  end
35
49
 
50
+ ##
51
+ # A mixin which provides resource cleanup.
52
+ #
53
+ # Exposes methods to keep a stack of DeleteCommands corresponding to each
54
+ # created resource which are then popped and invoked to perform the cleanup.
55
+ #
56
+ # The LIFO behavior is adopted as it is more likely to preserve integrity,
57
+ # such as removing children added to parents or similar dependencies.
36
58
  module CleanerUpper
37
59
 
38
- # HTTP client object used to issue DELETE calls
39
- # must support #delete(path)
40
- # to be injected by calling code
60
+ ##
61
+ # Set the Faraday HTTP client object used to issue DELETE calls.
62
+ #
63
+ # The client provided will be subsequently used to create DeleteCommands.
64
+ # This can be called multiple times where each DeleteCommand will use the
65
+ # most recently set value. In most use cases this will also be the client
66
+ # used to issue the creation requests and could therefore be passed to this
67
+ # method prior to use.
68
+ #
69
+ # @param client - The client to use to DELETE subsequently tracked resources.
41
70
  def set_cleaning_client(client)
42
71
  @client = client
43
72
  end
44
73
 
45
- # Record resource to be cleaned
74
+ ##
75
+ # Record resource to be later cleaned (pushes a DeleteCommand).
76
+ #
77
+ # @param path - The path for the created resource, will be issued a DELETE.
46
78
  def track_created_resource(path)
47
79
  created_resources << DeleteCommand.new(@client, path)
48
80
  end
49
81
 
50
- # Clean recorded resources
51
- # Expected to be called after test run
82
+ ##
83
+ # Clean recorded resources (normally after a test run).
52
84
  def cleanup_created_resources
53
85
  created_resources.reverse.each{|it| it.cleanup}
54
86
  end
55
87
 
56
88
  private
57
89
 
58
- # Lazily initialized array of resources to remove
90
+ ##
91
+ # The array which serves as the stack of DeleteCommands.
92
+ #
93
+ # Works as a "module provided property" which is a name I
94
+ # may have just made up.
95
+ #
96
+ # TODO: Find proper term for module provided property
97
+ # TODO: The name of this property seems sloppy as it contains commands.
59
98
  def created_resources
60
99
  @created_resources ||= []
61
100
  end
data/lib/brine/coercer.rb CHANGED
@@ -1,13 +1,63 @@
1
- ## coercer.rb::
1
+ ##
2
+ # @file coercer.rb
3
+ # Type coercion to support assertions.
2
4
 
5
+ ##
6
+ # Coerces the types of provided objects to support desired assertions.
7
+ #
8
+ # Coercion is used to support handling richer data types in Brine without
9
+ # introducing noisy type information to the language.
10
+ # Argument Transforms can be used to convert input provided directly
11
+ # to a Brine step, however data extracted from JSON will by default be
12
+ # limited to the small amount of JSON supported data types. As normally
13
+ # the data extracted from JSON will only be directly present on one side
14
+ # of the assertion (most likely the left), the simpler JSON data types can
15
+ # be coerced to a richer type based on the right hand side.
16
+ #
17
+ # A standard example (and that which is defined at the moment here) is date/time
18
+ # values. When testing an API that returns such values it is likely desirable to
19
+ # perform assertions with proper date/time semantics, and the coercer allows
20
+ # such assertions against a value retrieved out of a JSON structure.
21
+ #
22
+ # The combination of Argument Transforms and the Coercer can also effectively
23
+ # allow for user defined types to seemlessly exist within the Brine tests.
24
+ # The date/time implementation is an example of this.
25
+ # TODO: Document this in a friendlier place, with a contrived example.
26
+ # TODO: Having the default Time stuff should likely be removed for v2.
27
+ #
28
+ # Implementation (Most of the non-implementation info should later be moved).
29
+ # ---
30
+ # Internally the Coercer is a wrapper around a map of coercion functions
31
+ # where the keys are the pairs of classes for the operands and the
32
+ # values are the functions which accept a pair of instances (ostensibly) of the
33
+ # classes and return a pair of transformed values. The default coercion function
34
+ # returns the inputs unmodified (a nop), so any pair of classes which does not
35
+ # have a key will pass through unchanged.
36
+ #
37
+ # Note that this relies solely on the hash lookup and does not attempt any kind
38
+ # of inheritance lookup or similar. The current thinking is that there should be
39
+ # few expected types and lineage could be covered through explicit keys so the
40
+ # simple, dumb approach is likely sufficient.
3
41
  class Coercer
42
+ ##
43
+ # Instantiate a new Coercer.
44
+ #
45
+ # Initialize the standard map of coercion functions.
4
46
  def initialize
5
- @map = Hash.new(->(l, r){[l, r]})
6
- @map[[String, Time]] = ->(l, r){[Time.parse(l), r]}
47
+ @map = Hash.new(->(lhs, rhs){[lhs, rhs]})
48
+ @map[[String, Time]] = ->(lhs, rhs){[Time.parse(lhs), rhs]}
7
49
  end
8
50
 
9
- def coerce(l, r)
10
- @map[[l.class, r.class]].call(l, r)
51
+ ##
52
+ # Coerce the provided inputs as needed.
53
+ #
54
+ # Looks up and invokes the associated coercion function.
55
+ #
56
+ # @param lhs - The left hand side input.
57
+ # @param rhs - The right hand side input.
58
+ # @returns A pair (2 element array) of potentially coerced values.
59
+ def coerce(lhs, rhs)
60
+ @map[[lhs.class, rhs.class]].call(lhs, rhs)
11
61
  end
12
62
  end
13
63
 
@@ -4,23 +4,28 @@ require 'brine/selector'
4
4
  require 'jsonpath'
5
5
 
6
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')
7
+ Then(/^the response #{RESPONSE_ATTRIBUTES} has `([^`]*)` with a value that is not empty$/) do
8
+ |attribute, member|
9
+ replaced_with('Then', "the value of the response #{attribute} child `#{member}` is not empty", '0.9')
9
10
  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')
11
+
12
+ Then(/^the response #{RESPONSE_ATTRIBUTES} includes? the entries:$/) do |attribute, table|
13
+ replaced_with('Then', "the value of the response #{attribute} is including:\n\"\"\"#{table.hashes.to_json}\"\"\"", '0.9')
12
14
  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
15
+
16
+ Then(/^the response body is a list of length (\d+)$/) do |length|
17
+ replaced_with('Then', "the value of the response body is of length #{length}", '0.9')
16
18
  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
+
20
+ Then(/^the response body is a list (with|without) an entry containing:$/) do |with, data|
21
+ neg = (with == 'without') ? 'not ' : ''
22
+ replaced_with('Then', "the value of the response body does #{neg}have any element that is including:\n\"\"\"#{table.hashes.to_json}\"\"\"", '0.9')
19
23
  end
20
24
 
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')
25
+ Then(/^the response body has `([^`]*)` which (in|ex)cludes? the entries:$/) do
26
+ |child, in_or_ex, table|
27
+ neg = (in_or_ex=='ex') ? 'not ' : ''
28
+ replaced_with('Then', "the value of the response body child `#{child}` is #{neg}including:\n\"\"\"#{table.hashes.to_json}\"\"\"", '0.9')
24
29
  end
25
30
 
26
31
  # This file is legacy or unsorted steps which will be deprecated or moved into
@@ -33,13 +38,13 @@ def kv_table(table)
33
38
  transform_table!(table).rows_hash
34
39
  end
35
40
 
41
+ # TODO: Requires extensible is_a_valid for deprecation
36
42
  Then(/^the response body is a list which all are (\w+)$/) do |matcher|
37
43
  pass_it = method(matcher.to_sym).call
38
44
  expect(response_body_child.first).to all(pass_it)
39
45
  end
40
46
 
41
- #TODO: The binding environment should be able to be accessed directly
42
- # without requiring a custom step
47
+ # FIXME: In the process of being deprecated
43
48
  When(/^`([^`]*)` is bound to `([^`]*)` from the response body$/) do |name, path|
44
49
  binding[name] = response_body_child(path).first
45
50
  end
@@ -47,15 +52,6 @@ end
47
52
  #
48
53
  # Response attribute (non-body) assertions
49
54
  #
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
55
  Then(/^the response #{RESPONSE_ATTRIBUTES} contains? null fields:$/) do |attribute, table|
60
56
  expect(response)
61
57
  .to have_attributes(attribute.to_sym =>
@@ -71,21 +67,11 @@ end
71
67
  #
72
68
  # Response body assertions
73
69
  #
70
+ # TODO: Write specifications around this
74
71
  Then(/^the response body does not contain fields:$/) do |table|
75
72
  expect(response_body_child.first.keys).to_not include(*table.raw.flatten)
76
73
  end
77
74
 
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
75
  #TODO: Maybe worth optimizing these 2 to O(n) after tests are in place
90
76
  Then(/^the response body is a list sorted by `([^`]*)` ascending$/) do |path|
91
77
  values = response_body_child(path)
@@ -97,11 +83,6 @@ Then(/^the response body is a list sorted by `([^`]*)` descending$/) do |path|
97
83
  expect(values).to eq values.sort{|a,b| b.to_s.downcase <=> a.to_s.downcase}
98
84
  end
99
85
 
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
86
 
106
87
  Then(/^the response body is (\w+)$/) do |matcher|
107
88
  pass_it = method(matcher.to_sym).call
@@ -1,13 +1,41 @@
1
- # assignment.rb -- assignment related steps
2
-
3
- When(/^`([^`]*)` is assigned a random string$/) do |name|
4
- bind(name, SecureRandom.uuid)
5
- end
1
+ ##
2
+ # @file assignment.rb
3
+ # Assignment related steps.
6
4
 
5
+ ##
6
+ # Assign the provided parameter.
7
+ #
8
+ # @param name - The identifier to which the value will be bound.
9
+ # @param value - The value to bind to the identifier.
7
10
  When(/^`([^`]*)` is assigned `([^`]*)`$/) do |name, value|
8
11
  bind(name, value)
9
12
  end
10
13
 
14
+ ##
15
+ # Assign a random string (UUID).
16
+ #
17
+ # @param name - The identifier to which a random string will be bound.
18
+ When(/^`([^`]*)` is assigned a random string$/) do |name|
19
+ bind(name, SecureRandom.uuid)
20
+ end
21
+
22
+ ##
23
+ # Assign a current timestamp.
24
+ #
25
+ # @param name - The identifier to which the current timestamp will be bound.
11
26
  When(/^`([^`]*)` is assigned a timestamp$/) do |name|
12
27
  bind(name, DateTime.now)
13
28
  end
29
+
30
+ ##
31
+ # Assign a value extracted from a response attribute.
32
+ #
33
+ # @param name - The identifier to which the extracted value will be bound.
34
+ # @param attribute - The response member from which the value will be extracted.
35
+ # @param plural - When the path is provided,
36
+ # @param path - The path within the member to extract.
37
+ # whether to extract a single match or a collection of all matching.
38
+ When(/^`([^`]*)` is assigned the response #{RESPONSE_ATTRIBUTES}(?: child(ren)? `([^`]*)`)?$/) do
39
+ |name, attribute, plural, path|
40
+ bind(name, dig_from_response(attribute, path, !plural.nil?))
41
+ end
data/lib/brine.rb CHANGED
@@ -1,10 +1,12 @@
1
- # brine.rb -- loader file for the rest of brine
1
+ ##
2
+ # @file brine.rb
3
+ # Loader file for the rest of brine.
2
4
  #
3
5
  # The primary goal of this file is to load all resources needed to use brine.
4
- # A secondary goal which should inform how that is done is that loading this file by itself
5
- # should provide new objects but not otherwise impact existing state such as by
6
- # modifying the World or defining any Steps, Transforms, or similar.
7
- # Those types of changes should be done by [#brine_mix].
6
+ # A secondary goal which should inform how that is done is that loading this
7
+ # file by itself should provide new objects but not otherwise impact existing
8
+ # state such as by modifying the World or defining any Steps, Transforms, etc.
9
+ # Those types of changes should be done by @ref #brine_mix.
8
10
 
9
11
  require 'brine/cleaner_upper'
10
12
  require 'brine/mustache_binder'
@@ -13,7 +15,8 @@ require 'brine/util'
13
15
  require 'brine/selector'
14
16
  require 'brine/type_checks'
15
17
 
16
- # Modules to add to World
18
+ ##
19
+ # Meta-module for modules to mix into World.
17
20
  module Brine
18
21
  include CleanerUpper
19
22
  include MustacheBinder
@@ -23,8 +26,10 @@ module Brine
23
26
  include TypeChecking
24
27
  end
25
28
 
26
- # Load the more side effecty files and return the Module,
27
- # expected to be called as `World(brine_mix)`
29
+ ##
30
+ # Load the files with side effects and return @ref Brine.
31
+ #
32
+ # Expected to be called as `World(brine_mix)`
28
33
  def brine_mix
29
34
  require 'brine/step_definitions/assignment'
30
35
  require 'brine/step_definitions/request_construction'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brine-dsl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Whipple
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-30 00:00:00.000000000 Z
11
+ date: 2018-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cucumber
@@ -193,7 +193,7 @@ files:
193
193
  - Gemfile.lock
194
194
  - Guardfile
195
195
  - LICENSE
196
- - README.md
196
+ - README.adoc
197
197
  - Rakefile
198
198
  - brine-dsl.gemspec
199
199
  - config/cucumber.yml
@@ -226,6 +226,10 @@ files:
226
226
  - features/assertions/is_including.feature
227
227
  - features/assertions/is_matching.feature
228
228
  - features/assertions/is_of_length.feature
229
+ - features/assignment/parameter.feature
230
+ - features/assignment/random.feature
231
+ - features/assignment/response_attribute.feature
232
+ - features/assignment/timestamp.feature
229
233
  - features/deprecations/replaced_with.feature
230
234
  - features/request_construction/basic.feature
231
235
  - features/request_construction/body.feature
@@ -298,6 +302,10 @@ test_files:
298
302
  - features/assertions/is_including.feature
299
303
  - features/assertions/is_matching.feature
300
304
  - features/assertions/is_of_length.feature
305
+ - features/assignment/parameter.feature
306
+ - features/assignment/random.feature
307
+ - features/assignment/response_attribute.feature
308
+ - features/assignment/timestamp.feature
301
309
  - features/deprecations/replaced_with.feature
302
310
  - features/request_construction/basic.feature
303
311
  - features/request_construction/body.feature
data/README.md DELETED
@@ -1,7 +0,0 @@
1
- Brine
2
- ===
3
-
4
- > Cucumber DSL for testing REST APIs
5
-
6
- Documentation is hosted at:
7
- https://brightcove.github.io/brine/