cfoundry 0.4.21 → 0.5.0

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 (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