Files
sure/test/controllers/api/v1/users_controller_test.rb
Juan José Mata 3adc011df0 Require admin role for API family reset (#1189)
Prevent non-admin users with read_write API access from triggering family-wide reset jobs via /api/v1/users/reset.
2026-03-13 08:07:30 +01:00

135 lines
3.8 KiB
Ruby

# frozen_string_literal: true
require "test_helper"
class Api::V1::UsersControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:family_admin)
@user.api_keys.active.destroy_all
@api_key = ApiKey.create!(
user: @user,
name: "Test Read-Write Key",
scopes: [ "read_write" ],
display_key: "test_rw_#{SecureRandom.hex(8)}"
)
@read_only_api_key = ApiKey.create!(
user: @user,
name: "Test Read-Only Key",
scopes: [ "read" ],
display_key: "test_ro_#{SecureRandom.hex(8)}",
source: "mobile"
)
end
# -- Authentication --------------------------------------------------------
test "reset requires authentication" do
delete "/api/v1/users/reset"
assert_response :unauthorized
end
test "destroy requires authentication" do
delete "/api/v1/users/me"
assert_response :unauthorized
end
# -- Scope enforcement -----------------------------------------------------
test "reset requires write scope" do
delete "/api/v1/users/reset", headers: api_headers(@read_only_api_key)
assert_response :forbidden
end
test "destroy requires write scope" do
delete "/api/v1/users/me", headers: api_headers(@read_only_api_key)
assert_response :forbidden
end
# -- Reset -----------------------------------------------------------------
test "reset requires admin role" do
non_admin_api_key = ApiKey.create!(
user: users(:family_member),
name: "Member Read-Write Key",
scopes: [ "read_write" ],
display_key: "test_member_#{SecureRandom.hex(8)}"
)
assert_no_enqueued_jobs only: FamilyResetJob do
delete "/api/v1/users/reset", headers: api_headers(non_admin_api_key)
end
assert_response :forbidden
body = JSON.parse(response.body)
assert_equal "You are not authorized to perform this action", body["message"]
end
test "reset enqueues FamilyResetJob and returns 200" do
assert_enqueued_with(job: FamilyResetJob) 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"]
end
# -- Delete account --------------------------------------------------------
test "destroy deactivates user and returns 200" do
solo_family = Family.create!(name: "Solo Family", currency: "USD", locale: "en", date_format: "%m-%d-%Y")
solo_user = solo_family.users.create!(
email: "solo@example.com",
password: "password123",
password_confirmation: "password123",
role: :admin
)
solo_api_key = ApiKey.create!(
user: solo_user,
name: "Solo Key",
scopes: [ "read_write" ],
display_key: "test_solo_#{SecureRandom.hex(8)}"
)
delete "/api/v1/users/me", headers: api_headers(solo_api_key)
assert_response :ok
body = JSON.parse(response.body)
assert_equal "Account has been deleted", body["message"]
solo_user.reload
assert_not solo_user.active?
assert_not_equal "solo@example.com", solo_user.email
end
test "destroy returns 422 when admin has other family members" do
delete "/api/v1/users/me", headers: api_headers(@api_key)
assert_response :unprocessable_entity
body = JSON.parse(response.body)
assert_equal "Failed to delete account", body["error"]
end
# -- Deactivated user ------------------------------------------------------
test "rejects deactivated user with 401" do
@user.update_column(:active, false)
delete "/api/v1/users/reset", headers: api_headers(@api_key)
assert_response :unauthorized
body = JSON.parse(response.body)
assert_equal "Account has been deactivated", body["message"]
end
private
def api_headers(api_key)
{ "X-Api-Key" => api_key.display_key }
end
end