create(); $role = Role::create(['name' => 'ROLE_ADMIN']); $admin->roles()->attach($role); return $admin; } it('returns forbidden for non admin', function (): void { $controller = new AttachmentGroupController(); $user = User::factory()->create(); $index = Request::create('/api/attachment-groups', 'GET'); $index->setUserResolver(fn () => $user); expect($controller->index($index)->getStatusCode())->toBe(403); $store = Request::create('/api/attachment-groups', 'POST', [ 'name' => 'Images', 'max_size_kb' => 10, 'is_active' => true, ]); $store->setUserResolver(fn () => $user); expect($controller->store($store)->getStatusCode())->toBe(403); $group = AttachmentGroup::create([ 'name' => 'Docs', 'position' => 1, 'max_size_kb' => 10, 'is_active' => true, ]); $update = Request::create('/api/attachment-groups/'.$group->id, 'PATCH', [ 'name' => 'Docs', 'max_size_kb' => 10, 'is_active' => true, ]); $update->setUserResolver(fn () => $user); expect($controller->update($update, $group)->getStatusCode())->toBe(403); $destroy = Request::create('/api/attachment-groups/'.$group->id, 'DELETE'); $destroy->setUserResolver(fn () => $user); expect($controller->destroy($destroy, $group)->getStatusCode())->toBe(403); $reorder = Request::create('/api/attachment-groups/reorder', 'POST', [ 'parentId' => null, 'orderedIds' => [], ]); $reorder->setUserResolver(fn () => $user); expect($controller->reorder($reorder)->getStatusCode())->toBe(403); }); it('stores group and rejects duplicates', function (): void { $controller = new AttachmentGroupController(); $admin = makeAdminForGroupController(); $request = Request::create('/api/attachment-groups', 'POST', [ 'name' => 'Images', 'parent_id' => null, 'max_size_kb' => 100, 'is_active' => true, ]); $request->setUserResolver(fn () => $admin); $response = $controller->store($request); expect($response->getStatusCode())->toBe(201); $response = $controller->store($request); expect($response->getStatusCode())->toBe(422); }); it('updates group and handles parent change position', function (): void { $controller = new AttachmentGroupController(); $admin = makeAdminForGroupController(); $parentA = AttachmentGroup::create([ 'name' => 'Parent A', 'position' => 1, 'max_size_kb' => 100, 'is_active' => true, ]); $parentB = AttachmentGroup::create([ 'name' => 'Parent B', 'position' => 2, 'max_size_kb' => 100, 'is_active' => true, ]); $group = AttachmentGroup::create([ 'name' => 'Docs', 'parent_id' => $parentA->id, 'position' => 1, 'max_size_kb' => 10, 'is_active' => true, ]); $request = Request::create('/api/attachment-groups/'.$group->id, 'PATCH', [ 'name' => 'Docs', 'parent_id' => $parentB->id, 'max_size_kb' => 10, 'is_active' => false, ]); $request->setUserResolver(fn () => $admin); $response = $controller->update($request, $group); expect($response->getStatusCode())->toBe(200); }); it('update rejects duplicate group name', function (): void { $controller = new AttachmentGroupController(); $admin = makeAdminForGroupController(); AttachmentGroup::create([ 'name' => 'Images', 'position' => 1, 'max_size_kb' => 10, 'is_active' => true, ]); $group = AttachmentGroup::create([ 'name' => 'Docs', 'position' => 2, 'max_size_kb' => 10, 'is_active' => true, ]); $request = Request::create('/api/attachment-groups/'.$group->id, 'PATCH', [ 'name' => 'images', 'parent_id' => null, 'max_size_kb' => 10, 'is_active' => true, ]); $request->setUserResolver(fn () => $admin); $response = $controller->update($request, $group); expect($response->getStatusCode())->toBe(422); }); it('destroy returns errors for in-use group', function (): void { $controller = new AttachmentGroupController(); $admin = makeAdminForGroupController(); $group = AttachmentGroup::create([ 'name' => 'Images', 'position' => 1, 'max_size_kb' => 10, 'is_active' => true, ]); AttachmentExtension::create([ 'extension' => 'png', 'attachment_group_id' => $group->id, ]); $request = Request::create('/api/attachment-groups/'.$group->id, 'DELETE'); $request->setUserResolver(fn () => $admin); $response = $controller->destroy($request, $group); expect($response->getStatusCode())->toBe(422); AttachmentExtension::query()->delete(); Attachment::create([ 'thread_id' => null, 'post_id' => null, 'attachment_extension_id' => null, 'attachment_group_id' => $group->id, 'user_id' => null, 'disk' => 'local', 'path' => 'attachments/test.txt', 'original_name' => 'test.txt', 'extension' => 'txt', 'mime_type' => 'text/plain', 'size_bytes' => 1, ]); $response = $controller->destroy($request, $group); expect($response->getStatusCode())->toBe(422); }); it('destroy deletes empty group', function (): void { $controller = new AttachmentGroupController(); $admin = makeAdminForGroupController(); $group = AttachmentGroup::create([ 'name' => 'Empty', 'position' => 1, 'max_size_kb' => 10, 'is_active' => true, ]); $request = Request::create('/api/attachment-groups/'.$group->id, 'DELETE'); $request->setUserResolver(fn () => $admin); $response = $controller->destroy($request, $group); expect($response->getStatusCode())->toBe(204); }); it('reorders groups with string parent id handling', function (): void { $controller = new AttachmentGroupController(); $admin = makeAdminForGroupController(); $groupA = AttachmentGroup::create([ 'name' => 'A', 'position' => 1, 'max_size_kb' => 10, 'is_active' => true, ]); $groupB = AttachmentGroup::create([ 'name' => 'B', 'position' => 2, 'max_size_kb' => 10, 'is_active' => true, ]); $request = Request::create('/api/attachment-groups/reorder', 'POST', [ 'parentId' => 'null', 'orderedIds' => [$groupB->id, $groupA->id], ]); $request->setUserResolver(fn () => $admin); $response = $controller->reorder($request); expect($response->getStatusCode())->toBe(200); $groupA->refresh(); $groupB->refresh(); expect($groupB->position)->toBe(1); expect($groupA->position)->toBe(2); }); it('reorders groups with numeric parent id string', function (): void { $controller = new AttachmentGroupController(); $admin = makeAdminForGroupController(); $parent = AttachmentGroup::create([ 'name' => 'Parent', 'position' => 1, 'max_size_kb' => 10, 'is_active' => true, ]); $groupA = AttachmentGroup::create([ 'name' => 'A', 'parent_id' => $parent->id, 'position' => 1, 'max_size_kb' => 10, 'is_active' => true, ]); $groupB = AttachmentGroup::create([ 'name' => 'B', 'parent_id' => $parent->id, 'position' => 2, 'max_size_kb' => 10, 'is_active' => true, ]); $request = Request::create('/api/attachment-groups/reorder', 'POST', [ 'parentId' => (string) $parent->id, 'orderedIds' => [$groupB->id, $groupA->id], ]); $request->setUserResolver(fn () => $admin); $response = $controller->reorder($request); expect($response->getStatusCode())->toBe(200); }); it('normalizeParentId handles empty and null values', function (): void { $controller = new AttachmentGroupController(); $ref = new ReflectionMethod($controller, 'normalizeParentId'); $ref->setAccessible(true); expect($ref->invoke($controller, ''))->toBeNull(); expect($ref->invoke($controller, 'null'))->toBeNull(); expect($ref->invoke($controller, null))->toBeNull(); });