usps-imis-api 0.4.3 → 0.4.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e49829b6dee8973f1de4b275516060ced7b426cedd00fe7e4ccaba5e29efc52
4
- data.tar.gz: 8558346d33b416f3d58f9fa5047b925f580c8d8d5123463c6fe137335ddd93d7
3
+ metadata.gz: 602e95212148a4087db86d19ff47a8680d8a83a488331129588292150a00b0f2
4
+ data.tar.gz: 95ef0ede88acad9e0b66b5ac1b1bce88801604229f6a54b848ca4349fca34536
5
5
  SHA512:
6
- metadata.gz: 89f31684df0cb6001fa31966eec99bf8ec9e9dbdab86ae8213a43200f23bf306b204bc9ecb61d9f6ac066869387e4ba048923af1a77b4c7dd1dec59a9cb9b215
7
- data.tar.gz: 376cc34e675f9caadd5c985733b28ea2e93d4ba3752de8d9fd843d3a15866280f3075138a275be47dfd77d06bb24f2ba81cad8527308060a1e8893befbe5381e
6
+ metadata.gz: 8be9f80db96ff34ca7a40f459825a0537a4e1007a1ac6939eefae392133cbdca479b0b11099b0daab2c59690f83b9cfd7a0dfbbb9ed6e0b55090466bc3239f30
7
+ data.tar.gz: 7b502a0e9a7f7ce84ae79b17c1a945e2b01680dd7c4a4ff583d08bb6d08db32f6e6537c4c2d05f2e99ffb342cd1d35079119c38d7e750bd075992b59737d1a4f
data/.gitignore CHANGED
@@ -2,3 +2,4 @@ coverage/
2
2
  tmp/
3
3
  .env
4
4
  *.gem
5
+ .yardoc
data/.simplecov ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ SimpleCov.start do
4
+ enable_coverage :branch
5
+ primary_coverage :branch
6
+
7
+ add_filter '/spec'
8
+ end
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- usps-imis-api (0.4.3)
4
+ usps-imis-api (0.4.5)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/lib/usps/imis/api.rb CHANGED
@@ -2,14 +2,40 @@
2
2
 
3
3
  module Usps
4
4
  module Imis
5
+ # The core API wrapper
6
+ #
5
7
  class Api
8
+ # Endpoint for (re-)authentication requests
9
+ #
6
10
  AUTHENTICATION_PATH = 'Token'
11
+
12
+ # Endpoint for general API requests
13
+ #
7
14
  API_PATH = 'api'
15
+
16
+ # Endpoint for IQA query requests
17
+ #
8
18
  QUERY_PATH = 'api/Query'
9
- PANELS = Struct.new(:vsc, :education)
10
19
 
11
- attr_reader :token, :token_expiration, :imis_id
20
+ # API bearer token
21
+ #
22
+ attr_reader :token
23
+
24
+ # Expiration time for the API bearer token
25
+ #
26
+ # Used to automatically re-authenticate as needed
27
+ #
28
+ attr_reader :token_expiration
29
+
30
+ # Currently selected iMIS ID for API requests
31
+ #
32
+ attr_reader :imis_id
12
33
 
34
+ # A new instance of +Api+
35
+ #
36
+ # @param skip_authentication [bool] Skip authentication on initialization (used for tests)
37
+ # @param imis_id [Integer, String] iMIS ID to select immediately on initialization
38
+ #
13
39
  def initialize(skip_authentication: false, imis_id: nil)
14
40
  authenticate unless skip_authentication
15
41
  self.imis_id = imis_id if imis_id
@@ -17,12 +43,18 @@ module Usps
17
43
 
18
44
  # Manually set the current ID, if you already have it for a given member
19
45
  #
46
+ # @param id [Integer, String] iMIS ID to select for future requests
47
+ #
20
48
  def imis_id=(id)
21
49
  @imis_id = id.to_i.to_s
22
50
  end
23
51
 
24
52
  # Convert a member's certificate number into an iMIS ID number
25
53
  #
54
+ # @param certificate [String] Certificate number to lookup the corresponding iMIS ID for
55
+ #
56
+ # @return [String] Corresponding iMIS ID
57
+ #
26
58
  def imis_id_for(certificate)
27
59
  result = query(Imis.configuration.imis_id_query_name, { certificate: })
28
60
  @imis_id = result['Items']['$values'][0]['ID']
@@ -34,6 +66,13 @@ module Usps
34
66
  #
35
67
  # This should be used with methods that do not change the value of `imis_id`
36
68
  #
69
+ # @param id [Integer, String] iMIS ID to select for requests within the block
70
+ #
71
+ # @example
72
+ # with(12345) do
73
+ # update(mm: 15)
74
+ # end
75
+ #
37
76
  def with(id, &)
38
77
  old_id = imis_id
39
78
  self.imis_id = id
@@ -44,6 +83,11 @@ module Usps
44
83
 
45
84
  # Get a business object for the current member
46
85
  #
86
+ # @param business_object_name [String] Name of the business object
87
+ # @param url_id [String] Override the ID param of the URL (e.g. used for Panels)
88
+ #
89
+ # @return [Hash] Response data from the API
90
+ #
47
91
  def get(business_object_name, url_id: nil)
48
92
  uri = uri_for(business_object_name, url_id:)
49
93
  request = Net::HTTP::Get.new(uri)
@@ -53,7 +97,11 @@ module Usps
53
97
 
54
98
  # Update only specific fields on a business object for the current member
55
99
  #
56
- # fields - hash of shape: { field_name => new_value }
100
+ # @param business_object_name [String] Name of the business object
101
+ # @param fields [Hash] Conforms to pattern +{ field_key => value }+
102
+ # @param url_id [String] Override the ID param of the URL (e.g. used for Panels)
103
+ #
104
+ # @return [Hash] Response data from the API
57
105
  #
58
106
  def put_fields(business_object_name, fields, url_id: nil)
59
107
  updated = filter_fields(business_object_name, fields)
@@ -62,6 +110,12 @@ module Usps
62
110
 
63
111
  # Update a business object for the current member
64
112
  #
113
+ # @param business_object_name [String] Name of the business object
114
+ # @param body [Hash] Full raw API object data
115
+ # @param url_id [String] Override the ID param of the URL (e.g. used for Panels)
116
+ #
117
+ # @return [Hash] Response data from the API
118
+ #
65
119
  def put(business_object_name, body, url_id: nil)
66
120
  uri = uri_for(business_object_name, url_id:)
67
121
  request = Net::HTTP::Put.new(uri)
@@ -72,6 +126,12 @@ module Usps
72
126
 
73
127
  # Create a business object for the current member
74
128
  #
129
+ # @param business_object_name [String] Name of the business object
130
+ # @param body [Hash] Full raw API object data
131
+ # @param url_id [String] Override the ID param of the URL (e.g. used for Panels)
132
+ #
133
+ # @return [Hash] Response data from the API
134
+ #
75
135
  def post(business_object_name, body, url_id: nil)
76
136
  uri = uri_for(business_object_name, url_id:)
77
137
  request = Net::HTTP::Post.new(uri)
@@ -82,7 +142,10 @@ module Usps
82
142
 
83
143
  # Remove a business object for the current member
84
144
  #
85
- # Returns empty string on success
145
+ # @param business_object_name [String] Name of the business object
146
+ # @param url_id [String] Override the ID param of the URL (e.g. used for Panels)
147
+ #
148
+ # @return [String] Error response body from the API, or empty string on success
86
149
  #
87
150
  def delete(business_object_name, url_id: nil)
88
151
  uri = uri_for(business_object_name, url_id:)
@@ -93,8 +156,10 @@ module Usps
93
156
 
94
157
  # Run an IQA Query
95
158
  #
96
- # query_name - the full path of the query in IQA, e.g. `$/_ABC/Fiander/iMIS_ID`
97
- # query_params - hash of shape: { param_name => param_value }
159
+ # @param query_name [String] Full path of the query in IQA, e.g. +$/_ABC/Fiander/iMIS_ID+
160
+ # @query_params [Hash] Conforms to pattern +{ param_name => param_value }+
161
+ #
162
+ # @return [Hash] Response data from the API
98
163
  #
99
164
  def query(query_name, query_params = {})
100
165
  query_params[:QueryName] = query_name
@@ -105,21 +170,30 @@ module Usps
105
170
  JSON.parse(result.body)
106
171
  end
107
172
 
173
+ # An instance of Mapper, using this instance as its parent +Api+
174
+ #
108
175
  def mapper
109
176
  @mapper ||= Mapper.new(self)
110
177
  end
111
178
 
179
+ # Convenience accessor for available Panel objects, each using this instance as its parent
180
+ # +Api+
181
+ #
112
182
  def panels
113
- @panels ||= PANELS.new(
183
+ @panels ||= Struct.new(:vsc, :education).new(
114
184
  Panel::Vsc.new(self),
115
185
  Panel::Education.new(self)
116
186
  )
117
187
  end
118
188
 
189
+ # Convenience alias for updating mapped fields
190
+ #
119
191
  def update(data)
120
192
  mapper.update(data)
121
193
  end
122
194
 
195
+ # Ruby 3.5 instance variable filter
196
+ #
123
197
  def instance_variables_to_inspect = %i[@token_expiration @imis_id]
124
198
 
125
199
  private
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Usps
4
4
  module Imis
5
+ # API Configuration
6
+ #
5
7
  class Config
6
8
  IMIS_ROOT_URL_PROD = 'https://portal.americasboatingclub.org'
7
9
  IMIS_ROOT_URL_DEV = 'https://abcdev.imiscloud.com'
@@ -12,6 +14,10 @@ module Usps
12
14
  yield self if block_given?
13
15
  end
14
16
 
17
+ # Environment-specific API endpoint hostname
18
+ #
19
+ # @return The API hostname for the current environment
20
+ #
15
21
  def hostname
16
22
  case environment.to_sym
17
23
  when :production
@@ -23,6 +29,8 @@ module Usps
23
29
  end
24
30
  end
25
31
 
32
+ # Ruby 3.5 instance variable filter
33
+ #
26
34
  def instance_variables_to_inspect = %i[@environment @imis_id_query_name @username]
27
35
  end
28
36
  end
@@ -3,24 +3,40 @@
3
3
  module Usps
4
4
  module Imis
5
5
  module Error
6
+ # Base error class for all internal exceptions
7
+ #
6
8
  class Api < StandardError
9
+ # Additional call-specific metadata to pass through to Bugsnag
10
+ #
7
11
  attr_accessor :metadata
8
12
 
13
+ # A new instance of +Error::Api+
14
+ #
15
+ # @param message [String] The base exception message
16
+ # @param metadata [Hash] Additional call-specific metadata to pass through to Bugsnag
17
+ #
9
18
  def initialize(message, metadata = {})
10
19
  super(message)
11
20
  @metadata = metadata
12
-
13
- warn inspect if ENV.fetch('IMIS_ERROR_LOG_TO_STDERR', 'false') == 'true'
14
21
  end
15
22
 
23
+ # Additional metadata to include in Bugsnag reports
24
+ #
25
+ # Can include fields at the top level, which will be shows on the custom tab
26
+ #
27
+ # Can include fields nested under a top-level key, which will be shown on a tab with the
28
+ # top-level key as its name
29
+ #
30
+ # @return [Hash]
31
+ #
16
32
  def bugsnag_meta_data
17
- metadata == {} ? {} : { api: metadata }
33
+ metadata == {} ? {} : base_metadata
18
34
  end
19
35
 
20
36
  private
21
37
 
22
38
  def base_metadata
23
- { api: {} }
39
+ { api: metadata }
24
40
  end
25
41
  end
26
42
  end
@@ -3,6 +3,8 @@
3
3
  module Usps
4
4
  module Imis
5
5
  module Error
6
+ # Exception raised by a +Mapper+
7
+ #
6
8
  class Mapper < Api; end
7
9
  end
8
10
  end
@@ -3,23 +3,53 @@
3
3
  module Usps
4
4
  module Imis
5
5
  module Error
6
+ # Exception raised due to receiving an error response from the API
7
+ #
6
8
  class Response < Api
9
+ # [Net::HTTPResponse] The response received from the API
10
+ #
7
11
  attr_reader :response
12
+
13
+ # [Hash] Additional call-specific metadata to pass through to Bugsnag
14
+ #
8
15
  attr_accessor :metadata
9
16
 
17
+ # Create a new instance of +Error::Response+ from an API response
18
+ #
19
+ # @param response [Net::HTTPResponse] The response received from the API
20
+ #
10
21
  def self.from(response)
11
22
  new(nil, response)
12
23
  end
13
24
 
25
+ # Create a new instance of +Error::Response+
26
+ #
27
+ # @param _message Ignored
28
+ # @param response [Net::HTTPResponse] The response received from the API
29
+ # @param metadata [Hash] Additional call-specific metadata to pass through to Bugsnag
30
+ #
14
31
  def initialize(_message, response, metadata = {})
15
32
  @response = response
16
33
  super(message, metadata)
17
34
  end
18
35
 
36
+ # Additional metadata to include in Bugsnag reports
37
+ #
38
+ # Can include fields at the top level, which will be shows on the +custom+ tab
39
+ #
40
+ # Can include fields nested under a top-level key, which will be shown on a tab with the
41
+ # top-level key as its name
42
+ #
43
+ # @return [Hash]
44
+ #
19
45
  def bugsnag_meta_data
20
46
  base_metadata.tap { |m| m[:api].merge!(metadata) }
21
47
  end
22
48
 
49
+ # Auto-formatted exception message, based on the provided API response
50
+ #
51
+ # @return [String] The exception message
52
+ #
23
53
  def message
24
54
  [
25
55
  "#{self.class.name}: [#{status.to_s.upcase}] The iMIS API returned an error.",
@@ -47,6 +77,8 @@ module Usps
47
77
  :unprocessable_entity # validation error
48
78
  when /^50\d$/
49
79
  :internal_server_error # error within iMIS
80
+ else
81
+ response.code
50
82
  end
51
83
  end
52
84
 
@@ -2,7 +2,12 @@
2
2
 
3
3
  module Usps
4
4
  module Imis
5
+ # Specific known fields mapping to facilitate updating across multiple
6
+ # business objects.
7
+ #
5
8
  class Mapper
9
+ # List of known mapped fields
10
+ #
6
11
  FIELD_MAPPING = {
7
12
  grade: %w[ABC_ASC_Individual_Demog Grade],
8
13
  edpro: %w[ABC_ASC_Individual_Demog Educ_Proficiency],
@@ -12,19 +17,26 @@ module Usps
12
17
  mm_updated: %w[ABC_ASC_Individual_Demog MMS_Updated]
13
18
  }.freeze
14
19
 
20
+ # The parent +Api+ object
21
+ #
15
22
  attr_reader :api
16
23
 
24
+ # A new instance of +Mapper+
25
+ #
17
26
  def initialize(api = nil, imis_id: nil)
18
27
  @api = api || Api.new
19
28
  @api.imis_id = imis_id if imis_id
20
29
  end
21
30
 
22
31
  # Update a member's data on multiple affected business objects by arbitrary field names
32
+ #
23
33
  # Does not require knowing which business object / iMIS-specific field name to use
24
34
  #
25
- # Only available for previously-mapped fields
35
+ # Only available for fields defined in +FIELD_MAPPING+
36
+ #
37
+ # @param data [Hash] Conforms to pattern +{ field_key => value }+
26
38
  #
27
- # `data` is a hash of shape { field_key => value }
39
+ # @return [Array<Hash>] Response data from the API for each internal update request
28
40
  #
29
41
  def update(data)
30
42
  updates = data.each_with_object({}) do |(field_key, value), hash|
@@ -52,11 +64,13 @@ module Usps
52
64
 
53
65
  def missing_mapping(field_name)
54
66
  unless ENV['TESTING']
67
+ # :nocov:
55
68
  warn(
56
69
  "Mapper does not know how to handle field \"#{field_name}\".\n\n" \
57
70
  'You can use api.put_fields(business_object_name, { field_name => value }) ' \
58
71
  "if you know the business object and iMIS-specific field name.\n\n"
59
72
  )
73
+ # :nocov:
60
74
  end
61
75
 
62
76
  raise(
@@ -3,7 +3,11 @@
3
3
  module Usps
4
4
  module Imis
5
5
  module Panel
6
+ # Base class for configuring Panels
7
+ #
6
8
  class BasePanel
9
+ # The parent +Api+ object
10
+ #
7
11
  attr_reader :api
8
12
 
9
13
  def initialize(api = nil, imis_id: nil)
@@ -11,18 +15,35 @@ module Usps
11
15
  @api.imis_id = imis_id if imis_id
12
16
  end
13
17
 
18
+ # Get a specific object from the Panel
19
+ #
20
+ # @param ordinal [Integer] The ordinal identifier for the desired object
21
+ #
14
22
  def get(ordinal)
15
23
  api.get(business_object, url_id: "~#{api.imis_id}|#{ordinal}")
16
24
  end
17
25
 
26
+ # Create a new object in the Panel
27
+ #
28
+ # @param data [Hash] The record data for the desired object
29
+ #
18
30
  def create(data)
19
31
  api.post(business_object, payload(data), url_id: '')
20
32
  end
21
33
 
34
+ # Update an existing object in the Panel
35
+ #
36
+ # @param data [Hash] The record data for the desired object -- including the required
37
+ # +ordinal+ identifier
38
+ #
22
39
  def update(data)
23
40
  api.put(business_object, payload(data), url_id: "~#{api.imis_id}|#{data[:ordinal]}")
24
41
  end
25
42
 
43
+ # Remove a specific object from the Panel
44
+ #
45
+ # @param ordinal [Integer] The ordinal identifier for the desired object
46
+ #
26
47
  def destroy(ordinal)
27
48
  api.delete(business_object, url_id: "~#{api.imis_id}|#{ordinal}")
28
49
  end
@@ -3,6 +3,8 @@
3
3
  module Usps
4
4
  module Imis
5
5
  module Panel
6
+ # Panel for accessing the Educational completions business object
7
+ #
6
8
  class Education < BasePanel
7
9
  private
8
10
 
@@ -3,6 +3,8 @@
3
3
  module Usps
4
4
  module Imis
5
5
  module Panel
6
+ # Panel for accessing the annual VSC completed counts business object
7
+ #
6
8
  class Vsc < BasePanel
7
9
  private
8
10
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Usps
4
4
  module Imis
5
- VERSION = '0.4.3'
5
+ VERSION = '0.4.5'
6
6
  end
7
7
  end
data/lib/usps/imis.rb CHANGED
@@ -7,7 +7,9 @@ require 'time'
7
7
  require 'cgi'
8
8
 
9
9
  # Extensions
10
+ # :nocov:
10
11
  require 'ext/hash' unless defined?(Rails)
12
+ # :nocov:
11
13
 
12
14
  # Internal requires
13
15
  require_relative 'imis/config'
@@ -21,24 +23,26 @@ require_relative 'imis/panel/vsc'
21
23
  require_relative 'imis/panel/education'
22
24
 
23
25
  module Usps
26
+ # API wrapper for interacting with iMIS
27
+ #
24
28
  module Imis
25
29
  class << self
30
+ # Accessor for configuration values
31
+ #
32
+ # @return The configuration object
33
+ #
26
34
  def configuration
27
35
  @configuration ||= Config.new
28
36
  end
29
37
 
38
+ # Used to define a block of configuration settings
39
+ #
40
+ # @return The updated configuration object
41
+ #
30
42
  def configure
31
43
  yield(configuration) if block_given?
32
44
  configuration
33
45
  end
34
-
35
- # def mock!(value = true)
36
- # @mock = value
37
- # end
38
-
39
- # def mock
40
- # @mock || false
41
- # end
42
46
  end
43
47
  end
44
48
  end
@@ -5,10 +5,36 @@ require 'spec_helper'
5
5
  describe Usps::Imis::Api do
6
6
  let(:api) { described_class.new }
7
7
 
8
+ describe '#initialize' do
9
+ it 'skips authentication' do
10
+ # rubocop:disable RSpec/AnyInstance
11
+ expect_any_instance_of(described_class).not_to receive(:authenticate)
12
+ # rubocop:enable RSpec/AnyInstance
13
+
14
+ described_class.new(skip_authentication: true)
15
+ end
16
+
17
+ it 'stores the initial imis_id' do
18
+ api = described_class.new(imis_id: 42)
19
+
20
+ expect(api.imis_id).to eq('42')
21
+ end
22
+ end
23
+
8
24
  describe '#imis_id_for' do
9
25
  it 'gets the iMIS ID' do
10
26
  expect(api.imis_id_for('E231625')).to eq('31092')
11
27
  end
28
+
29
+ context 'with a query error' do
30
+ before { allow(api).to receive(:query).and_raise('Stub') }
31
+
32
+ it 'wraps errors' do
33
+ expect { api.imis_id_for('E231625') }.to raise_error(
34
+ Usps::Imis::Error::Api, 'Member not found'
35
+ )
36
+ end
37
+ end
12
38
  end
13
39
 
14
40
  describe '#put' do
@@ -17,6 +43,29 @@ describe Usps::Imis::Api do
17
43
  it 'sends an update' do
18
44
  expect(api.put_fields('ABC_ASC_Individual_Demog', { 'TotMMS' => 15 })).to be_a(Hash)
19
45
  end
46
+
47
+ context 'when receiving a response error' do
48
+ let(:warning_text) do
49
+ <<~WARNING.chomp
50
+ Usps::Imis::Error::Response: [INTERNAL_SERVER_ERROR] The iMIS API returned an error.
51
+ Something went wrong
52
+ WARNING
53
+ end
54
+
55
+ before do
56
+ error = Struct.new(:code, :body).new('500', 'Something went wrong')
57
+
58
+ # rubocop:disable RSpec/AnyInstance
59
+ allow_any_instance_of(Net::HTTP).to receive(:request).and_return(error)
60
+ # rubocop:enable RSpec/AnyInstance
61
+ end
62
+
63
+ it 'wraps the error' do
64
+ expect { api.put_fields('ABC_ASC_Individual_Demog', { 'TotMMS' => 15 }) }.to raise_error(
65
+ Usps::Imis::Error::Api, warning_text
66
+ )
67
+ end
68
+ end
20
69
  end
21
70
 
22
71
  describe '#with' do
@@ -46,4 +95,49 @@ describe Usps::Imis::Api do
46
95
  expect(api.inspect).not_to match(/ @token="/)
47
96
  end
48
97
  end
98
+
99
+ describe '#authorize' do
100
+ before { allow(api).to receive(:authenticate) }
101
+
102
+ it 'automatically refreshes an expired token' do
103
+ api.instance_variable_set(:@token_expiration, Time.now - 60)
104
+
105
+ request = Net::HTTP::Put.new('/')
106
+ api.send(:authorize, request)
107
+
108
+ expect(api).to have_received(:authenticate)
109
+ end
110
+ end
111
+
112
+ describe '#filter_fields' do
113
+ let(:expected) do
114
+ {
115
+ 'Properties' => {
116
+ '$values' => [
117
+ { 'Name' => 'Stub iMIS ID', 'Value' => { '$value' => '31092' } },
118
+ { 'Name' => 'Stub Integer', 'Value' => { '$value' => 43 } },
119
+ { 'Name' => 'Stub String', 'Value' => 'other' }
120
+ ]
121
+ }
122
+ }
123
+ end
124
+
125
+ before do
126
+ allow(api).to receive(:get).and_return({
127
+ 'Properties' => {
128
+ '$values' => [
129
+ { 'Name' => 'Stub iMIS ID', 'Value' => { '$value' => '31092' } },
130
+ { 'Name' => 'Stub Integer', 'Value' => { '$value' => 42 } },
131
+ { 'Name' => 'Stub String', 'Value' => 'something' }
132
+ ]
133
+ }
134
+ })
135
+ end
136
+
137
+ it 'formats fields correctly' do
138
+ updated = api.send(:filter_fields, 'Stub', { 'Stub Integer' => 43, 'Stub String' => 'other' })
139
+
140
+ expect(updated).to eq(expected)
141
+ end
142
+ end
49
143
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Usps::Imis::Config do
6
+ let(:config) { described_class.new }
7
+
8
+ it 'sets values on initialize' do
9
+ config = described_class.new { |c| c.environment = 'test' }
10
+
11
+ expect(config.environment).to eq('test')
12
+ end
13
+
14
+ describe '#hostname' do
15
+ context 'with production environment' do
16
+ before { config.environment = 'production' }
17
+
18
+ it 'returns the production hostname' do
19
+ expect(config.hostname).to eq(described_class::IMIS_ROOT_URL_PROD)
20
+ end
21
+ end
22
+
23
+ context 'with unrecognized environment' do
24
+ before { config.environment = 'nothing' }
25
+
26
+ it 'raises an error' do
27
+ expect { config.hostname }.to raise_error(
28
+ Usps::Imis::Error::Api, 'Unexpected API environment: nothing'
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Usps::Imis::Error::Api do
6
+ it 'builds Bugsnag metadata' do
7
+ error = described_class.new('Example', something: :else)
8
+
9
+ expect(error.bugsnag_meta_data).to eq(api: { something: :else })
10
+ end
11
+
12
+ it 'ignores Bugsnag metadata with none provided' do
13
+ error = described_class.new('Example')
14
+
15
+ expect(error.bugsnag_meta_data).to eq({})
16
+ end
17
+ end
@@ -5,15 +5,103 @@ require 'spec_helper'
5
5
  ApiResponseStub = Struct.new(:code, :body)
6
6
 
7
7
  describe Usps::Imis::Error::Response do
8
- let(:response) { ApiResponseStub.new('500', 'Body of the API response error') }
9
- let(:warning_text) do
10
- <<~WARNING.chomp
11
- Usps::Imis::Error::Response: [INTERNAL_SERVER_ERROR] The iMIS API returned an error.
12
- Body of the API response error
13
- WARNING
8
+ let(:error) { described_class.from(response) }
9
+
10
+ describe 'error codes' do
11
+ context 'with a 400' do
12
+ let(:response) { ApiResponseStub.new('400', 'body') }
13
+
14
+ it 'builds the Bugsnag metadata correctly' do
15
+ expect(error.bugsnag_meta_data).to eq(api: { status: :bad_request, body: 'body' })
16
+ end
17
+ end
18
+
19
+ context 'with a 401' do
20
+ let(:response) { ApiResponseStub.new('401', 'body') }
21
+
22
+ it 'builds the Bugsnag metadata correctly' do
23
+ expect(error.bugsnag_meta_data).to eq(api: { status: :unauthorized, body: 'body' })
24
+ end
25
+ end
26
+
27
+ context 'with a 404' do
28
+ let(:response) { ApiResponseStub.new('404', 'body') }
29
+
30
+ it 'builds the Bugsnag metadata correctly' do
31
+ expect(error.bugsnag_meta_data).to eq(api: { status: :not_found, body: 'body' })
32
+ end
33
+ end
34
+
35
+ context 'with a 422' do
36
+ let(:response) { ApiResponseStub.new('422', 'body') }
37
+
38
+ it 'builds the Bugsnag metadata correctly' do
39
+ expect(error.bugsnag_meta_data).to eq(api: { status: :unprocessable_entity, body: 'body' })
40
+ end
41
+ end
42
+
43
+ context 'with a 500' do
44
+ let(:response) { ApiResponseStub.new('500', 'body') }
45
+
46
+ it 'builds the Bugsnag metadata correctly' do
47
+ expect(error.bugsnag_meta_data).to eq(api: { status: :internal_server_error, body: 'body' })
48
+ end
49
+ end
50
+
51
+ context 'with a 429' do
52
+ let(:response) { ApiResponseStub.new('429', 'body') }
53
+
54
+ it 'builds the Bugsnag metadata correctly' do
55
+ expect(error.bugsnag_meta_data).to eq(api: { status: '429', body: 'body' })
56
+ end
57
+ end
58
+ end
59
+
60
+ context 'with a string response body' do
61
+ let(:response) { ApiResponseStub.new('500', 'Body of the API response error') }
62
+ let(:warning_text) do
63
+ <<~WARNING.chomp
64
+ Usps::Imis::Error::Response: [INTERNAL_SERVER_ERROR] The iMIS API returned an error.
65
+ Body of the API response error
66
+ WARNING
67
+ end
68
+
69
+ it 'builds the correct message' do
70
+ expect(error.message).to eq(warning_text)
71
+ end
14
72
  end
15
73
 
16
- it 'handles an API response correctly' do
17
- expect(described_class.from(response).message).to eq(warning_text)
74
+ context 'with an invalid_grant hash response body' do
75
+ let(:response_body) do
76
+ { 'error' => 'invalid_grant', 'error_description' => 'description' }
77
+ end
78
+ let(:response) { ApiResponseStub.new('500', response_body) }
79
+ let(:warning_text) do
80
+ <<~WARNING.chomp
81
+ Usps::Imis::Error::Response: [INTERNAL_SERVER_ERROR] The iMIS API returned an error.
82
+ description
83
+ WARNING
84
+ end
85
+
86
+ it 'builds the correct message' do
87
+ expect(error.message).to eq(warning_text)
88
+ end
89
+ end
90
+
91
+ context 'with a generic hash response body' do
92
+ let(:response_body) do
93
+ { 'error' => 'summary', 'error_description' => 'description' }
94
+ end
95
+ let(:response) { ApiResponseStub.new('500', response_body) }
96
+ let(:warning_text) do
97
+ <<~WARNING.chomp
98
+ Usps::Imis::Error::Response: [INTERNAL_SERVER_ERROR] The iMIS API returned an error.
99
+ #{response_body}
100
+ WARNING
101
+ end
102
+
103
+ it 'builds the correct message' do
104
+ expect(error.message).to eq(warning_text)
105
+ end
18
106
  end
19
107
  end
@@ -5,6 +5,14 @@ require 'spec_helper'
5
5
  describe Usps::Imis::Mapper do
6
6
  let(:api) { described_class.new.api }
7
7
 
8
+ describe 'initialize with imis_id' do
9
+ it 'stores the initial imis_id' do
10
+ mapper = described_class.new(imis_id: 42)
11
+
12
+ expect(mapper.api.imis_id).to eq('42')
13
+ end
14
+ end
15
+
8
16
  describe '#update' do
9
17
  before { api.imis_id = 31092 }
10
18
 
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ module Usps
6
+ module Imis
7
+ module Panel
8
+ class InvalidPanel < BasePanel; end
9
+
10
+ class InvalidPanelWithBusinessObject < BasePanel
11
+ private
12
+
13
+ def business_object = 'Something'
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ describe Usps::Imis::Panel::BasePanel do
20
+ it 'requires #business_object to be defined' do
21
+ expect { Usps::Imis::Panel::InvalidPanel.new.get(1) }.to raise_error(
22
+ Usps::Imis::Error::Api, 'Usps::Imis::Panel::InvalidPanel must implement #business_object'
23
+ )
24
+ end
25
+
26
+ it 'requires #payload(data) to be defined' do
27
+ expect { Usps::Imis::Panel::InvalidPanelWithBusinessObject.new.create({}) }.to raise_error(
28
+ Usps::Imis::Error::Api,
29
+ 'Usps::Imis::Panel::InvalidPanelWithBusinessObject must implement #payload(data)'
30
+ )
31
+ end
32
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Usps::Imis do
6
+ it 'returns configuration from configure without a block' do
7
+ described_class.configuration.environment = 'development'
8
+
9
+ expect(described_class.configure.environment).to eq('development')
10
+ end
11
+ end
data/spec/spec_helper.rb CHANGED
@@ -3,10 +3,7 @@
3
3
  require 'bundler/setup'
4
4
  Bundler.setup
5
5
  require 'simplecov'
6
- SimpleCov.start do
7
- add_filter '/spec'
8
- end
9
- # SimpleCov.minimum_coverage(100)
6
+ SimpleCov.minimum_coverage(line: 100, branch: 100)
10
7
 
11
8
  require 'dotenv/load'
12
9
  require 'usps/imis'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: usps-imis-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julian Fiander
@@ -20,6 +20,7 @@ files:
20
20
  - ".rspec"
21
21
  - ".rubocop.yml"
22
22
  - ".ruby-version"
23
+ - ".simplecov"
23
24
  - Gemfile
24
25
  - Gemfile.lock
25
26
  - Rakefile
@@ -39,10 +40,14 @@ files:
39
40
  - lib/usps/imis/panel/vsc.rb
40
41
  - lib/usps/imis/version.rb
41
42
  - spec/lib/usps/imis/api_spec.rb
43
+ - spec/lib/usps/imis/config_spec.rb
44
+ - spec/lib/usps/imis/error/api_spec.rb
42
45
  - spec/lib/usps/imis/error/response_spec.rb
43
46
  - spec/lib/usps/imis/mapper_spec.rb
47
+ - spec/lib/usps/imis/panel/base_panel_spec.rb
44
48
  - spec/lib/usps/imis/panel/education_spec.rb
45
49
  - spec/lib/usps/imis/panel/vsc_spec.rb
50
+ - spec/lib/usps/imis_spec.rb
46
51
  - spec/spec_helper.rb
47
52
  - usps-imis-api.gemspec
48
53
  homepage: http://rubygems.org/gems/usps-imis-api