glimr-api-client 0.1.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/bin/rails ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path('../..', __FILE__)
6
+ ENGINE_PATH = File.expand_path('../../lib/glimr_api_client/engine', __FILE__)
7
+
8
+ # Set up gems listed in the Gemfile.
9
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
10
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
11
+
12
+ require 'rails/all'
13
+ require 'rails/engine/commands'
data/circle.yml ADDED
@@ -0,0 +1,7 @@
1
+ database:
2
+ override:
3
+ - bundle exec rake db:setup
4
+
5
+ test:
6
+ override:
7
+ - bundle exec rake
@@ -0,0 +1,30 @@
1
+ $LOAD_PATH.push File.expand_path("../lib", __FILE__)
2
+
3
+ require 'glimr_api_client/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'glimr-api-client'
7
+ spec.version = GlimrApiClient::VERSION
8
+ spec.authors = ['Todd Tyree']
9
+ spec.email = ['todd.tyree@digital.justice.gov.uk']
10
+
11
+ spec.summary = 'Easy integration with the glimr case management system'
12
+ spec.homepage = 'https://github.com/ministryofjustice/glimr-api-client'
13
+ spec.licenses = 'MIT'
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16
+
17
+ spec.add_development_dependency 'bundler', '~> 1.10'
18
+ spec.add_development_dependency 'capybara', '~> 2.7'
19
+ spec.add_development_dependency 'codeclimate-test-reporter', '~> 0.2'
20
+ spec.add_development_dependency 'fuubar', '~> 2.0'
21
+ spec.add_development_dependency 'mutant-rspec', '~> 0.8'
22
+ spec.add_development_dependency 'pry-byebug', '~> 3.4'
23
+ spec.add_development_dependency 'rails', '~> 5.0'
24
+ spec.add_development_dependency 'rake', '~> 10.0'
25
+ spec.add_development_dependency 'rspec-rails', '~> 3.5'
26
+ spec.add_development_dependency 'rubocop', '~> 0.41'
27
+ spec.add_development_dependency 'sqlite3', '~> 1.3'
28
+
29
+ spec.add_dependency 'excon', '~> 0.51'
30
+ end
@@ -0,0 +1,14 @@
1
+ require 'glimr_api_client/railtie' if defined?(Rails)
2
+ require 'glimr_api_client/version'
3
+ require 'glimr_api_client/api'
4
+ require 'glimr_api_client/available'
5
+ require 'glimr_api_client/case'
6
+ require 'glimr_api_client/update'
7
+
8
+
9
+ module GlimrApiClient
10
+ class PaymentNotificationFailure < StandardError; end
11
+ class Unavailable < StandardError; end
12
+ class CaseNotFound < StandardError; end
13
+ class RequestError < StandardError; end;
14
+ end
@@ -0,0 +1,47 @@
1
+ module GlimrApiClient
2
+ module Api
3
+
4
+ def post
5
+ @post ||=
6
+ client.post(path: endpoint, body: request_body.to_query).tap { |resp|
7
+ # Only timeouts and network issues raise errors.
8
+ handle_response_errors(resp)
9
+ @body = resp.body
10
+ @status = resp.status
11
+ }
12
+ rescue Excon::Error => e
13
+ if endpoint == '/paymenttaken'
14
+ raise GlimrApiClient::PaymentNotificationFailure, e
15
+ else
16
+ raise GlimrApiClient::Unavailable, e
17
+ end
18
+ end
19
+
20
+ def response_body
21
+ @response_body ||= JSON.parse(@body, symbolize_names: true)
22
+ end
23
+
24
+ private
25
+
26
+ def handle_response_errors(resp)
27
+ if resp.status == 404
28
+ raise GlimrApiClient::CaseNotFound
29
+ elsif (400..599).cover?(resp.status) && endpoint == '/paymenttaken'
30
+ raise GlimrApiClient::PaymentNotificationFailure, resp.status
31
+ elsif (400..599).cover?(resp.status)
32
+ raise GlimrApiClient::Unavailable, resp.status
33
+ end
34
+ end
35
+
36
+ def client
37
+ @client ||= Excon.new(
38
+ ENV.fetch('GLIMR_API_URL', 'https://glimr-test.dsd.io'),
39
+ headers: {
40
+ 'Content-Type' => 'application/json',
41
+ 'Accept' => 'application/json'
42
+ },
43
+ persistent: true
44
+ )
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,32 @@
1
+ module GlimrApiClient
2
+ class Available
3
+ include GlimrApiClient::Api
4
+
5
+ class << self
6
+ delegate :call, to: :new
7
+ end
8
+
9
+ def call
10
+ post
11
+ self
12
+ end
13
+
14
+ def available?
15
+ response_body.fetch(:glimrAvailable).eql?('yes').tap { |status|
16
+ if status.equal?(false)
17
+ raise Unavailable
18
+ end
19
+ }
20
+ end
21
+
22
+ private
23
+
24
+ def endpoint
25
+ '/glimravailable'
26
+ end
27
+
28
+ def request_body
29
+ {}
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,45 @@
1
+ module GlimrApiClient
2
+ class Case
3
+ include GlimrApiClient::Api
4
+
5
+ def self.find(case_reference = nil)
6
+ new(case_reference).call
7
+ end
8
+
9
+ def initialize(case_reference = nil)
10
+ @case_reference = case_reference
11
+ end
12
+
13
+ def call
14
+ post
15
+ self
16
+ end
17
+
18
+ def title
19
+ @title ||= response_body.fetch(:caseTitle)
20
+ end
21
+
22
+ def fees
23
+ @fees ||= response_body.fetch(:feeLiabilities).map do |fee|
24
+ OpenStruct.new(
25
+ glimr_id: Integer(fee.fetch(:feeLiabilityId)),
26
+ description: fee.fetch(:onlineFeeTypeDescription),
27
+ amount: Integer(fee.fetch(:payableWithUnclearedInPence))
28
+ )
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def endpoint
35
+ '/requestpayablecasefees'
36
+ end
37
+
38
+ def request_body
39
+ {
40
+ jurisdictionId: 8, # TODO: Remove when no longer required in API
41
+ caseNumber: @case_reference
42
+ }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,11 @@
1
+ require 'glimr_api_client'
2
+ require 'rails'
3
+ module GlimrApiClient
4
+ class Railtie < Rails::Railtie
5
+ railtie_name :glimr_api_client
6
+
7
+ rake_tasks do
8
+ load 'tasks/spec_setup.rake'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,42 @@
1
+ module GlimrApiClient
2
+ class Update
3
+ include GlimrApiClient::Api
4
+
5
+ def self.call(*args)
6
+ new(*args).call
7
+ end
8
+
9
+ def initialize(fee)
10
+ @fee = fee
11
+ end
12
+
13
+ def call
14
+ check_request!
15
+ post
16
+ self
17
+ end
18
+
19
+ private
20
+
21
+ def check_request!
22
+ errors = []
23
+ [:feeLiabilityId, :paymentReference, :govpayReference, :paidAmountInPence].each do |required|
24
+ errors << required if request_body.fetch(required).blank?
25
+ end
26
+ raise RequestError, errors unless errors.blank?
27
+ end
28
+
29
+ def endpoint
30
+ '/paymenttaken'
31
+ end
32
+
33
+ def request_body
34
+ {
35
+ feeLiabilityId: @fee.glimr_id,
36
+ paymentReference: @fee.govpay_reference,
37
+ govpayReference: @fee.govpay_payment_id,
38
+ paidAmountInPence: @fee.amount
39
+ }
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ module GlimrApiClient
2
+ VERSION = '0.1.1'
3
+ end
@@ -0,0 +1,49 @@
1
+ task :mutant do
2
+ classes_to_mutate.each do |klass|
3
+ vars = 'NOCOVERAGE=true'
4
+ flags = '--include lib --use rspec'
5
+ unless system("#{vars} mutant #{flags} #{klass}")
6
+ raise 'Mutation testing failed'
7
+ end
8
+ end
9
+ end
10
+
11
+ task(:default).prerequisites << task(:mutant)
12
+
13
+ private
14
+
15
+ def classes_to_mutate
16
+ files = grep_files_for_classes
17
+ klasses = extract_classes_for_mutation(files)
18
+ klasses.map { |k|
19
+ setup_class_for_run(k)
20
+ }
21
+ end
22
+
23
+ # This is a nasty hack because we’re using POROs; otherwise, we could just use
24
+ # ApplicationRecord.descendants...
25
+ #
26
+ # Grepping through the source code seemed to be the most pragmatic solution
27
+ # so that developers don’t need to remember to add new classes to a list for
28
+ # mutation testing, but it’s not ideal
29
+ def grep_files_for_classes
30
+ Dir.glob('lib/**/*.rb').
31
+ map { |f|
32
+ # There are some examples of `class << self` in codebase.
33
+ File.readlines(f).grep(/\bclass(?!\s<<)/)
34
+ }.flatten
35
+ end
36
+
37
+ def extract_classes_for_mutation(files)
38
+ re = /class (?<klass>\w+)/
39
+
40
+ files.map { |s|
41
+ re.match(s)[:klass]
42
+ }.compact
43
+ end
44
+
45
+ def setup_class_for_run(klass)
46
+ Object.const_get(klass)
47
+ rescue NameError
48
+ Object.const_get("GlimrApiClient::#{klass}")
49
+ end
@@ -0,0 +1,6 @@
1
+ if Gem.loaded_specs.key?('rubocop')
2
+ require 'rubocop/rake_task'
3
+ RuboCop::RakeTask.new
4
+
5
+ task(:default).prerequisites << task(:rubocop)
6
+ end
@@ -0,0 +1,11 @@
1
+ namespace :glimr_api_client do
2
+ desc 'Copy the shared examples for glimr rspec testing'
3
+ task :install_shared_examples
4
+ source = File.join(
5
+ Gem.loaded_specs['glimr-api-client'].full_gem_path,
6
+ 'shared_examples',
7
+ 'shared_examples_for_glimr.rb'
8
+ )
9
+ target = File.join(Rails.root, 'spec', 'support', 'shared_examples_for_glimr.rb')
10
+ FileUtils.cp_r source, target
11
+ end
@@ -0,0 +1,212 @@
1
+ RSpec.shared_examples 'glimr availability request' do |glimr_response|
2
+ before do
3
+ Excon.stub(
4
+ { host: 'glimr-test.dsd.io', path: '/glimravailable' },
5
+ status: 200, body: glimr_response.to_json
6
+ )
7
+ end
8
+ end
9
+
10
+ RSpec.shared_examples 'glimr availability request returns a 500' do
11
+ before do
12
+ Excon.stub(
13
+ { host: 'glimr-test.dsd.io', path: '/glimravailable' },
14
+ status: 500
15
+ )
16
+ end
17
+ end
18
+
19
+ RSpec.shared_examples 'service is not available' do
20
+ scenario do
21
+ visit '/'
22
+ expect(page).not_to have_text('Start now')
23
+ expect(page).to have_text('The service is currently unavailable')
24
+ end
25
+ end
26
+
27
+ RSpec.shared_examples 'generic glimr response' do |case_number, _confirmation_code, status, glimr_response|
28
+ before do
29
+ Excon.stub(
30
+ {
31
+ host: 'glimr-test.dsd.io',
32
+ body: /caseNumber=#{CGI.escape(case_number)}/,
33
+ path: '/requestpayablecasefees'
34
+ },
35
+ status: status, body: glimr_response.to_json
36
+ )
37
+ end
38
+ end
39
+
40
+ RSpec.shared_examples 'case not found' do
41
+ before do
42
+ Excon.stub(
43
+ {
44
+ host: 'glimr-test.dsd.io',
45
+ path: '/requestpayablecasefees'
46
+ },
47
+ status: 404
48
+ )
49
+ end
50
+ end
51
+
52
+ RSpec.shared_examples 'no new fees are due' do |case_number, _confirmation_code|
53
+ let(:response_body) {
54
+ {
55
+ 'jurisdictionId' => 8,
56
+ 'tribunalCaseId' => 60_029,
57
+ 'caseTitle' => 'You vs HM Revenue & Customs',
58
+ 'feeLiabilities' => []
59
+ }
60
+ }
61
+
62
+ before do
63
+ Excon.stub(
64
+ {
65
+ host: 'glimr-test.dsd.io',
66
+ body: /caseNumber=#{CGI.escape(case_number)}/,
67
+ path: '/requestpayablecasefees'
68
+ },
69
+ status: 200, body: response_body.to_json
70
+ )
71
+ end
72
+ end
73
+
74
+ RSpec.shared_examples 'a case fee of £20 is due' do |case_number, _confirmation_code|
75
+ let(:response_body) {
76
+ {
77
+ 'jurisdictionId' => 8,
78
+ 'tribunalCaseId' => 60_029,
79
+ 'caseTitle' => 'You vs HM Revenue & Customs',
80
+ 'feeLiabilities' =>
81
+ [{ 'feeLiabilityId' => '7',
82
+ 'onlineFeeTypeDescription' => 'Lodgement Fee',
83
+ 'payableWithUnclearedInPence' => '2000' }]
84
+ }.to_json
85
+ }
86
+
87
+ before do
88
+ Excon.stub(
89
+ {
90
+ method: :post,
91
+ host: 'glimr-test.dsd.io',
92
+ body: /caseNumber=#{CGI.escape(case_number)}&jurisdictionId=8/,
93
+ path: '/requestpayablecasefees'
94
+ },
95
+ status: 200, body: response_body
96
+ )
97
+ end
98
+ end
99
+
100
+ RSpec.shared_examples 'no fees then a £20 fee' do |case_number, _confirmation_code|
101
+ let(:no_fees) {
102
+ {
103
+ 'jurisdictionId' => 8,
104
+ 'tribunalCaseId' => 60_029,
105
+ 'caseTitle' => 'You vs HM Revenue & Customs',
106
+ 'feeLiabilities' => []
107
+ }
108
+ }
109
+
110
+ let(:twenty_pound_fee) {
111
+ {
112
+ 'jurisdictionId' => 8,
113
+ 'tribunalCaseId' => 60_029,
114
+ 'caseTitle' => 'You vs HM Revenue & Customs',
115
+ 'feeLiabilities' =>
116
+ [{ 'feeLiabilityId' => 7,
117
+ 'onlineFeeTypeDescription' => 'Lodgement Fee',
118
+ 'payableWithUnclearedInPence' => 2000 }]
119
+ }
120
+ }
121
+
122
+ before do
123
+ Excon.stub(
124
+ {
125
+ method: :post,
126
+ host: 'glimr-test.dsd.io',
127
+ body: /caseNumber=#{CGI.escape(case_number)}/,
128
+ path: '/requestpayablecasefees'
129
+ },
130
+ status: 200, body: no_fees.to_json
131
+ )
132
+
133
+ Excon.stub(
134
+ {
135
+ method: :post,
136
+ host: 'glimr-test.dsd.io',
137
+ body: /caseNumber=#{CGI.escape(case_number)}/,
138
+ path: '/requestpayablecasefees'
139
+ },
140
+ status: 200, body: twenty_pound_fee.to_json
141
+ )
142
+ end
143
+ end
144
+
145
+ RSpec.shared_examples 'report payment taken to glimr' do |req_body|
146
+ let(:paymenttaken_response) {
147
+ {
148
+ feeLiabilityId: 1234,
149
+ feeTransactionId: 1234,
150
+ paidAmountInPence: 9999
151
+ }
152
+ }
153
+
154
+ before do
155
+ Excon.stub(
156
+ {
157
+ method: :post,
158
+ host: 'glimr-test.dsd.io',
159
+ body: req_body,
160
+ path: '/paymenttaken'
161
+ },
162
+ status: 200, body: paymenttaken_response.to_json
163
+ )
164
+ end
165
+ end
166
+
167
+ RSpec.shared_examples 'glimr fee_paid returns a 500' do
168
+ before do
169
+ Excon.stub(
170
+ {
171
+ method: :post,
172
+ host: 'glimr-test.dsd.io',
173
+ path: '/paymenttaken'
174
+ },
175
+ status: 500
176
+ )
177
+ end
178
+ end
179
+
180
+ RSpec.shared_examples 'glimr times out' do
181
+ let(:glimr_check) {
182
+ class_double(Excon, 'glimr availability')
183
+ }
184
+
185
+ before do
186
+ expect(glimr_check).
187
+ to receive(:post).
188
+ with(path: '/glimravailable', body: '').
189
+ and_raise(Excon::Errors::Timeout)
190
+
191
+ expect(Excon).to receive(:new).
192
+ with(Rails.configuration.glimr_api_url, anything).
193
+ and_return(glimr_check)
194
+ end
195
+ end
196
+
197
+ RSpec.shared_examples 'glimr has a socket error' do
198
+ let(:glimr_check) {
199
+ class_double(Excon, 'glimr availability')
200
+ }
201
+
202
+ before do
203
+ expect(glimr_check).
204
+ to receive(:post).
205
+ with(path: '/glimravailable', body: '').
206
+ and_raise(Excon::Errors::SocketError)
207
+
208
+ expect(Excon).to receive(:new).
209
+ with('https://glimr-test.dsd.io', anything).
210
+ and_return(glimr_check)
211
+ end
212
+ end