mirror of
https://github.com/we-promise/sure.git
synced 2026-05-10 14:15:01 +00:00
Merge branch 'main' into copilot/fix-twelvedata-api-limit-bug
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
156
test/models/provider/indexa_capital_test.rb
Normal file
156
test/models/provider/indexa_capital_test.rb
Normal file
@@ -0,0 +1,156 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class Provider::IndexaCapitalTest < ActiveSupport::TestCase
|
||||
test "initializes with api_token" do
|
||||
provider = Provider::IndexaCapital.new(api_token: "test_token")
|
||||
assert_instance_of Provider::IndexaCapital, provider
|
||||
end
|
||||
|
||||
test "initializes with username/document/password" do
|
||||
provider = Provider::IndexaCapital.new(
|
||||
username: "user@example.com",
|
||||
document: "12345678A",
|
||||
password: "secret"
|
||||
)
|
||||
assert_instance_of Provider::IndexaCapital, provider
|
||||
end
|
||||
|
||||
test "raises ConfigurationError without credentials" do
|
||||
assert_raises Provider::IndexaCapital::ConfigurationError do
|
||||
Provider::IndexaCapital.new
|
||||
end
|
||||
end
|
||||
|
||||
test "raises ConfigurationError with partial credentials" do
|
||||
assert_raises Provider::IndexaCapital::ConfigurationError do
|
||||
Provider::IndexaCapital.new(username: "user@example.com")
|
||||
end
|
||||
|
||||
assert_raises Provider::IndexaCapital::ConfigurationError do
|
||||
Provider::IndexaCapital.new(username: "user@example.com", document: "12345678A")
|
||||
end
|
||||
end
|
||||
|
||||
test "list_accounts calls API and returns accounts" do
|
||||
provider = Provider::IndexaCapital.new(api_token: "test_token")
|
||||
|
||||
stub_response = OpenStruct.new(
|
||||
code: 200,
|
||||
body: {
|
||||
accounts: [
|
||||
{ account_number: "ABC12345", type: "mutual", status: "active" },
|
||||
{ account_number: "DEF67890", type: "pension", status: "active" }
|
||||
]
|
||||
}.to_json
|
||||
)
|
||||
|
||||
Provider::IndexaCapital.stubs(:get).returns(stub_response)
|
||||
|
||||
accounts = provider.list_accounts
|
||||
assert_equal 2, accounts.size
|
||||
assert_equal "ABC12345", accounts[0][:account_number]
|
||||
assert_equal "Indexa Capital Mutual Fund (ABC12345)", accounts[0][:name]
|
||||
assert_equal "EUR", accounts[0][:currency]
|
||||
assert_equal "DEF67890", accounts[1][:account_number]
|
||||
assert_equal "Indexa Capital Pension Plan (DEF67890)", accounts[1][:name]
|
||||
end
|
||||
|
||||
test "get_holdings calls fiscal-results endpoint" do
|
||||
provider = Provider::IndexaCapital.new(api_token: "test_token")
|
||||
|
||||
stub_response = OpenStruct.new(
|
||||
code: 200,
|
||||
body: {
|
||||
fiscal_results: [
|
||||
{ amount: 1814.77, titles: 9.14, price: 175.34, instrument: { identifier: "IE00BFPM9P35" } }
|
||||
],
|
||||
total_fiscal_results: []
|
||||
}.to_json
|
||||
)
|
||||
|
||||
Provider::IndexaCapital.stubs(:get).returns(stub_response)
|
||||
|
||||
data = provider.get_holdings(account_number: "ABC12345")
|
||||
assert data[:fiscal_results].is_a?(Array)
|
||||
assert_equal 1, data[:fiscal_results].size
|
||||
end
|
||||
|
||||
test "get_account_balance extracts total_amount from portfolios" do
|
||||
provider = Provider::IndexaCapital.new(api_token: "test_token")
|
||||
|
||||
stub_response = OpenStruct.new(
|
||||
code: 200,
|
||||
body: {
|
||||
portfolios: [
|
||||
{ date: "2026-02-05", total_amount: 38000.0 },
|
||||
{ date: "2026-02-06", total_amount: 38905.21 }
|
||||
]
|
||||
}.to_json
|
||||
)
|
||||
|
||||
Provider::IndexaCapital.stubs(:get).returns(stub_response)
|
||||
|
||||
balance = provider.get_account_balance(account_number: "ABC12345")
|
||||
assert_equal 38905.21.to_d, balance
|
||||
end
|
||||
|
||||
test "get_account_balance returns 0 when no portfolios" do
|
||||
provider = Provider::IndexaCapital.new(api_token: "test_token")
|
||||
|
||||
stub_response = OpenStruct.new(
|
||||
code: 200,
|
||||
body: { portfolios: [] }.to_json
|
||||
)
|
||||
|
||||
Provider::IndexaCapital.stubs(:get).returns(stub_response)
|
||||
|
||||
balance = provider.get_account_balance(account_number: "ABC12345")
|
||||
assert_equal 0, balance
|
||||
end
|
||||
|
||||
test "get_activities returns empty array" do
|
||||
provider = Provider::IndexaCapital.new(api_token: "test_token")
|
||||
result = provider.get_activities(account_number: "ABC12345")
|
||||
assert_equal [], result
|
||||
end
|
||||
|
||||
test "raises AuthenticationError on 401" do
|
||||
provider = Provider::IndexaCapital.new(api_token: "bad_token")
|
||||
|
||||
stub_response = OpenStruct.new(code: 401, body: "Unauthorized")
|
||||
Provider::IndexaCapital.stubs(:get).returns(stub_response)
|
||||
|
||||
assert_raises Provider::IndexaCapital::AuthenticationError do
|
||||
provider.list_accounts
|
||||
end
|
||||
end
|
||||
|
||||
test "rejects invalid account_number with path traversal" do
|
||||
provider = Provider::IndexaCapital.new(api_token: "test_token")
|
||||
|
||||
assert_raises Provider::IndexaCapital::Error do
|
||||
provider.get_holdings(account_number: "../admin")
|
||||
end
|
||||
end
|
||||
|
||||
test "rejects blank account_number" do
|
||||
provider = Provider::IndexaCapital.new(api_token: "test_token")
|
||||
|
||||
assert_raises Provider::IndexaCapital::Error do
|
||||
provider.get_holdings(account_number: "")
|
||||
end
|
||||
end
|
||||
|
||||
test "raises Error on server error" do
|
||||
provider = Provider::IndexaCapital.new(api_token: "test_token")
|
||||
|
||||
stub_response = OpenStruct.new(code: 500, body: "Internal Server Error")
|
||||
Provider::IndexaCapital.stubs(:get).returns(stub_response)
|
||||
|
||||
assert_raises Provider::IndexaCapital::Error do
|
||||
provider.list_accounts
|
||||
end
|
||||
end
|
||||
end
|
||||
197
test/models/provider/openai/bank_statement_extractor_test.rb
Normal file
197
test/models/provider/openai/bank_statement_extractor_test.rb
Normal file
@@ -0,0 +1,197 @@
|
||||
require "test_helper"
|
||||
|
||||
class Provider::Openai::BankStatementExtractorTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@client = mock("openai_client")
|
||||
@model = "gpt-4.1"
|
||||
end
|
||||
|
||||
test "extracts transactions from PDF content" do
|
||||
mock_response = {
|
||||
"choices" => [ {
|
||||
"message" => {
|
||||
"content" => {
|
||||
"bank_name" => "Test Bank",
|
||||
"account_holder" => "John Doe",
|
||||
"account_number" => "1234",
|
||||
"statement_period" => {
|
||||
"start_date" => "2024-01-01",
|
||||
"end_date" => "2024-01-31"
|
||||
},
|
||||
"opening_balance" => 5000.00,
|
||||
"closing_balance" => 4500.00,
|
||||
"transactions" => [
|
||||
{ "date" => "2024-01-15", "description" => "Coffee Shop", "amount" => -5.50 },
|
||||
{ "date" => "2024-01-20", "description" => "Salary Deposit", "amount" => 3000.00 }
|
||||
]
|
||||
}.to_json
|
||||
}
|
||||
} ]
|
||||
}
|
||||
|
||||
@client.expects(:chat).returns(mock_response)
|
||||
|
||||
extractor = Provider::Openai::BankStatementExtractor.new(
|
||||
client: @client,
|
||||
pdf_content: "dummy",
|
||||
model: @model
|
||||
)
|
||||
|
||||
# Mock the PDF text extraction
|
||||
extractor.stubs(:extract_pages_from_pdf).returns([ "Page 1 bank statement text" ])
|
||||
|
||||
result = extractor.extract
|
||||
|
||||
assert_equal "Test Bank", result[:bank_name]
|
||||
assert_equal "John Doe", result[:account_holder]
|
||||
assert_equal "1234", result[:account_number]
|
||||
assert_equal 5000.00, result[:opening_balance]
|
||||
assert_equal 4500.00, result[:closing_balance]
|
||||
assert_equal 2, result[:transactions].size
|
||||
|
||||
first_txn = result[:transactions].first
|
||||
assert_equal "2024-01-15", first_txn[:date]
|
||||
assert_equal "Coffee Shop", first_txn[:name]
|
||||
assert_equal(-5.50, first_txn[:amount])
|
||||
end
|
||||
|
||||
test "handles empty PDF content" do
|
||||
extractor = Provider::Openai::BankStatementExtractor.new(
|
||||
client: @client,
|
||||
pdf_content: "",
|
||||
model: @model
|
||||
)
|
||||
|
||||
assert_raises(Provider::Openai::Error) do
|
||||
extractor.extract
|
||||
end
|
||||
end
|
||||
|
||||
test "handles nil PDF content" do
|
||||
extractor = Provider::Openai::BankStatementExtractor.new(
|
||||
client: @client,
|
||||
pdf_content: nil,
|
||||
model: @model
|
||||
)
|
||||
|
||||
assert_raises(Provider::Openai::Error) do
|
||||
extractor.extract
|
||||
end
|
||||
end
|
||||
|
||||
test "deduplicates transactions across chunk boundaries" do
|
||||
# First chunk response
|
||||
first_response = {
|
||||
"choices" => [ {
|
||||
"message" => {
|
||||
"content" => {
|
||||
"bank_name" => "Test Bank",
|
||||
"account_holder" => "John Doe",
|
||||
"account_number" => "1234",
|
||||
"statement_period" => { "start_date" => "2024-01-01", "end_date" => "2024-01-31" },
|
||||
"opening_balance" => 5000.00,
|
||||
"closing_balance" => 4500.00,
|
||||
"transactions" => [
|
||||
{ "date" => "2024-01-15", "description" => "Coffee Shop", "amount" => -5.50 },
|
||||
{ "date" => "2024-01-16", "description" => "Grocery Store", "amount" => -50.00 }
|
||||
]
|
||||
}.to_json
|
||||
}
|
||||
} ]
|
||||
}
|
||||
|
||||
# Second chunk response with duplicate at boundary
|
||||
second_response = {
|
||||
"choices" => [ {
|
||||
"message" => {
|
||||
"content" => {
|
||||
"transactions" => [
|
||||
{ "date" => "2024-01-16", "description" => "Grocery Store", "amount" => -50.00 },
|
||||
{ "date" => "2024-01-17", "description" => "Gas Station", "amount" => -40.00 }
|
||||
]
|
||||
}.to_json
|
||||
}
|
||||
} ]
|
||||
}
|
||||
|
||||
@client.expects(:chat).twice.returns(first_response, second_response)
|
||||
|
||||
extractor = Provider::Openai::BankStatementExtractor.new(
|
||||
client: @client,
|
||||
pdf_content: "dummy",
|
||||
model: @model
|
||||
)
|
||||
|
||||
# Mock multiple pages that will create multiple chunks
|
||||
extractor.stubs(:extract_pages_from_pdf).returns([
|
||||
"Page 1 " * 500, # ~3500 chars, first chunk
|
||||
"Page 2 " * 500 # ~3500 chars, second chunk
|
||||
])
|
||||
|
||||
result = extractor.extract
|
||||
|
||||
# Should deduplicate the "Grocery Store" transaction at chunk boundary
|
||||
assert_equal 3, result[:transactions].size
|
||||
names = result[:transactions].map { |t| t[:name] }
|
||||
assert_includes names, "Coffee Shop"
|
||||
assert_includes names, "Grocery Store"
|
||||
assert_includes names, "Gas Station"
|
||||
end
|
||||
|
||||
test "normalizes transaction amounts" do
|
||||
mock_response = {
|
||||
"choices" => [ {
|
||||
"message" => {
|
||||
"content" => {
|
||||
"transactions" => [
|
||||
{ "date" => "2024-01-15", "description" => "Test 1", "amount" => "-$5.50" },
|
||||
{ "date" => "2024-01-16", "description" => "Test 2", "amount" => "1,234.56" },
|
||||
{ "date" => "2024-01-17", "description" => "Test 3", "amount" => -100 }
|
||||
]
|
||||
}.to_json
|
||||
}
|
||||
} ]
|
||||
}
|
||||
|
||||
@client.expects(:chat).returns(mock_response)
|
||||
|
||||
extractor = Provider::Openai::BankStatementExtractor.new(
|
||||
client: @client,
|
||||
pdf_content: "dummy",
|
||||
model: @model
|
||||
)
|
||||
|
||||
extractor.stubs(:extract_pages_from_pdf).returns([ "Page 1 text" ])
|
||||
|
||||
result = extractor.extract
|
||||
|
||||
assert_equal(-5.50, result[:transactions][0][:amount])
|
||||
assert_equal 1234.56, result[:transactions][1][:amount]
|
||||
assert_equal(-100.0, result[:transactions][2][:amount])
|
||||
end
|
||||
|
||||
test "handles malformed JSON response gracefully" do
|
||||
mock_response = {
|
||||
"choices" => [ {
|
||||
"message" => {
|
||||
"content" => "This is not valid JSON"
|
||||
}
|
||||
} ]
|
||||
}
|
||||
|
||||
@client.expects(:chat).returns(mock_response)
|
||||
|
||||
extractor = Provider::Openai::BankStatementExtractor.new(
|
||||
client: @client,
|
||||
pdf_content: "dummy",
|
||||
model: @model
|
||||
)
|
||||
|
||||
extractor.stubs(:extract_pages_from_pdf).returns([ "Page 1 text" ])
|
||||
|
||||
result = extractor.extract
|
||||
|
||||
# Should return empty transactions on parse error
|
||||
assert_equal [], result[:transactions]
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@ class Provider::SimplefinTest < ActiveSupport::TestCase
|
||||
# First call raises timeout, second call succeeds
|
||||
mock_response = OpenStruct.new(code: 200, body: '{"accounts": []}')
|
||||
|
||||
HTTParty.expects(:get)
|
||||
Provider::Simplefin.expects(:get)
|
||||
.times(2)
|
||||
.raises(Net::ReadTimeout.new("Connection timed out"))
|
||||
.then.returns(mock_response)
|
||||
@@ -25,7 +25,7 @@ class Provider::SimplefinTest < ActiveSupport::TestCase
|
||||
test "retries on Net::OpenTimeout and succeeds on retry" do
|
||||
mock_response = OpenStruct.new(code: 200, body: '{"accounts": []}')
|
||||
|
||||
HTTParty.expects(:get)
|
||||
Provider::Simplefin.expects(:get)
|
||||
.times(2)
|
||||
.raises(Net::OpenTimeout.new("Connection timed out"))
|
||||
.then.returns(mock_response)
|
||||
@@ -39,7 +39,7 @@ class Provider::SimplefinTest < ActiveSupport::TestCase
|
||||
test "retries on SocketError and succeeds on retry" do
|
||||
mock_response = OpenStruct.new(code: 200, body: '{"accounts": []}')
|
||||
|
||||
HTTParty.expects(:get)
|
||||
Provider::Simplefin.expects(:get)
|
||||
.times(2)
|
||||
.raises(SocketError.new("Failed to open TCP connection"))
|
||||
.then.returns(mock_response)
|
||||
@@ -51,7 +51,7 @@ class Provider::SimplefinTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "raises SimplefinError after max retries exceeded" do
|
||||
HTTParty.expects(:get)
|
||||
Provider::Simplefin.expects(:get)
|
||||
.times(4) # Initial + 3 retries
|
||||
.raises(Net::ReadTimeout.new("Connection timed out"))
|
||||
|
||||
@@ -66,7 +66,7 @@ class Provider::SimplefinTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "does not retry on non-retryable errors" do
|
||||
HTTParty.expects(:get)
|
||||
Provider::Simplefin.expects(:get)
|
||||
.times(1)
|
||||
.raises(ArgumentError.new("Invalid argument"))
|
||||
|
||||
@@ -80,7 +80,7 @@ class Provider::SimplefinTest < ActiveSupport::TestCase
|
||||
test "handles HTTP 429 rate limit response" do
|
||||
mock_response = OpenStruct.new(code: 429, body: "Rate limit exceeded")
|
||||
|
||||
HTTParty.expects(:get).returns(mock_response)
|
||||
Provider::Simplefin.expects(:get).returns(mock_response)
|
||||
|
||||
error = assert_raises(Provider::Simplefin::SimplefinError) do
|
||||
@provider.get_accounts(@access_url)
|
||||
@@ -93,7 +93,7 @@ class Provider::SimplefinTest < ActiveSupport::TestCase
|
||||
test "handles HTTP 500 server error response" do
|
||||
mock_response = OpenStruct.new(code: 500, body: "Internal Server Error")
|
||||
|
||||
HTTParty.expects(:get).returns(mock_response)
|
||||
Provider::Simplefin.expects(:get).returns(mock_response)
|
||||
|
||||
error = assert_raises(Provider::Simplefin::SimplefinError) do
|
||||
@provider.get_accounts(@access_url)
|
||||
@@ -106,7 +106,7 @@ class Provider::SimplefinTest < ActiveSupport::TestCase
|
||||
setup_token = Base64.encode64("https://example.com/claim")
|
||||
mock_response = OpenStruct.new(code: 200, body: "https://example.com/access")
|
||||
|
||||
HTTParty.expects(:post)
|
||||
Provider::Simplefin.expects(:post)
|
||||
.times(2)
|
||||
.raises(Net::ReadTimeout.new("Connection timed out"))
|
||||
.then.returns(mock_response)
|
||||
|
||||
Reference in New Issue
Block a user