mirror of
https://github.com/we-promise/sure.git
synced 2026-07-12 12:55:20 +00:00
* Update trades api with support for additional types * rubocop fixes * fix missing amount validation for interest type * define missing schema reference * fix test api_headers to use display_key per guidelines * expand test coverage * replaced duplicate JSON response blocks with helper method * Add DB assertions to linked transfer test and fix invalid date test * update brakeman.ignore fingerpint for refactored code * Update the Brakeman ignore note to document validation for newly permitted keys * fix API key auth in Minitest test to follow correct pattern * update required in trades rswag spec to match the minimum fields that apply to all types * extract dividend handling from build_investment_trade_params to dedicated method * adjust response format to use the existing jbuilder views for Transfers and Transactions * normalize type before passing to create form * validate amount as a positive numeric value + tests * rubocop fixes * Add missing Trades API test coverage and docs - Add Minitest tests for withdrawal (422, transfer linking), interest (explicit ticker), and dividend update - Add rswag 401/403/404/422 response docs for create, update, destroy - Regenerate docs/api/openapi.yaml * Update Security.find line reference in brakeman.ignore note * Mark TransactionResponse account_type as nullable in rswag docs
848 lines
25 KiB
Ruby
848 lines
25 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "test_helper"
|
|
|
|
class Api::V1::TradesControllerTest < ActionDispatch::IntegrationTest
|
|
setup do
|
|
@user = users(:family_admin)
|
|
@user.api_keys.active.destroy_all
|
|
@investment_account = accounts(:investment)
|
|
@read_write_api_key = nil
|
|
@read_only_api_key = nil
|
|
end
|
|
|
|
test "create dividend with security returns 201" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "dividend",
|
|
date: Date.current,
|
|
amount: 25.50,
|
|
currency: "USD",
|
|
ticker: "AAPL|XNAS"
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :created
|
|
body = JSON.parse(response.body)
|
|
assert body["id"].present?
|
|
assert_equal "Dividend: AAPL", body["name"]
|
|
assert_equal "Dividend", body["investment_activity_label"]
|
|
assert_equal 0, body["qty"].to_i
|
|
end
|
|
|
|
test "create dividend without security returns 422" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "dividend",
|
|
date: Date.current,
|
|
amount: 25.50
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :unprocessable_entity
|
|
end
|
|
|
|
test "create dividend without amount returns 422" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "dividend",
|
|
date: Date.current,
|
|
ticker: "AAPL|XNAS"
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :unprocessable_entity
|
|
end
|
|
|
|
test "create buy trade returns 201" do
|
|
security = Security.create!(ticker: "TEST", name: "Test Security", country_code: "US")
|
|
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "buy",
|
|
date: Date.current,
|
|
qty: 10,
|
|
price: 100.00,
|
|
currency: "USD",
|
|
security_id: security.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :created
|
|
body = JSON.parse(response.body)
|
|
assert body["id"].present?
|
|
assert_equal "Buy", body["investment_activity_label"]
|
|
end
|
|
|
|
test "create sell trade returns 201" do
|
|
security = Security.create!(ticker: "TEST", name: "Test Security", country_code: "US")
|
|
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "sell",
|
|
date: Date.current,
|
|
qty: 10,
|
|
price: 100.00,
|
|
currency: "USD",
|
|
security_id: security.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :created
|
|
body = JSON.parse(response.body)
|
|
assert body["id"].present?
|
|
assert_equal "Sell", body["investment_activity_label"]
|
|
end
|
|
|
|
test "invalid type returns 422" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "invalid",
|
|
date: Date.current
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :unprocessable_entity
|
|
end
|
|
|
|
test "create deposit returns 201" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "deposit",
|
|
date: Date.current,
|
|
amount: 175.25,
|
|
currency: "USD"
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :created
|
|
body = JSON.parse(response.body)
|
|
assert body["id"].present?
|
|
assert_match(/Deposit to/, body["name"])
|
|
end
|
|
|
|
test "create withdrawal returns 201" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "withdrawal",
|
|
date: Date.current,
|
|
amount: 100.00,
|
|
currency: "USD"
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :created
|
|
body = JSON.parse(response.body)
|
|
assert body["id"].present?
|
|
assert_match(/Withdrawal/, body["name"])
|
|
end
|
|
|
|
test "create withdrawal without amount returns 422" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "withdrawal",
|
|
date: Date.current
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :unprocessable_entity
|
|
end
|
|
|
|
test "create withdrawal with transfer_account_id creates linked transfer" do
|
|
depository = accounts(:depository)
|
|
|
|
assert_difference "Transfer.count", 1 do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "withdrawal",
|
|
date: Date.current,
|
|
amount: 500.00,
|
|
currency: "USD",
|
|
transfer_account_id: depository.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
end
|
|
|
|
assert_response :created
|
|
body = JSON.parse(response.body)
|
|
assert body["id"].present?
|
|
assert body["outflow_transaction"]["account"]["id"].present?
|
|
assert body["outflow_transaction"]["account"]["account_type"].present?
|
|
|
|
transfer = Transfer.joins(outflow_transaction: :entry)
|
|
.where(entries: { account_id: @investment_account.id })
|
|
.last
|
|
|
|
assert transfer, "Transfer should exist linking accounts"
|
|
assert_equal depository.id, transfer.inflow_transaction.entry.account_id, "Inflow should be to depository"
|
|
assert_equal @investment_account.id, transfer.outflow_transaction.entry.account_id, "Outflow should come from investment"
|
|
end
|
|
|
|
test "create deposit without amount returns 422" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "deposit",
|
|
date: Date.current
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :unprocessable_entity
|
|
end
|
|
|
|
test "create deposit with transfer_account_id creates linked transfer" do
|
|
depository = accounts(:depository)
|
|
|
|
assert_difference "Transfer.count", 1 do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "deposit",
|
|
date: Date.current,
|
|
amount: 500.00,
|
|
currency: "USD",
|
|
transfer_account_id: depository.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
end
|
|
|
|
assert_response :created
|
|
body = JSON.parse(response.body)
|
|
assert body["id"].present?
|
|
assert body["inflow_transaction"]["account"]["id"].present?
|
|
assert body["inflow_transaction"]["account"]["account_type"].present?
|
|
|
|
transfer = Transfer.joins(inflow_transaction: :entry)
|
|
.where(entries: { account_id: @investment_account.id })
|
|
.last
|
|
|
|
assert transfer, "Transfer should exist linking accounts"
|
|
assert_equal depository.id, transfer.outflow_transaction.entry.account_id, "Outflow should be from depository"
|
|
assert_equal @investment_account.id, transfer.inflow_transaction.entry.account_id, "Inflow should go to investment"
|
|
end
|
|
|
|
test "create interest returns 201" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "interest",
|
|
date: Date.current,
|
|
amount: 25.00,
|
|
currency: "USD"
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :created
|
|
body = JSON.parse(response.body)
|
|
assert body["id"].present?
|
|
assert_equal "Interest", body["investment_activity_label"]
|
|
end
|
|
|
|
test "create interest with explicit ticker returns 201" do
|
|
security = Security.create!(ticker: "INTSEC", name: "Interest Security", country_code: "US")
|
|
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "interest",
|
|
date: Date.current,
|
|
amount: 25.00,
|
|
currency: "USD",
|
|
security_id: security.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :created
|
|
body = JSON.parse(response.body)
|
|
assert body["id"].present?
|
|
assert_equal "Interest", body["investment_activity_label"]
|
|
end
|
|
|
|
test "create interest without amount returns 422" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "interest",
|
|
date: Date.current
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :unprocessable_entity
|
|
end
|
|
|
|
test "create requires read_write scope" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "buy",
|
|
date: Date.current,
|
|
qty: 10,
|
|
price: 100
|
|
} },
|
|
headers: api_headers(read_only_api_key)
|
|
|
|
assert_response :forbidden
|
|
end
|
|
|
|
test "should reject create without API key" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "buy",
|
|
date: Date.current,
|
|
qty: 10,
|
|
price: 100
|
|
} }
|
|
|
|
assert_response :unauthorized
|
|
end
|
|
|
|
test "should return 404 for unknown account_id" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: 999999,
|
|
type: "buy",
|
|
date: Date.current,
|
|
qty: 10,
|
|
price: 100,
|
|
security_id: Security.create!(ticker: "TEST", name: "Test", country_code: "US").id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :not_found
|
|
end
|
|
|
|
test "should return 422 for invalid date format" do
|
|
security = Security.create!(ticker: "INVDATE", name: "Invalid Date Security", country_code: "US")
|
|
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "buy",
|
|
date: "invalid-date",
|
|
qty: 10,
|
|
price: 100,
|
|
security_id: security.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :unprocessable_entity
|
|
body = JSON.parse(response.body)
|
|
assert body["errors"].any? { |e| e.downcase.include?("date") }, "Expected date-related error, got: #{body["errors"].inspect}"
|
|
end
|
|
|
|
# INDEX action tests
|
|
test "should get index with valid API key" do
|
|
get api_v1_trades_url, headers: api_headers(read_write_api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
assert response_data.key?("trades")
|
|
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_trades_url, headers: api_headers(read_only_api_key)
|
|
assert_response :success
|
|
end
|
|
|
|
test "should filter trades by account_id" do
|
|
get api_v1_trades_url,
|
|
params: { account_id: @investment_account.id },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
response_data["trades"].each do |trade|
|
|
assert_equal @investment_account.id, trade["account"]["id"]
|
|
end
|
|
end
|
|
|
|
test "should filter trades by date range" do
|
|
start_date = 1.year.ago.to_date
|
|
end_date = Date.current
|
|
|
|
get api_v1_trades_url,
|
|
params: { start_date: start_date, end_date: end_date },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :success
|
|
end
|
|
|
|
test "should reject index request without API key" do
|
|
get api_v1_trades_url
|
|
assert_response :unauthorized
|
|
end
|
|
|
|
test "should reject index request with invalid API key" do
|
|
get api_v1_trades_url, headers: { "X-Api-Key" => "invalid-key" }
|
|
assert_response :unauthorized
|
|
end
|
|
|
|
test "should reject index request with invalid date format" do
|
|
get api_v1_trades_url,
|
|
params: { start_date: "invalid" },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :unprocessable_entity
|
|
end
|
|
|
|
# SHOW action tests
|
|
test "should show trade with valid API key" do
|
|
security = Security.create!(ticker: "SHOW", name: "Show Security", country_code: "US")
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "buy",
|
|
date: Date.current,
|
|
qty: 5,
|
|
price: 100,
|
|
currency: "USD",
|
|
security_id: security.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :created
|
|
trade_id = JSON.parse(response.body)["id"]
|
|
|
|
get api_v1_trade_url(trade_id), headers: api_headers(read_write_api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
assert_equal trade_id, response_data["id"]
|
|
assert response_data.key?("date")
|
|
assert response_data.key?("account")
|
|
end
|
|
|
|
test "should show trade with read-only API key" do
|
|
security = Security.create!(ticker: "SHOR", name: "Show RO Security", country_code: "US")
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "buy",
|
|
date: Date.current,
|
|
qty: 5,
|
|
price: 100,
|
|
currency: "USD",
|
|
security_id: security.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :created
|
|
trade_id = JSON.parse(response.body)["id"]
|
|
|
|
get api_v1_trade_url(trade_id), headers: api_headers(read_only_api_key)
|
|
assert_response :success
|
|
end
|
|
|
|
test "should return 404 for non-existent trade" do
|
|
get api_v1_trade_url(999999), headers: api_headers(read_write_api_key)
|
|
assert_response :not_found
|
|
end
|
|
|
|
test "should reject show request without API key" do
|
|
security = Security.create!(ticker: "SHON", name: "Show No Auth", country_code: "US")
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "buy",
|
|
date: Date.current,
|
|
qty: 5,
|
|
price: 100,
|
|
currency: "USD",
|
|
security_id: security.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :created
|
|
trade_id = JSON.parse(response.body)["id"]
|
|
|
|
get api_v1_trade_url(trade_id)
|
|
assert_response :unauthorized
|
|
end
|
|
|
|
# UPDATE action tests
|
|
test "should update trade with valid parameters" do
|
|
security = Security.create!(ticker: "UPD", name: "Update Security", country_code: "US")
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "buy",
|
|
date: Date.current,
|
|
qty: 5,
|
|
price: 100,
|
|
currency: "USD",
|
|
security_id: security.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :created
|
|
trade_id = JSON.parse(response.body)["id"]
|
|
|
|
update_params = {
|
|
trade: {
|
|
notes: "Updated notes"
|
|
}
|
|
}
|
|
|
|
put api_v1_trade_url(trade_id),
|
|
params: update_params,
|
|
headers: api_headers(read_write_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
|
|
security = Security.create!(ticker: "UPDRO", name: "Update RO Security", country_code: "US")
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "buy",
|
|
date: Date.current,
|
|
qty: 5,
|
|
price: 100,
|
|
currency: "USD",
|
|
security_id: security.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :created
|
|
trade_id = JSON.parse(response.body)["id"]
|
|
|
|
put api_v1_trade_url(trade_id),
|
|
params: { trade: { notes: "Test" } },
|
|
headers: api_headers(read_only_api_key)
|
|
assert_response :forbidden
|
|
end
|
|
|
|
test "should reject update for non-existent trade" do
|
|
put api_v1_trade_url(999999),
|
|
params: { trade: { notes: "Test" } },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :not_found
|
|
end
|
|
|
|
test "should reject update without API key" do
|
|
security = Security.create!(ticker: "UPDNO", name: "Update No Auth", country_code: "US")
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "buy",
|
|
date: Date.current,
|
|
qty: 5,
|
|
price: 100,
|
|
currency: "USD",
|
|
security_id: security.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :created
|
|
trade_id = JSON.parse(response.body)["id"]
|
|
|
|
put api_v1_trade_url(trade_id), params: { trade: { notes: "Test" } }
|
|
assert_response :unauthorized
|
|
end
|
|
|
|
test "should reject update with invalid date format" do
|
|
security = Security.create!(ticker: "UPDID", name: "Update Invalid Date", country_code: "US")
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "buy",
|
|
date: Date.current,
|
|
qty: 5,
|
|
price: 100,
|
|
currency: "USD",
|
|
security_id: security.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :created
|
|
trade_id = JSON.parse(response.body)["id"]
|
|
|
|
put api_v1_trade_url(trade_id),
|
|
params: { trade: { date: "invalid" } },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :unprocessable_entity
|
|
end
|
|
|
|
test "should update dividend trade with valid parameters" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "dividend",
|
|
date: Date.current,
|
|
amount: 25.50,
|
|
currency: "USD",
|
|
ticker: "AAPL|XNAS"
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :created
|
|
trade_id = JSON.parse(response.body)["id"]
|
|
|
|
update_params = {
|
|
trade: {
|
|
notes: "Updated dividend notes"
|
|
}
|
|
}
|
|
|
|
put api_v1_trade_url(trade_id),
|
|
params: update_params,
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
assert_equal "Updated dividend notes", response_data["notes"]
|
|
end
|
|
|
|
# DESTROY action tests
|
|
test "should destroy trade" do
|
|
security = Security.create!(ticker: "DEL", name: "Delete Security", country_code: "US")
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "buy",
|
|
date: Date.current,
|
|
qty: 5,
|
|
price: 100,
|
|
currency: "USD",
|
|
security_id: security.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :created
|
|
trade_id = JSON.parse(response.body)["id"]
|
|
|
|
delete api_v1_trade_url(trade_id), headers: api_headers(read_write_api_key)
|
|
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
|
|
security = Security.create!(ticker: "DELRO", name: "Delete RO Security", country_code: "US")
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "buy",
|
|
date: Date.current,
|
|
qty: 5,
|
|
price: 100,
|
|
currency: "USD",
|
|
security_id: security.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :created
|
|
trade_id = JSON.parse(response.body)["id"]
|
|
|
|
delete api_v1_trade_url(trade_id), headers: api_headers(read_only_api_key)
|
|
assert_response :forbidden
|
|
end
|
|
|
|
test "should reject destroy for non-existent trade" do
|
|
delete api_v1_trade_url(999999), headers: api_headers(read_write_api_key)
|
|
assert_response :not_found
|
|
end
|
|
|
|
test "should reject destroy without API key" do
|
|
security = Security.create!(ticker: "DELNO", name: "Delete No Auth", country_code: "US")
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "buy",
|
|
date: Date.current,
|
|
qty: 5,
|
|
price: 100,
|
|
currency: "USD",
|
|
security_id: security.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :created
|
|
trade_id = JSON.parse(response.body)["id"]
|
|
|
|
delete api_v1_trade_url(trade_id)
|
|
assert_response :unauthorized
|
|
end
|
|
|
|
# Non-numeric amount returns 422
|
|
test "create deposit with non-numeric amount returns 422" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "deposit",
|
|
date: Date.current,
|
|
amount: "abc",
|
|
currency: "USD"
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :unprocessable_entity
|
|
body = JSON.parse(response.body)
|
|
assert_equal "Amount must be a valid number", body["message"]
|
|
end
|
|
|
|
test "create deposit with zero amount returns 422" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "deposit",
|
|
date: Date.current,
|
|
amount: 0,
|
|
currency: "USD"
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :unprocessable_entity
|
|
body = JSON.parse(response.body)
|
|
assert_equal "Amount must be a valid number", body["message"]
|
|
end
|
|
|
|
test "create deposit with negative amount returns 422" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "deposit",
|
|
date: Date.current,
|
|
amount: -50.00,
|
|
currency: "USD"
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :unprocessable_entity
|
|
body = JSON.parse(response.body)
|
|
assert_equal "Amount must be a valid number", body["message"]
|
|
end
|
|
|
|
test "create interest with non-numeric amount returns 422" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "interest",
|
|
date: Date.current,
|
|
amount: "abc",
|
|
currency: "USD"
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :unprocessable_entity
|
|
body = JSON.parse(response.body)
|
|
assert_equal "Amount must be a valid number", body["message"]
|
|
end
|
|
|
|
test "create interest with zero amount returns 422" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "interest",
|
|
date: Date.current,
|
|
amount: 0,
|
|
currency: "USD"
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :unprocessable_entity
|
|
body = JSON.parse(response.body)
|
|
assert_equal "Amount must be a valid number", body["message"]
|
|
end
|
|
|
|
test "create dividend with non-numeric amount returns 422" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "dividend",
|
|
date: Date.current,
|
|
amount: "abc",
|
|
currency: "USD",
|
|
ticker: "AAPL|XNAS"
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :unprocessable_entity
|
|
body = JSON.parse(response.body)
|
|
assert_equal "Amount must be a valid number", body["message"]
|
|
end
|
|
|
|
test "create dividend with zero amount returns 422" do
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "dividend",
|
|
date: Date.current,
|
|
amount: 0,
|
|
currency: "USD",
|
|
ticker: "AAPL|XNAS"
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
|
|
assert_response :unprocessable_entity
|
|
body = JSON.parse(response.body)
|
|
assert_equal "Amount must be a valid number", body["message"]
|
|
end
|
|
|
|
test "trade JSON should have expected structure" do
|
|
security = Security.create!(ticker: "JSP", name: "JSON Structure Security", country_code: "US")
|
|
post "/api/v1/trades",
|
|
params: { trade: {
|
|
account_id: @investment_account.id,
|
|
type: "buy",
|
|
date: Date.current,
|
|
qty: 5,
|
|
price: 100,
|
|
currency: "USD",
|
|
security_id: security.id
|
|
} },
|
|
headers: api_headers(read_write_api_key)
|
|
assert_response :created
|
|
trade_id = JSON.parse(response.body)["id"]
|
|
|
|
get api_v1_trade_url(trade_id), headers: api_headers(read_write_api_key)
|
|
assert_response :success
|
|
|
|
trade_data = JSON.parse(response.body)
|
|
assert trade_data.key?("id")
|
|
assert trade_data.key?("date")
|
|
assert trade_data.key?("account")
|
|
assert trade_data["account"].key?("id")
|
|
assert trade_data["account"].key?("name")
|
|
assert trade_data["account"].key?("account_type")
|
|
assert trade_data.key?("notes")
|
|
end
|
|
|
|
private
|
|
|
|
def read_write_api_key
|
|
@read_write_api_key ||= ApiKey.create!(
|
|
user: @user,
|
|
name: "Test RW Key",
|
|
key: ApiKey.generate_secure_key,
|
|
scopes: %w[read_write],
|
|
source: "web"
|
|
).tap do |key|
|
|
Redis.new.del("api_rate_limit:#{key.id}")
|
|
end
|
|
end
|
|
|
|
def read_only_api_key
|
|
@read_only_api_key ||= ApiKey.create!(
|
|
user: @user,
|
|
name: "Test RO Key",
|
|
key: ApiKey.generate_secure_key,
|
|
scopes: %w[read],
|
|
source: "mobile"
|
|
).tap do |key|
|
|
Redis.new.del("api_rate_limit:#{key.id}")
|
|
end
|
|
end
|
|
|
|
def api_headers(api_key)
|
|
{ "X-Api-Key" => api_key.plain_key }
|
|
end
|
|
end
|