require "test_helper" class CoinstatsItem::ExchangeLinkerTest < ActiveSupport::TestCase setup do @family = families(:dylan_family) @family.update!(currency: "EUR") @coinstats_item = CoinstatsItem.create!( family: @family, name: "Test CoinStats Connection", api_key: "test_api_key_123" ) end def success_response(data) Provider::Response.new(success?: true, data: data, error: nil) end test "link creates one exchange portfolio account with embedded coins" do Provider::Coinstats.any_instance.expects(:exchange_options).returns([ { connection_id: "bitvavo", name: "Bitvavo", icon: "https://example.com/bitvavo.png", connection_fields: [ { key: "apiKey", name: "API Key" }, { key: "apiSecret", name: "API Secret" } ] } ]) Provider::Coinstats.any_instance.expects(:connect_portfolio_exchange) .with( connection_id: "bitvavo", connection_fields: { "apiKey" => "key", "apiSecret" => "secret" }, name: "Bitvavo Portfolio" ) .returns(success_response({ portfolioId: "portfolio_123" })) Provider::Coinstats.any_instance.expects(:list_portfolio_coins) .with(portfolio_id: "portfolio_123") .returns([ { coin: { identifier: "bitcoin", symbol: "BTC", name: "Bitcoin" }, count: "0.00335845", price: { EUR: "57950.0491" } }, { coin: { identifier: "ethereum", symbol: "ETH", name: "Ethereum" }, count: "0.05580825", price: { EUR: "1728.952252246" } }, { coin: { identifier: "FiatCoin:eur", symbol: "EUR", name: "Euro", isFiat: true }, count: "2.58", price: { EUR: "1" } } ]) @coinstats_item.expects(:sync_later).once assert_difference [ "CoinstatsAccount.count", "Account.count", "AccountProvider.count" ], 1 do result = CoinstatsItem::ExchangeLinker.new( @coinstats_item, connection_id: "bitvavo", connection_fields: { "apiKey" => "key", "apiSecret" => "secret" } ).link assert result.success? assert_equal 1, result.created_count end @coinstats_item.reload assert_equal "portfolio_123", @coinstats_item.exchange_portfolio_id coinstats_account = @coinstats_item.coinstats_accounts.last assert coinstats_account.exchange_portfolio_account? assert_equal "Bitvavo", coinstats_account.name assert_equal "exchange_portfolio:portfolio_123", coinstats_account.account_id assert_equal 3, coinstats_account.raw_payload["coins"].size account = coinstats_account.account assert_equal "Bitvavo", account.name assert_equal "EUR", account.currency assert_in_delta 293.69214193130284, account.balance.to_f, 0.0001 assert_in_delta 2.58, account.cash_balance.to_f, 0.0001 end test "link defers local account creation when initial portfolio coin fetch is missing" do Provider::Coinstats.any_instance.expects(:exchange_options).returns([ { connection_id: "bitvavo", name: "Bitvavo", icon: "https://example.com/bitvavo.png", connection_fields: [ { key: "apiKey", name: "API Key" } ] } ]) Provider::Coinstats.any_instance.expects(:connect_portfolio_exchange) .returns(success_response({ portfolioId: "portfolio_456" })) Provider::Coinstats.any_instance.expects(:list_portfolio_coins) .with(portfolio_id: "portfolio_456") .returns(nil) @coinstats_item.expects(:sync_later).once assert_no_difference [ "CoinstatsAccount.count", "Account.count", "AccountProvider.count" ] do result = CoinstatsItem::ExchangeLinker.new( @coinstats_item, connection_id: "bitvavo", connection_fields: { "apiKey" => "key" } ).link assert result.success? assert_equal 0, result.created_count end @coinstats_item.reload assert_equal "portfolio_456", @coinstats_item.exchange_portfolio_id assert_equal "bitvavo", @coinstats_item.exchange_connection_id assert_empty @coinstats_item.coinstats_accounts end end