From 1781623d3e7ca9882a5379175de21a0d449a245f Mon Sep 17 00:00:00 2001 From: Eleazar Resendez Date: Thu, 2 Jul 2026 10:38:28 -0600 Subject: [PATCH 1/2] FOUR-32114: Prevent duplicate user update signal on restore --- ProcessMaker/Models/User.php | 2 +- tests/Feature/Api/UsersTest.php | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/ProcessMaker/Models/User.php b/ProcessMaker/Models/User.php index c13c133659..dee34a92c0 100644 --- a/ProcessMaker/Models/User.php +++ b/ProcessMaker/Models/User.php @@ -160,8 +160,8 @@ public static function boot() }); static::restoring(function ($user) { + // SoftDeletes::restore() saves this with deleted_at=null; avoid a second updated event. $user->status = 'ACTIVE'; - $user->save(); }); static::deleted(function ($user) { diff --git a/tests/Feature/Api/UsersTest.php b/tests/Feature/Api/UsersTest.php index c1dacd9e18..f298e54c5b 100644 --- a/tests/Feature/Api/UsersTest.php +++ b/tests/Feature/Api/UsersTest.php @@ -5,7 +5,9 @@ use Database\Seeders\PermissionSeeder; use Faker\Factory as Faker; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Redis; +use ProcessMaker\Jobs\ThrowSignalEvent; use ProcessMaker\Models\Group; use ProcessMaker\Models\GroupMember; use ProcessMaker\Models\Process; @@ -644,6 +646,44 @@ public function testRestoreSoftDeletedUser() $response->assertJsonFragment(['id' => $id]); } + public function testRestoreSoftDeletedUserDispatchesOneUserUpdateSignal() + { + config(['user-signal.update' => true]); + + Bus::fake([ + ThrowSignalEvent::class, + ]); + + $deletedUser = null; + User::withoutEvents(function () use (&$deletedUser) { + $deletedUser = User::factory()->create([ + 'deleted_at' => now(), + 'status' => 'INACTIVE', + ]); + }); + + $response = $this->apiCall('PUT', self::API_TEST_URL . '/restore', [ + 'id' => $deletedUser->id, + ]); + + $response->assertStatus(200); + + $restoredUser = User::find($deletedUser->id); + $this->assertSame('ACTIVE', $restoredUser->status); + $this->assertNull($restoredUser->deleted_at); + $this->assertFalse($restoredUser->trashed()); + + $userUpdateSignalCount = 0; + Bus::assertDispatched(ThrowSignalEvent::class, function (ThrowSignalEvent $job) use (&$userUpdateSignalCount) { + if ($job->signalRef === 'user_update') { + $userUpdateSignalCount++; + } + + return true; + }); + $this->assertSame(1, $userUpdateSignalCount); + } + public function testCreateWithoutPassword() { $payload = [ From 1896a6dcdbdd1da982d314e1b554049dbc2afb29 Mon Sep 17 00:00:00 2001 From: Eleazar Resendez Date: Wed, 15 Jul 2026 10:09:43 -0600 Subject: [PATCH 2/2] FOUR-32202: Prevent user update signal on delete --- ProcessMaker/Models/User.php | 3 +- tests/Feature/Api/UsersTest.php | 53 +++++++++++++++++++++++---------- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/ProcessMaker/Models/User.php b/ProcessMaker/Models/User.php index dee34a92c0..41229368b3 100644 --- a/ProcessMaker/Models/User.php +++ b/ProcessMaker/Models/User.php @@ -156,7 +156,8 @@ public static function boot() static::deleting(function ($user) { $user->status = 'INACTIVE'; - $user->save(); + // Persist the inactive status without treating the delete as a user update. + $user->saveQuietly(); }); static::restoring(function ($user) { diff --git a/tests/Feature/Api/UsersTest.php b/tests/Feature/Api/UsersTest.php index f298e54c5b..4c58a444b5 100644 --- a/tests/Feature/Api/UsersTest.php +++ b/tests/Feature/Api/UsersTest.php @@ -5,9 +5,7 @@ use Database\Seeders\PermissionSeeder; use Faker\Factory as Faker; use Illuminate\Http\UploadedFile; -use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Redis; -use ProcessMaker\Jobs\ThrowSignalEvent; use ProcessMaker\Models\Group; use ProcessMaker\Models\GroupMember; use ProcessMaker\Models\Process; @@ -646,14 +644,8 @@ public function testRestoreSoftDeletedUser() $response->assertJsonFragment(['id' => $id]); } - public function testRestoreSoftDeletedUserDispatchesOneUserUpdateSignal() + public function testRestoreSoftDeletedUserDispatchesOneUpdatedEvent() { - config(['user-signal.update' => true]); - - Bus::fake([ - ThrowSignalEvent::class, - ]); - $deletedUser = null; User::withoutEvents(function () use (&$deletedUser) { $deletedUser = User::factory()->create([ @@ -662,6 +654,11 @@ public function testRestoreSoftDeletedUserDispatchesOneUserUpdateSignal() ]); }); + $updatedEventCount = 0; + User::updated(function () use (&$updatedEventCount) { + $updatedEventCount++; + }); + $response = $this->apiCall('PUT', self::API_TEST_URL . '/restore', [ 'id' => $deletedUser->id, ]); @@ -672,16 +669,40 @@ public function testRestoreSoftDeletedUserDispatchesOneUserUpdateSignal() $this->assertSame('ACTIVE', $restoredUser->status); $this->assertNull($restoredUser->deleted_at); $this->assertFalse($restoredUser->trashed()); + $this->assertSame(1, $updatedEventCount); + } - $userUpdateSignalCount = 0; - Bus::assertDispatched(ThrowSignalEvent::class, function (ThrowSignalEvent $job) use (&$userUpdateSignalCount) { - if ($job->signalRef === 'user_update') { - $userUpdateSignalCount++; - } + public function testDeleteUserDispatchesDeletedWithoutUpdatedEvent() + { + $user = User::factory()->create(); + $group = Group::factory()->create(); + $user->groups()->attach($group->id); - return true; + $updatedEventCount = 0; + $deletedEventCount = 0; + User::updated(function () use (&$updatedEventCount) { + $updatedEventCount++; }); - $this->assertSame(1, $userUpdateSignalCount); + User::deleted(function () use (&$deletedEventCount) { + $deletedEventCount++; + }); + + $response = $this->apiCall('DELETE', self::API_TEST_URL . '/' . $user->id); + + $response->assertStatus(204); + + $deletedUser = User::withTrashed()->findOrFail($user->id); + $this->assertSame('INACTIVE', $deletedUser->status); + $this->assertNotNull($deletedUser->deleted_at); + $this->assertTrue($deletedUser->trashed()); + $this->assertSame(0, $updatedEventCount); + $this->assertSame(1, $deletedEventCount); + + $this->assertDatabaseMissing('group_members', [ + 'group_id' => $group->id, + 'member_id' => $user->id, + 'member_type' => User::class, + ]); } public function testCreateWithoutPassword()