Files
sure/test/controllers/rules_controller_test.rb
Himank Dave 04549d80bf fix(rules): count blocked rule transactions (#1782)
* Add blocked count to rule run summary

* test(rules): cover rule run blocked counts

* fix(rules): derive blocked count from modified rows

Blocked rule transactions are the processed rows that were not modified. This keeps the displayed queued / processed / modified / blocked summary aligned when a run has already processed all matching rows but some were skipped by enrichment locks.

* fix(rules): count processed rows for rule jobs

Synchronous rule actions return the number of rows they modified, but rule-run processed counts should represent the number of matched transactions the job attempted to process. Using queued matches for processed preserves the distinction between processed and modified rows, which lets locked manual edits appear as blocked instead of making processed collapse to modified.

This changes RuleJob counter semantics, so it was committed separately from the derived blocked-count display change.
2026-05-14 21:56:49 +02:00

246 lines
7.2 KiB
Ruby

require "test_helper"
class RulesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in @user = users(:family_admin)
end
test "should get new" do
get new_rule_url(resource_type: "transaction")
assert_response :success
end
test "should get new with pre-filled name and action" do
category = categories(:food_and_drink)
get new_rule_url(
resource_type: "transaction",
name: "Starbucks",
action_type: "set_transaction_category",
action_value: category.id
)
assert_response :success
assert_select "input[name='rule[name]'][value='Starbucks']"
assert_select "input[name*='[value]'][value='Starbucks']"
assert_select "select[name*='[condition_type]'] option[selected][value='transaction_name']"
assert_select "select[name*='[action_type]'] option[selected][value='set_transaction_category']"
assert_select "select[name*='[value]'] option[selected][value='#{category.id}']"
end
test "should get edit" do
get edit_rule_url(rules(:one))
assert_response :success
end
# "Set all transactions with a name like 'starbucks' and an amount between 20 and 40 to the 'food and drink' category"
test "creates rule with nested conditions" do
post rules_url, params: {
rule: {
effective_date: 30.days.ago.to_date,
resource_type: "transaction",
conditions_attributes: {
"0" => {
condition_type: "transaction_name",
operator: "like",
value: "starbucks"
},
"1" => {
condition_type: "compound",
operator: "and",
sub_conditions_attributes: {
"0" => {
condition_type: "transaction_amount",
operator: ">",
value: 20
},
"1" => {
condition_type: "transaction_amount",
operator: "<",
value: 40
}
}
}
},
actions_attributes: {
"0" => {
action_type: "set_transaction_category",
value: categories(:food_and_drink).id
}
}
}
}
rule = @user.family.rules.order("created_at DESC").first
# Rule
assert_equal "transaction", rule.resource_type
assert_not rule.active # Not active by default
assert_equal 30.days.ago.to_date, rule.effective_date
# Conditions assertions
assert_equal 2, rule.conditions.count
compound_condition = rule.conditions.find { |condition| condition.condition_type == "compound" }
assert_equal "compound", compound_condition.condition_type
assert_equal 2, compound_condition.sub_conditions.count
# Actions assertions
assert_equal 1, rule.actions.count
assert_equal "set_transaction_category", rule.actions.first.action_type
assert_equal categories(:food_and_drink).id, rule.actions.first.value
assert_redirected_to confirm_rule_url(rule, reload_on_close: true)
end
test "can update rule" do
rule = rules(:one)
assert_difference -> { Rule.count } => 0,
-> { Rule::Condition.count } => 1,
-> { Rule::Action.count } => 1 do
patch rule_url(rule), params: {
rule: {
active: false,
conditions_attributes: {
"0" => {
id: rule.conditions.first.id,
value: "new_value"
},
"1" => {
condition_type: "transaction_amount",
operator: ">",
value: 100
}
},
actions_attributes: {
"0" => {
id: rule.actions.first.id,
value: "new_value"
},
"1" => {
action_type: "set_transaction_tags",
value: tags(:one).id
}
}
}
}
end
rule.reload
assert_not rule.active
assert_equal "new_value", rule.conditions.order("created_at ASC").first.value
assert_equal "new_value", rule.actions.order("created_at ASC").first.value
assert_equal tags(:one).id, rule.actions.order("created_at ASC").last.value
assert_equal "100", rule.conditions.order("created_at ASC").last.value
assert_redirected_to rules_url
end
test "can destroy conditions and actions while editing" do
rule = rules(:one)
assert_equal 1, rule.conditions.count
assert_equal 1, rule.actions.count
patch rule_url(rule), params: {
rule: {
conditions_attributes: {
"0" => { id: rule.conditions.first.id, _destroy: true },
"1" => {
condition_type: "transaction_name",
operator: "like",
value: "new_condition"
}
},
actions_attributes: {
"0" => { id: rule.actions.first.id, _destroy: true },
"1" => {
action_type: "set_transaction_tags",
value: tags(:one).id
}
}
}
}
assert_redirected_to rules_url
rule.reload
assert_equal 1, rule.conditions.count
assert_equal 1, rule.actions.count
end
test "can destroy rule" do
rule = rules(:one)
assert_difference [ "Rule.count", "Rule::Condition.count", "Rule::Action.count" ], -1 do
delete rule_url(rule)
end
assert_redirected_to rules_url
end
test "index renders when rule has empty compound condition" do
malformed_rule = @user.family.rules.build(resource_type: "transaction")
malformed_rule.conditions.build(condition_type: "compound", operator: "and")
malformed_rule.actions.build(action_type: "exclude_transaction")
malformed_rule.save!
get rules_url
assert_response :success
assert_includes response.body, I18n.t("rules.no_condition")
end
test "index uses next valid condition when first compound condition is empty" do
rule = @user.family.rules.build(resource_type: "transaction")
rule.conditions.build(condition_type: "compound", operator: "and")
rule.conditions.build(condition_type: "transaction_name", operator: "like", value: "edge-case-name")
rule.actions.build(action_type: "exclude_transaction")
rule.save!
get rules_url
assert_response :success
assert_select "##{ActionView::RecordIdentifier.dom_id(rule)}" do
assert_select "span", text: /edge-case-name/
assert_select "span", text: /#{Regexp.escape(I18n.t("rules.no_condition"))}/, count: 0
assert_select "p", text: /and 1 more condition/, count: 0
end
end
test "index shows blocked count in recent runs summary" do
rule = rules(:one)
RuleRun.create!(
rule: rule,
execution_type: "manual",
status: "success",
transactions_queued: 10,
transactions_processed: 7,
transactions_modified: 4,
pending_jobs_count: 0,
executed_at: Time.current
)
get rules_url
assert_response :success
assert_select "th", text: /Queued\s+Processed\s+Modified\s+Blocked/
assert_select "td", text: "10 / 7 / 4 / 3"
end
test "should get confirm_all" do
get confirm_all_rules_url
assert_response :success
end
test "apply_all enqueues job and redirects" do
assert_enqueued_with(job: ApplyAllRulesJob) do
post apply_all_rules_url
end
assert_redirected_to rules_url
end
end