cf-uaa-lib 1.3.1 → 1.3.2

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.
data/lib/uaa/util.rb CHANGED
@@ -16,13 +16,13 @@ require "base64"
16
16
  require 'logger'
17
17
  require 'uri'
18
18
 
19
- # :nodoc:
19
+ # Cloud Foundry namespace
20
20
  module CF
21
- # Namespace for Cloudfoundry User Account and Authentication service Ruby APIs
21
+ # Namespace for User Account and Authentication service
22
22
  module UAA end
23
23
  end
24
24
 
25
- class Logger # :nodoc:
25
+ class Logger # @private
26
26
  Severity::TRACE = Severity::DEBUG - 1
27
27
  def trace(progname, &blk); add(Logger::Severity::TRACE, nil, progname, &blk) end
28
28
  def trace? ; @level <= Logger::Severity::TRACE end
@@ -33,49 +33,44 @@ module CF::UAA
33
33
  # Useful parent class. All CF::UAA exceptions are derived from this.
34
34
  class UAAError < RuntimeError; end
35
35
 
36
- # Indicates an authentication error
36
+ # Indicates an authentication error.
37
37
  class AuthError < UAAError; end
38
38
 
39
- # Indicates an error occurred decoding a token, base64 decoding, or JSON
39
+ # Indicates an error occurred decoding a token, base64 decoding, or JSON.
40
40
  class DecodeError < UAAError; end
41
41
 
42
- # Low level helper functions useful to the UAA client APIs
42
+ # Helper functions useful to the UAA client APIs
43
43
  class Util
44
44
 
45
- # HTTP headers and various protocol tags tend to contain '-' characters,
46
- # are intended to be case-insensitive, and often end up as keys in ruby
47
- # hashes. SCIM[http://www.simplecloud.info/] specifies that attribute
48
- # names are case-insensitive and this code downcases such strings using
49
- # this method.
45
+ # General method to transform a hash key to a given style. Useful when
46
+ # dealing with HTTP headers and various protocol tags that tend to contain
47
+ # '-' characters and are case-insensitive and want to use them as keys in
48
+ # ruby hashes. Useful for dealing with {http://www.simplecloud.info/ SCIM}
49
+ # case-insensitive attribute names to normalize all attribute names (downcase).
50
50
  #
51
- # The various +styles+ convert +key+ as follows:
52
- # [+:undash+] to lowercase, '-' to '_', and to a symbol
53
- # [+:todash+] to string, '_' to '-'
54
- # [+:uncamel+] uppercase to underscore-lowercase, to symbol
55
- # [+:tocamel+] reverse of +uncamel+
56
- # [+:tosym+] to symbol
57
- # [+:tostr+] to string
58
- # [+:down+] to lowercase
59
- # [+:none+] leave the damn key alone
60
- #
61
- # returns new key
62
- def self.hash_key(k, style)
51
+ # @param [String, Symbol] key current key
52
+ # @param [Symbol] style can be sym, downsym, down, str, [un]dash, [un]camel, nil, none
53
+ # @return [String, Symbol] new key
54
+ def self.hash_key(key, style)
63
55
  case style
64
- when :undash then k.to_s.downcase.tr('-', '_').to_sym
65
- when :todash then k.to_s.downcase.tr('_', '-')
66
- when :uncamel then k.to_s.gsub(/([A-Z])([^A-Z]*)/,'_\1\2').downcase.to_sym
67
- when :tocamel then k.to_s.gsub(/(_[a-z])([^_]*)/) { $1[1].upcase + $2 }
68
- when :tosym then k.to_sym
69
- when :tostr then k.to_s
70
- when :down then k.to_s.downcase
71
- when :none then k
56
+ when nil, :none then key
57
+ when :downsym then key.to_s.downcase.to_sym
58
+ when :sym then key.to_sym
59
+ when :str then key.to_s
60
+ when :down then key.to_s.downcase
61
+ when :dash then key.to_s.downcase.tr('_', '-')
62
+ when :undash then key.to_s.downcase.tr('-', '_').to_sym
63
+ when :uncamel then key.to_s.gsub(/([A-Z])([^A-Z]*)/,'_\1\2').downcase.to_sym
64
+ when :camel then key.to_s.gsub(/(_[a-z])([^_]*)/) { $1[1].upcase + $2 }
72
65
  else raise ArgumentError, "unknown hash key style: #{style}"
73
66
  end
74
67
  end
75
68
 
76
- # Modifies obj in place changing any hash keys to style (see hash_key).
77
- # Recursively modifies subordinate hashes. Returns modified obj
78
- def self.hash_keys!(obj, style = :none)
69
+ # Modifies obj in place changing any hash keys to style. Recursively modifies
70
+ # subordinate hashes.
71
+ # @param style (see Util.hash_key)
72
+ # @return modified obj
73
+ def self.hash_keys!(obj, style = nil)
79
74
  return obj if style == :none
80
75
  return obj.each {|o| hash_keys!(o, style)} if obj.is_a? Array
81
76
  return obj unless obj.is_a? Hash
@@ -88,9 +83,11 @@ class Util
88
83
  obj.merge!(newkeys)
89
84
  end
90
85
 
91
- # Makes a new copy of obj with hash keys to style (see hash_key).
92
- # Recursively modifies subordinate hashes. Returns modified obj
93
- def self.hash_keys(obj, style = :none)
86
+ # Makes a new copy of obj with hash keys to style. Recursively modifies
87
+ # subordinate hashes.
88
+ # @param style (see Util.hash_key)
89
+ # @return obj or new object if hash keys were changed
90
+ def self.hash_keys(obj, style = nil)
94
91
  return obj.collect {|o| hash_keys(o, style)} if obj.is_a? Array
95
92
  return obj unless obj.is_a? Hash
96
93
  obj.each_with_object({}) {|(k, v), h|
@@ -98,64 +95,112 @@ class Util
98
95
  }
99
96
  end
100
97
 
101
- # Takes an x-www-form-urlencoded string and returns a hash of key value pairs.
102
- # Useful for OAuth parameters. It raises an ArgumentError if a key occurs
98
+ # handle ruby 1.8.7 compatibility for form encoding
99
+ if URI.respond_to?(:encode_www_form_component)
100
+ def self.encode_component(str) URI.encode_www_form_component(str) end #@private
101
+ def self.decode_component(str) URI.decode_www_form_component(str) end #@private
102
+ else
103
+ def self.encode_component(str) # @private
104
+ str.to_s.gsub(/([^ a-zA-Z0-9*_.-]+)/) {
105
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
106
+ }.tr(' ', '+')
107
+ end
108
+ def self.decode_component(str) # @private
109
+ str.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/) {[$1.delete('%')].pack('H*')}
110
+ end
111
+ end
112
+
113
+ # Takes an x-www-form-urlencoded string and returns a hash of utf-8 key/value
114
+ # pairs. Useful for OAuth parameters. Raises ArgumentError if a key occurs
103
115
  # more than once, which is a restriction of OAuth query strings.
104
- # OAuth parameters are case sensitive, scim parameters are case-insensitive
105
- # See ietf rfc 6749 section 3.1.
106
- def self.decode_form_to_hash(url_encoded_pairs, style = :none)
107
- URI.decode_www_form(url_encoded_pairs).each_with_object({}) do |p, o|
108
- k = hash_key(p[0], style)
109
- raise ArgumentError, "duplicate keys in form parameters" if o[k]
110
- o[k] = p[1]
116
+ # OAuth parameters are case sensitive, scim parameters are case-insensitive.
117
+ # @see http://tools.ietf.org/html/rfc6749#section-3.1
118
+ # @param [String] url_encoded_pairs in an x-www-form-urlencoded string
119
+ # @param style (see Util.hash_key)
120
+ # @return [Hash] of key value pairs
121
+ def self.decode_form(url_encoded_pairs, style = nil)
122
+ pairs = {}
123
+ url_encoded_pairs.split(/[&;]/).each do |pair|
124
+ k, v = pair.split('=', 2).collect { |v| decode_component(v) }
125
+ raise "duplicate keys in form parameters" if pairs.key?(k = hash_key(k, style))
126
+ pairs[k] = v
111
127
  end
128
+ pairs
112
129
  rescue Exception => e
113
130
  raise ArgumentError, e.message
114
131
  end
115
132
 
133
+ # Encode an object into x-www-form-urlencoded string suitable for oauth2.
134
+ # @note that ruby 1.9.3 converts form components to utf-8. Ruby 1.8.7
135
+ # users must ensure all data is in utf-8 format before passing to form encode.
136
+ # @param [Hash] obj a hash of key/value pairs to be encoded.
137
+ # @see http://tools.ietf.org/html/rfc6749#section-3.1
138
+ def self.encode_form(obj)
139
+ obj.map {|k, v| encode_component(k) << '=' << encode_component(v)}.join('&')
140
+ end
141
+
116
142
  # Converts +obj+ to JSON
143
+ # @return [String] obj in JSON form.
117
144
  def self.json(obj) MultiJson.dump(obj) end
118
145
 
119
146
  # Converts +obj+ to nicely formatted JSON
120
- def self.json_pretty(obj) MultiJson.dump(obj, pretty: true) end
147
+ # @return [String] obj in formatted json
148
+ def self.json_pretty(obj) MultiJson.dump(obj, :pretty => true) end
121
149
 
122
150
  # Converts +obj+ to a URL-safe base 64 encoded string
151
+ # @return [String]
123
152
  def self.json_encode64(obj = {}) encode64(json(obj)) end
124
153
 
125
- # Converts +str+ from base64 encoding of a JSON string to a (returned) hash.
126
- def self.json_decode64(str) json_parse(decode64(str)) end
127
-
128
- # encodes +obj+ as a URL-safe base 64 encoded string, with trailing padding removed.
129
- def self.encode64(obj) Base64::urlsafe_encode64(obj).gsub(/=*$/, '') end
154
+ # Decodes base64 encoding of JSON data.
155
+ # @param [String] str
156
+ # @param style (see Util.hash_key)
157
+ # @return [Hash]
158
+ def self.json_decode64(str, style = nil) json_parse(decode64(str), style) end
159
+
160
+ # Encodes +obj+ as a URL-safe base 64 encoded string, with trailing padding removed.
161
+ # @return [String]
162
+ def self.encode64(obj)
163
+ str = Base64.respond_to?(:urlsafe_encode64)? Base64.urlsafe_encode64(obj):
164
+ [obj].pack("m").tr("+/", "-_")
165
+ str.gsub!(/(\n|=*$)/, '')
166
+ str
167
+ end
130
168
 
131
- # adds proper padding to a URL-safe base 64 encoded string, and then returns the decoded string.
169
+ # Decodes a URL-safe base 64 encoded string. Adds padding if necessary.
170
+ # @return [String] decoded string
132
171
  def self.decode64(str)
133
172
  return unless str
134
173
  pad = str.length % 4
135
- str << '=' * (4 - pad) if pad > 0
136
- Base64::urlsafe_decode64(str)
174
+ str = str + '=' * (4 - pad) if pad > 0
175
+ Base64.respond_to?(:urlsafe_decode64) ?
176
+ Base64.urlsafe_decode64(str) : Base64.decode64(str.tr('-_', '+/'))
137
177
  rescue ArgumentError
138
178
  raise DecodeError, "invalid base64 encoding"
139
179
  end
140
180
 
141
- # Parses a JSON string into the returned hash. For possible values of +style+
142
- # see #hask_key
143
- def self.json_parse(str, style = :none)
181
+ # Parses a JSON string.
182
+ # @param style (see Util.hash_key)
183
+ # @return [Hash] parsed data
184
+ def self.json_parse(str, style = nil)
144
185
  hash_keys!(MultiJson.load(str), style) if str && !str.empty?
145
186
  rescue MultiJson::DecodeError
146
187
  raise DecodeError, "json decoding error"
147
188
  end
148
189
 
149
- def self.truncate(obj, limit = 50) # :nodoc:
190
+ # Converts obj to a string and truncates if over limit.
191
+ # @return [String]
192
+ def self.truncate(obj, limit = 50)
150
193
  return obj.to_s if limit == 0
151
194
  limit = limit < 5 ? 1 : limit - 4
152
195
  str = obj.to_s[0..limit]
153
196
  str.length > limit ? str + '...': str
154
197
  end
155
198
 
199
+ # Converts common input formats into array of strings.
156
200
  # Many parameters in these classes can be given as arrays, or as a list of
157
201
  # arguments separated by spaces or commas. This method handles the possible
158
- # inputs and returns an array of arguments.
202
+ # inputs and returns an array of strings.
203
+ # @return [Array<String>]
159
204
  def self.arglist(arg, default_arg = nil)
160
205
  arg = default_arg unless arg
161
206
  return arg if arg.nil? || arg.respond_to?(:join)
@@ -163,19 +208,25 @@ class Util
163
208
  arg.split(/[\s\,]+/).reject { |e| e.empty? }
164
209
  end
165
210
 
166
- # Reverse of arglist, puts arrays of strings into a single, space-delimited string
211
+ # Joins arrays of strings into a single string. Reverse of {Util.arglist}.
212
+ # @param [Object, #join] arg
213
+ # @param [String] delim delimiter to put between strings.
214
+ # @return [String]
167
215
  def self.strlist(arg, delim = ' ')
168
216
  arg.respond_to?(:join) ? arg.join(delim) : arg.to_s if arg
169
217
  end
170
218
 
171
219
  # Set the default logger used by the higher level classes.
220
+ # @param [String, Symbol] level such as info, debug trace.
221
+ # @param [IO] sink output for log messages, defaults to $stdout
222
+ # @return [Logger]
172
223
  def self.default_logger(level = nil, sink = nil)
173
224
  if sink || !@default_logger
174
225
  @default_logger = Logger.new(sink || $stdout)
175
226
  level = :info unless level
176
227
  @default_logger.formatter = Proc.new { |severity, time, pname, msg| puts msg }
177
228
  end
178
- @default_logger.level = Logger::Severity.const_get(level.upcase) if level
229
+ @default_logger.level = Logger::Severity.const_get(level.to_s.upcase) if level
179
230
  @default_logger
180
231
  end
181
232
 
data/lib/uaa/version.rb CHANGED
@@ -11,8 +11,9 @@
11
11
  # subcomponent's license, as noted in the LICENSE file.
12
12
  #++
13
13
 
14
+ # Cloud Foundry namespace
14
15
  module CF
15
16
  module UAA
16
- VERSION = "1.3.1"
17
+ VERSION = "1.3.2"
17
18
  end
18
19
  end
data/spec/http_spec.rb CHANGED
@@ -23,7 +23,7 @@ describe Http do
23
23
  include SpecHelper
24
24
 
25
25
  it "sets a request handler" do
26
- set_request_handler do |req|
26
+ set_request_handler do |url, method, body, headers|
27
27
  [200, "body", {"content-type" => "text/plain"}]
28
28
  end
29
29
  status, body, resp_headers = http_get("http://example.com")
@@ -0,0 +1,149 @@
1
+ #--
2
+ # Cloud Foundry 2012.02.03 Beta
3
+ # Copyright (c) [2009-2012] VMware, Inc. All Rights Reserved.
4
+ #
5
+ # This product is licensed to you under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this product except in compliance with the License.
7
+ #
8
+ # This product includes a number of subcomponents with
9
+ # separate copyright notices and license terms. Your use of these
10
+ # subcomponents is subject to the terms and conditions of the
11
+ # subcomponent's license, as noted in the LICENSE file.
12
+ #++
13
+
14
+ require 'spec_helper'
15
+ require 'uaa'
16
+ require 'pp'
17
+
18
+ # Example config for integration tests with defaults:
19
+ # ENV["UAA_CLIENT_ID"] = "admin"
20
+ # ENV["UAA_CLIENT_SECRET"] = "adminsecret"
21
+ # ENV["UAA_CLIENT_TARGET"] = "http://localhost:8080/uaa"
22
+
23
+ module CF::UAA
24
+
25
+ if ENV["UAA_CLIENT_TARGET"]
26
+
27
+ describe "UAA Integration:" do
28
+
29
+ def create_test_client
30
+ toki = TokenIssuer.new(@target, @admin_client, @admin_secret)
31
+ cr = Scim.new(@target, toki.client_credentials_grant.auth_header, :symbolize_keys => true)
32
+ @test_client = "test_client_#{Time.now.to_i}"
33
+ @test_secret = "+=tEsTsEcRet~!@"
34
+ gids = ["clients.read", "scim.read", "scim.write", "uaa.resource", "password.write"]
35
+ new_client = cr.add(:client, :client_id => @test_client, :client_secret => @test_secret,
36
+ :authorities => gids, :authorized_grant_types => ["client_credentials", "password"],
37
+ :scope => ["openid", "password.write"])
38
+ new_client[:client_id].should == @test_client
39
+ @username = "sam_#{Time.now.to_i}"
40
+ end
41
+
42
+ before :all do
43
+ #Util.default_logger(:trace)
44
+ @admin_client = ENV["UAA_CLIENT_ID"] || "admin"
45
+ @admin_secret = ENV["UAA_CLIENT_SECRET"] || "adminsecret"
46
+ @target = ENV["UAA_CLIENT_TARGET"]
47
+ @username = "sam_#{Time.now.to_i}"
48
+ end
49
+
50
+ it "should report the uaa client version" do
51
+ VERSION.should =~ /\d.\d.\d/
52
+ end
53
+
54
+ it "makes sure the server is there by getting the prompts for an implicit grant" do
55
+ prompts = TokenIssuer.new(@target, @admin_client, @admin_secret).prompts
56
+ prompts.should_not be_nil
57
+ end
58
+
59
+ it "gets a token with client credentials" do
60
+ tkn = TokenIssuer.new(@target, @admin_client, @admin_secret).client_credentials_grant
61
+ tkn.auth_header.should =~ /^bearer\s/i
62
+ info = TokenCoder.decode(tkn.info["access_token"], :verify => false, :symbolize_keys => true)
63
+ info[:exp].should be
64
+ info[:jti].should be
65
+ end
66
+
67
+ context "as a client," do
68
+
69
+ before :all do
70
+ create_test_client
71
+ toki = TokenIssuer.new(@target, @test_client, @test_secret)
72
+ @scim = Scim.new(@target, toki.client_credentials_grant.auth_header, :symbolize_keys => true)
73
+ @user_pwd = "sam's P@55w0rd~!`@\#\$%^&*()_/{}[]\\|:\";',.<>?/"
74
+ usr = @scim.add(:user, :username => @username, :password => @user_pwd,
75
+ :emails => [{:value => "sam@example.com"}],
76
+ :name => {:givenname => "none", :familyname => "none"})
77
+ @user_id = usr[:id]
78
+ end
79
+
80
+ after :all do
81
+ # TODO: delete user, delete test client
82
+ end
83
+
84
+ it "creates a user" do
85
+ @user_id.should be
86
+ end
87
+
88
+ it "finds the user by name" do
89
+ @scim.id(:user, @username).should == @user_id
90
+ end
91
+
92
+ it "gets the user by id" do
93
+ user_info = @scim.get(:user, @user_id)
94
+ user_info[:id].should == @user_id
95
+ user_info[:username].should == @username
96
+ end
97
+
98
+ it "gets a user token by an implicit grant" do
99
+ @toki = TokenIssuer.new(@target, "vmc")
100
+ token = @toki.implicit_grant_with_creds(:username => @username, :password => @user_pwd)
101
+ token.info["access_token"].should be
102
+ info = Misc.whoami(@target, token.auth_header)
103
+ info["user_name"].should == @username
104
+ contents = TokenCoder.decode(token.info["access_token"], :verify => false)
105
+ contents["user_name"].should == @username
106
+ end
107
+
108
+ it "changes the user's password by name" do
109
+ @scim.change_password(@scim.id(:user, @username), "newpassword")[:status].should == "ok"
110
+ end
111
+
112
+ it "lists all users" do
113
+ user_info = @scim.query(:user)
114
+ user_info.should_not be_nil
115
+ end
116
+
117
+ if ENV["UAA_CLIENT_LOGIN"]
118
+ it "should get a uri to be sent to the user agent to initiate autologin" do
119
+ logn = ENV["UAA_CLIENT_LOGIN"]
120
+ toki = TokenIssuer.new(logn, @test_client, @test_secret)
121
+ redir_uri = "http://call.back/uri_path"
122
+ uri_parts = toki.autologin_uri(redir_uri, :username => @username,
123
+ :password => "newpassword").split('?')
124
+ uri_parts[0].should == "#{logn}/oauth/authorize"
125
+ params = Util.decode_form(uri_parts[1], :sym)
126
+ params[:response_type].should == "code"
127
+ params[:client_id].should == @client_id
128
+ params[:scope].should be_nil
129
+ params[:redirect_uri].should == redir_uri
130
+ params[:state].should_not be_nil
131
+ params[:code].should_not be_nil
132
+ end
133
+ end
134
+
135
+ it "deletes the user" do
136
+ @scim.delete(:user, @user_id)
137
+ expect { @scim.id(:user, @username) }.to raise_exception(NotFound)
138
+ expect { @scim.get(:user, @user_id) }.to raise_exception(NotFound)
139
+ end
140
+
141
+ it "complains about an attempt to delete a non-existent user" do
142
+ expect { @scim.delete(:user, "non-existent-user") }.to raise_exception(NotFound)
143
+ end
144
+
145
+ end
146
+
147
+ end end
148
+
149
+ end
data/spec/scim_spec.rb CHANGED
@@ -43,14 +43,14 @@ describe Scim do
43
43
  check_headers(headers, :json, :json)
44
44
  [200, '{"ID":"id12345"}', {"content-type" => "application/json"}]
45
45
  end
46
- result = subject.add(:user, hair: "brown", shoe_size: "large",
47
- eye_color: ["blue", "green"], name: "fred")
46
+ result = subject.add(:user, :hair => "brown", :shoe_size => "large",
47
+ :eye_color => ["blue", "green"], :name => "fred")
48
48
  result["id"].should == "id12345"
49
49
  end
50
50
 
51
51
  it "replaces an object" do
52
- obj = {hair: "black", shoe_size: "medium", eye_color: ["hazel", "brown"],
53
- name: "fredrick", meta: {version: 'v567'}, id: "id12345"}
52
+ obj = {:hair => "black", :shoe_size => "medium", :eye_color => ["hazel", "brown"],
53
+ :name => "fredrick", :meta => {:version => 'v567'}, :id => "id12345"}
54
54
  subject.set_request_handler do |url, method, body, headers|
55
55
  url.should == "#{@target}/Users/id12345"
56
56
  method.should == :put
@@ -75,17 +75,18 @@ describe Scim do
75
75
 
76
76
  it "pages through all objects" do
77
77
  subject.set_request_handler do |url, method, body, headers|
78
- url.should =~ %r{^#{@target}/Users\?attributes=id&startIndex=[12]$}
78
+ url.should =~ %r{^#{@target}/Users\?}
79
+ url.should =~ %r{[\?&]attributes=id(&|$)}
80
+ url.should =~ %r{[\?&]startIndex=[12](&|$)}
79
81
  method.should == :get
80
82
  check_headers(headers, nil, :json)
81
- reply = url =~ /1$/ ?
83
+ reply = url =~ /startIndex=1/ ?
82
84
  '{"TotalResults":2,"ItemsPerPage":1,"StartIndex":1,"RESOURCES":[{"id":"id12345"}]}' :
83
85
  '{"TotalResults":2,"ItemsPerPage":1,"StartIndex":2,"RESOURCES":[{"id":"id67890"}]}'
84
86
  [200, reply, {"content-type" => "application/json"}]
85
87
  end
86
- result = subject.all_pages(:user, attributes: 'id')
87
- result[0]['id'].should == "id12345"
88
- result[1]['id'].should == "id67890"
88
+ result = subject.all_pages(:user, :attributes => 'id')
89
+ [result[0]['id'], result[1]['id']].to_set.should == ["id12345", "id67890"].to_set
89
90
  end
90
91
 
91
92
  it "changes a user's password" do
@@ -93,7 +94,7 @@ describe Scim do
93
94
  url.should == "#{@target}/Users/id12345/password"
94
95
  method.should == :put
95
96
  check_headers(headers, :json, :json)
96
- body.should == '{"password":"newpwd","oldPassword":"oldpwd"}'
97
+ body.should include('"password":"newpwd"', '"oldPassword":"oldpwd"')
97
98
  [200, '{"id":"id12345"}', {"content-type" => "application/json"}]
98
99
  end
99
100
  result = subject.change_password("id12345", "newpwd", "oldpwd")
@@ -105,7 +106,7 @@ describe Scim do
105
106
  url.should == "#{@target}/oauth/clients/id12345/secret"
106
107
  method.should == :put
107
108
  check_headers(headers, :json, :json)
108
- body.should == '{"secret":"newpwd","oldSecret":"oldpwd"}'
109
+ body.should include('"secret":"newpwd"', '"oldSecret":"oldpwd"')
109
110
  [200, '{"id":"id12345"}', {"content-type" => "application/json"}]
110
111
  end
111
112
  result = subject.change_secret("id12345", "newpwd", "oldpwd")