diff --git a/.gitignore b/.gitignore index 560d1a6..dc5573f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ tmp .yardoc _yardoc doc/ +.tool-versions +.idea/* diff --git a/Gemfile b/Gemfile index 56f82a1..e002d61 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,5 @@ -source 'http://rubygems.org' -ruby '2.1.1' +source 'https://rubygems.org' +ruby '2.5.3' gem 'sinatra' gem 'httparty' @@ -9,6 +9,8 @@ gem 'addressable' group :development do gem 'byebug' end -group :deploy do - gem 'heroku', '~> 3.8.2' + +group :test do + gem 'test-unit' + gem 'rack-test' end diff --git a/Gemfile.lock b/Gemfile.lock index 2c572d1..b34431e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,43 +1,34 @@ GEM - remote: http://rubygems.org/ + remote: https://rubygems.org/ specs: - addressable (2.3.5) - byebug (2.7.0) - columnize (~> 0.3) - debugger-linecache (~> 1.2) - columnize (0.3.6) - debugger-linecache (1.2.0) - excon (0.37.0) - heroku (3.8.3) - heroku-api (~> 0.3.17) - launchy (>= 0.3.2) - netrc (~> 0.7.7) - rest-client (~> 1.6.1) - rubyzip - heroku-api (0.3.18) - excon (~> 0.27) - multi_json (~> 1.8) - httparty (0.10.0) - multi_json (~> 1.0) - multi_xml - json (1.8.1) - launchy (2.4.2) - addressable (~> 2.3) - mime-types (2.3) - multi_json (1.10.1) - multi_xml (0.5.5) - netrc (0.7.7) - rack (1.5.2) - rack-protection (1.5.2) + addressable (2.6.0) + public_suffix (>= 2.0.2, < 4.0) + byebug (10.0.2) + httparty (0.16.3) + mime-types (~> 3.0) + multi_xml (>= 0.5.2) + json (2.1.0) + mime-types (3.2.2) + mime-types-data (~> 3.2015) + mime-types-data (3.2018.0812) + minitest (5.11.3) + multi_xml (0.6.0) + mustermann (1.0.3) + power_assert (1.1.1) + public_suffix (3.0.3) + rack (2.0.6) + rack-protection (2.0.5) rack - rest-client (1.6.7) - mime-types (>= 1.16) - rubyzip (1.1.4) - sinatra (1.4.4) - rack (~> 1.4) - rack-protection (~> 1.4) - tilt (~> 1.3, >= 1.3.4) - tilt (1.4.1) + rack-test (1.1.0) + rack (>= 1.0, < 3) + sinatra (2.0.5) + mustermann (~> 1.0) + rack (~> 2.0) + rack-protection (= 2.0.5) + tilt (~> 2.0) + test-unit (3.2.7) + power_assert + tilt (2.0.9) PLATFORMS ruby @@ -45,7 +36,15 @@ PLATFORMS DEPENDENCIES addressable byebug - heroku (~> 3.8.2) httparty json + minitest + rack-test sinatra + test-unit + +RUBY VERSION + ruby 2.5.3p105 + +BUNDLED WITH + 1.17.3 diff --git a/README.md b/README.md index c22d2dc..ef79615 100644 --- a/README.md +++ b/README.md @@ -1,120 +1,21 @@ -Shopify Offsite Gateway Simulator +Hosted Payment Simulator =========================== -This is a common API that simplifies the onboarding of new payment providers, specifically Offsite Gateways / Hosted Payment Pages, that are looking to be used on [Shopify](http://www.shopify.com). By supporting a common API for redirect, cancel, complete, and callback phases of a payment flow, we are giving gateway implementers a way to integrate with Shopify at any time, without requiring a large amount of customization or validation work or bottlenecks by the Shopify integration team. +This tool will allow you to simulate the redirects and callbacks needed to build an integration using the [Hosted Payment SDK](https://docs.shopify.com/hosted-payment-sdk). It also serves as a [calculator](https://offsite-gateway-sim.shopifycloud.com/calculator) that can be used to verify your signature algorithm. -### Getting Started +To use the simulator please familiarize yourself with the [Hosted Payment SDK](https://docs.shopify.com/hosted-payment-sdk) documentation and then: -Follow these simple steps to get started. +1. Add a payment gateway with "Redirect URL" of `https://offsite-gateway-sim.shopifycloud.com/`. -1. Review the rest of this document -2. Sign up for a free trial of Shopify at http://www.shopify.com/. You will use this shop to place test orders against your offsite gateway. -3. Send the name of this shop to payment-integrations@shopify.com and mention **Universal Offsite Dev Kit** in the subject +2. Add your gateway to a shop (see "[Creating a development store](https://help.shopify.com/api/sdks/hosted-payment-sdk/getting-started#create-a-development-store)" if you don't have one) and activate it using these credentials: -Once we enable developer mode, which normally happens the same day, you'll be ready to proceed with integration testing. + * **Login** - any non-empty value + * **Password** - iU44RWxeik -1. [Sign in](http://www.shopify.com/login) to your Shopify store. -2. Go to [Products](http://www.shopify.com/admin/products) and [add a dummy product](http://docs.shopify.com/manual/your-store/products/create-product). -2. Go to [Settings/Payments](http://www.shopify.com/admin/settings/payments), and select the **Universal Offsite Dev Kit** in the gateway dropdown. -3. Complete the 3 fields, - + ``x_account_id`` - this is an identifier for a test merchant on your system - + ``HMAC key`` - this is a key your gateway will use to verify requests and sign responses - + ``POST URL`` - this is URL on your system that will properly handle [Request Values](#request-values) and is then able to provide proper [Response Values](#response-values) to various return URLs at Shopify -4. Now you're ready to test! From your admin, click the 'view your website' link, and add a product to your cart. On the cart page, click the "Check out" button, enter in some dummy info, and complete the checkout using your gateway. +3. Complete a test purchase on your shop (you may need to add a product first). At the end of checkout you will be redirected to the sceen below. -> We are providing this simple implementation of an Offsite Gateway Sim as a way to demonstrate basics of this new API. If you want to see it in action, leave ``POST URL`` on your **Universal Offsite Dev Kit** empty, or set it to ``https://offsite-gateway-sim.herokuapp.com/``, then try placing another order in your test shop. + -### Payment Flow +The various buttons will allow you to simulate the callbacks and redirects required in your integration. -+ Customer initiates checkout on the Shopify storefront -+ Browser is redirected to gateway's URL using a POST request along with [Request Values](#request-values) (mandatory + whatever else may be available) -+ Gateway verifies ``x_signature`` value and presents their own payment flow to the customer (see [Signing Mechanism](#signing-mechanism)) -+ Customers who exit the payment flow before successfully completing it should be redirected back to ``x_url_cancel`` -+ Customers who complete the payment flow should be redirected back to ``x_url_complete`` with all required [Response Values](#response-values) as query parameters, including ``x_signature`` (see [Signing Mechanism](#signing-mechanism)) -+ We strongly recommend that gateway also POSTs a callback asynchronously to ``x_url_callback`` with the same [Response Values](#response-values). This ensures that order can be completed even in cases where customer's connection to Shopify is terminated prematurely - + HTTP 200 indicates successful receipt of a callback by Shopify. Otherwise up to 5 retries with an interval of at least 60 seconds are recommended - + Duplicate notifications for the same ``x_reference`` are ignored by Shopify - -### Signing Mechanism - -All requests and responses must be signed/verified using ``HMAC-SHA256`` ([HMAC](http://en.wikipedia.org/wiki/Hash-based_message_authentication_code)) where, - -+ ``key`` is a value known to both Shopify (``HMAC key`` field in gateway settings) and the gateway itself -+ ``message`` is a string of all key-value pairs that start with ``x_`` prefix, sorted alphabetically, and concatenated without any separators - + Resulting codes must be hex-encoded and passed as value of ``x_signature`` - + Make sure to use case-insensitive comparison when verifying provided ``x_signature`` values - -For example, - -```ruby -fields = {x_account_id: 123, x_currency: 'USD'} -=> {:x_account_id=>123, :x_currency=>"USD"} -message = fields.sort.join -=> "x_account_id123x_currencyUSD" -OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), 'secret key', message) -=> "06ef4be2654e089b4aa346f970a71988fa3a1452acaa6273573f9db0c32ea355" - -"x_signature=06ef4be2654e089b4aa346f970a71988fa3a1452acaa6273573f9db0c32ea355" -``` - -### Going Live - -As soon as you are confident that your implementation is complete, we'll need to collect some more information about your gateway, - - + Names for any fields that your gateway will require shops to input when setting it up within Shopify. - + Your gateway name - + Label for the ``x_account_id`` field, needs to match your existing terminology, e.g. ``Merchant ID`` or ``Account #`` - + Label for the ``HMAC key`` field, needs to match your existing terminology, e.g. ``Key`` or ``Shared Secret`` - + URL of a POST handler for [Request Values](#request-values) that presents a payment flow to the customer, likely the same one you used to configure *Universal Offsite Dev Kit* gateway during integration testing - + Your gateway's home page URL - + Image to display to customers during checkout process that identifies your gateway's supported payment options (PNG, height: 20px, max width: 340px). You may or may not want to include your gateway's logo along with this list, depending on whether it will be recognized by customers on checkout. - + Finally, please indicate whether or not your gateway supports ``x_test`` mode - - -### Request Values - -| Key | Type | Mandatory | Example | Comment | -| --------------------------------- |:-------------------------------------------------------------:|:---------:|:----------------------------------------:|----------------------------------------------------------------------------------| -| ``x_account_id`` | unicode string | ✓ | Z9s7Yt0Txsqbbx | This is an account identifier assigned to the merchant by the payment processor. | -| ``x_currency`` | [iso-4217](http://en.wikipedia.org/wiki/ISO_4217) | ✓ | USD | | -| ``x_amount`` | decimal | ✓ | 89.99 | | -| ``x_amount_shipping`` | decimal | | 8.99 | | -| ``x_amount_tax`` | decimal | | 11.70 | | -| ``x_reference`` | ascii string | ✓ | 19783 | Unique reference of an order assigned by the merchant. | -| ``x_shop_country`` | [iso-3166-1 alpha-2](http://en.wikipedia.org/wiki/ISO_3166-1) | ✓ | US | | -| ``x_shop_name`` | unicode string | ✓ | Widgets Inc | | -| ``x_transaction_type`` | ascii string | | sale | | -| ``x_description`` | unicode string | | Order #123 | | -| ``x_invoice`` | unicode string | | #123 | | -| ``x_test`` | true/false | ✓ | true | Indicates whether or not this request should be processed in test mode (if supported). | -| ``x_customer_first_name`` | unicode string | | Boris | | -| ``x_customer_last_name`` | unicode string | | Slobodin | | -| ``x_customer_email`` | unicode string | | boris.slobodin@example.com | | -| ``x_customer_phone`` | unicode string | | +1-613-987-6543 | | -| ``x_customer_shipping_city`` | unicode string | | Toronto | | -| ``x_customer_shipping_company`` | unicode string | | Shopify Toronto | | -| ``x_customer_shipping_address1`` | unicode string | | 241 Spadina Ave | | -| ``x_customer_shipping_address2`` | unicode string | | | | -| ``x_customer_shipping_state`` | unicode string | | ON | | -| ``x_customer_shipping_zip`` | unicode string | | M5T 3A8 | | -| ``x_customer_shipping_country`` | [iso-3166-1 alpha-2](http://en.wikipedia.org/wiki/ISO_3166-1) | | CA | | -| ``x_customer_shipping_phone`` | unicode string | | +1-416-123-4567 | | -| ``x_url_callback`` | url | ✓ | https://myshopify.io/ping/1 | URL to which a callback notification should be sent asynchronously. | -| ``x_url_cancel`` | url | ✓ | https://myshopify.io | URL to which customer must be redirected when they wish to quit payment flow and return to the merchant's site. | -| ``x_url_complete`` | url | ✓ | https://myshopify.io/orders/1/done | URL to which customer must be redirected upon successfully completing payment flow. | -| ``x_timestamp`` | [iso-8601](http://en.wikipedia.org/wiki/ISO_8601) in UTC | ✓ | 2014-03-24T12:13:12Z | | -| ``x_signature`` | hex string, case-insensitive | ✓ | 3a59e201a9b8692702b8c41dcba476d4a46e5f5c | See [Signing Mechanism](#signing-mechanism). | - -### Response Values - -| Key | Type | Mandatory | Example | Comment | -| ------------------------|:--------------------------------------------------------:|:---------:|------------------------------------------|-------------------------------------------------------------------------| -| ``x_account_id`` | unicode string | ✓ | Z9s7Yt0Txsqbbx | Echo request's ``x_account_id`` | -| ``x_reference`` | ascii string | ✓ | 19783 | Echo request's ``x_reference`` | -| ``x_currency`` | [iso-4217](http://en.wikipedia.org/wiki/ISO_4217) | ✓ | USD | Echo request's ``x_currency`` | -| ``x_test`` | true/false | ✓ | true | Echo request's ``x_test`` | -| ``x_amount`` | decimal | ✓ | 89.99 | Echo request's ``x_amount`` | -| ``x_gateway_reference`` | unicode string | ✓ | 123 | Unique reference for the authorization issued by the payment processor. | -| ``x_timestamp`` | [iso-8601](http://en.wikipedia.org/wiki/ISO_8601) in UTC | ✓ | 2014-03-24T12:15:41Z | | -| ``x_result`` | fixed choice | ✓ | completed | One of: completed, failed, pending | -| ``x_signature`` | hex string, case-insensitive | ✓ | 3a59e201a9b8692702b8c41dcba476d4a46e5f5c | See [Signing Mechanism](#signing-mechanism). | +Please email payment-integrations@shopify.com if you have any questions. diff --git a/app.rb b/app.rb index d23b174..754ebfa 100644 --- a/app.rb +++ b/app.rb @@ -8,13 +8,18 @@ class OffsiteGatewaySim < Sinatra::Base - def initialize + def initialize(base_path: '') + @base_path = base_path @key = 'iU44RWxeik' super end def fields - @fields ||= request.params.select {|k, v| k.start_with? 'x_'} + @fields ||= if request.content_type == 'application/json' + JSON.load(request.body.read) + else + request.params.select { |k, v| k.start_with?('x_') } + end end def request_fields @@ -26,29 +31,52 @@ def response_fields end def sign(fields, key=@key) - Digest::HMAC.hexdigest(fields.sort.join, key, Digest::SHA256) + OpenSSL::HMAC.hexdigest("SHA256", key, fields.sort.join) + end + + def signature_valid? + provided_signature = fields['x_signature'] + expected_signature = sign(fields.reject{|k,_| k == 'x_signature'}) + provided_signature && provided_signature.casecmp(expected_signature) == 0 end get '/' do - erb :get, :locals => {key: @key} + erb :get, :locals => { key: @key } end post '/' do - provided_signature = fields['x_signature'] - expected_signature = sign(fields.reject{|k,_| k == 'x_signature'}) - signature_ok = provided_signature && provided_signature.casecmp(expected_signature) == 0 - erb :post, :locals => {signature_ok: signature_ok} + erb :post, :locals => { signature_ok: signature_valid? } + end + + post '/incontext' do + erb :incontext, :locals => { signature_ok: signature_valid? } end get '/calculator' do erb :calculator, :locals => { request_fields: request_fields, response_fields: response_fields, - signature: sign(fields.delete_if { |_, v| v.nil? }, params['secret_key'] || @key) + signature: sign(fields.delete_if { |_, v| v.empty? }, params['secret_key'] || @key) } end - post '/execute/:action' do |action| + post %r{/(capture|refund|void)} do |action| + content_type :json + + if signature_valid? + [200, {}, fields.merge(x_result: 'pending', + x_gateway_reference: SecureRandom.hex, + x_timestamp: Time.now.utc.iso8601).to_json] + else + [401, {}, { x_status: 'failed', x_error_message: 'Invalid signature' }.to_json] + end + end + + get '/notification' do + erb :notification + end + + post '/execute/?:action?' do |action| ts = Time.now.utc.iso8601 payload = { 'x_account_id' => fields['x_account_id'], @@ -59,25 +87,34 @@ def sign(fields, key=@key) 'x_result' => action, 'x_gateway_reference' => SecureRandom.hex, 'x_timestamp' => ts - } + } + %w(x_transaction_type x_message x_result).each do |field| + payload[field] = fields[field] if fields[field] + end + + if action == "failed" + payload['x_message'] = "This is a custom error message." + end payload['x_signature'] = sign(payload) result = {timestamp: ts} - redirect_url = Addressable::URI.parse(fields['x_url_complete']) - redirect_url.query_values = payload + redirect_url = if fields['x_url_complete'] + uri = Addressable::URI.parse(fields['x_url_complete']) + uri.query_values = payload + uri + end + if request.params['fire_callback'] == 'true' callback_url = fields['x_url_callback'] response = HTTParty.post(callback_url, body: payload) if response.code == 200 - result[:redirect] = redirect_url + result[:redirect] = redirect_url if redirect_url else result[:error] = response end else - result[:redirect] = redirect_url + result[:redirect] = redirect_url if redirect_url end result.to_json end - run! if app_file == $0 - end diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..fcb4aed --- /dev/null +++ b/circle.yml @@ -0,0 +1,3 @@ +test: + override: + - bundle exec ruby test/app_test.rb diff --git a/dev.yml b/dev.yml new file mode 100644 index 0000000..c189279 --- /dev/null +++ b/dev.yml @@ -0,0 +1,25 @@ +name: hosted-payment-sim + +up: + - homebrew: + - openssl + - ruby: 2.5.3 + - railgun + - bundler + +commands: + console: + syntax: '' + desc: 'open a console with the app loaded' + run: bundle exec irb -r ./app.rb + server: + syntax: '' + desc: 'start a rack server' + run: bundle exec rackup -o 192.168.64.1 -p 20000 + test: + syntax: '' + desc: 'run app tests' + run: bundle exec ruby test/app_test.rb + +open: + 'Hosted Payment Simulator': https://hosted-payment-sim.myshopify.io diff --git a/offsite-gateway-sim-page.png b/offsite-gateway-sim-page.png new file mode 100644 index 0000000..7b76621 Binary files /dev/null and b/offsite-gateway-sim-page.png differ diff --git a/public/dev-kit-screenshot.png b/public/dev-kit-screenshot.png deleted file mode 100644 index b10639c..0000000 Binary files a/public/dev-kit-screenshot.png and /dev/null differ diff --git a/public/style.css b/public/style.css index 78fce2d..b653115 100644 --- a/public/style.css +++ b/public/style.css @@ -71,4 +71,8 @@ code { } table.pure-table th, table.pure-table td { padding: 3px 5px 3px 5px; -} \ No newline at end of file +} + +.pure-form-aligned .pure-control-group label { + width: 15em; +} diff --git a/railgun.yml b/railgun.yml new file mode 100644 index 0000000..04a2297 --- /dev/null +++ b/railgun.yml @@ -0,0 +1,17 @@ +# https://dev-accel.shopify.io/dev/railgun/Railgun-Config +name: hosted-payment-sim + +vm: + image: /opt/dev/misc/railgun-images/default + ip_address: 192.168.64.37 + memory: 2G + cores: 2 + +volumes: + root: 2G + +services: + - nginx + +hostnames: + - hosted-payment-sim.myshopify.io: { proxy_to_host_port: 20000 } diff --git a/request_fields.yml b/request_fields.yml index baf4c72..de6b5c0 100644 --- a/request_fields.yml +++ b/request_fields.yml @@ -1,116 +1,144 @@ -- +- key: x_account_id - name: "Account ID" - placeholder: Z9s7Yt0Txsqbbx -- - key: x_currency - name: Currency - placeholder: USD -- + name: x_account_id + placeholder: "Z9s7Yt0Txsqbbx" +- key: x_amount - name: Amount + name: x_amount placeholder: "89.99" -- - key: x_amount_shipping - name: "Amount Shipping" - placeholder: "8.99" -- - key: x_amount_tax - name: "Amount Tax" - placeholder: "11.70" -- +- + key: x_currency + name: x_currency + placeholder: "USD" +- key: x_reference - name: Reference + name: x_reference placeholder: "19783" -- +- + key: x_shopify_order_id + name: x_shopify_order_id + placeholder: "5160080594" +- key: x_shop_country - name: "Shop Country" - placeholder: US -- + name: x_shop_country + placeholder: "US" +- key: x_shop_name - name: "Shop Name" + name: x_shop_name placeholder: "Widgets Inc" -- - key: x_transaction_type - name: "Transaction Type" - placeholder: sale -- +- key: x_description - name: Description - placeholder: Order -- + name: x_description + placeholder: "Order" +- key: x_invoice - name: Invoice + name: x_invoice placeholder: "#123" -- +- key: x_test - name: Test + name: x_test placeholder: "true" -- +- key: x_customer_first_name - name: "Customer First Name" - placeholder: Boris -- + name: x_customer_first_name + placeholder: "Boris" +- key: x_customer_last_name - name: "Customer Last Name" - placeholder: Slobodin -- + name: x_customer_last_name + placeholder: "Slobodin" +- key: x_customer_email - name: "Customer Email" - placeholder: boris.slobodin@example.com -- + name: x_customer_email + placeholder: "boris.slobodin@example.com" +- key: x_customer_phone - name: "Customer Phone" - placeholder: +1-613-987-6543 -- + name: x_customer_phone + placeholder: "6139876543" +- key: x_customer_shipping_city - name: "Shipping City" - placeholder: Toronto -- + name: x_customer_shipping_city + placeholder: "Toronto" +- key: x_customer_shipping_company - name: "Shipping Company" + name: x_customer_shipping_company placeholder: "Shopify Toronto" -- +- + key: x_customer_shipping_first_name + name: x_customer_shipping_first_name + placeholder: "Boris" +- + key: x_customer_shipping_last_name + name: x_customer_shipping_last_name + placeholder: "Slobodin" +- key: x_customer_shipping_address1 - name: "Shipping Address 1" + name: x_customer_shipping_address1 placeholder: "241 Spadina Ave" -- +- key: x_customer_shipping_address2 - name: "Shipping Address 2" - placeholder: Suite 200 -- + name: x_customer_shipping_address2 + placeholder: "Suite 200" +- key: x_customer_shipping_state - name: "Shipping State" - placeholder: true -- + name: x_customer_shipping_state + placeholder: "true" +- key: x_customer_shipping_zip - name: "Shipping Zip" + name: x_customer_shipping_zip placeholder: "M5T 3A8" -- +- key: x_customer_shipping_country - name: "Shipping Country" - placeholder: CA -- + name: x_customer_shipping_country + placeholder: "CA" +- key: x_customer_shipping_phone - name: "Shipping Phone" - placeholder: +1-416-123-4567 -- + name: x_customer_shipping_phone + placeholder: "4161234567" +- + key: x_customer_billing_city + name: x_customer_billing_city + placeholder: "Toronto" +- + key: x_customer_billing_company + name: x_customer_billing_company + placeholder: "Shopify Toronto" +- + key: x_customer_billing_address1 + name: x_customer_billing_address1 + placeholder: "241 Spadina Ave" +- + key: x_customer_billing_address2 + name: x_customer_billing_address2 + placeholder: "Suite 200" +- + key: x_customer_billing_state + name: x_customer_billing_state + placeholder: "ON" +- + key: x_customer_billing_zip + name: x_customer_billing_zip + placeholder: "M5T 3A8" +- + key: x_customer_billing_country + name: x_customer_billing_country + placeholder: "CA" +- + key: x_customer_billing_phone + name: x_customer_billing_phone + placeholder: "4161234567" +- key: x_url_callback - name: "URL Callback" + name: x_url_callback placeholder: "https://myshopify.io/ping/1" -- +- key: x_url_cancel - name: "URL Cancel" + name: x_url_cancel placeholder: "https://myshopify.io" -- +- key: x_url_complete - name: "URL Complete" + name: x_url_complete placeholder: "https://myshopify.io/orders/1/done" -- - key: x_timestamp - name: Timestamp - placeholder: "2014-03-24 12:13:12 +00:00" -- +- key: secret_key - name: Secret Key - placeholder: iU44RWxeik \ No newline at end of file + name: secret_key + placeholder: "iU44RWxeik" diff --git a/response_fields.yml b/response_fields.yml index f6d8c5a..184d5a9 100644 --- a/response_fields.yml +++ b/response_fields.yml @@ -1,36 +1,40 @@ -- +- key: x_account_id - name: "Account ID" - placeholder: Z9s7Yt0Txsqbbx -- + name: x_account_id + placeholder: "Z9s7Yt0Txsqbbx" +- key: x_reference - name: Reference + name: x_reference placeholder: "19783" -- +- key: x_currency - name: Currency - placeholder: USD -- + name: x_currency + placeholder: "USD" +- key: x_test - name: Test + name: x_test placeholder: "true" -- +- key: x_amount - name: Amount + name: x_amount placeholder: "89.99" -- +- key: x_gateway_reference - name: "Gateway Reference" + name: x_gateway_reference placeholder: "123" -- - key: x_timespamp - name: Timestamp +- + key: x_timestamp + name: x_timestamp placeholder: "2014-03-24T12:15:41Z" -- +- key: x_result - name: Result - placeholder: completed -- + name: x_result + placeholder: "completed" +- + key: x_message + name: x_message + placeholder: "custom error message" +- key: secret_key - name: Secret Key - placeholder: iU44RWxeik \ No newline at end of file + name: secret_key + placeholder: "iU44RWxeik" diff --git a/shipit.yml b/shipit.yml deleted file mode 100644 index 67b3655..0000000 --- a/shipit.yml +++ /dev/null @@ -1,6 +0,0 @@ -dependencies: - override: [] - -deploy: - override: - - push-to-heroku offsite-gateway-sim diff --git a/test/app_test.rb b/test/app_test.rb new file mode 100644 index 0000000..97fc819 --- /dev/null +++ b/test/app_test.rb @@ -0,0 +1,111 @@ +ENV['RACK_ENV'] = 'test' + +require_relative '../app' +require 'test/unit' +require 'rack/test' +require 'yaml' + +class OffsiteGatewaySimTest < Test::Unit::TestCase + include Rack::Test::Methods + + def app + OffsiteGatewaySim + end + + RESPONSE_FIELDS = { + x_account_id: 'test123', + x_reference: '12345', + x_currency: 'USD', + x_test: true, + x_amount: 89.99, + x_gateway_reference: 123, + x_timestamp: '2014-03-24T12:15:41Z' + } + + REQUEST_FIELDS = begin + YAML.load_file('request_fields.yml').each.inject({}) do |h, field| + h[field['key']] = field['placeholder'] + h + end + end + + def test_get_root + get '/' + assert last_response.ok? + assert_equal 200, last_response.status + end + + def test_get_calculator + get '/calculator' + assert last_response.ok? + end + + def test_post_root_signature_validation + fields = RESPONSE_FIELDS.merge( + some_param: 'a_param_that_should_be_excluded', + x_result: 'completed', + x_signature: '2edd2a8f13d810560b7c09dd02c9b331f97961c0f5733b66b354ff5fa9da4716' + ) + + post '/', fields + assert last_response.ok? + assert last_response.body.include?('yes.png'), 'Signature\'s do not match' + end + + def test_post_completed + post 'execute/completed', REQUEST_FIELDS + assert last_response.ok? + assert last_response.body.include?('completed') + end + + def test_post_failed_with_custom_message + post 'execute/failed', REQUEST_FIELDS + assert last_response.ok? + assert last_response.body.include?('failed') + assert last_response.body.include?('x_message') + end + + %w(capture refund void).each do |type| + define_method "test_post_#{type}_with_valid_signature" do + fields = sign(RESPONSE_FIELDS.merge(x_transaction_type: type)) + + expected_response_keys = %w( + x_account_id + x_reference + x_currency + x_test + x_amount + x_gateway_reference + x_timestamp + x_transaction_type + x_signature + x_result + ) + + post "/#{type}", fields + + assert last_response.ok? + assert_equal 'application/json', last_response.header['content-type'] + assert expected_response_keys.all? { |k| JSON.parse(last_response.body).key? k } + end + + define_method "test_post_#{type}_with_invalid_signature" do + fields = RESPONSE_FIELDS.merge( + x_transaction_type: type, + x_signature: 'incorrect' + ) + + post "/#{type}", fields + + assert_equal 401, last_response.status + last_response.body.include?('failed') + end + end + + private + + def sign(fields) + signature = OpenSSL::HMAC.hexdigest("SHA256", 'iU44RWxeik', fields.sort.join) + fields.merge(x_signature: signature) + end +end diff --git a/views/get.erb b/views/get.erb index 6257500..d3e4dff 100644 --- a/views/get.erb +++ b/views/get.erb @@ -1,11 +1,9 @@
In order to use this simulator, you must set up Universal Offset Dev Kit in your Shopify store as follows,
+In order to use this simulator, you must add a hosted payment gateway with Redirect URL of https://offsite-gateway-sim.herokuapp.com/ to your Partner Dashboard, and activate it in payment settings with,
x_account_id: (any non-empty value)
HMAC key: <%= key %>
-
- POST URL: https://offsite-gateway-sim.herokuapp.com/ (or leave empty)
-You can also send capture/refund/void notifications by clicking here
+ diff --git a/views/incontext.erb b/views/incontext.erb new file mode 100644 index 0000000..fa63f3e --- /dev/null +++ b/views/incontext.erb @@ -0,0 +1,51 @@ + + + +<% fields = request.params.select {|k, v| k.start_with? 'x_'} %> +