mirror of
https://github.com/we-promise/sure.git
synced 2026-08-04 08:02:15 +00:00
Model-level:
- Category::GroupTest (new): for() grouping and the new select_options
helper (order + indent labels).
- CategoryTest: alphabetically_by_hierarchy scope ordering.
- Rule::ConditionFilter::TransactionCategoryTest (new)
- Rule::ActionExecutor::SetTransactionCategoryTest (new)
- Import::CategoryMappingTest (new): grouping + 'Add as new category'
still prepends correctly.
Controller/integration-level (asserting actual rendered HTML order):
- SplitsControllerTest: category combobox data-value ordering.
- Transactions::CategorizesControllerTest: bulk-categorize <select>
option ordering.
- TransactionsControllerTest:
- search filter checkbox ordering (q[categories][])
- new-transaction DS::Select category ordering (via trigger id +
ancestor traversal)
- new-transaction account select renders a search box
All new/modified test files verified with 'ruby -c' (syntax) and
cross-checked fixture names, family scoping, route helpers, and field
names against the actual fixtures/routes/views. Ruby/Bundler network
access to rubygems.org is unavailable in this sandbox, so the suite
itself has not been executed — run 'bin/rails test' before merging.
299 lines
10 KiB
Ruby
299 lines
10 KiB
Ruby
require "test_helper"
|
|
|
|
class Transactions::CategorizesControllerTest < ActionDispatch::IntegrationTest
|
|
include EntriesTestHelper
|
|
|
|
setup do
|
|
sign_in @user = users(:family_admin)
|
|
@family = @user.family
|
|
@account = accounts(:depository)
|
|
@category = categories(:food_and_drink)
|
|
# Clear entries for isolation
|
|
@family.accounts.each { |a| a.entries.delete_all }
|
|
end
|
|
|
|
# GET /transactions/categorize
|
|
|
|
test "show redirects with notice when nothing to categorize" do
|
|
get transactions_categorize_url
|
|
assert_redirected_to transactions_url
|
|
assert_match "categorized", flash[:notice]
|
|
end
|
|
|
|
test "show renders wizard when uncategorized transactions exist" do
|
|
create_transaction(account: @account, name: "Starbucks")
|
|
get transactions_categorize_url
|
|
assert_response :success
|
|
end
|
|
|
|
test "show groups subcategories immediately after their parent in the category select" do
|
|
create_transaction(account: @account, name: "Starbucks")
|
|
get transactions_categorize_url
|
|
|
|
assert_response :success
|
|
|
|
doc = Nokogiri::HTML::Document.parse(response.body)
|
|
option_values = doc.css("select[name='category_id'] option").map { |node| node["value"] }
|
|
|
|
parent_index = option_values.index(categories(:food_and_drink).id)
|
|
child_index = option_values.index(categories(:subcategory).id)
|
|
|
|
assert_not_nil parent_index
|
|
assert_not_nil child_index
|
|
assert_equal parent_index + 1, child_index
|
|
end
|
|
|
|
test "show renders full dates so multi-year lists are unambiguous" do
|
|
create_transaction(account: @account, name: "Starbucks", date: Date.new(2024, 7, 8))
|
|
|
|
get transactions_categorize_url
|
|
|
|
assert_response :success
|
|
# format_date uses the family's date_format preference, every variant of
|
|
# which includes the year; the previous :short format ("%b %d") did not,
|
|
# making rows ambiguous when the uncategorized list spans years.
|
|
expected = Date.new(2024, 7, 8).strftime(@family.date_format)
|
|
assert_match expected, response.body
|
|
end
|
|
|
|
test "show renders the first group at position 0" do
|
|
2.times { create_transaction(account: @account, name: "Netflix") }
|
|
3.times { create_transaction(account: @account, name: "Starbucks") }
|
|
|
|
get transactions_categorize_url(position: 0)
|
|
|
|
assert_response :success
|
|
assert_select "h2", text: "Starbucks"
|
|
end
|
|
|
|
test "show at position 1 skips first group" do
|
|
3.times { create_transaction(account: @account, name: "Starbucks") }
|
|
2.times { create_transaction(account: @account, name: "Netflix") }
|
|
|
|
get transactions_categorize_url(position: 1)
|
|
|
|
assert_response :success
|
|
assert_select "h2", text: "Netflix"
|
|
end
|
|
|
|
test "show redirects when position exceeds available groups" do
|
|
create_transaction(account: @account, name: "Starbucks")
|
|
|
|
get transactions_categorize_url(position: 99)
|
|
|
|
assert_redirected_to transactions_url
|
|
end
|
|
|
|
test "requires authentication" do
|
|
sign_out
|
|
get transactions_categorize_url
|
|
assert_redirected_to new_session_url
|
|
end
|
|
|
|
# Account sharing authorization
|
|
|
|
test "show only groups entries from accounts accessible to the user" do
|
|
accessible_account = accounts(:depository) # shared with family_member (full_control)
|
|
inaccessible_account = accounts(:investment) # not shared with family_member
|
|
|
|
create_transaction(account: accessible_account, name: "Starbucks")
|
|
create_transaction(account: inaccessible_account, name: "Starbucks")
|
|
|
|
sign_in users(:family_member)
|
|
get transactions_categorize_url(position: 0)
|
|
|
|
assert_response :success
|
|
# Only 1 entry should appear in the group — the inaccessible account's entry is hidden
|
|
assert_select "input[name='entry_ids[]']", count: 1
|
|
end
|
|
|
|
test "create does not categorize entries from inaccessible accounts" do
|
|
inaccessible_account = accounts(:investment) # not shared with family_member
|
|
entry = create_transaction(account: inaccessible_account, name: "Starbucks")
|
|
|
|
sign_in users(:family_member)
|
|
post transactions_categorize_url,
|
|
params: {
|
|
position: 0,
|
|
grouping_key: "Starbucks",
|
|
entry_ids: [ entry.id ],
|
|
all_entry_ids: [ entry.id ],
|
|
category_id: @category.id
|
|
},
|
|
headers: { "Accept" => "text/vnd.turbo-stream.html" }
|
|
|
|
assert_nil entry.transaction.reload.category
|
|
end
|
|
|
|
test "assign_entry does not categorize an entry from an inaccessible account" do
|
|
inaccessible_account = accounts(:investment) # not shared with family_member
|
|
entry = create_transaction(account: inaccessible_account, name: "Starbucks")
|
|
|
|
sign_in users(:family_member)
|
|
patch assign_entry_transactions_categorize_url, params: {
|
|
entry_id: entry.id,
|
|
category_id: @category.id,
|
|
position: 0,
|
|
all_entry_ids: [ entry.id ]
|
|
}
|
|
|
|
assert_response :not_found
|
|
assert_nil entry.transaction.reload.category
|
|
end
|
|
|
|
# GET /transactions/categorize/preview_rule
|
|
|
|
test "preview_rule returns matching entries for a filter" do
|
|
create_transaction(account: @account, name: "Amazon Prime")
|
|
create_transaction(account: @account, name: "Amazon Music")
|
|
create_transaction(account: @account, name: "Starbucks")
|
|
|
|
get preview_rule_transactions_categorize_url(filter: "Amazon"),
|
|
headers: { "Accept" => "text/vnd.turbo-stream.html" }
|
|
|
|
assert_response :success
|
|
assert_includes response.body, "Amazon Prime"
|
|
assert_includes response.body, "Amazon Music"
|
|
assert_not_includes response.body, "Starbucks"
|
|
end
|
|
|
|
test "preview_rule returns empty list for blank filter" do
|
|
create_transaction(account: @account, name: "Amazon")
|
|
|
|
get preview_rule_transactions_categorize_url(filter: ""),
|
|
headers: { "Accept" => "text/vnd.turbo-stream.html" }
|
|
|
|
assert_response :success
|
|
assert_not_includes response.body, "Amazon"
|
|
end
|
|
|
|
test "preview_rule requires authentication" do
|
|
sign_out
|
|
get preview_rule_transactions_categorize_url(filter: "Amazon")
|
|
assert_redirected_to new_session_url
|
|
end
|
|
|
|
private
|
|
|
|
def sign_out
|
|
@user.sessions.each { |s| delete session_path(s) }
|
|
end
|
|
|
|
# POST /transactions/categorize
|
|
|
|
test "create categorizes selected entries and returns redirect stream when all assigned" do
|
|
entry = create_transaction(account: @account, name: "Starbucks")
|
|
|
|
post transactions_categorize_url,
|
|
params: {
|
|
position: 0,
|
|
grouping_key: "Starbucks",
|
|
entry_ids: [ entry.id ],
|
|
all_entry_ids: [ entry.id ],
|
|
category_id: @category.id
|
|
},
|
|
headers: { "Accept" => "text/vnd.turbo-stream.html" }
|
|
|
|
assert_response :success
|
|
assert_equal @category, entry.transaction.reload.category
|
|
assert_includes response.body, "action=\"redirect\""
|
|
end
|
|
|
|
test "create removes assigned rows and replaces remaining when partial assignment" do
|
|
entry1 = create_transaction(account: @account, name: "Starbucks")
|
|
entry2 = create_transaction(account: @account, name: "Starbucks")
|
|
|
|
post transactions_categorize_url,
|
|
params: {
|
|
position: 0,
|
|
grouping_key: "Starbucks",
|
|
entry_ids: [ entry1.id ],
|
|
all_entry_ids: [ entry1.id, entry2.id ],
|
|
category_id: @category.id
|
|
},
|
|
headers: { "Accept" => "text/vnd.turbo-stream.html" }
|
|
|
|
assert_response :success
|
|
assert_equal @category, entry1.transaction.reload.category
|
|
assert_nil entry2.transaction.reload.category
|
|
# Remove stream for categorized entry
|
|
assert_includes response.body, "categorize_entry_#{entry1.id}"
|
|
# Replace stream for remaining entry (re-checked)
|
|
assert_includes response.body, "categorize_entry_#{entry2.id}"
|
|
# No redirect stream — still in the group
|
|
assert_not_includes response.body, "action=\"redirect\""
|
|
end
|
|
|
|
test "create with create_rule param creates rule with name and type conditions" do
|
|
entry = create_transaction(account: @account, name: "Netflix", amount: 15)
|
|
|
|
assert_difference "@family.rules.count", 1 do
|
|
post transactions_categorize_url,
|
|
params: {
|
|
position: 0,
|
|
grouping_key: "Netflix",
|
|
transaction_type: "expense",
|
|
entry_ids: [ entry.id ],
|
|
all_entry_ids: [ entry.id ],
|
|
category_id: @category.id,
|
|
create_rule: "1"
|
|
},
|
|
headers: { "Accept" => "text/vnd.turbo-stream.html" }
|
|
end
|
|
|
|
rule = @family.rules.find_by(name: "Netflix")
|
|
assert_not_nil rule
|
|
assert rule.active
|
|
assert rule.conditions.any? { |c| c.condition_type == "transaction_name" && c.value == "Netflix" }
|
|
assert rule.conditions.any? { |c| c.condition_type == "transaction_type" && c.value == "expense" }
|
|
end
|
|
|
|
test "create falls back to html redirect without turbo stream header" do
|
|
entry = create_transaction(account: @account, name: "Starbucks")
|
|
|
|
post transactions_categorize_url, params: {
|
|
position: 0,
|
|
grouping_key: "Starbucks",
|
|
entry_ids: [ entry.id ],
|
|
all_entry_ids: [ entry.id ],
|
|
category_id: @category.id
|
|
}
|
|
|
|
assert_redirected_to transactions_categorize_url(position: 0)
|
|
assert flash[:notice].present?
|
|
end
|
|
|
|
# PATCH /transactions/categorize/assign_entry
|
|
|
|
test "assign_entry categorizes single entry and returns remove stream" do
|
|
entry = create_transaction(account: @account, name: "Starbucks")
|
|
other = create_transaction(account: @account, name: "Starbucks")
|
|
|
|
patch assign_entry_transactions_categorize_url, params: {
|
|
entry_id: entry.id,
|
|
category_id: @category.id,
|
|
position: 0,
|
|
all_entry_ids: [ entry.id, other.id ]
|
|
}
|
|
|
|
assert_response :success
|
|
assert_equal @category, entry.transaction.reload.category
|
|
assert_includes response.body, "categorize_entry_#{entry.id}"
|
|
assert_not_includes response.body, "action=\"redirect\""
|
|
end
|
|
|
|
test "assign_entry returns redirect stream when last entry in group" do
|
|
entry = create_transaction(account: @account, name: "Starbucks")
|
|
|
|
patch assign_entry_transactions_categorize_url, params: {
|
|
entry_id: entry.id,
|
|
category_id: @category.id,
|
|
position: 0,
|
|
all_entry_ids: [ entry.id ]
|
|
}
|
|
|
|
assert_response :success
|
|
assert_includes response.body, "action=\"redirect\""
|
|
end
|
|
end
|