cloud_financial_officer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,110 @@
1
+ module CFO
2
+ include CloudCostTracker::Billing
3
+ describe RecordGrouper do
4
+
5
+ before(:all) do
6
+ BillingRecord.delete_all
7
+ BillingCode.delete_all
8
+ # We'll Create 16 BillingRecords so we can perform a tertiary sort,
9
+ # and still collapse the total Records to 8 by the grouping operation.
10
+ @groupings = ['account', 'project', 'environment']
11
+ # First, the BillingCodes must be created
12
+ BillingCode.create!(:key => 'project', :value => 'balance')
13
+ BillingCode.create!(:key => 'project', :value => 'imedidata')
14
+ BillingCode.create!(:key => 'environment', :value => 'sandbox')
15
+ BillingCode.create!(:key => 'environment', :value => 'validation')
16
+ BillingCode.create!(:key => 'environment', :value => 'innovate')
17
+ BillingCode.create!(:key => 'environment', :value => 'production')
18
+ # Except for the grouping fields, plus one extra field, resource_type,
19
+ # all records with have the same data...
20
+ @start, @stop, @per_record_cost = Time.now, (Time.now + 60), 1
21
+ [ # first, create 8 unique permuations of the @groupings
22
+ # Account | 'code:project' | 'code:environment' | resource_type (dropped)
23
+ [ 'dev', 'balance', 'sandbox', 'Server'],
24
+ [ 'dev', 'balance', 'validation', 'Server'],
25
+ [ 'dev', 'imedidata', 'sandbox', 'Server'],
26
+ [ 'dev', 'imedidata', 'validation', 'Server'],
27
+ [ 'prod', 'balance', 'innovate', 'Server'],
28
+ [ 'prod', 'balance', 'production', 'Server'],
29
+ [ 'prod', 'imedidata', 'innovate', 'Server'],
30
+ [ 'prod', 'imedidata', 'production', 'Server'],
31
+ # Now, make the same permutations, but with a different resource_type
32
+ [ 'dev', 'balance', 'sandbox', 'Volume'],
33
+ [ 'dev', 'balance', 'validation', 'Volume'],
34
+ [ 'dev', 'imedidata', 'sandbox', 'Volume'],
35
+ [ 'dev', 'imedidata', 'validation', 'Volume'],
36
+ [ 'prod', 'balance', 'innovate', 'Volume'],
37
+ [ 'prod', 'balance', 'production', 'Volume'],
38
+ [ 'prod', 'imedidata', 'innovate', 'Volume'],
39
+ [ 'prod', 'imedidata', 'production', 'Volume'],
40
+ ].map do |record_data|
41
+ BillingRecord.create!(
42
+ :account => record_data[0], :service => 'Compute', :provider => 'AWS',
43
+ :resource_type => record_data[3], :resource_id => "id-#{rand 65535}",
44
+ :start_time => @start, :stop_time => @stop, :cost_per_hour => 60,
45
+ :total_cost => @per_record_cost, :billing_type => 'whatever',
46
+ :billing_codes => [
47
+ BillingCode.where(:key => 'project', :value => record_data[1]).
48
+ first_or_create!,
49
+ BillingCode.where(:key => 'environment', :value => record_data[2]).
50
+ first_or_create!,
51
+ ]
52
+ )
53
+ end
54
+ end
55
+
56
+ describe '#by' do
57
+ before(:each) do
58
+ @results = RecordGrouper.by(@groupings, BillingRecord.all)
59
+ end
60
+
61
+ it "merges all Records with the same unique permutation of groupings" do
62
+ # make sure there are the right number of records for the above example
63
+ @results.should have(8).things
64
+ end
65
+ it "returns records with a total cost equal to the input's total" do
66
+ @results.each do |record|
67
+ record.total_cost.should == 2 * @per_record_cost
68
+ end
69
+ total = @results.inject(0) {|t,record| t += record.total_cost}
70
+ total.should == BillingRecord.count * @per_record_cost
71
+ end
72
+ it "returns composite records with shared component record values" do
73
+ @results.each do |record|
74
+ record.service.should == 'Compute'
75
+ record.provider.should == 'AWS'
76
+ record.billing_type.should == 'whatever'
77
+ record.resource_type.should == RecordGrouper::MULTIPLE_VALUES_VALUE
78
+ record.resource_id.should == RecordGrouper::MULTIPLE_VALUES_VALUE
79
+ end
80
+ end
81
+
82
+ context "when invoked with groupings that don't match all input records" do
83
+ before(:all) do
84
+ # Create 2 new records that don't match the groupings (in the same way)
85
+ 2.times do
86
+ BillingRecord.create!(
87
+ :account => 'other account', :service => 'Compute', :provider => 'RDS',
88
+ :resource_type => 'Snapshot', :resource_id => "id-#{rand 65535}",
89
+ :start_time => @start, :stop_time => @stop, :cost_per_hour => 60,
90
+ :total_cost => @per_record_cost, :billing_type => 'whatever')
91
+ end
92
+ @results = RecordGrouper.by(@groupings, BillingRecord.all)
93
+ end
94
+ it "treats all matches for a given BillingCode as a single group" do
95
+ @results.should have(9).things # a new composite record
96
+ non_matching_record = @results.find {|r| r.account == 'other account'}
97
+ non_matching_record.billing_codes.should be_empty
98
+ non_matching_record.total_cost.should == 2 * @per_record_cost
99
+ end
100
+ it "returns records with a total cost equal to the input's total" do
101
+ @results.each do |record|
102
+ record.total_cost.should == 2 * @per_record_cost
103
+ end
104
+ total = @results.inject(0) {|t,record| t += record.total_cost}
105
+ total.should == BillingRecord.count * @per_record_cost
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,46 @@
1
+ module CFO
2
+ describe Account do
3
+
4
+ before(:each) do
5
+ @account = build(:account,
6
+ :billing_codes => [['key1', 'val1'], ['key2', 'val2']]
7
+ )
8
+ end
9
+
10
+ describe '#to_hash' do
11
+ it "returns a hash of the account's attributes, with name as ID" do
12
+ account_hash = @account.to_hash
13
+ account_hash['id'].should == @account.name
14
+ account_hash['service'].should == @account.service
15
+ account_hash['provider'].should == @account.provider
16
+ account_hash['billing_codes'].should ==
17
+ [['key1', 'val1'], ['key2', 'val2']]
18
+ end
19
+ it "returns a URI link to itself" do
20
+ @account.to_hash['url'].should ==
21
+ "#{CFO.root}/accounts/#{URI.escape(@account.name)}"
22
+ end
23
+ end
24
+
25
+ describe '#uri' do
26
+ it "returns a URI link to itself" do
27
+ @account.uri.should == "#{CFO.root}/accounts/#{URI.escape(@account.name)}"
28
+ end
29
+ end
30
+
31
+ describe '#report_uri' do
32
+ it "returns a URI for its cost report" do
33
+ @account.report_uri.should ==
34
+ "#{CFO.root}/report/for_account/#{URI.escape(@account.name)}"
35
+ end
36
+ end
37
+
38
+ describe '#to_json' do
39
+ it "returns a JSON version of the #to_hash output" do
40
+ account_hash = @account.to_hash
41
+ @account.to_json.should == @account.to_hash.to_json
42
+ end
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,90 @@
1
+ module CFO
2
+ describe CloudResource do
3
+
4
+ before(:each) do
5
+ @resource = build(:resource, {
6
+ :billing_codes => [['key1', 'val1'], ['key2', 'val2']],
7
+ :account => build(:account),
8
+ })
9
+ end
10
+
11
+ describe '#to_hash' do
12
+ context "when invoked with verbose=false" do
13
+ it "returns a hash of the resource's attributes" do
14
+ resource_hash = @resource.to_hash(false)
15
+ resource_hash['resource_id'].should == @resource.resource_id
16
+ resource_hash['resource_type'].should == @resource.resource_type
17
+ end
18
+ end
19
+ context "when invoked with verbose=true" do
20
+ it "returns information on its account" do
21
+ account_data = @resource.to_hash(true)['account']
22
+ account_data['id'].should == @resource.account.name
23
+ account_data['service'].should == @resource.account.service
24
+ account_data['provider'].should == @resource.account.provider
25
+ account_data['url'].should ==
26
+ "#{CFO.root}/accounts/#{URI.escape(@resource.account.name)}"
27
+ end
28
+ it "returns a billing codes and the resource list as well" do
29
+ resource_hash = @resource.to_hash(true)
30
+ resource_hash['billing_codes'].should ==
31
+ [['key1', 'val1'], ['key2', 'val2']]
32
+ end
33
+ end
34
+ end
35
+
36
+ describe '#uri' do
37
+ it "returns a URI link to itself" do
38
+ @resource.uri.should ==
39
+ "#{CFO.root}/accounts/#{URI.escape(@resource.account.name)}"+
40
+ "/#{URI.escape(@resource.resource_type)}"+
41
+ "/#{URI.escape(@resource.resource_id)}"
42
+ end
43
+ end
44
+
45
+ describe '#report_uri' do
46
+ it "returns a URI link to its cost report" do
47
+ account = @resource.account
48
+ @resource.report_uri.should ==
49
+ "#{CFO.root}/report/for_account/#{URI.escape(account.name)}/for_type/"+
50
+ "#{URI.escape(@resource.resource_type)}/for_resource/"+
51
+ "#{URI.escape(@resource.resource_id)}"
52
+ end
53
+ end
54
+
55
+ describe '#to_json' do
56
+ it "returns a JSON version of the #to_hash output" do
57
+ resource_hash = @resource.to_hash
58
+ @resource.to_json.should == @resource.to_hash.to_json
59
+ end
60
+ end
61
+
62
+ describe '#hash_list' do
63
+ before(:each) do
64
+ CloudCostTracker::Billing::BillingRecord.delete_all
65
+ # Create a pair of distinct BillingRecords
66
+ @resource1 = create(:record,{:resource_id => 'fake-resource-1'})
67
+ @resource2 = create(:record,{:resource_id => 'fake-resource-2'})
68
+ end
69
+ context "when invoked with no conditions" do
70
+ it "returns an Array of all CloudResource Hash representations" do
71
+ resource_list = CloudResource.hash_list
72
+ resource_list.should have(2).things
73
+ (resource_list.map {|r| r['resource_id']}).should ==
74
+ [@resource1.resource_id, @resource2.resource_id]
75
+ end
76
+ end
77
+ context "when invoked with conditions" do
78
+ it "returns an Array of all matching CloudResource Hashes" do
79
+ resource_list = CloudResource.hash_list(
80
+ :resource_id => @resource1.resource_id
81
+ )
82
+ resource_list.should have(1).thing
83
+ (resource_list.map {|r| r['resource_id']}).should ==
84
+ [@resource1.resource_id]
85
+ end
86
+ end
87
+ end
88
+
89
+ end
90
+ end
@@ -0,0 +1,264 @@
1
+ module CFO
2
+ include CloudCostTracker::Billing
3
+
4
+ describe Report do
5
+
6
+ # Number of BillingRecords to report on
7
+ NUM_RECORDS = 50 # Each has its own unique provider, service, etc.
8
+
9
+ before(:all) do
10
+ BillingRecord.delete_all
11
+ @billing_start = DateTime.parse((Time.now - (RECORD_DURATION + 1)).to_s)
12
+ # Create many BillingRecords, each with its own unique attributes
13
+ (1..NUM_RECORDS).each do |i|
14
+ create(:record, {
15
+ :provider => "#{PROVIDER_NAME}#{i}",
16
+ :service => "#{SERVICE_NAME}#{i}",
17
+ :account => "#{ACCOUNT_NAME}#{i}",
18
+ :resource_id => "#{RESOURCE_ID}#{i}",
19
+ :resource_type => "#{RESOURCE_TYPE}#{i}",
20
+ :billing_type => "#{BILLING_TYPE}#{i}",
21
+ })
22
+ end
23
+ @billing_stop = DateTime.parse((Time.now + 1).to_s)
24
+ end
25
+
26
+ after(:all) do
27
+ BillingRecord.delete_all
28
+ end
29
+
30
+ describe '#to_json' do
31
+ it "renders a summary of the included billing records" do
32
+ report_hash = JSON.parse(Report.new.to_json)
33
+ report_hash['start_time'].should ==
34
+ report_hash['billing_records'].first['start_time']
35
+ report_hash['stop_time'].should ==
36
+ report_hash['billing_records'].last['stop_time']
37
+ report_hash['total_cost'].should be_a Numeric
38
+ report_hash['total_cost'].should == NUM_RECORDS * RECORD_TOTAL
39
+ report_hash['cost_per_hour'].should be_a Numeric
40
+ report_duration = Time.parse(report_hash['stop_time']) -
41
+ Time.parse(report_hash['start_time'])
42
+ report_hash['cost_per_hour'].should be_within(0.01).
43
+ of(SECONDS_PER_HOUR * report_hash['total_cost'] / report_duration)
44
+ end
45
+
46
+ # CONSTRAIN BY ACCOUNT NAME
47
+ context "when invoked with a matching account name constraint" do
48
+ it "returns a report on all the account's BillingRecords" do
49
+ # now search for a random account
50
+ account_name = "#{ACCOUNT_NAME}#{rand(NUM_RECORDS) + 1}"
51
+ constraint = {'for_account' => account_name}
52
+ response = Report.new([constraint]).to_json
53
+ billing_records = JSON.parse(response)['billing_records']
54
+ billing_records.size.should == 1
55
+ billing_records.first['account'].should == account_name
56
+ end
57
+ end
58
+ context "when invoked with a non-matching account name constraint" do
59
+ it "returns a report with no BillingRecords" do
60
+ # now search for a bogus account
61
+ constraint = {'for_account' => "UN-FINDABLE NAME"}
62
+ response = Report.new([constraint]).to_json
63
+ billing_records = JSON.parse(response)['billing_records']
64
+ billing_records.should be_empty
65
+ end
66
+ end
67
+
68
+ # CONSTRAIN BY RESOURCE ID
69
+ context "when invoked with a matching resource ID constraint" do
70
+ it "returns a report on all the resource's BillingRecords" do
71
+ # now search for a resource ID
72
+ resource_id = "#{RESOURCE_ID}#{rand(NUM_RECORDS) + 1}"
73
+ constraint = {'for_resource' => resource_id}
74
+ response = Report.new([constraint]).to_json
75
+ billing_records = JSON.parse(response)['billing_records']
76
+ billing_records.size.should == 1
77
+ billing_records.first['resource_id'].should == resource_id
78
+ end
79
+ end
80
+ context "when invoked with a non-matching resource ID constraint" do
81
+ it "returns a report with no BillingRecords" do
82
+ # now search for a bogus resource ID
83
+ constraint = {'for_resource' => "UN-FINDABLE NAME"}
84
+ response = Report.new([constraint]).to_json
85
+ billing_records = JSON.parse(response)['billing_records']
86
+ billing_records.should be_empty
87
+ end
88
+ end
89
+
90
+ # CONSTRAIN BY RESOURCE TYPE
91
+ context "when invoked with a matching resource type constraint" do
92
+ it "returns a report on all the resource's BillingRecords" do
93
+ # now search for a resource type
94
+ resource_type = "#{RESOURCE_TYPE}#{rand(NUM_RECORDS) + 1}"
95
+ constraint = {'for_type' => resource_type}
96
+ response = Report.new([constraint]).to_json
97
+ billing_records = JSON.parse(response)['billing_records']
98
+ billing_records.size.should == 1
99
+ billing_records.first['resource_type'].should == resource_type
100
+ end
101
+ end
102
+ context "when invoked with a non-matching resource type constraint" do
103
+ it "returns a report with no BillingRecords" do
104
+ # now search for a bogus resource type
105
+ constraint = {'for_type' => "UN-FINDABLE NAME"}
106
+ response = Report.new([constraint]).to_json
107
+ billing_records = JSON.parse(response)['billing_records']
108
+ billing_records.should be_empty
109
+ end
110
+ end
111
+
112
+ # CONSTRAIN BY PROVIDER
113
+ context "when invoked with a matching provider constraint" do
114
+ it "returns a report on all the provider's BillingRecords" do
115
+ # now search for a provider
116
+ provider = "#{PROVIDER_NAME}#{rand(NUM_RECORDS) + 1}"
117
+ constraint = {'for_provider' => provider}
118
+ response = Report.new([constraint]).to_json
119
+ billing_records = JSON.parse(response)['billing_records']
120
+ billing_records.size.should == 1
121
+ billing_records.first['provider'].should == provider
122
+ end
123
+ end
124
+ context "when invoked with a non-matching provider constraint" do
125
+ it "returns a report with no BillingRecords" do
126
+ # now search for a bogus provider
127
+ constraint = {'for_provider' => "UN-FINDABLE NAME"}
128
+ response = Report.new([constraint]).to_json
129
+ billing_records = JSON.parse(response)['billing_records']
130
+ billing_records.should be_empty
131
+ end
132
+ end
133
+
134
+ # CONSTRAIN BY SERVICE
135
+ context "when invoked with a matching service constraint" do
136
+ it "returns a report on all the service's BillingRecords" do
137
+ # now search for a service
138
+ service = "#{SERVICE_NAME}#{rand(NUM_RECORDS) + 1}"
139
+ constraint = {'for_service' => service}
140
+ response = Report.new([constraint]).to_json
141
+ billing_records = JSON.parse(response)['billing_records']
142
+ billing_records.size.should == 1
143
+ billing_records.first['service'].should == service
144
+ end
145
+ end
146
+ context "when invoked with a non-matching service constraint" do
147
+ it "returns a report with no BillingRecords" do
148
+ # now search for a bogus service
149
+ constraint = {'for_service' => "UN-FINDABLE NAME"}
150
+ response = Report.new([constraint]).to_json
151
+ billing_records = JSON.parse(response)['billing_records']
152
+ billing_records.should be_empty
153
+ end
154
+ end
155
+
156
+ # CONSTRAIN BY BILLING TYPE
157
+ context "when invoked with a matching billing type constraint" do
158
+ it "returns a report on all the BillingRecords of that type" do
159
+ # now search for a billing type
160
+ billing_type = "#{BILLING_TYPE}#{rand(NUM_RECORDS) + 1}"
161
+ constraint = {'for_billing_type' => billing_type}
162
+ response = Report.new([constraint]).to_json
163
+ billing_records = JSON.parse(response)['billing_records']
164
+ billing_records.size.should == 1
165
+ billing_records.first['billing_type'].should == billing_type
166
+ end
167
+ end
168
+ context "when invoked with a non-matching billing type constraint" do
169
+ it "returns a report with no BillingRecords" do
170
+ # now search for a bogus billing type
171
+ constraint = {'for_billing_type' => "UN-FINDABLE NAME"}
172
+ response = Report.new([constraint]).to_json
173
+ billing_records = JSON.parse(response)['billing_records']
174
+ billing_records.should be_empty
175
+ end
176
+ end
177
+
178
+ # CONSTRAIN BY TIME
179
+ context "when invoked with a time constraint encompassing all records" do
180
+ it "returns a report on all the BillingRecords" do
181
+ # now search on a time range encompassing all records
182
+ constraint = {'for_time' => [@billing_start, Time.now]}
183
+ response = Report.new([constraint]).to_json
184
+ billing_records = JSON.parse(response)['billing_records']
185
+ billing_records.size.should == NUM_RECORDS
186
+ first_start = DateTime.parse(billing_records.first['start_time'])
187
+ last_stop = DateTime.parse(billing_records.last['stop_time'])
188
+ first_start.should >= @billing_start
189
+ last_stop.should <= @billing_stop
190
+ end
191
+ end
192
+ context "when invoked with a single non-matching time constraint" do
193
+ it "returns a report with no BillingRecords" do
194
+ # now search for a zero-second time range
195
+ constraint = {'for_time' => [Time.now + 60, Time.now + 60]}
196
+ response = Report.new([constraint]).to_json
197
+ billing_records = JSON.parse(response)['billing_records']
198
+ billing_records.should be_empty
199
+ end
200
+ end
201
+ context "with a time constraint that partially overlaps a record" do
202
+ it "truncates the partially overlapped record" do
203
+ a_record = BillingRecord.first
204
+ record_duration = a_record.stop_time - a_record.start_time
205
+ query_start = a_record.start_time + (record_duration / 2)
206
+ constraint = {'for_time' => [query_start, Time.now]}
207
+ response = Report.new([constraint]).to_json
208
+ billing_records = JSON.parse(response)['billing_records']
209
+ first_start = Time.parse(billing_records.first['start_time'])
210
+ first_stop = Time.parse(billing_records.first['stop_time'])
211
+ first_start.should be_within(1).of query_start
212
+ first_stop.should be_within(1).of a_record.stop_time
213
+ end
214
+ end
215
+
216
+ # CONSTRAIN BY BILLING CODE
217
+ context "when coded records exist" do
218
+ before(:all) do
219
+ coded_record = BillingRecord.first
220
+ coded_record.billing_codes =
221
+ [BillingCode.new(:key => 'codekey', :value => 'codevalue')]
222
+ coded_record.save!
223
+ end
224
+ context "when invoked with a fully-matching code constraint" do
225
+ it "returns only BillingRecords with matching code" do
226
+ # now search for the coded record
227
+ constraint = {'for_code' => ['codekey', 'codevalue']}
228
+ response = Report.new([constraint]).to_json
229
+ billing_records = JSON.parse(response)['billing_records']
230
+ billing_records.size.should == 1
231
+ billing_records.first['billing_codes'].should ==
232
+ [[ 'codekey', 'codevalue' ]]
233
+ end
234
+ end
235
+ context "when invoked with a matching wildcard constraint" do
236
+ it "returns only BillingRecords with matching code" do
237
+ constraint = {'for_code' => ['codekey', '*']}
238
+ response = Report.new([constraint]).to_json
239
+ billing_records = JSON.parse(response)['billing_records']
240
+ billing_records.size.should == 1
241
+ billing_records.first['billing_codes'].should ==
242
+ [[ 'codekey', 'codevalue' ]]
243
+ constraint = {'for_code' => ['*', 'codevalue']}
244
+ response = Report.new([constraint]).to_json
245
+ billing_records = JSON.parse(response)['billing_records']
246
+ billing_records.size.should == 1
247
+ billing_records.first['billing_codes'].should ==
248
+ [[ 'codekey', 'codevalue' ]]
249
+ end
250
+ end
251
+ context "when invoked with only non-matching code constraints" do
252
+ it "returns a report with no BillingRecords" do
253
+ # now search for the coded record
254
+ constraint = {'for_code' => ['XXXXXXXXXX', '*']}
255
+ response = Report.new([constraint]).to_json
256
+ billing_records = JSON.parse(response)['billing_records']
257
+ billing_records.should be_empty
258
+ end
259
+ end
260
+ end
261
+
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,103 @@
1
+ module CFO
2
+ describe Service do
3
+ include Rack::Test::Methods
4
+ def app ; CFO::Service end
5
+
6
+ before(:all) do
7
+ @accounts = FogTracker.validate_accounts(
8
+ YAML.load(File.read('./config/accounts.yml')))
9
+ end
10
+
11
+ describe 'GET /' do
12
+ it "returns a list of the accounts" do
13
+ get "#{CFO.root}"
14
+ account_data = JSON.parse(last_response.body)
15
+ account_data.size.should == @accounts.size
16
+ account_data.each do |account|
17
+ @accounts.keys.should include(account['id'])
18
+ @accounts[account['id']][:service].should == account['service']
19
+ @accounts[account['id']][:provider].should == account['provider']
20
+ end
21
+ end
22
+ end
23
+
24
+ describe 'GET /accounts' do
25
+ it "returns a list of the accounts" do
26
+ get "#{CFO.root}/accounts"
27
+ account_data = JSON.parse(last_response.body)
28
+ account_data.size.should == @accounts.size
29
+ account_data.each do |account|
30
+ @accounts.keys.should include(account['id'])
31
+ @accounts[account['id']][:service].should == account['service']
32
+ @accounts[account['id']][:provider].should == account['provider']
33
+ end
34
+ end
35
+ end
36
+
37
+ describe 'GET /resources' do
38
+ it "returns a list of all known cloud computing resources" do
39
+ CloudCostTracker::Billing::BillingRecord.delete_all
40
+ # Create a pair of BillingRecords
41
+ resource1 = create(:record,{:resource_id => 'fake-resource-1'})
42
+ resource2 = create(:record,{:resource_id => 'fake-resource-2'})
43
+ get "#{CFO.root}/resources"
44
+ resource_data = JSON.parse(last_response.body)
45
+ (resource_data.map {|r| r['resource_id']}).should ==
46
+ ['fake-resource-1', 'fake-resource-2']
47
+ end
48
+ end
49
+
50
+ describe 'GET /accounts/:account_name' do
51
+ context "when passed a valid account name" do
52
+ it "returns the JSON representation of the account named :account_name" do
53
+ account = @accounts[@accounts.keys.sort.first]
54
+ get "#{CFO.root}/accounts/#{URI.escape(@accounts.keys.sort.first)}"
55
+ account_data = JSON.parse(last_response.body)
56
+ account_data['id'].should == @accounts.keys.sort.first
57
+ account_data['service'].should == account[:service]
58
+ account_data['provider'].should == account[:provider]
59
+ end
60
+ end
61
+ context "when passed an invalid account name" do
62
+ it "returns a 404 with an error message" do
63
+ get "#{CFO.root}/accounts/INVALID-ACCOUNT-NAME"
64
+ last_response.status.should == 404
65
+ error_data = JSON.parse(last_response.body)
66
+ error_data.keys.should include('error')
67
+ end
68
+ end
69
+ end
70
+
71
+ describe 'GET /accounts/:account_name/:type/:resource_id' do
72
+ before(:each) do
73
+ CloudCostTracker::Billing::BillingRecord.delete_all
74
+ @record = create(:record)
75
+ @record_url = "#{CFO.root}/accounts/#{URI.escape(@record.account)}"+
76
+ "/#{URI.escape(@record.resource_type)}"+
77
+ "/#{URI.escape(@record.resource_id)}"
78
+ end
79
+ after(:each) {CloudCostTracker::Billing::BillingRecord.delete_all}
80
+
81
+ context "when passed a matching account name, resource type, and ID" do
82
+ it "returns the JSON representation of the matching resource" do
83
+ get @record_url
84
+ resource_data = JSON.parse(last_response.body)
85
+ resource_data['resource_id'].should == @record.resource_id
86
+ resource_data['resource_type'].should == @record.resource_type
87
+ resource_data['account']['id'].should == @record.account
88
+ resource_data['account']['service'].should == @record.service
89
+ resource_data['account']['provider'].should == @record.provider
90
+ end
91
+ end
92
+ context "when passed non-matching data" do
93
+ it "returns a 404 with an error message" do
94
+ get "#{@record_url}XXXXXXXXXXXXXXXXX"
95
+ last_response.status.should == 404
96
+ error_data = JSON.parse(last_response.body)
97
+ error_data.keys.should include('error')
98
+ end
99
+ end
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,37 @@
1
+ require 'active_record'
2
+ ENV['RACK_ENV'] ||= 'test'
3
+
4
+ ## Establish ActiveRecord connection and Migrate database
5
+ # The following db config files are used, in order of preference:
6
+ # 1. ENV['DB_CONFIG_FILE']
7
+ # 2. ./config/database.yml
8
+ # 3. ./config/database.example.yml
9
+ db_conf_file = './config/database.example.yml'
10
+ db_conf_file = './config/database.yml' if File.exists? './config/database.yml'
11
+ if ENV['DB_CONFIG_FILE']
12
+ db_conf_file = ENV['DB_CONFIG_FILE']
13
+ else
14
+ ENV['DB_CONFIG_FILE'] = db_conf_file
15
+ end
16
+ puts "Reading DB config file #{db_conf_file}..."
17
+ db_config = YAML::load(File.open(db_conf_file))[ENV['RACK_ENV'] || 'test']
18
+ puts "Using DB #{db_config['database']}..."
19
+ ActiveRecord::Base.establish_connection(db_config)
20
+ ActiveRecord::Migrator.migrate(
21
+ File.join(Gem.loaded_specs['cloud_cost_tracker'].full_gem_path,
22
+ 'db', 'migrate'), ENV["VERSION"] ? ENV["VERSION"].to_i : nil
23
+ )
24
+
25
+ require './lib/cloud_financial_officer'
26
+ require './spec/factories'
27
+ require 'rack/test'
28
+
29
+ # Require RSpec support files. Logging is configured there
30
+ support_files = Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")]
31
+ support_files.sort.each {|f| require f}
32
+
33
+ # RSpec configuration block
34
+ RSpec.configure do |config|
35
+ config.mock_with :rspec # == Mock Framework
36
+ config.include FactoryGirl::Syntax::Methods
37
+ end
@@ -0,0 +1,6 @@
1
+ # Setup a global Logger for all tests
2
+ LOG_LEVEL = ::Logger::WARN
3
+ LOG = FogTracker.default_logger(STDOUT)
4
+ LOG.info "Logging configured in #{File.basename __FILE__}."
5
+ LOG.level = LOG_LEVEL
6
+ ActiveRecord::Base.logger = LOG # Uncomment for ActiveRecord outputs