mirror of
https://github.com/we-promise/sure.git
synced 2026-05-07 21:04:12 +00:00
* feat(api): expose valuation history index * fix(api): hide valuation exception details * fix(api): reuse eager-loaded valuation entries * fix(api): tighten valuation index contracts * fix(api): scope valuation filter errors * docs(api): nest valuation account filter format * Fix merge conflict mistakes --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <jjmata@jjmata.com>
300 lines
8.8 KiB
Ruby
300 lines
8.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" ],
|
|
source: "web",
|
|
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_valuations_url, headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
assert response_data.key?("valuations")
|
|
assert response_data.key?("pagination")
|
|
assert response_data["valuations"].is_a?(Array)
|
|
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_valuations_url, headers: api_headers(@read_only_api_key)
|
|
assert_response :success
|
|
end
|
|
|
|
test "should filter index by account_id" do
|
|
get api_v1_valuations_url,
|
|
params: { account_id: @account.id },
|
|
headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
response_data["valuations"].each do |valuation|
|
|
assert_equal @account.id, valuation["account"]["id"]
|
|
end
|
|
end
|
|
|
|
test "should filter index by date range" do
|
|
entry = @valuation.entry
|
|
|
|
get api_v1_valuations_url,
|
|
params: { start_date: entry.date, end_date: entry.date },
|
|
headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
assert_includes response_data["valuations"].map { |valuation| valuation["id"] }, entry.id
|
|
response_data["valuations"].each do |valuation|
|
|
valuation_date = Date.iso8601(valuation["date"])
|
|
assert_equal entry.date, valuation_date
|
|
end
|
|
end
|
|
|
|
test "should reject index with invalid date filter" do
|
|
get api_v1_valuations_url,
|
|
params: { start_date: "04/30/2026" },
|
|
headers: api_headers(@api_key)
|
|
assert_response :unprocessable_entity
|
|
|
|
response_data = JSON.parse(response.body)
|
|
assert_equal "validation_failed", response_data["error"]
|
|
end
|
|
|
|
test "should reject index with malformed account_id filter" do
|
|
get api_v1_valuations_url,
|
|
params: { account_id: "not-a-uuid" },
|
|
headers: api_headers(@api_key)
|
|
assert_response :unprocessable_entity
|
|
|
|
response_data = JSON.parse(response.body)
|
|
assert_equal "validation_failed", response_data["error"]
|
|
assert_equal "account_id must be a valid UUID", response_data["message"]
|
|
end
|
|
|
|
test "should not expose internal index errors" do
|
|
Api::V1::ValuationsController.any_instance.stubs(:safe_page_param).raises(StandardError, "database password leaked")
|
|
|
|
get api_v1_valuations_url, headers: api_headers(@api_key)
|
|
assert_response :internal_server_error
|
|
|
|
response_data = JSON.parse(response.body)
|
|
assert_equal "internal_server_error", response_data["error"]
|
|
assert_equal "An unexpected error occurred", response_data["message"]
|
|
assert_not_includes response.body, "database password leaked"
|
|
end
|
|
|
|
test "should reject index without API key" do
|
|
get api_v1_valuations_url
|
|
assert_response :unauthorized
|
|
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.plain_key }
|
|
end
|
|
end
|