634 lines
17 KiB
Ruby
634 lines
17 KiB
Ruby
|
|
# frozen_string_literal: true
|
||
|
|
|
||
|
|
require_relative "../helper"
|
||
|
|
require "csv"
|
||
|
|
require "rack/test"
|
||
|
|
|
||
|
|
# Skooma requires minitest/unit which was removed in Minitest 6
|
||
|
|
$LOADED_FEATURES << "minitest/unit.rb"
|
||
|
|
require "skooma"
|
||
|
|
|
||
|
|
require "versions/v2"
|
||
|
|
|
||
|
|
describe Versions::V2 do
|
||
|
|
include Rack::Test::Methods
|
||
|
|
include Skooma::Minitest[
|
||
|
|
File.expand_path("../../lib/public/v2/openapi.json", __dir__),
|
||
|
|
]
|
||
|
|
|
||
|
|
let(:app) { Versions::V2.freeze }
|
||
|
|
let(:json) { Oj.load(last_response.body) }
|
||
|
|
let(:historical_date) { Fixtures.business_day(30).to_s }
|
||
|
|
let(:range_start) { Fixtures.business_day(60).to_s }
|
||
|
|
let(:range_end) { Fixtures.business_day(30).to_s }
|
||
|
|
let(:year_start) { (Fixtures.latest_date - 365).to_s }
|
||
|
|
let(:year_end) { Fixtures.latest_date.to_s }
|
||
|
|
|
||
|
|
it "returns latest rates" do
|
||
|
|
get "/rates"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
assert_conform_schema(200)
|
||
|
|
_(json.first["base"]).must_equal("EUR")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns rates for a specific date" do
|
||
|
|
get "/rates?date=#{historical_date}"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
assert_conform_schema(200)
|
||
|
|
_(json.first["date"]).must_equal(historical_date)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "snaps to nearest business day for weekends" do
|
||
|
|
sunday = Fixtures.recent_sunday
|
||
|
|
friday = Fixtures.preceding_friday(sunday)
|
||
|
|
get "/rates?date=#{sunday}"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(json.first["date"]).must_equal(friday.to_s)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns rates for a date range" do
|
||
|
|
get "/rates?from=#{range_start}&to=#{range_end}"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
assert_conform_schema(200)
|
||
|
|
dates = json.map { |r| r["date"] }.uniq
|
||
|
|
|
||
|
|
_(dates.length).must_be(:>, 1)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "does not duplicate the first row of a range query" do
|
||
|
|
from = Fixtures.business_day(60).to_s
|
||
|
|
to = Fixtures.business_day(56).to_s
|
||
|
|
get "/rates?base=EUR"es=USD&providers=ecb&from=#{from}&to=#{to}"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
pairs = json.map { |r| [r["date"], r["base"], r["quote"]] }
|
||
|
|
|
||
|
|
_(pairs).must_equal(pairs.uniq)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "does not duplicate the start date when the range opens on a gap boundary" do
|
||
|
|
monday = Fixtures.gap_boundary_monday
|
||
|
|
to = (monday + 2).to_s
|
||
|
|
get "/rates?base=EUR"es=USD&providers=ecb&from=#{monday}&to=#{to}"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
starts = json.select { |r| r["date"] == monday.to_s && r["quote"] == "USD" }
|
||
|
|
|
||
|
|
_(starts.length).must_equal(1)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "does not duplicate the first row of an NDJSON range query" do
|
||
|
|
from = Fixtures.business_day(60).to_s
|
||
|
|
to = Fixtures.business_day(56).to_s
|
||
|
|
get(
|
||
|
|
"/rates?base=EUR"es=USD&providers=ecb&from=#{from}&to=#{to}",
|
||
|
|
{},
|
||
|
|
{ "HTTP_ACCEPT" => "application/x-ndjson" },
|
||
|
|
)
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(last_response.content_type).must_include("application/x-ndjson")
|
||
|
|
rows = last_response.body.split("\n").reject(&:empty?).map { |line| Oj.load(line) }
|
||
|
|
pairs = rows.map { |r| [r["date"], r["base"], r["quote"]] }
|
||
|
|
|
||
|
|
_(pairs).must_equal(pairs.uniq)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "does not duplicate the first row of a CSV range query" do
|
||
|
|
from = Fixtures.business_day(60).to_s
|
||
|
|
to = Fixtures.business_day(56).to_s
|
||
|
|
get "/rates.csv?base=EUR"es=USD&providers=ecb&from=#{from}&to=#{to}"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(last_response.content_type).must_include("text/csv")
|
||
|
|
rows = CSV.parse(last_response.body, headers: true)
|
||
|
|
pairs = rows.map { |r| [r["date"], r["base"], r["quote"]] }
|
||
|
|
|
||
|
|
_(pairs).must_equal(pairs.uniq)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "rebases to a different currency" do
|
||
|
|
get "/rates?base=USD"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
assert_conform_schema(200)
|
||
|
|
_(json.first["base"]).must_equal("USD")
|
||
|
|
|
||
|
|
eur = json.find { |r| r["quote"] == "EUR" }
|
||
|
|
|
||
|
|
_(eur["rate"]).must_be_kind_of(Float)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "does not produce duplicate rows when blending providers" do
|
||
|
|
get "/rates?base=CAD"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
pairs = json.map { |r| [r["date"], r["quote"]] }
|
||
|
|
|
||
|
|
_(pairs).must_equal(pairs.uniq)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "filters quotes" do
|
||
|
|
get "/rates?quotes=USD,GBP"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
assert_conform_schema(200)
|
||
|
|
quotes = json.map { |r| r["quote"] }.uniq.sort
|
||
|
|
|
||
|
|
_(quotes).must_equal(["GBP", "USD"])
|
||
|
|
end
|
||
|
|
|
||
|
|
it "filters by provider" do
|
||
|
|
get "/rates?providers=ecb"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
assert_conform_schema(200)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "filters by multiple providers" do
|
||
|
|
get "/rates?providers=ecb,boc"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
assert_conform_schema(200)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "downsamples by week" do
|
||
|
|
get "/rates?from=#{year_start}&to=#{year_end}&group=week"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
dates = json.map { |r| r["date"] }.uniq
|
||
|
|
|
||
|
|
_(dates.length).must_be(:<=, 55)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "downsamples by month" do
|
||
|
|
get "/rates?from=#{year_start}&to=#{year_end}&group=month"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
dates = json.map { |r| r["date"] }.uniq
|
||
|
|
|
||
|
|
_(dates.length).must_be(:<=, 13)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns 422 for invalid group" do
|
||
|
|
get "/rates?from=#{range_start}&to=#{range_end}&group=day"
|
||
|
|
|
||
|
|
_(last_response.status).must_equal(422)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns 422 for conflicting params" do
|
||
|
|
get "/rates?date=#{historical_date}&from=#{range_start}"
|
||
|
|
|
||
|
|
_(last_response.status).must_equal(422)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns 422 for invalid dates" do
|
||
|
|
get "/rates?date=not-a-date"
|
||
|
|
|
||
|
|
_(last_response.status).must_equal(422)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns an ETag for range queries" do
|
||
|
|
get "/rates?from=#{range_start}&to=#{range_end}"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(last_response.headers["ETag"]).wont_be_nil
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns 422 for unknown parameters" do
|
||
|
|
get "/rates?provider=ecb"
|
||
|
|
|
||
|
|
_(last_response.status).must_equal(422)
|
||
|
|
_(json["message"]).must_include("unknown parameter")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "routes deterministic errors in range queries through error_handler" do
|
||
|
|
bad_query = Object.new
|
||
|
|
def bad_query.range? = true
|
||
|
|
def bad_query.cache_key = "x"
|
||
|
|
|
||
|
|
def bad_query.each
|
||
|
|
return to_enum(:each) unless block_given?
|
||
|
|
|
||
|
|
raise "boom"
|
||
|
|
end
|
||
|
|
|
||
|
|
Versions::V2::RateQuery.stub(:new, bad_query) do
|
||
|
|
get "/rates?from=#{range_start}&to=#{range_end}"
|
||
|
|
end
|
||
|
|
|
||
|
|
_(last_response.status).must_equal(500)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns rates as CSV" do
|
||
|
|
get "/rates.csv"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(last_response.content_type).must_include("text/csv")
|
||
|
|
rows = CSV.parse(last_response.body, headers: true)
|
||
|
|
|
||
|
|
_(rows.headers).must_equal(["date", "base", "quote", "rate"])
|
||
|
|
_(rows.length).must_be(:>, 1)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "expands providers when requested" do
|
||
|
|
get "/rates?expand=providers"es=USD"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
json = Oj.load(last_response.body)
|
||
|
|
|
||
|
|
_(json).wont_be_empty
|
||
|
|
providers = json.first["providers"]
|
||
|
|
|
||
|
|
_(providers).must_be_kind_of(Array)
|
||
|
|
_(providers).wont_be_empty
|
||
|
|
_(providers.first).must_be_kind_of(Hash)
|
||
|
|
_(providers.first.keys.sort).must_equal(["key", "rate"])
|
||
|
|
_(providers.first["key"]).must_be_kind_of(String)
|
||
|
|
_(providers.first["rate"]).must_be_kind_of(Numeric)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "pipe-delimits providers as KEY:RATE in CSV when expand=providers" do
|
||
|
|
from = (Fixtures.latest_date - 7).to_s
|
||
|
|
to = Fixtures.latest_date.to_s
|
||
|
|
get "/rates.csv?expand=providers"es=USD&from=#{from}&to=#{to}"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
rows = CSV.parse(last_response.body, headers: true)
|
||
|
|
|
||
|
|
_(rows.headers).must_equal(["date", "base", "quote", "rate", "providers"])
|
||
|
|
_(rows.first["providers"]).wont_be_nil
|
||
|
|
_(rows.first["providers"]).must_match(/\A[A-Z]+:[\d.]+\*?(\|[A-Z]+:[\d.]+\*?)*\z/)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "rejects unknown expand value" do
|
||
|
|
get "/rates?expand=foo"
|
||
|
|
|
||
|
|
_(last_response.status).must_equal(422)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns 406 for CSV on unsupported endpoints" do
|
||
|
|
get "/currencies.csv"
|
||
|
|
|
||
|
|
_(last_response.status).must_equal(406)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns empty array for dates before dataset" do
|
||
|
|
get "/rates?date=1901-01-01"
|
||
|
|
|
||
|
|
_(last_response.status).must_equal(200)
|
||
|
|
_(Oj.load(last_response.body)).must_be_empty
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns currencies" do
|
||
|
|
get "/currencies?scope=all"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
assert_conform_schema(200)
|
||
|
|
usd = json.find { |c| c["iso_code"] == "USD" }
|
||
|
|
|
||
|
|
_(usd["name"]).must_equal("United States Dollar")
|
||
|
|
_(usd["symbol"]).must_equal("$")
|
||
|
|
_(usd["iso_numeric"]).must_equal("840")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns a single currency" do
|
||
|
|
get "/currency/usd"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(json["iso_code"]).must_equal("USD")
|
||
|
|
_(json["name"]).must_equal("United States Dollar")
|
||
|
|
_(json["providers"]).must_be_kind_of(Array)
|
||
|
|
_(json["providers"]).must_include("ECB")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns 404 for unknown currency" do
|
||
|
|
get "/currency/xyz"
|
||
|
|
|
||
|
|
_(last_response.status).must_equal(404)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "filters currencies by provider" do
|
||
|
|
get "/currencies?providers=ecb"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
codes = json.map { |c| c["iso_code"] }
|
||
|
|
|
||
|
|
_(codes).must_include("USD")
|
||
|
|
_(codes).must_include("EUR")
|
||
|
|
_(codes).wont_include("BMD")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "includes base currencies in currencies list" do
|
||
|
|
get "/currencies?scope=all"
|
||
|
|
eur = json.find { |c| c["iso_code"] == "EUR" }
|
||
|
|
|
||
|
|
_(eur).wont_be_nil
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns a single rate pair" do
|
||
|
|
get "/rate/EUR/USD"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(json["base"]).must_equal("EUR")
|
||
|
|
_(json["quote"]).must_equal("USD")
|
||
|
|
_(json["rate"]).must_be_kind_of(Float)
|
||
|
|
_(json["date"]).wont_be_nil
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns a single rate pair for a historical date" do
|
||
|
|
get "/rate/EUR/USD?date=#{historical_date}"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(json["date"]).must_equal(historical_date)
|
||
|
|
_(json["base"]).must_equal("EUR")
|
||
|
|
_(json["quote"]).must_equal("USD")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "handles case-insensitive currency codes in rate" do
|
||
|
|
get "/rate/eur/usd"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(json["base"]).must_equal("EUR")
|
||
|
|
_(json["quote"]).must_equal("USD")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "filters by provider in single rate" do
|
||
|
|
get "/rate/EUR/USD?providers=ECB"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(json["base"]).must_equal("EUR")
|
||
|
|
_(json["quote"]).must_equal("USD")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns 422 for unknown currency pair" do
|
||
|
|
get "/rate/EUR/XYZ"
|
||
|
|
|
||
|
|
_(last_response.status).must_equal(422)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "sets Vary header on NDJSON responses" do
|
||
|
|
get "/rates", {}, { "HTTP_ACCEPT" => "application/x-ndjson" }
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(last_response.headers["Vary"]).must_equal("Accept")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "prefers CSV extension over NDJSON accept header" do
|
||
|
|
get "/rates.csv", {}, { "HTTP_ACCEPT" => "application/x-ndjson" }
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(last_response.content_type).must_include("text/csv")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns NDJSON when requested" do
|
||
|
|
get(
|
||
|
|
"/rates?from=#{range_start}&to=#{range_end}",
|
||
|
|
{},
|
||
|
|
{ "HTTP_ACCEPT" => "application/x-ndjson" },
|
||
|
|
)
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(last_response.content_type).must_include("application/x-ndjson")
|
||
|
|
lines = last_response.body.strip.split("\n")
|
||
|
|
|
||
|
|
_(lines.length).must_be(:>, 1)
|
||
|
|
parsed = Oj.load(lines.first)
|
||
|
|
|
||
|
|
_(parsed["date"]).wont_be_nil
|
||
|
|
_(parsed["rate"]).wont_be_nil
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns NDJSON for single-date queries when requested" do
|
||
|
|
get "/rates", {}, { "HTTP_ACCEPT" => "application/x-ndjson" }
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(last_response.content_type).must_include("application/x-ndjson")
|
||
|
|
lines = last_response.body.strip.split("\n")
|
||
|
|
parsed = Oj.load(lines.first)
|
||
|
|
|
||
|
|
_(parsed["date"]).wont_be_nil
|
||
|
|
end
|
||
|
|
|
||
|
|
it "streams a valid JSON array for range queries" do
|
||
|
|
get "/rates?from=#{range_start}&to=#{range_end}"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(last_response.content_type).must_include("application/json")
|
||
|
|
parsed = Oj.load(last_response.body)
|
||
|
|
|
||
|
|
_(parsed).must_be_kind_of(Array)
|
||
|
|
_(parsed.first["date"]).wont_be_nil
|
||
|
|
end
|
||
|
|
|
||
|
|
it "iterates over range results" do
|
||
|
|
query = Versions::V2::RateQuery.new("from" => range_start, "to" => range_end)
|
||
|
|
records = []
|
||
|
|
query.each { |r| records << r }
|
||
|
|
|
||
|
|
_(records).wont_be_empty
|
||
|
|
_(records.first).must_include(:date)
|
||
|
|
_(records.first).must_include(:rate)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns rates in alphabetical order by quote" do
|
||
|
|
get "/rates"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
quotes = json.map { |r| r["quote"] }
|
||
|
|
|
||
|
|
_(quotes).must_equal(quotes.sort)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "excludes pegged currencies when providers filter is set" do
|
||
|
|
get "/rates?providers=ecb"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
quotes = json.map { |r| r["quote"] }
|
||
|
|
|
||
|
|
_(quotes).wont_include("BMD")
|
||
|
|
_(quotes).wont_include("FKP")
|
||
|
|
_(quotes).wont_include("BTN")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "filters pegged currencies by quotes param" do
|
||
|
|
get "/rates?quotes=USD,BMD"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
quotes = json.map { |r| r["quote"] }.uniq.sort
|
||
|
|
|
||
|
|
_(quotes).must_equal(["BMD", "USD"])
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns providers" do
|
||
|
|
get "/providers"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
assert_conform_schema(200)
|
||
|
|
ecb = json.find { |p| p["key"] == "ECB" }
|
||
|
|
|
||
|
|
_(ecb["name"]).must_equal("European Central Bank")
|
||
|
|
_(ecb["start_date"]).wont_be_nil
|
||
|
|
_(ecb["end_date"]).wont_be_nil
|
||
|
|
_(ecb["currencies"]).must_include("USD")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns structured provider metadata" do
|
||
|
|
get "/providers"
|
||
|
|
|
||
|
|
ecb = json.find { |p| p["key"] == "ECB" }
|
||
|
|
|
||
|
|
_(ecb["rate_type"]).must_equal("reference rate")
|
||
|
|
_(ecb["pivot_currency"]).must_equal("EUR")
|
||
|
|
_(ecb["country_code"]).must_equal("EU")
|
||
|
|
_(ecb).wont_include("description")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "includes publishes_missed per provider" do
|
||
|
|
get "/providers"
|
||
|
|
|
||
|
|
ecb = json.find { |p| p["key"] == "ECB" }
|
||
|
|
|
||
|
|
_(ecb).must_include("publishes_missed")
|
||
|
|
_(ecb["publishes_missed"]).must_be_kind_of(Integer)
|
||
|
|
_(ecb["publishes_missed"]).must_be(:>=, 0)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "excludes providers without rates" do
|
||
|
|
get "/providers"
|
||
|
|
|
||
|
|
keys = json.map { |p| p["key"] }
|
||
|
|
without_rates = (Provider.all.map(&:key) - Rate.distinct.select_map(:provider)).sample
|
||
|
|
|
||
|
|
_(keys).wont_include(without_rates)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "expands pegged currencies in rates" do
|
||
|
|
get "/rates?base=EUR"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
quotes = json.map { |r| r["quote"] }
|
||
|
|
|
||
|
|
_(quotes).must_include("BMD")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "derives correct rate for pegged currency" do
|
||
|
|
get "/rates?base=EUR"
|
||
|
|
|
||
|
|
usd = json.find { |r| r["quote"] == "USD" }
|
||
|
|
bmd = json.find { |r| r["quote"] == "BMD" }
|
||
|
|
|
||
|
|
_(bmd["rate"]).must_equal(usd["rate"])
|
||
|
|
end
|
||
|
|
|
||
|
|
it "derives correct rate for non-1:1 peg" do
|
||
|
|
get "/rates?base=EUR"
|
||
|
|
|
||
|
|
usd = json.find { |r| r["quote"] == "USD" }
|
||
|
|
ang = json.find { |r| r["quote"] == "ANG" }
|
||
|
|
|
||
|
|
expected = (usd["rate"] * 1.79)
|
||
|
|
|
||
|
|
_(ang["rate"]).must_be_close_to(expected, 0.01)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "expands GBP-pegged currencies" do
|
||
|
|
get "/rates?base=EUR"
|
||
|
|
|
||
|
|
quotes = json.map { |r| r["quote"] }
|
||
|
|
|
||
|
|
_(quotes).must_include("FKP")
|
||
|
|
_(quotes).must_include("GGP")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "expands INR-pegged currencies" do
|
||
|
|
get "/rates?base=EUR"
|
||
|
|
|
||
|
|
quotes = json.map { |r| r["quote"] }
|
||
|
|
|
||
|
|
_(quotes).must_include("BTN")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "resolves pegged base currency" do
|
||
|
|
get "/rates?base=BMD"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(json).wont_be_empty
|
||
|
|
_(json.first["base"]).must_equal("BMD")
|
||
|
|
|
||
|
|
eur = json.find { |r| r["quote"] == "EUR" }
|
||
|
|
|
||
|
|
_(eur).wont_be_nil
|
||
|
|
_(eur["rate"]).must_be_kind_of(Float)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns anchor currency as quote when base is pegged" do
|
||
|
|
get "/rate/GGP/GBP"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(json["base"]).must_equal("GGP")
|
||
|
|
_(json["quote"]).must_equal("GBP")
|
||
|
|
_(json["rate"]).must_equal(1.0)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns anchor currency with non-unity peg rate" do
|
||
|
|
get "/rate/ANG/USD"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(json["base"]).must_equal("ANG")
|
||
|
|
_(json["quote"]).must_equal("USD")
|
||
|
|
_(json["rate"]).must_be_close_to(1.0 / 1.79, 0.001)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "excludes pegs when providers filter is set" do
|
||
|
|
# BMD pegs 1:1 to USD; ECB does not publish BMD. Pegs are a source of rate data, so
|
||
|
|
# scoping ?providers= to ECB excludes pegs along with all other unlisted sources.
|
||
|
|
get "/rates?base=BMD&providers=ecb"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(json).must_be_empty
|
||
|
|
end
|
||
|
|
|
||
|
|
it "includes pegged currencies in currencies list" do
|
||
|
|
get "/currencies?scope=all"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
bmd = json.find { |c| c["iso_code"] == "BMD" }
|
||
|
|
|
||
|
|
_(bmd).wont_be_nil
|
||
|
|
_(bmd["name"]).must_equal("Bermudian Dollar")
|
||
|
|
_(bmd["start_date"]).wont_be_nil
|
||
|
|
_(bmd["end_date"]).wont_be_nil
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns peg metadata for pegged currency detail" do
|
||
|
|
get "/currency/bmd"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(json["iso_code"]).must_equal("BMD")
|
||
|
|
_(json["peg"]).wont_be_nil
|
||
|
|
_(json["peg"]["base"]).must_equal("USD")
|
||
|
|
_(json["peg"]["rate"]).must_equal(1.0)
|
||
|
|
_(json["peg"]["authority"]).must_equal("Bermuda Monetary Authority")
|
||
|
|
end
|
||
|
|
|
||
|
|
it "always includes providers for pegged currency detail" do
|
||
|
|
get "/currency/bmd"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(json["providers"]).must_be_kind_of(Array)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "returns providers for non-pegged currency detail" do
|
||
|
|
get "/currency/usd"
|
||
|
|
|
||
|
|
_(last_response).must_be(:ok?)
|
||
|
|
_(json["providers"]).must_be_kind_of(Array)
|
||
|
|
_(json).wont_include("peg")
|
||
|
|
end
|
||
|
|
end
|