vault-usage-client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .bundle
2
+ doc
3
+ test/.test.profile*
4
+ vendor
5
+ .yardoc
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ -m markdown
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem 'rake'
7
+ end
8
+
9
+ group :test do
10
+ gem 'rr'
11
+ gem 'vault-test-tools', '~> 0.2.2', :git => 'https://github.com/heroku/vault-test-tools.git'
12
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,58 @@
1
+ GIT
2
+ remote: https://github.com/heroku/vault-test-tools.git
3
+ revision: 5eabd67610f279b81cadb803cee8ef704093a05d
4
+ specs:
5
+ vault-test-tools (0.2.2)
6
+ minitest
7
+ nokogiri
8
+ rack-perftools_profiler
9
+ rack-test
10
+ rdoc
11
+ redcarpet
12
+ turn
13
+ yard
14
+
15
+ PATH
16
+ remote: .
17
+ specs:
18
+ vault-usage-client (0.0.1)
19
+ colorize
20
+ excon
21
+ yajl-ruby
22
+
23
+ GEM
24
+ remote: http://rubygems.org/
25
+ specs:
26
+ ansi (1.4.3)
27
+ colorize (0.5.8)
28
+ excon (0.16.10)
29
+ json (1.7.6)
30
+ minitest (4.6.0)
31
+ nokogiri (1.5.6)
32
+ open4 (1.3.0)
33
+ perftools.rb (2.0.0)
34
+ rack (1.5.2)
35
+ rack-perftools_profiler (0.6.0)
36
+ open4 (~> 1.0)
37
+ perftools.rb (~> 2.0.0)
38
+ rack (~> 1.0)
39
+ rack-test (0.6.2)
40
+ rack (>= 1.0)
41
+ rake (10.0.3)
42
+ rdoc (3.12.1)
43
+ json (~> 1.4)
44
+ redcarpet (2.2.2)
45
+ rr (1.0.4)
46
+ turn (0.9.6)
47
+ ansi
48
+ yajl-ruby (1.1.0)
49
+ yard (0.8.4.1)
50
+
51
+ PLATFORMS
52
+ ruby
53
+
54
+ DEPENDENCIES
55
+ rake
56
+ rr
57
+ vault-test-tools (~> 0.2.2)!
58
+ vault-usage-client!
data/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # Vault::Usage::Client
2
+
3
+ Client provides a Ruby API for interacting with the `Vault::Usage`
4
+ HTTP API.
5
+
6
+ ## Setting up a development environment
7
+
8
+ Install dependencies and run the test suite:
9
+
10
+ bundle install --binstubs vendor/bin
11
+ rbenv rehash
12
+ rake
13
+
14
+ Run tests:
15
+
16
+ rake test
17
+
18
+ See tasks:
19
+
20
+ rake -T
21
+
22
+ Generate API documentation:
23
+
24
+ rake yard
25
+
26
+ ## Using the client from the command-line
27
+
28
+ The `bin/vault-usage` command-line tool can be used to quickly see
29
+ usage data for a particular user:
30
+
31
+ ```bash
32
+ export VAULT_USAGE_URL=https://username:secret@vault-usage.herokuapp.com
33
+ bin/vault-usage user123@heroku.com 2013-01-01 2013-02-01
34
+ ```
35
+
36
+ ## Using the client in Ruby
37
+
38
+ The `Vault::Usage` API may only be accessed with valid credentials.
39
+ You must supply these when you create a new client:
40
+
41
+ ```ruby
42
+ require 'vault-usage-client'
43
+
44
+ client = Vault::Usage::Client::Client.new(
45
+ 'https://username:secret@vault-usage.herokuapp.com')
46
+ ```
47
+
48
+ ### Usage events
49
+
50
+ Usage events represent usage of a product, for a period of time, by an
51
+ app or user.
52
+
53
+ #### Opening a usage event
54
+
55
+ Each usage event must have a unique UUID provided by the service
56
+ reporting it and the start time must be in UTC:
57
+
58
+ ```ruby
59
+ event_id = SecureRandom.uuid
60
+ product_name = 'platform:dyno:logical'
61
+ consumer_hid = 'app1234@heroku.com'
62
+ start_time = Time.now.getutc
63
+ client.open_usage_event(event_id, product_name, consumer_hid, start_time)
64
+ ```
65
+
66
+ Arbitrary data related to the usage event can optionally be provided
67
+ by way of a detail object:
68
+
69
+ ```ruby
70
+ event_id = SecureRandom.uuid
71
+ product_name = 'platform:dyno:logical'
72
+ consumer_hid = 'app1234@heroku.com'
73
+ start_time = Time.now.getutc
74
+ detail = {type: 'web',
75
+ description: 'bundle exec bin/web',
76
+ kernel: 'us-east-1-a'}
77
+ client.open_usage_event(event_id, product_name, consumer_hid, start_time,
78
+ detail)
79
+ ```
80
+
81
+ Keys in the detail object must be of type `Symbol` and values may only
82
+ be of type `String`, `Fixnum`, `Bignum`, `Float`, `TrueClass`,
83
+ `FalseClass` or `NilClass`. In other words, it's not possible to use
84
+ nested structures in the detail object.
85
+
86
+ #### Closing a usage event
87
+
88
+ Closing a usage event works the same way as opening one:
89
+
90
+ ```ruby
91
+ event_id = SecureRandom.uuid
92
+ product_name = 'platform:dyno:logical'
93
+ consumer_hid = 'app1234@heroku.com'
94
+ stop_time = Time.now.getutc
95
+ client.close_event(event_id, product_name, consumer_hid, stop_time)
96
+ ```
97
+
98
+ #### Retrieving usage information
99
+
100
+ Usage information for a particular user can be retrieved by the
101
+ client. The start and stop time must both be specified in UTC:
102
+
103
+ ```ruby
104
+ user_hid = 'user1234@heroku.com'
105
+ start_time = Time.utc(2013, 1)
106
+ stop_time = Time.utc(2013, 2)
107
+ events = client.usage_for_user(user_hid, start_time, stop_time)
108
+ ```
109
+
110
+ The `events` result is an `Array` of objects matching this format:
111
+
112
+ ```ruby
113
+ [{id: '<event-uuid>',
114
+ product: '<name>',
115
+ consumer: '<consumer-hid>',
116
+ start_time: <Time>,
117
+ stop_time: <Time>,
118
+ detail: {<key1>: <value1>,
119
+ <key2>: <value2>,
120
+ ...}},
121
+ ...]}
122
+ ```
123
+
124
+ In some cases it can be useful to exclude event data for certain
125
+ products:
126
+
127
+ ```ruby
128
+ user_hid = 'user1234@heroku.com'
129
+ start_time = Time.utc(2013, 1)
130
+ stop_time = Time.utc(2013, 2)
131
+ events = client.usage_for_user(user_hid, start_time, stop_time,
132
+ ['platform:dyno:physical'])
133
+ ```
134
+
135
+ You can pass one or more product names to exclude.
136
+
137
+ ### App ownership events
138
+
139
+ App ownership events represent ownership of an app, for a period of
140
+ time, by a user.
141
+
142
+ #### Opening an app ownership event
143
+
144
+ Each app ownership event must have a unique UUID provided by the
145
+ service reporting it and the start time must be in UTC:
146
+
147
+ ```ruby
148
+ event_id = SecureRandom.uuid
149
+ user_hid = 'user1234@heroku.com'
150
+ app_hid = 'app1234@heroku.com'
151
+ start_time = Time.now.getutc
152
+ client.open_app_ownership_event(event_id, user_hid, app_hid, start_time)
153
+ ```
154
+
155
+ #### Closing an app ownership event
156
+
157
+ Closing an app ownership event works the same way as opening one:
158
+
159
+ ```ruby
160
+ event_id = SecureRandom.uuid
161
+ user_hid = 'user1234@heroku.com'
162
+ app_hid = 'app1234@heroku.com'
163
+ stop_time = Time.now.getutc
164
+ client.close_app_ownership_event(event_id, user_hid, app_hid, stop_time)
165
+ ```
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'yard'
3
+
4
+ desc "Test the things"
5
+ require 'vault-test-tools/rake_task'
6
+
7
+ desc "Doc the things"
8
+ YARD::Rake::YardocTask.new
9
+
10
+ desc "Run the test suite"
11
+ task :default => [:test]
data/bin/vault-usage ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path('../../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'vault-usage-client'
7
+ require 'colorize'
8
+
9
+ user_hid = ARGV[0]
10
+ start_time = Time.utc(*ARGV[1].split('-'))
11
+ stop_time = Time.utc(*ARGV[2].split('-'))
12
+
13
+ client = Vault::Usage::Client.new
14
+
15
+ all_events = client.usage_for_user(user_hid, start_time, stop_time, ['platform:dyno:physical'])[:events]
16
+
17
+ all_events.group_by { |e| e[:consumer] }.each do |consumer, app_events|
18
+ puts "=== #{consumer} ===".blue
19
+
20
+ app_events.group_by { |e| e[:product] }.each do |product, product_events|
21
+
22
+ puts " #{product}"
23
+ product_events.
24
+ map { |event| OpenStruct.new(event) }.
25
+ sort_by(&:start_time).
26
+ each do |e|
27
+ puts " #{e.id}".light_green
28
+ puts " #{e.start_time} -> #{e.stop_time}: #{e.detail}".green
29
+ if e.start_time > e.stop_time
30
+ puts " ERROR: start_time is after stop_time".red
31
+ end
32
+ puts
33
+ end
34
+
35
+ end
36
+ puts
37
+ end
@@ -0,0 +1,205 @@
1
+ module Vault::Usage
2
+ # Client for the `Vault::Usage` HTTP API.
3
+ class Client
4
+ attr_reader :url
5
+
6
+ # Raised if a non-UTC time is used with the client.
7
+ class InvalidTimeError < Exception
8
+ end
9
+
10
+ # Instantiate a client.
11
+ #
12
+ # @param url [String] The URL to connect to. Include the username and
13
+ # password to use when connecting.
14
+ def initialize(url = nil)
15
+ @url = url || ENV['VAULT_USAGE_URL']
16
+ end
17
+
18
+ # Report that usage of a product, by a user or app, started at a
19
+ # particular time.
20
+ #
21
+ # @param event_id [String] A UUID that uniquely identifies the usage
22
+ # event.
23
+ # @param product_name [String] The name of the product that was used, such
24
+ # as `platform:dyno:logical` or `addon:memcache:100mb`.
25
+ # @param consumer_hid [String] The Heroku ID, such as `app1234@heroku.com`
26
+ # or `user1234@heroku.com`, that represents the user or app that used
27
+ # the specified product.
28
+ # @param start_time [Time] The beginning of the usage period, always in
29
+ # UTC.
30
+ # @param detail [Hash] Optionally, additional details to store with the
31
+ # event. Keys must be of type `Symbol` and values may only be of type
32
+ # `String`, `Fixnum`, `Bignum`, `Float`, `TrueClass`, `FalseClass` or
33
+ # `NilClass`.
34
+ # @raise [InvalidTimeError] Raised if a non-UTC start time is provided.
35
+ # @raise [Excon::Errors::HTTPStatusError] Raised if the server returns an
36
+ # unsuccessful HTTP status code.
37
+ def open_usage_event(event_id, product_name, consumer_hid, start_time,
38
+ detail=nil)
39
+ unless start_time.zone.eql?('UTC')
40
+ raise InvalidTimeError.new('Start time must be in UTC.')
41
+ end
42
+ path = "/products/#{product_name}/usage/#{consumer_hid}" +
43
+ "/events/#{event_id}/open/#{iso_format(start_time)}"
44
+ unless detail.nil?
45
+ headers = {'Content-Type' => 'application/json'}
46
+ body = JSON.generate(detail)
47
+ end
48
+ connection = Excon.new(@url)
49
+ connection.put(path: path, headers: headers, body: body,
50
+ expects: [201])
51
+ end
52
+
53
+ # Report that usage of a product, by a user or app, stopped at a
54
+ # particular time.
55
+ #
56
+ # @param event_id [String] A UUID that uniquely identifies the usage
57
+ # event.
58
+ # @param product_name [String] The name of the product that was used, such
59
+ # as `platform:dyno:logical` or `addon:memcache:100mb`.
60
+ # @param consumer_hid [String] The Heroku ID, such as `app1234@heroku.com`
61
+ # or `user1234@heroku.com`, that represents the user or app that used
62
+ # the specified product.
63
+ # @param stop_time [Time] The end of the usage period, always in UTC.
64
+ # @raise [InvalidTimeError] Raised if a non-UTC stop time is provided.
65
+ # @raise [Excon::Errors::HTTPStatusError] Raised if the server returns an
66
+ # unsuccessful HTTP status code.
67
+ def close_usage_event(event_id, product_name, consumer_hid, stop_time)
68
+ unless stop_time.zone.eql?('UTC')
69
+ raise InvalidTimeError.new('Stop time must be in UTC.')
70
+ end
71
+ path = "/products/#{product_name}/usage/#{consumer_hid}" +
72
+ "/events/#{event_id}/close/#{iso_format(stop_time)}"
73
+ connection = Excon.new(@url)
74
+ connection.put(path: path, expects: [201])
75
+ end
76
+
77
+ # Get the usage events for the apps owned by the specified user during the
78
+ # specified period.
79
+ #
80
+ # @param user_hid [String] The user HID, such as `user1234@heroku.com`, to
81
+ # fetch usage data for.
82
+ # @param start_time [Time] The beginning of the usage period, always in
83
+ # UTC, within which events must overlap to be included in usage data.
84
+ # @param stop_time [Time] The end of the usage period, always in UTC,
85
+ # within which events must overlap to be included in usage data.
86
+ # @param exclude [Array] Optionally, a list of product names, such as
87
+ # `['platform:dyno:physical', 'addon:memcache:100mb']`, to be excluded
88
+ # from usage data.
89
+ # @raise [InvalidTimeError] Raised if a non-UTC start or stop time is
90
+ # provided.
91
+ # @raise [Excon::Errors::HTTPStatusError] Raised if the server returns an
92
+ # unsuccessful HTTP status code.
93
+ # @return [Array] A list of usage events for the specified user, matching
94
+ # the following format:
95
+ #
96
+ # ```
97
+ # [{id: '<event-uuid>',
98
+ # product: '<name>',
99
+ # consumer: '<heroku-id>',
100
+ # start_time: <Time>,
101
+ # stop_time: <Time>,
102
+ # detail: {<key1>: <value1>,
103
+ # <key2>: <value2>,
104
+ # ...}},
105
+ # ...]}
106
+ # ```
107
+ def usage_for_user(user_hid, start_time, stop_time, exclude=nil)
108
+ unless start_time.zone.eql?('UTC')
109
+ raise InvalidTimeError.new('Start time must be in UTC.')
110
+ end
111
+ unless stop_time.zone.eql?('UTC')
112
+ raise InvalidTimeError.new('Stop time must be in UTC.')
113
+ end
114
+ path = "/users/#{user_hid}/usage/#{iso_format(start_time)}/" +
115
+ "#{iso_format(stop_time)}"
116
+ unless exclude.nil? || exclude.empty?
117
+ query = {exclude: exclude.join(',')}
118
+ end
119
+ connection = Excon.new(@url)
120
+ response = connection.get(path: path, expects: [200], query: query)
121
+ events = JSON.parse(response.body, {symbolize_keys: true})
122
+ events.each do |event|
123
+ event.each do |key, value|
124
+ event[key] = parse_date(value) if date?(value)
125
+ end
126
+ end
127
+ end
128
+
129
+ # Report that ownership of an app by a user started at a particular time.
130
+ #
131
+ # @param event_id [String] A UUID that uniquely identifies the ownership
132
+ # event.
133
+ # @param user_hid [String] The user HID, such as `user1234@heroku.com`,
134
+ # that owns the specified app.
135
+ # @param app_hid [String] The app HID, such as `app1234@heroku.com`, that
136
+ # is being transferred to the specified user.
137
+ # @param start_time [Time] The beginning of the ownership period, always
138
+ # in UTC.
139
+ # @raise [InvalidTimeError] Raised if a non-UTC start time is provided.
140
+ # @raise [Excon::Errors::HTTPStatusError] Raised if the server returns an
141
+ # unsuccessful HTTP status code.
142
+ def open_app_ownership_event(event_id, user_hid, app_hid, start_time)
143
+ unless start_time.zone.eql?('UTC')
144
+ raise InvalidTimeError.new('Start time must be in UTC.')
145
+ end
146
+ path = "/users/#{user_hid}/apps/#{app_hid}/open/#{event_id}" +
147
+ "/#{iso_format(start_time)}"
148
+ connection = Excon.new(@url)
149
+ connection.put(path: path, expects: [201])
150
+ end
151
+
152
+ # Report that ownership of an app by a user stopped at a particular time.
153
+ #
154
+ # @param event_id [String] A UUID that uniquely identifies the ownership
155
+ # event.
156
+ # @param user_hid [String] The user HID, such as `user1234@heroku.com`,
157
+ # that owned the specified app.
158
+ # @param app_hid [String] The app HID, such as `app1234@heroku.com`, that
159
+ # is being transferred away from the specified user.
160
+ # @param stop_time [Time] The end of the ownership period, always in UTC.
161
+ # @raise [InvalidTimeError] Raised if a non-UTC stop time is provided.
162
+ # @raise [Excon::Errors::HTTPStatusError] Raised if the server returns an
163
+ # unsuccessful HTTP status code.
164
+ def close_app_ownership_event(event_id, user_hid, app_hid, stop_time)
165
+ unless stop_time.zone.eql?('UTC')
166
+ raise InvalidTimeError.new('Stop time must be in UTC.')
167
+ end
168
+ path = "/users/#{user_hid}/apps/#{app_hid}/close/#{event_id}" +
169
+ "/#{iso_format(stop_time)}"
170
+ connection = Excon.new(@url)
171
+ connection.put(path: path, expects: [201])
172
+ end
173
+
174
+ private
175
+
176
+ # Convert a time to an ISO 8601 combined data and time format.
177
+ #
178
+ # @param time [Time] The time to convert to ISO 8601 format.
179
+ # @return [String] An ISO 8601 date in `YYYY-MM-DDTHH:MM:SSZ` format.
180
+ def iso_format(time)
181
+ time.strftime('%Y-%m-%dT%H:%M:%SZ')
182
+ end
183
+
184
+ # Determine if the value is an ISO 8601 date in `YYYY-MM-DDTHH:MM:SSZ`
185
+ # format.
186
+ #
187
+ # @param value [String] The value to check for date-ness.
188
+ # @return [TrueClass,FalseClass] True if the value resembles a date,
189
+ # otherwise false.
190
+ def date?(value)
191
+ value =~ /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/
192
+ end
193
+
194
+ # Parse an ISO 8601 date in `YYYY-MM-DDTHH:MM:SSZ` format into a Time
195
+ # instance.
196
+ #
197
+ # @param value [String] The value to parse.
198
+ # @raise [ArgumentError] Raised if the value doesn't represent a valid
199
+ # date.
200
+ # @return [Time] The Time instance created from the date string in UTC.
201
+ def parse_date(value)
202
+ DateTime.parse(value).to_time.getutc
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,8 @@
1
+ module Vault
2
+ module Usage
3
+ class Client
4
+ # The `Vault::Usage::Client` gem version.
5
+ VERSION = '0.0.1'
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ require 'bundler'
2
+ Bundler.require :default, ENV['RACK_ENV'].to_sym
3
+
4
+ require 'excon'
5
+ require 'yajl/json_gem'
6
+
7
+ module Vault
8
+ module Usage
9
+ # Client provides a Ruby API to access the Vault::Usage HTTP API.
10
+ class Client
11
+ end
12
+ end
13
+ end
14
+
15
+ require 'vault-usage-client/client'
16
+ require 'vault-usage-client/version'
@@ -0,0 +1,338 @@
1
+ require 'helper'
2
+
3
+ class ClientTest < Vault::TestCase
4
+ include Vault::Test::EnvironmentHelpers
5
+
6
+ def setup
7
+ super
8
+ Excon.stubs.clear
9
+ Excon.defaults[:mock] = true
10
+ @client = Vault::Usage::Client.new(
11
+ 'https://username:secret@vault-usage.herokuapp.com')
12
+ @event_id = 'd8bb95d1-6279-4488-961d-133514b772fa'
13
+ @product_name = 'platform:dyno:logical'
14
+ @app_hid = 'app123@heroku.com'
15
+ @user_hid = 'user456@heroku.com'
16
+ @start_time = Time.utc(2013, 1)
17
+ @stop_time = Time.utc(2013, 2)
18
+ end
19
+
20
+ def teardown
21
+ # FIXME This is a bit ugly, but Excon doesn't provide a builtin way to
22
+ # ensure that a request was invoked, so we have to do it ourselves.
23
+ # Without this, and the Excon.stubs.pop calls in the tests below, tests
24
+ # will pass if request logic is completely removed from application
25
+ # code. -jkakar
26
+ assert(Excon.stubs.empty?, 'Expected HTTP requests were not made.')
27
+ super
28
+ end
29
+
30
+ # Convert a time to an ISO 8601 combined data and time format.
31
+ def iso_format(time)
32
+ time.strftime('%Y-%m-%dT%H:%M:%SZ')
33
+ end
34
+
35
+ # Client.new looks for VAULT_USAGE_URL if none is passed in to the constructor
36
+ def test_uses_env_for_url_if_not_given
37
+ url = 'http://foo:bar@example.com'
38
+ set_env 'VAULT_USAGE_URL', url
39
+ @client = Vault::Usage::Client.new
40
+ assert_equal(url, @client.url)
41
+ end
42
+
43
+ # Client.open_usage_event makes a PUT request to the Vault::Usage HTTP API,
44
+ # passing the supplied credentials using HTTP basic auth, to report that
45
+ # usage of a product began at a particular time.
46
+ def test_open_usage_event
47
+ Excon.stub(method: :put) do |request|
48
+ assert_equal('Basic dXNlcm5hbWU6c2VjcmV0',
49
+ request[:headers]['Authorization'])
50
+ assert_equal('vault-usage.herokuapp.com:443', request[:host_port])
51
+ assert_equal("/products/#{@product_name}/usage/#{@app_hid}" +
52
+ "/events/#{@event_id}/open/#{iso_format(@start_time)}",
53
+ request[:path])
54
+ Excon.stubs.pop
55
+ {status: 201}
56
+ end
57
+ @client.open_usage_event(@event_id, @product_name, @app_hid, @start_time)
58
+ end
59
+
60
+ # Client.open_usage_event optionally accepts a detail hash which is sent as
61
+ # a JSON payload in the request body when provided.
62
+ def test_open_usage_event_with_detail
63
+ detail = {type: 'web',
64
+ description: 'bundle exec bin/web',
65
+ kernel: 'us-east-1-a'}
66
+ Excon.stub(method: :put) do |request|
67
+ assert_equal('application/json', request[:headers]['Content-Type'])
68
+ assert_equal(detail, JSON.parse(request[:body], {symbolize_keys: true}))
69
+ Excon.stubs.pop
70
+ {status: 201}
71
+ end
72
+ @client.open_usage_event(@event_id, @product_name, @app_hid, @start_time,
73
+ detail)
74
+ end
75
+
76
+ # Client.open_usage_event raises an InvalidTimeError if the start time is
77
+ # not in UTC.
78
+ def test_open_usage_event_with_non_utc_start_time
79
+ start_time = Time.new(2013, 1, 12, 15, 25, 0, '+09:00')
80
+ error = assert_raises Vault::Usage::Client::InvalidTimeError do
81
+ @client.open_usage_event(@event_id, @product_name, @app_hid, start_time)
82
+ end
83
+ assert_equal('Start time must be in UTC.', error.message)
84
+ end
85
+
86
+ # Client.open_usage_event raises the appropriate
87
+ # Excon::Errors::HTTPStatusError if an unsuccessful HTTP status code is
88
+ # returned by the server.
89
+ def test_open_usage_event_with_unsuccessful_response
90
+ Excon.stub(method: :put) do |request|
91
+ Excon.stubs.pop
92
+ {status: 400, body: 'Bad inputs provided.'}
93
+ end
94
+ assert_raises Excon::Errors::BadRequest do
95
+ @client.open_usage_event(@event_id, @product_name, @app_hid, @start_time)
96
+ end
97
+ end
98
+
99
+ # Client.close_usage_event makes a PUT request to the Vault::Usage HTTP
100
+ # API, passing the supplied credentials using HTTP basic auth, to report
101
+ # that usage of a product ended at a particular time.
102
+ def test_close_usage_event
103
+ Excon.stub(method: :put) do |request|
104
+ assert_equal('Basic dXNlcm5hbWU6c2VjcmV0',
105
+ request[:headers]['Authorization'])
106
+ assert_equal('vault-usage.herokuapp.com:443', request[:host_port])
107
+ assert_equal("/products/#{@product_name}/usage/#{@app_hid}" +
108
+ "/events/#{@event_id}/close/#{iso_format(@stop_time)}",
109
+ request[:path])
110
+ Excon.stubs.pop
111
+ {status: 201}
112
+ end
113
+ @client.close_usage_event(@event_id, @product_name, @app_hid, @stop_time)
114
+ end
115
+
116
+ # Client.close_usage_event raises an InvalidTimeError if the stop time is
117
+ # not in UTC.
118
+ def test_close_usage_event_with_non_utc_stop_time
119
+ stop_time = Time.new(2013, 1, 12, 15, 25, 0, '+09:00')
120
+ error = assert_raises Vault::Usage::Client::InvalidTimeError do
121
+ @client.close_usage_event(@event_id, @product_name, @app_hid, stop_time)
122
+ end
123
+ assert_equal('Stop time must be in UTC.', error.message)
124
+ end
125
+
126
+ # Client.close_usage_event raises the appropriate
127
+ # Excon::Errors::HTTPStatusError if an unsuccessful HTTP status code is
128
+ # returned by the server.
129
+ def test_close_usage_event_with_unsuccessful_response
130
+ Excon.stub(method: :put) do |request|
131
+ Excon.stubs.pop
132
+ {status: 400, body: 'Bad inputs provided.'}
133
+ end
134
+ assert_raises Excon::Errors::BadRequest do
135
+ @client.close_usage_event(@event_id, @product_name, @app_hid, @stop_time)
136
+ end
137
+ end
138
+
139
+ # Client.usage_for_user makes a GET request to the Vault::Usage HTTP API,
140
+ # passing the supplied credentials using HTTP basic auth, to retrieve the
141
+ # usage events for a particular user that occurred during the specified
142
+ # period of time.
143
+ def test_usage_for_user
144
+ Excon.stub(method: :get) do |request|
145
+ assert_equal('Basic dXNlcm5hbWU6c2VjcmV0',
146
+ request[:headers]['Authorization'])
147
+ assert_equal('vault-usage.herokuapp.com:443', request[:host_port])
148
+ assert_equal("/users/#{@user_hid}/usage/#{iso_format(@start_time)}/" +
149
+ "#{iso_format(@stop_time)}",
150
+ request[:path])
151
+ Excon.stubs.pop
152
+ {status: 200, body: JSON.generate([])}
153
+ end
154
+ assert_equal([], @client.usage_for_user(@user_hid, @start_time,
155
+ @stop_time))
156
+ end
157
+
158
+ # Client.usage_for_user optionally accepts a list of products to exclude.
159
+ # They're passed to the server using query string arguments.
160
+ def test_usage_for_user_with_exclude
161
+ Excon.stub(method: :get) do |request|
162
+ assert_equal({exclude: 'platform:dyno:physical'}, request[:query])
163
+ Excon.stubs.pop
164
+ {status: 200, body: JSON.generate([])}
165
+ end
166
+ assert_equal([], @client.usage_for_user(@user_hid, @start_time, @stop_time,
167
+ ['platform:dyno:physical']))
168
+ end
169
+
170
+ # Client.usage_for_user comma-separates product names, when many are
171
+ # provided in the exclusion list, and passes them to the server using a
172
+ # single query argument.
173
+ def test_usage_for_user_with_many_excludes
174
+ Excon.stub(method: :get) do |request|
175
+ assert_equal({exclude: 'platform:dyno:physical,addons:memcache:100mb'},
176
+ request[:query])
177
+ Excon.stubs.pop
178
+ {status: 200, body: JSON.generate([])}
179
+ end
180
+ assert_equal([], @client.usage_for_user(@user_hid, @start_time, @stop_time,
181
+ ['platform:dyno:physical',
182
+ 'addons:memcache:100mb']))
183
+ end
184
+
185
+ # Client.usage_for_user with an empty product exclusion list is the same as
186
+ # not passing one at all.
187
+ def test_usage_for_user_with_empty_exclude
188
+ Excon.stub(method: :get) do |request|
189
+ assert_equal(nil, request[:query])
190
+ Excon.stubs.pop
191
+ {status: 200, body: JSON.generate([])}
192
+ end
193
+ assert_equal([], @client.usage_for_user(@user_hid, @start_time, @stop_time,
194
+ []))
195
+ end
196
+
197
+ # Client.usage_for_user converts event start and stop times to Time
198
+ # instances.
199
+ def test_usage_for_user_converts_times
200
+ Excon.stub(method: :get) do |request|
201
+ Excon.stubs.pop
202
+ events = [{id: @event_id,
203
+ product: @product_name,
204
+ consumer: @app_hid,
205
+ start_time: iso_format(@start_time),
206
+ stop_time: iso_format(@stop_time),
207
+ detail: {}}]
208
+ {status: 200, body: JSON.generate(events)}
209
+ end
210
+ assert_equal([{id: @event_id,
211
+ product: @product_name,
212
+ consumer: @app_hid,
213
+ start_time: @start_time,
214
+ stop_time: @stop_time,
215
+ detail: {}}],
216
+ @client.usage_for_user(@user_hid, @start_time, @stop_time))
217
+ end
218
+
219
+ # Client.usage_for_user raises an InvalidTimeError if the start time is not
220
+ # in UTC.
221
+ def test_usage_for_user_with_non_utc_start_time
222
+ start_time = Time.new(2013, 1, 12, 15, 25, 0, '+09:00')
223
+ error = assert_raises Vault::Usage::Client::InvalidTimeError do
224
+ @client.usage_for_user(@user_hid, start_time, @stop_time)
225
+ end
226
+ assert_equal('Start time must be in UTC.', error.message)
227
+ end
228
+
229
+ # Client.usage_for_user raises an InvalidTimeError if the stop time is not
230
+ # in UTC.
231
+ def test_usage_for_user_with_non_utc_stop_time
232
+ stop_time = Time.new(2013, 1, 12, 15, 25, 0, '+09:00')
233
+ error = assert_raises Vault::Usage::Client::InvalidTimeError do
234
+ @client.usage_for_user(@user_hid, @start_time, stop_time)
235
+ end
236
+ assert_equal('Stop time must be in UTC.', error.message)
237
+ end
238
+
239
+ # Client.usage_for_user raises the appropriate Excon::Errors::HTTPStatusError
240
+ # if an unsuccessful HTTP status code is returned by the server.
241
+ def test_usage_for_user_with_unsuccessful_response
242
+ Excon.stub(method: :get) do |request|
243
+ Excon.stubs.pop
244
+ {status: 400, body: 'Bad inputs provided.'}
245
+ end
246
+ assert_raises Excon::Errors::BadRequest do
247
+ @client.usage_for_user(@user_hid, @start_time, @stop_time)
248
+ end
249
+ end
250
+
251
+ # Client.open_app_ownership_event makes a PUT request to the Vault::Usage
252
+ # HTTP API, passing the supplied credentials using HTTP basic auth, to
253
+ # report that ownership of an app, by a particular user, began at a
254
+ # particular time.
255
+ def test_open_app_ownership_event
256
+ Excon.stub(method: :put) do |request|
257
+ assert_equal('Basic dXNlcm5hbWU6c2VjcmV0',
258
+ request[:headers]['Authorization'])
259
+ assert_equal('vault-usage.herokuapp.com:443', request[:host_port])
260
+ assert_equal("/users/#{@user_hid}/apps/#{@app_hid}/open/#{@event_id}" +
261
+ "/#{iso_format(@start_time)}",
262
+ request[:path])
263
+ Excon.stubs.pop
264
+ {status: 201}
265
+ end
266
+ @client.open_app_ownership_event(@event_id, @user_hid, @app_hid,
267
+ @start_time)
268
+ end
269
+
270
+ # Client.open_app_ownership_event raises an InvalidTimeError if the start
271
+ # time is not in UTC.
272
+ def test_open_app_ownership_event_with_non_utc_start_time
273
+ start_time = Time.new(2013, 1, 12, 15, 25, 0, '+09:00')
274
+ error = assert_raises Vault::Usage::Client::InvalidTimeError do
275
+ @client.open_app_ownership_event(@event_id, @user_hid, @app_hid,
276
+ start_time)
277
+ end
278
+ assert_equal('Start time must be in UTC.', error.message)
279
+ end
280
+
281
+ # Client.open_app_ownership_event raises the appropriate
282
+ # Excon::Errors::HTTPStatusError if an unsuccessful HTTP status code is
283
+ # returned by the server.
284
+ def test_open_app_ownership_event_with_unsuccessful_response
285
+ Excon.stub(method: :put) do |request|
286
+ Excon.stubs.pop
287
+ {status: 400, body: 'Bad inputs provided.'}
288
+ end
289
+ assert_raises Excon::Errors::BadRequest do
290
+ @client.open_app_ownership_event(@event_id, @user_hid, @app_hid,
291
+ @start_time)
292
+ end
293
+ end
294
+
295
+ # Client.close_app_ownership_event makes a PUT request to the Vault::Usage
296
+ # HTTP API, passing the supplied credentials using HTTP basic auth, to
297
+ # report that ownership of an app, by a particular user, began at a
298
+ # particular time.
299
+ def test_close_app_ownership_event
300
+ Excon.stub(method: :put) do |request|
301
+ assert_equal('Basic dXNlcm5hbWU6c2VjcmV0',
302
+ request[:headers]['Authorization'])
303
+ assert_equal('vault-usage.herokuapp.com:443', request[:host_port])
304
+ assert_equal("/users/#{@user_hid}/apps/#{@app_hid}/close/#{@event_id}" +
305
+ "/#{iso_format(@stop_time)}",
306
+ request[:path])
307
+ Excon.stubs.pop
308
+ {status: 201}
309
+ end
310
+ @client.close_app_ownership_event(@event_id, @user_hid, @app_hid,
311
+ @stop_time)
312
+ end
313
+
314
+ # Client.close_app_ownership_event raises an InvalidTimeError if the stop
315
+ # time is not in UTC.
316
+ def test_close_app_ownership_event_with_non_utc_stop_time
317
+ stop_time = Time.new(2013, 1, 12, 15, 25, 0, '+09:00')
318
+ error = assert_raises Vault::Usage::Client::InvalidTimeError do
319
+ @client.close_app_ownership_event(@event_id, @user_hid, @app_hid,
320
+ stop_time)
321
+ end
322
+ assert_equal('Stop time must be in UTC.', error.message)
323
+ end
324
+
325
+ # Client.close_app_ownership_event raises the appropriate
326
+ # Excon::Errors::HTTPStatusError if an unsuccessful HTTP status code is
327
+ # returned by the server.
328
+ def test_close_app_ownership_event_with_unsuccessful_response
329
+ Excon.stub(method: :put) do |request|
330
+ Excon.stubs.pop
331
+ {status: 400, body: 'Bad inputs provided.'}
332
+ end
333
+ assert_raises Excon::Errors::BadRequest do
334
+ @client.close_app_ownership_event(@event_id, @user_hid, @app_hid,
335
+ @stop_time)
336
+ end
337
+ end
338
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,4 @@
1
+ ENV['RACK_ENV'] = 'test'
2
+
3
+ require 'vault-usage-client'
4
+ require 'vault-test-tools'
@@ -0,0 +1,9 @@
1
+ require 'helper'
2
+
3
+ class VersionTest < Vault::TestCase
4
+ # Vault::Usage::Client::VERSION is a string matching the `major.minor.patch`
5
+ # format.
6
+ def test_version
7
+ assert_match(/\d+\.\d+\.\d+/, Vault::Usage::Client::VERSION)
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'vault-usage-client/version'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = "vault-usage-client"
7
+ gem.version = Vault::Usage::Client::VERSION
8
+ gem.authors = ["Chris Continanza", "Jamu Kakar"]
9
+ gem.email = ["csquared@heroku.com", "jkakar@heroku.com"]
10
+ gem.description = "Client for Vault::Usage"
11
+ gem.summary = "A simple wrapper around the Vault::Usage HTTP API"
12
+ gem.homepage = "https://github.com/heroku/vault-usage-client"
13
+
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.test_files = gem.files.grep('^(test|spec|features)/')
16
+ gem.require_paths = ["lib"]
17
+
18
+ gem.add_dependency 'excon'
19
+ gem.add_dependency 'yajl-ruby'
20
+ gem.add_dependency 'colorize'
21
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vault-usage-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Chris Continanza
9
+ - Jamu Kakar
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-02-21 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: excon
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ - !ruby/object:Gem::Dependency
32
+ name: yajl-ruby
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: colorize
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ description: Client for Vault::Usage
64
+ email:
65
+ - csquared@heroku.com
66
+ - jkakar@heroku.com
67
+ executables: []
68
+ extensions: []
69
+ extra_rdoc_files: []
70
+ files:
71
+ - .gitignore
72
+ - .yardopts
73
+ - Gemfile
74
+ - Gemfile.lock
75
+ - README.md
76
+ - Rakefile
77
+ - bin/vault-usage
78
+ - lib/vault-usage-client.rb
79
+ - lib/vault-usage-client/client.rb
80
+ - lib/vault-usage-client/version.rb
81
+ - test/client_test.rb
82
+ - test/helper.rb
83
+ - test/version_test.rb
84
+ - vault-usage-client.gemspec
85
+ homepage: https://github.com/heroku/vault-usage-client
86
+ licenses: []
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ segments:
98
+ - 0
99
+ hash: 3440025206376780543
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ! '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ segments:
107
+ - 0
108
+ hash: 3440025206376780543
109
+ requirements: []
110
+ rubyforge_project:
111
+ rubygems_version: 1.8.23
112
+ signing_key:
113
+ specification_version: 3
114
+ summary: A simple wrapper around the Vault::Usage HTTP API
115
+ test_files: []
116
+ has_rdoc: