pact 1.3.0 → 1.3.1

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.
Files changed (45) hide show
  1. data/.travis.yml +1 -1
  2. data/CHANGELOG.md +14 -0
  3. data/Gemfile.lock +5 -5
  4. data/README.md +25 -29
  5. data/documentation/README.md +9 -7
  6. data/lib/pact/configuration.rb +83 -12
  7. data/lib/pact/consumer/mock_service/interaction_mismatch.rb +5 -1
  8. data/lib/pact/consumer/spec_hooks.rb +7 -4
  9. data/lib/pact/consumer/world.rb +12 -0
  10. data/lib/pact/consumer_contract/headers.rb +51 -0
  11. data/lib/pact/consumer_contract/request.rb +6 -1
  12. data/lib/pact/provider/configuration/service_provider_dsl.rb +6 -1
  13. data/lib/pact/provider/matchers/messages.rb +2 -2
  14. data/lib/pact/provider/rspec.rb +7 -1
  15. data/lib/pact/provider/rspec/backtrace_formatter.rb +30 -6
  16. data/lib/pact/provider/rspec/matchers.rb +9 -6
  17. data/lib/pact/shared/json_differ.rb +15 -0
  18. data/lib/pact/shared/request.rb +11 -2
  19. data/lib/pact/shared/text_differ.rb +14 -0
  20. data/lib/pact/version.rb +1 -1
  21. data/spec/lib/pact/configuration_spec.rb +127 -5
  22. data/spec/lib/pact/consumer/mock_service/interaction_mismatch_spec.rb +8 -5
  23. data/spec/lib/pact/consumer_contract/headers_spec.rb +107 -0
  24. data/spec/lib/pact/consumer_contract/request_spec.rb +9 -0
  25. data/spec/lib/pact/matchers/matchers_spec.rb +35 -0
  26. data/spec/lib/pact/provider/configuration/service_provider_dsl_spec.rb +16 -0
  27. data/spec/lib/pact/provider/matchers/messages_spec.rb +5 -4
  28. data/spec/lib/pact/provider/rspec_spec.rb +15 -11
  29. data/spec/lib/pact/shared/json_differ_spec.rb +36 -0
  30. data/spec/lib/pact/shared/request_spec.rb +25 -1
  31. data/spec/lib/pact/shared/text_differ_spec.rb +54 -0
  32. data/spec/support/pact_helper.rb +2 -0
  33. data/spec/support/test_app_with_right_content_type_differ.json +23 -0
  34. data/tasks/pact-test.rake +6 -0
  35. metadata +72 -30
  36. checksums.yaml +0 -7
  37. data/documentation/Testing with pact.png +0 -0
  38. data/documentation/best-practices.md +0 -33
  39. data/documentation/development-workflow.md +0 -22
  40. data/documentation/faq.md +0 -81
  41. data/documentation/provider-states.md +0 -178
  42. data/documentation/raq.md +0 -39
  43. data/documentation/terminology.md +0 -25
  44. data/documentation/troubleshooting.md +0 -4
  45. data/documentation/verifying-pacts.md +0 -106
@@ -13,7 +13,12 @@ module Pact
13
13
 
14
14
  attr_accessor :name, :app_block
15
15
 
16
- CONFIG_RU_APP = lambda { Rack::Builder.parse_file(Pact.configuration.config_ru_path).first }
16
+ CONFIG_RU_APP = lambda {
17
+ unless File.exist? Pact.configuration.config_ru_path
18
+ raise "Could not find config.ru file at #{Pact.configuration.config_ru_path} Please configure the service provider app or create a config.ru file in the root directory of the project. See https://github.com/realestate-com-au/pact/blob/master/documentation/verifying-pacts.md for more information."
19
+ end
20
+ Rack::Builder.parse_file(Pact.configuration.config_ru_path).first
21
+ }
17
22
 
18
23
  def initialize name
19
24
  @name = name
@@ -5,9 +5,9 @@ module Pact
5
5
  module Matchers
6
6
  module Messages
7
7
 
8
- def match_term_failure_message diff, actual, color_enabled
8
+ def match_term_failure_message diff, actual, diff_formatter, color_enabled
9
9
  message = "Actual: #{(String === actual ? actual : actual.to_json)}\n\n"
10
- formatted_diff = Pact.configuration.diff_formatter.call(diff)
10
+ formatted_diff = diff_formatter.call(diff)
11
11
  message + colorize_if_enabled(formatted_diff, color_enabled)
12
12
  end
13
13
 
@@ -106,6 +106,12 @@ module Pact
106
106
  let(:response) { interaction_context.last_response }
107
107
  let(:response_status) { response.status }
108
108
  let(:response_body) { parse_body_from_response(response) }
109
+ let(:differ) { Pact.configuration.body_differ_for_content_type diff_content_type }
110
+ let(:diff_formatter) { Pact.configuration.diff_formatter_for_content_type diff_content_type }
111
+ let(:expected_content_type) { Pact::Headers.new(expected_response['headers'] || {})['Content-Type'] }
112
+ let(:actual_content_type) { response.headers['Content-Type']}
113
+ let(:diff_content_type) { String === expected_content_type ? expected_content_type : actual_content_type } # expected_content_type may be a Regexp
114
+ let(:options) { { with: differ, diff_formatter: diff_formatter } }
109
115
 
110
116
  if expected_response['status']
111
117
  it "has status code #{expected_response['status']}" do
@@ -126,7 +132,7 @@ module Pact
126
132
 
127
133
  if expected_response['body']
128
134
  it "has a matching body" do
129
- expect(response_body).to match_term expected_response_body
135
+ expect(response_body).to match_term expected_response_body, options
130
136
  end
131
137
  end
132
138
  end
@@ -1,19 +1,43 @@
1
1
  module RSpec
2
2
  module Core
3
3
 
4
+ # RSpec 3 has a hardwired @system_exclusion_patterns which removes everything matching /bin\//
5
+ # This causes *all* the backtrace lines to be cleaned, as rake pact:verify now shells out
6
+ # to the executable `pact verify ...`
7
+ # which then causes *all* the lines to be included as the BacktraceFormatter will
8
+ # include all lines of the backtrace if all lines were filtered out.
9
+ # This monkey patch only shows lines including bin/pact and removes the
10
+ # "show all lines if no lines would otherwise be shown" logic.
11
+
4
12
  class BacktraceFormatter
5
13
 
6
- # RSpec 3 has a hardwired @system_exclusion_patterns which removes everything matching /bin\//
7
- # This causes *all* the backtrace lines to be cleaned, as rake pact:verify now shells out
8
- # to the executable `pact verify ...`
9
- # which then causes *all* the lines to be included
14
+
15
+ def format_backtrace(backtrace, options = {})
16
+ return backtrace if options[:full_backtrace]
17
+ backtrace.map { |l| backtrace_line(l) }.compact
18
+ end
19
+
20
+ def backtrace_line(line)
21
+ relative_path(line) unless exclude?(line)
22
+ rescue SecurityError
23
+ nil
24
+ end
10
25
 
11
26
  def exclude?(line)
12
27
  return false if @full_backtrace
13
- matches_an_exclusion_pattern?(line) &&
14
- @inclusion_patterns.none? { |p| p =~ line }
28
+ relative_line = relative_path(line)
29
+ return true unless /bin\/pact/ =~ relative_line
15
30
  end
16
31
 
32
+ # Copied from Metadata so a refactor can't break this overridden class
33
+ def relative_path(line)
34
+ line = line.sub(File.expand_path("."), ".")
35
+ line = line.sub(/\A([^:]+:\d+)$/, '\\1')
36
+ return nil if line == '-e:1'
37
+ line
38
+ rescue SecurityError
39
+ nil
40
+ end
17
41
  end
18
42
  end
19
43
  end
@@ -2,6 +2,8 @@ require 'rspec'
2
2
  require 'pact/matchers'
3
3
  require 'pact/provider/matchers/messages'
4
4
  require 'pact/rspec'
5
+ require 'pact/shared/json_differ'
6
+
5
7
 
6
8
  module Pact
7
9
  module RSpec
@@ -20,27 +22,28 @@ module Pact
20
22
 
21
23
  class MatchTerm
22
24
 
23
- include Pact::Matchers
24
25
  include Pact::Matchers::Messages
25
26
  include RSpec2Delegator
26
27
 
27
- def initialize expected
28
+ def initialize expected, differ, diff_formatter
28
29
  @expected = expected
30
+ @differ = differ
31
+ @diff_formatter = diff_formatter
29
32
  end
30
33
 
31
34
  def matches? actual
32
35
  @actual = actual
33
- (@difference = diff(@expected, @actual)).empty?
36
+ (@difference = @differ.call(@expected, @actual)).empty?
34
37
  end
35
38
 
36
39
  def failure_message
37
- match_term_failure_message @difference, @actual, Pact::RSpec.color_enabled?
40
+ match_term_failure_message @difference, @actual, @diff_formatter, Pact::RSpec.color_enabled?
38
41
  end
39
42
 
40
43
  end
41
44
 
42
- def match_term expected
43
- MatchTerm.new(expected)
45
+ def match_term expected, options
46
+ MatchTerm.new(expected, options.fetch(:with), options.fetch(:diff_formatter))
44
47
  end
45
48
 
46
49
  class MatchHeader
@@ -0,0 +1,15 @@
1
+ require 'pact/matchers/matchers'
2
+
3
+ module Pact
4
+ class JsonDiffer
5
+
6
+ extend Matchers
7
+
8
+
9
+ def self.call expected, actual, options = {}
10
+ diff expected, actual, options
11
+ end
12
+
13
+
14
+ end
15
+ end
@@ -1,5 +1,6 @@
1
1
  require 'pact/matchers'
2
2
  require 'pact/symbolize_keys'
3
+ require 'pact/consumer_contract/headers'
3
4
 
4
5
  module Pact
5
6
 
@@ -15,7 +16,7 @@ module Pact
15
16
  def initialize(method, path, headers, body, query)
16
17
  @method = method.to_s
17
18
  @path = path.chomp('/')
18
- @headers = headers
19
+ @headers = Hash === headers ? Headers.new(headers) : headers # Could be a NullExpectation - TODO make this more elegant
19
20
  @body = body
20
21
  @query = query
21
22
  end
@@ -48,6 +49,11 @@ module Pact
48
49
  display_path + display_query
49
50
  end
50
51
 
52
+ def content_type
53
+ return nil if headers.is_a? self.class.key_not_found.class
54
+ headers['Content-Type']
55
+ end
56
+
51
57
  protected
52
58
 
53
59
  def self.key_not_found
@@ -56,7 +62,9 @@ module Pact
56
62
 
57
63
  def to_hash_without_body
58
64
  keep_keys = [:method, :path, :headers, :query]
59
- as_json.reject{ |key, value| !keep_keys.include? key }
65
+ as_json.reject{ |key, value| !keep_keys.include? key }.tap do | hash |
66
+ hash[:method] = method.upcase
67
+ end
60
68
  end
61
69
 
62
70
  def display_path
@@ -66,6 +74,7 @@ module Pact
66
74
  def display_query
67
75
  (query.nil? || query.empty?) ? '' : "?#{Pact::Reification.from_term(query)}"
68
76
  end
77
+
69
78
  end
70
79
  end
71
80
  end
@@ -0,0 +1,14 @@
1
+ require 'pact/matchers/matchers'
2
+ require 'pact/matchers/difference'
3
+
4
+ module Pact
5
+ class TextDiffer
6
+
7
+ extend Matchers
8
+
9
+ def self.call expected, actual, options = {}
10
+ diff expected, actual, options
11
+ end
12
+
13
+ end
14
+ end
data/lib/pact/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pact
2
- VERSION = "1.3.0"
2
+ VERSION = "1.3.1"
3
3
  end
@@ -83,12 +83,12 @@ describe Pact do
83
83
 
84
84
  end
85
85
 
86
- describe "#diff_formatter" do
86
+ describe "#diff_formatter_for_content_type" do
87
87
 
88
88
  let(:subject) { Pact::Configuration.new }
89
89
 
90
90
  it "returns the Pact::Matchers::UnixDiffFormatter by default" do
91
- expect(subject.diff_formatter).to eq(Pact::Matchers::UnixDiffFormatter)
91
+ expect(subject.diff_formatter_for_content_type 'anything').to eq(Pact::Matchers::UnixDiffFormatter)
92
92
  end
93
93
 
94
94
  Pact::Configuration::DIFF_FORMATTERS.each_pair do | key, diff_formatter |
@@ -100,7 +100,7 @@ describe Pact do
100
100
  end
101
101
 
102
102
  it "sets the diff_formatter to #{diff_formatter}" do
103
- expect(subject.diff_formatter).to be diff_formatter
103
+ expect(subject.diff_formatter_for_content_type nil).to be diff_formatter
104
104
  end
105
105
  end
106
106
 
@@ -115,18 +115,140 @@ describe Pact do
115
115
  end
116
116
 
117
117
  it "sets the diff_formatter to the object" do
118
- expect(subject.diff_formatter).to be diff_formatter
118
+ expect(subject.diff_formatter_for_content_type nil).to be diff_formatter
119
119
  end
120
120
  end
121
121
 
122
122
  context "when set to an object that does not respond to call and isn't a known default option" do
123
123
  it "raises an error" do
124
- expect { subject.diff_formatter = Object.new }.to raise_error "Pact.configuration.diff_formatter needs to respond to call, or be in the preconfigured list: [:embedded, :unix, :list]"
124
+ expect { subject.diff_formatter = Object.new }.to raise_error "Pact diff_formatter needs to respond to call, or be in the preconfigured list: [:embedded, :unix, :list]"
125
+ expect { subject.diff_formatter = Object.new }.to raise_error "Pact diff_formatter needs to respond to call, or be in the preconfigured list: [:embedded, :unix, :list]"
125
126
  end
126
127
  end
127
128
 
128
129
  end
129
130
 
131
+ describe "diff_formatter_for_content_type" do
132
+ let(:diff_formatter) { lambda { |expected, actual| }}
133
+ context "with the default configuration" do
134
+ context "when the content type is nil" do
135
+ it "returns the UnixDiffFormatter" do
136
+ expect(Pact.configuration.diff_formatter_for_content_type nil).to eq Pact::Matchers::UnixDiffFormatter
137
+ end
138
+ end
139
+ context "when the content type is application/json" do
140
+ it "returns the UnixDiffFormatter" do
141
+ expect(Pact.configuration.diff_formatter_for_content_type nil).to eq Pact::Matchers::UnixDiffFormatter
142
+ end
143
+ end
144
+ context "when the content type is text/plain" do
145
+ it "returns the UnixDiffFormatter" do
146
+ expect(Pact.configuration.diff_formatter_for_content_type nil).to eq Pact::Matchers::UnixDiffFormatter
147
+ end
148
+ end
149
+ end
150
+ context "with a custom diff_formatter registered for nil content type" do
151
+ context "when the content_type is nil" do
152
+ it "returns the custom diff_formatter" do
153
+ Pact.configuration.register_diff_formatter nil, diff_formatter
154
+ expect(Pact.configuration.diff_formatter_for_content_type nil).to eq diff_formatter
155
+ end
156
+ end
157
+ end
158
+ context "with a custom diff_formatter registered for json content type" do
159
+ context "when the content_type is application/json" do
160
+ it "returns the custom diff_formatter" do
161
+ Pact.configuration.register_diff_formatter /json/, diff_formatter
162
+ expect(Pact.configuration.diff_formatter_for_content_type 'application/json').to eq diff_formatter
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ describe "register_body_differ" do
169
+
170
+ let(:differ) { lambda{ |expected, actual| } }
171
+
172
+ context "with a string for a content type" do
173
+ it "configures the differ for the given content type" do
174
+ Pact.configure do | config |
175
+ config.register_body_differ 'application/xml', differ
176
+ end
177
+
178
+ expect(Pact.configuration.body_differ_for_content_type 'application/xml').to be differ
179
+ end
180
+ end
181
+
182
+ context "with a regexp for a content type" do
183
+ it "returns a matching differ" do
184
+ Pact.configuration.register_body_differ /application\/.*xml/, differ
185
+ expect(Pact.configuration.body_differ_for_content_type 'application/hal+xml').to be differ
186
+ end
187
+ end
188
+
189
+ context "when a non string or regexp is used to register a differ" do
190
+ it "raises an error" do
191
+ expect { Pact.configuration.register_body_differ 1, differ }.to raise_error /Invalid/
192
+ end
193
+ end
194
+
195
+ context "when something that does not respond to call is sumbitted as a differ" do
196
+ it "raises an error" do
197
+ expect { Pact.configuration.register_body_differ 'thing', Object.new }.to raise_error /responds to call/
198
+ end
199
+ end
200
+
201
+ context "when a nil content type is registered for responses without a content type header" do
202
+ it "returns that differ if the differ for a nil content type is requested" do
203
+ Pact.configuration.register_body_differ nil, differ
204
+ expect(Pact.configuration.body_differ_for_content_type(nil)).to be differ
205
+ end
206
+ end
207
+
208
+ end
209
+
210
+ describe "body_differ_for_content_type" do
211
+
212
+ let(:differ) { lambda { |expected, actual| }}
213
+
214
+ context "when 2 potentially matching content types have a differ registered" do
215
+ let(:differ_1) { lambda{ |expected, actual| } }
216
+ let(:differ_2) { lambda{ |expected, actual| } }
217
+
218
+ it "returns the differ that was configured first" do
219
+ Pact.configuration.register_body_differ /application\/.*xml/, differ_2
220
+ Pact.configuration.register_body_differ /application\/hal\+xml/, differ_1
221
+ expect(Pact.configuration.body_differ_for_content_type 'application/hal+xml').to be differ_2
222
+ end
223
+ end
224
+
225
+ context "when a nil content type is given" do
226
+ it "returns the text differ" do
227
+ expect(Pact.configuration.body_differ_for_content_type nil).to be Pact::TextDiffer
228
+ end
229
+ end
230
+
231
+ context "when no matching content type is found" do
232
+ it "returns the text differ" do
233
+ expect(Pact.configuration.body_differ_for_content_type 'blah').to be Pact::TextDiffer
234
+ end
235
+ end
236
+
237
+ context "when the nil content type has a custom differ configured" do
238
+ it "returns the custom differ" do
239
+ Pact.configuration.register_body_differ nil, differ
240
+ expect(Pact.configuration.body_differ_for_content_type(nil)).to be differ
241
+ end
242
+ end
243
+
244
+ context "when a custom differ is registered for a content type that has a default differ" do
245
+ it "returns the custom differ" do
246
+ Pact.configuration.register_body_differ /application\/json/, differ
247
+ expect(Pact.configuration.body_differ_for_content_type 'application/json').to be differ
248
+ end
249
+ end
250
+ end
251
+
130
252
  describe "pactfile_write_mode" do
131
253
  context "when @pactfile_write_mode is :overwrite" do
132
254
  it 'returns :overwrite' do
@@ -4,9 +4,10 @@ require 'pact/consumer/mock_service/interaction_mismatch'
4
4
  module Pact
5
5
  module Consumer
6
6
  describe InteractionMismatch do
7
+ let(:content_type) { 'some/content' }
7
8
  let(:actual_request) { instance_double('Pact::Consumer::Request::Actual', :method_and_path => 'GET /path') }
8
- let(:expected_request_1) { instance_double('Pact::Request::Expected') }
9
- let(:expected_request_2) { instance_double('Pact::Request::Expected') }
9
+ let(:expected_request_1) { instance_double('Pact::Request::Expected', :content_type => content_type) }
10
+ let(:expected_request_2) { instance_double('Pact::Request::Expected', :content_type => content_type) }
10
11
  let(:candidate_1) { instance_double('Pact::Interaction', request: expected_request_1, description_with_provider_state_quoted: "desc 1") }
11
12
  let(:candidate_2) { instance_double('Pact::Interaction', request: expected_request_2, description_with_provider_state_quoted: "desc 2") }
12
13
  let(:candidate_interactions) { [candidate_1, candidate_2] }
@@ -48,13 +49,15 @@ module Pact
48
49
  describe "to_s" do
49
50
  let(:expected_message) { "Diff with interaction: desc 1\ndiff 1\nDiff with interaction: desc 2\ndiff 2" }
50
51
 
52
+ let(:diff_formatter) { double("diff_formatter")}
51
53
  before do
52
- allow(Pact.configuration.diff_formatter).to receive(:call).and_return("diff 1", "diff 2")
54
+ allow(Pact.configuration).to receive(:diff_formatter_for_content_type).with(content_type).and_return(diff_formatter)
55
+ allow(diff_formatter).to receive(:call).and_return("diff 1", "diff 2")
53
56
  end
54
57
 
55
58
  it "creates diff output using the configured diff_formatter" do
56
- expect(Pact.configuration.diff_formatter).to receive(:call).with(diff_1, colour: false)
57
- expect(Pact.configuration.diff_formatter).to receive(:call).with(diff_2, colour: false)
59
+ expect(diff_formatter).to receive(:call).with(diff_1, colour: false)
60
+ expect(diff_formatter).to receive(:call).with(diff_2, colour: false)
58
61
  subject.to_s
59
62
  end
60
63
 
@@ -0,0 +1,107 @@
1
+ require 'spec_helper'
2
+ require 'pact/consumer_contract/headers'
3
+
4
+ module Pact
5
+ describe Headers do
6
+
7
+ describe "initialize" do
8
+
9
+ context "with duplicate headers" do
10
+
11
+ subject { Headers.new('Content-Type' => 'application/hippo', 'CONTENT-TYPE' => 'application/giraffe') }
12
+
13
+ it "raises an error" do
14
+ expect { subject }.to raise_error DuplicateHeaderError, /Content\-Type.*CONTENT\-TYPE/
15
+ end
16
+
17
+ end
18
+
19
+ context "with a symbol as a header name" do
20
+
21
+ subject { Headers.new(:'content-type' => 'application/hippo') }
22
+
23
+ it "converts the header name to a String" do
24
+ expect( subject.to_hash ).to eq 'content-type' => 'application/hippo'
25
+ end
26
+
27
+ end
28
+
29
+ context "with a nil header name" do
30
+
31
+ subject { Headers.new(nil => 'application/hippo') }
32
+
33
+ it "raises an error" do
34
+ expect{ subject }.to raise_error InvalidHeaderNameTypeError
35
+ end
36
+
37
+ end
38
+
39
+ context "with a boolean header name" do
40
+
41
+ subject { Headers.new(false => 'application/hippo') }
42
+
43
+ it "raises an error" do
44
+ expect{ subject }.to raise_error InvalidHeaderNameTypeError
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+
51
+ describe "[]" do
52
+
53
+ subject { Headers.new 'Content-Type' => 'application/hippo' }
54
+
55
+ it "is case insensitive as HTTP headers are case insensitive" do
56
+ expect(subject['Content-Type']).to eq('application/hippo')
57
+ expect(subject['CONTENT-TYPE']).to eq('application/hippo')
58
+ expect(subject['content-type']).to eq('application/hippo')
59
+ end
60
+
61
+ end
62
+
63
+ describe "fetch" do
64
+
65
+ subject { Headers.new 'Content-Type' => 'application/hippo' }
66
+
67
+ it "is case insensitive as HTTP headers are case insensitive" do
68
+ expect(subject.fetch('Content-Type')).to eq('application/hippo')
69
+ expect(subject.fetch('CONTENT-TYPE')).to eq('application/hippo')
70
+ expect(subject.fetch('content-type')).to eq('application/hippo')
71
+ expect(subject.fetch('Content-Length','1')).to eq('1')
72
+ expect { subject.fetch('Content-Length')}.to raise_error KeyError
73
+ end
74
+
75
+ end
76
+
77
+ describe "key?" do
78
+
79
+ subject { Headers.new 'Content-Type' => 'application/hippo' }
80
+
81
+ it "is case insensitive as HTTP headers are case insensitive" do
82
+ expect(subject.key?('CONTENT-TYPE')).to be true
83
+ expect(subject.key?('CONTENT-LENGTH')).to be false
84
+ end
85
+ end
86
+
87
+ describe "has_key?" do
88
+
89
+ subject { Headers.new 'Content-Type' => 'application/hippo' }
90
+
91
+ it "is case insensitive as HTTP headers are case insensitive" do
92
+ expect(subject.has_key?('CONTENT-TYPE')).to be true
93
+ expect(subject.has_key?('CONTENT-LENGTH')).to be false
94
+ end
95
+ end
96
+
97
+ describe "[]=" do
98
+
99
+ subject { Headers.new }
100
+
101
+ it "does not allow modification" do
102
+ expect{ subject['Content-Type'] = 'application/hippo' }.to raise_error /frozen/
103
+ end
104
+
105
+ end
106
+ end
107
+ end