Files
sure/test/controllers/api/v1/valuations_controller_test.rb
Pere Montpeó 9f5fdd4d13 feat: add valuations API endpoints for managing account reconciliations (#745)
* feat: add valuations API endpoints for managing account reconciliations

* refactor: formatting

* fix: make account extraction clearer

* feat: validation and error handling improvements

* feat: transaction

* feat: error handling

* Add API documentation LLM context

* Make it easier for people

* feat: transaction in creation

* feat: add OpenAPI spec for Valuations API

* fix: update notes validation to check for key presence

* Prevent double render

* All other docs use `apiKeyAuth`

* More `apiKeyAuth`

* Remove testing assertions from API doc specs

* fix: correct valuation entry references

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-01-30 18:54:15 +01:00

213 lines
5.8 KiB
Ruby

# frozen_string_literal: true
require "test_helper"
class Api::V1::ValuationsControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:family_admin)
@family = @user.family
@account = @family.accounts.first
@valuation = @family.entries.valuations.first.entryable
# 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 (rate limiting in test)
@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
# CREATE action tests
test "should create valuation with valid parameters" do
valuation_params = {
valuation: {
account_id: @account.id,
amount: 10000.00,
date: Date.current,
notes: "Quarterly statement"
}
}
assert_difference("@family.entries.valuations.count", 1) do
post api_v1_valuations_url,
params: valuation_params,
headers: api_headers(@api_key)
end
assert_response :created
response_data = JSON.parse(response.body)
assert_equal Date.current.to_s, response_data["date"]
assert_equal @account.id, response_data["account"]["id"]
end
test "should reject create with read-only API key" do
valuation_params = {
valuation: {
account_id: @account.id,
amount: 10000.00,
date: Date.current
}
}
post api_v1_valuations_url,
params: valuation_params,
headers: api_headers(@read_only_api_key)
assert_response :forbidden
end
test "should reject create with invalid account_id" do
valuation_params = {
valuation: {
account_id: 999999,
amount: 10000.00,
date: Date.current
}
}
post api_v1_valuations_url,
params: valuation_params,
headers: api_headers(@api_key)
assert_response :not_found
end
test "should reject create with invalid parameters" do
valuation_params = {
valuation: {
# Missing required fields
account_id: @account.id
}
}
post api_v1_valuations_url,
params: valuation_params,
headers: api_headers(@api_key)
assert_response :unprocessable_entity
end
test "should reject create without API key" do
post api_v1_valuations_url, params: { valuation: { account_id: @account.id } }
assert_response :unauthorized
end
# UPDATE action tests
test "should update valuation with valid parameters" do
entry = @valuation.entry
update_params = {
valuation: {
amount: 15000.00,
date: Date.current
}
}
put api_v1_valuation_url(entry),
params: update_params,
headers: api_headers(@api_key)
assert_response :success
response_data = JSON.parse(response.body)
assert_equal Date.current.to_s, response_data["date"]
end
test "should update valuation notes only" do
entry = @valuation.entry
update_params = {
valuation: {
notes: "Updated notes"
}
}
put api_v1_valuation_url(entry),
params: update_params,
headers: api_headers(@api_key)
assert_response :success
response_data = JSON.parse(response.body)
assert_equal "Updated notes", response_data["notes"]
end
test "should reject update with read-only API key" do
entry = @valuation.entry
update_params = {
valuation: {
amount: 15000.00
}
}
put api_v1_valuation_url(entry),
params: update_params,
headers: api_headers(@read_only_api_key)
assert_response :forbidden
end
test "should reject update for non-existent valuation" do
put api_v1_valuation_url(999999),
params: { valuation: { amount: 15000.00 } },
headers: api_headers(@api_key)
assert_response :not_found
end
test "should reject update without API key" do
entry = @valuation.entry
put api_v1_valuation_url(entry), params: { valuation: { amount: 15000.00 } }
assert_response :unauthorized
end
# JSON structure tests
test "valuation JSON should have expected structure" do
# Create a new valuation to test the structure
entry = @account.entries.create!(
name: Valuation.build_reconciliation_name(@account.accountable_type),
date: Date.current,
amount: 10000,
currency: @account.currency,
entryable: Valuation.new(kind: :reconciliation)
)
get api_v1_valuation_url(entry), headers: api_headers(@api_key)
assert_response :success
valuation_data = JSON.parse(response.body)
# Basic fields
assert_equal entry.id, valuation_data["id"]
assert valuation_data.key?("id")
assert valuation_data.key?("date")
assert valuation_data.key?("amount")
assert valuation_data.key?("currency")
assert valuation_data.key?("kind")
assert valuation_data.key?("created_at")
assert valuation_data.key?("updated_at")
# Account information
assert valuation_data.key?("account")
assert valuation_data["account"].key?("id")
assert valuation_data["account"].key?("name")
assert valuation_data["account"].key?("account_type")
# Optional fields should be present (even if nil)
assert valuation_data.key?("notes")
end
private
def api_headers(api_key)
{ "X-Api-Key" => api_key.display_key }
end
end