defra_ruby_mocks 1.1.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +52 -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 +33 -12
  6. data/app/services/defra_ruby_mocks/{worldpay_request_service.rb → worldpay_payment_service.rb} +3 -9
  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 +71 -52
  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/lib/defra_ruby_mocks/engine.rb +2 -1
  15. data/lib/defra_ruby_mocks/missing_resource_error.rb +9 -0
  16. data/lib/defra_ruby_mocks/unrecognised_worldpay_request_error.rb +5 -0
  17. data/lib/defra_ruby_mocks/version.rb +1 -1
  18. data/spec/dummy/log/development.log +180 -0
  19. data/spec/dummy/log/test.log +1287 -323
  20. data/spec/examples.txt +99 -56
  21. data/spec/fixtures/{worldpay_request_invalid.xml → payment_request_invalid.xml} +0 -0
  22. data/spec/fixtures/{worldpay_request_valid.xml → payment_request_valid.xml} +0 -0
  23. data/spec/fixtures/refund_request_invalid.xml +6 -0
  24. data/spec/fixtures/refund_request_valid.xml +11 -0
  25. data/spec/fixtures/unrecognised_request.xml +6 -0
  26. data/spec/requests/worldpay_spec.rb +87 -26
  27. data/spec/services/{worldpay_request_service_spec.rb → worldpay_payment_service_spec.rb} +25 -22
  28. data/spec/services/worldpay_refund_service_spec.rb +68 -0
  29. data/spec/services/worldpay_request_handler_service_spec.rb +79 -0
  30. data/spec/services/worldpay_resource_service_spec.rb +112 -0
  31. data/spec/services/worldpay_response_service_spec.rb +185 -58
  32. data/spec/support/helpers/xml_matchers.rb +19 -0
  33. metadata +50 -17
  34. data/app/views/defra_ruby_mocks/worldpay/payments_service.xml.erb +0 -4
  35. data/lib/defra_ruby_mocks/missing_registration_error.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 8fcd5775c21149e97c7c65f92b1f469adf065565
4
- data.tar.gz: b056b6419ada5eb1bf86dcabaee805cf6eae155a
2
+ SHA256:
3
+ metadata.gz: 49581eea82699de82215ca95ff2dab0ff58b782e9c322003859bf211669ae717
4
+ data.tar.gz: 9ee91cdb992f56c2fee2a71be9c4c19d2289b19ea03c29e61076e23f064b19a6
5
5
  SHA512:
6
- metadata.gz: b6239c9c0d4ea46e2cb47b89dc44c67df263fcddf2704f85b46536529f6db67427f3f830d4842c0943f2eb9eb61558e92d08ac0d2bfd0bd81ec3b010f66f5de7
7
- data.tar.gz: 494e1b4a8ef9bf6645be512e915faf5c41dfbc0927da4eb2dcda28f52bef8389adc7bd944437c01aca8ba8e40f10565fd6a0f02ad5a5377e773a22b7011cc141
6
+ metadata.gz: ceeb708b2dd35c00eca628e935edc3e6db111cb731a6cb2721885f00dc7618d1778ff0ad4487b1f9b131e1db9c193ed222b74a148623dcfc84e1de2e55fc842b
7
+ data.tar.gz: 2db7ba1c4d5c1c484b414949e04622809fcd906767c8af69990d19709e6e210f968fbc6657af3f2adfa561e938398e79211b95c4a3cca95224c380ed377b7660
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
 
@@ -108,6 +108,8 @@ The list of possible statuses was taken from
108
108
 
109
109
  When mounted into an app you can simulate interacting with the Worldpay hosted pages service.
110
110
 
111
+ #### Payments
112
+
111
113
  Making a payment with Worldpay essentially comes in 2 stages
112
114
 
113
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
@@ -120,6 +122,54 @@ This Worldpay mock replicates those 2 interactions with the following urls
120
122
  - `../worldpay/payments-service`
121
123
  - `../worldpay/dispatcher`
122
124
 
125
+ ##### Cancelled payments
126
+
127
+ The engine has the ability to mock a user cancelling a payment when on the Worldpay site. To have the mock return a cancelled payment response just ensure the registration's company name includes the word `cancel` (case doesn't matter).
128
+
129
+ If it does the engine will redirect back to the cancelled url instead of the success url provided, plus set the payment status to `CANCELLED`.
130
+
131
+ This allows us to test how the application handles Worldpay responding with a cancelled payment response.
132
+
133
+ ##### Errored payments
134
+
135
+ The engine has the ability to Worldpay erroring during a payment. To have the mock return an errored payment response just ensure the registration's company name includes the word `error` (case doesn't matter).
136
+
137
+ If it does the engine will redirect back to the error url instead of the success url provided, plus set the payment status to `ERROR`.
138
+
139
+ This allows us to test how the application handles Worldpay responding with an errored payment response.
140
+
141
+ ##### Pending payments
142
+
143
+ The engine has the ability to also mock Worldpay marking a payment as pending. To have the mock return a payment pending response just ensure the registration's company name includes the word `pending` (case doesn't matter).
144
+
145
+ If it does the engine will redirect back to the pending url instead of the success url provided, plus set the payment status to `SENT_FOR_AUTHORISATION`.
146
+
147
+ This allows us to test how the application handles Worldpay responding with a payment pending response.
148
+
149
+ ##### Refused payments
150
+
151
+ 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).
152
+
153
+ 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`.
154
+
155
+ This allows us to test how the application handles both successful and unsucessful Worldpay payments.
156
+
157
+ ##### Stuck payments
158
+
159
+ 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).
160
+
161
+ If it does the engine will not redirect back to the service, but instead render a 'You're stuck!' page.
162
+
163
+ This allows us to test how the application handles Worldpay not returning after we redirect a user to them.
164
+
165
+ #### Refunds
166
+
167
+ Requesting a refund from Worldpay is a single step process.
168
+
169
+ 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
170
+
171
+ 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.
172
+
123
173
  #### Configuration
124
174
 
125
175
  In order to use the Worldpay mock you'll need to provide additional configuration details
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
 
@@ -1,27 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DefraRubyMocks
4
- class WorldpayController < ApplicationController
4
+ class WorldpayController < ::DefraRubyMocks::ApplicationController
5
5
 
6
6
  before_action :set_default_response_format
7
7
 
8
8
  def payments_service
9
- response_values = WorldpayRequestService.run(request.body.read)
9
+ @values = WorldpayRequestHandlerService.run(convert_request_body_to_xml)
10
10
 
11
- @merchant_code = response_values[:merchant_code]
12
- @order_code = response_values[:order_code]
13
- @worldpay_id = response_values[:id]
14
- @worldpay_url = response_values[:url]
15
-
16
- respond_to :xml
17
- rescue StandardError
11
+ render_payment_response if @values[:request_type] == :payment
12
+ render_refund_response if @values[:request_type] == :refund
13
+ rescue StandardError => e
14
+ Rails.logger.error("MOCKS: Worldpay payments service error: #{e.message}")
18
15
  head 500
19
16
  end
20
17
 
21
18
  def dispatcher
22
- success_url = params[:successURL]
23
- redirect_to WorldpayResponseService.run(success_url)
24
- rescue StandardError
19
+ @response = WorldpayResponseService.run(
20
+ success_url: params[:successURL],
21
+ failure_url: params[:failureURL],
22
+ pending_url: params[:pendingURL],
23
+ cancel_url: params[:cancelURL],
24
+ error_url: params[:errorURL]
25
+ )
26
+
27
+ if @response.status == :STUCK
28
+ render formats: :html, action: "stuck", layout: false
29
+ else
30
+ redirect_to @response.url
31
+ end
32
+ rescue StandardError => e
33
+ Rails.logger.error("MOCKS: Worldpay dispatcher error: #{e.message}")
25
34
  head 500
26
35
  end
27
36
 
@@ -31,5 +40,17 @@ module DefraRubyMocks
31
40
  request.format = :xml
32
41
  end
33
42
 
43
+ def convert_request_body_to_xml
44
+ Nokogiri::XML(request.body.read)
45
+ end
46
+
47
+ def render_payment_response
48
+ render "defra_ruby_mocks/worldpay/payment_request"
49
+ end
50
+
51
+ def render_refund_response
52
+ render "defra_ruby_mocks/worldpay/refund_request"
53
+ end
54
+
34
55
  end
35
56
  end
@@ -1,12 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DefraRubyMocks
4
- class WorldpayRequestService < BaseService
5
- def run(data)
4
+ class WorldpayPaymentService < BaseService
5
+ def run(merchant_code:, xml:)
6
6
  check_config
7
7
 
8
- xml = Nokogiri::XML(data)
9
- @merchant_code = extract_merchant_code(xml)
8
+ @merchant_code = merchant_code
10
9
  @order_code = extract_order_code(xml)
11
10
 
12
11
  {
@@ -25,11 +24,6 @@ module DefraRubyMocks
25
24
  raise InvalidConfigError, :worldpay_domain if domain.blank?
26
25
  end
27
26
 
28
- def extract_merchant_code(xml)
29
- payment_service = xml.at_xpath("//paymentService")
30
- payment_service.attribute("merchantCode").text
31
- end
32
-
33
27
  def extract_order_code(xml)
34
28
  order = xml.at_xpath("//order")
35
29
  order.attribute("orderCode").text
@@ -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
@@ -3,16 +3,40 @@
3
3
  module DefraRubyMocks
4
4
  class WorldpayResponseService < BaseService
5
5
 
6
- def run(success_url)
7
- parse_reference(success_url)
8
- locate_registration
9
- @order = last_order
10
-
11
- response_url(success_url)
6
+ def run(success_url:, failure_url:, pending_url:, cancel_url:, error_url:)
7
+ urls = {
8
+ success: success_url,
9
+ failure: failure_url,
10
+ pending: pending_url,
11
+ cancel: cancel_url,
12
+ error: error_url
13
+ }
14
+
15
+ parse_reference(urls[:success])
16
+ @resource = WorldpayResourceService.run(reference: @reference)
17
+
18
+ generate_response(urls)
12
19
  end
13
20
 
14
21
  private
15
22
 
23
+ WorldpayResponse = Struct.new(:supplied_url, :separator, :order_key, :mac, :value, :status, :reference) do
24
+ def url
25
+ [supplied_url, separator, params].join
26
+ end
27
+
28
+ def params
29
+ [
30
+ "orderKey=#{order_key}",
31
+ "paymentStatus=#{status}",
32
+ "paymentAmount=#{value}",
33
+ "paymentCurrency=GBP",
34
+ "mac=#{mac}",
35
+ "source=WP"
36
+ ].join("&")
37
+ end
38
+ end
39
+
16
40
  def parse_reference(url)
17
41
  path = URI.parse(url).path
18
42
  parts = path.split("/")
@@ -28,71 +52,66 @@ module DefraRubyMocks
28
52
  end
29
53
  end
30
54
 
31
- def locate_registration
32
- @registration = locate_transient_registration || locate_completed_registration
33
-
34
- raise(MissingRegistrationError, @reference) if @registration.nil?
35
- end
36
-
37
- def locate_transient_registration
38
- "WasteCarriersEngine::TransientRegistration"
39
- .constantize
40
- .where(token: @reference)
41
- .first
42
- end
43
-
44
- def locate_completed_registration
45
- "WasteCarriersEngine::Registration"
46
- .constantize
47
- .where(reg_uuid: @reference)
48
- .first
49
- end
50
-
51
- def last_order
52
- @registration.finance_details&.orders&.order_by(dateCreated: :desc)&.first
53
- end
54
-
55
55
  def order_key
56
56
  [
57
57
  DefraRubyMocks.configuration.worldpay_admin_code,
58
58
  DefraRubyMocks.configuration.worldpay_merchant_code,
59
- @order.order_code
59
+ @resource.order.order_code
60
60
  ].join("^")
61
61
  end
62
62
 
63
63
  def order_value
64
- @order.total_amount.to_s
64
+ @resource.order.total_amount.to_s
65
+ end
66
+
67
+ def payment_status
68
+ return :REFUSED if @resource.company_name.include?("reject")
69
+ return :STUCK if @resource.company_name.include?("stuck")
70
+ return :SENT_FOR_AUTHORISATION if @resource.company_name.include?("pending")
71
+ return :CANCELLED if @resource.company_name.include?("cancel")
72
+ return :ERROR if @resource.company_name.include?("error")
73
+
74
+ :AUTHORISED
75
+ end
76
+
77
+ def url(payment_status, urls)
78
+ return urls[:failure] if %i[REFUSED STUCK].include?(payment_status)
79
+ return urls[:pending] if payment_status == :SENT_FOR_AUTHORISATION
80
+ return urls[:cancel] if payment_status == :CANCELLED
81
+ return urls[:error] if payment_status == :ERROR
82
+
83
+ urls[:success]
65
84
  end
66
85
 
67
- def generate_mac
86
+ # Generate a mac that matches what Worldpay would generate
87
+ #
88
+ # For whatever reason, if the payment is cancelled by the user Worldpay does
89
+ # not include the payment status in the mac it generates. Plus the order of
90
+ # things in the array is important.
91
+ def generate_mac(status)
68
92
  data = [
69
93
  order_key,
70
94
  order_value,
71
- "GBP",
72
- "AUTHORISED",
73
- DefraRubyMocks.configuration.worldpay_mac_secret
95
+ "GBP"
74
96
  ]
97
+ data << status unless status == :CANCELLED
98
+ data << DefraRubyMocks.configuration.worldpay_mac_secret
75
99
 
76
100
  Digest::MD5.hexdigest(data.join).to_s
77
101
  end
78
102
 
79
- def query_string
80
- [
81
- "orderKey=#{order_key}",
82
- "paymentStatus=AUTHORISED",
83
- "paymentAmount=#{order_value}",
84
- "paymentCurrency=GBP",
85
- "mac=#{generate_mac}",
86
- "source=WP"
87
- ].join("&")
88
- end
103
+ def generate_response(urls)
104
+ status = payment_status
89
105
 
90
- def response_url(success_url)
91
- if @url_format == :new
92
- "#{success_url}?#{query_string}"
93
- else
94
- "#{success_url}&#{query_string}"
95
- end
106
+ WorldpayResponse.new(
107
+ url(status, urls),
108
+ @url_format == :new ? "?" : "&",
109
+ order_key,
110
+ generate_mac(status),
111
+ order_value,
112
+ status,
113
+ @reference
114
+ )
96
115
  end
97
116
  end
98
117
  end