1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-27 09:09:41 +02:00
Maybe/test/controllers/api/v1/transactions_controller_test.rb
Zach Gollwitzer 018310d4d1 Fix rate limiting errors in API transaction controller tests
When tests run in parallel, they were sharing the same API key fixtures
which caused Redis rate limit counters to accumulate across test workers,
leading to unexpected rate limit errors.

Changes:
- Create fresh API keys in setup instead of using fixtures
- Each API key gets a unique auto-generated ID
- Clear existing active keys to avoid validation conflicts
- Use different sources (web/mobile) for multiple test keys
- Clear Redis rate limit data in setup to ensure clean state
- Update api_headers helper to use display_key instead of plain_key

This follows the existing pattern used in UsageControllerTest for
handling API keys that interact with Redis state.
2025-07-08 11:46:33 -04:00

361 lines
11 KiB
Ruby

# frozen_string_literal: true
require "test_helper"
class Api::V1::TransactionsControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:family_admin)
@family = @user.family
@account = @family.accounts.first
@transaction = @family.transactions.first
# Destroy existing active API keys to avoid validation errors
@user.api_keys.active.destroy_all
# Create fresh API keys instead of using fixtures to avoid parallel test conflicts
@api_key = ApiKey.create!(
user: @user,
name: "Test Read-Write Key",
scopes: ["read_write"],
display_key: "test_rw_#{SecureRandom.hex(8)}"
)
@read_only_api_key = ApiKey.create!(
user: @user,
name: "Test Read-Only Key",
scopes: ["read"],
display_key: "test_ro_#{SecureRandom.hex(8)}",
source: "mobile" # Use different source to allow multiple keys
)
# Clear any existing rate limit data
Redis.new.del("api_rate_limit:#{@api_key.id}")
Redis.new.del("api_rate_limit:#{@read_only_api_key.id}")
end
# INDEX action tests
test "should get index with valid API key" do
get api_v1_transactions_url, headers: api_headers(@api_key)
assert_response :success
response_data = JSON.parse(response.body)
assert response_data.key?("transactions")
assert response_data.key?("pagination")
assert response_data["pagination"].key?("page")
assert response_data["pagination"].key?("per_page")
assert response_data["pagination"].key?("total_count")
assert response_data["pagination"].key?("total_pages")
end
test "should get index with read-only API key" do
get api_v1_transactions_url, headers: api_headers(@read_only_api_key)
assert_response :success
end
test "should filter transactions by account_id" do
get api_v1_transactions_url, params: { account_id: @account.id }, headers: api_headers(@api_key)
assert_response :success
response_data = JSON.parse(response.body)
response_data["transactions"].each do |transaction|
assert_equal @account.id, transaction["account"]["id"]
end
end
test "should filter transactions by date range" do
start_date = 1.month.ago.to_date
end_date = Date.current
get api_v1_transactions_url,
params: { start_date: start_date, end_date: end_date },
headers: api_headers(@api_key)
assert_response :success
response_data = JSON.parse(response.body)
response_data["transactions"].each do |transaction|
transaction_date = Date.parse(transaction["date"])
assert transaction_date >= start_date
assert transaction_date <= end_date
end
end
test "should search transactions" do
# Create a transaction with a specific name for testing
entry = @account.entries.create!(
name: "Test Coffee Purchase",
amount: 5.50,
currency: "USD",
date: Date.current,
entryable: Transaction.new
)
get api_v1_transactions_url,
params: { search: "Coffee" },
headers: api_headers(@api_key)
assert_response :success
response_data = JSON.parse(response.body)
found_transaction = response_data["transactions"].find { |t| t["id"] == entry.transaction.id }
assert_not_nil found_transaction, "Should find the coffee transaction"
end
test "should paginate transactions" do
get api_v1_transactions_url,
params: { page: 1, per_page: 5 },
headers: api_headers(@api_key)
assert_response :success
response_data = JSON.parse(response.body)
assert response_data["transactions"].size <= 5
assert_equal 1, response_data["pagination"]["page"]
assert_equal 5, response_data["pagination"]["per_page"]
end
test "should reject index request without API key" do
get api_v1_transactions_url
assert_response :unauthorized
end
test "should reject index request with invalid API key" do
get api_v1_transactions_url, headers: { "X-Api-Key" => "invalid-key" }
assert_response :unauthorized
end
# SHOW action tests
test "should show transaction with valid API key" do
get api_v1_transaction_url(@transaction), headers: api_headers(@api_key)
assert_response :success
response_data = JSON.parse(response.body)
assert_equal @transaction.id, response_data["id"]
assert response_data.key?("name")
assert response_data.key?("amount")
assert response_data.key?("date")
assert response_data.key?("account")
end
test "should show transaction with read-only API key" do
get api_v1_transaction_url(@transaction), headers: api_headers(@read_only_api_key)
assert_response :success
end
test "should return 404 for non-existent transaction" do
get api_v1_transaction_url(999999), headers: api_headers(@api_key)
assert_response :not_found
end
test "should reject show request without API key" do
get api_v1_transaction_url(@transaction)
assert_response :unauthorized
end
# CREATE action tests
test "should create transaction with valid parameters" do
transaction_params = {
transaction: {
account_id: @account.id,
name: "Test Transaction",
amount: 25.00,
date: Date.current,
currency: "USD",
nature: "expense"
}
}
assert_difference("@account.entries.count", 1) do
post api_v1_transactions_url,
params: transaction_params,
headers: api_headers(@api_key)
end
assert_response :created
response_data = JSON.parse(response.body)
assert_equal "Test Transaction", response_data["name"]
assert_equal @account.id, response_data["account"]["id"]
end
test "should reject create with read-only API key" do
transaction_params = {
transaction: {
account_id: @account.id,
name: "Test Transaction",
amount: 25.00,
date: Date.current
}
}
post api_v1_transactions_url,
params: transaction_params,
headers: api_headers(@read_only_api_key)
assert_response :forbidden
end
test "should reject create with invalid parameters" do
transaction_params = {
transaction: {
# Missing required fields
name: "Test Transaction"
}
}
post api_v1_transactions_url,
params: transaction_params,
headers: api_headers(@api_key)
assert_response :unprocessable_entity
end
test "should reject create without API key" do
post api_v1_transactions_url, params: { transaction: { name: "Test" } }
assert_response :unauthorized
end
# UPDATE action tests
test "should update transaction with valid parameters" do
update_params = {
transaction: {
name: "Updated Transaction Name",
amount: 30.00
}
}
put api_v1_transaction_url(@transaction),
params: update_params,
headers: api_headers(@api_key)
assert_response :success
response_data = JSON.parse(response.body)
assert_equal "Updated Transaction Name", response_data["name"]
end
test "should reject update with read-only API key" do
update_params = {
transaction: {
name: "Updated Transaction Name"
}
}
put api_v1_transaction_url(@transaction),
params: update_params,
headers: api_headers(@read_only_api_key)
assert_response :forbidden
end
test "should reject update for non-existent transaction" do
put api_v1_transaction_url(999999),
params: { transaction: { name: "Test" } },
headers: api_headers(@api_key)
assert_response :not_found
end
test "should reject update without API key" do
put api_v1_transaction_url(@transaction), params: { transaction: { name: "Test" } }
assert_response :unauthorized
end
# DESTROY action tests
test "should destroy transaction" do
entry_to_delete = @account.entries.create!(
name: "Transaction to Delete",
amount: 10.00,
currency: "USD",
date: Date.current,
entryable: Transaction.new
)
transaction_to_delete = entry_to_delete.transaction
assert_difference("@account.entries.count", -1) do
delete api_v1_transaction_url(transaction_to_delete), headers: api_headers(@api_key)
end
assert_response :success
response_data = JSON.parse(response.body)
assert response_data.key?("message")
end
test "should reject destroy with read-only API key" do
delete api_v1_transaction_url(@transaction), headers: api_headers(@read_only_api_key)
assert_response :forbidden
end
test "should reject destroy for non-existent transaction" do
delete api_v1_transaction_url(999999), headers: api_headers(@api_key)
assert_response :not_found
end
test "should reject destroy without API key" do
delete api_v1_transaction_url(@transaction)
assert_response :unauthorized
end
# JSON structure tests
test "transaction JSON should have expected structure" do
get api_v1_transaction_url(@transaction), headers: api_headers(@api_key)
assert_response :success
transaction_data = JSON.parse(response.body)
# Basic fields
assert transaction_data.key?("id")
assert transaction_data.key?("date")
assert transaction_data.key?("amount")
assert transaction_data.key?("currency")
assert transaction_data.key?("name")
assert transaction_data.key?("classification")
assert transaction_data.key?("created_at")
assert transaction_data.key?("updated_at")
# Account information
assert transaction_data.key?("account")
assert transaction_data["account"].key?("id")
assert transaction_data["account"].key?("name")
assert transaction_data["account"].key?("account_type")
# Optional fields should be present (even if nil)
assert transaction_data.key?("category")
assert transaction_data.key?("merchant")
assert transaction_data.key?("tags")
assert transaction_data.key?("transfer")
assert transaction_data.key?("notes")
end
test "transactions with transfers should include transfer information" do
# Create a transfer between two accounts to test transfer rendering
from_account = @family.accounts.create!(
name: "Transfer From Account",
balance: 1000,
currency: "USD",
accountable: Depository.new
)
to_account = @family.accounts.create!(
name: "Transfer To Account",
balance: 0,
currency: "USD",
accountable: Depository.new
)
transfer = Transfer::Creator.new(
family: @family,
source_account_id: from_account.id,
destination_account_id: to_account.id,
date: Date.current,
amount: 100
).create
get api_v1_transaction_url(transfer.inflow_transaction), headers: api_headers(@api_key)
assert_response :success
transaction_data = JSON.parse(response.body)
assert_not_nil transaction_data["transfer"]
assert transaction_data["transfer"].key?("id")
assert transaction_data["transfer"].key?("amount")
assert transaction_data["transfer"].key?("currency")
assert transaction_data["transfer"].key?("other_account")
end
private
def api_headers(api_key)
{ "X-Api-Key" => api_key.display_key }
end
end