bandcampbx 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +8 -0
- data/LICENSE.md +20 -0
- data/README.md +17 -0
- data/Rakefile +13 -0
- data/bandcampbx.gemspec +30 -0
- data/lib/bandcampbx.rb +14 -0
- data/lib/bandcampbx/client.rb +60 -0
- data/lib/bandcampbx/entities/balance.rb +21 -0
- data/lib/bandcampbx/entities/base.rb +58 -0
- data/lib/bandcampbx/entities/order.rb +34 -0
- data/lib/bandcampbx/mapper.rb +37 -0
- data/lib/bandcampbx/net.rb +56 -0
- data/lib/bandcampbx/version.rb +3 -0
- data/spec/integration/client_spec.rb +105 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/unit/bandcampbx/client_spec.rb +139 -0
- data/spec/unit/bandcampbx/entities/balance_spec.rb +48 -0
- data/spec/unit/bandcampbx/entities/order_spec.rb +68 -0
- data/spec/unit/bandcampbx/mapper_spec.rb +44 -0
- data/spec/unit/bandcampbx/net_spec.rb +43 -0
- data/spec/unit/bandcampbx_spec.rb +13 -0
- data/travis.yml +3 -0
- metadata +190 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NDUxODJjY2FlZGE0NGU5ODBiYmQ0NjU5MDFkNjgwOWZlMTYzOTkxMw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MjcxNjQyNjYwNDg3MjRlMzExZTYyMDk1YWFkMGQ5N2VlMzAwZTRjOA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
OTU2YmJlNTMxYTU1NDE3YjY5NDMwZWUxYTBhMzcxMjYwZDA1OGQ4ZWY4MDYw
|
10
|
+
MDkyZTA2ZjcwM2VkYzc2MjdjM2JhNTc3MTMwYzZkNWM4ZjE0NGY0MWI2MjA2
|
11
|
+
ZTNkMDU2M2IxOGYyYjc5M2MzZjVjNTNlOWVkNjhiNjBkMzFhMGQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MjZjODJjNzVmZTJjMzZiMzgwZmQwNTc2MDg2ODI1ZjM4NjZkZTNjNzhkYmY2
|
14
|
+
YjAyNWFkOThkZWNhY2RmMDVmZjUzNzM3ODI5ZWM0N2UwZDlmZjgzMjc5OWFk
|
15
|
+
ZGY1ZTUzOGY1MjQ5YWY3NGNjMTNmMTNmMDZkYzYyNTIzYTRkYjM=
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm 1.9.3@bandcampbx --create
|
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Seth Messer
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#BandCampBX
|
2
|
+
|
3
|
+
BandCampBX is a gem for accessing CampBX's Private API
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'bandcampbx', github: 'megalithic/bandcampbx'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
## License
|
16
|
+
|
17
|
+
This software is licensed under [the MIT License.](./LICENSE.md)
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
Bundler::GemHelper.install_tasks
|
8
|
+
|
9
|
+
require 'rspec/core/rake_task'
|
10
|
+
|
11
|
+
RSpec::Core::RakeTask.new(:spec)
|
12
|
+
|
13
|
+
task :default => :spec
|
data/bandcampbx.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'bandcampbx/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "bandcampbx"
|
8
|
+
spec.version = BandCampBX::VERSION
|
9
|
+
spec.authors = ["Seth Messer"]
|
10
|
+
spec.email = ["seth.messer@gmail.com"]
|
11
|
+
spec.description = %q{This is a client library for the CampBX API that supports instantiating multiple clients in the same process.}
|
12
|
+
spec.summary = %q{Outstanding CampBX library.}
|
13
|
+
spec.homepage = "http://github.com/megalithic/bandcampbx"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'net-http-persistent'
|
22
|
+
spec.add_dependency 'multi_json'
|
23
|
+
spec.add_dependency 'json_pure'
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
26
|
+
spec.add_development_dependency "rake"
|
27
|
+
spec.add_development_dependency "rspec", '2.14.0.rc1'
|
28
|
+
spec.add_development_dependency "fakeweb"
|
29
|
+
spec.add_development_dependency "pry"
|
30
|
+
end
|
data/lib/bandcampbx.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative './bandcampbx/version'
|
2
|
+
require_relative './bandcampbx/client'
|
3
|
+
require 'multi_json'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module BandCampBX
|
7
|
+
class StandardError < ::StandardError; end
|
8
|
+
|
9
|
+
module Helpers
|
10
|
+
def self.json_parse(string)
|
11
|
+
MultiJson.load(string)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require_relative 'net'
|
2
|
+
require_relative 'mapper'
|
3
|
+
require 'bigdecimal/util'
|
4
|
+
|
5
|
+
module BandCampBX
|
6
|
+
class Client
|
7
|
+
attr_accessor :key
|
8
|
+
attr_accessor :secret
|
9
|
+
|
10
|
+
def initialize(key = nil, secret = nil)
|
11
|
+
@key = key
|
12
|
+
@secret = secret
|
13
|
+
end
|
14
|
+
|
15
|
+
def balance
|
16
|
+
mapper.map_balance(net.post("myfunds.php"))
|
17
|
+
end
|
18
|
+
|
19
|
+
def orders
|
20
|
+
mapper.map_orders(net.post("myorders.php"))
|
21
|
+
end
|
22
|
+
|
23
|
+
def buy!(quantity, price, order_type)
|
24
|
+
trade!("tradeenter.php", quantity, price, order_type)
|
25
|
+
end
|
26
|
+
|
27
|
+
def sell!(quantity, price, order_type)
|
28
|
+
trade!("tradeenter.php", quantity, price, order_type)
|
29
|
+
end
|
30
|
+
|
31
|
+
def cancel(id, type)
|
32
|
+
wrapping_standard_error do
|
33
|
+
mapper.map_cancel(net.post("tradecancel.php", { id: id.to_s, type: type.to_s }))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def net
|
39
|
+
@net ||= Net.new(self)
|
40
|
+
end
|
41
|
+
|
42
|
+
def mapper
|
43
|
+
@mapper ||= Mapper.new
|
44
|
+
end
|
45
|
+
|
46
|
+
def trade!(endpoint, quantity, price, order_type)
|
47
|
+
wrapping_standard_error do
|
48
|
+
mapper.map_order(net.post(endpoint, { price: price.to_digits, quantity: quantity.to_digits, order_type: order_type.to_s }))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def wrapping_standard_error &block
|
53
|
+
begin
|
54
|
+
yield
|
55
|
+
rescue ::StandardError => e
|
56
|
+
raise BandCampBX::StandardError.new(e.message)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative './base'
|
2
|
+
|
3
|
+
module BandCampBX
|
4
|
+
module Entities
|
5
|
+
class Balance < Base
|
6
|
+
def self.mappings
|
7
|
+
{
|
8
|
+
usd_balance: map_decimal,
|
9
|
+
btc_balance: map_decimal,
|
10
|
+
usd_reserved: map_decimal,
|
11
|
+
btc_reserved: map_decimal,
|
12
|
+
usd_available: map_decimal,
|
13
|
+
btc_available: map_decimal,
|
14
|
+
fee: map_decimal
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
setup_readers
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
3
|
+
module BandCampBX
|
4
|
+
module Entities
|
5
|
+
class Base
|
6
|
+
def self.setup_readers
|
7
|
+
keys.each {|k| attr_reader k.to_sym }
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.keys
|
11
|
+
self.mappings.keys
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(hash)
|
15
|
+
check_for_errors(hash)
|
16
|
+
map_instance_variables(hash)
|
17
|
+
end
|
18
|
+
|
19
|
+
def inspect
|
20
|
+
inspect_string = "#<#{self.class}:#{self.object_id} "
|
21
|
+
self.class.keys.each do |key|
|
22
|
+
inspect_string << "#{key}: #{send(key).inspect} "
|
23
|
+
end
|
24
|
+
inspect_string << " >"
|
25
|
+
inspect_string
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.map_time
|
29
|
+
->(val) { Time.parse(val) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.map_int
|
33
|
+
->(val) { val.to_i }
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.map_decimal
|
37
|
+
->(val) { BigDecimal(val) }
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
def map_instance_variables(hash)
|
42
|
+
self.class.keys.each do |key|
|
43
|
+
instance_variable_set("@#{key}", self.class.mappings[key].call(hash[key.to_s].to_s))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def check_for_errors(hash)
|
48
|
+
if hash.has_key?("error")
|
49
|
+
if hash["error"].has_key?("__all__")
|
50
|
+
raise BandCampBX::StandardError.new(hash["error"]["__all__"].join(". "))
|
51
|
+
else
|
52
|
+
raise BandCampBX::StandardError.new("CampBX API Error #404")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative './base'
|
2
|
+
|
3
|
+
module BandCampBX
|
4
|
+
module Entities
|
5
|
+
class Order < Base
|
6
|
+
class InvalidTypeError < StandardError; end
|
7
|
+
|
8
|
+
def self.map_type
|
9
|
+
->(val) do
|
10
|
+
case val.to_s
|
11
|
+
when '0'
|
12
|
+
:buy
|
13
|
+
when '1'
|
14
|
+
:sell
|
15
|
+
else
|
16
|
+
raise InvalidTypeError
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.mappings
|
22
|
+
{
|
23
|
+
id: map_int,
|
24
|
+
datetime: map_time,
|
25
|
+
type: map_type,
|
26
|
+
price: map_decimal,
|
27
|
+
amount: map_decimal
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
setup_readers
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative './entities/balance'
|
2
|
+
require_relative './entities/order'
|
3
|
+
|
4
|
+
module BandCampBX
|
5
|
+
class Mapper
|
6
|
+
def initialize
|
7
|
+
end
|
8
|
+
|
9
|
+
def map_balance(json)
|
10
|
+
Entities::Balance.new(parsed(json))
|
11
|
+
end
|
12
|
+
|
13
|
+
def map_orders(json)
|
14
|
+
parsed(json).map{|o| map_order(o) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def map_order(order)
|
18
|
+
Entities::Order.new(parsed(order))
|
19
|
+
end
|
20
|
+
|
21
|
+
def map_cancel(result)
|
22
|
+
parsed(result) == 'true'
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
# Allow passing either a String or anything else in. If it's not a string,
|
27
|
+
# we assume we've already parsed it and just give it back to you. This
|
28
|
+
# allows us to handle things like collections more easily.
|
29
|
+
def parsed(json)
|
30
|
+
if(json.is_a?(String))
|
31
|
+
BandCampBX::Helpers.json_parse(json)
|
32
|
+
else
|
33
|
+
json
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'net/http/persistent'
|
2
|
+
|
3
|
+
module BandCampBX
|
4
|
+
class Net
|
5
|
+
attr_reader :client
|
6
|
+
|
7
|
+
def initialize(client)
|
8
|
+
@client = client
|
9
|
+
end
|
10
|
+
|
11
|
+
def secret
|
12
|
+
client.secret
|
13
|
+
end
|
14
|
+
|
15
|
+
def key
|
16
|
+
client.key
|
17
|
+
end
|
18
|
+
|
19
|
+
def post(endpoint, options={})
|
20
|
+
raw_post(endpoint, options)
|
21
|
+
# map_response(raw_post(endpoint, options))
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def url_for(endpoint)
|
26
|
+
base_url + endpoint
|
27
|
+
end
|
28
|
+
|
29
|
+
def base_url
|
30
|
+
'https://campbx.com/api/'
|
31
|
+
end
|
32
|
+
|
33
|
+
def auth_options
|
34
|
+
{
|
35
|
+
user: key,
|
36
|
+
pass: secret
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
# # For some crazy reason, CampBX is returning ruby hash strings rather than
|
41
|
+
# # JSON objects right now ಠ_ಠ I'm just going to gsub '=>' to ':' to 'solve'
|
42
|
+
# # it for now. Not thrilled with this.
|
43
|
+
# def map_response(wish_this_were_reliably_json)
|
44
|
+
# wish_this_were_reliably_json.gsub('=>', ':')
|
45
|
+
# end
|
46
|
+
|
47
|
+
def raw_post(endpoint, options)
|
48
|
+
uri = URI url_for(endpoint)
|
49
|
+
http = ::Net::HTTP::Persistent.new 'bandcampbx'
|
50
|
+
post = ::Net::HTTP::Post.new uri.path
|
51
|
+
post.set_form_data options.merge(auth_options)
|
52
|
+
http.request(uri, post).body
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe "Integrating a client" do
|
4
|
+
subject{ Client.new }
|
5
|
+
|
6
|
+
before do
|
7
|
+
subject.secret = '1'
|
8
|
+
subject.key = '2'
|
9
|
+
end
|
10
|
+
|
11
|
+
it "handles #balance" do
|
12
|
+
example_balance = <<-JSON
|
13
|
+
{
|
14
|
+
"usd_balance": "12.34",
|
15
|
+
"btc_balance": "23.45",
|
16
|
+
"usd_reserved": "1.11",
|
17
|
+
"btc_reserved": "2.22",
|
18
|
+
"usd_available": "11.23",
|
19
|
+
"btc_available": "21.23",
|
20
|
+
"fee": "0.5"
|
21
|
+
}
|
22
|
+
JSON
|
23
|
+
|
24
|
+
FakeWeb.register_uri(:post, "https://campbx.com/api/myfunds.php", body: example_balance)
|
25
|
+
|
26
|
+
bal = subject.balance
|
27
|
+
expect(bal.usd_balance).to eq(BigDecimal('12.34'))
|
28
|
+
end
|
29
|
+
|
30
|
+
it "handles #orders" do
|
31
|
+
example_orders = <<-JSON
|
32
|
+
[
|
33
|
+
{
|
34
|
+
"id": "1",
|
35
|
+
"datetime": "1234567",
|
36
|
+
"type": 0,
|
37
|
+
"price": "12.34",
|
38
|
+
"quantity": "100"
|
39
|
+
}
|
40
|
+
]
|
41
|
+
JSON
|
42
|
+
|
43
|
+
FakeWeb.register_uri(:post, "https://campbx.com/api/myorders.php", body: example_orders)
|
44
|
+
|
45
|
+
orders = subject.orders
|
46
|
+
expect(orders[0].type).to eq(:buy)
|
47
|
+
end
|
48
|
+
|
49
|
+
context "handling #buy!" do
|
50
|
+
it "succeeds properly" do
|
51
|
+
example_buy_response = <<-JSON
|
52
|
+
{
|
53
|
+
"id": "1",
|
54
|
+
"datetime": "1234567",
|
55
|
+
"type": "0",
|
56
|
+
"price": "12.34",
|
57
|
+
"quantity": "100"
|
58
|
+
}
|
59
|
+
JSON
|
60
|
+
|
61
|
+
FakeWeb.register_uri(:post, "https://campbx.com/api/tradeenter.php", body: example_buy_response)
|
62
|
+
|
63
|
+
buy = subject.buy!(BigDecimal('1'), BigDecimal('100'), "QuickBuy")
|
64
|
+
expect(buy.type).to eq(:buy)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "fails properly" do
|
68
|
+
example_buy_response = <<-JSON
|
69
|
+
{"error":{"__all__":["Minimum order quantity is 0.1"]}}
|
70
|
+
JSON
|
71
|
+
|
72
|
+
FakeWeb.register_uri(:post, "https://campbx.com/api/tradeenter.php", body: example_buy_response)
|
73
|
+
|
74
|
+
expect{ subject.buy!(BigDecimal('0.01'), BigDecimal('100'), 'QuickBuy') }.to raise_error(BandCampBX::StandardError, "Minimum order quantity is 0.1")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
it "handles #sell!" do
|
79
|
+
example_sell_response = <<-JSON
|
80
|
+
{
|
81
|
+
"id": "1",
|
82
|
+
"datetime": "1234567",
|
83
|
+
"type": "1",
|
84
|
+
"price": "12.34",
|
85
|
+
"quantity": "100"
|
86
|
+
}
|
87
|
+
JSON
|
88
|
+
|
89
|
+
FakeWeb.register_uri(:post, "https://campbx.com/api/tradeenter.php", body: example_sell_response)
|
90
|
+
|
91
|
+
sell = subject.sell!(BigDecimal('1'), BigDecimal('100'), "QuickSell")
|
92
|
+
expect(sell.type).to eq(:sell)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "handles #cancel" do
|
96
|
+
example_cancel_response = <<-JSON
|
97
|
+
"true"
|
98
|
+
JSON
|
99
|
+
|
100
|
+
FakeWeb.register_uri(:post, "https://campbx.com/api/tradecancel.php", body: example_cancel_response)
|
101
|
+
|
102
|
+
cancel = subject.cancel(12345, "Buy")
|
103
|
+
expect(cancel).to eq(true)
|
104
|
+
end
|
105
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'coveralls'
|
2
|
+
Coveralls.wear!
|
3
|
+
|
4
|
+
require 'rspec'
|
5
|
+
require 'rspec/autorun'
|
6
|
+
require 'bundler'
|
7
|
+
Bundler.setup
|
8
|
+
|
9
|
+
require 'fakeweb'
|
10
|
+
FakeWeb.allow_net_connect = %r[coveralls\.io] # Only allow the coveralls api call through
|
11
|
+
|
12
|
+
require 'multi_json'
|
13
|
+
def json_parse(string)
|
14
|
+
MultiJson.load(string)
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'pry'
|
18
|
+
|
19
|
+
require_relative '../lib/bandcampbx'
|
20
|
+
include BandCampBX
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require_relative '../../spec_helper'
|
2
|
+
|
3
|
+
describe BandCampBX::Client do
|
4
|
+
subject(:client) { described_class.new }
|
5
|
+
let(:net){ client.send(:net) }
|
6
|
+
let(:mapper){ client.send(:mapper) }
|
7
|
+
|
8
|
+
before do
|
9
|
+
client.key = '1'
|
10
|
+
client.secret = '2'
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#initialize' do
|
14
|
+
it 'does not require any initial parameters' do
|
15
|
+
expect { described_class.new }.not_to raise_error
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'allows specifying key/secret on initialize' do
|
19
|
+
client = described_class.new('KEY', 'SECRET')
|
20
|
+
|
21
|
+
expect(client.key).to eql('KEY')
|
22
|
+
expect(client.secret).to eql('SECRET')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#balance' do
|
27
|
+
let(:api_balance_response){ double }
|
28
|
+
let(:balance_object){ double }
|
29
|
+
|
30
|
+
before do
|
31
|
+
net.stub(:post).and_return(api_balance_response)
|
32
|
+
mapper.stub(:map_balance).and_return(balance_object)
|
33
|
+
client.balance
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'requests the balance from the API' do
|
37
|
+
expect(net).to have_received(:post).with('myfunds.php')
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'maps the API response to a Balance object' do
|
41
|
+
expect(mapper).to have_received(:map_balance).with(api_balance_response)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'returns the mapped object' do
|
45
|
+
expect(client.balance).to eq(balance_object)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#orders' do
|
50
|
+
let(:api_orders_response){ double }
|
51
|
+
let(:order_object){ double }
|
52
|
+
|
53
|
+
before do
|
54
|
+
net.stub(:post).and_return(api_orders_response)
|
55
|
+
mapper.stub(:map_orders).and_return([order_object])
|
56
|
+
client.orders
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'requests open orders from the API' do
|
60
|
+
expect(net).to have_received(:post).with('myorders.php')
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'maps the API response to an array of Order objects' do
|
64
|
+
expect(mapper).to have_received(:map_orders).with(api_orders_response)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe 'buy!' do
|
69
|
+
let(:api_buy_response){ double }
|
70
|
+
let(:order_object){ double }
|
71
|
+
|
72
|
+
before do
|
73
|
+
net.stub(:post).and_return(api_buy_response)
|
74
|
+
mapper.stub(:map_order).and_return(order_object)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'submits a buy order to the API' do
|
78
|
+
client.buy!(BigDecimal('100'), BigDecimal('1'), "QuickBuy")
|
79
|
+
expect(net).to have_received(:post).with('tradeenter.php', { quantity: '100.0', price: '1.0', order_type: 'QuickBuy' })
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'maps the API response to an Order object' do
|
83
|
+
client.buy!(BigDecimal('100'), BigDecimal('1'), "QuickBuy")
|
84
|
+
expect(mapper).to have_received(:map_order).with(api_buy_response)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'wraps exceptions in its own class' do
|
88
|
+
net.stub(:post).and_raise(StandardError)
|
89
|
+
expect{ client.buy!(BigDecimal('100'), BigDecimal('1'), "QuickBuy") }.to raise_error(BandCampBX::StandardError)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe 'sell!' do
|
94
|
+
let(:api_sell_response){ double }
|
95
|
+
let(:order_object){ double }
|
96
|
+
|
97
|
+
before do
|
98
|
+
net.stub(:post).and_return(api_sell_response)
|
99
|
+
mapper.stub(:map_order).and_return(order_object)
|
100
|
+
client.sell!(BigDecimal('100'), BigDecimal('1'), "QuickSell")
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'submits a sell order to the API' do
|
104
|
+
expect(net).to have_received(:post).with('tradeenter.php', { quantity: '100.0', price: '1.0', order_type: 'QuickSell' })
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'maps the API response to an Order object' do
|
108
|
+
expect(mapper).to have_received(:map_order).with(api_sell_response)
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'wraps exceptions in its own class' do
|
112
|
+
net.stub(:post).and_raise(StandardError)
|
113
|
+
expect{ client.sell!(BigDecimal('100'), BigDecimal('1'), "QuickSell") }.to raise_error(BandCampBX::StandardError)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe 'cancel' do
|
118
|
+
let(:api_cancel_response){ double }
|
119
|
+
|
120
|
+
before do
|
121
|
+
net.stub(:post).and_return(api_cancel_response)
|
122
|
+
mapper.stub(:map_cancel).and_return(true)
|
123
|
+
client.cancel(1234, "Buy")
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'submits a cancel order to the API' do
|
127
|
+
expect(net).to have_received(:post).with('tradecancel.php', { id: '1234', type: 'Buy' })
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'maps the API response to a boolean' do
|
131
|
+
expect(mapper).to have_received(:map_cancel).with(api_cancel_response)
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'wraps exceptions in its own class' do
|
135
|
+
net.stub(:post).and_raise(StandardError)
|
136
|
+
expect{ client.cancel(123, "Buy") }.to raise_error(BandCampBX::StandardError)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative '../../../spec_helper'
|
2
|
+
|
3
|
+
describe BandCampBX::Entities::Balance do
|
4
|
+
let(:balance_hash){
|
5
|
+
{
|
6
|
+
"usd_balance" => "111.12",
|
7
|
+
"btc_balance" => "211.23",
|
8
|
+
"usd_reserved" => "1.20",
|
9
|
+
"btc_reserved" => "2.30",
|
10
|
+
"usd_available" => "5.50",
|
11
|
+
"btc_available" => "6.60",
|
12
|
+
"fee" => "1.11"
|
13
|
+
}
|
14
|
+
}
|
15
|
+
subject(:balance) { described_class.new(balance_hash) }
|
16
|
+
|
17
|
+
it "has a usd_balance" do
|
18
|
+
expect(balance.usd_balance).to eq(BigDecimal('111.12'))
|
19
|
+
end
|
20
|
+
|
21
|
+
it "has a btc_balance" do
|
22
|
+
expect(balance.btc_balance).to eq(BigDecimal('211.23'))
|
23
|
+
end
|
24
|
+
|
25
|
+
it "has a usd_reserved" do
|
26
|
+
expect(balance.usd_reserved).to eq(BigDecimal('1.20'))
|
27
|
+
end
|
28
|
+
|
29
|
+
it "has a btc_reserved" do
|
30
|
+
expect(balance.btc_reserved).to eq(BigDecimal('2.30'))
|
31
|
+
end
|
32
|
+
|
33
|
+
it "has a usd_available" do
|
34
|
+
expect(balance.usd_available).to eq(BigDecimal('5.50'))
|
35
|
+
end
|
36
|
+
|
37
|
+
it "has a btc_available" do
|
38
|
+
expect(balance.btc_available).to eq(BigDecimal('6.60'))
|
39
|
+
end
|
40
|
+
|
41
|
+
it "has a fee" do
|
42
|
+
expect(balance.fee).to eq(BigDecimal('1.11'))
|
43
|
+
end
|
44
|
+
|
45
|
+
it "can be inspected" do
|
46
|
+
expect { balance.inspect }.to_not raise_error
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require_relative '../../../spec_helper'
|
2
|
+
|
3
|
+
describe BandCampBX::Entities::Order do
|
4
|
+
subject{ described_class.new(order_hash) }
|
5
|
+
|
6
|
+
context "a successful order" do
|
7
|
+
let(:order_hash){
|
8
|
+
{
|
9
|
+
"id" => "1",
|
10
|
+
"datetime" => 1234567,
|
11
|
+
"type" => 0,
|
12
|
+
"price" => "1.23",
|
13
|
+
"amount" => "10"
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
it "has an id" do
|
18
|
+
expect(subject.id).to eq(1)
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "type" do
|
22
|
+
it "maps 0 to :buy" do
|
23
|
+
expect(subject.type).to eq(:buy)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "maps 1 to :sell" do
|
27
|
+
order = described_class.new(order_hash.merge({"type" => 1}))
|
28
|
+
expect(order.type).to eq(:sell)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "raises InvalidTypeError for other values" do
|
32
|
+
expect { described_class.new(order_hash.merge({"type" => 2})) }.to raise_error(BandCampBX::Entities::Order::InvalidTypeError)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "an unsuccessful order" do
|
38
|
+
context "with an __all__ key" do
|
39
|
+
let(:order_hash){
|
40
|
+
# Don't get mad at me, not my fault CampBX errors look like this
|
41
|
+
{
|
42
|
+
"error" => {
|
43
|
+
"__all__" => ["Minimum order size is $1"]
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
it "raises an appropriate error" do
|
49
|
+
expect{ subject }.to raise_error(BandCampBX::StandardError, "Minimum order size is $1")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "without an __all__ key" do
|
54
|
+
let(:order_hash){
|
55
|
+
# Don't get mad at me, not my fault CampBX errors look like this
|
56
|
+
{
|
57
|
+
"error" => {
|
58
|
+
"neg" => ["Minimum order size is $1"]
|
59
|
+
}
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
it "raises an appropriate error" do
|
64
|
+
expect{ subject }.to raise_error(BandCampBX::StandardError, "CampBX API Error #404")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative '../../spec_helper'
|
2
|
+
|
3
|
+
describe BandCampBX::Mapper do
|
4
|
+
subject(:mapper) { described_class.new }
|
5
|
+
let(:json_object){ '{"foo": "bar"}' }
|
6
|
+
let(:json_array){ '[{"foo": "bar"}]' }
|
7
|
+
|
8
|
+
describe '#map_balance' do
|
9
|
+
let(:balance) { double }
|
10
|
+
|
11
|
+
before do
|
12
|
+
Entities::Balance.stub(:new).and_return(balance)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "maps a balance API response into a Balance entity" do
|
16
|
+
mapper.map_balance(json_object)
|
17
|
+
expect(Entities::Balance).to have_received(:new).with(json_parse(json_object))
|
18
|
+
end
|
19
|
+
|
20
|
+
it "returns the mapped Balance entity" do
|
21
|
+
expect(mapper.map_balance(json_object)).to eq(balance)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#map_orders' do
|
26
|
+
let(:order) { double }
|
27
|
+
|
28
|
+
before do
|
29
|
+
Entities::Order.stub(:new).and_return(order)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "maps an open_orders API response into an array of Order entities" do
|
33
|
+
mapper.map_orders(json_array)
|
34
|
+
expect(Entities::Order).to have_received(:new).with(json_parse(json_array)[0])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#map_cancel' do
|
39
|
+
it "maps a cancel API response to a boolean" do
|
40
|
+
expect(mapper.map_cancel('"true"')).to eq(true)
|
41
|
+
expect(mapper.map_cancel('"false"')).to eq(false)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require_relative '../../spec_helper'
|
2
|
+
|
3
|
+
describe BandCampBX::Net do
|
4
|
+
let(:client){ double }
|
5
|
+
subject(:net) { described_class.new(client) }
|
6
|
+
|
7
|
+
before do
|
8
|
+
client.stub(:secret).and_return(1)
|
9
|
+
client.stub(:key).and_return(2)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'gets instantiated with a client' do
|
13
|
+
expect(net.client).to eq(client)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'defers to its client for secret' do
|
17
|
+
expect(net.secret).to eq(1)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'defers to its client for key' do
|
21
|
+
expect(net.key).to eq(2)
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#post' do
|
25
|
+
describe 'any_endpoint' do
|
26
|
+
let(:example_balance) do
|
27
|
+
<<-JSON
|
28
|
+
{
|
29
|
+
"foo": "bar"
|
30
|
+
}
|
31
|
+
JSON
|
32
|
+
end
|
33
|
+
|
34
|
+
before do
|
35
|
+
FakeWeb.register_uri(:post, "https://campbx.com/api/myfunds.php", body: example_balance)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "queries the appropriate endpoint and returns its body as a string" do
|
39
|
+
expect(json_parse(net.post('myfunds.php'))).to eq(json_parse(example_balance))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe BandCampBX do
|
4
|
+
it "should be an instance rather than a singleton" do
|
5
|
+
client = BandCampBX::Client.new
|
6
|
+
expect(client.key).to eq(nil)
|
7
|
+
expect(client.secret).to eq(nil)
|
8
|
+
client.key = '1'
|
9
|
+
client.secret = '2'
|
10
|
+
expect(client.key).to eq('1')
|
11
|
+
expect(client.secret).to eq('2')
|
12
|
+
end
|
13
|
+
end
|
data/travis.yml
ADDED
metadata
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bandcampbx
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Seth Messer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-08-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: net-http-persistent
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: multi_json
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: json_pure
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.3'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 2.14.0.rc1
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 2.14.0.rc1
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: fakeweb
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: pry
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ! '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: This is a client library for the CampBX API that supports instantiating
|
126
|
+
multiple clients in the same process.
|
127
|
+
email:
|
128
|
+
- seth.messer@gmail.com
|
129
|
+
executables: []
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- .gitignore
|
134
|
+
- .rspec
|
135
|
+
- .rvmrc
|
136
|
+
- Gemfile
|
137
|
+
- LICENSE.md
|
138
|
+
- README.md
|
139
|
+
- Rakefile
|
140
|
+
- bandcampbx.gemspec
|
141
|
+
- lib/bandcampbx.rb
|
142
|
+
- lib/bandcampbx/client.rb
|
143
|
+
- lib/bandcampbx/entities/balance.rb
|
144
|
+
- lib/bandcampbx/entities/base.rb
|
145
|
+
- lib/bandcampbx/entities/order.rb
|
146
|
+
- lib/bandcampbx/mapper.rb
|
147
|
+
- lib/bandcampbx/net.rb
|
148
|
+
- lib/bandcampbx/version.rb
|
149
|
+
- spec/integration/client_spec.rb
|
150
|
+
- spec/spec_helper.rb
|
151
|
+
- spec/unit/bandcampbx/client_spec.rb
|
152
|
+
- spec/unit/bandcampbx/entities/balance_spec.rb
|
153
|
+
- spec/unit/bandcampbx/entities/order_spec.rb
|
154
|
+
- spec/unit/bandcampbx/mapper_spec.rb
|
155
|
+
- spec/unit/bandcampbx/net_spec.rb
|
156
|
+
- spec/unit/bandcampbx_spec.rb
|
157
|
+
- travis.yml
|
158
|
+
homepage: http://github.com/megalithic/bandcampbx
|
159
|
+
licenses:
|
160
|
+
- MIT
|
161
|
+
metadata: {}
|
162
|
+
post_install_message:
|
163
|
+
rdoc_options: []
|
164
|
+
require_paths:
|
165
|
+
- lib
|
166
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
167
|
+
requirements:
|
168
|
+
- - ! '>='
|
169
|
+
- !ruby/object:Gem::Version
|
170
|
+
version: '0'
|
171
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
172
|
+
requirements:
|
173
|
+
- - ! '>='
|
174
|
+
- !ruby/object:Gem::Version
|
175
|
+
version: '0'
|
176
|
+
requirements: []
|
177
|
+
rubyforge_project:
|
178
|
+
rubygems_version: 2.0.6
|
179
|
+
signing_key:
|
180
|
+
specification_version: 4
|
181
|
+
summary: Outstanding CampBX library.
|
182
|
+
test_files:
|
183
|
+
- spec/integration/client_spec.rb
|
184
|
+
- spec/spec_helper.rb
|
185
|
+
- spec/unit/bandcampbx/client_spec.rb
|
186
|
+
- spec/unit/bandcampbx/entities/balance_spec.rb
|
187
|
+
- spec/unit/bandcampbx/entities/order_spec.rb
|
188
|
+
- spec/unit/bandcampbx/mapper_spec.rb
|
189
|
+
- spec/unit/bandcampbx/net_spec.rb
|
190
|
+
- spec/unit/bandcampbx_spec.rb
|