From 6917e0bf1ccd6f343dc8fad8fc431702399a7fd3 Mon Sep 17 00:00:00 2001 From: Reem Ibrahim Date: Thu, 13 Aug 2026 14:26:55 +0100 Subject: [PATCH 1/2] updates relating to self serve user deactivation --- app/controllers/v1/users_controller.rb | 18 ++++++ app/models/user.rb | 6 ++ app/serializable/serializable_user.rb | 2 +- app/services/deactivate_user.rb | 19 ++++++- config/routes.rb | 1 + spec/models/offboard/deactivate_users_spec.rb | 1 + spec/models/user_spec.rb | 51 +++++++++++++++++ spec/requests/v1/users_spec.rb | 57 +++++++++++++++++++ spec/services/deactivate_user_spec.rb | 25 ++++++++ 9 files changed, 178 insertions(+), 2 deletions(-) diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index 3ef853c73..82c2486ae 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -73,4 +73,22 @@ def user_auth_logs render jsonapi: objects, class: { OpenStruct: SerializableUserAuthLog }, status: :ok end + + def deactivate + user = User.find_by!(auth_id: current_auth_id) + + unless user.can_deactivate? + return render jsonapi_errors: { user: ['Cannot be deactivated because they are the only user associated with their suppliers'] }, + status: :unprocessable_entity + end + + result = DeactivateUser.new(user: user).call + + if result.success? + render jsonapi: user, status: :ok + else + render jsonapi_errors: { user: ['Could not be deactivated'] }, + status: :unprocessable_entity + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index 32cba1e76..e4e08b89b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -40,4 +40,10 @@ def multiple_suppliers? def active? !auth_id.nil? end + + def can_deactivate? + suppliers.all? do |supplier| + supplier.active_users.where.not(id: id).exists? + end + end end diff --git a/app/serializable/serializable_user.rb b/app/serializable/serializable_user.rb index 14da8dae9..080926e1d 100644 --- a/app/serializable/serializable_user.rb +++ b/app/serializable/serializable_user.rb @@ -1,4 +1,4 @@ class SerializableUser < JSONAPI::Serializable::Resource type 'users' - attributes :multiple_suppliers?, :name, :email, :created_at + attributes :multiple_suppliers?, :can_deactivate?, :name, :email, :created_at end diff --git a/app/services/deactivate_user.rb b/app/services/deactivate_user.rb index d62e20bd7..338b917f8 100644 --- a/app/services/deactivate_user.rb +++ b/app/services/deactivate_user.rb @@ -9,6 +9,13 @@ def call result = Result.new(true) User.transaction do + lock_linked_suppliers! + + unless user.can_deactivate? + result.success = false + raise ActiveRecord::Rollback + end + begin DeleteUserInAuth0.new(user: user).call rescue Auth0::Exception @@ -16,9 +23,19 @@ def call Rails.logger.error("Error adding user #{user.email} to Auth0 during DeactivateUser") raise ActiveRecord::Rollback end - user.update(auth_id: nil) + + unless user.update(auth_id: nil) + result.success = false + raise ActiveRecord::Rollback + end end result end + + private + + def lock_linked_suppliers! + user.suppliers.lock.load + end end diff --git a/config/routes.rb b/config/routes.rb index 9a8cdf107..fb51d4dea 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -25,6 +25,7 @@ collection do patch :update_name patch :update_email + patch :deactivate get :user_auth_logs end end diff --git a/spec/models/offboard/deactivate_users_spec.rb b/spec/models/offboard/deactivate_users_spec.rb index 4d9e8e97f..bdcf54a11 100644 --- a/spec/models/offboard/deactivate_users_spec.rb +++ b/spec/models/offboard/deactivate_users_spec.rb @@ -6,6 +6,7 @@ end let!(:user) { FactoryBot.create(:user, name: 'User One', email: 'email_one@ccs.co.uk', suppliers: [supplier]) } + let!(:user_two) { FactoryBot.create(:user, name: 'User Two', email: 'email_two@ccs.co.uk', suppliers: [supplier]) } before do stub_auth0_token_request diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 21f23eb59..2fdce7623 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -53,6 +53,57 @@ end end + describe '#can_deactivate?' do + subject(:can_deactivate?) { user.can_deactivate? } + + let(:user) { FactoryBot.create(:user) } + + context 'when the user is the only active user for a supplier' do + before do + supplier = FactoryBot.create(:supplier) + user.suppliers << supplier + end + + it { is_expected.to be_falsy } + end + + context 'when the user is not the only active user for a supplier' do + before do + supplier = FactoryBot.create(:supplier) + user.suppliers << supplier + other_user = FactoryBot.create(:user) + other_user.suppliers << supplier + end + + it { is_expected.to be_truthy } + end + + context 'when another linked user is inactive' do + before do + supplier = FactoryBot.create(:supplier) + user.suppliers << supplier + other_user = FactoryBot.create(:user, :inactive) + other_user.suppliers << supplier + end + + it { is_expected.to be_falsy } + end + + context 'when one supplier has multiple active users and another has only one' do + before do + supplier1 = FactoryBot.create(:supplier) + user.suppliers << supplier1 + other_user1 = FactoryBot.create(:user) + other_user1.suppliers << supplier1 + + supplier2 = FactoryBot.create(:supplier) + user.suppliers << supplier2 + end + + it { is_expected.to be_falsy } + end + end + describe '.search' do let!(:bob) { FactoryBot.create(:user, name: 'Bob Booker', email: 'bob@sheffield.com') } let!(:bobby) { FactoryBot.create(:user, name: 'Bobby Brown', email: 'bobby_b_66@hotmail.com') } diff --git a/spec/requests/v1/users_spec.rb b/spec/requests/v1/users_spec.rb index 2d52dc755..2e66cf141 100644 --- a/spec/requests/v1/users_spec.rb +++ b/spec/requests/v1/users_spec.rb @@ -16,6 +16,8 @@ expect(json['data'][0]) .to have_attribute(:multiple_suppliers?) .with_value(false) + expect(json['data'][0]) + .to have_attribute(:can_deactivate?) end it 'returns the details of the current user who belongs to more than one supplier' do @@ -30,6 +32,22 @@ .to have_attribute(:multiple_suppliers?) .with_value(true) end + + it 'returns that the user can be deactivated if they are not the only active user for a supplier' do + user = FactoryBot.create(:user) + supplier = FactoryBot.create(:supplier) + user.suppliers << supplier + other_user = FactoryBot.create(:user) + other_user.suppliers << supplier + + get '/v1/users', headers: { 'X-Auth-Id' => JWT.encode(user.auth_id, 'test') } + + expect(json['data'].size).to eql 1 + expect(response).to be_successful + expect(json['data'][0]) + .to have_attribute(:can_deactivate?) + .with_value(true) + end end describe 'PATCH /v1/users/update_name' do @@ -125,4 +143,43 @@ expect(response.status).to eq 200 end end + + describe 'PATCH /v1/users/deactivate' do + let(:user) { FactoryBot.create(:user) } + let(:headers) { { 'X-Auth-Id' => JWT.encode(user.auth_id, 'test') } } + + context 'when the user can be deactivated' do + before do + supplier = FactoryBot.create(:supplier) + user.suppliers << supplier + other_user = FactoryBot.create(:user) + other_user.suppliers << supplier + end + + it 'deactivates the user' do + stub_auth0_token_request + stub_auth0_delete_user_request(user) + + patch '/v1/users/deactivate', headers: headers + + expect(response).to be_successful + expect(user.reload.auth_id).to be_nil + end + end + + context 'when the user cannot be deactivated' do + before do + supplier = FactoryBot.create(:supplier) + user.suppliers << supplier + end + + it 'returns an error' do + patch '/v1/users/deactivate', headers: headers + + expect(response.status).to eq 422 + expect(json['errors']).not_to be_empty + expect(user.reload.auth_id).not_to be_nil + end + end + end end diff --git a/spec/services/deactivate_user_spec.rb b/spec/services/deactivate_user_spec.rb index 870320a68..461706b21 100644 --- a/spec/services/deactivate_user_spec.rb +++ b/spec/services/deactivate_user_spec.rb @@ -1,8 +1,13 @@ require 'rails_helper' RSpec.describe DeactivateUser do + let(:suppler) { create(:supplier) } let(:user) { create(:user) } + let(:other_user) { create(:user) } + before(:each) do + user.suppliers << suppler + other_user.suppliers << suppler stub_auth0_token_request end @@ -16,6 +21,26 @@ expect(result.failure?).to eq(false) end + context 'when the user is the only active user for a supplier' do + let(:other_user) { create(:user, :inactive) } + + it 'returns a failed result' do + result = described_class.new(user: user).call + expect(result).to be_failure + end + + it 'does not delete the user in Auth0' do + expect_any_instance_of(DeleteUserInAuth0).not_to receive(:call) + described_class.new(user: user).call + end + + it 'does not clear the auth_id of the user' do + original_auth_id = user.auth_id + described_class.new(user: user).call + expect(user.auth_id).to eql(original_auth_id) + end + end + context 'when Auth0 errors' do before(:each) do stub_auth0_delete_user_request_failure(user) From 33649468a28e7efce214bc1b120d10878fc34ddd Mon Sep 17 00:00:00 2001 From: Reem Ibrahim Date: Thu, 13 Aug 2026 14:44:18 +0100 Subject: [PATCH 2/2] rubocop --- app/controllers/v1/users_controller.rb | 4 +++- app/services/deactivate_user.rb | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb index 82c2486ae..1bdf8c292 100644 --- a/app/controllers/v1/users_controller.rb +++ b/app/controllers/v1/users_controller.rb @@ -78,8 +78,10 @@ def deactivate user = User.find_by!(auth_id: current_auth_id) unless user.can_deactivate? + # rubocop:disable Layout/LineLength return render jsonapi_errors: { user: ['Cannot be deactivated because they are the only user associated with their suppliers'] }, - status: :unprocessable_entity + status: :unprocessable_entity + # rubocop:enable Layout/LineLength end result = DeactivateUser.new(user: user).call diff --git a/app/services/deactivate_user.rb b/app/services/deactivate_user.rb index 338b917f8..935beae87 100644 --- a/app/services/deactivate_user.rb +++ b/app/services/deactivate_user.rb @@ -36,6 +36,6 @@ def call private def lock_linked_suppliers! - user.suppliers.lock.load + user.suppliers.lock.load end end