defra_ruby_mocks 1.0.0 → 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +90 -2
  3. data/Rakefile +0 -2
  4. data/app/controllers/defra_ruby_mocks/company_controller.rb +1 -1
  5. data/app/controllers/defra_ruby_mocks/worldpay_controller.rb +51 -0
  6. data/app/services/defra_ruby_mocks/worldpay_payment_service.rb +47 -0
  7. data/app/services/defra_ruby_mocks/worldpay_refund_service.rb +37 -0
  8. data/app/services/defra_ruby_mocks/worldpay_request_handler_service.rb +40 -0
  9. data/app/services/defra_ruby_mocks/worldpay_resource_service.rb +55 -0
  10. data/app/services/defra_ruby_mocks/worldpay_response_service.rb +92 -0
  11. data/app/views/defra_ruby_mocks/worldpay/payment_request.xml.erb +4 -0
  12. data/app/views/defra_ruby_mocks/worldpay/refund_request.xml.erb +4 -0
  13. data/app/views/defra_ruby_mocks/worldpay/stuck.html.erb +37 -0
  14. data/config/routes.rb +11 -0
  15. data/lib/defra_ruby_mocks/configuration.rb +2 -0
  16. data/lib/defra_ruby_mocks/engine.rb +3 -0
  17. data/lib/defra_ruby_mocks/invalid_config_error.rb +9 -0
  18. data/lib/defra_ruby_mocks/missing_resource_error.rb +9 -0
  19. data/lib/defra_ruby_mocks/unrecognised_worldpay_request_error.rb +5 -0
  20. data/lib/defra_ruby_mocks/version.rb +1 -1
  21. data/spec/dummy/log/development.log +180 -0
  22. data/spec/dummy/log/test.log +679 -99
  23. data/spec/examples.txt +83 -30
  24. data/spec/fixtures/payment_request_invalid.xml +6 -0
  25. data/spec/fixtures/payment_request_valid.xml +30 -0
  26. data/spec/fixtures/refund_request_invalid.xml +6 -0
  27. data/spec/fixtures/refund_request_valid.xml +11 -0
  28. data/spec/fixtures/unrecognised_request.xml +6 -0
  29. data/spec/requests/company_spec.rb +3 -3
  30. data/spec/requests/worldpay_spec.rb +142 -0
  31. data/spec/services/worldpay_payment_service_spec.rb +95 -0
  32. data/spec/services/worldpay_refund_service_spec.rb +68 -0
  33. data/spec/services/worldpay_request_handler_service_spec.rb +79 -0
  34. data/spec/services/worldpay_resource_service_spec.rb +112 -0
  35. data/spec/services/worldpay_response_service_spec.rb +156 -0
  36. data/spec/spec_helper.rb +3 -2
  37. data/spec/support/helpers/configuration.rb +1 -0
  38. data/spec/support/helpers/helpers.rb +13 -0
  39. data/spec/support/helpers/waste_carriers_engine.rb +10 -0
  40. data/spec/support/helpers/xml_matchers.rb +19 -0
  41. metadata +75 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: a51d8ad9a6aacfc79ec4354df4b6ac22a95e4d3e
4
- data.tar.gz: f97be7da287ba2987204dea1cc6ae72b188aa454
2
+ SHA256:
3
+ metadata.gz: 37b80527c008c3d05100caffb316ae36fd75050a7d5612db07a180f163e3e17a
4
+ data.tar.gz: 622dc9a174d7e0af97046b33a12164b37deec2d3574afebd42932b7c7b699171
5
5
  SHA512:
6
- metadata.gz: 4251aedd24958f9cb6bac0f0fc6378f741f8fbf798e87f07e4cce3379c2c276bbd5ce28ec6a89b8d8cef378eb8a9182679c4f53d823763df38c0d7b49ed45b87
7
- data.tar.gz: d9ea74e870ab67ad6cca27b328a41ac7ddb0e132f435c718a596b91f1f0f9161067364497fc43b546b2fbff94c8a59a25a597976d65385d9fb3e166d4b7a1e07
6
+ metadata.gz: ab9dec997252c0e656cb39c4364e9d2f28e28bfd707309178a3d0e17d3d3289241720cc4c78a1f542edef1545a5d30e43573aea389e6fc69ee33237e47414c95
7
+ data.tar.gz: e3f3707826e51e569703a04aaaaeb4593f99db011331ffd73018909c8548f702cf317ccefe0f30308047b3734107b4ce292c8ef740fb362e9647834b3eb79e4c
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Defra Ruby Mocks
2
2
 
3
3
  [![Build Status](https://travis-ci.com/DEFRA/defra-ruby-mocks.svg?branch=master)](https://travis-ci.com/DEFRA/defra-ruby-mocks)
4
- [![Maintainability](https://api.codeclimate.com/v1/badges/8b14cc1e0e1c1d6a33cc/maintainability)](https://codeclimate.com/github/DEFRA/defra-ruby-mocks/maintainability)
5
- [![Test Coverage](https://api.codeclimate.com/v1/badges/8b14cc1e0e1c1d6a33cc/test_coverage)](https://codeclimate.com/github/DEFRA/defra-ruby-mocks/test_coverage)
4
+ [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=DEFRA_defra-ruby-mocks&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=DEFRA_defra-ruby-mocks)
5
+ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=DEFRA_defra-ruby-mocks&metric=coverage)](https://sonarcloud.io/dashboard?id=DEFRA_defra-ruby-mocks)
6
6
  [![security](https://hakiri.io/github/DEFRA/defra-ruby-mocks/master.svg)](https://hakiri.io/github/DEFRA/defra-ruby-mocks/master)
7
7
  [![Licence](https://img.shields.io/badge/Licence-OGLv3-blue.svg)](http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3)
8
8
 
@@ -104,6 +104,94 @@ The list of possible statuses was taken from
104
104
  - [Companies House API](https://developer.companieshouse.gov.uk/api/docs/company/company_number/companyProfile-resource.html)
105
105
  - [Companies House API enumerations](https://github.com/companieshouse/api-enumerations/blob/master/constants.yml)
106
106
 
107
+ ### Worldpay
108
+
109
+ When mounted into an app you can simulate interacting with the Worldpay hosted pages service.
110
+
111
+ #### Payments
112
+
113
+ Making a payment with Worldpay essentially comes in 2 stages
114
+
115
+ 1. The app sends an XML request to Worldpay asking it to prepare for a new payment. Worldpay responds with a reference and a URL to redirect the user to
116
+ 2. The app redirects the user to the URL and adds to it query params that tell Worldpay where to redirect the user to when the payment is complete
117
+
118
+ For more details check out [Making a payment with WorldPay](https://github.com/DEFRA/ruby-services-team/blob/master/services/wcr/payment_with_worldpay.md)
119
+
120
+ This Worldpay mock replicates those 2 interactions with the following urls
121
+
122
+ - `../worldpay/payments-service`
123
+ - `../worldpay/dispatcher`
124
+
125
+ ##### Refused payments
126
+
127
+ The engine has the ability to also mock Worldpay refusing a payment. To have the mock refuse payment just ensure the registration's company name includes the word `reject` (case doesn't matter).
128
+
129
+ If it does the engine will redirect back to the failure url instead of the success url provided, plus set the payment status to `REFUSED`.
130
+
131
+ This allows us to test how the application handles both successful and unsucessful Worldpay payments.
132
+
133
+ ##### Stuck payments
134
+
135
+ The engine has the ability to also mock Worldpay not redirecting back to the service. This is the equivalent of a registration getting 'stuck at Worldpay'. To have the mock not respond just ensure the registration's company name includes the word `stuck` (case doesn't matter).
136
+
137
+ If it does the engine will not redirect back to the service, but instead render a 'You're stuck!' page.
138
+
139
+ This allows us to test how the application handles Worldpay not returning after we redirect a user to them.
140
+
141
+ #### Refunds
142
+
143
+ Requesting a refund from Worldpay is a single step process.
144
+
145
+ 1. The app sends an XML request to Worldpay with details of the order to be refunded and the amount. Worldpay returns an XML response confirming the request has been received
146
+
147
+ Like payments, refund requests are also sent to the same url `../worldpay/payments-service`. The mock handles determining what request is being made and returns the appropriate response.
148
+
149
+ #### Configuration
150
+
151
+ In order to use the Worldpay mock you'll need to provide additional configuration details
152
+
153
+ ```ruby
154
+ # config/initializers/defra_ruby_mocks.rb
155
+ require "defra_ruby_mocks"
156
+
157
+ DefraRubyMocks.configure do |config|
158
+ config.enable = true
159
+ config.delay = 1000
160
+
161
+ config.worldpay_admin_code = "admincode1"
162
+ config.worldpay_mac_secret = "macsecret1"
163
+ config.worldpay_merchant_code = "merchantcode1"
164
+ config.worldpay_domain = "http://localhost:3000/mocks"
165
+ end
166
+ ```
167
+
168
+ It's important that the admin code, mac secret and merchant code are the same as used by the apps calling the Worldpay mock. These values are used when generating the responses and are validated by the apps so it's important they match.
169
+
170
+ The domain is used when generating the URL we tell the app to redirect users to. As this is just an engine and not a standalone service, we need to tell it what domain it is running from. For example, if the engine is mounted into the app like this
171
+
172
+ ```ruby
173
+ mount DefraRubyMocks::Engine => "/mocks"
174
+ ```
175
+
176
+ And the app is running at `http://localhost:3000`, this engine can then use that information to tell the app to redirect users to `http://localhost:3000/mocks/worldpay/dispatcher` as part of the `payments-service` response.
177
+
178
+ #### Only for Waste Carriers
179
+
180
+ At this time there is only one digital service built using Ruby on Rails that uses Worldpay; the [Waste Carriers Registration service](https://github.com/DEFRA/ruby-services-team/tree/master/services/wcr). So the Worldpay mock has been written with the assumption it will only be mounted into one of the Waste Carriers apps.
181
+
182
+ A critical aspect of this is the expectation that the following classes will be loaded and available when the engine is mounted and the app is running
183
+
184
+ - `WasteCarriersEngine::TransientRegistration`
185
+ - `WasteCarriersEngine::Registration`
186
+
187
+ We need these classes so we can use them to query the database for the registration the payment is being made against. We only get the registration reference in the request made to `/worldpay/dispatcher`, not the order code. As the response needs to include the order code we need access to these ActiveRecord models to locate the last order added.
188
+
189
+ In the live Worldpay service this information (along with the amount to be paid) is saved after the initial request to `/payments-service`. The mock however isn't persisting anything to reduce complexity. So instead it needs to be able to query the database for the information it needs via ActiveRecord.
190
+
191
+ #### Payment pages are not mocked
192
+
193
+ The actual Worldpay service presents payment pages that display a form where users are able to enter their credit card details and confirm the payment is correct. This mock does **not** replicate the UI of Worldpay, only the API. Bear this in mind when building any automated acceptance tests.
194
+
107
195
  ## Installation
108
196
 
109
197
  You don't need to do this if you're just mounting the engine without making any changes.
data/Rakefile CHANGED
@@ -25,7 +25,6 @@ Bundler::GemHelper.install_tasks
25
25
  # This is wrapped to prevent an error when rake is called in environments where
26
26
  # rspec may not be available, e.g. production. As such we don't need to handle
27
27
  # the error.
28
- # rubocop:disable Lint/HandleExceptions
29
28
  begin
30
29
  require "rspec/core/rake_task"
31
30
 
@@ -35,4 +34,3 @@ begin
35
34
  rescue LoadError
36
35
  # no rspec available
37
36
  end
38
- # rubocop:enable Lint/HandleExceptions
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DefraRubyMocks
4
- class CompanyController < ApplicationController
4
+ class CompanyController < ::DefraRubyMocks::ApplicationController
5
5
 
6
6
  before_action :set_default_response_format
7
7
 
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DefraRubyMocks
4
+ class WorldpayController < ::DefraRubyMocks::ApplicationController
5
+
6
+ before_action :set_default_response_format
7
+
8
+ def payments_service
9
+ @values = WorldpayRequestHandlerService.run(convert_request_body_to_xml)
10
+
11
+ render_payment_response if @values[:request_type] == :payment
12
+ render_refund_response if @values[:request_type] == :refund
13
+ rescue StandardError
14
+ head 500
15
+ end
16
+
17
+ def dispatcher
18
+ @response = WorldpayResponseService.run(
19
+ success_url: params[:successURL],
20
+ failure_url: params[:failureURL]
21
+ )
22
+
23
+ if @response.status == :STUCK
24
+ render formats: :html, action: "stuck", layout: false
25
+ else
26
+ redirect_to @response.url
27
+ end
28
+ rescue StandardError
29
+ head 500
30
+ end
31
+
32
+ private
33
+
34
+ def set_default_response_format
35
+ request.format = :xml
36
+ end
37
+
38
+ def convert_request_body_to_xml
39
+ Nokogiri::XML(request.body.read)
40
+ end
41
+
42
+ def render_payment_response
43
+ render "defra_ruby_mocks/worldpay/payment_request"
44
+ end
45
+
46
+ def render_refund_response
47
+ render "defra_ruby_mocks/worldpay/refund_request"
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DefraRubyMocks
4
+ class WorldpayPaymentService < BaseService
5
+ def run(merchant_code:, xml:)
6
+ check_config
7
+
8
+ @merchant_code = merchant_code
9
+ @order_code = extract_order_code(xml)
10
+
11
+ {
12
+ merchant_code: @merchant_code,
13
+ order_code: @order_code,
14
+ id: generate_id,
15
+ url: generate_url
16
+ }
17
+ end
18
+
19
+ private
20
+
21
+ def check_config
22
+ domain = DefraRubyMocks.configuration.worldpay_domain
23
+
24
+ raise InvalidConfigError, :worldpay_domain if domain.blank?
25
+ end
26
+
27
+ def extract_order_code(xml)
28
+ order = xml.at_xpath("//order")
29
+ order.attribute("orderCode").text
30
+ end
31
+
32
+ def generate_id
33
+ # Worldpay seems to generate 10 digit numbers for all its ID's. So we
34
+ # replicate that here with this.
35
+ # https://stackoverflow.com/a/31043825
36
+ rand(1e9...1e10).to_i.to_s
37
+ end
38
+
39
+ def generate_url
40
+ "#{base_url}?OrderKey=#{@merchant_code}%5E#{@order_code}"
41
+ end
42
+
43
+ def base_url
44
+ File.join(DefraRubyMocks.configuration.worldpay_domain, "/worldpay/dispatcher")
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DefraRubyMocks
4
+ class WorldpayRefundService < BaseService
5
+ def run(merchant_code:, xml:)
6
+ {
7
+ merchant_code: merchant_code,
8
+ order_code: extract_order_code(xml),
9
+ refund_value: extract_refund_value(xml),
10
+ currency_code: extract_currency_code(xml),
11
+ exponent: extract_exponent(xml)
12
+ }
13
+ end
14
+
15
+ private
16
+
17
+ def extract_order_code(xml)
18
+ order_modification = xml.at_xpath("//orderModification")
19
+ order_modification.attribute("orderCode").text
20
+ end
21
+
22
+ def extract_refund_value(xml)
23
+ amount = xml.at_xpath("//amount")
24
+ amount.attribute("value").text
25
+ end
26
+
27
+ def extract_currency_code(xml)
28
+ amount = xml.at_xpath("//amount")
29
+ amount.attribute("currencyCode").text
30
+ end
31
+
32
+ def extract_exponent(xml)
33
+ amount = xml.at_xpath("//amount")
34
+ amount.attribute("exponent").text
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DefraRubyMocks
4
+ class WorldpayRequestHandlerService < BaseService
5
+ def run(xml)
6
+ arguments = {
7
+ merchant_code: extract_merchant_code(xml),
8
+ xml: xml
9
+ }
10
+
11
+ generate_response(arguments)
12
+ end
13
+
14
+ private
15
+
16
+ def extract_merchant_code(xml)
17
+ payment_service = xml.at_xpath("//paymentService")
18
+ payment_service.attribute("merchantCode").text
19
+ end
20
+
21
+ def generate_response(arguments)
22
+ return WorldpayPaymentService.run(arguments).merge(request_type: :payment) if payment_request?(arguments[:xml])
23
+ return WorldpayRefundService.run(arguments).merge(request_type: :refund) if refund_request?(arguments[:xml])
24
+
25
+ raise UnrecognisedWorldpayRequestError
26
+ end
27
+
28
+ def payment_request?(xml)
29
+ submit = xml.at_xpath("//submit")
30
+
31
+ !submit.nil?
32
+ end
33
+
34
+ def refund_request?(xml)
35
+ modify = xml.at_xpath("//modify")
36
+
37
+ !modify.nil?
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DefraRubyMocks
4
+ class WorldpayResourceService < BaseService
5
+
6
+ def run(reference:)
7
+ @reference = reference
8
+
9
+ raise(MissingResourceError, @reference) if resource.nil?
10
+
11
+ WorldpayResource.new(resource, order, company_name)
12
+ end
13
+
14
+ private
15
+
16
+ WorldpayResource = Struct.new(:resource, :order, :company_name)
17
+
18
+ def resource
19
+ @_resource ||= locate_transient_registration || locate_completed_registration
20
+ end
21
+
22
+ def locate_transient_registration
23
+ "WasteCarriersEngine::TransientRegistration"
24
+ .constantize
25
+ .where(token: @reference)
26
+ .first
27
+ end
28
+
29
+ def locate_completed_registration
30
+ "WasteCarriersEngine::Registration"
31
+ .constantize
32
+ .where(reg_uuid: @reference)
33
+ .first
34
+ end
35
+
36
+ def locate_original_registration(reg_identifier)
37
+ "WasteCarriersEngine::Registration"
38
+ .constantize
39
+ .where(reg_identifier: reg_identifier)
40
+ .first
41
+ end
42
+
43
+ def order
44
+ @_order ||= resource.finance_details&.orders&.order_by(dateCreated: :desc)&.first
45
+ end
46
+
47
+ def company_name
48
+ if resource.class.to_s == "WasteCarriersEngine::OrderCopyCardsRegistration"
49
+ locate_original_registration(resource.reg_identifier).company_name.downcase
50
+ else
51
+ resource.company_name.downcase
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DefraRubyMocks
4
+ class WorldpayResponseService < BaseService
5
+
6
+ def run(success_url:, failure_url:)
7
+ parse_reference(success_url)
8
+ @resource = WorldpayResourceService.run(reference: @reference)
9
+
10
+ generate_response(success_url, failure_url)
11
+ end
12
+
13
+ private
14
+
15
+ WorldpayResponse = Struct.new(:supplied_url, :separator, :order_key, :mac, :value, :status, :reference) do
16
+ def url
17
+ [supplied_url, separator, params].join
18
+ end
19
+
20
+ def params
21
+ [
22
+ "orderKey=#{order_key}",
23
+ "paymentStatus=#{status}",
24
+ "paymentAmount=#{value}",
25
+ "paymentCurrency=GBP",
26
+ "mac=#{mac}",
27
+ "source=WP"
28
+ ].join("&")
29
+ end
30
+ end
31
+
32
+ def parse_reference(url)
33
+ path = URI.parse(url).path
34
+ parts = path.split("/")
35
+
36
+ if parts[1].downcase == "your-registration"
37
+ # ["", "your-registration", "xP2zj9nqWYI0SAsMtGZn6w", "worldpay", "success", "1577812071", "NEWREG"]
38
+ @reference = parts[-5]
39
+ @url_format = :old
40
+ else
41
+ # ["", "fo", "jq6sTt2RQguAu4ZyKFfRg9zm", "worldpay", "success"]
42
+ @reference = parts[-3]
43
+ @url_format = :new
44
+ end
45
+ end
46
+
47
+ def order_key
48
+ [
49
+ DefraRubyMocks.configuration.worldpay_admin_code,
50
+ DefraRubyMocks.configuration.worldpay_merchant_code,
51
+ @resource.order.order_code
52
+ ].join("^")
53
+ end
54
+
55
+ def order_value
56
+ @resource.order.total_amount.to_s
57
+ end
58
+
59
+ def payment_status
60
+ return :REFUSED if @resource.company_name.include?("reject")
61
+ return :STUCK if @resource.company_name.include?("stuck")
62
+
63
+ :AUTHORISED
64
+ end
65
+
66
+ def generate_mac(status)
67
+ data = [
68
+ order_key,
69
+ order_value,
70
+ "GBP",
71
+ status,
72
+ DefraRubyMocks.configuration.worldpay_mac_secret
73
+ ]
74
+
75
+ Digest::MD5.hexdigest(data.join).to_s
76
+ end
77
+
78
+ def generate_response(success_url, failure_url)
79
+ status = payment_status
80
+
81
+ WorldpayResponse.new(
82
+ status == :AUTHORISED ? success_url : failure_url,
83
+ @url_format == :new ? "?" : "&",
84
+ order_key,
85
+ generate_mac(status),
86
+ order_value,
87
+ status,
88
+ @reference
89
+ )
90
+ end
91
+ end
92
+ end