mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-27 09:09:41 +02:00
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.
361 lines
11 KiB
Ruby
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
|