Files
sure/test/controllers/kraken_items_controller_test.rb
ghost be598aecf0 feat(providers): add Kraken exchange sync (#1759)
* feat(providers): add Kraken exchange sync

Adds family-scoped Kraken API-key connections, read-only balance and trade import, account setup/linking flows, provider status wiring, and focused test coverage.

Closes #1758

* test(providers): avoid Kraken sample secret false positive

* fix(providers): address Kraken review findings

* fix(providers): address Kraken review cleanup

* test(imports): stabilize transaction import ordering
2026-05-12 00:22:37 +02:00

279 lines
9.1 KiB
Ruby

# frozen_string_literal: true
require "test_helper"
class KrakenItemsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in users(:family_admin)
SyncJob.stubs(:perform_later)
@family = families(:dylan_family)
@existing_item = kraken_items(:one)
kraken_items(:requires_update).update!(scheduled_for_deletion: true)
@second_item = KrakenItem.create!(
family: @family,
name: "Business Kraken",
api_key: "second_kraken_key",
api_secret: "second_kraken_secret"
)
end
test "create adds a new kraken connection without overwriting existing credentials" do
existing_key = @existing_item.api_key
existing_secret = @existing_item.api_secret
assert_difference "KrakenItem.count", 1 do
post kraken_items_url, params: {
kraken_item: {
name: "Joint Kraken",
api_key: "joint_kraken_key",
api_secret: "joint_kraken_secret"
}
}
end
assert_redirected_to settings_providers_path
assert_equal existing_key, @existing_item.reload.api_key
assert_equal existing_secret, @existing_item.api_secret
assert_equal "joint_kraken_key", @family.kraken_items.find_by!(name: "Joint Kraken").api_key
end
test "update changes only the selected kraken connection" do
existing_key = @existing_item.api_key
patch kraken_item_url(@second_item), params: {
kraken_item: {
name: "Renamed Business Kraken",
api_key: "updated_second_key",
api_secret: "updated_second_secret"
}
}
assert_redirected_to settings_providers_path
assert_equal existing_key, @existing_item.reload.api_key
assert_equal "Renamed Business Kraken", @second_item.reload.name
assert_equal "updated_second_key", @second_item.api_key
assert_equal "updated_second_secret", @second_item.api_secret
end
test "blank secret update preserves the selected kraken credentials" do
original_key = @second_item.api_key
original_secret = @second_item.api_secret
patch kraken_item_url(@second_item), params: {
kraken_item: {
name: "Renamed Business Kraken",
api_key: "",
api_secret: ""
}
}
assert_redirected_to settings_providers_path
assert_equal "Renamed Business Kraken", @second_item.reload.name
assert_equal original_key, @second_item.api_key
assert_equal original_secret, @second_item.api_secret
end
test "create rejects whitespace-only credentials" do
assert_no_difference "KrakenItem.count" do
post kraken_items_url, params: {
kraken_item: {
name: "Blank Kraken",
api_key: " ",
api_secret: "\n"
}
}
end
assert_redirected_to settings_providers_path
assert_match(/API key can't be blank/i, flash[:alert])
end
test "select accounts requires an explicit connection when multiple kraken items exist" do
get select_accounts_kraken_items_url, params: { accountable_type: "Crypto" }
assert_redirected_to settings_providers_path
assert_equal "Choose a Kraken connection in Provider Settings.", flash[:alert]
end
test "select accounts targets selected kraken item" do
get select_accounts_kraken_items_url, params: {
kraken_item_id: @second_item.id,
accountable_type: "Crypto"
}
assert_redirected_to setup_accounts_kraken_item_path(@second_item, return_to: nil)
end
test "select accounts rejects protocol-relative return paths" do
get select_accounts_kraken_items_url, params: {
kraken_item_id: @second_item.id,
accountable_type: "Crypto",
return_to: "//evil.example/accounts"
}
assert_redirected_to setup_accounts_kraken_item_path(@second_item, return_to: nil)
end
test "sync only queues a sync for the selected kraken item" do
assert_difference -> { Sync.where(syncable: @second_item).count }, 1 do
assert_no_difference -> { Sync.where(syncable: @existing_item).count } do
post sync_kraken_item_url(@second_item)
end
end
assert_response :redirect
end
test "setup accounts creates crypto exchange account for selected item only" do
first_account = kraken_accounts(:one)
second_account = @second_item.kraken_accounts.create!(
name: "Second Kraken",
account_id: "combined",
account_type: "combined",
currency: "USD",
current_balance: 1000
)
KrakenAccount::Processor.any_instance.stubs(:process).returns(nil)
assert_difference "Account.count", 1 do
post complete_account_setup_kraken_item_url(@second_item), params: {
selected_accounts: [ second_account.id ]
}
end
assert_redirected_to accounts_path
assert_nil first_account.reload.current_account
assert_equal "Crypto", second_account.reload.current_account.accountable_type
assert_equal "exchange", second_account.current_account.accountable.subtype
end
test "link existing account links manual crypto exchange account to selected kraken account" do
manual_account = manual_crypto_exchange_account
kraken_account = @second_item.kraken_accounts.create!(
name: "Kraken",
account_id: "combined",
account_type: "combined",
currency: "USD",
current_balance: 1000
)
assert_difference "AccountProvider.count", 1 do
post link_existing_account_kraken_items_url, params: {
kraken_item_id: @second_item.id,
account_id: manual_account.id,
kraken_account_id: kraken_account.id
}
end
assert_redirected_to accounts_path
assert_equal manual_account, kraken_account.reload.current_account
end
test "link existing account requires explicit connection when multiple items exist" do
account = manual_crypto_exchange_account
assert_no_difference "AccountProvider.count" do
post link_existing_account_kraken_items_url, params: {
account_id: account.id,
kraken_account_id: "combined"
}
end
assert_redirected_to settings_providers_path
assert_equal "Choose a Kraken connection before linking accounts.", flash[:alert]
end
test "link existing account rejects non crypto accounts" do
account = @family.accounts.create!(
name: "Manual Checking",
balance: 0,
currency: "USD",
accountable: Depository.new
)
kraken_account = @second_item.kraken_accounts.create!(name: "Kraken", account_id: "combined", account_type: "combined", currency: "USD")
assert_no_difference "AccountProvider.count" do
post link_existing_account_kraken_items_url, params: {
kraken_item_id: @second_item.id,
account_id: account.id,
kraken_account_id: kraken_account.id
}
end
assert_redirected_to account_path(account)
end
test "link existing account rejects accounts with existing provider links" do
account = manual_crypto_exchange_account
linked_kraken_account = kraken_accounts(:one)
AccountProvider.create!(account: account, provider: linked_kraken_account)
kraken_account = @second_item.kraken_accounts.create!(name: "Kraken", account_id: "combined", account_type: "combined", currency: "USD")
assert_no_difference "AccountProvider.count" do
post link_existing_account_kraken_items_url, params: {
kraken_item_id: @second_item.id,
account_id: account.id,
kraken_account_id: kraken_account.id
}
end
assert_redirected_to account_path(account)
end
test "link existing account rejects kraken accounts already linked elsewhere" do
linked_account = manual_crypto_exchange_account
available_account = manual_crypto_exchange_account
kraken_account = @second_item.kraken_accounts.create!(name: "Kraken", account_id: "combined", account_type: "combined", currency: "USD")
AccountProvider.create!(account: linked_account, provider: kraken_account)
assert_no_difference "AccountProvider.count" do
post link_existing_account_kraken_items_url, params: {
kraken_item_id: @second_item.id,
account_id: available_account.id,
kraken_account_id: kraken_account.id
}
end
assert_redirected_to account_path(available_account)
end
test "select existing account renders selected kraken item id" do
account = manual_crypto_exchange_account
@second_item.kraken_accounts.create!(name: "Kraken", account_id: "combined", account_type: "combined", currency: "USD")
get select_existing_account_kraken_items_url, params: {
kraken_item_id: @second_item.id,
account_id: account.id
}
assert_response :success
assert_includes @response.body, %(name="kraken_item_id")
assert_includes @response.body, %(value="#{@second_item.id}")
end
test "cannot access another family's kraken item" do
other_item = KrakenItem.create!(
family: families(:empty),
name: "Other Kraken",
api_key: "other_key",
api_secret: "other_secret"
)
get setup_accounts_kraken_item_url(other_item)
assert_response :not_found
end
private
def manual_crypto_exchange_account
@family.accounts.create!(
name: "Manual Crypto",
balance: 0,
currency: "USD",
accountable: Crypto.create!(subtype: "exchange")
)
end
end