cfoundry 0.4.21 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/Rakefile +47 -24
  2. data/lib/cfoundry/auth_token.rb +48 -0
  3. data/lib/cfoundry/baseclient.rb +96 -277
  4. data/lib/cfoundry/client.rb +2 -0
  5. data/lib/cfoundry/concerns/login_helpers.rb +13 -0
  6. data/lib/cfoundry/errors.rb +21 -13
  7. data/lib/cfoundry/rest_client.rb +290 -0
  8. data/lib/cfoundry/test_support.rb +3 -0
  9. data/lib/cfoundry/trace_helpers.rb +9 -9
  10. data/lib/cfoundry/uaaclient.rb +66 -74
  11. data/lib/cfoundry/upload_helpers.rb +2 -0
  12. data/lib/cfoundry/v1/app.rb +2 -2
  13. data/lib/cfoundry/v1/base.rb +4 -51
  14. data/lib/cfoundry/v1/client.rb +7 -30
  15. data/lib/cfoundry/v1/model.rb +22 -5
  16. data/lib/cfoundry/v1/model_magic.rb +30 -15
  17. data/lib/cfoundry/v2/app.rb +2 -5
  18. data/lib/cfoundry/v2/base.rb +10 -74
  19. data/lib/cfoundry/v2/client.rb +19 -30
  20. data/lib/cfoundry/v2/domain.rb +1 -4
  21. data/lib/cfoundry/v2/model.rb +1 -3
  22. data/lib/cfoundry/v2/model_magic.rb +13 -23
  23. data/lib/cfoundry/version.rb +1 -1
  24. data/lib/cfoundry/zip.rb +1 -1
  25. data/spec/cfoundry/auth_token_spec.rb +77 -0
  26. data/spec/cfoundry/baseclient_spec.rb +54 -30
  27. data/spec/cfoundry/errors_spec.rb +10 -13
  28. data/spec/cfoundry/rest_client_spec.rb +238 -0
  29. data/spec/cfoundry/trace_helpers_spec.rb +10 -5
  30. data/spec/cfoundry/uaaclient_spec.rb +141 -114
  31. data/spec/cfoundry/upload_helpers_spec.rb +129 -0
  32. data/spec/cfoundry/v1/base_spec.rb +2 -2
  33. data/spec/cfoundry/v1/client_spec.rb +17 -0
  34. data/spec/cfoundry/v1/model_magic_spec.rb +43 -0
  35. data/spec/cfoundry/v2/base_spec.rb +256 -33
  36. data/spec/cfoundry/v2/client_spec.rb +68 -0
  37. data/spec/cfoundry/v2/model_magic_spec.rb +49 -0
  38. data/spec/fixtures/apps/with_vmcignore/ignored_dir/file_in_ignored_dir.txt +1 -0
  39. data/spec/fixtures/apps/with_vmcignore/ignored_file.txt +1 -0
  40. data/spec/fixtures/apps/with_vmcignore/non_ignored_dir/file_in_non_ignored_dir.txt +1 -0
  41. data/spec/fixtures/apps/with_vmcignore/non_ignored_dir/ignored_file.txt +1 -0
  42. data/spec/fixtures/apps/with_vmcignore/non_ignored_file.txt +1 -0
  43. data/spec/fixtures/empty_file +0 -0
  44. data/spec/spec_helper.rb +4 -4
  45. data/spec/support/randoms.rb +3 -0
  46. data/spec/support/shared_examples/client_login_examples.rb +46 -0
  47. data/spec/support/{summaries.rb → shared_examples/model_summary_examples.rb} +0 -0
  48. data/spec/support/v1_fake_helper.rb +144 -0
  49. metadata +101 -37
  50. data/lib/cfoundry/spec_helper.rb +0 -1
@@ -0,0 +1,129 @@
1
+ require "spec_helper"
2
+
3
+ describe CFoundry::UploadHelpers do
4
+ describe '#upload' do
5
+ let(:base) { Object.new }
6
+ let(:guid) { "123" }
7
+ let(:path) { "#{SPEC_ROOT}/fixtures/apps/with_vmcignore" }
8
+ let(:check_resources) { false }
9
+ let(:tmpdir) { "#{SPEC_ROOT}/tmp/fake_tmpdir" }
10
+
11
+ let(:client) do
12
+ client = Object.new
13
+ stub(client).base { base }
14
+ client
15
+ end
16
+
17
+ let(:fake_model) do
18
+ class FakeModel
19
+ include CFoundry::UploadHelpers
20
+
21
+ def initialize(client, guid)
22
+ @client = client
23
+ @guid = guid
24
+ end
25
+ end
26
+
27
+ FakeModel.new(client, guid)
28
+ end
29
+
30
+ before do
31
+ stub(Dir).tmpdir do
32
+ FileUtils.mkdir_p tmpdir
33
+ tmpdir
34
+ end
35
+ stub(base).upload_app.with_any_args
36
+ end
37
+
38
+ after { FileUtils.rm_rf tmpdir }
39
+
40
+ subject { fake_model.upload(path, check_resources) }
41
+
42
+ def relative_glob(dir)
43
+ base_pathname = Pathname.new(dir)
44
+ Dir["#{dir}/**/{*,.[^\.]*}"].map do |file|
45
+ Pathname.new(file).relative_path_from(base_pathname).to_s
46
+ end
47
+ end
48
+
49
+ def mock_zip(*args, &block)
50
+ if args.empty?
51
+ mock(CFoundry::Zip).pack.with_any_args(&block)
52
+ else
53
+ mock(CFoundry::Zip).pack(*args, &block)
54
+ end
55
+ end
56
+
57
+ it 'zips the app and uploads the zip file' do
58
+ zip_path = "#{tmpdir}/#{guid}.zip"
59
+ mock_zip(anything, zip_path) { true }
60
+ mock(base).upload_app(guid, zip_path, [])
61
+ subject
62
+ end
63
+
64
+ it 'uploads an app with the right guid' do
65
+ mock_zip
66
+ mock(base).upload_app(guid, anything, anything)
67
+ subject
68
+ end
69
+
70
+ it 'uses a unique directory name when it copies the app' do
71
+ mock_zip(/#{tmpdir}.*#{guid}.*/, anything)
72
+ subject
73
+ end
74
+
75
+ it 'cleans up after itself correctly' do
76
+ subject
77
+ expect(relative_glob(tmpdir)).to be_empty
78
+ end
79
+
80
+ it 'includes the source files of the app in the zip file' do
81
+ mock_zip do |src, _|
82
+ files = relative_glob(src)
83
+ expect(files).to include "non_ignored_dir"
84
+ expect(files).to include "non_ignored_file.txt"
85
+ expect(files).to include "non_ignored_dir/file_in_non_ignored_dir.txt"
86
+ end
87
+ subject
88
+ end
89
+
90
+ it 'includes hidden files (though stager ignores them currently)' do
91
+ mock_zip do |src, _|
92
+ expect(relative_glob(src)).to include ".hidden_file"
93
+ end
94
+ subject
95
+ end
96
+
97
+ it 'does not include files and directories specified in the vmcignore (including glob patterns)' do
98
+ mock_zip do |src, _|
99
+ files = relative_glob(src)
100
+ expect(files).not_to include "ignored_dir"
101
+ expect(files).not_to include "ignored_file.txt"
102
+ expect(files).not_to include "non_ignored_dir/ignored_file.txt" # glob pattern **/ignored_file.txt
103
+ end
104
+ subject
105
+ end
106
+
107
+ %w(.git _darcs .svn).each do |source_control_dir_name|
108
+ context "when there is a #{source_control_dir_name} directory in the app" do
109
+ before { FileUtils.mkdir_p("#{path}/#{source_control_dir_name}") }
110
+
111
+ it "ignores that directory" do
112
+ mock_zip do |src, _|
113
+ expect(relative_glob(src)).not_to include source_control_dir_name
114
+ end
115
+ subject
116
+ end
117
+ end
118
+ end
119
+
120
+ context 'when there are no files to zip' do
121
+ before { mock_zip { false } }
122
+
123
+ it 'passes `false` to #upload_app' do
124
+ mock(base).upload_app(guid, false, [])
125
+ subject
126
+ end
127
+ end
128
+ end
129
+ end
@@ -3,9 +3,9 @@ require "spec_helper"
3
3
  describe CFoundry::V1::Base do
4
4
  let(:base) { CFoundry::V1::Base.new("https://api.cloudfoundry.com") }
5
5
 
6
- describe '#request_uri' do
6
+ describe '#get' do
7
7
  let(:options) { {} }
8
- subject { base.request_uri URI.parse(base.target + "/foo"), Net::HTTP::Get, options }
8
+ subject { base.get("foo", options) }
9
9
 
10
10
  context 'when successful' do
11
11
  context 'and the accept type is JSON' do
@@ -0,0 +1,17 @@
1
+ require "spec_helper"
2
+
3
+ describe CFoundry::V1::Client do
4
+ let(:client) { CFoundry::V1::Client.new }
5
+
6
+ describe "#version" do
7
+ its(:version) { should eq 1 }
8
+ end
9
+
10
+ describe "#login" do
11
+ include_examples "client login"
12
+ end
13
+
14
+ describe "#login_prompts" do
15
+ include_examples "client login prompts"
16
+ end
17
+ end
@@ -0,0 +1,43 @@
1
+ require "spec_helper"
2
+
3
+ describe CFoundry::V1::ModelMagic do
4
+ let(:client) { v1_fake_client }
5
+ let(:mymodel) { v1_fake_model }
6
+ let(:guid) { random_string("my-object-guid") }
7
+ let(:myobject) { mymodel.new(guid, client) }
8
+
9
+ describe "#read_manifest" do
10
+ context "with an attribute with different read/write locations" do
11
+ let(:mymodel) do
12
+ v1_fake_model do
13
+ attribute :foo, :string, :read => :x, :write => [:y, :z]
14
+ end
15
+ end
16
+
17
+ before do
18
+ stub(client.base).my_fake_model { { :x => "abc" } }
19
+ end
20
+
21
+ it "reads from the write location" do
22
+ expect {
23
+ myobject.foo = "def"
24
+ }.to change { myobject.read_manifest[:foo] }.from("abc").to("def")
25
+ end
26
+ end
27
+ end
28
+
29
+ describe "#write_manifest" do
30
+ context "with a read-only attribute" do
31
+ let(:mymodel) do
32
+ v1_fake_model do
33
+ attribute :foo, :string, :read_only => true
34
+ end
35
+ end
36
+
37
+ it "does not include the attribute" do
38
+ myobject.fake(:foo => "bar")
39
+ expect(myobject.write_manifest).to_not include :foo
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,58 +1,281 @@
1
1
  require "spec_helper"
2
2
 
3
3
  describe CFoundry::V2::Base do
4
- let(:base) { CFoundry::V2::Base.new("https://api.cloudfoundry.com") }
4
+ let(:target) { "https://api.cloudfoundry.com" }
5
+ let(:base) { CFoundry::V2::Base.new(target) }
5
6
 
6
- describe '#request_uri' do
7
- let(:options) { {} }
8
- subject { base.request_uri URI.parse(base.target + "/foo"), Net::HTTP::Get, options }
7
+ describe "helper methods for HTTP verbs" do
8
+ let(:rest_client) { base.rest_client }
9
+ let(:path) { "some-path" }
10
+ let(:options) { { :some => :option} }
11
+ let(:url) { target + "/" + path }
12
+ let(:args) { [path, options] }
9
13
 
10
- context 'when successful' do
11
- context 'and the accept type is JSON' do
12
- let(:options) { {:accept => :json} }
14
+ shared_examples "handling responses" do |verb|
15
+ context 'when successful' do
16
+ context 'and the accept type is JSON' do
17
+ let(:options) { {:accept => :json} }
13
18
 
14
- it 'returns the parsed JSON' do
15
- stub_request(:get, 'https://api.cloudfoundry.com/foo').to_return :status => 200, :body => "{\"hello\": \"there\"}"
16
- expect(subject).to eq(:hello => "there")
19
+ it 'returns the parsed JSON' do
20
+ stub_request(:any, 'https://api.cloudfoundry.com/some-path').to_return(:status => 200, :body => "{\"hello\": \"there\"}")
21
+ expect(subject).to eq(:hello => "there")
22
+ end
23
+ end
24
+
25
+ context 'and the accept type is not JSON' do
26
+ let(:options) { {:accept => :form} }
27
+
28
+ it 'returns the body' do
29
+ stub_request(:any, 'https://api.cloudfoundry.com/some-path').to_return :status => 200, :body => "body"
30
+ expect(subject).to eq "body"
31
+ end
32
+ end
33
+ end
34
+
35
+ context 'when an error occurs' do
36
+ let(:response_code) { 404 }
37
+
38
+ it 'raises the correct error if JSON is parsed successfully' do
39
+ stub_request(:any, 'https://api.cloudfoundry.com/some-path').to_return(
40
+ :status => response_code,
41
+ :body => "{\"code\": 111, \"description\": \"Something bad happened\"}"
42
+ )
43
+ expect {subject}.to raise_error(CFoundry::SystemError, "111: Something bad happened")
44
+ end
45
+
46
+ it 'raises the correct error if code is missing from response' do
47
+ stub_request(:any, 'https://api.cloudfoundry.com/some-path').to_return(
48
+ :status => response_code,
49
+ :body => "{\"description\": \"Something bad happened\"}"
50
+ )
51
+ expect {subject}.to raise_error CFoundry::NotFound
52
+ end
53
+
54
+ it 'raises the correct error if response body is not JSON' do
55
+ stub_request(:any, 'https://api.cloudfoundry.com/some-path').to_return(
56
+ :status => response_code,
57
+ :body => "Error happened"
58
+ )
59
+ expect {subject}.to raise_error CFoundry::NotFound
60
+ end
61
+
62
+ it 'raises a generic APIError if code is not recognized' do
63
+ stub_request(:any, 'https://api.cloudfoundry.com/some-path').to_return :status => response_code,
64
+ :body => "{\"code\": 6932, \"description\": \"Something bad happened\"}"
65
+ expect {subject}.to raise_error CFoundry::APIError, "6932: Something bad happened"
66
+ end
67
+
68
+ context 'when a timeout exception occurs' do
69
+ before { stub_request(:any, url).to_raise(::Timeout::Error) }
70
+
71
+ it 'raises the correct error' do
72
+ expect { subject }.to raise_error CFoundry::Timeout, /#{url} timed out/
73
+ end
74
+ end
75
+
76
+ context 'when an HTTPNotFound error occurs' do
77
+ before { stub_request(:any, url).to_return(:status => 404, :body => "NOT FOUND") }
78
+
79
+ it 'raises the correct error' do
80
+ expect {subject}.to raise_error CFoundry::NotFound, "404: NOT FOUND"
81
+ end
82
+ end
83
+
84
+ context 'when an HTTPForbidden error occurs' do
85
+ before { stub_request(:any, url).to_return(:status => 403, :body => "NONE SHALL PASS") }
86
+
87
+ it 'raises the correct error' do
88
+ expect { subject }.to raise_error CFoundry::Denied, "403: NONE SHALL PASS"
89
+ end
90
+ end
91
+
92
+ context "when any other type of error occurs" do
93
+ before { stub_request(:any, url).to_return(:status => 411, :body => "NOT LONG ENOUGH") }
94
+
95
+ it 'raises the correct error' do
96
+ expect { subject }.to raise_error CFoundry::BadResponse, "411: NOT LONG ENOUGH"
97
+ end
98
+ end
99
+
100
+ it 'includes the request and response hashes when it raises errors' do
101
+ stub_request(:any, url).to_return(:status => 411, :body => "NOT LONG ENOUGH")
102
+
103
+ begin
104
+ subject
105
+ rescue CFoundry::BadResponse => e
106
+ expect(e.response).to eq({
107
+ :status => "411",
108
+ :headers => {},
109
+ :body => "NOT LONG ENOUGH"
110
+ })
111
+ expect(e.request).to eq({
112
+ :headers => { "Content-Length" => 0 },
113
+ :method => verb,
114
+ :body => nil,
115
+ :url => "https://api.cloudfoundry.com/some-path"
116
+ })
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ shared_examples "normalizing arguments" do |verb|
123
+ context "when multiple path segments are passed" do
124
+ let(:segments) { ["first-segment", "next-segment"] }
125
+
126
+ context "when an options hash is not supplied" do
127
+ let(:args) { segments }
128
+
129
+ it "makes a request with the correct url and options" do
130
+ mock(rest_client).request(verb, "first-segment/next-segment", {}) { [request, response] }
131
+ subject
132
+ end
133
+ end
134
+
135
+ context "when an options has is supplied" do
136
+ let(:args) { segments + [options] }
137
+
138
+ it "makes a request with the correct url and options" do
139
+ mock(rest_client).request(verb, "first-segment/next-segment", options) { [request, response] }
140
+ subject
141
+ end
17
142
  end
18
143
  end
19
144
 
20
- context 'and the accept type is not JSON' do
21
- let(:options) { {:accept => :form} }
145
+ context "when a single path segment is passed" do
146
+ context "when an options hash is not supplied" do
147
+ let(:args) { ["first-segment"] }
22
148
 
23
- it 'returns the body' do
24
- stub_request(:get, 'https://api.cloudfoundry.com/foo').to_return :status => 200, :body => "body"
25
- expect(subject).to eq "body"
149
+ it "makes a request with the correct url and options" do
150
+ mock(rest_client).request(verb, "first-segment", {}) { [request, response] }
151
+ subject
152
+ end
153
+ end
154
+
155
+ context "when an options has is supplied" do
156
+ let(:args) { ["first-segment", options] }
157
+
158
+ it "makes a request with the correct url and options" do
159
+ mock(rest_client).request(verb, "first-segment", options) { [request, response] }
160
+ subject
161
+ end
26
162
  end
27
163
  end
164
+ end
28
165
 
166
+ let(:response) { { :status => "201", :headers => { "some-header-key" => "some-header-value" }, :body => "some-body" } }
167
+ let(:request) do
168
+ {
169
+ :method => "GET",
170
+ :url => "http://api.cloudfoundry.com/some-path",
171
+ :headers => { "some-header-key" => "some-header-value" },
172
+ :body => "some-body"
173
+ }
29
174
  end
30
175
 
31
- context 'when an error occurs' do
32
- let(:response_code) { 404 }
176
+ describe "#get" do
177
+ subject { base.get(*args) }
33
178
 
34
- it 'raises the correct error if JSON is parsed successfully' do
35
- stub_request(:get, 'https://api.cloudfoundry.com/foo').to_return :status => response_code,
36
- :body => "{\"code\": 111, \"description\": \"Something bad happened\"}"
37
- expect {subject}.to raise_error CFoundry::SystemError, "111: Something bad happened"
179
+ it "makes a GET request" do
180
+ mock(rest_client).request("GET", "some-path", options) { [request, response] }
181
+ subject
38
182
  end
39
183
 
40
- it 'raises the correct error if code is missing from response' do
41
- stub_request(:get, 'https://api.cloudfoundry.com/foo').to_return :status => response_code,
42
- :body => "{\"description\": \"Something bad happened\"}"
43
- expect {subject}.to raise_error CFoundry::NotFound
184
+ include_examples "handling responses", "GET"
185
+ include_examples "normalizing arguments", "GET"
186
+ end
187
+
188
+ describe "#post" do
189
+ subject { base.post(*args) }
190
+
191
+ it "makes a POST request" do
192
+ mock(rest_client).request("POST", "some-path", options) { [request, response] }
193
+ subject
194
+ end
195
+
196
+ include_examples "handling responses", "POST"
197
+ include_examples "normalizing arguments", "POST"
198
+ end
199
+
200
+ describe "#put" do
201
+ subject { base.put(*args) }
202
+
203
+ it "makes a PUT request" do
204
+ mock(rest_client).request("PUT", "some-path", options) { [request, response] }
205
+ subject
44
206
  end
45
207
 
46
- it 'raises the correct error if response body is not JSON' do
47
- stub_request(:get, 'https://api.cloudfoundry.com/foo').to_return :status => response_code,
48
- :body => "Error happened"
49
- expect {subject}.to raise_error CFoundry::NotFound
208
+ include_examples "handling responses", "PUT"
209
+ include_examples "normalizing arguments", "PUT"
210
+ end
211
+
212
+ describe "#delete" do
213
+ subject { base.delete(*args) }
214
+
215
+ it "makes a DELETE request" do
216
+ mock(rest_client).request("DELETE", "some-path", options) { [request, response] }
217
+ subject
50
218
  end
51
219
 
52
- it 'raises a generic APIError if code is not recognized' do
53
- stub_request(:get, 'https://api.cloudfoundry.com/foo').to_return :status => response_code,
54
- :body => "{\"code\": 6932, \"description\": \"Something bad happened\"}"
55
- expect {subject}.to raise_error CFoundry::APIError, "6932: Something bad happened"
220
+ include_examples "handling responses", "DELETE"
221
+ include_examples "normalizing arguments", "DELETE"
222
+ end
223
+ end
224
+
225
+ describe "#resource_match" do
226
+ let(:fingerprints) { "some-fingerprints" }
227
+
228
+ it "makes a PUT request to the resource_match endpoint with the correct payload" do
229
+ stub = stub_request(:put, "https://api.cloudfoundry.com/v2/resource_match").
230
+ with(:body => fingerprints).
231
+ to_return(:body => "{}")
232
+ base.resource_match(fingerprints)
233
+ expect(stub).to have_been_requested
234
+ end
235
+ end
236
+
237
+ describe "#upload_app" do
238
+ let(:guid) { "some-guid" }
239
+ let(:bits) { "some-bits" }
240
+ let(:fake_zipfile) { File.new("#{SPEC_ROOT}/fixtures/empty_file") }
241
+
242
+ it "makes a PUT request to the app bits endpoint with the correct payload" do
243
+ stub = stub_request(:put, "https://api.cloudfoundry.com/v2/apps/#{guid}/bits").to_return(:body => "{}")
244
+ base.upload_app(guid, fake_zipfile)
245
+ expect(stub).to have_been_requested
246
+ end
247
+ end
248
+
249
+ describe "#stream_file" do
250
+ let(:app_guid) { "1234" }
251
+ let(:instance_guid) { "3456" }
252
+ let(:api_url) { "https://api.cloudfoundry.com/v2/apps/#{app_guid}/instances/#{instance_guid}/files/some/path/segments" }
253
+ let(:file_url) { "http://api.cloudfoundry.com/static/path/to/some/file" }
254
+
255
+ before do
256
+ stub(base).token { CFoundry::AuthToken.new("bearer foo") }
257
+ end
258
+
259
+ it "follows the redirect returned by the files endpoint" do
260
+ stub_request(:get, api_url).to_return(
261
+ :status => 301,
262
+ :headers => { "location" => file_url },
263
+ :body => ""
264
+ )
265
+
266
+ request =
267
+ stub_request(
268
+ :get, file_url + "&tail"
269
+ ).with(
270
+ :headers => { "Accept" => "*/*", "Authorization" => "bearer foo" }
271
+ ).to_return(
272
+ :status => 200,
273
+ :body => "some body chunks"
274
+ )
275
+
276
+ base.stream_file(app_guid, instance_guid, "some", "path", "segments") do |body|
277
+ expect(request).to have_been_made
278
+ expect(body).to eql("some body chunks")
56
279
  end
57
280
  end
58
281
  end