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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +28 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +1151 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +252 -0
- data/MIT-LICENSE +20 -0
- data/README.md +141 -0
- data/Rakefile +36 -0
- data/bin/rails +13 -0
- data/circle.yml +7 -0
- data/glimr_api_client.gemspec +30 -0
- data/lib/glimr_api_client.rb +14 -0
- data/lib/glimr_api_client/api.rb +47 -0
- data/lib/glimr_api_client/available.rb +32 -0
- data/lib/glimr_api_client/case.rb +45 -0
- data/lib/glimr_api_client/railtie.rb +11 -0
- data/lib/glimr_api_client/update.rb +42 -0
- data/lib/glimr_api_client/version.rb +3 -0
- data/lib/tasks/mutant.rake +49 -0
- data/lib/tasks/rubocop.rake +6 -0
- data/lib/tasks/spec_setup.rake +11 -0
- data/shared_examples/shared_examples_for_glimr.rb +212 -0
- metadata +235 -0
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,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,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,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,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
|