pact_broker 2.60.1 → 2.61.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2af8e33b43adbd78dff9e58ae2c16ff69668694dc71b203487d789bd754f4e1a
4
- data.tar.gz: 1cf9633412dd10faac7179cfd34ee84bf84738961d7c131d5660f1e3938fe21a
3
+ metadata.gz: 3495dd128672a4f4e614b7c60d93734cb3eee3799863af054963c503172d86f2
4
+ data.tar.gz: be415648babf90774ef011a0485835f386632768fb95b5a1f125ea9bddea2492
5
5
  SHA512:
6
- metadata.gz: c02c5a85ba278137ad2c15022ecf055c74c59ddf9565617c9df8ca65dc83181d9ec444385bbec65f557de42e6686e837f3230825945cde1da6bb1bc5e20a6b01
7
- data.tar.gz: '0386fc2e7a2fcb3c3f019bcdbb49e37861259408b1aadfe298368ece37bbbb363260e4a01df6746ca137dea0be0ba220abbf497065300084576bd62d1bcf76f1'
6
+ metadata.gz: aabedcd424d01dfa172424f99feac69d71ce2c55fe97f5c6b25dc1e29247168c14f0aa0d56c144c353a9444cd1cce99e14f7b677161f609ca829d00fdd2cf72c
7
+ data.tar.gz: a18db48c05cb2f3ae9ebc48b18673d45b4bafe19574633fef2d0ae6c21c2242a7b5cd80dab359eeccedb7044dcb1c2895b8290065fd4e7c7db5ba9c6686fefc4
@@ -1,3 +1,10 @@
1
+ <a name="v2.61.0"></a>
2
+ ### v2.61.0 (2020-09-12)
3
+
4
+ #### Features
5
+
6
+ * add back support for GET requests to the 'pacts for verification' API with a deprecation notice in the response ([8f45cc9f](/../../commit/8f45cc9f))
7
+
1
8
  <a name="v2.60.1"></a>
2
9
  ### v2.60.1 (2020-09-10)
3
10
 
@@ -0,0 +1,37 @@
1
+ require 'dry-validation'
2
+ require 'pact_broker/api/contracts/dry_validation_workarounds'
3
+ require 'pact_broker/api/contracts/dry_validation_predicates'
4
+
5
+ module PactBroker
6
+ module Api
7
+ module Contracts
8
+ class VerifiablePactsQuerySchema
9
+ extend DryValidationWorkarounds
10
+ using PactBroker::HashRefinements
11
+
12
+ SCHEMA = Dry::Validation.Schema do
13
+ configure do
14
+ predicates(DryValidationPredicates)
15
+ config.messages_file = File.expand_path("../../../locale/en.yml", __FILE__)
16
+ end
17
+ optional(:provider_version_tags).maybe(:array?)
18
+ optional(:consumer_version_selectors).each do
19
+ schema do
20
+ required(:tag).filled(:str?)
21
+ optional(:latest).filled(included_in?: ["true", "false"])
22
+ optional(:fallback_tag).filled(:str?)
23
+ optional(:consumer).filled(:str?, :not_blank?)
24
+ end
25
+ end
26
+ optional(:include_pending_status).filled(included_in?: ["true", "false"])
27
+ optional(:include_wip_pacts_since).filled(:date?)
28
+ end
29
+
30
+ def self.call(params)
31
+ select_first_message(flatten_indexed_messages(SCHEMA.call(params&.symbolize_keys).messages(full: true)))
32
+ end
33
+
34
+ end
35
+ end
36
+ end
37
+ end
@@ -14,14 +14,14 @@ module PactBroker
14
14
 
15
15
  property :pending,
16
16
  if: ->(context) { context[:options][:user_options][:include_pending_status] }
17
- property :wip, if: -> (context) { context[:represented].wip }
18
-
19
- property :notices, getter: -> (context) { context[:decorator].notices(context[:options][:user_options]) }
20
- property :noteToDevelopers, getter: -> (_) { "Please print out the text from the 'notices' rather than using the inclusionReason and the pendingReason fields. These will be removed when this API moves out of beta."}
17
+ property :wip,
18
+ if: -> (context) { context[:represented].wip }
19
+ property :notices,
20
+ getter: -> (context) { context[:decorator].notices(context[:options][:user_options]) }
21
21
 
22
22
  def notices(user_options)
23
23
  pact_url = pact_version_url(represented, user_options[:base_url])
24
- PactBroker::Pacts::BuildVerifiablePactNotices.call(represented, pact_url, include_pending_status: user_options[:include_pending_status])
24
+ PactBroker::Pacts::BuildVerifiablePactNotices.call(represented, pact_url, user_options)
25
25
  end
26
26
  end
27
27
 
@@ -46,7 +46,7 @@ module PactBroker
46
46
  end
47
47
 
48
48
  def policy_name
49
- :'webhooks::webooks'
49
+ :'webhooks::webhooks'
50
50
  end
51
51
 
52
52
  private
@@ -1,5 +1,6 @@
1
1
  require 'pact_broker/api/resources/provider_pacts'
2
2
  require 'pact_broker/api/decorators/verifiable_pacts_decorator'
3
+ require 'pact_broker/api/contracts/verifiable_pacts_query_schema'
3
4
  require 'pact_broker/api/decorators/verifiable_pacts_query_decorator'
4
5
  require 'pact_broker/api/contracts/verifiable_pacts_json_query_schema'
5
6
  require 'pact_broker/hash_refinements'
@@ -11,7 +12,7 @@ module PactBroker
11
12
  using PactBroker::HashRefinements
12
13
 
13
14
  def allowed_methods
14
- ["POST", "OPTIONS"]
15
+ ["GET", "POST", "OPTIONS"]
15
16
  end
16
17
 
17
18
  def content_types_accepted
@@ -51,11 +52,21 @@ module PactBroker
51
52
  end
52
53
 
53
54
  def to_json
54
- PactBroker::Api::Decorators::VerifiablePactsDecorator.new(pacts).to_json(to_json_options)
55
+ PactBroker::Api::Decorators::VerifiablePactsDecorator.new(pacts).to_json(
56
+ decorator_options(
57
+ include_pending_status: parsed_query_params.include_pending_status,
58
+ title: "Pacts to be verified by provider #{provider_name}",
59
+ deprecated: request.get?
60
+ )
61
+ )
55
62
  end
56
63
 
57
64
  def query_schema
58
- PactBroker::Api::Contracts::VerifiablePactsJSONQuerySchema
65
+ if request.get?
66
+ PactBroker::Api::Contracts::VerifiablePactsQuerySchema
67
+ else
68
+ PactBroker::Api::Contracts::VerifiablePactsJSONQuerySchema
69
+ end
59
70
  end
60
71
 
61
72
  def parsed_query_params
@@ -71,10 +82,6 @@ module PactBroker
71
82
  end
72
83
  end
73
84
  end
74
-
75
- def to_json_options
76
- super.deep_merge(user_options: { include_pending_status: parsed_query_params.include_pending_status })
77
- end
78
85
  end
79
86
  end
80
87
  end
@@ -32,7 +32,7 @@ module PactBroker
32
32
  end
33
33
 
34
34
  def policy_name
35
- :'pact::pacts'
35
+ :'pacts::pacts'
36
36
  end
37
37
  end
38
38
  end
@@ -25,7 +25,7 @@ module PactBroker
25
25
  end
26
26
 
27
27
  def policy_name
28
- :'webhook::webhook'
28
+ :'webhooks::webhook'
29
29
  end
30
30
 
31
31
  private
@@ -227,7 +227,7 @@ module PactBroker
227
227
 
228
228
  def configure_sucker_punch
229
229
  SuckerPunch.exception_handler = -> (ex, klass, args) do
230
- PactBroker.log_error(ex, "Unhandled Suckerpunch error for #{klass}.perform(#{args.inspect})")
230
+ PactBroker.logger.warn("Unhandled Suckerpunch error for #{klass}.perform(#{args.inspect})", ex)
231
231
  end
232
232
  end
233
233
 
@@ -100,7 +100,7 @@ module PactBroker
100
100
  logger.warn "Timeout retrieving badge from #{uri} #{e.class} - #{e.message}"
101
101
  nil
102
102
  rescue StandardError => e
103
- log_error e, "Error retrieving badge from #{uri}"
103
+ logger.warn("Error retrieving badge from #{uri}", e)
104
104
  nil
105
105
  end
106
106
  end
@@ -18,7 +18,7 @@ module PactBroker
18
18
  logger.debug("Loading certificate #{certificate.subject} in to cert store")
19
19
  cert_store.add_cert(certificate)
20
20
  rescue StandardError => e
21
- log_error e, "Error adding certificate object #{certificate.to_s} to store"
21
+ logger.warn("Error adding certificate object #{certificate.to_s} to store", e)
22
22
  end
23
23
  end
24
24
  cert_store
@@ -31,7 +31,7 @@ module PactBroker
31
31
  begin
32
32
  OpenSSL::X509::Certificate.new(c)
33
33
  rescue StandardError => e
34
- log_error e, "Error creating certificate object from certificate #{certificate.uuid} '#{certificate.description}'"
34
+ logger.warn("Error creating certificate object from certificate #{certificate.uuid} '#{certificate.description}'", e)
35
35
  nil
36
36
  end
37
37
  end
@@ -7,10 +7,13 @@ module PactBroker
7
7
  def self.call(verifiable_pact, pact_url, options)
8
8
  messages = VerifiablePactMessages.new(verifiable_pact, pact_url)
9
9
 
10
- notices = [{
11
- when: 'before_verification',
12
- text: messages.inclusion_reason
13
- }]
10
+ notices = []
11
+
12
+ if options[:deprecated]
13
+ append_notice(notices, 'before_verification', 'WARNING - this version of the Pact library uses a beta version of the API which will be removed in the future. Please upgrade your Pact library. See https://docs.pact.io/pact_broker/advanced_topics/provider_verification_results/#pacts-for-verification for minimum required versions.')
14
+ end
15
+
16
+ append_notice(notices, 'before_verification', messages.inclusion_reason)
14
17
 
15
18
  if options[:include_pending_status]
16
19
  append_notice(notices, 'before_verification', messages.pending_reason)
@@ -1,3 +1,3 @@
1
1
  module PactBroker
2
- VERSION = '2.60.1'
2
+ VERSION = '2.61.0'
3
3
  end
@@ -143,7 +143,7 @@ module PactBroker
143
143
  # Delay slightly to make sure the request transaction has finished before we execute the webhook
144
144
  Job.perform_in(5, job_data)
145
145
  rescue StandardError => e
146
- log_error e
146
+ logger.warn("Error scheduling webhook execution for webhook with uuid #{webhook.uuid}", e)
147
147
  end
148
148
  end
149
149
  end
@@ -46,24 +46,46 @@ webhook_body = {
46
46
 
47
47
  PactBroker.configuration.base_equality_only_on_content_that_affects_verification_results = false
48
48
 
49
- # .create_global_webhook(
50
- # method: 'POST',
51
- # url: "http://localhost:9292/pact-changed-webhook",
52
- # body: webhook_body.to_json,
53
- # username: "foo",
54
- # password: "bar")
55
- TestDataBuilder.new
56
- .create_global_verification_succeeded_webhook(
57
- method: 'POST',
58
- url: "http://localhost:9292/verification-published-webhook",
59
- body: webhook_body.to_json)
60
- .set_now(Date.today - 101)
61
- .create_pact_with_hierarchy("Foo/Foo", "100", "Bar/Bar")
62
- .create_pact_with_hierarchy("Foo", "1", "Bar")
63
- .create_pact_with_hierarchy("<script>alert('hello')</script>", "<script>alert(\"version\")</script>", "Bar/Bar")
64
- .create_consumer_version_tag("prod")
65
- .create_verification(provider_version: "1", tag_names: "prod")
49
+ json_content = <<-HEREDOC
50
+ {
51
+ "consumer": {
52
+ "name": "Foo"
53
+ },
54
+ "provider": {
55
+ "name": "Bar"
56
+ },
57
+ "interactions": [
58
+ {
59
+ "description": "a retrieve thing request",
60
+ "request": {
61
+ "method": "get",
62
+ "path": "/thing"
63
+ },
64
+ "response": {
65
+ "status": 200,
66
+ "headers": {
67
+ "Content-Type": "application/json"
68
+ },
69
+ "body": {
70
+ "name": "Thing 1"
71
+ }
72
+ }
73
+ }
74
+ ],
75
+ "metadata": {
76
+ "pactSpecification": {
77
+ "version": "2.0.0"
78
+ }
79
+ }
80
+ }
81
+ HEREDOC
66
82
 
83
+ TestDataBuilder.new
84
+ .create_pact_with_hierarchy("Foo", "1", "Bar", json_content)
85
+ .create_consumer_version_tag("master")
86
+ .create_verification(provider_version: "1", tag_name: "master")
87
+ .create_pact_with_hierarchy("Foo", "2", "Bar", json_content.gsub("200", "201"))
88
+ .create_consumer_version_tag("feat/x")
67
89
 
68
90
  # .create_certificate(path: 'spec/fixtures/certificates/self-signed.badssl.com.pem')
69
91
  # .create_consumer("Bethtest")
@@ -24,6 +24,28 @@ describe "Get provider pacts for verification" do
24
24
 
25
25
  let(:path) { "/pacts/provider/Provider/for-verification" }
26
26
 
27
+ context "when using GET" do
28
+ it "returns a 200 HAL JSON response" do
29
+ expect(subject).to be_a_hal_json_success_response
30
+ end
31
+
32
+ it "returns a list of links to the pacts" do
33
+ expect(pacts.size).to eq 1
34
+ end
35
+
36
+ it "returns a deprecation notice" do
37
+ expect(last_response_body[:_embedded][:pacts].first[:verificationProperties][:notices].first[:text]).to start_with("WARNING")
38
+ end
39
+
40
+ context "when the provider does not exist" do
41
+ let(:path) { "/pacts/provider/ProviderThatDoesNotExist/for-verification" }
42
+
43
+ it "returns a 404 response" do
44
+ expect(subject).to be_a_404_response
45
+ end
46
+ end
47
+ end
48
+
27
49
  context "when using POST" do
28
50
  let(:request_body) do
29
51
  {
@@ -44,6 +66,10 @@ describe "Get provider pacts for verification" do
44
66
  expect(pacts.size).to eq 1
45
67
  end
46
68
 
69
+ it "does not include a deprecation notice" do
70
+ expect(last_response_body[:_embedded][:pacts].first[:verificationProperties][:notices].first[:text]).to_not start_with("WARNING")
71
+ end
72
+
47
73
  context "when the provider does not exist" do
48
74
  let(:path) { "/pacts/provider/ProviderThatDoesNotExist/for-verification" }
49
75
 
@@ -0,0 +1,97 @@
1
+ require 'pact_broker/api/contracts/verifiable_pacts_query_schema'
2
+
3
+ module PactBroker
4
+ module Api
5
+ module Contracts
6
+ describe VerifiablePactsQuerySchema do
7
+ let(:params) do
8
+ {
9
+ provider_version_tags: provider_version_tags,
10
+ consumer_version_selectors: consumer_version_selectors
11
+ }
12
+ end
13
+
14
+ let(:provider_version_tags) { %w[master] }
15
+
16
+ let(:consumer_version_selectors) do
17
+ [{
18
+ tag: "master",
19
+ latest: "true"
20
+ }]
21
+ end
22
+
23
+ subject { VerifiablePactsQuerySchema.(params) }
24
+
25
+ context "when the params are valid" do
26
+ it "has no errors" do
27
+ expect(subject).to eq({})
28
+ end
29
+ end
30
+
31
+ context "when provider_version_tags is not an array" do
32
+ let(:provider_version_tags) { "foo" }
33
+
34
+ it { is_expected.to have_key(:provider_version_tags) }
35
+ end
36
+
37
+ context "when the consumer_version_selector is missing a tag" do
38
+ let(:consumer_version_selectors) do
39
+ [{}]
40
+ end
41
+
42
+ it "flattens the messages" do
43
+ expect(subject[:consumer_version_selectors].first).to eq "tag is missing at index 0"
44
+ end
45
+ end
46
+
47
+ context "when the consumer_version_selectors is missing the latest" do
48
+ let(:consumer_version_selectors) do
49
+ [{
50
+ tag: "master"
51
+ }]
52
+ end
53
+
54
+ it { is_expected.to be_empty }
55
+ end
56
+
57
+ context "when include_wip_pacts_since key exists" do
58
+ let(:include_wip_pacts_since) { nil }
59
+ let(:params) do
60
+ {
61
+ include_wip_pacts_since: include_wip_pacts_since
62
+ }
63
+ end
64
+
65
+ context "when it is nil" do
66
+ it { is_expected.to have_key(:include_wip_pacts_since) }
67
+ end
68
+
69
+ context "when it is not a date" do
70
+ let(:include_wip_pacts_since) { "foo" }
71
+
72
+ it { is_expected.to have_key(:include_wip_pacts_since) }
73
+ end
74
+
75
+ context "when it is a valid date" do
76
+ let(:include_wip_pacts_since) { "2013-02-13T20:04:45.000+11:00" }
77
+
78
+ it { is_expected.to_not have_key(:include_wip_pacts_since) }
79
+ end
80
+ end
81
+
82
+ context "when a blank consumer name is specified" do
83
+ let(:consumer_version_selectors) do
84
+ [{
85
+ tag: "feat-x",
86
+ consumer: ""
87
+ }]
88
+ end
89
+
90
+ it "has an error" do
91
+ expect(subject[:consumer_version_selectors].first).to include "blank"
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -155,7 +155,6 @@ module PactBroker
155
155
 
156
156
  it "has a policy_name method" do
157
157
  expect(resource).to respond_to(:policy_name)
158
- puts resource.policy_name
159
158
  end
160
159
  end
161
160
  end
@@ -23,7 +23,35 @@ module PactBroker
23
23
  }
24
24
  end
25
25
 
26
- subject { post(path, request_body.to_json, request_headers) }
26
+ subject { get(path, query) }
27
+
28
+ describe "GET" do
29
+ it "finds the pacts for verification by the provider" do
30
+ # Naughty not mocking out the query parsing...
31
+ expect(PactBroker::Pacts::Service).to receive(:find_for_verification).with(
32
+ "Bar",
33
+ ["master"],
34
+ PactBroker::Pacts::Selectors.new([PactBroker::Pacts::Selector.latest_for_tag("dev")]),
35
+ {
36
+ include_wip_pacts_since: DateTime.parse('2018-01-01'),
37
+ include_pending_status: true
38
+ }
39
+ )
40
+ subject
41
+ end
42
+
43
+ context "when there are validation errors" do
44
+ let(:query) do
45
+ {
46
+ provider_version_tags: true,
47
+ }
48
+ end
49
+
50
+ it "returns the keys with the right case" do
51
+ expect(JSON.parse(subject.body)['errors']).to have_key('provider_version_tags')
52
+ end
53
+ end
54
+ end
27
55
 
28
56
  describe "POST" do
29
57
  let(:request_body) do
@@ -42,6 +70,8 @@ module PactBroker
42
70
  }
43
71
  end
44
72
 
73
+ subject { post(path, request_body.to_json, request_headers) }
74
+
45
75
  it "finds the pacts for verification by the provider" do
46
76
  # Naughty not mocking out the query parsing...
47
77
  expect(PactBroker::Pacts::Service).to receive(:find_for_verification).with(
@@ -67,14 +97,14 @@ module PactBroker
67
97
  expect(JSON.parse(subject.body)['errors']).to have_key('providerVersionTags')
68
98
  end
69
99
  end
100
+ end
70
101
 
71
- it "uses the correct options for the decorator" do
72
- expect(decorator).to receive(:to_json) do | options |
73
- expect(options[:user_options][:title]).to eq "Pacts to be verified by provider Bar"
74
- expect(options[:user_options][:include_pending_status]).to eq true
75
- end
76
- subject
102
+ it "uses the correct options for the decorator" do
103
+ expect(decorator).to receive(:to_json) do | options |
104
+ expect(options[:user_options][:title]).to eq "Pacts to be verified by provider Bar"
105
+ expect(options[:user_options][:include_pending_status]).to eq true
77
106
  end
107
+ subject
78
108
  end
79
109
  end
80
110
  end
@@ -286,7 +286,7 @@ module PactBroker
286
286
  end
287
287
 
288
288
  it "logs the error" do
289
- expect(logger).to receive(:error).with(/Error retrieving badge from.*shield.*an error/)
289
+ expect(logger).to receive(:warn).with(/Error retrieving badge from.*shield.*/, RuntimeError)
290
290
  subject
291
291
  end
292
292
 
@@ -30,8 +30,8 @@ module PactBroker
30
30
  it "logs the error" do
31
31
  subject
32
32
 
33
- expect(logger).to have_received(:error)
34
- .with(/Error adding certificate/).at_least(1).times
33
+ expect(logger).to have_received(:warn)
34
+ .with(/Error adding certificate/, StandardError).at_least(1).times
35
35
  end
36
36
 
37
37
  it "returns an OpenSSL::X509::Store" do
@@ -66,7 +66,7 @@ module PactBroker
66
66
  let(:certificate_content) { File.read('spec/fixtures/certificate-invalid.pem') }
67
67
 
68
68
  it "logs an error" do
69
- expect(logger).to receive(:error).with(/Error.*1234/)
69
+ expect(logger).to receive(:warn).with(/Error.*1234/, StandardError)
70
70
  subject
71
71
  end
72
72
 
@@ -233,8 +233,8 @@ module PactBroker
233
233
  end
234
234
 
235
235
  it "logs the error" do
236
- allow(Service.logger).to receive(:error)
237
- expect(Service.logger).to receive(:error).with(/an error/)
236
+ allow(Service.logger).to receive(:warn)
237
+ expect(Service.logger).to receive(:warn).with(/Error scheduling/, StandardError)
238
238
  subject
239
239
  end
240
240
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pact_broker
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.60.1
4
+ version: 2.61.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bethany Skurrie
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-09-09 00:00:00.000000000 Z
13
+ date: 2020-09-17 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: httparty
@@ -581,6 +581,7 @@ files:
581
581
  - lib/pact_broker/api/contracts/put_pact_params_contract.rb
582
582
  - lib/pact_broker/api/contracts/request_validations.rb
583
583
  - lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema.rb
584
+ - lib/pact_broker/api/contracts/verifiable_pacts_query_schema.rb
584
585
  - lib/pact_broker/api/contracts/verification_contract.rb
585
586
  - lib/pact_broker/api/contracts/webhook_contract.rb
586
587
  - lib/pact_broker/api/decorators.rb
@@ -1120,6 +1121,7 @@ files:
1120
1121
  - spec/lib/pact/doc/markdown/index_renderer_spec.rb
1121
1122
  - spec/lib/pact_broker/api/contracts/put_pact_params_contract_spec.rb
1122
1123
  - spec/lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema_spec.rb
1124
+ - spec/lib/pact_broker/api/contracts/verifiable_pacts_query_schema_spec.rb
1123
1125
  - spec/lib/pact_broker/api/contracts/verification_contract_spec.rb
1124
1126
  - spec/lib/pact_broker/api/contracts/webhook_contract_spec.rb
1125
1127
  - spec/lib/pact_broker/api/decorators/dashboard_decorator_spec.rb
@@ -1505,6 +1507,7 @@ test_files:
1505
1507
  - spec/lib/pact/doc/markdown/index_renderer_spec.rb
1506
1508
  - spec/lib/pact_broker/api/contracts/put_pact_params_contract_spec.rb
1507
1509
  - spec/lib/pact_broker/api/contracts/verifiable_pacts_json_query_schema_spec.rb
1510
+ - spec/lib/pact_broker/api/contracts/verifiable_pacts_query_schema_spec.rb
1508
1511
  - spec/lib/pact_broker/api/contracts/verification_contract_spec.rb
1509
1512
  - spec/lib/pact_broker/api/contracts/webhook_contract_spec.rb
1510
1513
  - spec/lib/pact_broker/api/decorators/dashboard_decorator_spec.rb