Compare commits

..

3 Commits

Author SHA1 Message Date
J. Paul Daigle
ccea3a4ea0 Merge pull request #20 from heisler3030/add_error_checking
Add error checking
2014-11-05 10:51:07 -05:00
Hamish Eisler
9c24e3acee add response code 2014-11-04 16:42:10 -08:00
Hamish Eisler
44253dd34e throw errors on HTTP error code 2014-11-04 16:26:00 -08:00
34 changed files with 249 additions and 1269 deletions

9
.gitignore vendored
View File

@ -2,12 +2,3 @@
test.rb
Gemfile.lock
pkg
.c9
.ruby-version
.ruby-gemset
*.swo
*.swp
bitpaykey.pem
constants.txt
coverage/
.pem.data

View File

@ -1,5 +1,5 @@
sudo: false
rvm:
- 2.1.10
- 2.2.5
- 2.3.1
- 1.9.2
- 1.9.3
- 2.0.0
- 2.1.0

View File

@ -1,24 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [2.4.4] - 2015-04-14
### Changed
- Separated key utilities into its own Gem
## [2.4.3] - 2015-04-13
### Changed
- Loosened production gem requirements from patch level to major level
## [2.4.2] - 2015-03-11
### Fixed
- GitHub issue 39: handling post paths that include a ? and require a token. A workaround exists for this issue.
## [2.4.1] - 2015-03-11
### Fixed
- GitHub issue 40: error for endpoints that did not return a 'data' field
## [2.4.0] - 2015-03-05
### Changed
- Add feature: Accept refunds
- Fix Bug: Accept bitcoin payments like 0.003

233
GUIDE.md
View File

@ -1,233 +0,0 @@
# Using the BitPay Ruby Client Library
## Prerequisites
You must have a BitPay merchant account to use this library. It's free to [sign-up for a BitPay merchant account](https://bitpay.com/start).
Once you have a BitPay merchant account, you will need [a working BitPay Access Token](/api/getting-access.html) this can be done either [via the library](#pairing) or manually in [the BitPay Dashboard](https://bitpay.com/tokens).
## Quick Start
### Installation
```bash
gem install bitpay-sdk
```
In your Gemfile:
```ruby
gem 'bitpay-sdk', :require => 'bitpay_sdk'
```
Or directly:
```ruby
require 'bitpay_sdk'
```
### Configuration
The bitpay client creates a cryptographically secure connection to your server by pairing an API code with keys generated by the library. The client can be initialized with pre-existing keys passed in as a pem file, or paired if initialized with a pem file and a tokens hash. Examples can be found in the cucumber step helpers.
## Pairing
Most calls to the BitPay REST API require that your client is paired with the bitpay.com server. To pair with bitpay.com you need to have an approved merchant account.
Your client can be paired via the `pos` (point-of-sale) or `merchant` facade (or both). The `pos` facade allows for invoices to be created. The `merchant` facade has broader privileges to view all invoices, bills, and ledger entries, as well as to issue refunds. Consider the level of access required when you pair your client.
### A quick note on keys
The BitPay client gem includes the BitPay KeyUtilities gem, which can be used to generate new public private key pairs which it returns in PEM format. However, there are no methods which save the keys anywhere, so it is your responsibility to store the PEM file somewhere secure.
### BitPay authentication
BitPay authentication depends on four parts:
1. An account on our servers.
1. A token shared between the client and the server.
1. A public key, shared between the client and the server.
1. A private key, held exclusively by the client.
In order to complete authentication, you have to associate your private key with a token, and associate that token with an account. Once this authentication is complete, as long as you have the private key, you never have to authenticate again. The token you created will always be associated with that private key, so any time you create a new bitpay client object with that key, it is authenticated with BitPay. This is true whether you use the ruby-client, python client, or no client at all, the key is the important thing.
There are two ways to authenticate, from the client side or the server side. The Ruby Client supports both.
To pair from the server side, you log in to the BitPay server, navigate to dashboard/merchant/api-tokens, and create a new token. This creates a new token, which is associated with your account. It is not associated with a key, so we provide a pairing code that you can use as a one time secret to associate the token with a key. From the client side, you can use the client.pair_pos_client(<pairing_code>) method to associate that method with a key held by the client.
To pair from the client side, you use the client to call the /tokens endpoint on the server with no parameters. This creates a token on the server and associates that token with a public key. What it doesn't do is associate that token to an account (because we don't know what account to associate with). This call returns a pairing code, which is a one time secret that allows you to find the token you just created. In order to associate the token with an account, you log in to the BitPay server, and use the dashboard/merchant/api-tokens interface to associate the token with a specific account. And example of client side pairing is shown below.
### Pairing Programatically
If you are developing a client with built-in pairing capability, you can pair programattically using the `pair_client` method. This method can be called in two ways:
* `pair_client()` will perform a client-initiated pairing, and will provide a pairing code that can be entered at https://bitpay.com/dashboard/merchant/api-tokens to assign either `merchant` or `pos` facade.
* `pair_client('pairing_code')` will complete a server-initiated pairing, when provided a pre-generated pairing code from https://bitpay.com/dashboard/merchant/api-tokens. In this case, the `pos` facade will be automatically assigned.
This is an example of creating a paired client with the BitPay toolset.
```bash
$ gem install bitpay-sdk
Successfully installed bitpay-sdk-2.2.0
1 gem installed
$ irb
2.1.1 :001 > require 'bitpay_sdk'
=> true
2.1.2 :002 > pem = BitPay::KeyUtils.generate_pem
=> "-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIH8oSTRm8lVMTVOsDZleIB8AmkiuHnp+ctEknqeUmZahoAcGBSuBBAAK\noUQDQgAEbjhdKA+X8NEKgcbHhyJaBMvePV7Sj6AQuOMQzuZYdskdkPY1/jlfQwNG\n4GVd/zSw4uhfukw/SDBOEKlQGVAmxQ==\n-----END EC PRIVATE KEY-----\n"
2.1.1 :002 > client = BitPay::SDK::Client.new(api_uri: 'https://test.bitpay.com', pem: pem)
=> #<BitPay::SDK::Client:0x000000019c6d40 @pem="---... @tokens={}>
2.1.1 :003 > client.pair_client()
=> {"data"=>[{"policies"=>[{"policy"=>"id", "method"=>"inactive", "params"=>["Tf49SFeiUAtytFEW2EUqZgWj32nP51PK73M"]}], "token"=>"BKQyVdaGQZAArdkkSuvtZN5gcN2355c8vXLj5eFPkfuK", "dateCreated"=>1422474475162, "pairingExpiration"=>1422560875162, "pairingCode"=>"Vy76yTh"}]}
```
As described above, using the value from the `pairingCode` element, visit https://test.bitpay.com/api-tokens and search to register for the appropriate facade. That client is now paired. As previously mentioned, you must save the pem string you generated in order to use the client again.
## General Usage
### Initialize the client
```ruby
client = BitPay::SDK::Client.new(pem: File.read('bitpaykey.pem'))
```
Optional parameters:
* `api_uri` - specify a different api endpoint (e.g. 'https://test.bitpay.com'). Ensure no trailing slash.
* `tokens` - pass a stored hash of bitpay API tokens
* `user-agent` - specify a custom user-agent value
* `debug: true` - enable HTTP request logging to $stdout
* `insecure: true` - disable HTTPs certificate validation (for local test environments)
### Create a new bitcoin invoice
```ruby
invoice = client.create_invoice(price: <price>, currency: <currency>)
```
With invoice creation, `price` and `currency` are the only required fields. If you are sending a customer from your website to make a purchase, setting `redirectURL` will redirect the customer to your website when the invoice is paid.
Response will be a hash with information on your newly created invoice. Send your customer to the `url` to complete payment:
```javascript
{
"url": "https://bitpay.com/invoice?id=NKaqMuZWy3BAcP77RdkEEv",
"paymentUrls": {
"BIP21": "bitcoin:mvYRECDxKPaPHnjNz9ZxiTpbx29xYNoRy4?amount=0.3745",
"BIP72": "bitcoin:mvYRECDxKPaPHnjNz9ZxiTpbx29xYNoRy4?amount=0.3745&r=https://bitpay.com/i/NKaqMuZWy3BAcP77RdkEEv",
"BIP72b": "bitcoin:?r=https://bitpay.com/i/NKaqMuZWy3BAcP77RdkEEv",
"BIP73": "https://bitpay.com/i/NKaqMuZWy3BAcP77RdkEEv"
},
"status": "new",
"btcPrice": "0.3745",
"btcDue": "0.3745",
"price": 148,
"currency": "USD",
"exRates": {
"USD": 395.20000000000005
},
"invoiceTime": 1415987168612,
"expirationTime": 1415988068612,
"currentTime": 1415987168629,
"guid": "438e8237-fff1-483c-81b4-dc7dba28922a",
"id": "NKaqMuZWy3BAcP77RdkEEv",
"transactions": [
],
"btcPaid": "0.0000",
"rate": 395.2,
"exceptionStatus": false,
"token": "9kZgUXFb5AC6qMuLaMpP9WopbM8X2UjMhkphKKdaprRbSKgUJNE6JNTX8bGsmgxKKv",
"buyer": {
}
}
```
There are many options available when creating invoices, which are listed in the [BitPay API documentation](https://bitpay.com/bitcoin-payment-gateway-api).
### Get invoice status
The ruby library provides two methods for fetching an existing invoice:
```ruby
# For authorized clients with a 'merchant' token
client.get_invoice(id: 'PvVhgBfA7wKPWhuVC24rJo')
# For non-authenticated clients (public facade)
# Returns the public subset of invoice fields
client.get_public_invoice(id: 'PvVhgBfA7wKPWhuVC24rJo')
```
### Create a refund request
Clients with a `merchant` token can initiate a refund request for a paid invoice:
```ruby
client.refund_invoice(id: '6pbV13VBZfGFJ8BBmXmLZ8', params: {amount: 10, currency: 'USD'})
```
Refund rules:
* Invoices cannot be refunded prior to 6 blockchain confirmations
* Invoices without `["flags"]["refundable"] == true` must specify a `bitcoinAddress` param (one was not provided as part of the transaction)
* Invoices that are paid in full must specify an `amount` and `currency` param to indicate the amount to be refunded
### View Refund Requests
The ruby library provides two methods for viewing refund requests. Both require a `merchant` token.
```ruby
# To get an array of all refunds against a specific invoice
client.get_all_refunds_for_invoice(id: 'PvVhgBfA7wKPWhuVC24rJo')
# To get a specific refund for a specific invoice
client.get_refund(id: 'JB49z2MsDH7FunczeyDS8j', request_id: '4evCrXq4EDXk4oqDXdWQhX')
```
### Cancel Refund Requests
Requires a `merchant` token.
```ruby
client.cancel_refund(id: 'JB49z2MsDH7FunczeyDS8j', request_id: '4evCrXq4EDXk4oqDXdWQhX')
```
### Make a HTTP request directly against the REST API
For API tasks which lack a dedicated library method, BitPay provides methods that will automatically apply the proper cryptographic parameters to a request.
```ruby
client.send_request("GET", "invoices/JB49z2MsDH7FunczeyDS8j", facade: 'merchant')
## This request is identical to:
token = client.get_token("merchant")
client.get(path: "invoices/JB49z2MsDH7FunczeyDS8j", token: token)
## post requests are also possible
token = client.get_token("merchant")
client.post(path: "tokens", token: token, params: {facade: "pos"}) #returns a new token with pairing code
## equivalent to
client.send_request("POST", "tokens", facade: 'merchant', params: {facade: 'pos'})
```
Usage:
* Specify HTTP verb and REST endpoint
* Specifying a `facade` will fetch and apply the corresponding `token`
* Alternatively provide a `token` explicitly
* For `POST` requests, the `params` hash will be included as the message body
## Testnet Usage
During development and testing, take advantage of the [Bitcoin TestNet](https://en.bitcoin.it/wiki/Testnet) by passing a custom `api_uri` option on initialization:
```ruby
BitPay::SDK::Client.new({api_uri: "https://test.bitpay.com/api"})
```
Note that in order to pair with testnet, you will need a pairing code from test.bitpay.com and will need to use the bitpay client with the --test option.
## API Documentation
API Documentation is available on the [BitPay site](https://bitpay.com/api).
## Running the Tests
In order to run the tests, you must have phantomjs installed and on your PATH.
The tests require that environment variables be set for the bitpay server, user name, password, an invoice id for refunds and a valid testnet bitcoin address for refunds. First run:
```bash
$ source ./spec/set_constants.sh https://test.bitpay.com <yourusername> <yourpassword> <a-confirmed-invoice-id> <a-valid-testnet-address>
$ bundle install
$ bundle exec rake
```
Tests are likely to run up against rate limiters on test.bitpay.com if used too frequently. Rake tasks which interact directly with BitPay will not run for the general public.

View File

@ -1,2 +1,6 @@
source 'https://rubygems.org'
gemspec
platform :jruby do
gem 'jruby-openssl'
end

View File

@ -1,4 +1,4 @@
Copyright (C) 2014 BitPay
Copyright (C) 2013 BitPay
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -1,16 +1,68 @@
# BitPay Library for Ruby
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://raw.githubusercontent.com/bitpay/ruby-client/master/LICENSE.md)
[![Travis](https://img.shields.io/travis/bitpay/ruby-client.svg?style=flat-square)](https://travis-ci.org/bitpay/ruby-client)
[![Gem](https://img.shields.io/gem/v/bitpay-sdk.svg?style=flat-square)](https://rubygems.org/gems/bitpay-sdk)
[![Code Coverage](https://img.shields.io/coveralls/bitpay/ruby-client.svg?style=flat-square)](https://coveralls.io/r/bitpay/ruby-client?branch=master)
[![Code Climate](https://img.shields.io/codeclimate/github/bitpay/ruby-client.svg?style=flat-square)](https://codeclimate.com/github/bitpay/ruby-client)
# BitPay Library for Ruby [![](https://secure.travis-ci.org/bitpay/ruby-client.png)](http://travis-ci.org/bitpay/ruby-client)
Powerful, flexible, lightweight interface to the BitPay Bitcoin Payment Gateway API.
The `bitpay-sdk` gem provides all the programattic tools required to implement a ruby client application for the BitPay REST API.
## Installation
## [Getting Started &raquo;](https://github.com/bitpay/ruby-client/blob/master/GUIDE.md)
gem install bitpay-client
In your Gemfile:
gem 'bitpay-client', :require => 'bitpay'
Or directly:
require 'bitpay'
## Basic Usage
To create an invoice:
client = BitPay::Client.new 'YOUR_API_KEY'
invoice = client.post 'invoice', {:price => 10.00, :currency => 'USD'}
With invoice creation, `price` and `currency` are the only required fields. If you are sending a customer from your website to make a purchase, setting `redirectURL` will redirect the customer to your website when the invoice is paid.
Response will be a hash with information on your newly created invoice. Send your customer to the `url` to complete payment:
{
"id" => "DGrAEmbsXe9bavBPMJ8kuk",
"url" => "https://bitpay.com/invoice?id=DGrAEmbsXe9bavBPMJ8kuk",
"status" => "new",
"btcPrice" => "0.0495",
"price" => 10,
"currency" => "USD",
"invoiceTime" => 1383265343674,
"expirationTime" => 1383266243674,
"currentTime" => 1383265957613
}
There are many options available when creating invoices, which are listed in the [BitPay API documentation](https://bitpay.com/bitcoin-payment-gateway-api).
To get updated information on this invoice, make a get call with the id returned:
invoice = client.get 'invoice/DGrAEmbsXe9bavBPMJ8kuk'
## Testnet Usage
During development and testing, take advantage of the [Bitcoin TestNet](https://en.bitcoin.it/wiki/Testnet) by passing a custom `api_uri` option on initialization:
BitPay::Client.new("myAPIKey", {api_uri: "https://test.bitpay.com/api"})
Note that you will need a separate API key for `test.bitpay.com` which can be obtained by registering for a test account at https://test.bitpay.com/start
## API Documentation
API Documentation is available on the [BitPay site](https://bitpay.com/bitcoin-payment-gateway-api).
## RDoc/YARD Documentation
The code has been fully code documented, and the latest version is always available at the [Rubydoc Site](http://rubydoc.info/gems/bitpay-client).
## Running the Tests
$ bundle install
$ bundle exec rake
In addition to a full test suite, there is Travis integration for MRI 1.9, JRuby and Rubinius.
## Found a bug?
Let us know! Send a pull request or a patch. Questions? Ask! We're here to help. We will respond to all filed issues.

View File

@ -1,77 +1,11 @@
require "bundler/gem_tasks"
require 'rspec/core/rake_task'
require 'mongo'
require 'cucumber'
require 'cucumber/rake/task'
require_relative 'config/constants.rb'
require "rake/testtask"
RSpec::Core::RakeTask.new(:spec)
#task :default => :spec
task :default => :default_tasks
Cucumber::Rake::Task.new(:features) do |t|
t.cucumber_opts = "features --format pretty"
desc "Run all tests"
Rake::TestTask.new do |t|
t.libs << "spec"
t.test_files = FileList['test/*_test.rb','test/bitpay/*_test.rb']
t.verbose = true
end
desc "Run BitPay tests"
task :default_tasks do
Rake::Task["spec"].invoke
Rake::Task["features"].invoke
end
desc "Bitpay Tasks"
namespace :bitpay do
desc "Clear all claim codes from the test server."
task :clear_claim_codes do
puts "clearing claim codes"
client = Mongo::MongoClient.new
db = client['bitpay-dev']
coll = db['tokenaccesses']
coll.remove()
puts "claim codes cleared"
end
desc "Clear rate limiters from local mongo host"
task :clear_rate_limiters do
puts "clearing rate limiters"
client = Mongo::MongoClient.new
db = client['bitpay-dev']
coll = db['ratelimiters']
coll.remove()
puts "rate limiters cleared"
end
desc "Clear local pem and token file"
task :clear_local_files do
puts "clearing local files"
HOME_DIR = File.join(Dir.home, '.bitpay')
KEY_FILE = File.join(HOME_DIR, 'bitpay.pem')
TOKEN_FILE = File.join(HOME_DIR, 'tokens.json')
File.delete(KEY_FILE) if File.file?(KEY_FILE)
File.delete(TOKEN_FILE) if File.file?(TOKEN_FILE)
puts "local files cleared"
end
desc "Clear tokens, rate limiters, and local files."
task :clear do
["bitpay:clear_local_files", "bitpay:clear_rate_limiters", "bitpay:clear_claim_codes"].each{|task| Rake::Task[task].reenable}
["bitpay:clear_local_files", "bitpay:clear_rate_limiters", "bitpay:clear_claim_codes"].each{|task| Rake::Task[task].invoke}
end
desc "Run specs and clear claim codes and rate_limiters."
task :spec_clear => ['spec', 'clear_claim_codes', 'clear_rate_limiters']
desc "Run specs, clear data, run cukes, clear data"
task :tests_clear do
Rake::Task["bitpay:clear"].invoke
Rake::Task["spec"].invoke
Rake::Task["bitpay:clear"].reenable
Rake::Task["bitpay:clear"].invoke
Rake::Task["features"].invoke
Rake::Task["bitpay:clear"].reenable
Rake::Task["bitpay:clear"].invoke
end
end
task :default => :test

23
bitpay-client.gemspec Normal file
View File

@ -0,0 +1,23 @@
require './lib/bitpay/version.rb'
Gem::Specification.new do |s|
s.name = 'bitpay-client'
s.version = BitPay::VERSION
s.authors = 'Bitpay, Inc.'
s.email = 'info@bitpay.com'
s.homepage = 'https://github.com/bitpay/ruby-client'
s.summary = 'Official ruby client library for the BitPay API'
s.description = 'Powerful, flexible, lightweight, thread-safe interface to the BitPay developers API'
s.files = `git ls-files`.split("\n")
s.require_paths = %w[lib]
s.rubyforge_project = s.name
s.required_rubygems_version = '>= 1.3.4'
s.add_dependency 'json'
s.add_dependency 'rack', '>= 0'
s.add_development_dependency 'rake'
s.add_development_dependency 'minitest'
s.add_development_dependency 'webmock'
s.add_development_dependency 'yard'
end

View File

@ -1,33 +0,0 @@
require './lib/bitpay/version.rb'
Gem::Specification.new do |s|
s.name = 'bitpay-sdk'
s.version = BitPay::VERSION
s.licenses = ['MIT']
s.authors = 'Bitpay, Inc.'
s.email = 'info@bitpay.com'
s.homepage = 'https://github.com/bitpay/ruby-client'
s.summary = 'Official Ruby library for the BitPay API'
s.description = 'Powerful, flexible, lightweight, thread-safe interface to the BitPay developers API'
s.files = `git ls-files`.split("\n")
s.require_paths = ["lib"]
s.rubyforge_project = s.name
s.required_rubygems_version = '>= 1.3.4'
s.required_ruby_version = '~> 2.1'
s.bindir = 'bin'
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.add_dependency 'bitpay-key-utils', '~>2.0.0'
s.add_development_dependency 'rack', '~> 2.0'
s.add_development_dependency 'rake', '12.0'
s.add_development_dependency 'webmock', '1.18.0'
s.add_development_dependency 'pry', '0.10.1'
s.add_development_dependency 'pry-byebug', '2.0.0'
s.add_development_dependency 'pry-rescue', '1.4.1'
s.add_development_dependency 'cucumber', '~> 1.3.17'
s.add_development_dependency 'airborne', '0.0.20'
s.add_development_dependency 'rspec', '3.1.0'
s.add_development_dependency 'mongo', '1.11.1'
s.add_development_dependency 'coveralls'
end

View File

@ -1,12 +0,0 @@
## Verifies test variables have been set correctly
#
# Use 'set_constants.sh' to pre-configure test variables
# e.g.
# source ./spec/set_constants.sh https://test.bitpay.com testuser@gmail.com mypassword
#
APIURI = ENV['BITPAYAPI']
# Specify a bitpay txid which has 6+ confirmations. Default belongs to 'bitpayrubyclient@gmail.com' test account
REFUND_TRANSACTION = ENV['REFUND_TRANSACTION']
REFUND_ADDRESS = ENV['REFUND_ADDRESS']

View File

@ -1,28 +0,0 @@
@invoices
Feature: creating an invoice
The user won't get any money
If they can't
Create Invoices
Background:
Given the user is authenticated with BitPay
Scenario Outline: The request is correct
When the user creates an invoice for <price> <currency>
Then they should recieve an invoice in response for <price> <currency>
Examples:
| price | currency |
| "5.23" | "USD" |
| "10.21" | "EUR" |
| "0.225" | "BTC" |
Scenario Outline: The invoice contains illegal characters
When the user creates an invoice for <price> <currency>
Then they will receive a BitPay::ArgumentError matching <message>
Examples:
| price | currency | message |
| "5,023" | "USD" | "Price must be formatted as a float" |
| "3.21" | "EaUR" | "Currency is invalid." |
| "" | "USD" | "Price must be formatted as a float" |
| "Ten" | "USD" | "Price must be formatted as a float" |
| "10" | "" | "Currency is invalid." |

View File

@ -1,19 +0,0 @@
Feature: pairing with bitpay
In order to access bitpay
It is required that the library
Is able to pair successfully
Scenario: the client has a correct pairing code
Given the user pairs with BitPay with a valid pairing code
Then the user receives a require token from bitpay
Scenario: the client initiates pairing
Given the user performs a client-side pairing
Then the user receives an inactive token from bitpay
Scenario Outline: the client has a bad pairing code
Given the user fails to pair with a semantically <valid> code <code>
Then they will receive a <error> matching <message>
Examples:
| valid | code | error | message |
| invalid | "a1b2c3d4" | BitPay::ArgumentError | "pairing code is not legal" |

View File

@ -1,23 +0,0 @@
@refunds
Feature: issuing a refund
The merchant wants to issue a refund
So that they can serve their customers
Background:
Given the user is authenticated with BitPay
Scenario: creating a refund
Given the user creates a refund
Then they will receive a refund id
Scenario: retrieving a refund
Given the user requests a specific refund
Then they will receive the refund
Scenario: retrieving all refunds
Given the user requests all refunds for an invoice
Then they will receive an array of refunds
Scenario: canceling a refund
Given a properly formatted cancellation request
Then the refund will be cancelled

View File

@ -1,11 +0,0 @@
Feature: retrieving an invoice
The user may want to retrieve invoices
So that they can view them
Scenario: Correct public request
Given that a user knows an invoice id
Then they can retrieve the public version of that invoice
Scenario: Correct merchant request
Given that a user knows an invoice id
Then they can retrieve the merchant-scoped version of that invoice

View File

@ -1,31 +0,0 @@
When(/^the user (?:tries to |)creates? an invoice (?:for|without) "(.*?)" (?:or |and |)"(.*?)"$/) do |price, currency|
begin
@response = @client.create_invoice(price: price, currency: currency, facade: 'merchant')
rescue => error
@error = error
end
end
Then(/^they should recieve an invoice in response for "(.*?)" "(.*?)"$/) do |price, currency|
raise "#{@response['price']} != #{price} or #{@response['currency']} != #{currency}" unless (price == @response['price'].to_s && currency == @response['currency'])
end
Given(/^there is an invalid token$/) do
pending # express the regexp above with the code you wish you had
end
Given(/^that a user knows an invoice id$/) do
@client = new_client_from_stored_values
@id = (@client.create_invoice(price: 3, currency: "USD", facade: 'merchant' ))['id']
end
Then(/^they can retrieve the public version of that invoice$/) do
invoice = @client.get_public_invoice(id: @id)
raise "That's the wrong invoice" unless invoice['id'] == @id
end
Then(/^they can retrieve the merchant\-scoped version of that invoice$/) do
invoice = @client.get_invoice(id: @id)
raise "That's the wrong invoice" unless invoice['id'] == @id
end

View File

@ -1,56 +0,0 @@
@token = nil
@error = nil
When(/^the user pairs with BitPay(?: with a valid pairing code|)$/) do
@client = new_client_from_stored_values
claim_code = get_claim_code_from_server @client
sleep 1 # rate limit compliance
@token = @client.pair_pos_client(claim_code)
end
Given(/^the user is authenticated with BitPay$/) do
@client = new_client_from_stored_values
raise "client not authenticated" unless client_has_tokens(@client)
end
Given(/^the user is paired with BitPay$/) do
raise "Client is not paired" unless @client.verify_tokens
end
Then(/^the user receives an? ([A-z]+) token from bitpay$/) do |expected|
actual = @token[0]["policies"][0]["method"]
raise "Token not correct, #{actual} != #{expected}" unless actual == expected
end
Given(/^the user has a bad pairing_code "(.*?)"$/) do |arg1|
# This is a no-op, pairing codes are transient and never actually saved
end
Then(/^the user fails to pair with a semantically (?:in|)valid code "(.*?)"$/) do |code|
pem = BitPay::KeyUtils.generate_pem
client = BitPay::SDK::Client.new(api_uri: APIURI, pem: pem, insecure: true)
begin
sleep 1 # rate limit compliance
client.pair_pos_client(code)
raise "pairing unexpectedly worked"
rescue => error
@error = error
true
end
end
Then(/^they will receive an? (.*?) matching "(.*?)"$/) do |error_class, error_message|
raise "Error: #{@error.class}, message: #{@error.message}" unless Object.const_get(error_class) == @error.class && @error.message.include?(error_message)
end
Given(/^the user performs a client\-side pairing$/) do
sleep 1
pem = BitPay::KeyUtils.generate_pem
@client = BitPay::SDK::Client.new(api_uri: APIURI, pem: pem, insecure: true)
@token = @client.pair_client({facade: 'merchant'})
end
Then(/^the user has a merchant token$/) do
tokens = {'merchant' => @token}
raise "Merchant token not authorized" unless @client.verify_tokens(tokens: tokens)
end

View File

@ -1,37 +0,0 @@
Given(/^the user creates a refund$/) do
sleep(1)
@response = @client.refund_invoice(id: REFUND_TRANSACTION, params: {amount: 1, currency: 'USD', bitcoinAddress: REFUND_ADDRESS})
end
Then(/^they will receive a refund id$/) do
@refund_id = @response["id"]
expect(@refund_id).not_to be_empty
end
Given(/^the user requests a specific refund$/) do
@response = @client.get_refund(invoice_id: REFUND_TRANSACTION, request_id: @refund_id)
end
Then(/^they will receive the refund$/) do
expect(@response.first["status"]).not_to be_empty
end
Given(/^the user requests all refunds for an invoice$/) do
client = new_client_from_stored_values
@response = client.get_all_refunds_for_invoice(id: REFUND_TRANSACTION)
end
Then(/^they will receive an array of refunds$/) do
expect(@response).to be_instance_of Array
end
Given(/^a properly formatted cancellation request$/) do
sleep(1)
client = new_client_from_stored_values
@refund_id = client.get_all_refunds_for_invoice(id: REFUND_TRANSACTION).first["id"]
@response = client.cancel_refund(invoice_id: REFUND_TRANSACTION, request_id: @refund_id)
end
Then(/^the refund will be cancelled$/) do
expect(@response).to eq("Success")
end

View File

@ -1,31 +0,0 @@
require 'pry'
require 'fileutils'
require File.join File.dirname(__FILE__), '..', '..', 'lib', 'bitpay_sdk.rb'
require_relative '../../config/constants.rb'
module BitPay
# Location for API Credentials
BITPAY_CREDENTIALS_DIR = File.join(Dir.home, ".bitpay")
PRIVATE_KEY_FILE = 'bitpay.pem'
PRIVATE_KEY_PATH = File.join(BITPAY_CREDENTIALS_DIR, PRIVATE_KEY_FILE)
TOKEN_FILE = 'tokens.json'
TOKEN_FILE_PATH = File.join(BITPAY_CREDENTIALS_DIR, TOKEN_FILE)
end
def new_client_from_stored_values
pem = ENV['BITPAYPEM'].gsub("\\n", "\n")
BitPay::SDK::Client.new(api_uri: APIURI, pem: pem, insecure: true)
end
def get_claim_code_from_server client
token = client.get(path: "tokens")["data"].select{|tuple| tuple["merchant"]}.first.values.first
client.post(path: "tokens", token: token, params: {facade: "pos"})["data"][0]["pairingCode"]
end
def client_has_tokens client
data = client.get(path: "tokens")["data"]
data.select{|tuple| tuple["pos"]}.any? && data.select{|tuple| tuple["merchant"]}.any?
end

17
lib/bitpay.rb Normal file
View File

@ -0,0 +1,17 @@
libdir = File.dirname(__FILE__)
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
require 'bitpay/client'
require 'bitpay/version'
module BitPay
# Location of SSL Certificate Authority File
# As sourced from http://curl.haxx.se/ca/cacert.pem
CA_FILE = File.join File.dirname(__FILE__), 'bitpay','cacert.pem'
# Location of API
API_URI = 'https://bitpay.com/api'
# User agent reported to API
USER_AGENT = 'ruby-bitpay-client '+VERSION
end

View File

@ -1,179 +1,72 @@
# license Copyright 2011-2015 BitPay, Inc., MIT License
# see http://opensource.org/licenses/MIT
# or https://github.com/bitpay/php-bitpay-client/blob/master/LICENSE
require 'uri'
require 'net/https'
require 'json'
require 'bitpay_key_utils'
require_relative 'rest_connector'
module BitPay
# This class is used to instantiate a BitPay Client object. It is expected to be thread safe.
#
module SDK
class Client
include BitPay::RestConnector
# @return [Client]
# @example
# # Create a client with a pem file created by the bitpay client:
# client = BitPay::SDK::Client.new
def initialize(opts={})
@pem = opts[:pem] || ENV['BITPAY_PEM'] || KeyUtils.generate_pem
@key = KeyUtils.create_key @pem
@priv_key = KeyUtils.get_private_key @key
@pub_key = KeyUtils.get_public_key @key
@client_id = KeyUtils.generate_sin_from_pem @pem
@uri = URI.parse opts[:api_uri] || API_URI
@user_agent = opts[:user_agent] || USER_AGENT
@https = Net::HTTP.new @uri.host, @uri.port
@https.use_ssl = true
@https.open_timeout = 10
@https.read_timeout = 10
# @example
# # Create a client with your BitPay API key (obtained from the BitPay API access page at BitPay.com):
# client = BitPay::Client.new 'YOUR_API_KEY'
class Client
class BitPayError < StandardError; end
@https.ca_file = CA_FILE
@tokens = opts[:tokens] || {}
# Creates a BitPay Client object. The second parameter is a hash for overriding defaults.
#
# @return [Client]
# @example
# # Create a client with your BitPay API key (obtained from the BitPay API access page at BitPay.com):
# client = BitPay::Client.new 'YOUR_API_KEY'
def initialize(api_key, opts={})
@api_key = api_key
@uri = URI.parse opts[:api_uri] || API_URI
@https = Net::HTTP.new @uri.host, @uri.port
@https.use_ssl = true
@https.ca_file = CA_FILE
# Option to disable certificate validation in extraordinary circumstance. NOT recommended for production use
@https.verify_mode = opts[:insecure] == true ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
# Option to enable http request debugging
@https.set_debug_output($stdout) if opts[:debug] == true
end
## Pair client with BitPay service
# => Pass empty hash {} to retreive client-initiated pairing code
# => Pass {pairingCode: 'WfD01d2'} to claim a server-initiated pairing code
#
def pair_client(params={})
tokens = post(path: 'tokens', params: params)
return tokens["data"]
end
# Option to disable certificate validation in extraordinary circumstance. NOT recommended for production use
@https.verify_mode = opts[:insecure] == true ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
## Compatibility method for pos pairing
#
def pair_pos_client(claimCode)
raise BitPay::ArgumentError, "pairing code is not legal" unless verify_claim_code(claimCode)
pair_client({pairingCode: claimCode})
end
end
## Create bitcoin invoice
#
# Defaults to pos facade, also works with merchant facade
#
def create_invoice(price:, currency:, facade: 'pos', params:{})
raise BitPay::ArgumentError, "Illegal Argument: Price must be formatted as a float" unless
price.is_a?(Numeric) ||
/^[[:digit:]]+(\.[[:digit:]]{2})?$/.match(price) ||
currency == 'BTC' && /^[[:digit:]]+(\.[[:digit:]]{1,8})?$/.match(price)
raise BitPay::ArgumentError, "Illegal Argument: Currency is invalid." unless /^[[:upper:]]{3}$/.match(currency)
params.merge!({price: price, currency: currency})
token = get_token(facade)
invoice = post(path: "invoices", token: token, params: params)
invoice["data"]
end
# Makes a GET call to the BitPay API.
# @return [Hash]
# @see get_invoice
# @example
# # Get an invoice:
# existing_invoice = client.get 'invoice/YOUR_INVOICE_ID'
def get(path)
request(Net::HTTP::Get, path)
end
## Gets the privileged merchant-version of the invoice
# Requires merchant facade token
#
def get_invoice(id:)
token = get_token('merchant')
invoice = get(path: "invoices/#{id}", token: token)
invoice["data"]
end
# Makes a POST call to the BitPay API.
# @return [Hash]
# @see create_invoice
# # Create an invoice:
# created_invoice = client.post 'invoice', {:price => 1.45, :currency => 'BTC'}
def post(path, params={})
request(Net::HTTP::Post, path, params)
end
def get_invoices(params = {})
token = get_token('merchant')
invoice = get(path: "invoices", token: token, params: params)
invoice["data"]
end
private
## Gets the public version of the invoice
def get_public_invoice(id:)
invoice = get(path: "invoices/#{id}", public_request: true)
invoice["data"]
end
## Refund paid BitPay invoice
#
# If invoice["data"]["flags"]["refundable"] == true the a refund address was
# provided with the payment and the refund_address parameter is an optional override
#
# Amount and Currency are required fields for fully paid invoices but optional
# for under or overpaid invoices which will otherwise be completely refunded
#
# Requires merchant facade token
#
# @example
# client.refund_invoice(id: 'JB49z2MsDH7FunczeyDS8j', params: {amount: 10, currency: 'USD', bitcoinAddress: '1Jtcygf8W3cEmtGgepggtjCxtmFFjrZwRV'})
#
def refund_invoice(id:, params:{})
invoice = get_invoice(id: id)
refund = post(path: "invoices/#{id}/refunds", token: invoice["token"], params: params)
refund["data"]
end
## Get All Refunds for Invoice
# Returns an array of all refund requests for a specific invoice,
#
# Requires merchant facade token
#
# @example:
# client.get_all_refunds_for_invoice(id: 'JB49z2MsDH7FunczeyDS8j')
#
def get_all_refunds_for_invoice(id:)
urlpath = "invoices/#{id}/refunds"
invoice = get_invoice(id: id)
refunds = get(path: urlpath, token: invoice["token"])
refunds["data"]
end
def request(request_klass, path, params=nil)
request = make_request(request_klass, path, params)
response = @https.request(request)
raise BitPayError, "HTTP Status " + response.code + " with body: " + response.body unless response.kind_of?(Net::HTTPSuccess)
return JSON.parse(response.body)
rescue => e
fail BitPayError, "Bitpay Request Error: #{e}"
end
## Get Refund
# Requires merchant facade token
#
# @example:
# client.get_refund(id: 'JB49z2MsDH7FunczeyDS8j', request_id: '4evCrXq4EDXk4oqDXdWQhX')
#
def get_refund(invoice_id:, request_id:)
urlpath = "invoices/#{invoice_id}/refunds/#{request_id}"
invoice = get_invoice(id: invoice_id)
refund = get(path: urlpath, token: invoice["token"])
refund["data"]
end
## Cancel Refund
# Requires merchant facade token
#
# @example:
# client.cancel_refund(id: 'JB49z2MsDH7FunczeyDS8j', request_id: '4evCrXq4EDXk4oqDXdWQhX')
#
def cancel_refund(invoice_id:, request_id:)
urlpath = "invoices/#{invoice_id}/refunds/#{request_id}"
refund = get_refund(invoice_id: invoice_id, request_id: request_id)
deletion = delete(path: urlpath, token: refund["token"])
deletion["data"]
end
## Checks that the passed tokens are valid by
# comparing them to those that are authorized by the server
#
# Uses local @tokens variable if no tokens are passed
# in order to validate the connector is properly paired
#
def verify_tokens(tokens: @tokens)
server_tokens = refresh_tokens
tokens.each{|key, value| return false if server_tokens[key] != value}
return true
end
private
def verify_claim_code(claim_code)
regex = /^[[:alnum:]]{7}$/
matches = regex.match(claim_code)
!(matches.nil?)
def make_request(request_klass, path, params)
request_klass.new(@uri.path + '/' + path).tap do |r|
r.basic_auth @api_key, ''
r['User-Agent'] = USER_AGENT
r['Content-Type'] = 'application/json'
r['X-BitPay-Plugin-Info'] = 'Rubylib' + VERSION
r.body = params.to_json if params
end
end
end

View File

@ -1,106 +0,0 @@
# license Copyright 2011-2015 BitPay, Inc., MIT License
# see http://opensource.org/licenses/MIT
# or https://github.com/bitpay/php-bitpay-client/blob/master/LICENSE
module BitPay
module RestConnector
def send_request(verb, path, facade: 'merchant', params: {}, token: nil)
token ||= get_token(facade)
case verb.upcase
when "GET"
return get(path: path, token: token, params: params)
when "POST"
return post(path: path, token: token, params: params)
else
raise(BitPayError, "Invalid HTTP verb: #{verb.upcase}")
end
end
def get(path:, token: nil, public_request: false, params: {})
urlpath = '/' + path + '?'
urlpath = urlpath + 'token=' + token if token
urlpath = urlpath + '&' + params.to_param if params.present?
request = Net::HTTP::Get.new urlpath
unless public_request
request['X-Signature'] = KeyUtils.sign(@uri.to_s + urlpath, @priv_key)
request['X-Identity'] = @pub_key
end
process_request(request)
end
def post(path:, token: nil, params:)
urlpath = '/' + path
request = Net::HTTP::Post.new urlpath
params[:token] = token if token
params[:guid] = SecureRandom.uuid
params[:id] = @client_id
request.body = params.to_json
if token
request['X-Signature'] = KeyUtils.sign(@uri.to_s + urlpath + request.body, @priv_key)
request['X-Identity'] = @pub_key
end
process_request(request)
end
def delete(path:, token: nil)
urlpath = '/' + path
urlpath = urlpath + '?token=' + token if token
request = Net::HTTP::Delete.new urlpath
request['X-Signature'] = KeyUtils.sign(@uri.to_s + urlpath, @priv_key)
request['X-Identity'] = @pub_key
process_request(request)
end
private
## Processes HTTP Request and returns parsed response
# Otherwise throws error
#
def process_request(request)
request['User-Agent'] = @user_agent
request['Content-Type'] = 'application/json'
request['X-BitPay-Plugin-Info'] = 'Rubylib' + VERSION
begin
response = @https.request request
rescue => error
raise BitPay::ConnectionError, "#{error.message}"
end
if response.kind_of? Net::HTTPSuccess
return JSON.parse(response.body)
elsif JSON.parse(response.body)["error"]
raise(BitPayError, "#{response.code}: #{JSON.parse(response.body)['error']}")
else
raise BitPayError, "#{response.code}: #{JSON.parse(response.body)}"
end
end
## Fetches the tokens hash from the server and
# updates @tokens
#
def refresh_tokens
response = get(path: 'tokens')["data"]
token_array = response || {}
tokens = {}
token_array.each do |t|
tokens[t.keys.first] = t.values.first
end
@tokens = tokens
return tokens
end
## Makes a request to /tokens for pairing
# Adds passed params as post parameters
# If empty params, retrieves server-generated pairing code
# If pairingCode key/value is passed, will pair client ID to this account
# Returns response hash
#
def get_token(facade)
token = @tokens[facade] || refresh_tokens[facade] || raise(BitPayError, "Not authorized for facade: #{facade}")
end
end
end

View File

@ -1,7 +1,3 @@
# license Copyright 2011-2014 BitPay, Inc., MIT License
# see http://opensource.org/licenses/MIT
# or https://github.com/bitpay/php-bitpay-client/blob/master/LICENSE
module BitPay
VERSION = '2.4.6'
VERSION = '0.1.5'
end

View File

@ -1,28 +0,0 @@
# license Copyright 2011-2014 BitPay, Inc., MIT License
# see http://opensource.org/licenses/MIT
# or https://github.com/bitpay/php-bitpay-client/blob/master/LICENSE
libdir = File.dirname(__FILE__)
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
require 'bitpay/client'
require 'bitpay/version'
module BitPay
# Location of SSL Certificate Authority File
# As sourced from http://curl.haxx.se/ca/cacert.pem
CA_FILE = File.join File.dirname(__FILE__), 'bitpay','cacert.pem'
# Location of API
API_URI = 'https://bitpay.com'
TEST_API_URI = 'https://test.bitpay.com'
CLIENT_REGISTRATION_PATH = '/api-access-request'
# User agent reported to API
USER_AGENT = 'ruby-bitpay-sdk '+VERSION
class BitPayError < StandardError; end
class ConnectionError < Errno::ECONNREFUSED; end
end

View File

@ -1,190 +0,0 @@
require 'spec_helper'
def tokens
{"data" =>
[{"merchant" => "MERCHANT_TOKEN"},
{"pos" =>"POS_TOKEN"},
{"merchant/invoice" => "9kv7gGqZLoQ2fxbKEgfgndLoxwjp5na6VtGSH3sN7buX"}
]
}
end
describe BitPay::SDK::Client do
let(:bitpay_client) { BitPay::SDK::Client.new({api_uri: BitPay::TEST_API_URI}) }
let(:claim_code) { "a12bc3d" }
before do
# Stub JSON responses from fixtures
stub_request(:get, /#{BitPay::TEST_API_URI}\/tokens.*/)
.to_return(:status => 200, :body => tokens.to_json, :headers => {})
stub_request(:get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID?token=MERCHANT_TOKEN").
to_return(:body => get_fixture('invoices_{id}-GET.json'))
stub_request(:get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds?token=MERCHANT_INVOICE_TOKEN").
to_return(:body => get_fixture('invoices_{id}_refunds-GET.json'))
stub_request(:get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds/TEST_REQUEST_ID?token=MERCHANT_INVOICE_TOKEN").
to_return(:body => get_fixture('invoices_{id}_refunds-GET.json'))
stub_request(:post, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds").
to_return(:body => get_fixture('invoices_{id}_refunds-POST.json'))
stub_request(:post, "#{BitPay::TEST_API_URI}/nuttin").
to_return(:body => get_fixture('response-nodata.json'))
stub_request(:get, "#{BitPay::TEST_API_URI}/nuttin").
to_return(:body => get_fixture('response-nodata.json'))
stub_request(:delete, "#{BitPay::TEST_API_URI}/nuttin").
to_return(:body => get_fixture('response-nodata.json'))
end
describe "#initialize" do
it 'should be able to get pem file from the env' do
stub_const('ENV', {'BITPAY_PEM' => PEM})
expect {bitpay_client}.to_not raise_error
end
end
describe "requests to endpoint without data field" do
it "should return the json body" do
expect(bitpay_client.post(path: "nuttin", params: {})["facile"]).to eq("is easy")
expect(bitpay_client.get(path: "nuttin")["facile"]).to eq("is easy")
expect(bitpay_client.delete(path: "nuttin")["facile"]).to eq( "is easy")
end
end
describe "#send_request" do
before do
stub_const('ENV', {'BITPAY_PEM' => PEM})
end
context "GET" do
it 'should generate a get request' do
stub_request(:get, /#{BitPay::TEST_API_URI}\/whatever.*/).to_return(:body => '{"awesome": "json"}')
bitpay_client.send_request("GET", "whatever", facade: "merchant")
expect(WebMock).to have_requested(:get, "#{BitPay::TEST_API_URI}/whatever?token=MERCHANT_TOKEN")
end
it 'should handle query parameters gracefully' do
stub_request(:get, /#{BitPay::TEST_API_URI}\/ledgers.*/).to_return(:body => '{"awesome": "json"}')
bitpay_client.send_request("GET", "ledgers/BTC?startDate=2015-01-01&endDate=2015-02-01", facade: "merchant")
expect(WebMock).to have_requested(:get, "#{BitPay::TEST_API_URI}/ledgers/BTC?startDate=2015-01-01&endDate=2015-02-01&token=MERCHANT_TOKEN")
end
end
context "POST" do
it 'should generate a post request' do
stub_request(:post, /#{BitPay::TEST_API_URI}.*/).to_return(:body => '{"awesome": "json"}')
bitpay_client.send_request("POST", "whatever", facade: "merchant")
expect(WebMock).to have_requested(:post, "#{BitPay::TEST_API_URI}/whatever")
end
end
end
describe "#pair_pos_client" do
before do
stub_const('ENV', {'BITPAY_PEM' => PEM})
end
it 'throws a BitPayError with the error message if the token setting fails' do
stub_request(:any, /#{BitPay::TEST_API_URI}.*/).to_return(status: 500, body: "{\n \"error\": \"Unable to create token\"\n}")
expect { bitpay_client.pair_pos_client(claim_code) }.to raise_error(BitPay::BitPayError, '500: Unable to create token')
end
it 'gracefully handles 4xx errors' do
stub_request(:any, /#{BitPay::TEST_API_URI}.*/).to_return(status: 403, body: "{\n \"error\": \"this is a 403 error\"\n}")
expect { bitpay_client.pair_pos_client(claim_code) }.to raise_error(BitPay::BitPayError, '403: this is a 403 error')
end
it 'short circuits on invalid pairing codes' do
100.times do
claim_code = an_illegal_claim_code
expect { bitpay_client.pair_pos_client(claim_code) }.to raise_error BitPay::ArgumentError, "pairing code is not legal"
end
end
end
describe "#create_invoice" do
subject { bitpay_client }
before {stub_const('ENV', {'BITPAY_PEM' => PEM})}
it { is_expected.to respond_to(:create_invoice) }
describe "should make the call to the server to create an invoice" do
it 'allows numeric input for the price' do
stub_request(:post, /#{BitPay::TEST_API_URI}\/invoices.*/).to_return(:body => '{"data": "awesome"}')
bitpay_client.create_invoice(price: 20.00, currency: "USD")
assert_requested :post, "#{BitPay::TEST_API_URI}/invoices"
end
it 'allows string input for the price' do
stub_request(:post, /#{BitPay::TEST_API_URI}\/invoices.*/).to_return(:body => '{"data": "awesome"}')
bitpay_client.create_invoice(price: "20.00", currency: "USD")
assert_requested :post, "#{BitPay::TEST_API_URI}/invoices"
end
end
it 'should pass through the API error message from load_tokens' do
stub_request(:get, /#{BitPay::TEST_API_URI}\/tokens.*/).to_return(status: 500, body: '{"error": "load_tokens_error"}')
expect { bitpay_client.create_invoice(price: 20, currency: "USD") }.to raise_error(BitPay::BitPayError, '500: load_tokens_error')
end
it 'verifies the validity of the price argument' do
expect { bitpay_client.create_invoice(price: "3,999", currency: "USD") }.to raise_error(BitPay::ArgumentError, 'Illegal Argument: Price must be formatted as a float')
end
it 'verifies the validity of the currency argument' do
expect { bitpay_client.create_invoice(price: "3999", currency: "UASD") }.to raise_error(BitPay::ArgumentError, 'Illegal Argument: Currency is invalid.')
end
end
describe '#refund_invoice' do
subject { bitpay_client }
before { stub_const('ENV', {'BITPAY_PEM' => PEM}) }
it { is_expected.to respond_to(:refund_invoice) }
it 'should get the token for the invoice' do
bitpay_client.refund_invoice(id: 'TEST_INVOICE_ID')
expect(WebMock).to have_requested :get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID?token=MERCHANT_TOKEN"
end
it 'should generate a POST to the invoices/refund endpoint' do
bitpay_client.refund_invoice(id: 'TEST_INVOICE_ID')
expect(WebMock).to have_requested :post, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds"
end
end
describe '#get_all_refunds_for_invoice' do
subject { bitpay_client }
before {stub_const('ENV', {'BITPAY_PEM' => PEM})}
it { is_expected.to respond_to(:get_all_refunds_for_invoice) }
it 'should get the token for the invoice' do
bitpay_client.get_all_refunds_for_invoice(id: 'TEST_INVOICE_ID')
expect(WebMock).to have_requested :get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID?token=MERCHANT_TOKEN"
end
it 'should GET all refunds' do
bitpay_client.get_all_refunds_for_invoice(id: 'TEST_INVOICE_ID')
expect(WebMock).to have_requested :get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds?token=MERCHANT_INVOICE_TOKEN"
end
end
describe '#get_refund' do
subject { bitpay_client }
before {stub_const('ENV', {'BITPAY_PEM' => PEM})}
it { is_expected.to respond_to(:get_refund) }
it 'should get the token for the invoice' do
bitpay_client.get_refund(invoice_id: 'TEST_INVOICE_ID', request_id: 'TEST_REQUEST_ID')
expect(WebMock).to have_requested :get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID?token=MERCHANT_TOKEN"
end
it 'should GET a single refund' do
bitpay_client.get_refund(invoice_id: 'TEST_INVOICE_ID', request_id: 'TEST_REQUEST_ID')
expect(WebMock).to have_requested :get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds/TEST_REQUEST_ID?token=MERCHANT_INVOICE_TOKEN"
end
end
describe "#verify_tokens" do
subject { bitpay_client }
before {stub_const('ENV', {'BITPAY_PEM' => PEM})}
it { is_expected.to respond_to(:verify_tokens) }
end
end

View File

@ -1,29 +0,0 @@
{
"facade": "pos/invoice",
"data": {
"url": "https://test.bitpay.com/invoice?id=2RSyNDvsiTrA31rPwnnEcd",
"status": "new",
"btcPrice": "0.037523",
"btcDue": "0.037523",
"price": 10,
"currency": "USD",
"exRates": {
"USD": 266.5
},
"invoiceTime": 1422319964413,
"expirationTime": 1422320864413,
"currentTime": 1422319964431,
"guid": "34d7be05-eb65-4f72-a2ce-79bf23e93f17",
"id": "2RSyNDvsiTrA31rPwnnEcd",
"btcPaid": "0.000000",
"rate": 266.5,
"exceptionStatus": false,
"paymentUrls": {
"BIP21": "bitcoin:mhPM48eieakd6AgCuHMwAtpFXE5yQ3N7om?amount=0.037523",
"BIP72": "bitcoin:mhPM48eieakd6AgCuHMwAtpFXE5yQ3N7om?amount=0.037523&r=https://test.bitpay.com/i/2RSyNDvsiTrA31rPwnnEcd",
"BIP72b": "bitcoin:?r=https://test.bitpay.com/i/2RSyNDvsiTrA31rPwnnEcd",
"BIP73": "https://test.bitpay.com/i/2RSyNDvsiTrA31rPwnnEcd"
},
"token": "2RPipMRUXAvt5wAfthCzF7Tj4SppBWPHGQ7hCeWYeWDm7RtwUtDds1XUNt11VTf5C6UfCAACBhsKwjW6SAocLsd7"
}
}

View File

@ -1,35 +0,0 @@
{
"facade": "merchant/invoice",
"data": {
"url": "https://test.bitpay.com/invoice?id=CcRgegwTMs866Sr7vdLnru",
"status": "complete",
"btcPrice": "0.074661",
"btcDue": "0.000000",
"price": 15.79,
"currency": "USD",
"exRates": {
"USD": 211.49
},
"invoiceTime": 1421719631301,
"expirationTime": 1421720531301,
"currentTime": 1422316288768,
"id": "TEST_INVOICE_ID",
"btcPaid": "0.074661",
"rate": 211.49,
"exceptionStatus": false,
"transactions": [
{
"amount": 7466100,
"confirmations": 6,
"time": "2015-01-20T02:07:46.000Z",
"receivedTime": "2015-01-20T02:07:45.881Z"
}
],
"flags": {
"refundable": true
},
"token": "MERCHANT_INVOICE_TOKEN",
"buyer": {
}
}
}

View File

@ -1,17 +0,0 @@
{
"facade": "merchant/supportRequest",
"data": [
{
"id": "TEST_REQUEST_ID",
"requestDate": "2015-01-27T00:36:12.360Z",
"status": "pending",
"token": "REFUND_REQUEST_TOKEN"
},
{
"id": "ANOTHER_ID",
"requestDate": "2015-01-27T00:36:12.360Z",
"status": "OTHER_STATUS",
"token": "ANOTHER_REFUND_REQUEST_TOKEN"
}
]
}

View File

@ -1,9 +0,0 @@
{
"facade": "merchant/supportRequest",
"data": {
"id": "Q6CuxYF83MfV1XgUBQBdbA",
"requestDate": "2015-01-27T00:36:12.360Z",
"status": "pending",
"token": "REFUND_REQUEST_TOKEN"
}
}

View File

@ -1,11 +0,0 @@
{
"facade": "merchant/supportRequest",
"data": [
{
"id": "TEST_REQUEST_ID",
"requestDate": "2015-01-27T00:36:12.360Z",
"status": "pending",
"token": "REFUND_REQUEST_TOKEN"
}
]
}

View File

@ -1,10 +0,0 @@
{
"facile": "is easy",
"diti": [
{
"requestDate": "2015-01-27T00:36:12.360Z",
"status": "pending"
}
]
}

View File

@ -1,34 +0,0 @@
require 'webmock/rspec'
require 'pry'
require 'coveralls'
Coveralls.wear!
require File.join File.dirname(__FILE__), '..', 'lib', 'bitpay_sdk.rb'
require_relative '../config/constants.rb'
#
## Test Variables
#
PEM = "-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEICg7E4NN53YkaWuAwpoqjfAofjzKI7Jq1f532dX+0O6QoAcGBSuBBAAK\noUQDQgAEjZcNa6Kdz6GQwXcUD9iJ+t1tJZCx7hpqBuJV2/IrQBfue8jh8H7Q/4vX\nfAArmNMaGotTpjdnymWlMfszzXJhlw==\n-----END EC PRIVATE KEY-----\n"
PUB_KEY = '038d970d6ba29dcfa190c177140fd889fadd6d2590b1ee1a6a06e255dbf22b4017'
CLIENT_ID = "TeyN4LPrXiG5t2yuSamKqP3ynVk3F52iHrX"
def generate_code(number)
legal_map = [*'A'..'Z'] + [*'a'..'z'] + [*0..9]
Array.new(number) { legal_map.sample }.join
end
def an_illegal_claim_code
short_code = generate_code(rand(6))
long_code = generate_code(rand(8..25))
[nil, short_code, long_code].sample
end
## Gets JSON responses from the fixtures directory
#
def get_fixture(name)
#JSON.parse(File.read(File.expand_path("../fixtures/#{name}", __FILE__)))
File.read(File.expand_path("../fixtures/#{name}", __FILE__))
end

View File

@ -0,0 +1,70 @@
require File.join File.dirname(__FILE__), '..', 'env.rb'
USER_AGENT = 'ruby-bitpay-client '+BitPay::VERSION
def invoice_create_body
{:price => 1, :currency => 'USD'}
end
def invoice_response_body
{
"id" => "DGrAEmbsXe9bavBPMJ8kuk",
"url" => "https://bitpay.com/invoice?id=DGrAEmbsXe9bavBPMJ8kuk",
"status" => "new",
"btcPrice" => "0.0495",
"price" => 10,
"currency" => "USD",
"invoiceTime" => 1383265343674,
"expirationTime" => 1383266243674,
"currentTime" => 1383265957613
}
end
def unauthorized_key_body
{"error" => {"type" => "unauthorized", "message" => "invalid api key"}}
end
stub_request(:post, "https://KEY:@bitpay.com/api/invoice/create").
with(
:headers => {'User-Agent'=>USER_AGENT, 'Content-Type' => 'application/json'},
:body => invoice_create_body
).
to_return(:body => invoice_response_body.to_json)
stub_request(:get, "https://KEY:@bitpay.com/api/invoice/DGrAEmbsXe9bavBPMJ8kuk").
with(:headers => {'User-Agent'=>USER_AGENT}).
to_return(:body => invoice_response_body.to_json)
stub_request(:get, "https://KEY:@bitpay.com/api/invoice/BADAPIKEY").
with(:headers => {'User-Agent'=>USER_AGENT}).
to_return(:status => 403, :body => unauthorized_key_body.to_json)
describe BitPay::Client do
before do
@client = BitPay::Client.new 'KEY'
end
describe 'post' do
it 'creates invoice' do
response = @client.post 'invoice/create', invoice_create_body
response.class.must_equal Hash
response['id'].must_equal 'DGrAEmbsXe9bavBPMJ8kuk'
end
end
describe 'get' do
it 'retreives invoice' do
response = @client.get 'invoice/DGrAEmbsXe9bavBPMJ8kuk'
response.class.must_equal Hash
response['id'].must_equal 'DGrAEmbsXe9bavBPMJ8kuk'
end
end
describe 'bad API Key' do
it 'returns Error' do
assert_raises(BitPay::Client::BitPayError) {
response = @client.get 'invoice/BADAPIKEY'
}
end
end
end

7
test/env.rb Normal file
View File

@ -0,0 +1,7 @@
require 'addressable/uri'
require 'json'
require 'minitest/autorun'
require 'webmock'
require './lib/bitpay.rb'
include WebMock::API