pact_broker-client 1.68.0 → 1.70.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: 34449d696b8c2dd03158cb963bb29d5a98df30563b1dfc34172a2e0005421824
4
- data.tar.gz: 77f60ba7270a1b1fb8f59a41fd97e047d28536d6857d88109a52efedb6b018ff
3
+ metadata.gz: c39e09b6dd262ec6baa6da53747ac6adfcac1b83e87050973d10624d6d0dbc85
4
+ data.tar.gz: 817e36de371bb43762e42c8103735f291a1645a782541581cbff501a70b309da
5
5
  SHA512:
6
- metadata.gz: 62f45e1885ff173605607913f796f87c1dae40b2618c92b240ffca95fd9e2be4cbae2c50d89676ac82fb26b6e0deb610bec00b2f81a23314b4d9a2927f7c9a9b
7
- data.tar.gz: 4bac04b4928c5f1846a963f7b8fb27a860332090ae7c69edce1de6537f63974e5a959ed62333a5fa17feda196525503d6dda3c470794303ddb9d03aff9723063
6
+ metadata.gz: 68d61e0fef6e96bd4acce08bc781aee2895b209cecbb0e3f453fe8e7e3843f417e5f3f62604dfda8ed5bee803c382c49eb552d17cc5fe06101fcab9848e1e34e
7
+ data.tar.gz: bf36dbe78ff7fdd10af8983be7130928389175c8b6770ee1772f9a31ccac6920d6316890850d33d8c179a8e9ce3dd1467338810e2879dd71079c751bc2e117a4
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "monthly"
@@ -31,7 +31,7 @@ jobs:
31
31
  - uses: actions/checkout@v3
32
32
  with:
33
33
  fetch-depth: 0
34
- - uses: pact-foundation/release-gem@v0.0.13
34
+ - uses: pact-foundation/release-gem@v0.0.14
35
35
  id: release
36
36
  env:
37
37
  GEM_HOST_API_KEY: '${{ secrets.RUBYGEMS_API_KEY }}'
@@ -0,0 +1,15 @@
1
+ name: Triage Issue
2
+
3
+ on:
4
+ issues:
5
+ types:
6
+ - opened
7
+ - labeled
8
+ pull_request:
9
+ types:
10
+ - labeled
11
+
12
+ jobs:
13
+ call-workflow:
14
+ uses: pact-foundation/.github/.github/workflows/triage.yml@master
15
+ secrets: inherit
data/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ <a name="v1.70.0"></a>
2
+ ### v1.70.0 (2023-08-29)
3
+
4
+ #### Features
5
+
6
+ * sort can-i-deploy table by consumer name, then provider name ([83412e7](/../../commit/83412e7))
7
+ * do not accept gzip responses when VERBOSE=true ([a72a529](/../../commit/a72a529))
8
+
9
+ <a name="v1.69.0"></a>
10
+ ### v1.69.0 (2023-08-29)
11
+
12
+ #### Features
13
+
14
+ * can-i-merge (#117) ([badb030](/../../commit/badb030))
15
+
1
16
  <a name="v1.68.0"></a>
2
17
  ### v1.68.0 (2023-07-08)
3
18
 
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -444,16 +444,24 @@ Options:
444
444
  -l, [--latest=[TAG]]
445
445
  # Use the latest pacticipant version. Optionally specify a TAG
446
446
  to use the latest version with the specified tag.
447
- [--to-environment=ENVIRONMENT]
448
- # The environment into which the pacticipant(s) are to be
449
- deployed
450
447
  [--branch=BRANCH]
451
448
  # The branch of the version for which you want to check the
449
+ verification results.
450
+ [--main-branch], [--no-main-branch]
451
+ # Use the latest version of the configured main branch of the
452
+ pacticipant as the version for which you want to check the
452
453
  verification results
454
+ [--to-environment=ENVIRONMENT]
455
+ # The environment into which the pacticipant(s) are to be
456
+ deployed
453
457
  [--to=TAG]
454
458
  # The tag that represents the branch or environment of the
455
459
  integrated applications for which you want to check the
456
460
  verification result status.
461
+ [--ignore=IGNORE]
462
+ # The pacticipant name to ignore. Use once for each pacticipant
463
+ being ignored. A specific version can be ignored by also
464
+ specifying a --version after the pacticipant name option.
457
465
  -o, [--output=OUTPUT]
458
466
  # json or table
459
467
  # Default: table
@@ -1,20 +1,25 @@
1
+ require "pact_broker/client/string_refinements"
1
2
  module PactBroker
2
3
  module Client
3
4
  module CLI
4
5
  module MatrixCommands
6
+ using PactBroker::Client::StringRefinements
7
+
5
8
  def self.included(thor)
6
9
  thor.class_eval do
7
10
 
8
- desc "can-i-deploy", ""
11
+ desc "can-i-deploy", "Checks if the specified pacticipant version is safe to be deployed."
9
12
  long_desc File.read(File.join(__dir__, "can_i_deploy_long_desc.txt"))
10
13
 
11
14
  method_option :pacticipant, required: true, aliases: "-a", desc: "The pacticipant name. Use once for each pacticipant being checked."
12
15
  method_option :version, required: false, aliases: "-e", desc: "The pacticipant version. Must be entered after the --pacticipant that it relates to."
13
16
  method_option :ignore, required: false, desc: "The pacticipant name to ignore. Use once for each pacticipant being ignored. A specific version can be ignored by also specifying a --version after the pacticipant name option. The environment variable PACT_BROKER_CAN_I_DEPLOY_IGNORE may also be used to specify a pacticipant name to ignore, with commas to separate multiple pacticipant names if necessary."
14
17
  method_option :latest, required: false, aliases: "-l", banner: "[TAG]", desc: "Use the latest pacticipant version. Optionally specify a TAG to use the latest version with the specified tag."
18
+ method_option :branch, required: false, desc: "The branch of the version for which you want to check the verification results.", default: nil
19
+ method_option :main_branch, required: false, type: :boolean, desc: "Use the latest version of the configured main branch of the pacticipant as the version for which you want to check the verification results", default: false
15
20
  method_option :to_environment, required: false, banner: "ENVIRONMENT", desc: "The environment into which the pacticipant(s) are to be deployed", default: nil
16
- method_option :branch, required: false, desc: "The branch of the version for which you want to check the verification results", default: nil
17
21
  method_option :to, required: false, banner: "TAG", desc: "The tag that represents the branch or environment of the integrated applications for which you want to check the verification result status.", default: nil
22
+ method_option :ignore, required: false, desc: "The pacticipant name to ignore. Use once for each pacticipant being ignored. A specific version can be ignored by also specifying a --version after the pacticipant name option."
18
23
  method_option :output, aliases: "-o", desc: "json or table", default: "table"
19
24
  method_option :retry_while_unknown, banner: "TIMES", type: :numeric, default: 0, required: false, desc: "The number of times to retry while there is an unknown verification result (ie. the provider verification is likely still running)"
20
25
  method_option :retry_interval, banner: "SECONDS", type: :numeric, default: 10, required: false, desc: "The time between retries in seconds. Use in conjuction with --retry-while-unknown"
@@ -40,6 +45,31 @@ module PactBroker
40
45
  exit(can_i_deploy_exit_status) unless result.success
41
46
  end
42
47
 
48
+ desc "can-i-merge", "Checks if the specified pacticipant version is safe to merge into the main branch."
49
+ long_desc "Checks if the specified pacticipant version is compatible with the configured main branch of each of the pacticipants with which it is integrated."
50
+ method_option :pacticipant, required: true, aliases: "-a", desc: "The pacticipant name. Use once for each pacticipant being checked."
51
+ method_option :version, required: false, aliases: "-e", desc: "The pacticipant version. Must be entered after the --pacticipant that it relates to."
52
+ method_option :output, aliases: "-o", desc: "json or table", default: "table"
53
+ method_option :retry_while_unknown, banner: "TIMES", type: :numeric, default: 0, required: false, desc: "The number of times to retry while there is an unknown verification result (ie. the provider verification is likely still running)"
54
+ method_option :retry_interval, banner: "SECONDS", type: :numeric, default: 10, required: false, desc: "The time between retries in seconds. Use in conjuction with --retry-while-unknown"
55
+ method_option :dry_run, type: :boolean, default: false, desc: "When dry-run is enabled, always exit process with a success code. Can also be enabled by setting the environment variable PACT_BROKER_CAN_I_MERGE_DRY_RUN=true. This mode is useful when setting up your CI/CD pipeline for the first time, or in a 'break glass' situation where you need to knowingly deploy what Pact considers a breaking change. For the second scenario, it is recommended to use the environment variable and just set it for the build required to deploy that particular version, so you don't accidentally leave the dry run mode enabled."
56
+ shared_authentication_options
57
+
58
+ def can_i_merge(*ignored_but_necessary)
59
+ require "pact_broker/client/cli/version_selector_options_parser"
60
+ require "pact_broker/client/can_i_deploy"
61
+
62
+ validate_credentials
63
+ selectors = VersionSelectorOptionsParser.call(ARGV)
64
+ validate_can_i_deploy_selectors(selectors)
65
+ dry_run = options.dry_run || ENV["PACT_BROKER_CAN_I_MERGE_DRY_RUN"] == "true"
66
+ can_i_merge_options = { output: options.output, retry_while_unknown: options.retry_while_unknown, retry_interval: options.retry_interval, dry_run: dry_run, verbose: options.verbose }
67
+ result = CanIDeploy.call(selectors, { with_main_branches: true }, can_i_merge_options, pact_broker_client_options)
68
+ $stdout.puts result.message
69
+ $stdout.flush
70
+ exit(1) unless result.success
71
+ end
72
+
43
73
  if ENV.fetch("PACT_BROKER_FEATURES", "").include?("verification_required")
44
74
 
45
75
  method_option :pacticipant, required: true, aliases: "-a", desc: "The pacticipant name. Use once for each pacticipant being checked."
@@ -50,7 +80,7 @@ module PactBroker
50
80
  method_option :output, aliases: "-o", desc: "json or table", default: "table"
51
81
 
52
82
  shared_authentication_options
53
- desc "verification-required", "Checks if there is a verification required between the given pacticipant versions"
83
+ desc "verification-required", "Checks if there is a verification required between the given pacticipant versions."
54
84
  def verification_required(*ignored_but_necessary)
55
85
  require "pact_broker/client/cli/version_selector_options_parser"
56
86
  require "pact_broker/client/verification_required"
@@ -78,7 +108,7 @@ module PactBroker
78
108
  end
79
109
  end
80
110
 
81
- def validate_can_i_deploy_selectors selectors
111
+ def validate_can_i_deploy_selectors(selectors)
82
112
  pacticipants_without_versions = selectors.select{ |s| s[:version].nil? && s[:latest].nil? && s[:tag].nil? && s[:branch].nil? }.collect{ |s| s[:pacticipant] }
83
113
  raise ::Thor::RequiredArgumentMissingError, "The version must be specified using `--version VERSION`, `--branch BRANCH` `--latest`, `--latest TAG`, or `--all TAG` for pacticipant #{pacticipants_without_versions.join(", ")}" if pacticipants_without_versions.any?
84
114
  end
@@ -17,6 +17,10 @@ module PactBroker
17
17
  when "--latest", "-l"
18
18
  selectors << { pacticipant: nil } if selectors.empty?
19
19
  selectors.last[:latest] = true
20
+ when "--main-branch"
21
+ selectors << { pacticipant: nil } if selectors.empty?
22
+ selectors.last[:main_branch] = true
23
+ selectors.last[:latest] = true
20
24
  when /^\-/
21
25
  nil
22
26
  else
@@ -35,6 +39,10 @@ module PactBroker
35
39
  selectors << { pacticipant: nil } if selectors.empty?
36
40
  selectors.last[:branch] = word
37
41
  selectors.last[:latest] = true
42
+ when "--main-branch"
43
+ selectors << { pacticipant: nil } if selectors.empty?
44
+ selectors.last[:main_branch] = true
45
+ selectors.last[:latest] = true
38
46
  when "--all"
39
47
  selectors << { pacticipant: nil } if selectors.empty?
40
48
  selectors.last[:tag] = word
@@ -50,6 +50,7 @@ module PactBroker
50
50
  request['Content-Type'] ||= "application/json" if ['Post', 'Put'].include?(http_method)
51
51
  request['Content-Type'] ||= "application/merge-patch+json" if ['Patch'].include?(http_method)
52
52
  request['Accept'] = "application/hal+json"
53
+ request['Accept-Encoding'] = nil if verbose?
53
54
  headers.each do | key, value |
54
55
  request[key] = value
55
56
  end
@@ -43,6 +43,9 @@ module PactBroker
43
43
  if matrix_options[:to_tag]
44
44
  opts[:latest] = 'true'
45
45
  opts[:tag] = matrix_options[:to_tag]
46
+ elsif matrix_options[:with_main_branches]
47
+ opts[:latest] = 'true'
48
+ opts[:mainBranch] = 'true'
46
49
  elsif selectors.size == 1 && !matrix_options[:to_environment]
47
50
  opts[:latest] = 'true'
48
51
  end
@@ -59,11 +62,11 @@ module PactBroker
59
62
  hash[:latest] = 'true' if selector[:latest]
60
63
  hash[:tag] = selector[:tag] if selector[:tag]
61
64
  hash[:branch] = selector[:branch] if selector[:branch]
65
+ hash[:mainBranch] = 'true' if selector[:main_branch]
62
66
  end
63
67
  end
64
68
  end
65
69
 
66
-
67
70
  def result_message
68
71
  if json_output?
69
72
  response_entity.response.raw_body
@@ -12,11 +12,11 @@ module PactBroker
12
12
  Line = Struct.new(:consumer, :consumer_version, :provider, :provider_version, :success, :ref, :ignored)
13
13
 
14
14
  def self.call(matrix)
15
- matrix_rows = matrix[:matrix]
15
+ matrix_rows = sort_matrix_rows(matrix[:matrix] || [])
16
16
  return "" if matrix_rows.size == 0
17
17
  data = prepare_data(matrix_rows)
18
18
  printer = TablePrint::Printer.new(data, tp_options(data))
19
- printer.table_print + verification_result_urls_text(matrix)
19
+ printer.table_print + verification_result_urls_text(matrix_rows)
20
20
  end
21
21
 
22
22
  def self.prepare_data(matrix_rows)
@@ -55,8 +55,8 @@ module PactBroker
55
55
  default
56
56
  end
57
57
 
58
- def self.verification_results_urls_and_successes(matrix)
59
- (matrix[:matrix] || []).collect do | row |
58
+ def self.verification_results_urls_and_successes(matrix_rows)
59
+ matrix_rows.collect do | row |
60
60
  url = row.dig(:verificationResult, :_links, :self, :href)
61
61
  if url
62
62
  success = row.dig(:verificationResult, :success)
@@ -67,8 +67,8 @@ module PactBroker
67
67
  end.compact
68
68
  end
69
69
 
70
- def self.verification_result_urls_text(matrix)
71
- text = self.verification_results_urls_and_successes(matrix).each_with_index.collect do |(url, success), i|
70
+ def self.verification_result_urls_text(matrix_rows)
71
+ text = self.verification_results_urls_and_successes(matrix_rows).each_with_index.collect do |(url, success), i|
72
72
  status = success ? 'success' : 'failure'
73
73
  "#{i+1}. #{url} (#{status})"
74
74
  end.join("\n")
@@ -83,6 +83,19 @@ module PactBroker
83
83
  def self.max_width(data, column, title)
84
84
  (data.collect{ |row| row.send(column) } + [title]).compact.collect(&:size).max
85
85
  end
86
+
87
+ def self.sort_matrix_rows(matrix_rows)
88
+ matrix_rows&.sort { |row_1, row_2| sortable_attributes(row_1) <=> sortable_attributes(row_2) }
89
+ end
90
+
91
+ def self.sortable_attributes(matrix_row)
92
+ [
93
+ matrix_row.dig(:consumer, :name)&.downcase || "",
94
+ matrix_row.dig(:provider, :name)&.downcase || "",
95
+ matrix_row.dig(:consumer, :version, :number) || "",
96
+ matrix_row.dig(:provider, :version, :number) || ""
97
+ ]
98
+ end
86
99
  end
87
100
  end
88
101
  end
@@ -1,5 +1,5 @@
1
1
  module PactBroker
2
2
  module Client
3
- VERSION = '1.68.0'
3
+ VERSION = '1.70.0'
4
4
  end
5
5
  end
@@ -0,0 +1,43 @@
1
+ require "pact_broker/client/cli/broker"
2
+
3
+ module PactBroker
4
+ module Client
5
+ module CLI
6
+ describe Broker do
7
+ before do
8
+ subject.options = OpenStruct.new(minimum_valid_options)
9
+ allow($stdout).to receive(:puts)
10
+ allow($stderr).to receive(:puts)
11
+ allow(Retry).to receive(:sleep)
12
+
13
+ stub_const("ARGV", %w[--pacticipant Foo --version 1])
14
+ stub_request(:get, "http://pact-broker/matrix?latest=true&latestby=cvp&mainBranch=true&q%5B%5D%5Bpacticipant%5D=Foo&q%5B%5D%5Bversion%5D=1").
15
+ with(
16
+ headers: {
17
+ 'Accept'=>'application/hal+json',
18
+ }).
19
+ to_return(status: 200, body: File.read("spec/support/matrix.json"), headers: { "Content-Type" => "application/hal+json" })
20
+ end
21
+
22
+ let(:minimum_valid_options) do
23
+ {
24
+ broker_base_url: 'http://pact-broker',
25
+ output: 'table',
26
+ verbose: 'verbose',
27
+ retry_while_unknown: 1,
28
+ retry_interval: 2,
29
+ limit: 1000,
30
+ dry_run: false
31
+ }
32
+ end
33
+
34
+ let(:invoke_can_i_merge) { subject.can_i_merge }
35
+
36
+ it "sends a matrix query" do
37
+ expect($stdout).to receive(:puts).with(/Computer says yes/)
38
+ invoke_can_i_merge
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -75,6 +75,12 @@ module PactBroker
75
75
  ],[
76
76
  ["--branch", "main"],
77
77
  [{ pacticipant: nil, branch: "main", latest: true }]
78
+ ],[
79
+ ["--pacticipant", "Foo", "--main-branch", "--pacticipant", "Bar", "--version", "1"],
80
+ [{ pacticipant: "Foo", main_branch: true, latest: true }, { pacticipant: "Bar", version: "1" }]
81
+ ],[
82
+ ["--main-branch"],
83
+ [{ pacticipant: nil, main_branch: true, latest: true }]
78
84
  ]
79
85
  ]
80
86
 
@@ -36,9 +36,18 @@ module PactBroker
36
36
  line_1 = line_creator.call
37
37
  line_2 = line_creator.call
38
38
  line_3 = line_creator.call
39
+
40
+ # ensure the data is as expected
41
+ expect(line_1.dig(:consumer, :version, :number)).to_not be nil
42
+ expect(line_1.dig(:provider, :version, :number)).to_not be nil
43
+
44
+ line_1[:consumer][:version][:number] = "4"
45
+ line_2[:consumer][:version][:number] = "3"
46
+ line_3[:consumer][:version][:number] = "5"
47
+
39
48
  line_2[:verificationResult] = nil
40
49
  line_3[:verificationResult][:success] = false
41
- [line_1, line_2, line_3]
50
+ [line_1, line_2, line_3].shuffle
42
51
  end
43
52
 
44
53
  let(:matrix) { PactBroker::Client::Matrix::Resource.new(matrix: matrix_lines) }
@@ -1,8 +1,8 @@
1
1
  CONSUMER | C.VERSION | PROVIDER | P.VERSION | SUCCESS? | RESULT#
2
2
  ---------|-----------|----------|-----------|----------|--------
3
+ Foo | 3 | Bar | 5 | ??? |
3
4
  Foo | 4 | Bar | 5 | true | 1
4
- Foo | 4 | Bar | 5 | ??? |
5
- Foo | 4 | Bar | 5 | false | 2
5
+ Foo | 5 | Bar | 5 | false | 2
6
6
 
7
7
  VERIFICATION RESULTS
8
8
  --------------------
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pact_broker-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.68.0
4
+ version: 1.70.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Beth Skurrie
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-10 00:00:00.000000000 Z
11
+ date: 2023-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -214,9 +214,11 @@ executables:
214
214
  extensions: []
215
215
  extra_rdoc_files: []
216
216
  files:
217
+ - ".github/dependabot.yml"
217
218
  - ".github/workflows/release_gem.yml"
218
219
  - ".github/workflows/smartbear-issue-label-added.yml"
219
220
  - ".github/workflows/test.yml"
221
+ - ".github/workflows/triage.yml"
220
222
  - ".github/workflows/trigger_pact_docs_update.yml"
221
223
  - ".gitignore"
222
224
  - ".rspec"
@@ -224,6 +226,7 @@ files:
224
226
  - CHANGELOG.md
225
227
  - Dockerfile
226
228
  - Gemfile
229
+ - LICENSE.txt
227
230
  - README.md
228
231
  - RELEASING.md
229
232
  - Rakefile
@@ -353,6 +356,7 @@ files:
353
356
  - spec/fixtures/approvals/list_environments.approved.txt
354
357
  - spec/fixtures/foo-bar.json
355
358
  - spec/integration/can_i_deploy_spec.rb
359
+ - spec/integration/can_i_merge_spec.rb
356
360
  - spec/integration/create_version_tag_spec.rb
357
361
  - spec/integration/describe_environment_spec.rb
358
362
  - spec/lib/pact_broker/client/base_client_spec.rb
@@ -449,7 +453,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
449
453
  - !ruby/object:Gem::Version
450
454
  version: '0'
451
455
  requirements: []
452
- rubygems_version: 3.4.15
456
+ rubygems_version: 3.4.19
453
457
  signing_key:
454
458
  specification_version: 4
455
459
  summary: See description
@@ -462,6 +466,7 @@ test_files:
462
466
  - spec/fixtures/approvals/list_environments.approved.txt
463
467
  - spec/fixtures/foo-bar.json
464
468
  - spec/integration/can_i_deploy_spec.rb
469
+ - spec/integration/can_i_merge_spec.rb
465
470
  - spec/integration/create_version_tag_spec.rb
466
471
  - spec/integration/describe_environment_spec.rb
467
472
  - spec/lib/pact_broker/client/base_client_spec.rb