feat(api): expose reset status polling (#1598)

* feat(api): expose reset status polling

* fix(api): hide reset enqueue exception details

* fix(api): use stable reset authorization message

* fix(api): narrow reset enqueue error handling

* fix(api): document reset enqueue failures

* docs(api): regenerate reset status OpenAPI

* fix(api): address reset polling review feedback
This commit is contained in:
ghost
2026-05-02 14:56:42 -06:00
committed by GitHub
parent 95c2208bdb
commit a8425a2488
6 changed files with 330 additions and 10 deletions

View File

@@ -12,6 +12,7 @@ class Api::V1::UsersControllerTest < ActionDispatch::IntegrationTest
user: @user,
name: "Test Read-Write Key",
scopes: [ "read_write" ],
source: "web",
display_key: "test_rw_#{SecureRandom.hex(8)}"
)
@@ -56,6 +57,7 @@ class Api::V1::UsersControllerTest < ActionDispatch::IntegrationTest
user: users(:family_member),
name: "Member Read-Write Key",
scopes: [ "read_write" ],
source: "web",
display_key: "test_member_#{SecureRandom.hex(8)}"
)
@@ -69,13 +71,65 @@ class Api::V1::UsersControllerTest < ActionDispatch::IntegrationTest
end
test "reset enqueues FamilyResetJob and returns 200" do
assert_enqueued_with(job: FamilyResetJob) do
assert_enqueued_with(job: FamilyResetJob, args: [ @user.family ]) do
delete "/api/v1/users/reset", headers: api_headers(@api_key)
end
assert_response :ok
body = JSON.parse(response.body)
assert_equal "Account reset has been initiated", body["message"]
assert_equal "queued", body["status"]
assert_equal @user.family.id, body["family_id"]
assert body["job_id"].present?
assert_equal "/api/v1/users/reset/status", body["status_url"]
end
test "reset returns controlled error when enqueue fails" do
FamilyResetJob.stub(:perform_later, ->(_family) { raise StandardError, "queue down" }) do
delete "/api/v1/users/reset", headers: api_headers(@api_key)
end
assert_response :internal_server_error
body = JSON.parse(response.body)
assert_equal "reset_enqueue_failed", body["error"]
assert_equal "Account reset could not be queued", body["message"]
assert_not_includes response.body, "queue down"
end
test "reset status requires authentication" do
get "/api/v1/users/reset/status"
assert_response :unauthorized
end
test "reset status requires admin role" do
non_admin_api_key = ApiKey.create!(
user: users(:family_member),
name: "Member Read Key",
scopes: [ "read_write" ],
source: "web",
display_key: "test_member_read_#{SecureRandom.hex(8)}"
)
get "/api/v1/users/reset/status", headers: api_headers(non_admin_api_key)
assert_response :forbidden
end
test "reset status returns family data counts" do
get "/api/v1/users/reset/status", headers: api_headers(@read_only_api_key)
assert_response :ok
body = JSON.parse(response.body)
assert_equal @user.family.id, body["family_id"]
assert_includes %w[complete data_remaining], body["status"]
assert_equal body["counts"].values.sum.zero?, body["reset_complete"]
assert body["counts"].key?("accounts")
assert body["counts"].key?("categories")
assert body["counts"].key?("tags")
assert body["counts"].key?("merchants")
assert body["counts"].key?("plaid_items")
assert body["counts"].key?("imports")
assert body["counts"].key?("budgets")
end
# -- Delete account --------------------------------------------------------
@@ -92,6 +146,7 @@ class Api::V1::UsersControllerTest < ActionDispatch::IntegrationTest
user: solo_user,
name: "Solo Key",
scopes: [ "read_write" ],
source: "web",
display_key: "test_solo_#{SecureRandom.hex(8)}"
)
@@ -129,6 +184,6 @@ class Api::V1::UsersControllerTest < ActionDispatch::IntegrationTest
private
def api_headers(api_key)
{ "X-Api-Key" => api_key.display_key }
{ "X-Api-Key" => api_key.plain_key }
end
end