stacker_bee 1.0.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 (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +7 -0
  5. data/.travis.yml +9 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +112 -0
  9. data/Rakefile +6 -0
  10. data/bin/stacker_bee +97 -0
  11. data/config.default.yml +3 -0
  12. data/config/4.2.json +67126 -0
  13. data/lib/stacker_bee.rb +16 -0
  14. data/lib/stacker_bee/api.rb +44 -0
  15. data/lib/stacker_bee/body_parser.rb +23 -0
  16. data/lib/stacker_bee/client.rb +105 -0
  17. data/lib/stacker_bee/configuration.rb +6 -0
  18. data/lib/stacker_bee/connection.rb +31 -0
  19. data/lib/stacker_bee/middleware/logger.rb +48 -0
  20. data/lib/stacker_bee/middleware/signed_query.rb +29 -0
  21. data/lib/stacker_bee/rash.rb +95 -0
  22. data/lib/stacker_bee/request.rb +41 -0
  23. data/lib/stacker_bee/request_error.rb +39 -0
  24. data/lib/stacker_bee/response.rb +29 -0
  25. data/lib/stacker_bee/utilities.rb +18 -0
  26. data/lib/stacker_bee/version.rb +3 -0
  27. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/.yml +33 -0
  28. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/a_nil_request_parameter/properly_executes_the_request.yml +33 -0
  29. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/a_request_parameter_with_an_Array/.yml +35 -0
  30. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/a_request_parameter_with_and_empty_string/properly_executes_the_request.yml +33 -0
  31. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/containing_an_error/.yml +37 -0
  32. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/containing_an_error/should_log_response_as_error.yml +37 -0
  33. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/first/.yml +33 -0
  34. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/first_item/.yml +33 -0
  35. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/first_item/_account_type_/.yml +33 -0
  36. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/first_item/_accounttype_/.yml +33 -0
  37. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/should_log_request.yml +33 -0
  38. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/should_not_log_response_as_error.yml +33 -0
  39. data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/space_character_in_a_request_parameter/properly_signs_the_request.yml +32 -0
  40. data/spec/fixtures/4.2.json +67126 -0
  41. data/spec/fixtures/simple.json +871 -0
  42. data/spec/integration/request_spec.rb +116 -0
  43. data/spec/spec_helper.rb +58 -0
  44. data/spec/units/stacker_bee/api_spec.rb +24 -0
  45. data/spec/units/stacker_bee/client_spec.rb +181 -0
  46. data/spec/units/stacker_bee/configuration_spec.rb +7 -0
  47. data/spec/units/stacker_bee/connection_spec.rb +45 -0
  48. data/spec/units/stacker_bee/middleware/logger_spec.rb +55 -0
  49. data/spec/units/stacker_bee/rash_spec.rb +87 -0
  50. data/spec/units/stacker_bee/request_error_spec.rb +44 -0
  51. data/spec/units/stacker_bee/request_spec.rb +49 -0
  52. data/spec/units/stacker_bee/response_spec.rb +79 -0
  53. data/spec/units/stacker_bee/utilities_spec.rb +25 -0
  54. data/spec/units/stacker_bee_spec.rb +6 -0
  55. data/stacker_bee.gemspec +30 -0
  56. metadata +225 -0
@@ -0,0 +1,116 @@
1
+ require 'spec_helper'
2
+ require 'logger'
3
+
4
+ describe "A response to a request sent to the CloudStack API", :vcr do
5
+ let(:io) { StringIO.new }
6
+ let(:logger) { Logger.new(io) }
7
+ let(:log_string) { io.string }
8
+ let(:url) { CONFIG["url"] }
9
+ let(:config_hash) do
10
+ {
11
+ url: url,
12
+ api_key: CONFIG["api_key"],
13
+ secret_key: CONFIG["secret_key"],
14
+ logger: logger,
15
+ apis_path: File.join(File.dirname(__FILE__), '../fixtures/4.2.json')
16
+ }
17
+ end
18
+ let(:client) do
19
+ StackerBee::Client.new(config_hash)
20
+ end
21
+ subject { client.list_accounts }
22
+
23
+ it { should_not be_empty }
24
+
25
+ context "first item" do
26
+ subject { client.list_accounts.first }
27
+ it { should include "id" }
28
+ its(["accounttype"]) { should be_a Numeric }
29
+ its(["account_type"]) { should be_a Numeric }
30
+ end
31
+
32
+ it "should log request" do
33
+ subject
34
+ log_string.should include "listAccounts"
35
+ end
36
+
37
+ it "should not log response as error" do
38
+ subject
39
+ log_string.should_not include "ERROR"
40
+ end
41
+
42
+ context "containing an error" do
43
+ subject do
44
+ client.deploy_virtual_machine
45
+ end
46
+ it { expect { subject }.to raise_error StackerBee::ClientError }
47
+
48
+ it "should log response as error" do
49
+ begin
50
+ subject
51
+ rescue StackerBee::ClientError
52
+ log_string.should include "ERROR"
53
+ end
54
+ end
55
+ end
56
+
57
+ context "failing to connect" do
58
+ let(:url) { "http://127.0.0.1:1234/client/api" }
59
+ it "should raise helpful exception" do
60
+ klass = StackerBee::ConnectionError
61
+ expect(-> { subject }).to raise_error klass, /#{url}/
62
+ end
63
+ end
64
+
65
+ context "trailing slash in URL", :regression do
66
+ let(:url) { CONFIG["url"].gsub(/\/$/, '') + '/' }
67
+ it "makes request with trailing slash" do
68
+ stub = stub_request(:get, /#{url}/).to_return(body: '{"foo": {}}')
69
+ subject
70
+ stub.should have_been_requested
71
+ end
72
+ end
73
+
74
+ context "space character in a request parameter", :regression do
75
+ let(:params) { { name: "stacker bee" } }
76
+ subject do
77
+ client.list_accounts(params)
78
+ end
79
+
80
+ it "properly signs the request" do
81
+ expect { subject }.not_to raise_error
82
+ end
83
+ end
84
+
85
+ context "a nil request parameter", :regression do
86
+ let(:params) { { name: nil } }
87
+ subject do
88
+ client.list_accounts(params)
89
+ end
90
+
91
+ it "properly executes the request" do
92
+ expect { subject }.not_to raise_error
93
+ end
94
+ end
95
+
96
+ context "a request parameter with and empty string", :regression do
97
+ let(:params) { { name: '' } }
98
+ subject do
99
+ client.list_accounts(params)
100
+ end
101
+
102
+ it "properly executes the request" do
103
+ expect { subject }.not_to raise_error
104
+ end
105
+ end
106
+
107
+ context "a request parameter with an Array", :regression do
108
+ let(:params) { { page: 1, pagesize: 1, details: [:events, :stats] } }
109
+ subject do
110
+ client.list_hosts(params).first.keys
111
+ end
112
+ it { should include "cpuused" }
113
+ it { should include "events" }
114
+ it { should_not include "cpuallocated" }
115
+ end
116
+ end
@@ -0,0 +1,58 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+
8
+ require File.expand_path("../../lib/stacker_bee", __FILE__)
9
+
10
+ require 'yaml'
11
+
12
+ default_config_file = File.expand_path("../../config.default.yml", __FILE__)
13
+ config_file = File.expand_path("../../config.yml", __FILE__)
14
+
15
+ CONFIG = YAML.load(File.read(default_config_file))
16
+ CONFIG.merge!(YAML.load(File.read(config_file))) if File.exists?(config_file)
17
+
18
+ require 'webmock/rspec'
19
+
20
+ RSpec.configure do |config|
21
+ config.treat_symbols_as_metadata_keys_with_true_values = true
22
+ config.run_all_when_everything_filtered = true
23
+ config.filter_run :focus
24
+
25
+ # Run specs in random order to surface order dependencies. If you find an
26
+ # order dependency and want to debug it, you can fix the order by providing
27
+ # the seed, which is printed after each run.
28
+ # --seed 1234
29
+ config.order = 'random'
30
+
31
+ config.after(:each) do
32
+ StackerBee::Client.reset!
33
+ end
34
+ end
35
+
36
+ require 'vcr'
37
+
38
+ VCR.configure do |c|
39
+ c.hook_into :webmock
40
+ c.cassette_library_dir = 'spec/cassettes'
41
+ c.filter_sensitive_data('<CLOUD_STACK_URL>') do
42
+ CONFIG["url"]
43
+ end
44
+ c.filter_sensitive_data('<CLOUD_STACK_API_KEY>') do
45
+ CONFIG["api_key"]
46
+ end
47
+ c.filter_sensitive_data('<CLOUD_STACK_SECRET_KEY>') do
48
+ CONFIG["secret_key"]
49
+ end
50
+ c.default_cassette_options = {
51
+ record: :new_episodes,
52
+ match_requests_on: [
53
+ :method,
54
+ VCR.request_matchers.uri_without_param(:signature)
55
+ ]
56
+ }
57
+ c.configure_rspec_metadata!
58
+ end
@@ -0,0 +1,24 @@
1
+ require "spec_helper"
2
+
3
+ describe StackerBee::API do
4
+ subject { api }
5
+
6
+ let(:api) { StackerBee::API.new(api_path: api_path) }
7
+ let(:api_path) do
8
+ File.join(File.dirname(__FILE__), '../../fixtures/simple.json')
9
+ end
10
+
11
+ its(:api_path) { should eq api_path }
12
+
13
+ its(["list_virtual_machines"]) { should be_a Hash }
14
+ its(["get_vm_password"]) { should be_a Hash }
15
+ its(["getvmpassword"]) { should be_a Hash }
16
+ its(["getVMPassword"]) { should be_a Hash }
17
+ its(["getVmPassword"]) { should be_a Hash }
18
+ its(["getWRONG"]) { should be_nil }
19
+
20
+ it { should be_key "get_vm_password" }
21
+ it { should be_key "getvmpassword" }
22
+ it { should be_key "getVMPassword" }
23
+ it { should be_key "getVmPassword" }
24
+ end
@@ -0,0 +1,181 @@
1
+ require "spec_helper"
2
+ require "ostruct"
3
+
4
+ describe StackerBee::Client, ".api" do
5
+ let(:api) { double }
6
+ before do
7
+ StackerBee::API.stub(:new) { api }
8
+ StackerBee::Client.api_path = nil
9
+ end
10
+ subject { StackerBee::Client }
11
+ its(:api_path) { should_not be_nil }
12
+ its(:api) { should eq api }
13
+ end
14
+
15
+ describe StackerBee::Client, "calling endpoint" do
16
+ let(:url) { "cloud-stack.com" }
17
+ let(:api_key) { "cloud-stack-api-key" }
18
+ let(:secret_key) { "cloud-stack-secret-key" }
19
+ let(:config_hash) do
20
+ {
21
+ url: url,
22
+ api_key: api_key,
23
+ secret_key: secret_key
24
+ }
25
+ end
26
+ let(:client) { StackerBee::Client.new config_hash }
27
+ let(:endpoint) { :list_virtual_machines }
28
+ let(:params) { { list: :all } }
29
+ let(:connection) { double }
30
+ let(:request) { double :allow_empty_string_params= => nil }
31
+ let(:raw_response) { double }
32
+ let(:response) { double }
33
+ let(:api_path) do
34
+ File.join(File.dirname(__FILE__), '../../fixtures/simple.json')
35
+ end
36
+
37
+ before do
38
+ StackerBee::Client.api_path = api_path
39
+ StackerBee::Connection.stub(:new) { connection }
40
+ StackerBee::Request.stub(:new).with(
41
+ "listVirtualMachines", api_key, params
42
+ ) do
43
+ request
44
+ end
45
+ connection.stub(:get).with(request) { raw_response }
46
+ StackerBee::Response.stub(:new).with(raw_response) { response }
47
+ end
48
+
49
+ subject { client }
50
+ it { should respond_to endpoint }
51
+ describe "response to endpoint request" do
52
+ subject { client.list_virtual_machines(params) }
53
+ it { should eq response }
54
+ end
55
+ end
56
+
57
+ describe StackerBee::Client, "#request" do
58
+ subject { client.request(endpoint, params) }
59
+ let(:endpoint) { "listVirtualMachines" }
60
+ let(:params) { { list: :all } }
61
+
62
+ let(:url) { "cloud-stack.com" }
63
+ let(:api_key) { "cloud-stack-api-key" }
64
+ let(:secret_key) { "cloud-stack-secret-key" }
65
+ let(:config_hash) do
66
+ {
67
+ url: url,
68
+ api_key: api_key,
69
+ secret_key: secret_key
70
+ }
71
+ end
72
+ let(:client) { StackerBee::Client.new config_hash }
73
+ let(:connection) { double }
74
+ let(:request) { double :allow_empty_string_params= => nil }
75
+ let(:raw_response) { double }
76
+ let(:response) { double }
77
+
78
+ before do
79
+ StackerBee::Connection.should_receive(:new) { connection }
80
+ StackerBee::Request.should_receive(:new).with(endpoint, api_key, params) do
81
+ request
82
+ end
83
+ connection.should_receive(:get).with(request) { raw_response }
84
+ StackerBee::Response.should_receive(:new).with(raw_response) { response }
85
+ end
86
+
87
+ it { should eq response }
88
+
89
+ context "called with a differently-cased endpoint" do
90
+ subject { client.request("list_Virtual_mACHINES", params) }
91
+ it { should eq response }
92
+ end
93
+ end
94
+
95
+ describe StackerBee::Client, "configuration" do
96
+ let(:default_url) { "default_cloud-stack.com" }
97
+ let(:default_api_key) { "default-cloud-stack-api-key" }
98
+ let(:default_secret_key) { "default-cloud-stack-secret-key" }
99
+ let(:default_config_hash) do
100
+ {
101
+ url: default_url,
102
+ api_key: default_api_key,
103
+ secret_key: default_secret_key,
104
+ allow_empty_string_params: false
105
+ }
106
+ end
107
+ let!(:default_configuration) do
108
+ StackerBee::Configuration.new(default_config_hash)
109
+ end
110
+ let(:instance_url) { "instance-cloud-stack.com" }
111
+ let(:instance_api_key) { "instance-cloud-stack-api-key" }
112
+ let(:instance_secret_key) { "instance-cloud-stack-secret-key" }
113
+ let(:instance_config_hash) do
114
+ {
115
+ url: instance_url,
116
+ api_key: instance_api_key,
117
+ secret_key: instance_secret_key,
118
+ allow_empty_string_params: false
119
+ }
120
+ end
121
+ let!(:instance_configuration) do
122
+ StackerBee::Configuration.new(instance_config_hash)
123
+ end
124
+ before do
125
+ StackerBee::Configuration.stub(:new) do
126
+ fail "Unexpected Configuration instantiation"
127
+ end
128
+ StackerBee::Configuration.stub(:new).with(default_config_hash) do
129
+ default_configuration
130
+ end
131
+ StackerBee::Configuration.stub(:new).with(instance_config_hash) do
132
+ instance_configuration
133
+ end
134
+ StackerBee::Client.configuration = default_config_hash
135
+ end
136
+
137
+ describe ".new" do
138
+ subject { StackerBee::Client.new }
139
+
140
+ context "with default, class configuration" do
141
+ its(:url) { should eq default_url }
142
+ its(:api_key) { should eq default_api_key }
143
+ its(:secret_key) { should eq default_secret_key }
144
+ end
145
+
146
+ context "with instance-specific configuration" do
147
+ subject { StackerBee::Client.new(instance_config_hash) }
148
+ its(:configuration) { should eq instance_configuration }
149
+ its(:url) { should eq instance_url }
150
+ its(:api_key) { should eq instance_api_key }
151
+ its(:secret_key) { should eq instance_secret_key }
152
+ end
153
+
154
+ context "with instance-specific configuration that's not a hash" do
155
+ subject { StackerBee::Client.new(config) }
156
+ let(:config) { double(to_hash: instance_config_hash) }
157
+ its(:configuration) { should eq instance_configuration }
158
+ its(:url) { should eq instance_url }
159
+ its(:api_key) { should eq instance_api_key }
160
+ its(:secret_key) { should eq instance_secret_key }
161
+ end
162
+
163
+ describe "#url" do
164
+ let(:other_url) { "other-cloud-stack.com" }
165
+ before { subject.url = other_url }
166
+ its(:url) { should eq other_url }
167
+ end
168
+
169
+ describe "#api_key" do
170
+ let(:other_api_key) { "other-cloud-stack-api-key" }
171
+ before { subject.api_key = other_api_key }
172
+ its(:api_key) { should eq other_api_key }
173
+ end
174
+
175
+ describe "#secret_key" do
176
+ let(:other_secret_key) { "other-cloud-stack-secret-key" }
177
+ before { subject.secret_key = other_secret_key }
178
+ its(:secret_key) { should eq other_secret_key }
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,7 @@
1
+ require "spec_helper"
2
+
3
+ describe StackerBee::Configuration do
4
+ its(:url) { should be_nil }
5
+ its(:api_key) { should be_nil }
6
+ its(:secret_key) { should be_nil }
7
+ end
@@ -0,0 +1,45 @@
1
+ require "spec_helper"
2
+
3
+ describe StackerBee::Connection do
4
+ let(:url) { "http://test.com:1234/foo/bar/" }
5
+ let(:secret_key) { "shhh" }
6
+ let(:configuration) { double url: url, secret_key: secret_key }
7
+ let(:query_params) { [[:foo, :bar]] }
8
+ let(:request) { double query_params: query_params }
9
+ let(:response) { double }
10
+ let(:faraday) { double get: response }
11
+ let(:connection) { StackerBee::Connection.new configuration }
12
+ subject { connection.get request }
13
+ before do
14
+ Faraday.stub(:new) { faraday }
15
+ end
16
+
17
+ context "successfully connecting" do
18
+ before do
19
+ faraday.should_receive(:get).with('/foo/bar/', query_params) { response }
20
+ end
21
+ it { should be response }
22
+ it "specifies url without path when creating connection" do
23
+ subject
24
+ Faraday.should have_received(:new).with(url: "http://test.com:1234")
25
+ end
26
+ end
27
+
28
+ context "failing to connect" do
29
+ before do
30
+ faraday.stub(:get) { fail Faraday::Error::ConnectionFailed, "boom" }
31
+ end
32
+ it "should raise helpful exception" do
33
+ klass = StackerBee::ConnectionError
34
+ expect(-> { subject }).to raise_error klass, /#{url}/
35
+ end
36
+ end
37
+
38
+ context "no protocol specified in URL" do
39
+ let(:url) { "wrong.com" }
40
+ it "should raise helpful exception" do
41
+ klass = StackerBee::ConnectionError
42
+ expect(-> { subject }).to raise_error klass, /no protocol/
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,55 @@
1
+ require "spec_helper"
2
+
3
+ describe StackerBee::Middleware::Logger do
4
+ let(:app) { double }
5
+ let(:io) { StringIO.new }
6
+ let(:logger) { Logger.new(io) }
7
+ let(:log_string) { io.string }
8
+ let(:middleware) { described_class.new app, logger }
9
+ subject { middleware }
10
+
11
+ context "with a no logger specified" do
12
+ let(:middleware) { described_class.new app, nil }
13
+ its(:logger) { should be_a Logger }
14
+ end
15
+
16
+ describe "logging request" do
17
+ let(:env) do
18
+ {
19
+ method: "PATCH",
20
+ url: "http://localhost",
21
+ request_headers: { "User-Agent" => "RSpec" }
22
+ }
23
+ end
24
+ before do
25
+ middleware.log_request(env)
26
+ end
27
+ subject { log_string }
28
+ it { should include "PATCH" }
29
+ it { should include "localhost" }
30
+ it { should include "User-Agent" }
31
+ end
32
+
33
+ describe "logging a response" do
34
+ let(:status) { 200 }
35
+ let(:env) do
36
+ {
37
+ status: status,
38
+ body: "OK",
39
+ response_headers: { "Server" => "RSpec" }
40
+ }
41
+ end
42
+ before do
43
+ middleware.log_response(env)
44
+ end
45
+ subject { log_string }
46
+ it { should =~ /INFO.*#{status}/ }
47
+ it { should include "OK" }
48
+ it { should include "Server" }
49
+
50
+ context "failing status" do
51
+ let(:status) { 401 }
52
+ it { should =~ /ERROR.*#{status}/ }
53
+ end
54
+ end
55
+ end