vault-usage-client 0.0.1

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.
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: