feature - 8 - Add other user actions

This commit is contained in:
myrmidex 2025-12-29 19:58:58 +01:00
parent 1174f2fbda
commit 09236f6f10
6 changed files with 708 additions and 11 deletions

View file

@ -0,0 +1,84 @@
<?php
namespace App\Actions\User;
use App\Models\User;
use Exception;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use InvalidArgumentException;
class CreateUserAction
{
/**
* @throws Exception
*/
public function execute(array $data): User
{
try {
// Validate required fields first
if (!isset($data['name']) || empty($data['name'])) {
throw new InvalidArgumentException('Name is required');
}
if (!isset($data['planner_id']) || empty($data['planner_id'])) {
throw new InvalidArgumentException('Planner ID is required');
}
DB::beginTransaction();
Log::info('CreateUserAction: Starting user creation', [
'name' => $data['name'],
'planner_id' => $data['planner_id'],
]);
// Create the user
$user = User::create([
'name' => $data['name'],
'planner_id' => $data['planner_id'],
]);
if (!$user) {
throw new Exception('User creation returned null');
}
Log::info('CreateUserAction: User creation result', [
'user_id' => $user->id,
'name' => $user->name,
'planner_id' => $user->planner_id,
]);
// Verify the user was actually created
$createdUser = User::find($user->id);
if (!$createdUser) {
throw new Exception('User creation did not persist to database');
}
if ($createdUser->name !== $data['name']) {
throw new Exception('User creation data mismatch');
}
DB::commit();
Log::info('CreateUserAction: User successfully created', [
'user_id' => $user->id,
'name' => $user->name,
'planner_id' => $user->planner_id,
]);
return $user;
} catch (Exception $e) {
DB::rollBack();
Log::error('CreateUserAction: User creation failed', [
'name' => $data['name'] ?? 'N/A',
'planner_id' => $data['planner_id'] ?? 'N/A',
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw $e;
}
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace App\Actions\User;
use App\Models\User;
use Exception;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class DeleteUserAction
{
/**
* @throws Exception
*/
public function execute(User $user): bool
{
try {
DB::beginTransaction();
Log::info('DeleteUserAction: Starting user deletion', [
'user_id' => $user->id,
'user_name' => $user->name,
'planner_id' => $user->planner_id,
]);
// Check for related data
$userDishCount = $user->userDishes()->count();
$dishCount = $user->dishes()->count();
Log::info('DeleteUserAction: User relationship counts', [
'user_id' => $user->id,
'user_dishes_count' => $userDishCount,
'dishes_count' => $dishCount,
]);
// Store user info before deletion for verification
$userId = $user->id;
$userName = $user->name;
// Delete the user (cascading deletes should handle related records)
$result = $user->delete();
Log::info('DeleteUserAction: Delete result', [
'result' => $result,
'user_id' => $userId,
]);
if (! $result) {
throw new Exception('User deletion returned false');
}
// Verify the deletion actually happened
$stillExists = User::find($userId);
if ($stillExists) {
throw new Exception('User deletion did not persist to database');
}
DB::commit();
Log::info('DeleteUserAction: User successfully deleted', [
'user_id' => $userId,
'user_name' => $userName,
]);
return true;
} catch (Exception $e) {
DB::rollBack();
Log::error('DeleteUserAction: User deletion failed', [
'user_id' => $user->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw $e;
}
}
}

View file

@ -3,6 +3,8 @@
namespace App\Livewire\Users; namespace App\Livewire\Users;
use App\Models\User; use App\Models\User;
use App\Actions\User\CreateUserAction;
use App\Actions\User\DeleteUserAction;
use App\Actions\User\EditUserAction; use App\Actions\User\EditUserAction;
use Exception; use Exception;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
@ -49,15 +51,19 @@ public function store(): void
{ {
$this->validate(); $this->validate();
User::create([ try {
'name' => $this->name, (new CreateUserAction())->execute([
'planner_id' => auth()->id(), 'name' => $this->name,
]); 'planner_id' => auth()->id(),
]);
$this->showCreateModal = false; $this->showCreateModal = false;
$this->reset(['name']); $this->reset(['name']);
session()->flash('success', 'User created successfully.'); session()->flash('success', 'User created successfully.');
} catch (Exception $e) {
session()->flash('error', 'Failed to create user: ' . $e->getMessage());
}
} }
public function edit(User $user): void public function edit(User $user): void
@ -95,11 +101,19 @@ public function confirmDelete(User $user): void
public function delete(): void public function delete(): void
{ {
$this->deletingUser->delete(); try {
$this->showDeleteModal = false; (new DeleteUserAction())->execute($this->deletingUser);
$this->deletingUser = null;
session()->flash('success', 'User deleted successfully.'); $this->showDeleteModal = false;
$this->deletingUser = null;
session()->flash('success', 'User deleted successfully.');
// Force component to re-render with fresh data
$this->resetPage();
} catch (Exception $e) {
session()->flash('error', 'Failed to delete user: ' . $e->getMessage());
}
} }
public function cancel(): void public function cancel(): void

View file

@ -0,0 +1,238 @@
<?php
namespace Tests\Unit\Actions\User;
use App\Actions\User\CreateUserAction;
use App\Models\Planner;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Log;
use Tests\TestCase;
class CreateUserActionTest extends TestCase
{
use RefreshDatabase;
private CreateUserAction $action;
private Planner $planner;
protected function setUp(): void
{
parent::setUp();
$this->action = new CreateUserAction();
// Create a planner for testing
$this->planner = Planner::factory()->create();
}
public function test_it_can_create_a_user_successfully(): void
{
// Arrange
$userData = [
'name' => 'Test User',
'planner_id' => $this->planner->id,
];
// Act
$user = $this->action->execute($userData);
// Assert
$this->assertInstanceOf(User::class, $user);
$this->assertEquals('Test User', $user->name);
$this->assertEquals($this->planner->id, $user->planner_id);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'name' => 'Test User',
'planner_id' => $this->planner->id,
]);
}
public function test_it_throws_exception_when_name_is_empty(): void
{
// Arrange
$userData = [
'name' => '',
'planner_id' => $this->planner->id,
];
// Act & Assert
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Name is required');
$this->action->execute($userData);
// Verify no user was created
$this->assertDatabaseMissing('users', [
'planner_id' => $this->planner->id,
]);
}
public function test_it_throws_exception_when_name_is_missing(): void
{
// Arrange
$userData = [
'planner_id' => $this->planner->id,
];
// Act & Assert
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Name is required');
$this->action->execute($userData);
}
public function test_it_throws_exception_when_planner_id_is_empty(): void
{
// Arrange
$userData = [
'name' => 'Test User',
'planner_id' => '',
];
// Act & Assert
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Planner ID is required');
$this->action->execute($userData);
}
public function test_it_throws_exception_when_planner_id_is_missing(): void
{
// Arrange
$userData = [
'name' => 'Test User',
];
// Act & Assert
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Planner ID is required');
$this->action->execute($userData);
}
public function test_it_logs_creation_process(): void
{
// Arrange
Log::spy();
$userData = [
'name' => 'Test User',
'planner_id' => $this->planner->id,
];
// Act
$user = $this->action->execute($userData);
// Assert
Log::shouldHaveReceived('info')
->with('CreateUserAction: Starting user creation', [
'name' => 'Test User',
'planner_id' => $this->planner->id,
]);
Log::shouldHaveReceived('info')
->with('CreateUserAction: User successfully created', [
'user_id' => $user->id,
'name' => 'Test User',
'planner_id' => $this->planner->id,
]);
}
public function test_it_handles_database_transaction_rollback_on_failure(): void
{
// Arrange
$userData = [
'name' => 'Test User',
'planner_id' => 999999, // Non-existent planner ID should cause foreign key constraint error
];
// Act & Assert
$this->expectException(\Exception::class);
try {
$this->action->execute($userData);
} catch (\Exception $e) {
// Verify no user was created (transaction rolled back)
$this->assertDatabaseMissing('users', [
'name' => 'Test User',
]);
throw $e;
}
}
public function test_it_logs_errors_on_failure(): void
{
// Arrange
Log::spy();
$userData = [
'name' => 'Test User',
'planner_id' => 999999, // Non-existent planner ID
];
// Act & Assert
try {
$this->action->execute($userData);
} catch (\Exception $e) {
// Expected
}
// Assert
Log::shouldHaveReceived('error')
->with('CreateUserAction: User creation failed', \Mockery::on(function ($data) {
return $data['name'] === 'Test User' &&
$data['planner_id'] === 999999 &&
isset($data['error']) &&
isset($data['trace']);
}));
}
public function test_it_creates_user_with_whitespace_trimmed_name(): void
{
// Arrange
$userData = [
'name' => ' Test User ',
'planner_id' => $this->planner->id,
];
// Act
$user = $this->action->execute($userData);
// Assert
$this->assertEquals(' Test User ', $user->name); // Should preserve original data as passed
$this->assertDatabaseHas('users', [
'id' => $user->id,
'name' => ' Test User ',
'planner_id' => $this->planner->id,
]);
}
public function test_it_can_create_multiple_users_with_same_planner(): void
{
// Arrange
$userData1 = [
'name' => 'User One',
'planner_id' => $this->planner->id,
];
$userData2 = [
'name' => 'User Two',
'planner_id' => $this->planner->id,
];
// Act
$user1 = $this->action->execute($userData1);
$user2 = $this->action->execute($userData2);
// Assert
$this->assertNotEquals($user1->id, $user2->id);
$this->assertEquals($this->planner->id, $user1->planner_id);
$this->assertEquals($this->planner->id, $user2->planner_id);
$this->assertDatabaseHas('users', ['name' => 'User One']);
$this->assertDatabaseHas('users', ['name' => 'User Two']);
}
protected function tearDown(): void
{
\Mockery::close();
parent::tearDown();
}
}

View file

@ -0,0 +1,195 @@
<?php
namespace Tests\Unit\Actions\User;
use App\Actions\User\DeleteUserAction;
use App\Models\Dish;
use App\Models\Planner;
use App\Models\User;
use App\Models\UserDish;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Log;
use Tests\TestCase;
class DeleteUserActionTest extends TestCase
{
use RefreshDatabase;
private DeleteUserAction $action;
private Planner $planner;
protected function setUp(): void
{
parent::setUp();
$this->action = new DeleteUserAction();
// Create a planner for testing
$this->planner = Planner::factory()->create();
}
public function test_it_can_delete_a_user_successfully(): void
{
// Arrange
$user = User::factory()->create([
'planner_id' => $this->planner->id,
'name' => 'Test User'
]);
$userId = $user->id;
// Act
$result = $this->action->execute($user);
// Assert
$this->assertTrue($result);
$this->assertDatabaseMissing('users', ['id' => $userId]);
}
public function test_it_can_delete_a_user_with_associated_dishes(): void
{
// Arrange
$user = User::factory()->create(['planner_id' => $this->planner->id]);
$dish = Dish::factory()->create(['planner_id' => $this->planner->id]);
// Associate user with dish
UserDish::create([
'user_id' => $user->id,
'dish_id' => $dish->id
]);
$userId = $user->id;
// Verify the association exists
$this->assertDatabaseHas('user_dishes', [
'user_id' => $userId,
'dish_id' => $dish->id
]);
// Act
$result = $this->action->execute($user);
// Assert
$this->assertTrue($result);
$this->assertDatabaseMissing('users', ['id' => $userId]);
// Verify cascade deletion removed the association
$this->assertDatabaseMissing('user_dishes', ['user_id' => $userId]);
// Verify the dish itself still exists
$this->assertDatabaseHas('dishes', ['id' => $dish->id]);
}
public function test_it_logs_deletion_process(): void
{
// Arrange
Log::spy();
$user = User::factory()->create(['planner_id' => $this->planner->id]);
$userId = $user->id;
$userName = $user->name;
// Act
$this->action->execute($user);
// Assert
Log::shouldHaveReceived('info')
->with('DeleteUserAction: Starting user deletion', [
'user_id' => $userId,
'user_name' => $userName,
'planner_id' => $this->planner->id,
]);
Log::shouldHaveReceived('info')
->with('DeleteUserAction: User successfully deleted', [
'user_id' => $userId,
'user_name' => $userName,
]);
}
public function test_it_handles_database_transaction_rollback_on_failure(): void
{
// Arrange
$user = User::factory()->create(['planner_id' => $this->planner->id]);
// Mock the user to throw an exception during deletion
$mockUser = \Mockery::mock(User::class);
$mockUser->shouldReceive('getAttribute')->with('id')->andReturn($user->id);
$mockUser->shouldReceive('getAttribute')->with('name')->andReturn($user->name);
$mockUser->shouldReceive('getAttribute')->with('planner_id')->andReturn($user->planner_id);
$mockUser->shouldReceive('userDishes')->andReturn($user->userDishes());
$mockUser->shouldReceive('dishes')->andReturn($user->dishes());
$mockUser->shouldReceive('delete')->andThrow(new \Exception('Database error'));
// Act & Assert
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Database error');
$this->action->execute($mockUser);
// Verify original user still exists (transaction rolled back)
$this->assertDatabaseHas('users', ['id' => $user->id]);
}
public function test_it_throws_exception_when_deletion_returns_false(): void
{
// Arrange
$user = User::factory()->create(['planner_id' => $this->planner->id]);
// Mock the user to return false on delete
$mockUser = \Mockery::mock(User::class);
$mockUser->shouldReceive('getAttribute')->with('id')->andReturn($user->id);
$mockUser->shouldReceive('getAttribute')->with('name')->andReturn($user->name);
$mockUser->shouldReceive('getAttribute')->with('planner_id')->andReturn($user->planner_id);
$mockUser->shouldReceive('userDishes')->andReturn($user->userDishes());
$mockUser->shouldReceive('dishes')->andReturn($user->dishes());
$mockUser->shouldReceive('delete')->andReturn(false);
// Act & Assert
$this->expectException(\Exception::class);
$this->expectExceptionMessage('User deletion returned false');
$this->action->execute($mockUser);
}
public function test_it_throws_exception_when_deletion_does_not_persist(): void
{
// This test is tricky to implement realistically since we can't easily
// mock the User::find() call in a way that makes sense.
// We'll skip this edge case for now, but in a real scenario you might
// want to test database connection issues, etc.
$this->markTestSkipped('Edge case test - difficult to implement without complex mocking');
}
public function test_it_logs_errors_on_failure(): void
{
// Arrange
Log::spy();
$user = User::factory()->create(['planner_id' => $this->planner->id]);
// Mock the user to throw an exception during deletion
$mockUser = \Mockery::mock(User::class);
$mockUser->shouldReceive('getAttribute')->with('id')->andReturn($user->id);
$mockUser->shouldReceive('getAttribute')->with('name')->andReturn($user->name);
$mockUser->shouldReceive('getAttribute')->with('planner_id')->andReturn($user->planner_id);
$mockUser->shouldReceive('userDishes')->andReturn($user->userDishes());
$mockUser->shouldReceive('dishes')->andReturn($user->dishes());
$mockUser->shouldReceive('delete')->andThrow(new \Exception('Test error'));
// Act & Assert
try {
$this->action->execute($mockUser);
} catch (\Exception $e) {
// Expected
}
// Assert
Log::shouldHaveReceived('error')
->with('DeleteUserAction: User deletion failed', \Mockery::on(function ($data) use ($user) {
return $data['user_id'] === $user->id &&
$data['error'] === 'Test error' &&
isset($data['trace']);
}));
}
protected function tearDown(): void
{
\Mockery::close();
parent::tearDown();
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace Tests\Unit\Actions;
use App\Actions\User\CreateUserAction;
use App\Actions\User\DeleteUserAction;
use App\Models\Planner;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserActionIntegrationTest extends TestCase
{
use RefreshDatabase;
private Planner $planner;
private CreateUserAction $createAction;
private DeleteUserAction $deleteAction;
protected function setUp(): void
{
parent::setUp();
$this->planner = Planner::factory()->create();
$this->createAction = new CreateUserAction();
$this->deleteAction = new DeleteUserAction();
}
public function test_complete_user_lifecycle_with_actions(): void
{
// Test creation
$userData = [
'name' => 'Integration Test User',
'planner_id' => $this->planner->id,
];
$user = $this->createAction->execute($userData);
$this->assertInstanceOf(User::class, $user);
$this->assertEquals('Integration Test User', $user->name);
$this->assertEquals($this->planner->id, $user->planner_id);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'name' => 'Integration Test User',
'planner_id' => $this->planner->id,
]);
// Test deletion
$userId = $user->id;
$result = $this->deleteAction->execute($user);
$this->assertTrue($result);
$this->assertDatabaseMissing('users', ['id' => $userId]);
}
public function test_creating_and_deleting_user_with_relationships(): void
{
// Create user
$user = $this->createAction->execute([
'name' => 'User With Relationships',
'planner_id' => $this->planner->id,
]);
// Create a dish and associate it with the user
$dish = \App\Models\Dish::factory()->create(['planner_id' => $this->planner->id]);
$user->dishes()->attach($dish->id);
// Verify the relationship exists
$this->assertEquals(1, $user->dishes()->count());
$this->assertDatabaseHas('user_dishes', [
'user_id' => $user->id,
'dish_id' => $dish->id,
]);
// Delete the user
$userId = $user->id;
$result = $this->deleteAction->execute($user);
// Verify deletion and cascade
$this->assertTrue($result);
$this->assertDatabaseMissing('users', ['id' => $userId]);
$this->assertDatabaseMissing('user_dishes', ['user_id' => $userId]);
// Dish should still exist
$this->assertDatabaseHas('dishes', ['id' => $dish->id]);
}
}