1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-02 20:15:22 +02:00

Data provider simplification, tests, and documentation (#1997)

* Ignore env.test from source control

* Simplification of providers interface

* Synth tests

* Update money to use new find rates method

* Remove unused issues code

* Additional issue feature removals

* Update price data fetching and tests

* Update documentation for providers

* Security test fixes

* Fix self host test

* Update synth usage data access

* Remove AI pr schema changes
This commit is contained in:
Zach Gollwitzer 2025-03-17 11:54:53 -04:00 committed by GitHub
parent dd75cadebc
commit f65b93a352
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
95 changed files with 2014 additions and 1638 deletions

View file

@ -1,19 +0,0 @@
require "test_helper"
class Issue::ExchangeRateProviderMissingsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in users(:family_admin)
@issue = issues(:one)
end
test "should update issue" do
patch issue_exchange_rate_provider_missing_url(@issue), params: {
issue_exchange_rate_provider_missing: {
synth_api_key: "1234"
}
}
assert_enqueued_with job: SyncJob
assert_redirected_to @issue.issuable
end
end

View file

@ -1,18 +0,0 @@
require "test_helper"
class IssuesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in users(:family_admin)
end
test "should get show polymorphically" do
issues.each do |issue|
get issue_url(issue)
assert_response :success
assert_dom "h2", text: issue.title
assert_dom "h3", text: "Issue Description"
assert_dom "h3", text: "How to fix this issue"
end
end
end

View file

@ -1,8 +1,22 @@
require "test_helper"
require "ostruct"
class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
include ProviderTestHelper
setup do
sign_in users(:family_admin)
@provider = mock
Providers.stubs(:synth).returns(@provider)
@usage_response = provider_success_response(
OpenStruct.new(
used: 10,
limit: 100,
utilization: 10,
plan: "free",
)
)
end
test "cannot edit when self hosting is disabled" do
@ -16,6 +30,8 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
end
test "should get edit when self hosting is enabled" do
@provider.expects(:usage).returns(@usage_response)
with_self_hosting do
get settings_hosting_url
assert_response :success

View file

@ -1,5 +0,0 @@
one:
issuable: depository
issuable_type: Account
type: Issue::Unknown
last_observed_at: 2024-08-15 08:54:04

View file

@ -3,20 +3,34 @@ require "test_helper"
module ExchangeRateProviderInterfaceTest
extend ActiveSupport::Testing::Declarative
test "exchange rate provider interface" do
assert_respond_to @subject, :healthy?
assert_respond_to @subject, :fetch_exchange_rate
assert_respond_to @subject, :fetch_exchange_rates
end
test "fetches single exchange rate" do
VCR.use_cassette("#{vcr_key_prefix}/exchange_rate") do
response = @subject.fetch_exchange_rate(
from: "USD",
to: "GBP",
date: Date.parse("01.01.2024")
)
test "exchange rate provider response contract" do
VCR.use_cassette "synth/exchange_rate" do
response = @subject.fetch_exchange_rate from: "USD", to: "MXN", date: Date.iso8601("2024-08-01")
rate = response.data.rate
assert_respond_to response, :rate
assert_respond_to response, :success?
assert_respond_to response, :error
assert_respond_to response, :raw_response
assert_kind_of ExchangeRate, rate
assert_equal "USD", rate.from_currency
assert_equal "GBP", rate.to_currency
end
end
test "fetches paginated exchange_rate historical data" do
VCR.use_cassette("#{vcr_key_prefix}/exchange_rates") do
response = @subject.fetch_exchange_rates(
from: "USD", to: "GBP", start_date: Date.parse("01.01.2024"), end_date: Date.parse("31.07.2024")
)
assert 213, response.data.rates.count # 213 days between 01.01.2024 and 31.07.2024
end
end
private
def vcr_key_prefix
@subject.class.name.demodulize.underscore
end
end

View file

@ -1,26 +0,0 @@
require "test_helper"
module SecurityPriceProviderInterfaceTest
extend ActiveSupport::Testing::Declarative
test "security price provider interface" do
assert_respond_to @subject, :healthy?
assert_respond_to @subject, :fetch_security_prices
end
test "security price provider response contract" do
VCR.use_cassette "synth/security_prices" do
response = @subject.fetch_security_prices(
ticker: "AAPL",
mic_code: "XNAS",
start_date: Date.iso8601("2024-01-01"),
end_date: Date.iso8601("2024-08-01")
)
assert_respond_to response, :prices
assert_respond_to response, :success?
assert_respond_to response, :error
assert_respond_to response, :raw_response
end
end
end

View file

@ -0,0 +1,62 @@
require "test_helper"
module SecurityProviderInterfaceTest
extend ActiveSupport::Testing::Declarative
test "fetches security price" do
aapl = securities(:aapl)
VCR.use_cassette("#{vcr_key_prefix}/security_price") do
response = @subject.fetch_security_price(aapl, date: Date.iso8601("2024-08-01"))
assert response.success?
assert response.data.price.present?
end
end
test "fetches paginated securities prices" do
aapl = securities(:aapl)
VCR.use_cassette("#{vcr_key_prefix}/security_prices") do
response = @subject.fetch_security_prices(
aapl,
start_date: Date.iso8601("2024-01-01"),
end_date: Date.iso8601("2024-08-01")
)
assert response.success?
assert 213, response.data.prices.count
end
end
test "searches securities" do
VCR.use_cassette("#{vcr_key_prefix}/security_search") do
response = @subject.search_securities("AAPL", country_code: "US")
securities = response.data.securities
assert securities.any?
security = securities.first
assert_kind_of Security, security
assert_equal "AAPL", security.ticker
end
end
test "fetches security info" do
aapl = securities(:aapl)
VCR.use_cassette("#{vcr_key_prefix}/security_info") do
response = @subject.fetch_security_info(aapl)
info = response.data
assert_equal "AAPL", info.ticker
assert_equal "Apple Inc.", info.name
assert info.logo_url.present?
assert_equal "common stock", info.kind
assert info.description.present?
end
end
private
def vcr_key_prefix
@subject.class.name.demodulize.underscore
end
end

View file

@ -91,13 +91,13 @@ class MoneyTest < ActiveSupport::TestCase
end
test "converts currency when rate available" do
ExchangeRate.expects(:find_rate).returns(OpenStruct.new(rate: 1.2))
ExchangeRate.expects(:find_or_fetch_rate).returns(OpenStruct.new(rate: 1.2))
assert_equal Money.new(1000).exchange_to(:eur), Money.new(1000 * 1.2, :eur)
end
test "raises when no conversion rate available and no fallback rate provided" do
ExchangeRate.expects(:find_rate).returns(nil)
ExchangeRate.expects(:find_or_fetch_rate).returns(nil)
assert_raises Money::ConversionError do
Money.new(1000).exchange_to(:jpy)
@ -105,7 +105,7 @@ class MoneyTest < ActiveSupport::TestCase
end
test "converts currency with a fallback rate" do
ExchangeRate.expects(:find_rate).returns(nil).twice
ExchangeRate.expects(:find_or_fetch_rate).returns(nil).twice
assert_equal 0, Money.new(1000).exchange_to(:jpy, fallback_rate: 0)
assert_equal Money.new(1000, :jpy), Money.new(1000, :usd).exchange_to(:jpy, fallback_rate: 1)

View file

@ -2,7 +2,7 @@ require "test_helper"
require "ostruct"
class Account::ConvertibleTest < ActiveSupport::TestCase
include Account::EntriesTestHelper
include Account::EntriesTestHelper, ProviderTestHelper
setup do
@family = families(:empty)
@ -16,33 +16,28 @@ class Account::ConvertibleTest < ActiveSupport::TestCase
end
test "syncs required exchange rates for an account" do
create_valuation(account: @account, date: 5.days.ago.to_date, amount: 9500, currency: "EUR")
create_valuation(account: @account, date: 1.day.ago.to_date, amount: 9500, currency: "EUR")
# Since we had a valuation 5 days ago, this account starts 6 days ago and needs daily exchange rates looking forward
assert_equal 6.days.ago.to_date, @account.start_date
# Since we had a valuation 1 day ago, this account starts 2 days ago and needs daily exchange rates looking forward
assert_equal 2.days.ago.to_date, @account.start_date
ExchangeRate.delete_all
provider_response = provider_success_response(
ExchangeRate::Provideable::FetchRatesData.new(
rates: [
ExchangeRate.new(from_currency: "EUR", to_currency: "USD", date: 2.days.ago.to_date, rate: 1.1),
ExchangeRate.new(from_currency: "EUR", to_currency: "USD", date: 1.day.ago.to_date, rate: 1.2),
ExchangeRate.new(from_currency: "EUR", to_currency: "USD", date: Date.current, rate: 1.3)
]
)
)
@provider.expects(:fetch_exchange_rates)
.with(
from: "EUR",
to: "USD",
start_date: 6.days.ago.to_date,
end_date: Date.current
).returns(
OpenStruct.new(
success?: true,
rates: [
OpenStruct.new(date: 6.days.ago.to_date, rate: 1.1),
OpenStruct.new(date: 5.days.ago.to_date, rate: 1.2),
OpenStruct.new(date: 4.days.ago.to_date, rate: 1.3),
OpenStruct.new(date: 3.days.ago.to_date, rate: 1.4),
OpenStruct.new(date: 2.days.ago.to_date, rate: 1.5),
OpenStruct.new(date: 1.day.ago.to_date, rate: 1.6),
OpenStruct.new(date: Date.current, rate: 1.7)
]
)
)
.with(from: "EUR", to: "USD", start_date: 2.days.ago.to_date, end_date: Date.current)
.returns(provider_response)
assert_difference "ExchangeRate.count", 7 do
assert_difference "ExchangeRate.count", 3 do
@account.sync_required_exchange_rates
end
end

View file

@ -1,63 +1,93 @@
require "test_helper"
class Account::Holding::PortfolioCacheTest < ActiveSupport::TestCase
include Account::EntriesTestHelper
include Account::EntriesTestHelper, ProviderTestHelper
setup do
# Prices, highest to lowest priority
@db_price = 210
@provider_price = 220
@trade_price = 200
@holding_price = 250
@provider = mock
Security.stubs(:provider).returns(@provider)
@account = families(:empty).accounts.create!(name: "Test Brokerage", balance: 10000, currency: "USD", accountable: Investment.new)
@test_security = Security.create!(name: "Test Security", ticker: "TEST")
@account = families(:empty).accounts.create!(
name: "Test Brokerage",
balance: 10000,
currency: "USD",
accountable: Investment.new
)
@trade = create_trade(@test_security, account: @account, qty: 1, date: Date.current, price: @trade_price)
@holding = Account::Holding.create!(security: @test_security, account: @account, date: Date.current, qty: 1, price: @holding_price, amount: @holding_price, currency: "USD")
Security::Price.create!(security: @test_security, date: Date.current, price: @db_price)
@security = Security.create!(name: "Test Security", ticker: "TEST", exchange_operating_mic: "TEST")
@trade = create_trade(@security, account: @account, qty: 1, date: 2.days.ago.to_date, price: 210.23).account_trade
end
test "gets price from DB if available" do
cache = Account::Holding::PortfolioCache.new(@account)
db_price = 210
assert_equal @db_price, cache.get_price(@test_security.id, Date.current).price
Security::Price.create!(
security: @security,
date: Date.current,
price: db_price
)
expect_provider_prices([], start_date: @account.start_date)
cache = Account::Holding::PortfolioCache.new(@account)
assert_equal db_price, cache.get_price(@security.id, Date.current).price
end
test "if no price in DB, try fetching from provider" do
Security::Price.destroy_all
Security::Price.expects(:find_prices)
.with(security: @test_security, start_date: @account.start_date, end_date: Date.current)
.returns([
Security::Price.new(security: @test_security, date: Date.current, price: @provider_price, currency: "USD")
])
Security::Price.delete_all
provider_price = Security::Price.new(
security: @security,
date: Date.current,
price: 220,
currency: "USD"
)
expect_provider_prices([ provider_price ], start_date: @account.start_date)
cache = Account::Holding::PortfolioCache.new(@account)
assert_equal @provider_price, cache.get_price(@test_security.id, Date.current).price
assert_equal provider_price.price, cache.get_price(@security.id, Date.current).price
end
test "if no price from db or provider, try getting the price from trades" do
Security::Price.destroy_all # No DB prices
Security::Price.expects(:find_prices)
.with(security: @test_security, start_date: @account.start_date, end_date: Date.current)
.returns([]) # No provider prices
Security::Price.destroy_all
expect_provider_prices([], start_date: @account.start_date)
cache = Account::Holding::PortfolioCache.new(@account)
assert_equal @trade_price, cache.get_price(@test_security.id, Date.current).price
assert_equal @trade.price, cache.get_price(@security.id, @trade.entry.date).price
end
test "if no price from db, provider, or trades, search holdings" do
Security::Price.destroy_all # No DB prices
Security::Price.expects(:find_prices)
.with(security: @test_security, start_date: @account.start_date, end_date: Date.current)
.returns([]) # No provider prices
Security::Price.delete_all
Account::Entry.delete_all
@account.entries.destroy_all # No prices from trades
holding = Account::Holding.create!(
security: @security,
account: @account,
date: Date.current,
qty: 1,
price: 250,
amount: 250 * 1,
currency: "USD"
)
expect_provider_prices([], start_date: @account.start_date)
cache = Account::Holding::PortfolioCache.new(@account, use_holdings: true)
assert_equal @holding_price, cache.get_price(@test_security.id, Date.current).price
assert_equal holding.price, cache.get_price(@security.id, holding.date).price
end
private
def expect_provider_prices(prices, start_date:, end_date: Date.current)
@provider.expects(:fetch_security_prices)
.with(@security, start_date: start_date, end_date: end_date)
.returns(
provider_success_response(
Security::Provideable::PricesData.new(
prices: prices
)
)
)
end
end

View file

@ -2,116 +2,99 @@ require "test_helper"
require "ostruct"
class ExchangeRateTest < ActiveSupport::TestCase
include ProviderTestHelper
setup do
@provider = mock
ExchangeRate.stubs(:provider).returns(@provider)
end
test "exchange rate provider nil if no api key configured" do
ExchangeRate.unstub(:provider)
test "finds rate in DB" do
existing_rate = exchange_rates(:one)
Setting.stubs(:synth_api_key).returns(nil)
with_env_overrides SYNTH_API_KEY: nil do
assert_not ExchangeRate.provider
end
end
test "finds single rate in DB" do
@provider.expects(:fetch_exchange_rate).never
rate = exchange_rates(:one)
assert_equal rate, ExchangeRate.find_rate(from: rate.from_currency, to: rate.to_currency, date: rate.date)
assert_equal existing_rate, ExchangeRate.find_or_fetch_rate(
from: existing_rate.from_currency,
to: existing_rate.to_currency,
date: existing_rate.date
)
end
test "finds single rate from provider and caches to DB" do
expected_rate = 1.21
@provider.expects(:fetch_exchange_rate).once.returns(OpenStruct.new(success?: true, rate: expected_rate))
test "fetches rate from provider without cache" do
ExchangeRate.delete_all
fetched_rate = ExchangeRate.find_rate(from: "USD", to: "EUR", date: Date.current, cache: true)
refetched_rate = ExchangeRate.find_rate(from: "USD", to: "EUR", date: Date.current, cache: true)
provider_response = provider_success_response(
ExchangeRate::Provideable::FetchRateData.new(
rate: ExchangeRate.new(
from_currency: "USD",
to_currency: "EUR",
date: Date.current,
rate: 1.2
)
)
)
assert_equal expected_rate, fetched_rate.rate
assert_equal expected_rate, refetched_rate.rate
end
@provider.expects(:fetch_exchange_rate).returns(provider_response)
test "nil if rate is not found in DB and provider throws an error" do
@provider.expects(:fetch_exchange_rate).with(from: "USD", to: "EUR", date: Date.current).once.returns(OpenStruct.new(success?: false))
assert_not ExchangeRate.find_rate(from: "USD", to: "EUR", date: Date.current)
end
test "nil if rate is not found in DB and provider is disabled" do
ExchangeRate.unstub(:provider)
Setting.stubs(:synth_api_key).returns(nil)
with_env_overrides SYNTH_API_KEY: nil do
assert_not ExchangeRate.find_rate(from: "USD", to: "EUR", date: Date.current)
assert_no_difference "ExchangeRate.count" do
assert_equal 1.2, ExchangeRate.find_or_fetch_rate(from: "USD", to: "EUR", date: Date.current, cache: false).rate
end
end
test "finds multiple rates in DB" do
@provider.expects(:fetch_exchange_rate).never
test "fetches rate from provider with cache" do
ExchangeRate.delete_all
rate1 = exchange_rates(:one) # EUR -> GBP, today
rate2 = exchange_rates(:two) # EUR -> GBP, yesterday
fetched_rates = ExchangeRate.find_rates(from: rate1.from_currency, to: rate1.to_currency, start_date: 1.day.ago.to_date).sort_by(&:date)
assert_equal rate1, fetched_rates[1]
assert_equal rate2, fetched_rates[0]
end
test "finds multiple rates from provider and caches to DB" do
@provider.expects(:fetch_exchange_rates).with(from: "EUR", to: "USD", start_date: 1.day.ago.to_date, end_date: Date.current)
.returns(
OpenStruct.new(
rates: [
OpenStruct.new(date: 1.day.ago.to_date, rate: 1.1),
OpenStruct.new(date: Date.current, rate: 1.2)
],
success?: true
provider_response = provider_success_response(
ExchangeRate::Provideable::FetchRateData.new(
rate: ExchangeRate.new(
from_currency: "USD",
to_currency: "EUR",
date: Date.current,
rate: 1.2
)
).once
)
)
fetched_rates = ExchangeRate.find_rates(from: "EUR", to: "USD", start_date: 1.day.ago.to_date, cache: true)
refetched_rates = ExchangeRate.find_rates(from: "EUR", to: "USD", start_date: 1.day.ago.to_date)
@provider.expects(:fetch_exchange_rate).returns(provider_response)
assert_equal [ 1.1, 1.2 ], fetched_rates.sort_by(&:date).map(&:rate)
assert_equal [ 1.1, 1.2 ], refetched_rates.sort_by(&:date).map(&:rate)
end
test "finds missing db rates from provider and appends to results" do
@provider.expects(:fetch_exchange_rates).with(from: "EUR", to: "GBP", start_date: 2.days.ago.to_date, end_date: 2.days.ago.to_date)
.returns(
OpenStruct.new(
rates: [
OpenStruct.new(date: 2.day.ago.to_date, rate: 1.1)
],
success?: true
)
).once
rate1 = exchange_rates(:one) # EUR -> GBP, today
rate2 = exchange_rates(:two) # EUR -> GBP, yesterday
fetched_rates = ExchangeRate.find_rates(from: "EUR", to: "GBP", start_date: 2.days.ago.to_date, cache: true)
refetched_rates = ExchangeRate.find_rates(from: "EUR", to: "GBP", start_date: 2.days.ago.to_date)
assert_equal [ 1.1, rate2.rate, rate1.rate ], fetched_rates.sort_by(&:date).map(&:rate)
assert_equal [ 1.1, rate2.rate, rate1.rate ], refetched_rates.sort_by(&:date).map(&:rate)
end
test "returns empty array if no rates found in DB or provider" do
ExchangeRate.unstub(:provider)
Setting.stubs(:synth_api_key).returns(nil)
with_env_overrides SYNTH_API_KEY: nil do
assert_equal [], ExchangeRate.find_rates(from: "USD", to: "JPY", start_date: 10.days.ago.to_date)
assert_difference "ExchangeRate.count", 1 do
assert_equal 1.2, ExchangeRate.find_or_fetch_rate(from: "USD", to: "EUR", date: Date.current, cache: true).rate
end
end
test "returns nil on provider error" do
provider_response = provider_error_response(Provider::ProviderError.new("Test error"))
@provider.expects(:fetch_exchange_rate).returns(provider_response)
assert_nil ExchangeRate.find_or_fetch_rate(from: "USD", to: "EUR", date: Date.current, cache: true)
end
test "upserts rates for currency pair and date range" do
ExchangeRate.delete_all
ExchangeRate.create!(date: 1.day.ago.to_date, from_currency: "USD", to_currency: "EUR", rate: 0.9)
provider_response = provider_success_response(
ExchangeRate::Provideable::FetchRatesData.new(
rates: [
ExchangeRate.new(from_currency: "USD", to_currency: "EUR", date: Date.current, rate: 1.3),
ExchangeRate.new(from_currency: "USD", to_currency: "EUR", date: 1.day.ago.to_date, rate: 1.4),
ExchangeRate.new(from_currency: "USD", to_currency: "EUR", date: 2.days.ago.to_date, rate: 1.5)
]
)
)
@provider.expects(:fetch_exchange_rates)
.with(from: "USD", to: "EUR", start_date: 2.days.ago.to_date, end_date: Date.current)
.returns(provider_response)
ExchangeRate.sync_provider_rates(from: "USD", to: "EUR", start_date: 2.days.ago.to_date)
assert_equal 1.3, ExchangeRate.find_by(from_currency: "USD", to_currency: "EUR", date: Date.current).rate
assert_equal 1.4, ExchangeRate.find_by(from_currency: "USD", to_currency: "EUR", date: 1.day.ago.to_date).rate
assert_equal 1.5, ExchangeRate.find_by(from_currency: "USD", to_currency: "EUR", date: 2.days.ago.to_date).rate
end
end

View file

@ -2,55 +2,42 @@ require "test_helper"
require "ostruct"
class Provider::SynthTest < ActiveSupport::TestCase
include ExchangeRateProviderInterfaceTest, SecurityPriceProviderInterfaceTest
include ExchangeRateProviderInterfaceTest, SecurityProviderInterfaceTest
setup do
@subject = @synth = Provider::Synth.new(ENV["SYNTH_API_KEY"])
end
test "fetches paginated securities prices" do
VCR.use_cassette("synth/security_prices") do
response = @synth.fetch_security_prices(
ticker: "AAPL",
mic_code: "XNAS",
start_date: Date.iso8601("2024-01-01"),
end_date: Date.iso8601("2024-08-01")
)
assert 213, response.size
test "health check" do
VCR.use_cassette("synth/health") do
assert @synth.healthy?
end
end
test "fetches paginated exchange_rate historical data" do
VCR.use_cassette("synth/exchange_rate_historical") do
response = @synth.fetch_exchange_rates(
from: "USD", to: "GBP", start_date: Date.parse("01.01.2024"), end_date: Date.parse("31.07.2024")
)
assert 213, response.rates.size # 213 days between 01.01.2024 and 31.07.2024
assert_equal [ :date, :rate ], response.rates.first.keys
test "usage info" do
VCR.use_cassette("synth/usage") do
usage = @synth.usage.data
assert usage.used.present?
assert usage.limit.present?
assert usage.utilization.present?
assert usage.plan.present?
end
end
test "retries then provides failed response" do
@client = mock
Faraday.stubs(:new).returns(@client)
test "enriches transaction" do
VCR.use_cassette("synth/transaction_enrich") do
response = @synth.enrich_transaction(
"UBER EATS",
amount: 25.50,
date: Date.iso8601("2025-03-16"),
city: "San Francisco",
state: "CA",
country: "US"
)
@client.expects(:get).returns(OpenStruct.new(success?: false)).times(3)
response = @synth.fetch_exchange_rate from: "USD", to: "MXN", date: Date.iso8601("2024-08-01")
assert_match "Failed to fetch data from Provider::Synth", response.error.message
end
test "retrying, then raising on network error" do
@client = mock
Faraday.stubs(:new).returns(@client)
@client.expects(:get).raises(Faraday::TimeoutError).times(3)
assert_raises Faraday::TimeoutError do
@synth.fetch_exchange_rate from: "USD", to: "MXN", date: Date.iso8601("2024-08-01")
data = response.data
assert data.name.present?
assert data.category.present?
end
end
end

View file

@ -0,0 +1,61 @@
require "test_helper"
require "ostruct"
class TestProvider < Provider
def fetch_data
provider_response(retries: 3) do
client.get("/test")
end
end
private
def client
@client ||= Faraday.new
end
def retryable_errors
[ Faraday::TimeoutError ]
end
end
class ProviderTest < ActiveSupport::TestCase
setup do
@provider = TestProvider.new
end
test "retries then provides failed response" do
client = mock
Faraday.stubs(:new).returns(client)
client.expects(:get)
.with("/test")
.raises(Faraday::TimeoutError)
.times(3)
response = @provider.fetch_data
assert_not response.success?
assert_match "timeout", response.error.message
end
test "fail, retry, succeed" do
client = mock
Faraday.stubs(:new).returns(client)
sequence = sequence("retry_sequence")
client.expects(:get)
.with("/test")
.raises(Faraday::TimeoutError)
.in_sequence(sequence)
client.expects(:get)
.with("/test")
.returns(Provider::ProviderResponse.new(success?: true, data: "success", error: nil))
.in_sequence(sequence)
response = @provider.fetch_data
assert response.success?
end
end

View file

@ -0,0 +1,27 @@
require "test_helper"
class ProvidersTest < ActiveSupport::TestCase
test "synth configured with ENV" do
Setting.stubs(:synth_api_key).returns(nil)
with_env_overrides SYNTH_API_KEY: "123" do
assert_instance_of Provider::Synth, Providers.synth
end
end
test "synth configured with Setting" do
Setting.stubs(:synth_api_key).returns("123")
with_env_overrides SYNTH_API_KEY: nil do
assert_instance_of Provider::Synth, Providers.synth
end
end
test "synth not configured" do
Setting.stubs(:synth_api_key).returns(nil)
with_env_overrides SYNTH_API_KEY: nil do
assert_nil Providers.synth
end
end
end

View file

@ -2,120 +2,82 @@ require "test_helper"
require "ostruct"
class Security::PriceTest < ActiveSupport::TestCase
include ProviderTestHelper
setup do
@provider = mock
Security.stubs(:provider).returns(@provider)
Security::Price.stubs(:provider).returns(@provider)
end
test "security price provider nil if no api key provided" do
Security::Price.unstub(:provider)
Setting.stubs(:synth_api_key).returns(nil)
with_env_overrides SYNTH_API_KEY: nil do
assert_not Security::Price.provider
end
@security = securities(:aapl)
end
test "finds single security price in DB" do
@provider.expects(:fetch_security_prices).never
security = securities(:aapl)
@provider.expects(:fetch_security_price).never
price = security_prices(:one)
assert_equal price, Security::Price.find_price(security: security, date: price.date)
assert_equal price, @security.find_or_fetch_price(date: price.date)
end
test "caches prices to DB" do
expected_price = 314.34
security = securities(:aapl)
tomorrow = Date.current + 1.day
test "caches prices from provider to DB" do
price_date = 10.days.ago.to_date
@provider.expects(:fetch_security_prices)
.with(ticker: security.ticker, mic_code: security.exchange_operating_mic, start_date: tomorrow, end_date: tomorrow)
.once
.returns(
OpenStruct.new(
success?: true,
prices: [ { date: tomorrow, price: expected_price, currency: "USD" } ]
)
)
expected_price = Security::Price.new(
security: @security,
date: price_date,
price: 314.34,
currency: "USD"
)
fetched_rate = Security::Price.find_price(security: security, date: tomorrow, cache: true)
refetched_rate = Security::Price.find_price(security: security, date: tomorrow, cache: true)
expect_provider_price(security: @security, price: expected_price, date: price_date)
assert_equal expected_price, fetched_rate.price
assert_equal expected_price, refetched_rate.price
assert_difference "Security::Price.count", 1 do
fetched_price = @security.find_or_fetch_price(date: price_date, cache: true)
assert_equal expected_price.price, fetched_price.price
end
end
test "returns nil if no price found in DB or from provider" do
security = securities(:aapl)
Security::Price.delete_all # Clear any existing prices
@provider.expects(:fetch_security_prices)
.with(ticker: security.ticker, mic_code: security.exchange_operating_mic, start_date: Date.current, end_date: Date.current)
.once
.returns(OpenStruct.new(success?: false))
provider_response = provider_error_response(Provider::ProviderError.new("Test error"))
assert_not Security::Price.find_price(security: security, date: Date.current)
@provider.expects(:fetch_security_price)
.with(security, date: Date.current)
.returns(provider_response)
assert_not @security.find_or_fetch_price(date: Date.current)
end
test "returns nil if price not found in DB and provider disabled" do
Security::Price.unstub(:provider)
test "upserts historical prices from provider" do
Security::Price.delete_all
Setting.stubs(:synth_api_key).returns(nil)
# Will be overwritten by upsert
Security::Price.create!(security: @security, date: 1.day.ago.to_date, price: 190, currency: "USD")
security = Security.new(ticker: "NVDA")
expect_provider_prices(security: @security, start_date: 2.days.ago.to_date, end_date: Date.current, prices: [
Security::Price.new(security: @security, date: Date.current, price: 215, currency: "USD"),
Security::Price.new(security: @security, date: 1.day.ago.to_date, price: 214, currency: "USD"),
Security::Price.new(security: @security, date: 2.days.ago.to_date, price: 213, currency: "USD")
])
with_env_overrides SYNTH_API_KEY: nil do
assert_not Security::Price.find_price(security: security, date: Date.current)
@security.sync_provider_prices(start_date: 2.days.ago.to_date)
assert_equal 215, @security.prices.find_by(date: Date.current).price
assert_equal 214, @security.prices.find_by(date: 1.day.ago.to_date).price
assert_equal 213, @security.prices.find_by(date: 2.days.ago.to_date).price
end
private
def expect_provider_price(security:, price:, date:)
@provider.expects(:fetch_security_price)
.with(security, date: date)
.returns(provider_success_response(Security::Provideable::PriceData.new(price: price)))
end
end
test "fetches multiple dates at once" do
@provider.expects(:fetch_security_prices).never
security = securities(:aapl)
price1 = security_prices(:one) # AAPL today
price2 = security_prices(:two) # AAPL yesterday
fetched_prices = Security::Price.find_prices(security: security, start_date: 1.day.ago.to_date, end_date: Date.current).sort_by(&:date)
assert_equal price1, fetched_prices[1]
assert_equal price2, fetched_prices[0]
end
test "caches multiple prices to DB" do
missing_price = 213.21
security = securities(:aapl)
@provider.expects(:fetch_security_prices)
.with(ticker: security.ticker,
mic_code: security.exchange_operating_mic,
start_date: 2.days.ago.to_date,
end_date: 2.days.ago.to_date)
.returns(OpenStruct.new(success?: true, prices: [ { date: 2.days.ago.to_date, price: missing_price, currency: "USD" } ]))
.once
price1 = security_prices(:one) # AAPL today
price2 = security_prices(:two) # AAPL yesterday
fetched_prices = Security::Price.find_prices(security: security, start_date: 2.days.ago.to_date, end_date: Date.current, cache: true)
refetched_prices = Security::Price.find_prices(security: security, start_date: 2.days.ago.to_date, end_date: Date.current, cache: true)
assert_equal [ missing_price, price2.price, price1.price ], fetched_prices.sort_by(&:date).map(&:price)
assert_equal [ missing_price, price2.price, price1.price ], refetched_prices.sort_by(&:date).map(&:price)
assert Security::Price.exists?(security: security, date: 2.days.ago.to_date, price: missing_price)
end
test "returns empty array if no prices found in DB or from provider" do
Security::Price.unstub(:provider)
Setting.stubs(:synth_api_key).returns(nil)
with_env_overrides SYNTH_API_KEY: nil do
assert_equal [], Security::Price.find_prices(security: Security.new(ticker: "NVDA"), start_date: 10.days.ago.to_date, end_date: Date.current)
def expect_provider_prices(security:, prices:, start_date:, end_date:)
@provider.expects(:fetch_security_prices)
.with(security, start_date: start_date, end_date: end_date)
.returns(provider_success_response(Security::Provideable::PricesData.new(prices: prices)))
end
end
end

View file

@ -6,6 +6,8 @@ class TradeImportTest < ActiveSupport::TestCase
setup do
@subject = @import = imports(:trade)
@provider = mock
Security.stubs(:provider).returns(@provider)
end
test "imports trades and accounts" do
@ -14,7 +16,7 @@ class TradeImportTest < ActiveSupport::TestCase
# We should only hit the provider for GOOGL since AAPL already exists
Security.expects(:search_provider).with(
query: "GOOGL",
"GOOGL",
exchange_operating_mic: "XNAS"
).returns([
Security.new(

View file

@ -0,0 +1,17 @@
module ProviderTestHelper
def provider_success_response(data)
Provider::ProviderResponse.new(
success?: true,
data: data,
error: nil
)
end
def provider_error_response(error)
Provider::ProviderResponse.new(
success?: false,
data: nil,
error: error
)
end
end

View file

@ -5,6 +5,9 @@ class ImportsTest < ApplicationSystemTestCase
setup do
sign_in @user = users(:family_admin)
# Trade securities will be imported as "offline" tickers
Security.stubs(:provider).returns(nil)
end
test "transaction import" do
@ -52,8 +55,6 @@ class ImportsTest < ApplicationSystemTestCase
end
test "trade import" do
Security.stubs(:search_provider).returns([])
visit new_import_path
click_on "Import investments"

View file

@ -33,6 +33,7 @@ class SettingsTest < ApplicationSystemTestCase
test "can update self hosting settings" do
Rails.application.config.app_mode.stubs(:self_hosted?).returns(true)
Providers.stubs(:synth).returns(nil)
open_settings_from_sidebar
assert_selector "li", text: "Self hosting"
click_link "Self hosting"

View file

@ -10,16 +10,8 @@ class TradesTest < ApplicationSystemTestCase
visit_account_portfolio
Security.stubs(:search_provider).returns([
Security.new(
ticker: "AAPL",
name: "Apple Inc.",
logo_url: "https://logo.synthfinance.com/ticker/AAPL",
exchange_acronym: "NASDAQ",
exchange_mic: "XNAS",
country_code: "US"
)
])
# Disable provider to focus on form testing
Security.stubs(:provider).returns(nil)
end
test "can create buy transaction" do
@ -28,7 +20,6 @@ class TradesTest < ApplicationSystemTestCase
open_new_trade_modal
fill_in "Ticker symbol", with: "AAPL"
select_combobox_option("Apple")
fill_in "Date", with: Date.current
fill_in "Quantity", with: shares_qty
fill_in "account_entry[price]", with: 214.23
@ -50,7 +41,6 @@ class TradesTest < ApplicationSystemTestCase
select "Sell", from: "Type"
fill_in "Ticker symbol", with: aapl.ticker
select_combobox_option(aapl.security.name)
fill_in "Date", with: Date.current
fill_in "Quantity", with: aapl.qty
fill_in "account_entry[price]", with: 215.33
@ -81,10 +71,4 @@ class TradesTest < ApplicationSystemTestCase
def visit_account_portfolio
visit account_path(@account, tab: "holdings")
end
def select_combobox_option(text)
within "#account_entry_ticker-hw-listbox" do
find("li", text: text).click
end
end
end

View file

@ -2,15 +2,19 @@
http_interactions:
- request:
method: get
uri: https://api.synthfinance.com/rates/historical?date=2024-08-01&from=USD&to=MXN
uri: https://api.synthfinance.com/rates/historical?date=2024-01-01&from=USD&to=GBP
body:
encoding: US-ASCII
string: ''
headers:
User-Agent:
- Faraday v2.10.0
Authorization:
- Bearer <SYNTH_API_KEY>
X-Source:
- maybe_app
X-Source-Type:
- managed
User-Agent:
- Faraday v2.12.2
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
@ -21,29 +25,25 @@ http_interactions:
message: OK
headers:
Date:
- Thu, 01 Aug 2024 17:20:28 GMT
- Sat, 15 Mar 2025 22:18:46 GMT
Content-Type:
- application/json; charset=utf-8
Transfer-Encoding:
- chunked
- chunked
Connection:
- keep-alive
Cf-Ray:
- 8ac77fbcc9d013ae-CMH
Cf-Cache-Status:
- DYNAMIC
Cache-Control:
- max-age=0, private, must-revalidate
Etag:
- W/"668c8ac287a5ff6d6a705c35c69823b1"
- W/"b0b21c870fe53492404cc5ac258fa465"
Referrer-Policy:
- strict-origin-when-cross-origin
Rndr-Id:
- 44367fcb-e5b4-457d
Strict-Transport-Security:
- max-age=63072000; includeSubDomains
Vary:
- Accept-Encoding
Referrer-Policy:
- strict-origin-when-cross-origin
Rndr-Id:
- ff56c2fe-6252-4b2c
X-Content-Type-Options:
- nosniff
X-Frame-Options:
@ -53,17 +53,29 @@ http_interactions:
X-Render-Origin-Server:
- Render
X-Request-Id:
- 61992b01-969b-4af5-8119-9b17e385da07
- 8ce9dc85-afbd-437c-b18d-ec788b712334
X-Runtime:
- '0.369358'
- '0.031963'
X-Xss-Protection:
- '0'
Cf-Cache-Status:
- DYNAMIC
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=SwRPS1vBsrKtk%2Ftb7Ix8j%2FCWYw9tZgbJxR1FCmotWn%2FIZAE3Ri%2FUwHtvkOSqBq6HN5pLVetfem5hp%2BkqWmD5GRCVho0mp3VgRr3J1tBMwrVK2p50tfpmb3X22Jj%2BOfapq1C22PnN"}],"group":"cf-nel","max_age":604800}'
Nel:
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
Speculation-Rules:
- '"/cdn-cgi/speculation"'
Server:
- cloudflare
Cf-Ray:
- 920f6378fe582237-ORD
Alt-Svc:
- h3=":443"; ma=86400
Server-Timing:
- cfL4;desc="?proto=TCP&rtt=26670&min_rtt=26569&rtt_var=10167&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2829&recv_bytes=922&delivery_rate=105759&cwnd=181&unsent_bytes=0&cid=f0a872e0b2909c59&ts=188&x=0"
body:
encoding: ASCII-8BIT
string: '{"data":{"date":"2024-08-01","source":"USD","rates":{"MXN":18.645877}},"meta":{"total_records":1,"credits_used":1,"credits_remaining":248999}}'
recorded_at: Thu, 01 Aug 2024 17:20:28 GMT
recorded_with: VCR 6.2.0
string: '{"data":{"date":"2024-01-01","source":"USD","rates":{"GBP":0.785476}},"meta":{"total_records":1,"credits_used":1,"credits_remaining":249830,"date":"2024-01-01"}}'
recorded_at: Sat, 15 Mar 2025 22:18:46 GMT
recorded_with: VCR 6.3.1

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,82 @@
---
http_interactions:
- request:
method: get
uri: https://api.synthfinance.com/user
body:
encoding: US-ASCII
string: ''
headers:
Authorization:
- Bearer <SYNTH_API_KEY>
X-Source:
- maybe_app
X-Source-Type:
- managed
User-Agent:
- Faraday v2.12.2
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Date:
- Sat, 15 Mar 2025 22:18:47 GMT
Content-Type:
- application/json; charset=utf-8
Transfer-Encoding:
- chunked
Connection:
- keep-alive
Cache-Control:
- max-age=0, private, must-revalidate
Etag:
- W/"4ec3e0a20895d90b1e1241ca67f10ca3"
Referrer-Policy:
- strict-origin-when-cross-origin
Rndr-Id:
- 0cab64c9-e312-4bec
Strict-Transport-Security:
- max-age=63072000; includeSubDomains
Vary:
- Accept-Encoding
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-Render-Origin-Server:
- Render
X-Request-Id:
- 1958563c-7c18-4201-a03c-a4b343dc68ab
X-Runtime:
- '0.014938'
X-Xss-Protection:
- '0'
Cf-Cache-Status:
- DYNAMIC
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=P3OWn4c8LFFWI0Dwr2CSYwHLaNhf9iD9TfAhqdx5PtLoWZ0pSImebfUsh00ZbOmh4r2cRJEQOmvy67wAwl6p0W%2Fx9017EkCnCaXibBBCKqJTBOdGnsSuV%2B45LrHsQmg%2BGeBwrw4b"}],"group":"cf-nel","max_age":604800}'
Nel:
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
Speculation-Rules:
- '"/cdn-cgi/speculation"'
Server:
- cloudflare
Cf-Ray:
- 920f637aa8cf1152-ORD
Alt-Svc:
- h3=":443"; ma=86400
Server-Timing:
- cfL4;desc="?proto=TCP&rtt=25627&min_rtt=25594&rtt_var=9664&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2827&recv_bytes=878&delivery_rate=111991&cwnd=248&unsent_bytes=0&cid=c8e4c4e269114d14&ts=263&x=0"
body:
encoding: ASCII-8BIT
string: '{"id":"user_3208c49393f54b3e974795e4bea5b864","email":"test@maybe.co","name":"Test
User","plan":"Business","api_calls_remaining":249830,"api_limit":250000,"credits_reset_at":"2025-04-01T00:00:00.000-04:00","current_period_start":"2025-03-01T00:00:00.000-05:00"}'
recorded_at: Sat, 15 Mar 2025 22:18:47 GMT
recorded_with: VCR 6.3.1

View file

@ -0,0 +1,105 @@
---
http_interactions:
- request:
method: get
uri: https://api.synthfinance.com/tickers/AAPL?operating_mic=XNAS
body:
encoding: US-ASCII
string: ''
headers:
Authorization:
- Bearer <SYNTH_API_KEY>
X-Source:
- maybe_app
X-Source-Type:
- managed
User-Agent:
- Faraday v2.12.2
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Date:
- Sun, 16 Mar 2025 12:04:12 GMT
Content-Type:
- application/json; charset=utf-8
Transfer-Encoding:
- chunked
Connection:
- keep-alive
Cache-Control:
- max-age=0, private, must-revalidate
Etag:
- W/"a9deeb6437d359f080be449b9b2c547b"
Referrer-Policy:
- strict-origin-when-cross-origin
Rndr-Id:
- 1e77ae49-050a-45fc
Strict-Transport-Security:
- max-age=63072000; includeSubDomains
Vary:
- Accept-Encoding
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-Render-Origin-Server:
- Render
X-Request-Id:
- 222dacf1-37f3-4eb8-91d5-edf13d732d46
X-Runtime:
- '0.059222'
X-Xss-Protection:
- '0'
Cf-Cache-Status:
- DYNAMIC
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=%2BLW%2Fd%2BbcNg4%2FleO6ECyB4RJBMbm6vWG3%2FX4oKQXfn1ROSPVrISc3ZFVlXfITGW4XYJSPyUDF%2FXrrRF6p3Wzow07QamOrsux7sxBMvtWmcubgpCMFI4zgnhESklW6KcmAefwrgj9i"}],"group":"cf-nel","max_age":604800}'
Nel:
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
Speculation-Rules:
- '"/cdn-cgi/speculation"'
Server:
- cloudflare
Cf-Ray:
- 92141c97bfd9124c-ORD
Alt-Svc:
- h3=":443"; ma=86400
Server-Timing:
- cfL4;desc="?proto=TCP&rtt=27459&min_rtt=26850&rtt_var=11288&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2828&recv_bytes=905&delivery_rate=91272&cwnd=104&unsent_bytes=0&cid=ccd6aa7e48e4b0eb&ts=287&x=0"
body:
encoding: ASCII-8BIT
string: '{"data":{"ticker":"AAPL","name":"Apple Inc.","links":{"homepage_url":"https://www.apple.com"},"logo_url":"https://logo.synthfinance.com/ticker/AAPL","description":"Apple
Inc. designs, manufactures, and markets smartphones, personal computers, tablets,
wearables, and accessories worldwide. The company offers iPhone, a line of
smartphones; Mac, a line of personal computers; iPad, a line of multi-purpose
tablets; and wearables, home, and accessories comprising AirPods, Apple TV,
Apple Watch, Beats products, and HomePod. It also provides AppleCare support
and cloud services; and operates various platforms, including the App Store
that allow customers to discover and download applications and digital content,
such as books, music, video, games, and podcasts. In addition, the company
offers various services, such as Apple Arcade, a game subscription service;
Apple Fitness+, a personalized fitness service; Apple Music, which offers
users a curated listening experience with on-demand radio stations; Apple
News+, a subscription news and magazine service; Apple TV+, which offers exclusive
original content; Apple Card, a co-branded credit card; and Apple Pay, a cashless
payment service, as well as licenses its intellectual property. The company
serves consumers, and small and mid-sized businesses; and the education, enterprise,
and government markets. It distributes third-party applications for its products
through the App Store. The company also sells its products through its retail
and online stores, and direct sales force; and third-party cellular network
carriers, wholesalers, retailers, and resellers. Apple Inc. was founded in
1976 and is headquartered in Cupertino, California.","kind":"common stock","cik":"0000320193","currency":"USD","address":{"country":"USA","address_line1":"One
Apple Park Way","city":"Cupertino","state":"CA","postal_code":"95014"},"exchange":{"name":"Nasdaq/Ngs
(Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
States","country_code":"US","timezone":"America/New_York"},"ceo":"Mr. Timothy
D. Cook","founding_year":1976,"industry":"Consumer Electronics","sector":"Technology","phone":"408-996-1010","total_employees":161000,"composite_figi":"BBG000B9Y5X2","market_data":{"high_today":213.95,"low_today":209.58,"open_today":211.25,"close_today":213.49,"volume_today":60060200.0,"fifty_two_week_high":260.1,"fifty_two_week_low":164.08,"average_volume":62848099.37313433,"price_change":0.0,"percent_change":0.0}},"meta":{"credits_used":1,"credits_remaining":249808}}'
recorded_at: Sun, 16 Mar 2025 12:04:12 GMT
recorded_with: VCR 6.3.1

View file

@ -0,0 +1,83 @@
---
http_interactions:
- request:
method: get
uri: https://api.synthfinance.com/tickers/AAPL/open-close?end_date=2024-08-01&operating_mic_code=XNAS&page=1&start_date=2024-08-01
body:
encoding: US-ASCII
string: ''
headers:
Authorization:
- Bearer <SYNTH_API_KEY>
X-Source:
- maybe_app
X-Source-Type:
- managed
User-Agent:
- Faraday v2.12.2
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Date:
- Sun, 16 Mar 2025 12:08:00 GMT
Content-Type:
- application/json; charset=utf-8
Transfer-Encoding:
- chunked
Connection:
- keep-alive
Cache-Control:
- max-age=0, private, must-revalidate
Etag:
- W/"cdf04c2cd77e230c03117dd13d0921f9"
Referrer-Policy:
- strict-origin-when-cross-origin
Rndr-Id:
- e74b3425-0b7c-447d
Strict-Transport-Security:
- max-age=63072000; includeSubDomains
Vary:
- Accept-Encoding
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-Render-Origin-Server:
- Render
X-Request-Id:
- b906c5e1-18cc-44cc-9085-313ff066a6ce
X-Runtime:
- '0.544708'
X-Xss-Protection:
- '0'
Cf-Cache-Status:
- DYNAMIC
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=dZNe6qCGGI2XGXgByLr69%2FYrDQdy2FLtnXafxJnlsvyVjrRFiCvmbbIzgF5CDgtj9HZ8RC5Rh9jbuEI6hPokpa3Al4FEIAZB5AbfZ9toP%2Bc5muG%2FuBgHR%2FnIZpsWG%2BQKmBPu9MBa"}],"group":"cf-nel","max_age":604800}'
Nel:
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
Speculation-Rules:
- '"/cdn-cgi/speculation"'
Server:
- cloudflare
Cf-Ray:
- 921422292d0feacc-ORD
Alt-Svc:
- h3=":443"; ma=86400
Server-Timing:
- cfL4;desc="?proto=TCP&rtt=30826&min_rtt=26727&rtt_var=12950&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2827&recv_bytes=970&delivery_rate=108354&cwnd=219&unsent_bytes=0&cid=43c717161effdc57&ts=695&x=0"
body:
encoding: ASCII-8BIT
string: '{"ticker":"AAPL","currency":"USD","exchange":{"name":"Nasdaq/Ngs (Global
Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
States","country_code":"US","timezone":"America/New_York"},"prices":[{"date":"2024-08-01","open":224.37,"close":218.36,"high":224.48,"low":217.02,"volume":62501000}],"paging":{"prev":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=\u0026start_date=2024-08-01","next":"/tickers/AAPL/open-close?end_date=2024-08-01\u0026operating_mic_code=XNAS\u0026page=\u0026start_date=2024-08-01","total_records":1,"current_page":1,"per_page":100,"total_pages":1},"meta":{"credits_used":1,"credits_remaining":249807}}'
recorded_at: Sun, 16 Mar 2025 12:08:00 GMT
recorded_with: VCR 6.3.1

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,104 @@
---
http_interactions:
- request:
method: get
uri: https://api.synthfinance.com/tickers/search?country_code=US&dataset=limited&limit=25&name=AAPL
body:
encoding: US-ASCII
string: ''
headers:
Authorization:
- Bearer <SYNTH_API_KEY>
X-Source:
- maybe_app
X-Source-Type:
- managed
User-Agent:
- Faraday v2.12.2
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Date:
- Sun, 16 Mar 2025 12:01:58 GMT
Content-Type:
- application/json; charset=utf-8
Transfer-Encoding:
- chunked
Connection:
- keep-alive
Cache-Control:
- max-age=0, private, must-revalidate
Etag:
- W/"3e444869eacbaf17006766a691cc8fdc"
Referrer-Policy:
- strict-origin-when-cross-origin
Rndr-Id:
- 2effb56b-f67f-402d
Strict-Transport-Security:
- max-age=63072000; includeSubDomains
Vary:
- Accept-Encoding
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-Render-Origin-Server:
- Render
X-Request-Id:
- 33470619-5119-4923-b4e0-e9a0eeb532a1
X-Runtime:
- '0.453770'
X-Xss-Protection:
- '0'
Cf-Cache-Status:
- DYNAMIC
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=ayZOlXkCwLgUl%2FrB2%2BlqtqR5HCllubf4HLDipEt3klWKyHS4nilHi9XZ1fiEQWx7xwiRMJZ5EW0Xzm7ISoHWTtEbkgMQHWYQwSTeg30ahFFHK1pkOOnET1fuW1UxiZwlJtq1XZGB"}],"group":"cf-nel","max_age":604800}'
Nel:
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
Speculation-Rules:
- '"/cdn-cgi/speculation"'
Server:
- cloudflare
Cf-Ray:
- 921419514e0a6399-ORD
Alt-Svc:
- h3=":443"; ma=86400
Server-Timing:
- cfL4;desc="?proto=TCP&rtt=25809&min_rtt=25801&rtt_var=9692&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2829&recv_bytes=939&delivery_rate=111952&cwnd=121&unsent_bytes=0&cid=2beb787f15cd8ab9&ts=610&x=0"
body:
encoding: ASCII-8BIT
string: '{"data":[{"symbol":"AAPL","name":"Apple Inc.","logo_url":"https://logo.synthfinance.com/ticker/AAPL","currency":"USD","exchange":{"name":"Nasdaq/Ngs
(Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"APLY","isin":"US88634T8577","name":"YieldMax
AAPL Option Income ETF","logo_url":"https://logo.synthfinance.com/ticker/APLY","currency":"USD","exchange":{"name":"Nyse
Arca","mic_code":"ARCX","operating_mic_code":"XNYS","acronym":"NYSE","country":"United
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPD","name":"Direxion
Daily AAPL Bear 1X ETF","logo_url":"https://logo.synthfinance.com/ticker/AAPD","currency":"USD","exchange":{"name":"Nasdaq/Nms
(Global Market)","mic_code":"XNMS","operating_mic_code":"XNAS","acronym":"","country":"United
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPU","isin":"US25461A8743","name":"Direxion
Daily AAPL Bull 2X Shares","logo_url":"https://logo.synthfinance.com/ticker/AAPU","currency":"USD","exchange":{"name":"Nasdaq/Nms
(Global Market)","mic_code":"XNMS","operating_mic_code":"XNAS","acronym":"","country":"United
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPB","isin":"XXXXXXXR8842","name":"GraniteShares
2x Long AAPL Daily ETF","logo_url":"https://logo.synthfinance.com/ticker/AAPB","currency":"USD","exchange":{"name":"Nasdaq/Ngs
(Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPD","isin":"US25461A3041","name":"Direxion
Daily AAPL Bear 1X Shares","logo_url":"https://logo.synthfinance.com/ticker/AAPD","currency":"USD","exchange":{"name":"Nasdaq/Ngs
(Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPU","isin":"US25461A8743","name":"Direxion
Daily AAPL Bull 1.5X Shares","logo_url":"https://logo.synthfinance.com/ticker/AAPU","currency":"USD","exchange":{"name":"Nasdaq/Ngs
(Global Select Market)","mic_code":"XNGS","operating_mic_code":"XNAS","acronym":"NGS","country":"United
States","country_code":"US","timezone":"America/New_York"}},{"symbol":"AAPJ","isin":"US00037T1034","name":"AAP,
Inc.","logo_url":"https://logo.synthfinance.com/ticker/AAPJ","currency":"USD","exchange":{"name":"Otc
Pink Marketplace","mic_code":"PINX","operating_mic_code":"OTCM","acronym":"","country":"United
States","country_code":"US","timezone":"America/New_York"}}]}'
recorded_at: Sun, 16 Mar 2025 12:01:58 GMT
recorded_with: VCR 6.3.1

View file

@ -0,0 +1,82 @@
---
http_interactions:
- request:
method: get
uri: https://api.synthfinance.com/enrich?amount=25.5&city=San%20Francisco&country=US&date=2025-03-16&description=UBER%20EATS&state=CA
body:
encoding: US-ASCII
string: ''
headers:
Authorization:
- Bearer <SYNTH_API_KEY>
X-Source:
- maybe_app
X-Source-Type:
- managed
User-Agent:
- Faraday v2.12.2
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Date:
- Sun, 16 Mar 2025 12:09:33 GMT
Content-Type:
- application/json; charset=utf-8
Transfer-Encoding:
- chunked
Connection:
- keep-alive
Cache-Control:
- max-age=0, private, must-revalidate
Etag:
- W/"00411c83cfeaade519bcc3e57d9e461e"
Referrer-Policy:
- strict-origin-when-cross-origin
Rndr-Id:
- 56a8791d-85ed-4342
Strict-Transport-Security:
- max-age=63072000; includeSubDomains
Vary:
- Accept-Encoding
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-Render-Origin-Server:
- Render
X-Request-Id:
- 1b35b9c1-0092-40b1-8b70-2bce7c5796af
X-Runtime:
- '0.884634'
X-Xss-Protection:
- '0'
Cf-Cache-Status:
- DYNAMIC
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=qUtB0aWbK%2Fh5W7cV%2FugsUGbWKtJzsf%2FXd5i8cm8KlepEtLyuVPH7XX0fqwzHp43OCWQkGr9r8hRBBSEcx9LWW5vS7%2B1kXCJaKPaTRn%2BWtsEymHg78OHqDcMahwSuy%2FkpSGLWo0or"}],"group":"cf-nel","max_age":604800}'
Nel:
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
Speculation-Rules:
- '"/cdn-cgi/speculation"'
Server:
- cloudflare
Cf-Ray:
- 921424681aa4acab-ORD
Alt-Svc:
- h3=":443"; ma=86400
Server-Timing:
- cfL4;desc="?proto=TCP&rtt=26975&min_rtt=26633&rtt_var=10231&sent=4&recv=6&lost=0&retrans=0&sent_bytes=2829&recv_bytes=969&delivery_rate=108737&cwnd=210&unsent_bytes=0&cid=318ff675628918e1&ts=1035&x=0"
body:
encoding: ASCII-8BIT
string: '{"merchant":"Uber Eats","merchant_id":"mer_aea41e7f29ce47b5873f3caf49d5972d","category":"Dining
Out","website":"ubereats.com","icon":"https://logo.synthfinance.com/ubereats.com","meta":{"credits_used":1,"credits_remaining":249806}}'
recorded_at: Sun, 16 Mar 2025 12:09:33 GMT
recorded_with: VCR 6.3.1

View file

@ -0,0 +1,82 @@
---
http_interactions:
- request:
method: get
uri: https://api.synthfinance.com/user
body:
encoding: US-ASCII
string: ''
headers:
Authorization:
- Bearer <SYNTH_API_KEY>
X-Source:
- maybe_app
X-Source-Type:
- managed
User-Agent:
- Faraday v2.12.2
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
response:
status:
code: 200
message: OK
headers:
Date:
- Sat, 15 Mar 2025 22:18:47 GMT
Content-Type:
- application/json; charset=utf-8
Transfer-Encoding:
- chunked
Connection:
- keep-alive
Cache-Control:
- max-age=0, private, must-revalidate
Etag:
- W/"4ec3e0a20895d90b1e1241ca67f10ca3"
Referrer-Policy:
- strict-origin-when-cross-origin
Rndr-Id:
- 54c8ecf9-6858-4db6
Strict-Transport-Security:
- max-age=63072000; includeSubDomains
Vary:
- Accept-Encoding
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-Render-Origin-Server:
- Render
X-Request-Id:
- a4112cfb-0eac-4e3e-a880-7536d90dcba0
X-Runtime:
- '0.007036'
X-Xss-Protection:
- '0'
Cf-Cache-Status:
- DYNAMIC
Report-To:
- '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=Rt0BTtrgXzYjWOQFgb%2Bg6N4xKvXtPI66Q251bq9nWtqUhGHo17GmVVAPkutwN7Gisw1RmvYfxYUiMCCxlc4%2BjuHxbU1%2BXr9KHy%2F5pUpLhgLNNrtkqqKOCW4GduODnDbw2I38Rocu"}],"group":"cf-nel","max_age":604800}'
Nel:
- '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}'
Speculation-Rules:
- '"/cdn-cgi/speculation"'
Server:
- cloudflare
Cf-Ray:
- 920f637d1fe8eb68-ORD
Alt-Svc:
- h3=":443"; ma=86400
Server-Timing:
- cfL4;desc="?proto=TCP&rtt=28779&min_rtt=27036&rtt_var=11384&sent=5&recv=6&lost=0&retrans=0&sent_bytes=2828&recv_bytes=878&delivery_rate=107116&cwnd=203&unsent_bytes=0&cid=52bc39ad09dd9eff&ts=145&x=0"
body:
encoding: ASCII-8BIT
string: '{"id":"user_3208c49393f54b3e974795e4bea5b864","email":"test@maybe.co","name":"Test
User","plan":"Business","api_calls_remaining":1200,"api_limit":5000,"credits_reset_at":"2025-04-01T00:00:00.000-04:00","current_period_start":"2025-03-01T00:00:00.000-05:00"}'
recorded_at: Sat, 15 Mar 2025 22:18:47 GMT
recorded_with: VCR 6.3.1