mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 20:14:08 +00:00
Add (beta) CoinStats Crypto Wallet Integration with Balance and Transaction Syncing (#512)
* Feat(CoinStats): Scaffold implementation, not yet functional * Feat(CoinStats): Implement crypto wallet balance and transactions * Feat(CoinStats): Add tests, Minor improvements * Feat(CoinStats): Utilize bulk fetch API endpoints * Feat(CoinStats): Migrate strings to i8n * Feat(CoinStats): Fix error handling in wallet link modal * Feat(CoinStats): Implement hourly provider sync job * Feat(CoinStats): Generate docstrings * Fix(CoinStats): Validate API Key on provider update * Fix(Providers): Safely handle race condition in merchance creation * Fix(CoinStats): Don't catch system signals in account processor * Fix(CoinStats): Preload before iterating accounts * Fix(CoinStats): Add no opener / referrer to API dashboard link * Fix(CoinStats): Use strict matching for symbols * Fix(CoinStats): Remove dead code in transactions importer * Fix(CoinStats): Avoid transaction fallback ID collisions * Fix(CoinStats): Improve Blockchains fetch error handling * Fix(CoinStats): Enforce NOT NULL constraint for API Key schema * Fix(CoinStats): Migrate sync status strings to i8n * Fix(CoinStats): Use class name rather than hardcoded string * Fix(CoinStats): Use account currency rather than hardcoded USD * Fix(CoinStats): Migrate from standalone to Provider class * Fix(CoinStats): Fix test failures due to string changes
This commit is contained in:
124
test/models/provider/coinstats_adapter_test.rb
Normal file
124
test/models/provider/coinstats_adapter_test.rb
Normal file
@@ -0,0 +1,124 @@
|
||||
require "test_helper"
|
||||
|
||||
class Provider::CoinstatsAdapterTest < ActiveSupport::TestCase
|
||||
include ProviderAdapterTestInterface
|
||||
|
||||
setup do
|
||||
@family = families(:dylan_family)
|
||||
@coinstats_item = CoinstatsItem.create!(
|
||||
family: @family,
|
||||
name: "Test CoinStats Bank",
|
||||
api_key: "test_api_key_123"
|
||||
)
|
||||
@coinstats_account = CoinstatsAccount.create!(
|
||||
coinstats_item: @coinstats_item,
|
||||
name: "CoinStats Crypto Account",
|
||||
account_id: "cs_mock_1",
|
||||
currency: "USD",
|
||||
current_balance: 1000,
|
||||
institution_metadata: {
|
||||
"name" => "CoinStats Test Wallet",
|
||||
"domain" => "coinstats.app",
|
||||
"url" => "https://coinstats.app",
|
||||
"logo" => "https://example.com/logo.png"
|
||||
}
|
||||
)
|
||||
@account = accounts(:crypto)
|
||||
@adapter = Provider::CoinstatsAdapter.new(@coinstats_account, account: @account)
|
||||
end
|
||||
|
||||
def adapter
|
||||
@adapter
|
||||
end
|
||||
|
||||
# Run shared interface tests
|
||||
test_provider_adapter_interface
|
||||
test_syncable_interface
|
||||
test_institution_metadata_interface
|
||||
|
||||
# Provider-specific tests
|
||||
test "returns correct provider name" do
|
||||
assert_equal "coinstats", @adapter.provider_name
|
||||
end
|
||||
|
||||
test "returns correct provider type" do
|
||||
assert_equal "CoinstatsAccount", @adapter.provider_type
|
||||
end
|
||||
|
||||
test "returns coinstats item" do
|
||||
assert_equal @coinstats_account.coinstats_item, @adapter.item
|
||||
end
|
||||
|
||||
test "returns account" do
|
||||
assert_equal @account, @adapter.account
|
||||
end
|
||||
|
||||
test "can_delete_holdings? returns false" do
|
||||
assert_equal false, @adapter.can_delete_holdings?
|
||||
end
|
||||
|
||||
test "parses institution domain from institution_metadata" do
|
||||
assert_equal "coinstats.app", @adapter.institution_domain
|
||||
end
|
||||
|
||||
test "parses institution name from institution_metadata" do
|
||||
assert_equal "CoinStats Test Wallet", @adapter.institution_name
|
||||
end
|
||||
|
||||
test "parses institution url from institution_metadata" do
|
||||
assert_equal "https://coinstats.app", @adapter.institution_url
|
||||
end
|
||||
|
||||
test "returns logo_url from institution_metadata" do
|
||||
assert_equal "https://example.com/logo.png", @adapter.logo_url
|
||||
end
|
||||
|
||||
test "derives domain from url if domain is blank" do
|
||||
@coinstats_account.update!(institution_metadata: {
|
||||
"url" => "https://www.example.com/path"
|
||||
})
|
||||
|
||||
adapter = Provider::CoinstatsAdapter.new(@coinstats_account, account: @account)
|
||||
assert_equal "example.com", adapter.institution_domain
|
||||
end
|
||||
|
||||
test "supported_account_types includes Crypto" do
|
||||
assert_includes Provider::CoinstatsAdapter.supported_account_types, "Crypto"
|
||||
end
|
||||
|
||||
test "connection_configs returns configurations when family can connect" do
|
||||
@family.stubs(:can_connect_coinstats?).returns(true)
|
||||
|
||||
configs = Provider::CoinstatsAdapter.connection_configs(family: @family)
|
||||
|
||||
assert_equal 1, configs.length
|
||||
assert_equal "coinstats", configs.first[:key]
|
||||
assert_equal "CoinStats", configs.first[:name]
|
||||
assert configs.first[:can_connect]
|
||||
end
|
||||
|
||||
test "connection_configs returns empty when family cannot connect" do
|
||||
@family.stubs(:can_connect_coinstats?).returns(false)
|
||||
|
||||
configs = Provider::CoinstatsAdapter.connection_configs(family: @family)
|
||||
|
||||
assert_equal [], configs
|
||||
end
|
||||
|
||||
test "build_provider returns nil when family is nil" do
|
||||
result = Provider::CoinstatsAdapter.build_provider(family: nil)
|
||||
assert_nil result
|
||||
end
|
||||
|
||||
test "build_provider returns nil when no coinstats_items with api_key" do
|
||||
empty_family = families(:empty)
|
||||
result = Provider::CoinstatsAdapter.build_provider(family: empty_family)
|
||||
assert_nil result
|
||||
end
|
||||
|
||||
test "build_provider returns Provider::Coinstats when credentials configured" do
|
||||
result = Provider::CoinstatsAdapter.build_provider(family: @family)
|
||||
|
||||
assert_instance_of Provider::Coinstats, result
|
||||
end
|
||||
end
|
||||
164
test/models/provider/coinstats_test.rb
Normal file
164
test/models/provider/coinstats_test.rb
Normal file
@@ -0,0 +1,164 @@
|
||||
require "test_helper"
|
||||
|
||||
class Provider::CoinstatsTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@provider = Provider::Coinstats.new("test_api_key")
|
||||
end
|
||||
|
||||
test "extract_wallet_balance finds matching wallet by address and connectionId" do
|
||||
bulk_data = [
|
||||
{
|
||||
blockchain: "ethereum",
|
||||
address: "0x123abc",
|
||||
connectionId: "ethereum",
|
||||
balances: [
|
||||
{ coinId: "ethereum", name: "Ethereum", amount: 1.5, price: 2000 }
|
||||
]
|
||||
},
|
||||
{
|
||||
blockchain: "bitcoin",
|
||||
address: "bc1qxyz",
|
||||
connectionId: "bitcoin",
|
||||
balances: [
|
||||
{ coinId: "bitcoin", name: "Bitcoin", amount: 0.5, price: 50000 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
result = @provider.extract_wallet_balance(bulk_data, "0x123abc", "ethereum")
|
||||
|
||||
assert_equal 1, result.size
|
||||
assert_equal "ethereum", result.first[:coinId]
|
||||
end
|
||||
|
||||
test "extract_wallet_balance handles case insensitive matching" do
|
||||
bulk_data = [
|
||||
{
|
||||
blockchain: "Ethereum",
|
||||
address: "0x123ABC",
|
||||
connectionId: "Ethereum",
|
||||
balances: [
|
||||
{ coinId: "ethereum", name: "Ethereum", amount: 1.5, price: 2000 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
result = @provider.extract_wallet_balance(bulk_data, "0x123abc", "ethereum")
|
||||
|
||||
assert_equal 1, result.size
|
||||
assert_equal "ethereum", result.first[:coinId]
|
||||
end
|
||||
|
||||
test "extract_wallet_balance returns empty array when wallet not found" do
|
||||
bulk_data = [
|
||||
{
|
||||
blockchain: "ethereum",
|
||||
address: "0x123abc",
|
||||
connectionId: "ethereum",
|
||||
balances: [
|
||||
{ coinId: "ethereum", name: "Ethereum", amount: 1.5, price: 2000 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
result = @provider.extract_wallet_balance(bulk_data, "0xnotfound", "ethereum")
|
||||
|
||||
assert_equal [], result
|
||||
end
|
||||
|
||||
test "extract_wallet_balance returns empty array for nil bulk_data" do
|
||||
result = @provider.extract_wallet_balance(nil, "0x123abc", "ethereum")
|
||||
|
||||
assert_equal [], result
|
||||
end
|
||||
|
||||
test "extract_wallet_balance returns empty array for non-array bulk_data" do
|
||||
result = @provider.extract_wallet_balance({ error: "invalid" }, "0x123abc", "ethereum")
|
||||
|
||||
assert_equal [], result
|
||||
end
|
||||
|
||||
test "extract_wallet_balance matches by blockchain when connectionId differs" do
|
||||
bulk_data = [
|
||||
{
|
||||
blockchain: "ethereum",
|
||||
address: "0x123abc",
|
||||
connectionId: "eth-mainnet", # Different connectionId
|
||||
balances: [
|
||||
{ coinId: "ethereum", name: "Ethereum", amount: 1.5, price: 2000 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
result = @provider.extract_wallet_balance(bulk_data, "0x123abc", "ethereum")
|
||||
|
||||
assert_equal 1, result.size
|
||||
end
|
||||
|
||||
test "extract_wallet_transactions finds matching wallet transactions" do
|
||||
bulk_data = [
|
||||
{
|
||||
blockchain: "ethereum",
|
||||
address: "0x123abc",
|
||||
connectionId: "ethereum",
|
||||
transactions: [
|
||||
{ hash: { id: "0xtx1" }, type: "Received", date: "2025-01-01T10:00:00.000Z" },
|
||||
{ hash: { id: "0xtx2" }, type: "Sent", date: "2025-01-02T11:00:00.000Z" }
|
||||
]
|
||||
},
|
||||
{
|
||||
blockchain: "bitcoin",
|
||||
address: "bc1qxyz",
|
||||
connectionId: "bitcoin",
|
||||
transactions: [
|
||||
{ hash: { id: "btctx1" }, type: "Received", date: "2025-01-03T12:00:00.000Z" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
result = @provider.extract_wallet_transactions(bulk_data, "0x123abc", "ethereum")
|
||||
|
||||
assert_equal 2, result.size
|
||||
assert_equal "0xtx1", result.first[:hash][:id]
|
||||
end
|
||||
|
||||
test "extract_wallet_transactions returns empty array when wallet not found" do
|
||||
bulk_data = [
|
||||
{
|
||||
blockchain: "ethereum",
|
||||
address: "0x123abc",
|
||||
connectionId: "ethereum",
|
||||
transactions: [
|
||||
{ hash: { id: "0xtx1" }, type: "Received" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
result = @provider.extract_wallet_transactions(bulk_data, "0xnotfound", "ethereum")
|
||||
|
||||
assert_equal [], result
|
||||
end
|
||||
|
||||
test "extract_wallet_transactions returns empty array for nil bulk_data" do
|
||||
result = @provider.extract_wallet_transactions(nil, "0x123abc", "ethereum")
|
||||
|
||||
assert_equal [], result
|
||||
end
|
||||
|
||||
test "extract_wallet_transactions handles case insensitive matching" do
|
||||
bulk_data = [
|
||||
{
|
||||
blockchain: "Ethereum",
|
||||
address: "0x123ABC",
|
||||
connectionId: "Ethereum",
|
||||
transactions: [
|
||||
{ hash: { id: "0xtx1" }, type: "Received" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
result = @provider.extract_wallet_transactions(bulk_data, "0x123abc", "ethereum")
|
||||
|
||||
assert_equal 1, result.size
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user