'create expense']); Permission::create(['name' => 'update expense']); Permission::create(['name' => 'delete expense']); }); function createUserWithOutlet(bool $multipleOutlets = false) { $user = User::factory() ->has(Employee::factory(), 'employee') ->create(); $outlet1 = Outlet::factory()->create(); $user->outlets()->attach($outlet1); if ($multipleOutlets) { $outlet2 = Outlet::factory()->create(); $user->outlets()->attach($outlet2); } // Assign permissions $user->givePermissionTo(['create expense', 'update expense', 'delete expense']); return [$user, $outlet1]; } it('renders successfully', function () { [$user, $outlet] = createUserWithOutlet(); $this->actingAs($user); Livewire::test(Expense::class) ->assertStatus(200); }); it('validates required fields', function () { [$user, $outlet] = createUserWithOutlet(); $this->actingAs($user); Livewire::test(Expense::class) ->set('form.description', '') ->set('form.amount', '') ->call('create') ->assertHasErrors(['form.description', 'form.amount']); }); it('validates amount must be numeric', function () { [$user, $outlet] = createUserWithOutlet(); $this->actingAs($user); Livewire::test(Expense::class) ->set('form.amount', 'abc') ->call('create') ->assertHasErrors(['form.amount']); }); it('validates outlet_id is required for multi-outlet users', function () { [$user, $outlet] = createUserWithOutlet(true); // Multiple outlets $this->actingAs($user); Livewire::test(Expense::class) ->set('form.description', 'Test Expense') ->set('form.amount', '50000') // outlet_id not set ->call('create') ->assertHasErrors(['form.outlet_id']); }); it('does not require manually selecting outlet_id for single-outlet users', function () { [$user, $outlet] = createUserWithOutlet(false); $this->actingAs($user); Livewire::test(Expense::class) ->set('form.description', 'Test Expense') ->set('form.amount', '50000') ->call('create') ->assertHasNoErrors(['form.outlet_id']); }); it('can create an expense', function () { [$user, $outlet] = createUserWithOutlet(); $this->actingAs($user); Livewire::test(Expense::class) ->set('form.description', 'Buy Coffee') ->set('form.amount', '25000') ->call('create') ->assertDispatched('refreshDatatable'); $this->assertDatabaseHas('expenses', [ 'description' => 'Buy Coffee', 'amount' => 25000, 'user_id' => $user->id, 'outlet_id' => $outlet->id, ]); }); it('can create an expense with image', function () { [$user, $outlet] = createUserWithOutlet(); $this->actingAs($user); $image = UploadedFile::fake()->image('receipt.jpg'); // The WithMediaHandler contract seems to expect an array with 'path', not raw file object $imageData = [['path' => $image->getPathname()]]; Livewire::test(Expense::class) ->set('form.description', 'Buy Coffee with Receipt') ->set('form.amount', '30000') ->set('form.image', $imageData) ->call('create'); $expense = ExpenseModel::where('description', 'Buy Coffee with Receipt')->first(); expect($expense)->not->toBeNull(); // Assuming media library is set up, verify media count or existence // Note: In-memory/testing sqlite might behave differently with media library if not fully mocked, // but basic relation check is good. expect($expense->getMedia('image'))->toHaveCount(1); }); it('can update an expense', function () { [$user, $outlet] = createUserWithOutlet(); $this->actingAs($user); $expense = ExpenseModel::factory()->create([ 'user_id' => $user->id, 'outlet_id' => $outlet->id, 'description' => 'Old Description', 'amount' => 10000, ]); Livewire::test(Expense::class) ->call('openModal', 'update', 'Edit Expense', $expense->hash) ->assertSet('form.description', 'Old Description') ->set('form.description', 'New Description') ->set('form.amount', '15000') // Updating amount ->call('update') ->assertDispatched('refreshDatatable'); $this->assertDatabaseHas('expenses', [ 'id' => $expense->id, 'description' => 'New Description', 'amount' => 15000, ]); }); it('can update an expense image', function () { [$user, $outlet] = createUserWithOutlet(); $this->actingAs($user); $expense = ExpenseModel::factory()->create([ 'user_id' => $user->id, 'outlet_id' => $outlet->id, 'description' => 'Expense with Image', 'amount' => 10000, ]); // Initial image upload (simulated via factory or direct DB check if needed, // but here we just test REPLACING or ADDING via update) $newImage = UploadedFile::fake()->image('new_receipt.jpg'); $imageData = [['path' => $newImage->getPathname()]]; Livewire::test(Expense::class) ->call('openModal', 'update', 'Edit Expense', $expense->hash) ->set('form.image', $imageData) ->call('update'); $expense->refresh(); expect($expense->getMedia('image'))->toHaveCount(1); // In a real scenario we'd check filename, but hash names make it tricky in tests without mocking 'fake' }); it('can delete an expense', function () { [$user, $outlet] = createUserWithOutlet(); $this->actingAs($user); $expense = ExpenseModel::factory()->create([ 'user_id' => $user->id, 'outlet_id' => $outlet->id, ]); Livewire::test(Expense::class) ->call('delete', $expense->hash) ->assertDispatched('refreshDatatable'); $this->assertSoftDeleted('expenses', [ 'id' => $expense->id, ]); }); // AUTHORIZATION TESTS it('checks authorization for create', function () { [$user, $outlet] = createUserWithOutlet(); $user->revokePermissionTo('create expense'); $this->actingAs($user); Livewire::test(Expense::class) ->call('create') ->assertForbidden(); }); it('checks authorization for update', function () { [$user, $outlet] = createUserWithOutlet(); $user->revokePermissionTo('update expense'); $this->actingAs($user); $expense = ExpenseModel::factory()->create([ 'user_id' => $user->id, 'outlet_id' => $outlet->id, ]); Livewire::test(Expense::class) ->call('openModal', 'update', 'Edit', $expense->hash) // openModal doesn't check perm, update() does ->call('update') ->assertForbidden(); }); it('checks authorization for delete', function () { [$user, $outlet] = createUserWithOutlet(); $user->revokePermissionTo('delete expense'); $this->actingAs($user); $expense = ExpenseModel::factory()->create([ 'user_id' => $user->id, 'outlet_id' => $outlet->id, ]); Livewire::test(Expense::class) ->call('delete', $expense->hash) ->assertForbidden(); });