Spring cleaning

- bumped gems
- rm bots
- rm pry byebug
- added rubocop-shopify and corrected generated warnings
This commit is contained in:
Hakan Ensari 2024-11-19 20:06:47 +01:00
parent 243269a322
commit e5815737c1
No known key found for this signature in database
45 changed files with 504 additions and 524 deletions

View File

@ -1,11 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "bundler" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

17
.github/stale.yml vendored
View File

@ -1,17 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@ -1,3 +1,6 @@
inherit_gem:
rubocop-shopify: rubocop.yml
inherit_from: ".rubocop_todo.yml" inherit_from: ".rubocop_todo.yml"
require: require:

48
Gemfile
View File

@ -1,34 +1,34 @@
# frozen_string_literal: true # frozen_string_literal: true
source 'https://rubygems.org' source "https://rubygems.org"
ruby File.read('.ruby-version').chomp ruby File.read(".ruby-version").chomp
gem 'money' gem "money"
gem 'oj' gem "oj"
gem 'ox' gem "ox"
gem 'rack-contrib' gem "rack-contrib"
gem 'rack-cors' gem "rack-cors"
gem 'rake' gem "rake"
gem 'roda' gem "roda"
gem 'rufus-scheduler' gem "rufus-scheduler"
gem 'sequel_pg' gem "sequel_pg"
gem 'unicorn' gem "unicorn"
group :development, :test do group :development, :test do
gem 'pry-byebug' gem "rubocop-minitest"
gem 'rubocop-minitest' gem "rubocop-performance"
gem 'rubocop-performance' gem "rubocop-rake"
gem 'rubocop-rake' gem "rubocop-sequel"
gem 'rubocop-sequel' gem "rubocop-shopify"
end end
group :test do group :test do
gem 'minitest' gem "minitest"
gem 'minitest-around' gem "minitest-around"
gem 'minitest-focus' gem "minitest-focus"
gem 'rack-test' gem "rack-test"
gem 'simplecov' gem "simplecov"
gem 'vcr' gem "vcr"
gem 'webmock' gem "webmock"
end end

View File

@ -6,8 +6,6 @@ GEM
ast (2.4.2) ast (2.4.2)
base64 (0.2.0) base64 (0.2.0)
bigdecimal (3.1.8) bigdecimal (3.1.8)
byebug (11.1.3)
coderay (1.1.3)
concurrent-ruby (1.3.4) concurrent-ruby (1.3.4)
crack (1.0.0) crack (1.0.0)
bigdecimal bigdecimal
@ -18,13 +16,12 @@ GEM
fugit (1.11.1) fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11) et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4) raabro (~> 1.4)
hashdiff (1.1.1) hashdiff (1.1.2)
i18n (1.14.6) i18n (1.14.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
json (2.8.2) json (2.8.2)
kgio (2.11.4) kgio (2.11.4)
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
method_source (1.1.0)
minitest (5.25.1) minitest (5.25.1)
minitest-around (0.5.0) minitest-around (0.5.0)
minitest (~> 5.0) minitest (~> 5.0)
@ -35,19 +32,13 @@ GEM
oj (3.16.7) oj (3.16.7)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
ostruct (>= 0.2) ostruct (>= 0.2)
ostruct (0.6.0) ostruct (0.6.1)
ox (2.14.18) ox (2.14.18)
parallel (1.26.3) parallel (1.26.3)
parser (3.3.6.0) parser (3.3.6.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg (1.5.8) pg (1.5.9)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
pry-byebug (3.10.1)
byebug (~> 11.0)
pry (>= 0.13, < 0.15)
public_suffix (6.0.1) public_suffix (6.0.1)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
@ -87,10 +78,12 @@ GEM
rubocop (~> 1.0) rubocop (~> 1.0)
rubocop-sequel (0.3.7) rubocop-sequel (0.3.7)
rubocop (~> 1.0) rubocop (~> 1.0)
rubocop-shopify (2.15.1)
rubocop (~> 1.51)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
rufus-scheduler (3.9.2) rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1) fugit (~> 1.1, >= 1.11.1)
sequel (5.85.0) sequel (5.86.0)
bigdecimal bigdecimal
sequel_pg (1.17.1) sequel_pg (1.17.1)
pg (>= 0.18.0, != 1.2.0) pg (>= 0.18.0, != 1.2.0)
@ -124,7 +117,6 @@ DEPENDENCIES
money money
oj oj
ox ox
pry-byebug
rack-contrib rack-contrib
rack-cors rack-cors
rack-test rack-test
@ -134,6 +126,7 @@ DEPENDENCIES
rubocop-performance rubocop-performance
rubocop-rake rubocop-rake
rubocop-sequel rubocop-sequel
rubocop-shopify
rufus-scheduler rufus-scheduler
sequel_pg sequel_pg
simplecov simplecov

View File

@ -1,3 +1,3 @@
# frozen_string_literal: true # frozen_string_literal: true
Dir.glob('lib/tasks/*.rake').each { |r| import r } Dir.glob("lib/tasks/*.rake").each { |r| import r }

View File

@ -1,12 +1,12 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
# frozen_string_literal: true # frozen_string_literal: true
require 'rufus-scheduler' require "rufus-scheduler"
scheduler = Rufus::Scheduler.new scheduler = Rufus::Scheduler.new
scheduler.cron '*/30 15,16,17 * * 1-5' do scheduler.cron("*/30 15,16,17 * * 1-5") do
`rake rates:current` %x(rake rates:current)
end end
scheduler.join scheduler.join

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require './config/environment' require "./config/environment"
require 'web/server' require "web/server"
run Web::Server.freeze.app run Web::Server.freeze.app

View File

@ -1,16 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'pathname' require "pathname"
# Encapsulates app configuration # Encapsulates app configuration
module App module App
class << self class << self
def env def env
ENV['APP_ENV'] || 'development' ENV["APP_ENV"] || "development"
end end
def root def root
Pathname.new(File.expand_path('..', __dir__)) Pathname.new(File.expand_path("..", __dir__))
end end
end end
end end

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'app' require_relative "app"
$LOAD_PATH << App.root.join('lib') $LOAD_PATH << App.root.join("lib")
Dir[App.root.join('config/initializers/*.rb')].each { |f| require f } Dir[App.root.join("config/initializers/*.rb")].each { |f| require f }

View File

@ -1,10 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'pg' require "pg"
require 'sequel' require "sequel"
Sequel.extension :pg_json_ops Sequel.extension(:pg_json_ops)
Sequel.single_threaded = true Sequel.single_threaded = true
Sequel.connect(ENV['DATABASE_URL'] || Sequel.connect(ENV["DATABASE_URL"] ||
"postgres://localhost:#{ENV.fetch('PGPORT', nil)}/frankfurter_#{App.env}") "postgres://localhost:#{ENV.fetch("PGPORT", nil)}/frankfurter_#{App.env}")
.extension :pg_json .extension(:pg_json)

View File

@ -1,8 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
`rake db:prepare` %x(rake db:prepare)
worker_process_count = (ENV['WORKER_PROCESSES'] || 4).to_i worker_process_count = (ENV["WORKER_PROCESSES"] || 4).to_i
preload_app true preload_app true
worker_processes worker_process_count worker_processes worker_process_count
@ -12,7 +12,7 @@ initialized = false
before_fork do |_server, _worker| before_fork do |_server, _worker|
Sequel::DATABASES.each(&:disconnect) Sequel::DATABASES.each(&:disconnect)
unless initialized unless initialized
require 'scheduler/daemon' require "scheduler/daemon"
Scheduler::Daemon.start Scheduler::Daemon.start
initialized = true initialized = true
end end

View File

@ -7,7 +7,7 @@ Sequel.migration do
String :iso_code String :iso_code
Float :rate Float :rate
index %i[date iso_code], unique: true index [:date, :iso_code], unique: true
end end
end end

View File

@ -15,7 +15,7 @@ Sequel.migration do
date :date date :date
string :iso_code string :iso_code
float :rate float :rate
index %i[date iso_code], unique: true index [:date, :iso_code], unique: true
end end
drop_table :days drop_table :days
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'day' require "day"
require 'bank/feed' require "bank/feed"
module Bank module Bank
class << self class << self

View File

@ -1,45 +1,47 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'net/http' require "net/http"
require 'ox' require "ox"
module Bank module Bank
class Feed class Feed
include Enumerable include Enumerable
def self.current class << self
url = URI('https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml') def current
url = URI("https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml")
xml = Net::HTTP.get(url) xml = Net::HTTP.get(url)
new(xml) new(xml)
end end
def self.ninety_days def ninety_days
url = URI('https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml') url = URI("https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml")
xml = Net::HTTP.get(url) xml = Net::HTTP.get(url)
new(xml) new(xml)
end end
def self.historical def historical
url = URI('https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml') url = URI("https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml")
xml = Net::HTTP.get(url) xml = Net::HTTP.get(url)
new(xml) new(xml)
end end
def self.saved_data def saved_data
xml = File.read(File.join(__dir__, 'eurofxref-hist.xml')) xml = File.read(File.join(__dir__, "eurofxref-hist.xml"))
new(xml) new(xml)
end end
end
def initialize(xml) def initialize(xml)
@document = Ox.load(xml) @document = Ox.load(xml)
end end
def each def each
@document.locate('gesmes:Envelope/Cube/Cube').each do |day| @document.locate("gesmes:Envelope/Cube/Cube").each do |day|
yield(date: Date.parse(day['time']), yield(date: Date.parse(day["time"]),
rates: day.nodes.each_with_object({}) do |currency, rates| rates: day.nodes.each_with_object({}) do |currency, rates|
rates[currency[:currency]] = Float(currency[:rate]) rates[currency[:currency]] = Float(currency[:rate])
end) end)

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'day' require "day"
require 'forwardable' require "forwardable"
class Currency < Sequel::Model(Day.currencies) class Currency < Sequel::Model(Day.currencies)
class << self class << self
@ -17,7 +17,7 @@ class Currency < Sequel::Model(Day.currencies)
def between(interval) def between(interval)
case interval.last - interval.first case interval.last - interval.first
when 366.. then super.sample('week') when 366.. then super.sample("week")
else super else super
end end
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'currency' require "currency"
require 'money/currency' require "money/currency"
class CurrencyNames class CurrencyNames
def cache_key def cache_key
@ -17,7 +17,7 @@ class CurrencyNames
private private
def iso_codes def iso_codes
currencies.map(&:iso_code).append('EUR').sort currencies.map(&:iso_code).append("EUR").sort
end end
def currencies def currencies

View File

@ -3,7 +3,7 @@
class Day < Sequel::Model class Day < Sequel::Model
dataset_module do dataset_module do
def latest(date = Date.today) def latest(date = Date.today)
where(date: select(:date).where(Sequel.lit('date <= ?', date)) where(date: select(:date).where(Sequel.lit("date <= ?", date))
.order(Sequel.desc(:date)) .order(Sequel.desc(:date))
.limit(1)) .limit(1))
end end
@ -13,9 +13,11 @@ class Day < Sequel::Model
end end
def currencies def currencies
select(:date, select(
Sequel.lit('rates.key').as(:iso_code), :date,
Sequel.lit('rates.value::text::float').as(:rate)) Sequel.lit("rates.key").as(:iso_code),
Sequel.lit("rates.value::text::float").as(:rate),
)
.join(Sequel.function(:jsonb_each, :rates).lateral.as(:rates), true) .join(Sequel.function(:jsonb_each, :rates).lateral.as(:rates), true)
end end
end end

View File

@ -1,9 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class Query class Query
def self.build(params) class << self
def build(params)
new(params).to_h new(params).to_h
end end
end
def initialize(params = {}) def initialize(params = {})
@params = params @params = params
@ -20,7 +22,7 @@ class Query
end end
def symbols def symbols
@params.values_at(:to, :symbols).compact.first&.upcase&.split(',') @params.values_at(:to, :symbols).compact.first&.upcase&.split(",")
end end
def date def date

View File

@ -1,4 +1,4 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'quote/end_of_day' require "quote/end_of_day"
require 'quote/interval' require "quote/interval"

View File

@ -1,16 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'roundable' require "roundable"
module Quote module Quote
class Base class Base
include Roundable include Roundable
DEFAULT_BASE = 'EUR' DEFAULT_BASE = "EUR"
attr_reader :amount, :base, :date, :symbols, :result attr_reader :amount, :base, :date, :symbols, :result
def initialize(date:, amount: 1.0, base: 'EUR', symbols: nil) def initialize(date:, amount: 1.0, base: "EUR", symbols: nil)
@date = date @date = date
@amount = amount @amount = amount
@base = base @base = base
@ -29,7 +29,7 @@ module Quote
end end
def must_rebase? def must_rebase?
base != 'EUR' base != "EUR"
end end
def formatted def formatted
@ -64,7 +64,7 @@ module Quote
def rebase_rates def rebase_rates
result.each do |date, rates| result.each do |date, rates|
rates['EUR'] = amount if symbols.nil? || symbols.include?('EUR') rates["EUR"] = amount if symbols.nil? || symbols.include?("EUR")
divisor = rates.delete(base) divisor = rates.delete(base)
if divisor.nil? || rates.empty? if divisor.nil? || rates.empty?
result.delete(date) result.delete(date)

View File

@ -1,15 +1,17 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'quote/base' require "quote/base"
require 'digest' require "digest"
module Quote module Quote
class EndOfDay < Base class EndOfDay < Base
def formatted def formatted
{ amount:, {
amount:,
base:, base:,
date: result.keys.first, date: result.keys.first,
rates: result.values.first } rates: result.values.first,
}
end end
def cache_key def cache_key
@ -21,7 +23,7 @@ module Quote
private private
def fetch_data def fetch_data
require 'currency' require "currency"
scope = Currency.latest(date) scope = Currency.latest(date)
scope = scope.only(*(symbols + [base])) if symbols scope = scope.only(*(symbols + [base])) if symbols

View File

@ -1,15 +1,17 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'quote/base' require "quote/base"
module Quote module Quote
class Interval < Base class Interval < Base
def formatted def formatted
{ amount:, {
amount:,
base:, base:,
start_date: result.keys.first, start_date: result.keys.first,
end_date: result.keys.last, end_date: result.keys.last,
rates: result } rates: result,
}
end end
def cache_key def cache_key
@ -21,7 +23,7 @@ module Quote
private private
def fetch_data def fetch_data
require 'currency' require "currency"
scope = Currency.between(date) scope = Currency.between(date)
scope = scope.only(*(symbols + [base])) if symbols scope = scope.only(*(symbols + [base])) if symbols

View File

@ -13,18 +13,18 @@ module Roundable
if value > 5000 if value > 5000
value.round value.round
elsif value > 80 elsif value > 80
Float(format('%<value>.2f', value:)) Float(format("%<value>.2f", value:))
elsif value > 20 elsif value > 20
Float(format('%<value>.3f', value:)) Float(format("%<value>.3f", value:))
elsif value > 1 elsif value > 1
Float(format('%<value>.4f', value:)) Float(format("%<value>.4f", value:))
# I had originally opted to round smaller numbers simply to five decimal # I had originally opted to round smaller numbers simply to five decimal
# places but introduced this refinement to handle an edge case where a # places but introduced this refinement to handle an edge case where a
# lower-rate base currency like IDR produces less precise quotes. # lower-rate base currency like IDR produces less precise quotes.
elsif value > 0.0001 elsif value > 0.0001
Float(format('%<value>.5f', value:)) Float(format("%<value>.5f", value:))
else else
Float(format('%<value>.6f', value:)) Float(format("%<value>.6f", value:))
end end
end end
end end

View File

@ -4,9 +4,11 @@ module Scheduler
class Daemon class Daemon
attr_reader :pid attr_reader :pid
def self.start class << self
def start
new.start new.start
end end
end
def initialize def initialize
@parent_pid = Process.pid @parent_pid = Process.pid
@ -25,7 +27,7 @@ module Scheduler
private private
def run def run
load 'bin/schedule' load("bin/schedule")
end end
def monitor_child def monitor_child
@ -33,7 +35,7 @@ module Scheduler
@child_monitor = Thread.new do @child_monitor = Thread.new do
loop do loop do
sleep 5 sleep(5)
unless alive?(pid) unless alive?(pid)
@pid = nil @pid = nil
start start
@ -46,7 +48,7 @@ module Scheduler
Thread.new do Thread.new do
loop do loop do
exit unless alive?(@parent_pid) exit unless alive?(@parent_pid)
sleep 1 sleep(1)
end end
end end
end end

View File

@ -1,26 +1,26 @@
# frozen_string_literal: true # frozen_string_literal: true
namespace :db do namespace :db do
desc 'Run database migrations' desc "Run database migrations"
task migrate: :environment do task migrate: :environment do
Sequel.extension(:migration) Sequel.extension(:migration)
db = Sequel::DATABASES.first db = Sequel::DATABASES.first
dir = App.root.join('db/migrate') dir = App.root.join("db/migrate")
opts = {} opts = {}
opts.update(target: ENV['VERSION'].to_i) if ENV['VERSION'] opts.update(target: ENV["VERSION"].to_i) if ENV["VERSION"]
Sequel::IntegerMigrator.new(db, dir, opts).run Sequel::IntegerMigrator.new(db, dir, opts).run
end end
desc 'Run database migrations and seed data' desc "Run database migrations and seed data"
task prepare: %w[db:migrate rates:all] task prepare: ["db:migrate", "rates:all"]
namespace :test do namespace :test do
desc 'Run database migrations and seed with saved data' desc "Run database migrations and seed with saved data"
task :prepare do task :prepare do
ENV['APP_ENV'] ||= 'test' ENV["APP_ENV"] ||= "test"
Rake::Task['db:migrate'].invoke Rake::Task["db:migrate"].invoke
Rake::Task['rates:seed_with_saved_data'].invoke Rake::Task["rates:seed_with_saved_data"].invoke
end end
end end
end end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
desc 'Load environment' desc "Load environment"
task :environment do task :environment do
require './config/environment' require "./config/environment"
end end

View File

@ -1,27 +1,27 @@
# frozen_string_literal: true # frozen_string_literal: true
namespace :rates do namespace :rates do
desc 'Load all' desc "Load all"
task all: :environment do task all: :environment do
require 'bank' require "bank"
Bank.fetch_all! Bank.fetch_all!
end end
desc 'Load last 90 days' desc "Load last 90 days"
task ninety_days: :environment do task ninety_days: :environment do
require 'bank' require "bank"
Bank.fetch_ninety_days! Bank.fetch_ninety_days!
end end
desc 'Load current' desc "Load current"
task current: :environment do task current: :environment do
require 'bank' require "bank"
Bank.fetch_current! Bank.fetch_current!
end end
desc 'Seed with saved data' desc "Seed with saved data"
task :seed_with_saved_data do task :seed_with_saved_data do
require 'bank' require "bank"
Bank.seed_with_saved_data! Bank.seed_with_saved_data!
end end
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
begin begin
require 'sitemap_generator/tasks' require "sitemap_generator/tasks"
rescue LoadError rescue LoadError
return return
end end

View File

@ -1,18 +1,18 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rake/testtask' require "rake/testtask"
begin begin
require 'rubocop/rake_task' require "rubocop/rake_task"
rescue LoadError rescue LoadError
return return
end end
Rake::TestTask.new(test: :environment) do |t| Rake::TestTask.new(test: :environment) do |t|
t.libs.push('lib') t.libs.push("lib")
t.test_files = FileList['spec/**/*_spec.rb'] t.test_files = FileList["spec/**/*_spec.rb"]
t.ruby_opts += ['-W0'] t.ruby_opts += ["-W0"]
end end
RuboCop::RakeTask.new RuboCop::RakeTask.new
task default: %w[rubocop db:test:prepare test] task default: ["rubocop", "db:test:prepare", "test"]

View File

@ -1,20 +1,20 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'oj' require "oj"
require 'rack/contrib/jsonp' require "rack/contrib/jsonp"
require 'rack/cors' require "rack/cors"
require 'roda' require "roda"
require 'currency_names' require "currency_names"
require 'query' require "query"
require 'quote' require "quote"
module Web module Web
class Server < Roda class Server < Roda
use Rack::Cors do use Rack::Cors do
allow do allow do
origins '*' origins "*"
resource '*', headers: :any, methods: %i[get options] resource "*", headers: :any, methods: [:get, :options]
end end
end end
use Rack::JSONP use Rack::JSONP
@ -23,51 +23,52 @@ module Web
plugin :halt plugin :halt
plugin :error_handler do |error| plugin :error_handler do |error|
request.halt 422, { message: error.message } request.halt(422, { message: error.message })
end end
plugin :indifferent_params plugin :indifferent_params
plugin :json, content_type: 'application/json; charset=utf-8', plugin :json,
content_type: "application/json; charset=utf-8",
serializer: ->(o) { Oj.dump(o, mode: :compat) } serializer: ->(o) { Oj.dump(o, mode: :compat) }
plugin :params_capturing plugin :params_capturing
route do |r| route do |r|
response.cache_control public: true, max_age: 900 response.cache_control(public: true, max_age: 900)
r.root do r.root do
{ docs: 'https://www.frankfurter.app/docs' } { docs: "https://www.frankfurter.app/docs" }
end end
r.is(/latest|current/) do r.is(/latest|current/) do
r.params['date'] = Date.today.to_s r.params["date"] = Date.today.to_s
quote = quote_end_of_day(r) quote = quote_end_of_day(r)
r.etag quote.cache_key r.etag(quote.cache_key)
quote.formatted quote.formatted
end end
r.is(/(\d{4}-\d{2}-\d{2})/) do r.is(/(\d{4}-\d{2}-\d{2})/) do
r.params['date'] = r.params['captures'].first r.params["date"] = r.params["captures"].first
quote = quote_end_of_day(r) quote = quote_end_of_day(r)
r.etag quote.cache_key r.etag(quote.cache_key)
quote.formatted quote.formatted
end end
r.is(/(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})?/) do r.is(/(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})?/) do
r.params['start_date'] = r.params['captures'].first r.params["start_date"] = r.params["captures"].first
r.params['end_date'] = r.params['captures'][1] || Date.today.to_s r.params["end_date"] = r.params["captures"][1] || Date.today.to_s
quote = quote_interval(r) quote = quote_interval(r)
r.etag quote.cache_key r.etag(quote.cache_key)
quote.formatted quote.formatted
end end
r.is 'currencies' do r.is("currencies") do
currency_names = CurrencyNames.new currency_names = CurrencyNames.new
r.etag currency_names.cache_key r.etag(currency_names.cache_key)
currency_names.formatted currency_names.formatted
end end
@ -79,7 +80,7 @@ module Web
query = Query.build(request.params) query = Query.build(request.params)
quote = Quote::EndOfDay.new(**query) quote = Quote::EndOfDay.new(**query)
quote.perform quote.perform
request.halt 404, { message: 'not found' } if quote.not_found? request.halt(404, { message: "not found" }) if quote.not_found?
quote quote
end end
@ -88,7 +89,7 @@ module Web
query = Query.build(request.params) query = Query.build(request.params)
quote = Quote::Interval.new(**query) quote = Quote::Interval.new(**query)
quote.perform quote.perform
request.halt 404, { message: 'not found' } if quote.not_found? request.halt(404, { message: "not found" }) if quote.not_found?
quote quote
end end

View File

@ -1,46 +1,46 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative '../helper' require_relative "../helper"
require 'bank/feed' require "bank/feed"
module Bank module Bank
describe Feed do describe Feed do
before do before do
VCR.insert_cassette 'feed' VCR.insert_cassette("feed")
end end
after do after do
VCR.eject_cassette VCR.eject_cassette
end end
it 'fetches current rates' do it "fetches current rates" do
feed = Feed.current feed = Feed.current
_(feed.count).must_be :==, 1 _(feed.count).must_be(:==, 1)
end end
it 'fetches rates for the past 90 days' do it "fetches rates for the past 90 days" do
feed = Feed.ninety_days feed = Feed.ninety_days
_(feed.count).must_be :>, 1 _(feed.count).must_be(:>, 1)
_(feed.count).must_be :<=, 90 _(feed.count).must_be(:<=, 90)
end end
it 'fetches historical rates' do it "fetches historical rates" do
feed = Feed.historical feed = Feed.historical
_(feed.count).must_be :>, 90 _(feed.count).must_be(:>, 90)
end end
it 'parses dates' do it "parses dates" do
feed = Feed.current feed = Feed.current
day = feed.first day = feed.first
_(day[:date]).must_be_kind_of Date _(day[:date]).must_be_kind_of(Date)
end end
it 'parses rates' do it "parses rates" do
feed = Feed.current feed = Feed.current
day = feed.first day = feed.first
day[:rates].each do |iso_code, value| day[:rates].each do |iso_code, value|
_(iso_code).must_be_kind_of String _(iso_code).must_be_kind_of(String)
_(value).must_be_kind_of Float _(value).must_be_kind_of(Float)
end end
end end
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'helper' require_relative "helper"
require 'bank' require "bank"
describe Bank do describe Bank do
around do |test| around do |test|
@ -13,55 +13,55 @@ describe Bank do
end end
before do before do
VCR.insert_cassette 'feed' VCR.insert_cassette("feed")
end end
after do after do
VCR.eject_cassette VCR.eject_cassette
end end
it 'fetches all rates' do it "fetches all rates" do
Bank.fetch_all! Bank.fetch_all!
_(Day.count).must_be :>, 90 _(Day.count).must_be(:>, 90)
end end
it 'does not duplicate when fetching all rates' do it "does not duplicate when fetching all rates" do
Bank.fetch_all! Bank.fetch_all!
count = Day.count count = Day.count
Bank.fetch_all! Bank.fetch_all!
_(Day.count).must_equal count _(Day.count).must_equal(count)
end end
it 'fetches rates for last 90 days' do it "fetches rates for last 90 days" do
Bank.fetch_ninety_days! Bank.fetch_ninety_days!
_(Day.count).must_be :>, 1 _(Day.count).must_be(:>, 1)
_(Day.count).must_be :<, 90 _(Day.count).must_be(:<, 90)
end end
it 'seeds rates with saved data' do it "seeds rates with saved data" do
Bank.seed_with_saved_data! Bank.seed_with_saved_data!
_(Day.count).must_be :>, 90 _(Day.count).must_be(:>, 90)
end end
it 'does not duplicate when fetching rates for last 90 days' do it "does not duplicate when fetching rates for last 90 days" do
Bank.fetch_ninety_days! Bank.fetch_ninety_days!
count = Day.count count = Day.count
Bank.fetch_ninety_days! Bank.fetch_ninety_days!
_(Day.count).must_equal count _(Day.count).must_equal(count)
end end
it 'fetches current rates' do it "fetches current rates" do
Bank.fetch_current! Bank.fetch_current!
_(Day.count).must_equal 1 _(Day.count).must_equal(1)
end end
it 'does not duplicate when fetching current rates' do it "does not duplicate when fetching current rates" do
2.times { Bank.fetch_current! } 2.times { Bank.fetch_current! }
_(Day.count).must_equal 1 _(Day.count).must_equal(1)
end end
it 'replaces all rates' do it "replaces all rates" do
Bank.replace_all! Bank.replace_all!
_(Day.count).must_be :>, 90 _(Day.count).must_be(:>, 90)
end end
end end

View File

@ -1,18 +1,18 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'helper' require_relative "helper"
require 'currency_names' require "currency_names"
describe CurrencyNames do describe CurrencyNames do
let(:currency_names) do let(:currency_names) do
CurrencyNames.new CurrencyNames.new
end end
it 'returns currency codes and names' do it "returns currency codes and names" do
_(currency_names.formatted['USD']).must_equal 'United States Dollar' _(currency_names.formatted["USD"]).must_equal("United States Dollar")
end end
it 'has a cache key' do it "has a cache key" do
_(currency_names.cache_key).wont_be :empty? _(currency_names.cache_key).wont_be(:empty?)
end end
end end

View File

@ -1,48 +1,48 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'helper' require_relative "helper"
require 'currency' require "currency"
require 'minitest/autorun' require "minitest/autorun"
describe Currency do describe Currency do
describe '.latest' do describe ".latest" do
it 'returns latest rates' do it "returns latest rates" do
data = Currency.latest.all data = Currency.latest.all
_(data.count).must_be :>, 1 _(data.count).must_be(:>, 1)
end end
end end
describe '.between' do describe ".between" do
let(:day) do let(:day) do
Date.parse('2010-01-01') Date.parse("2010-01-01")
end end
it 'returns everything up to a year' do it "returns everything up to a year" do
interval = day..day + 365 interval = day..day + 365
_(Currency.between(interval).map(:date).uniq.count).must_be :>, 52 _(Currency.between(interval).map(:date).uniq.count).must_be(:>, 52)
end end
it 'samples weekly over a year' do it "samples weekly over a year" do
interval = day..day + 366 interval = day..day + 366
_(Currency.between(interval).map(:date).uniq.count).must_be :<, 54 _(Currency.between(interval).map(:date).uniq.count).must_be(:<, 54)
end end
it 'sorts by date when sampling' do it "sorts by date when sampling" do
interval = day..day + 366 interval = day..day + 366
dates = Currency.between(interval).map(:date) dates = Currency.between(interval).map(:date)
_(dates).must_equal dates.sort _(dates).must_equal(dates.sort)
end end
end end
describe '.only' do describe ".only" do
it 'filters symbols' do it "filters symbols" do
iso_codes = %w[CAD USD] iso_codes = ["CAD", "USD"]
data = Currency.latest.only(*iso_codes).all data = Currency.latest.only(*iso_codes).all
_(data.map(&:iso_code).sort).must_equal iso_codes _(data.map(&:iso_code).sort).must_equal(iso_codes)
end end
it 'returns nothing if no matches' do it "returns nothing if no matches" do
_(Currency.only('FOO').all).must_be_empty _(Currency.only("FOO").all).must_be_empty
end end
end end
end end

View File

@ -1,32 +1,32 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'helper' require_relative "helper"
require 'day' require "day"
describe Day do describe Day do
describe '.latest' do describe ".latest" do
it 'returns latest rates before given date' do it "returns latest rates before given date" do
date = Date.parse('2010-01-01') date = Date.parse("2010-01-01")
data = Day.latest(date) data = Day.latest(date)
_(data.first.date).must_be :<=, date _(data.first.date).must_be(:<=, date)
end end
it 'returns nothing if there are no rates before given date' do it "returns nothing if there are no rates before given date" do
_(Day.latest(Date.parse('1998-01-01'))).must_be_empty _(Day.latest(Date.parse("1998-01-01"))).must_be_empty
end end
end end
describe '.between' do describe ".between" do
it 'returns rates between given dates' do it "returns rates between given dates" do
start_date = Date.parse('2010-01-01') start_date = Date.parse("2010-01-01")
end_date = Date.parse('2010-01-31') end_date = Date.parse("2010-01-31")
dates = Day.between((start_date..end_date)).map(:date).sort dates = Day.between((start_date..end_date)).map(:date).sort
_(dates.first).must_be :>=, start_date _(dates.first).must_be(:>=, start_date)
_(dates.last).must_be :<=, end_date _(dates.last).must_be(:<=, end_date)
end end
it 'returns nothing if there are no rates between given dates' do it "returns nothing if there are no rates between given dates" do
interval = (Date.parse('1998-01-01')..Date.parse('1998-01-31')) interval = (Date.parse("1998-01-01")..Date.parse("1998-01-31"))
_(Day.between(interval)).must_be_empty _(Day.between(interval)).must_be_empty
end end
end end

View File

@ -1,10 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'helper' require_relative "helper"
require 'rack/test' require "rack/test"
require 'web/server' require "web/server"
describe 'the server' do describe "the server" do
include Rack::Test::Methods include Rack::Test::Methods
let(:app) { Web::Server.freeze } let(:app) { Web::Server.freeze }
@ -13,38 +13,38 @@ describe 'the server' do
Oj.load(last_response.body) Oj.load(last_response.body)
end end
it 'handles unfound pages' do it "handles unfound pages" do
get '/foo' get "/foo"
_(last_response.status).must_equal 404 _(last_response.status).must_equal(404)
end end
it 'will not process an invalid date' do it "will not process an invalid date" do
get '/2010-31-01' get "/2010-31-01"
_(last_response).must_be :unprocessable? _(last_response).must_be(:unprocessable?)
end end
it 'will not process a date before 2000' do it "will not process a date before 2000" do
get '/1999-01-01' get "/1999-01-01"
_(last_response).must_be :not_found? _(last_response).must_be(:not_found?)
end end
it 'will not process an unavailable base' do it "will not process an unavailable base" do
get '/latest?base=UAH' get "/latest?base=UAH"
_(last_response).must_be :not_found? _(last_response).must_be(:not_found?)
end end
it 'handles malformed queries' do it "handles malformed queries" do
get '/latest?base=USD?callback=?' get "/latest?base=USD?callback=?"
_(last_response).must_be :not_found? _(last_response).must_be(:not_found?)
end end
it 'does not return stale dates' do it "does not return stale dates" do
Day.db.transaction do Day.db.transaction do
get '/latest' get "/latest"
date = json['date'] date = json["date"]
Day.latest.delete Day.latest.delete
get '/latest' get "/latest"
_(json['date']).wont_equal date _(json["date"]).wont_equal(date)
raise Sequel::Rollback raise Sequel::Rollback
end end
end end

View File

@ -1,24 +1,23 @@
# frozen_string_literal: true # frozen_string_literal: true
ENV['APP_ENV'] ||= 'test' ENV["APP_ENV"] ||= "test"
# Keep SimpleCov at top. # Keep SimpleCov at top.
require 'simplecov' require "simplecov"
SimpleCov.start do SimpleCov.start do
add_filter '/spec/' add_filter "/spec/"
end end
require_relative '../config/environment' require_relative "../config/environment"
require 'minitest/autorun' require "minitest/autorun"
require 'minitest/around/spec' require "minitest/around/spec"
require 'minitest/focus' require "minitest/focus"
require 'pry-byebug' require "vcr"
require 'vcr' require "webmock"
require 'webmock'
VCR.configure do |c| VCR.configure do |c|
c.cassette_library_dir = 'spec/vcr_cassettes' c.cassette_library_dir = "spec/vcr_cassettes"
c.hook_into :webmock c.hook_into(:webmock)
end end

View File

@ -1,72 +1,72 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'helper' require_relative "helper"
require 'query' require "query"
describe Query do describe Query do
it 'builds a query hash' do it "builds a query hash" do
_(Query.build(date: '2014-01-01')).must_be_kind_of Hash _(Query.build(date: "2014-01-01")).must_be_kind_of(Hash)
end end
it 'returns given amount' do it "returns given amount" do
query = Query.new(amount: '100') query = Query.new(amount: "100")
_(query.amount).must_equal 100.0 _(query.amount).must_equal(100.0)
end end
it 'defaults amount to nothing' do it "defaults amount to nothing" do
query = Query.new query = Query.new
_(query.amount).must_be_nil _(query.amount).must_be_nil
end end
it 'returns given base' do it "returns given base" do
query = Query.new(base: 'USD') query = Query.new(base: "USD")
_(query.base).must_equal 'USD' _(query.base).must_equal("USD")
end end
it 'upcases given base' do it "upcases given base" do
query = Query.new(base: 'usd') query = Query.new(base: "usd")
_(query.base).must_equal 'USD' _(query.base).must_equal("USD")
end end
it 'defaults base to nothing' do it "defaults base to nothing" do
query = Query.new query = Query.new
_(query.base).must_be_nil _(query.base).must_be_nil
end end
it 'aliases base with from' do it "aliases base with from" do
query = Query.new(from: 'USD') query = Query.new(from: "USD")
_(query.base).must_equal 'USD' _(query.base).must_equal("USD")
end end
it 'returns given symbols' do it "returns given symbols" do
query = Query.new(symbols: 'USD,GBP') query = Query.new(symbols: "USD,GBP")
_(query.symbols).must_equal %w[USD GBP] _(query.symbols).must_equal(["USD", "GBP"])
end end
it 'upcases given symbols' do it "upcases given symbols" do
query = Query.new(symbols: 'usd,gbp') query = Query.new(symbols: "usd,gbp")
_(query.symbols).must_equal %w[USD GBP] _(query.symbols).must_equal(["USD", "GBP"])
end end
it 'aliases symbols with to' do it "aliases symbols with to" do
query = Query.new(to: 'USD') query = Query.new(to: "USD")
_(query.symbols).must_equal ['USD'] _(query.symbols).must_equal(["USD"])
end end
it 'defaults symbols to nothing' do it "defaults symbols to nothing" do
query = Query.new query = Query.new
_(query.symbols).must_be_nil _(query.symbols).must_be_nil
end end
it 'returns given date' do it "returns given date" do
date = '2014-01-01' date = "2014-01-01"
query = Query.new(date:) query = Query.new(date:)
_(query.date).must_equal Date.parse(date) _(query.date).must_equal(Date.parse(date))
end end
it 'returns given date interval' do it "returns given date interval" do
start_date = '2014-01-01' start_date = "2014-01-01"
end_date = '2014-12-31' end_date = "2014-12-31"
query = Query.new(start_date:, end_date:) query = Query.new(start_date:, end_date:)
_(query.date).must_equal((Date.parse(start_date)..Date.parse(end_date))) _(query.date).must_equal((Date.parse(start_date)..Date.parse(end_date)))
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative '../helper' require_relative "../helper"
require 'quote/base' require "quote/base"
module Quote module Quote
describe Base do describe Base do
@ -13,83 +13,83 @@ module Quote
klass.new(date: Date.today) klass.new(date: Date.today)
end end
it 'requires data' do it "requires data" do
_ { quote.perform }.must_raise NotImplementedError _ { quote.perform }.must_raise(NotImplementedError)
end end
it 'does not know how to format result' do it "does not know how to format result" do
_ { quote.formatted }.must_raise NotImplementedError _ { quote.formatted }.must_raise(NotImplementedError)
end end
it 'does not know how to generate a cache key' do it "does not know how to generate a cache key" do
_ { quote.cache_key }.must_raise NotImplementedError _ { quote.cache_key }.must_raise(NotImplementedError)
end end
it 'defaults base to Euro' do it "defaults base to Euro" do
_(quote.base).must_equal 'EUR' _(quote.base).must_equal("EUR")
end end
it 'defaults amount to 1' do it "defaults amount to 1" do
_(quote.amount).must_equal 1 _(quote.amount).must_equal(1)
end end
describe 'when given data' do describe "when given data" do
before do before do
def quote.fetch_data def quote.fetch_data
[] []
end end
end end
it 'performs' do it "performs" do
assert quote.perform assert quote.perform
end end
it 'performs only once' do it "performs only once" do
quote.perform quote.perform
refute quote.perform refute quote.perform
end end
end end
describe 'when rebasing from an unavailable currency' do describe "when rebasing from an unavailable currency" do
let(:date) do let(:date) do
Date.parse('2000-01-01') Date.parse("2000-01-01")
end end
let(:quote) do let(:quote) do
klass.new(date:, base: 'ILS') klass.new(date:, base: "ILS")
end end
before do before do
def quote.fetch_data def quote.fetch_data
[{ date:, iso_code: 'USD', rate: 1 }] [{ date:, iso_code: "USD", rate: 1 }]
end end
end end
it 'finds nothing' do it "finds nothing" do
quote.perform quote.perform
_(quote.not_found?).must_equal true _(quote.not_found?).must_equal(true)
end end
end end
describe 'when rebasing and converting to an unavailable currency' do describe "when rebasing and converting to an unavailable currency" do
let(:date) do let(:date) do
Date.today Date.today
end end
let(:quote) do let(:quote) do
klass.new(date:, base: 'USD', symbols: ['FOO']) klass.new(date:, base: "USD", symbols: ["FOO"])
end end
before do before do
def quote.fetch_data def quote.fetch_data
[{ date:, iso_code: 'USD', rate: 1 }] [{ date:, iso_code: "USD", rate: 1 }]
end end
end end
it 'finds nothing' do it "finds nothing" do
quote.perform quote.perform
_(quote.not_found?).must_equal true _(quote.not_found?).must_equal(true)
end end
end end
end end

View File

@ -1,12 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative '../helper' require_relative "../helper"
require 'quote/end_of_day' require "quote/end_of_day"
module Quote module Quote
describe EndOfDay do describe EndOfDay do
let(:date) do let(:date) do
Date.parse('2010-10-10') Date.parse("2010-10-10")
end end
let(:quote) do let(:quote) do
@ -17,66 +17,66 @@ module Quote
quote.perform quote.perform
end end
it 'returns rates' do it "returns rates" do
_(quote.formatted[:rates]).wont_be :empty? _(quote.formatted[:rates]).wont_be(:empty?)
end end
it 'quotes given date' do it "quotes given date" do
_(Date.parse(quote.formatted[:date])).must_be :<=, date _(Date.parse(quote.formatted[:date])).must_be(:<=, date)
end end
it 'quotes against the Euro' do it "quotes against the Euro" do
_(quote.formatted[:rates].keys).wont_include 'EUR' _(quote.formatted[:rates].keys).wont_include("EUR")
end end
it 'sorts rates' do it "sorts rates" do
rates = quote.formatted[:rates] rates = quote.formatted[:rates]
_(rates.keys).must_equal rates.keys.sort _(rates.keys).must_equal(rates.keys.sort)
end end
it 'has a cache key' do it "has a cache key" do
_(quote.cache_key).wont_be :empty? _(quote.cache_key).wont_be(:empty?)
end end
describe 'given a new base' do describe "given a new base" do
let(:quote) do let(:quote) do
EndOfDay.new(date:, base: 'USD') EndOfDay.new(date:, base: "USD")
end end
it 'quotes against that base' do it "quotes against that base" do
_(quote.formatted[:rates].keys).wont_include 'USD' _(quote.formatted[:rates].keys).wont_include("USD")
end end
it 'sorts rates' do it "sorts rates" do
rates = quote.formatted[:rates] rates = quote.formatted[:rates]
_(rates.keys).must_equal rates.keys.sort _(rates.keys).must_equal(rates.keys.sort)
end end
end end
describe 'given symbols' do describe "given symbols" do
let(:quote) do let(:quote) do
EndOfDay.new(date:, symbols: %w[USD GBP JPY]) EndOfDay.new(date:, symbols: ["USD", "GBP", "JPY"])
end end
it 'quotes only for those symbols' do it "quotes only for those symbols" do
rates = quote.formatted[:rates] rates = quote.formatted[:rates]
_(rates.keys).must_include 'USD' _(rates.keys).must_include("USD")
_(rates.keys).wont_include 'CAD' _(rates.keys).wont_include("CAD")
end end
it 'sorts rates' do it "sorts rates" do
rates = quote.formatted[:rates] rates = quote.formatted[:rates]
_(rates.keys).must_equal rates.keys.sort _(rates.keys).must_equal(rates.keys.sort)
end end
end end
describe 'when given an amount' do describe "when given an amount" do
let(:quote) do let(:quote) do
EndOfDay.new(date:, amount: 100) EndOfDay.new(date:, amount: 100)
end end
it 'calculates quotes for that amount' do it "calculates quotes for that amount" do
_(quote.formatted[:rates]['USD']).must_be :>, 10 _(quote.formatted[:rates]["USD"]).must_be(:>, 10)
end end
end end
end end

View File

@ -1,12 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative '../helper' require_relative "../helper"
require 'quote/interval' require "quote/interval"
module Quote module Quote
describe Interval do describe Interval do
let(:dates) do let(:dates) do
(Date.parse('2010-01-01')..Date.parse('2010-12-31')) (Date.parse("2010-01-01")..Date.parse("2010-12-31"))
end end
let(:quote) do let(:quote) do
@ -17,76 +17,76 @@ module Quote
quote.perform quote.perform
end end
it 'returns rates' do it "returns rates" do
_(quote.formatted[:rates]).wont_be :empty? _(quote.formatted[:rates]).wont_be(:empty?)
end end
it 'quotes given date interval' do it "quotes given date interval" do
_(Date.parse(quote.formatted[:start_date])).must_be :>=, dates.first _(Date.parse(quote.formatted[:start_date])).must_be(:>=, dates.first)
_(Date.parse(quote.formatted[:end_date])).must_be :<=, dates.last _(Date.parse(quote.formatted[:end_date])).must_be(:<=, dates.last)
end end
it 'quotes against the Euro' do it "quotes against the Euro" do
quote.formatted[:rates].each_value do |rates| quote.formatted[:rates].each_value do |rates|
_(rates.keys).wont_include 'EUR' _(rates.keys).wont_include("EUR")
end end
end end
it 'sorts rates' do it "sorts rates" do
quote.formatted[:rates].each_value do |rates| quote.formatted[:rates].each_value do |rates|
_(rates.keys).must_equal rates.keys.sort _(rates.keys).must_equal(rates.keys.sort)
end end
end end
it 'has a cache key' do it "has a cache key" do
_(quote.cache_key).wont_be :empty? _(quote.cache_key).wont_be(:empty?)
end end
describe 'given a new base' do describe "given a new base" do
let(:quote) do let(:quote) do
Interval.new(date: dates, base: 'USD') Interval.new(date: dates, base: "USD")
end end
it 'quotes against that base' do it "quotes against that base" do
quote.formatted[:rates].each_value do |rates| quote.formatted[:rates].each_value do |rates|
_(rates.keys).wont_include 'USD' _(rates.keys).wont_include("USD")
end end
end end
it 'sorts rates' do it "sorts rates" do
quote.formatted[:rates].each_value do |rates| quote.formatted[:rates].each_value do |rates|
_(rates.keys).must_equal rates.keys.sort _(rates.keys).must_equal(rates.keys.sort)
end end
end end
end end
describe 'given symbols' do describe "given symbols" do
let(:quote) do let(:quote) do
Interval.new(date: dates, symbols: %w[USD GBP JPY]) Interval.new(date: dates, symbols: ["USD", "GBP", "JPY"])
end end
it 'quotes only for those symbols' do it "quotes only for those symbols" do
quote.formatted[:rates].each_value do |rates| quote.formatted[:rates].each_value do |rates|
_(rates.keys).must_include 'USD' _(rates.keys).must_include("USD")
_(rates.keys).wont_include 'CAD' _(rates.keys).wont_include("CAD")
end end
end end
it 'sorts rates' do it "sorts rates" do
quote.formatted[:rates].each_value do |rates| quote.formatted[:rates].each_value do |rates|
_(rates.keys).must_equal rates.keys.sort _(rates.keys).must_equal(rates.keys.sort)
end end
end end
end end
describe 'when given an amount' do describe "when given an amount" do
let(:quote) do let(:quote) do
Interval.new(date: dates, amount: 100) Interval.new(date: dates, amount: 100)
end end
it 'calculates quotes for that amount' do it "calculates quotes for that amount" do
quote.formatted[:rates].each_value do |rates| quote.formatted[:rates].each_value do |rates|
_(rates['USD']).must_be :>, 10 _(rates["USD"]).must_be(:>, 10)
end end
end end
end end

View File

@ -1,44 +1,44 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'helper' require_relative "helper"
require 'roundable' require "roundable"
describe Roundable do describe Roundable do
include Roundable include Roundable
it 'rounds values over 5,000 to zero decimal places' do it "rounds values over 5,000 to zero decimal places" do
_(round(5000.123456)).must_equal 5000 _(round(5000.123456)).must_equal(5000)
end end
it 'rounds values over 80 and below 5,000 to two decimal places' do it "rounds values over 80 and below 5,000 to two decimal places" do
_(round(80.123456)).must_equal 80.12 _(round(80.123456)).must_equal(80.12)
_(round(4999.123456)).must_equal 4999.12 _(round(4999.123456)).must_equal(4999.12)
end end
it 'rounds values over 20 and below 80 to three decimal places' do it "rounds values over 20 and below 80 to three decimal places" do
_(round(79.123456)).must_equal 79.123 _(round(79.123456)).must_equal(79.123)
_(round(20.123456)).must_equal 20.123 _(round(20.123456)).must_equal(20.123)
end end
it 'rounds values over 1 and below 20 to four decimal places' do it "rounds values over 1 and below 20 to four decimal places" do
_(round(19.123456)).must_equal 19.1235 _(round(19.123456)).must_equal(19.1235)
_(round(1.123456)).must_equal 1.1235 _(round(1.123456)).must_equal(1.1235)
end end
it 'rounds values below 1 to five decimal places' do it "rounds values below 1 to five decimal places" do
_(round(0.123456)).must_equal 0.12346 _(round(0.123456)).must_equal(0.12346)
end end
it 'rounds values below 0.0001 to six decimal places' do it "rounds values below 0.0001 to six decimal places" do
_(round(0.0000655)).must_equal 0.000066 _(round(0.0000655)).must_equal(0.000066)
end end
it 'conforms to ECB conventions' do it "conforms to ECB conventions" do
skip "We don't conform ¯_(ツ)_/¯" skip "We don't conform ¯_(ツ)_/¯"
require 'day' require "day"
rates = Day.all.sample.rates.to_a rates = Day.all.sample.rates.to_a
rates.shuffle.each_value do |rate| rates.shuffle.each_value do |rate|
_(round(rate)).must_equal rate _(round(rate)).must_equal(rate)
end end
end end
end end

View File

@ -1,119 +1,119 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative '../helper' require_relative "../helper"
require 'rack/test' require "rack/test"
require 'web/server' require "web/server"
describe 'the server' do describe "the server" do
include Rack::Test::Methods include Rack::Test::Methods
let(:app) { Web::Server.freeze } let(:app) { Web::Server.freeze }
let(:json) { Oj.load(last_response.body) } let(:json) { Oj.load(last_response.body) }
let(:headers) { last_response.headers } let(:headers) { last_response.headers }
it 'returns link to docs' do it "returns link to docs" do
get '/' get "/"
_(json['docs']).wont_be_nil _(json["docs"]).wont_be_nil
end end
it 'returns latest quotes' do it "returns latest quotes" do
get '/latest' get "/latest"
_(last_response).must_be :ok? _(last_response).must_be(:ok?)
end end
it 'sets base currency' do it "sets base currency" do
get '/latest' get "/latest"
res = Oj.load(last_response.body) res = Oj.load(last_response.body)
get '/latest?from=USD' get "/latest?from=USD"
_(json).wont_equal res _(json).wont_equal(res)
end end
it 'sets base amount' do it "sets base amount" do
get '/latest?amount=10' get "/latest?amount=10"
_(json['rates']['USD']).must_be :>, 10 _(json["rates"]["USD"]).must_be(:>, 10)
end end
it 'filters symbols' do it "filters symbols" do
get '/latest?to=USD' get "/latest?to=USD"
_(json['rates'].keys).must_equal %w[USD] _(json["rates"].keys).must_equal(["USD"])
end end
it 'returns historical quotes' do it "returns historical quotes" do
get '/2012-11-20' get "/2012-11-20"
_(json['rates']).wont_be :empty? _(json["rates"]).wont_be(:empty?)
_(json['date']).must_equal '2012-11-20' _(json["date"]).must_equal("2012-11-20")
end end
it 'works around holidays' do it "works around holidays" do
get '/2010-01-01' get "/2010-01-01"
_(json['rates']).wont_be :empty? _(json["rates"]).wont_be(:empty?)
end end
it 'returns an ETag' do it "returns an ETag" do
%w[/latest /2012-11-20].each do |path| ["/latest", "/2012-11-20"].each do |path|
get path get path
_(headers['ETag']).wont_be_nil _(headers["ETag"]).wont_be_nil
end end
end end
it 'returns a cache control header' do it "returns a cache control header" do
%w[/latest /2012-11-20].each do |path| ["/latest", "/2012-11-20"].each do |path|
get path get path
_(headers['Cache-Control']).wont_be_nil _(headers["Cache-Control"]).wont_be_nil
end end
end end
it 'allows cross-origin requests' do it "allows cross-origin requests" do
%w[/ /latest /2012-11-20].each do |path| ["/", "/latest", "/2012-11-20"].each do |path|
header 'Origin', '*' header "Origin", "*"
get path get path
assert headers.key?('Access-Control-Allow-Methods') assert headers.key?("Access-Control-Allow-Methods")
end end
end end
it 'responds to preflight requests' do it "responds to preflight requests" do
%w[/ /latest /2012-11-20].each do |path| ["/", "/latest", "/2012-11-20"].each do |path|
header 'Origin', '*' header "Origin", "*"
header 'Access-Control-Request-Method', 'GET' header "Access-Control-Request-Method", "GET"
header 'Access-Control-Request-Headers', 'Content-Type' header "Access-Control-Request-Headers", "Content-Type"
options path options path
assert headers.key?('Access-Control-Allow-Methods') assert headers.key?("Access-Control-Allow-Methods")
end end
end end
it 'converts an amount' do it "converts an amount" do
get '/latest?from=GBP&to=USD&amount=100' get "/latest?from=GBP&to=USD&amount=100"
_(json['rates']['USD']).must_be :>, 100 _(json["rates"]["USD"]).must_be(:>, 100)
end end
it 'returns rates for a given period' do it "returns rates for a given period" do
get '/2010-01-01..2010-12-31' get "/2010-01-01..2010-12-31"
_(json['start_date']).wont_be :empty? _(json["start_date"]).wont_be(:empty?)
_(json['end_date']).wont_be :empty? _(json["end_date"]).wont_be(:empty?)
_(json['rates']).wont_be :empty? _(json["rates"]).wont_be(:empty?)
end end
it 'returns rates when given period does not include end date' do it "returns rates when given period does not include end date" do
get '/2010-01-01..' get "/2010-01-01.."
_(json['start_date']).wont_be :empty? _(json["start_date"]).wont_be(:empty?)
_(json['end_date']).wont_be :empty? _(json["end_date"]).wont_be(:empty?)
_(json['rates']).wont_be :empty? _(json["rates"]).wont_be(:empty?)
end end
it 'returns currencies' do it "returns currencies" do
get '/currencies' get "/currencies"
_(json['USD']).must_equal 'United States Dollar' _(json["USD"]).must_equal("United States Dollar")
end end
it 'handles JSONP' do it "handles JSONP" do
get '/latest?callback=foo' get "/latest?callback=foo"
_(last_response.body).must_be :start_with?, '/**/foo' _(last_response.body).must_be(:start_with?, "/**/foo")
end end
it 'sets charset to utf-8' do it "sets charset to utf-8" do
get '/currencies' get "/currencies"
_(last_response.headers['content-type']).must_be :end_with?, 'charset=utf-8' _(last_response.headers["content-type"]).must_be(:end_with?, "charset=utf-8")
end end
end end