brine-dsl 0.7.0 → 0.8.0

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