mirror of
https://github.com/we-promise/sure.git
synced 2026-05-11 14:45:01 +00:00
feat(api): expose rule run history (#1646)
* feat(api): expose rule run history * fix(api): address rule run review * fix(api): complete rule run review * test(api): cover unauthenticated rule run show * test(api): align rule run api key helper * Small Sonnet nit-pick --------- Co-authored-by: Juan José Mata <jjmata@jjmata.com>
This commit is contained in:
207
test/controllers/api/v1/rule_runs_controller_test.rb
Normal file
207
test/controllers/api/v1/rule_runs_controller_test.rb
Normal file
@@ -0,0 +1,207 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class Api::V1::RuleRunsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@user = users(:family_admin)
|
||||
@family = @user.family
|
||||
|
||||
@user.api_keys.active.destroy_all
|
||||
@api_key = ApiKey.create!(
|
||||
user: @user,
|
||||
name: "Test Read Key",
|
||||
scopes: [ "read" ],
|
||||
source: "web",
|
||||
display_key: "test_read_#{SecureRandom.hex(8)}"
|
||||
)
|
||||
@redis = Redis.new
|
||||
@redis.del("api_rate_limit:#{@api_key.id}")
|
||||
|
||||
@rule = @family.rules.build(
|
||||
name: "Coffee cleanup",
|
||||
resource_type: "transaction",
|
||||
active: true,
|
||||
effective_date: Date.new(2024, 1, 1)
|
||||
)
|
||||
@rule.conditions.build(condition_type: "transaction_name", operator: "like", value: "coffee")
|
||||
@rule.actions.build(action_type: "set_transaction_name", value: "Coffee")
|
||||
@rule.save!
|
||||
|
||||
@rule_run = @rule.rule_runs.create!(
|
||||
rule_name: @rule.name,
|
||||
execution_type: "manual",
|
||||
status: "success",
|
||||
transactions_queued: 10,
|
||||
transactions_processed: 10,
|
||||
transactions_modified: 4,
|
||||
pending_jobs_count: 0,
|
||||
executed_at: Time.zone.parse("2024-01-15 12:00:00")
|
||||
)
|
||||
@failed_rule_run = @rule.rule_runs.create!(
|
||||
rule_name: @rule.name,
|
||||
execution_type: "scheduled",
|
||||
status: "failed",
|
||||
transactions_queued: 5,
|
||||
transactions_processed: 2,
|
||||
transactions_modified: 0,
|
||||
pending_jobs_count: 0,
|
||||
executed_at: Time.zone.parse("2024-01-16 12:00:00"),
|
||||
error_message: "Rule failed"
|
||||
)
|
||||
end
|
||||
|
||||
test "lists rule runs scoped to family rules" do
|
||||
other_rule_run = create_other_family_rule_run
|
||||
|
||||
get api_v1_rule_runs_url, headers: api_headers(@api_key)
|
||||
|
||||
assert_response :success
|
||||
response_data = JSON.parse(response.body)
|
||||
rule_run_ids = response_data["data"].map { |rule_run| rule_run["id"] }
|
||||
|
||||
assert_includes rule_run_ids, @rule_run.id
|
||||
assert_includes rule_run_ids, @failed_rule_run.id
|
||||
assert_not_includes rule_run_ids, other_rule_run.id
|
||||
expected_count = RuleRun.joins(:rule).where(rules: { family_id: @family.id }).count
|
||||
assert_equal expected_count, response_data["meta"]["total_count"]
|
||||
end
|
||||
|
||||
test "shows a rule run" do
|
||||
get api_v1_rule_run_url(@rule_run), headers: api_headers(@api_key)
|
||||
|
||||
assert_response :success
|
||||
rule_run = JSON.parse(response.body)["data"]
|
||||
|
||||
assert_equal @rule_run.id, rule_run["id"]
|
||||
assert_equal @rule.id, rule_run["rule_id"]
|
||||
assert_equal "manual", rule_run["execution_type"]
|
||||
assert_equal "success", rule_run["status"]
|
||||
assert_equal 10, rule_run["transactions_queued"]
|
||||
assert_equal 4, rule_run["transactions_modified"]
|
||||
assert_equal @rule.id, rule_run.dig("rule", "id")
|
||||
end
|
||||
|
||||
test "does not show another family's rule run" do
|
||||
other_rule_run = create_other_family_rule_run
|
||||
|
||||
get api_v1_rule_run_url(other_rule_run), headers: api_headers(@api_key)
|
||||
|
||||
assert_response :not_found
|
||||
response_data = JSON.parse(response.body)
|
||||
assert_equal "record_not_found", response_data["error"]
|
||||
end
|
||||
|
||||
test "returns not found for malformed rule run id" do
|
||||
get api_v1_rule_run_url("not-a-uuid"), headers: api_headers(@api_key)
|
||||
|
||||
assert_response :not_found
|
||||
response_data = JSON.parse(response.body)
|
||||
assert_equal "record_not_found", response_data["error"]
|
||||
end
|
||||
|
||||
test "filters rule runs" do
|
||||
get api_v1_rule_runs_url,
|
||||
params: {
|
||||
rule_id: @rule.id,
|
||||
status: "failed",
|
||||
execution_type: "scheduled",
|
||||
start_executed_at: "2024-01-16T00:00:00Z",
|
||||
end_executed_at: "2024-01-17T00:00:00Z"
|
||||
},
|
||||
headers: api_headers(@api_key)
|
||||
|
||||
assert_response :success
|
||||
response_data = JSON.parse(response.body)
|
||||
assert_equal [ @failed_rule_run.id ], response_data["data"].map { |rule_run| rule_run["id"] }
|
||||
end
|
||||
|
||||
test "rejects invalid filters" do
|
||||
get api_v1_rule_runs_url, params: { status: "unknown" }, headers: api_headers(@api_key)
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
response_data = JSON.parse(response.body)
|
||||
assert_equal "validation_failed", response_data["error"]
|
||||
end
|
||||
|
||||
test "clamps oversized per_page values to the documented maximum" do
|
||||
get api_v1_rule_runs_url, params: { per_page: 500 }, headers: api_headers(@api_key)
|
||||
|
||||
assert_response :success
|
||||
response_data = JSON.parse(response.body)
|
||||
assert_equal 100, response_data["meta"]["per_page"]
|
||||
end
|
||||
|
||||
test "rejects malformed rule_id filter" do
|
||||
get api_v1_rule_runs_url, params: { rule_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"]
|
||||
end
|
||||
|
||||
test "rejects invalid timestamp filters" do
|
||||
get api_v1_rule_runs_url, params: { start_executed_at: "not-a-date" }, headers: api_headers(@api_key)
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
response_data = JSON.parse(response.body)
|
||||
assert_equal "validation_failed", response_data["error"]
|
||||
end
|
||||
|
||||
test "requires authentication" do
|
||||
get api_v1_rule_runs_url
|
||||
|
||||
assert_response :unauthorized
|
||||
end
|
||||
|
||||
test "show requires authentication" do
|
||||
get api_v1_rule_run_url(@rule_run)
|
||||
|
||||
assert_response :unauthorized
|
||||
end
|
||||
|
||||
test "requires read scope" do
|
||||
api_key_without_read = ApiKey.new(
|
||||
user: @user,
|
||||
name: "No Read Key",
|
||||
scopes: [],
|
||||
source: "web",
|
||||
display_key: "no_read_#{SecureRandom.hex(8)}"
|
||||
)
|
||||
api_key_without_read.save!(validate: false)
|
||||
|
||||
get api_v1_rule_runs_url, headers: api_headers(api_key_without_read)
|
||||
|
||||
assert_response :forbidden
|
||||
ensure
|
||||
api_key_without_read&.destroy
|
||||
end
|
||||
|
||||
teardown do
|
||||
@redis&.close
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def api_headers(api_key)
|
||||
{ "X-Api-Key" => api_key.display_key }
|
||||
end
|
||||
|
||||
def create_other_family_rule_run
|
||||
other_family = Family.create!(name: "Other Family", currency: "USD", locale: "en")
|
||||
other_rule = other_family.rules.build(name: "Other", resource_type: "transaction", active: true)
|
||||
other_rule.conditions.build(condition_type: "transaction_name", operator: "like", value: "other")
|
||||
other_rule.actions.build(action_type: "set_transaction_name", value: "Other")
|
||||
other_rule.save!
|
||||
other_rule.rule_runs.create!(
|
||||
rule_name: other_rule.name,
|
||||
execution_type: "manual",
|
||||
status: "success",
|
||||
transactions_queued: 1,
|
||||
transactions_processed: 1,
|
||||
transactions_modified: 1,
|
||||
pending_jobs_count: 0,
|
||||
executed_at: Time.current
|
||||
)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user