forgetInstance(Gate::class); }); afterEach(function () { Mockery::close(); }); /** * Create a mock user that can/cannot perform an action. */ function mockUserWithPermission(bool $canPerform): void { $user = Mockery::mock(Authorizable::class); $user->shouldReceive('can') ->andReturn($canPerform); test()->actingAs($user); } /** * Create a mock resource for testing authorization. */ function mockResource(): object { return new class { public int $id = 1; public int $workspace_id = 1; }; } // ============================================================================= // Button Component Authorization Tests // ============================================================================= describe('Button component authorization', function () { it('is enabled when no authorization props are provided', function () { mockUserWithPermission(true); $button = new Button( type: 'submit', variant: 'primary', ); expect($button->disabled)->toBeFalse() ->and($button->hidden)->toBeFalse(); }); it('is enabled when user has permission', function () { mockUserWithPermission(true); $button = new Button( type: 'submit', variant: 'primary', canGate: 'update', canResource: mockResource(), ); expect($button->disabled)->toBeFalse() ->and($button->hidden)->toBeFalse(); }); it('is disabled when user lacks permission', function () { mockUserWithPermission(false); $button = new Button( type: 'submit', variant: 'primary', canGate: 'update', canResource: mockResource(), ); expect($button->disabled)->toBeTrue() ->and($button->hidden)->toBeFalse(); }); it('is hidden when user lacks permission and canHide is true', function () { mockUserWithPermission(false); $button = new Button( type: 'submit', variant: 'danger', canGate: 'delete', canResource: mockResource(), canHide: true, ); expect($button->disabled)->toBeTrue() ->and($button->hidden)->toBeTrue(); }); it('is visible when user has permission and canHide is true', function () { mockUserWithPermission(true); $button = new Button( type: 'submit', variant: 'danger', canGate: 'delete', canResource: mockResource(), canHide: true, ); expect($button->disabled)->toBeFalse() ->and($button->hidden)->toBeFalse(); }); it('respects explicit disabled state over authorization', function () { mockUserWithPermission(true); $button = new Button( type: 'submit', variant: 'primary', disabled: true, canGate: 'update', canResource: mockResource(), ); // Even with permission, explicit disabled takes precedence expect($button->disabled)->toBeTrue(); }); it('does not check authorization when only canGate is provided', function () { mockUserWithPermission(false); // Without canResource, authorization check should not happen $button = new Button( type: 'submit', variant: 'primary', canGate: 'update', canResource: null, ); expect($button->disabled)->toBeFalse(); }); it('is disabled when no authenticated user', function () { // No user authenticated $button = new Button( type: 'submit', variant: 'primary', canGate: 'update', canResource: mockResource(), ); // Should be disabled when no user is authenticated expect($button->disabled)->toBeTrue(); }); }); // ============================================================================= // Input Component Authorization Tests // ============================================================================= describe('Input component authorization', function () { it('is enabled when no authorization props are provided', function () { mockUserWithPermission(true); $input = new Input( id: 'name', label: 'Name', ); expect($input->disabled)->toBeFalse() ->and($input->hidden)->toBeFalse(); }); it('is enabled when user has permission', function () { mockUserWithPermission(true); $input = new Input( id: 'name', label: 'Name', canGate: 'update', canResource: mockResource(), ); expect($input->disabled)->toBeFalse() ->and($input->hidden)->toBeFalse(); }); it('is disabled when user lacks permission', function () { mockUserWithPermission(false); $input = new Input( id: 'name', label: 'Name', canGate: 'update', canResource: mockResource(), ); expect($input->disabled)->toBeTrue() ->and($input->hidden)->toBeFalse(); }); it('is hidden when user lacks permission and canHide is true', function () { mockUserWithPermission(false); $input = new Input( id: 'secret_key', label: 'Secret Key', canGate: 'viewSecrets', canResource: mockResource(), canHide: true, ); expect($input->disabled)->toBeTrue() ->and($input->hidden)->toBeTrue(); }); it('respects explicit disabled state', function () { mockUserWithPermission(true); $input = new Input( id: 'readonly_field', label: 'Read Only', disabled: true, canGate: 'update', canResource: mockResource(), ); expect($input->disabled)->toBeTrue(); }); }); // ============================================================================= // Select Component Authorization Tests // ============================================================================= describe('Select component authorization', function () { it('is enabled when no authorization props are provided', function () { mockUserWithPermission(true); $select = new Select( id: 'status', options: ['draft' => 'Draft', 'published' => 'Published'], label: 'Status', ); expect($select->disabled)->toBeFalse() ->and($select->hidden)->toBeFalse(); }); it('is enabled when user has permission', function () { mockUserWithPermission(true); $select = new Select( id: 'status', options: ['draft' => 'Draft', 'published' => 'Published'], label: 'Status', canGate: 'update', canResource: mockResource(), ); expect($select->disabled)->toBeFalse() ->and($select->hidden)->toBeFalse(); }); it('is disabled when user lacks permission', function () { mockUserWithPermission(false); $select = new Select( id: 'status', options: ['draft' => 'Draft', 'published' => 'Published'], label: 'Status', canGate: 'update', canResource: mockResource(), ); expect($select->disabled)->toBeTrue() ->and($select->hidden)->toBeFalse(); }); it('is hidden when user lacks permission and canHide is true', function () { mockUserWithPermission(false); $select = new Select( id: 'role', options: ['admin' => 'Admin', 'user' => 'User'], label: 'Role', canGate: 'assignRoles', canResource: mockResource(), canHide: true, ); expect($select->disabled)->toBeTrue() ->and($select->hidden)->toBeTrue(); }); it('respects explicit disabled state', function () { mockUserWithPermission(true); $select = new Select( id: 'locked_field', options: ['a' => 'A', 'b' => 'B'], label: 'Locked', disabled: true, canGate: 'update', canResource: mockResource(), ); expect($select->disabled)->toBeTrue(); }); }); // ============================================================================= // Checkbox Component Authorization Tests // ============================================================================= describe('Checkbox component authorization', function () { it('is enabled when no authorization props are provided', function () { mockUserWithPermission(true); $checkbox = new Checkbox( id: 'is_active', label: 'Active', ); expect($checkbox->disabled)->toBeFalse() ->and($checkbox->hidden)->toBeFalse(); }); it('is enabled when user has permission', function () { mockUserWithPermission(true); $checkbox = new Checkbox( id: 'is_active', label: 'Active', canGate: 'update', canResource: mockResource(), ); expect($checkbox->disabled)->toBeFalse() ->and($checkbox->hidden)->toBeFalse(); }); it('is disabled when user lacks permission', function () { mockUserWithPermission(false); $checkbox = new Checkbox( id: 'is_active', label: 'Active', canGate: 'update', canResource: mockResource(), ); expect($checkbox->disabled)->toBeTrue() ->and($checkbox->hidden)->toBeFalse(); }); it('is hidden when user lacks permission and canHide is true', function () { mockUserWithPermission(false); $checkbox = new Checkbox( id: 'is_admin', label: 'Administrator', canGate: 'promoteToAdmin', canResource: mockResource(), canHide: true, ); expect($checkbox->disabled)->toBeTrue() ->and($checkbox->hidden)->toBeTrue(); }); it('respects explicit disabled state', function () { mockUserWithPermission(true); $checkbox = new Checkbox( id: 'locked_option', label: 'Locked', disabled: true, canGate: 'update', canResource: mockResource(), ); expect($checkbox->disabled)->toBeTrue(); }); }); // ============================================================================= // Toggle Component Authorization Tests // ============================================================================= describe('Toggle component authorization', function () { it('is enabled when no authorization props are provided', function () { mockUserWithPermission(true); $toggle = new Toggle( id: 'is_public', label: 'Public', ); expect($toggle->disabled)->toBeFalse() ->and($toggle->hidden)->toBeFalse(); }); it('is enabled when user has permission', function () { mockUserWithPermission(true); $toggle = new Toggle( id: 'is_public', label: 'Public', canGate: 'update', canResource: mockResource(), ); expect($toggle->disabled)->toBeFalse() ->and($toggle->hidden)->toBeFalse(); }); it('is disabled when user lacks permission', function () { mockUserWithPermission(false); $toggle = new Toggle( id: 'is_public', label: 'Public', canGate: 'update', canResource: mockResource(), ); expect($toggle->disabled)->toBeTrue() ->and($toggle->hidden)->toBeFalse(); }); it('is hidden when user lacks permission and canHide is true', function () { mockUserWithPermission(false); $toggle = new Toggle( id: 'enable_feature', label: 'Enable Feature', canGate: 'manageFeatures', canResource: mockResource(), canHide: true, ); expect($toggle->disabled)->toBeTrue() ->and($toggle->hidden)->toBeTrue(); }); it('respects explicit disabled state', function () { mockUserWithPermission(true); $toggle = new Toggle( id: 'locked_toggle', label: 'Locked', disabled: true, canGate: 'update', canResource: mockResource(), ); expect($toggle->disabled)->toBeTrue(); }); it('wireChange returns null when instantSave is disabled', function () { $toggle = new Toggle( id: 'test', instantSave: false, ); expect($toggle->wireChange())->toBeNull(); }); it('wireChange returns default save method when instantSave is enabled', function () { $toggle = new Toggle( id: 'test', instantSave: true, ); expect($toggle->wireChange())->toBe('save'); }); it('wireChange returns custom method when specified', function () { $toggle = new Toggle( id: 'test', instantSave: true, instantSaveMethod: 'updateSetting', ); expect($toggle->wireChange())->toBe('updateSetting'); }); }); // ============================================================================= // Textarea Component Authorization Tests // ============================================================================= describe('Textarea component authorization', function () { it('is enabled when no authorization props are provided', function () { mockUserWithPermission(true); $textarea = new Textarea( id: 'description', label: 'Description', ); expect($textarea->disabled)->toBeFalse() ->and($textarea->hidden)->toBeFalse(); }); it('is enabled when user has permission', function () { mockUserWithPermission(true); $textarea = new Textarea( id: 'description', label: 'Description', canGate: 'update', canResource: mockResource(), ); expect($textarea->disabled)->toBeFalse() ->and($textarea->hidden)->toBeFalse(); }); it('is disabled when user lacks permission', function () { mockUserWithPermission(false); $textarea = new Textarea( id: 'description', label: 'Description', canGate: 'update', canResource: mockResource(), ); expect($textarea->disabled)->toBeTrue() ->and($textarea->hidden)->toBeFalse(); }); it('is hidden when user lacks permission and canHide is true', function () { mockUserWithPermission(false); $textarea = new Textarea( id: 'internal_notes', label: 'Internal Notes', canGate: 'viewInternalNotes', canResource: mockResource(), canHide: true, ); expect($textarea->disabled)->toBeTrue() ->and($textarea->hidden)->toBeTrue(); }); it('respects explicit disabled state', function () { mockUserWithPermission(true); $textarea = new Textarea( id: 'readonly_notes', label: 'Notes', disabled: true, canGate: 'update', canResource: mockResource(), ); expect($textarea->disabled)->toBeTrue(); }); }); // ============================================================================= // Workspace Context Tests // ============================================================================= describe('Workspace context in authorization', function () { it('Button works with workspace-scoped resource', function () { $workspaceResource = new class { public int $id = 1; public int $workspace_id = 42; public string $name = 'Test Resource'; }; mockUserWithPermission(true); $button = new Button( type: 'submit', variant: 'primary', canGate: 'update', canResource: $workspaceResource, ); expect($button->disabled)->toBeFalse() ->and($button->canResource)->toBe($workspaceResource) ->and($button->canResource->workspace_id)->toBe(42); }); it('Input works with workspace-scoped resource', function () { $workspaceResource = new class { public int $id = 1; public int $workspace_id = 42; }; mockUserWithPermission(false); $input = new Input( id: 'workspace_field', label: 'Workspace Field', canGate: 'update', canResource: $workspaceResource, ); expect($input->disabled)->toBeTrue() ->and($input->canResource->workspace_id)->toBe(42); }); }); // ============================================================================= // Edge Cases and Boundary Tests // ============================================================================= describe('Edge cases in authorization', function () { it('button with null resource does not check authorization', function () { $button = new Button( type: 'submit', variant: 'primary', canGate: 'update', canResource: null, ); expect($button->disabled)->toBeFalse() ->and($button->hidden)->toBeFalse(); }); it('button with empty canGate does not check authorization', function () { mockUserWithPermission(false); $button = new Button( type: 'submit', variant: 'primary', canGate: '', canResource: mockResource(), ); // Empty gate = no check = enabled expect($button->disabled)->toBeFalse(); }); it('canHide without canGate does nothing', function () { $button = new Button( type: 'submit', variant: 'primary', canHide: true, ); expect($button->hidden)->toBeFalse(); }); it('canHide without canResource does nothing', function () { $button = new Button( type: 'submit', variant: 'primary', canGate: 'delete', canResource: null, canHide: true, ); expect($button->hidden)->toBeFalse(); }); }); // ============================================================================= // Cross-Component Consistency Tests // ============================================================================= describe('Cross-component consistency', function () { it('all components disable consistently when user lacks permission', function () { mockUserWithPermission(false); $resource = mockResource(); $components = [ 'Button' => new Button(type: 'submit', variant: 'primary', canGate: 'update', canResource: $resource), 'Input' => new Input(id: 'test', canGate: 'update', canResource: $resource), 'Select' => new Select(id: 'test', options: [], canGate: 'update', canResource: $resource), 'Checkbox' => new Checkbox(id: 'test', canGate: 'update', canResource: $resource), 'Toggle' => new Toggle(id: 'test', canGate: 'update', canResource: $resource), 'Textarea' => new Textarea(id: 'test', canGate: 'update', canResource: $resource), ]; foreach ($components as $name => $component) { expect($component->disabled)->toBeTrue("$name should be disabled when user lacks permission"); expect($component->hidden)->toBeFalse("$name should not be hidden without canHide flag"); } }); it('all components hide consistently when canHide is true', function () { mockUserWithPermission(false); $resource = mockResource(); $components = [ 'Button' => new Button(type: 'submit', variant: 'primary', canGate: 'update', canResource: $resource, canHide: true), 'Input' => new Input(id: 'test', canGate: 'update', canResource: $resource, canHide: true), 'Select' => new Select(id: 'test', options: [], canGate: 'update', canResource: $resource, canHide: true), 'Checkbox' => new Checkbox(id: 'test', canGate: 'update', canResource: $resource, canHide: true), 'Toggle' => new Toggle(id: 'test', canGate: 'update', canResource: $resource, canHide: true), 'Textarea' => new Textarea(id: 'test', canGate: 'update', canResource: $resource, canHide: true), ]; foreach ($components as $name => $component) { expect($component->disabled)->toBeTrue("$name should be disabled"); expect($component->hidden)->toBeTrue("$name should be hidden with canHide flag"); } }); it('all components enable consistently when user has permission', function () { mockUserWithPermission(true); $resource = mockResource(); $components = [ 'Button' => new Button(type: 'submit', variant: 'primary', canGate: 'update', canResource: $resource), 'Input' => new Input(id: 'test', canGate: 'update', canResource: $resource), 'Select' => new Select(id: 'test', options: [], canGate: 'update', canResource: $resource), 'Checkbox' => new Checkbox(id: 'test', canGate: 'update', canResource: $resource), 'Toggle' => new Toggle(id: 'test', canGate: 'update', canResource: $resource), 'Textarea' => new Textarea(id: 'test', canGate: 'update', canResource: $resource), ]; foreach ($components as $name => $component) { expect($component->disabled)->toBeFalse("$name should be enabled when user has permission"); expect($component->hidden)->toBeFalse("$name should be visible when user has permission"); } }); });