et_ccd_client 0.3.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.
@@ -0,0 +1,2 @@
1
+ require 'et_ccd_client/exceptions/base'
2
+ Dir.glob(File.absolute_path(File.join('.', 'exceptions', '**', "*.rb"), __dir__)).each { |f| require f }
@@ -0,0 +1,52 @@
1
+ module EtCcdClient
2
+ module Exceptions
3
+ class Base < ::StandardError
4
+ attr_reader :original_exception, :url, :request
5
+
6
+ def self.raise_exception(original_exception, **kw_args)
7
+ expected_error_class = original_exception.class.name.split('::').last
8
+ if EtCcdClient::Exceptions.const_defined?(expected_error_class)
9
+ raise EtCcdClient::Exceptions.const_get(expected_error_class).new original_exception, **kw_args
10
+ else
11
+ raise new(original_exception, **kw_args)
12
+ end
13
+ end
14
+
15
+ def self.exception(*args, **kw_args)
16
+ new(*args, **kw_args)
17
+ end
18
+
19
+ def initialize(original_exception, url: nil, request: nil)
20
+ self.original_exception = original_exception
21
+ self.url = url
22
+ self.request = request
23
+ end
24
+
25
+ def response
26
+ original_exception.response
27
+ end
28
+
29
+ def to_s
30
+ json = JSON.parse(response.body) rescue JSON::JSONError
31
+ message = if json.nil? || json == JSON::JSONError
32
+ ''
33
+ else
34
+ json['message'] || json['error'] || ''
35
+ end
36
+ if url
37
+ "#{original_exception.message} - #{message} ('#{url}')"
38
+ else
39
+ "#{original_exception.message} - #{message}"
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ attr_writer :original_exception, :url
46
+
47
+ def request=(request)
48
+ @request = request&.args
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,6 @@
1
+ module EtCcdClient
2
+ module Exceptions
3
+ class Forbidden < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module EtCcdClient
2
+ module Exceptions
3
+ class GatewayTimeout < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module EtCcdClient
2
+ module Exceptions
3
+ class InternalServerError < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,13 @@
1
+ module EtCcdClient
2
+ module Exceptions
3
+ class NotFound < Base
4
+ def to_s
5
+ json = JSON.parse(response.body) rescue JSON::JSONError
6
+ return "Not Found" if json.nil?
7
+
8
+ super
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ module EtCcdClient
2
+ module Exceptions
3
+ class Unauthorized < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,18 @@
1
+ module EtCcdClient
2
+ module Exceptions
3
+ class UnprocessableEntity < Base
4
+ def to_s
5
+ json = JSON.parse(response.body) rescue JSON::JSONError
6
+ return super if json.nil? || json == JSON::JSONError
7
+
8
+ field_errors = json.dig('details', 'field_errors')&.map do |field_error|
9
+ "#{field_error['id']} => #{field_error['message']}"
10
+ end
11
+ return super if field_errors.nil?
12
+
13
+ "#{super} - #{field_errors.join(', ')}"
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,57 @@
1
+ require "addressable/template"
2
+ require 'rest_client'
3
+ require 'et_ccd_client/config'
4
+ require 'et_ccd_client/common_rest_client'
5
+ require 'rotp'
6
+ module EtCcdClient
7
+ class IdamClient
8
+ include CommonRestClient
9
+ attr_reader :service_token, :user_token, :user_details
10
+
11
+ def initialize(config: ::EtCcdClient.config)
12
+ self.config = config
13
+ self.logger = config.logger
14
+ self.service_token = nil
15
+ self.user_token = nil
16
+ self.user_details = nil
17
+ end
18
+
19
+ def login(username: config.sidam_username, password: config.sidam_password)
20
+ logger.tagged('EtCcdClient::IdamClient') do
21
+ self.service_token = exchange_service_token
22
+ self.user_token = exchange_sidam_user_token(username, password)
23
+ self.user_details = fetch_user_details
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_writer :service_token, :user_token, :user_details
30
+ attr_accessor :config, :logger
31
+
32
+ def exchange_service_token
33
+ url = config.idam_service_token_exchange_url
34
+ data = { microservice: config.microservice, oneTimePassword: otp }.to_json
35
+ post_request(url, data, log_subject: 'Idam service token exchange', decode: false)
36
+ end
37
+
38
+ def exchange_sidam_user_token(username, password)
39
+ url = config.idam_user_token_exchange_url
40
+ resp = post_request(url, { username: username, password: password }, log_subject: 'Idam user token exchange')
41
+ resp['access_token']
42
+ end
43
+
44
+ def fetch_user_details
45
+ url = config.user_details_url
46
+ get_request(url, extra_headers: {'Accept' => 'application/json', 'Authorization' => user_token}, log_subject: 'Idam get user details')
47
+ end
48
+
49
+ def otp
50
+ totp.now
51
+ end
52
+
53
+ def totp
54
+ @totp ||= ROTP::TOTP.new(config.microservice_secret)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EtCcdClient
4
+ # Null logger class. This is essentially the same as sending data down the
5
+ # `/dev/null` black hole.
6
+ #
7
+ # @example Basic Usage
8
+ #
9
+ # logger = NullLogger.new
10
+ # Rails.logger = logger
11
+ #
12
+ #
13
+ # @example Basic Pattern Usage
14
+ # class SomeService
15
+ # def initialize(options = {})
16
+ # @logger = options[:logger] || NullLogger.new
17
+ # end
18
+ #
19
+ # def perform
20
+ # @logger.debug -> { "do some work here" }
21
+ # # .. ..
22
+ # @logger.info -> { "finished working" }
23
+ # end
24
+ # end
25
+ #
26
+ # service = SomeService.new(logger: Logger.new(STDOUT))
27
+ # service.perform
28
+ #
29
+ # silent = SomeService.new(logger: NullLogger.new
30
+ # silent.perform
31
+ #
32
+ class NullLogger
33
+
34
+ # @param _args Anything that we want to ignore
35
+ # @return [nil]
36
+ def tagged(*)
37
+ yield if block_given?
38
+ end
39
+ # @param _args Anything that we want to ignore
40
+ # @return [nil]
41
+ def unknown(*_args)
42
+ nil
43
+ end
44
+
45
+ # @param _args Anything that we want to ignore
46
+ # @return [nil]
47
+ def fatal(*_args)
48
+ nil
49
+ end
50
+
51
+ # @return [FALSE]
52
+ def fatal?
53
+ false
54
+ end
55
+
56
+ # @param _args Anything that we want to ignore
57
+ # @return [nil]
58
+ def error(*_args)
59
+ nil
60
+ end
61
+
62
+ # @return [FALSE]
63
+ def error?
64
+ false
65
+ end
66
+
67
+ # @param _args Anything that we want to ignore
68
+ # @return [nil]
69
+ def warn(*_args)
70
+ nil
71
+ end
72
+
73
+ # @return [FALSE]
74
+ def warn?
75
+ false
76
+ end
77
+
78
+ # @param _args Anything that we want to ignore
79
+ # @return [nil]
80
+ def info(*_args)
81
+ nil
82
+ end
83
+
84
+ # @return [FALSE]
85
+ def info?
86
+ false
87
+ end
88
+
89
+ # @param _args Anything that we want to ignore
90
+ # @return [nil]
91
+ def debug(*_args)
92
+ nil
93
+ end
94
+
95
+ # @return [FALSE]
96
+ def debug?
97
+ false
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,64 @@
1
+ require "addressable/template"
2
+ require 'rest_client'
3
+ require 'et_ccd_client/config'
4
+ require 'rotp'
5
+ module EtCcdClient
6
+ class TidamClient
7
+ attr_reader :service_token, :user_token, :user_details
8
+
9
+ def initialize(config: ::EtCcdClient.config)
10
+ self.config = config
11
+ self.logger = config.logger
12
+ self.service_token = nil
13
+ self.user_token = nil
14
+ self.user_details = nil
15
+ end
16
+
17
+ def login(user_id: config.user_id, role: config.user_role)
18
+ logger.tagged('EtCcdClient::IdamClient') do
19
+ self.service_token = exchange_service_token
20
+ self.user_token = exchange_tidam_user_token(user_id, role)
21
+ self.user_details = get_user_details(user_id, role)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_writer :service_token, :user_token, :user_details
28
+ attr_accessor :config, :logger
29
+
30
+ def exchange_service_token
31
+ url = config.idam_service_token_exchange_url
32
+ data = { microservice: config.microservice, oneTimePassword: otp }.to_json
33
+ logger.debug("ET > Idam service token exchange (#{url}) - #{data}")
34
+ resp = RestClient::Request.execute(method: :post, url: url, payload: data, headers: { content_type: 'application/json' }, verify_ssl: config.verify_ssl)
35
+ resp.body.tap do |resp_body|
36
+ logger.debug "ET < Idam service token exchange - #{resp_body}"
37
+ end
38
+ end
39
+
40
+ def exchange_tidam_user_token(user_id, user_role)
41
+ url = config.idam_user_token_exchange_url
42
+ logger.debug("ET > Idam user token exchange (#{url}) - id: #{user_id} role: #{user_role}")
43
+ resp = RestClient::Request.execute(method: :post, url: url, payload: { id: user_id, role: user_role }, verify_ssl: config.verify_ssl)
44
+ resp.body.tap do |resp_body|
45
+ logger.debug "ET < Idam user token exchange - #{resp_body}"
46
+ end
47
+ end
48
+
49
+ def get_user_details(user_id, role)
50
+ {
51
+ 'id' => user_id,
52
+ 'roles' => role.split(',').map(&:strip)
53
+ }
54
+ end
55
+
56
+ def otp
57
+ totp.now
58
+ end
59
+
60
+ def totp
61
+ @totp ||= ROTP::TOTP.new(config.microservice_secret)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,159 @@
1
+ require "addressable/template"
2
+ require 'rest_client'
3
+ require 'et_ccd_client/ui_idam_client'
4
+ require 'et_ccd_client/config'
5
+ require 'et_ccd_client/exceptions'
6
+ require 'et_ccd_client/common_rest_client'
7
+ require 'json'
8
+ require 'forwardable'
9
+ module EtCcdClient
10
+ # A client to interact with the CCD UI API (front end)
11
+ class UiClient
12
+ extend Forwardable
13
+ include CommonRestClient
14
+
15
+ def initialize(ui_idam_client: UiIdamClient.new, config: ::EtCcdClient.config)
16
+ self.ui_idam_client = ui_idam_client
17
+ self.config = config
18
+ self.logger = config.logger
19
+ end
20
+
21
+ delegate login: :ui_idam_client
22
+
23
+ # Search for cases by reference - useful for testing
24
+ # @param [String] reference The reference number to search for
25
+ # @param [String] case_type_id The case type ID to set the search scope to
26
+ # @param [Integer] page - The page number to fetch
27
+ # @param [String] sort_direction (defaults to 'desc') - Change to 'asc' to do oldest first
28
+ #
29
+ # @return [Array<Hash>] The json response from the server
30
+ def caseworker_search_by_reference(reference, case_type_id:, page: 1, sort_direction: 'desc')
31
+ logger.tagged('EtCcdClient::UiClient') do
32
+ tpl = Addressable::Template.new(config.cases_path)
33
+ path = tpl.expand(uid: ui_idam_client.user_details['id'], jid: config.jurisdiction_id, ctid: case_type_id, query: { 'case.feeGroupReference' => reference, page: page, 'sortDirection' => sort_direction }).to_s
34
+ url = "#{config.gateway_api_url}/aggregated#{path}"
35
+ resp = get_request(url, log_subject: 'Caseworker search by reference', extra_headers: { content_type: 'application/json', accept: 'application/json' }, cookies: { accessToken: ui_idam_client.user_token })
36
+ unless config.document_store_url_rewrite == false
37
+ resp = reverse_rewrite_document_store_urls(resp)
38
+ end
39
+ resp["results"]
40
+ end
41
+ end
42
+
43
+ # Search for the latest case matching the reference. Useful for testing
44
+ # @param [String] reference The reference number to search for
45
+ # @param [String] case_type_id The case type ID to set the search scope to
46
+ # @return [Hash] The case object returned from the server
47
+ def caseworker_search_latest_by_reference(reference, case_type_id:)
48
+ results = caseworker_search_by_reference(reference, case_type_id: case_type_id, page: 1, sort_direction: 'desc')
49
+ results.first
50
+ end
51
+
52
+ # Search for cases by ethos case reference - useful for testing
53
+ # @param [String] reference The ethos case reference number to search for
54
+ # @param [String] case_type_id The case type ID to set the search scope to
55
+ # @param [Integer] page - The page number to fetch
56
+ # @param [String] sort_direction (defaults to 'desc') - Change to 'asc' to do oldest first
57
+ #
58
+ # @return [Array<Hash>] The json response from the server
59
+ def caseworker_search_by_ethos_case_reference(reference, case_type_id:, page: 1, sort_direction: 'desc')
60
+ logger.tagged('EtCcdClient::UiClient') do
61
+ tpl = Addressable::Template.new(config.cases_path)
62
+ path = tpl.expand(uid: ui_idam_client.user_details['id'], jid: config.jurisdiction_id, ctid: case_type_id, query: { 'case.ethosCaseReference' => reference, page: page, 'sortDirection' => sort_direction }).to_s
63
+ url = "#{config.gateway_api_url}/aggregated#{path}"
64
+ resp = get_request(url, log_subject: 'Caseworker search by ethos case reference', extra_headers: { content_type: 'application/json', accept: 'application/json' }, cookies: { accessToken: ui_idam_client.user_token })
65
+ unless config.document_store_url_rewrite == false
66
+ resp = reverse_rewrite_document_store_urls(resp)
67
+ end
68
+ resp["results"]
69
+ end
70
+ end
71
+
72
+ # Search for the latest case matching the ethos case reference. Useful for testing
73
+ # @param [String] reference The ethos case reference number to search for
74
+ # @param [String] case_type_id The case type ID to set the search scope to
75
+ # @return [Hash] The case object returned from the server
76
+ def caseworker_search_latest_by_ethos_case_reference(reference, case_type_id:)
77
+ results = caseworker_search_by_ethos_case_reference(reference, case_type_id: case_type_id, page: 1, sort_direction: 'desc')
78
+ results.first
79
+ end
80
+
81
+ # Search for cases by multiples reference - useful for testing
82
+ # @param [String] reference The multiples reference number to search for
83
+ # @param [String] case_type_id The case type ID to set the search scope to
84
+ # @param [Integer] page - The page number to fetch
85
+ # @param [String] sort_direction (defaults to 'desc') - Change to 'asc' to do oldest first
86
+ #
87
+ # @return [Array<Hash>] The json response from the server
88
+ def caseworker_search_by_multiple_reference(reference, case_type_id:, page: 1, sort_direction: 'desc')
89
+ logger.tagged('EtCcdClient::UiClient') do
90
+ tpl = Addressable::Template.new(config.cases_path)
91
+ path = tpl.expand(uid: ui_idam_client.user_details['id'], jid: config.jurisdiction_id, ctid: case_type_id, query: { 'case.multipleReference' => reference, page: page, 'sortDirection' => sort_direction }).to_s
92
+ url = "#{config.gateway_api_url}/aggregated#{path}"
93
+ resp = get_request(url, log_subject: 'Case worker search by multiple reference', extra_headers: { content_type: 'application/json', accept: 'application/json' }, cookies: { accessToken: ui_idam_client.user_token })
94
+ resp["results"]
95
+ end
96
+ end
97
+
98
+ # Search for the latest case matching the multiple reference. Useful for testing
99
+ # @param [String] reference The multiples reference number to search for
100
+ # @param [String] case_type_id The case type ID to set the search scope to
101
+ # @return [Hash] The case object returned from the server
102
+ def caseworker_search_latest_by_multiple_reference(reference, case_type_id:)
103
+ results = caseworker_search_by_multiple_reference(reference, case_type_id: case_type_id, page: 1, sort_direction: 'desc')
104
+ results.first
105
+ end
106
+
107
+ # Search for cases by bulk case title - useful for testing
108
+ # @param [String] case_title The bulk case title to search for
109
+ # @param [String] case_type_id The case type ID to set the search scope to
110
+ # @param [Integer] page - The page number to fetch
111
+ # @param [String] sort_direction (defaults to 'desc') - Change to 'asc' to do oldest first
112
+ #
113
+ # @return [Array<Hash>] The json response from the server
114
+ def caseworker_search_by_bulk_case_title(case_title, case_type_id:, page: 1, sort_direction: 'desc')
115
+ logger.tagged('EtCcdClient::UiClient') do
116
+ tpl = Addressable::Template.new(config.cases_path)
117
+ path = tpl.expand(uid: ui_idam_client.user_details['id'], jid: config.jurisdiction_id, ctid: case_type_id, query: { 'case.multipleName' => case_title, page: page, 'sortDirection' => sort_direction }).to_s
118
+ url = "#{config.gateway_api_url}/aggregated#{path}"
119
+ resp = get_request(url, log_subject: 'Case worker search by bulk case title', extra_headers: { content_type: 'application/json', accept: 'application/json' }, cookies: { accessToken: ui_idam_client.user_token })
120
+ resp["results"]
121
+ end
122
+ end
123
+
124
+ # Search for the latest case matching the bulk case title. Useful for testing
125
+ # @param [String] case-title The bulk case title to search for
126
+ # @param [String] case_type_id The case type ID to set the search scope to
127
+ # @return [Hash] The case object returned from the server
128
+ def caseworker_search_latest_by_bulk_case_title(case_title, case_type_id:)
129
+ results = caseworker_search_by_bulk_case_title(case_title, case_type_id: case_type_id, page: 1, sort_direction: 'desc')
130
+ results.first
131
+ end
132
+
133
+ # List all cases (paginated)
134
+ # @param [String] case_type_id The case type ID to set the search scope to
135
+ # @param [Integer] page - The page number to fetch
136
+ # @param [String] sort_direction (defaults to 'desc') - Change to 'asc' to do oldest first
137
+ #
138
+ # @return [Array<Hash>] The json response from the server
139
+ def caseworker_list_cases(case_type_id:, page: 1, sort_direction: 'desc')
140
+ logger.tagged('EtCcdClient::UiClient') do
141
+ tpl = Addressable::Template.new(config.cases_path)
142
+ path = tpl.expand(uid: ui_idam_client.user_details['id'], jid: config.jurisdiction_id, ctid: case_type_id, query: { page: page, 'sortDirection' => sort_direction }).to_s
143
+ url = "#{config.gateway_api_url}/aggregated#{path}"
144
+ resp = get_request(url, log_subject: 'List all cases', extra_headers: { content_type: 'application/json', accept: 'application/json' }, cookies: { accessToken: ui_idam_client.user_token })
145
+ resp["results"]
146
+ end
147
+ end
148
+
149
+
150
+ private
151
+
152
+ attr_accessor :ui_idam_client, :config, :logger
153
+
154
+ def reverse_rewrite_document_store_urls(json)
155
+ source_host, source_port, dest_host, dest_port = config.document_store_url_rewrite
156
+ JSON.parse(JSON.generate(json).gsub(/(https?):\/\/#{Regexp.quote dest_host}:#{Regexp.quote dest_port}/, "\\1://#{source_host}:#{source_port}"))
157
+ end
158
+ end
159
+ end