setAccessible(true); $parserProp->setValue( \Mockery::mock(\s9e\TextFormatter\Parser::class) ->shouldReceive('parse') ->andReturn('') ->getMock() ); $rendererProp = new ReflectionProperty(\App\Actions\BbcodeFormatter::class, 'renderer'); $rendererProp->setAccessible(true); $rendererProp->setValue( \Mockery::mock(\s9e\TextFormatter\Renderer::class) ->shouldReceive('render') ->andReturn('

') ->getMock() ); }); afterEach(function (): void { \Mockery::close(); }); function makeForumForPostController(): Forum { $category = Forum::create([ 'name' => 'Category', 'description' => null, 'type' => 'category', 'parent_id' => null, 'position' => 1, ]); return Forum::create([ 'name' => 'Forum', 'description' => null, 'type' => 'forum', 'parent_id' => $category->id, 'position' => 1, ]); } it('returns unauthorized on update when no user', function (): void { $controller = new PostController(); $forum = makeForumForPostController(); $thread = Thread::create([ 'forum_id' => $forum->id, 'user_id' => null, 'title' => 'Thread', 'body' => 'Body', ]); $post = Post::create([ 'thread_id' => $thread->id, 'user_id' => null, 'body' => 'Body', ]); $request = Request::create('/api/posts/'.$post->id, 'PATCH', ['body' => 'x']); $request->setUserResolver(fn () => null); $response = $controller->update($request, $post); expect($response->getStatusCode())->toBe(401); }); it('returns forbidden on update when user is not owner or admin', function (): void { $controller = new PostController(); $forum = makeForumForPostController(); $owner = User::factory()->create(); $viewer = User::factory()->create(); $thread = Thread::create([ 'forum_id' => $forum->id, 'user_id' => $owner->id, 'title' => 'Thread', 'body' => 'Body', ]); $post = Post::create([ 'thread_id' => $thread->id, 'user_id' => $owner->id, 'body' => 'Body', ]); $request = Request::create('/api/posts/'.$post->id, 'PATCH', ['body' => 'x']); $request->setUserResolver(fn () => $viewer); $response = $controller->update($request, $post); expect($response->getStatusCode())->toBe(403); }); it('updates post when user is owner', function (): void { $controller = new PostController(); $forum = makeForumForPostController(); $owner = User::factory()->create(); $thread = Thread::create([ 'forum_id' => $forum->id, 'user_id' => $owner->id, 'title' => 'Thread', 'body' => 'Body', ]); $post = Post::create([ 'thread_id' => $thread->id, 'user_id' => $owner->id, 'body' => 'Body', ]); $request = Request::create('/api/posts/'.$post->id, 'PATCH', ['body' => 'Updated']); $request->setUserResolver(fn () => $owner); $response = $controller->update($request, $post); expect($response->getStatusCode())->toBe(200); $post->refresh(); expect($post->body)->toBe('Updated'); }); it('parseIriId handles empty and numeric values', function (): void { $controller = new PostController(); $ref = new ReflectionMethod($controller, 'parseIriId'); $ref->setAccessible(true); expect($ref->invoke($controller, null))->toBeNull(); expect($ref->invoke($controller, ''))->toBeNull(); expect($ref->invoke($controller, '/threads/12'))->toBe(12); expect($ref->invoke($controller, '7'))->toBe(7); expect($ref->invoke($controller, 'abc'))->toBeNull(); }); it('serializes posts with attachments and rank data', function (): void { $forum = makeForumForPostController(); $role = Role::create(['name' => 'ROLE_MOD', 'color' => '#00ff00']); $rank = Rank::create(['name' => 'Gold', 'badge_image_path' => 'ranks/badge.png']); $user = User::factory()->create([ 'rank_id' => $rank->id, 'avatar_path' => 'avatars/u.png', 'location' => 'Here', ]); $user->roles()->attach($role); $thread = Thread::create([ 'forum_id' => $forum->id, 'user_id' => $user->id, 'title' => 'Thread', 'body' => 'Body', ]); $post = Post::create([ 'thread_id' => $thread->id, 'user_id' => $user->id, 'body' => 'See [attachment]file.png[/attachment]', ]); $group = AttachmentGroup::create([ 'name' => 'Images', 'max_size_kb' => 100, 'is_active' => true, ]); $attachment = Attachment::create([ 'thread_id' => null, 'post_id' => $post->id, 'attachment_extension_id' => null, 'attachment_group_id' => $group->id, 'user_id' => $user->id, 'disk' => 'local', 'path' => 'attachments/posts/'.$post->id.'/file.png', 'thumbnail_path' => 'attachments/posts/'.$post->id.'/thumb.png', 'original_name' => 'file.png', 'extension' => 'png', 'mime_type' => 'image/png', 'size_bytes' => 10, ]); $post->load(['user.rank', 'user.roles', 'attachments.group']); $controller = new PostController(); $ref = new ReflectionMethod($controller, 'serializePost'); $ref->setAccessible(true); $payload = $ref->invoke($controller, $post); expect($payload['user_rank_badge_url'])->not->toBeNull(); expect($payload['user_group_color'])->toBe('#00ff00'); expect($payload['attachments'][0]['group']['name'])->toBe('Images'); expect($payload['attachments'][0]['thumbnail_url'])->toContain('/thumbnail'); }); it('serializes posts with null user and no attachments', function (): void { $forum = makeForumForPostController(); $thread = Thread::create([ 'forum_id' => $forum->id, 'user_id' => null, 'title' => 'Thread', 'body' => 'Body', ]); $post = Post::create([ 'thread_id' => $thread->id, 'user_id' => null, 'body' => 'Body', ]); $controller = new PostController(); $ref = new ReflectionMethod($controller, 'serializePost'); $ref->setAccessible(true); $payload = $ref->invoke($controller, $post); expect($payload['user_avatar_url'])->toBeNull(); expect($payload['user_rank_badge_url'])->toBeNull(); expect($payload['user_group_color'])->toBeNull(); expect($payload['attachments'])->toBe([]); }); it('replaceAttachmentTags handles inline images and links', function (): void { $controller = new PostController(); $ref = new ReflectionMethod($controller, 'replaceAttachmentTags'); $ref->setAccessible(true); Setting::updateOrCreate(['key' => 'attachments.display_images_inline'], ['value' => '1']); $attachment = new Attachment([ 'id' => 1, 'original_name' => 'file.png', 'mime_type' => 'image/png', 'thumbnail_path' => null, ]); $body = 'See [attachment]file.png[/attachment]'; $result = $ref->invoke($controller, $body, collect([$attachment])); expect($result)->toContain('[img]'); $attachment->thumbnail_path = 'thumb'; $result = $ref->invoke($controller, $body, collect([$attachment])); expect($result)->toContain('[url='); Setting::updateOrCreate(['key' => 'attachments.display_images_inline'], ['value' => '0']); $result = $ref->invoke($controller, $body, collect([$attachment])); expect($result)->toContain('[url='); $result = $ref->invoke($controller, 'No match', collect([$attachment])); expect($result)->toContain('No match'); }); it('replaceAttachmentTags returns original tag when attachment name missing in map', function (): void { $controller = new PostController(); $ref = new ReflectionMethod($controller, 'replaceAttachmentTags'); $ref->setAccessible(true); Setting::updateOrCreate(['key' => 'attachments.display_images_inline'], ['value' => '1']); $attachment = new Attachment([ 'id' => 2, 'original_name' => 'actual.txt', 'mime_type' => 'text/plain', ]); $body = 'See [attachment]missing.txt[/attachment]'; $result = $ref->invoke($controller, $body, collect([$attachment])); expect($result)->toBe($body); }); it('replaceAttachmentTags renders non-image attachments as links', function (): void { $controller = new PostController(); $ref = new ReflectionMethod($controller, 'replaceAttachmentTags'); $ref->setAccessible(true); Setting::updateOrCreate(['key' => 'attachments.display_images_inline'], ['value' => 'yes']); $attachment = new Attachment([ 'id' => 3, 'original_name' => 'doc.txt', 'mime_type' => 'text/plain', ]); $body = 'See [attachment]doc.txt[/attachment]'; $result = $ref->invoke($controller, $body, collect([$attachment])); expect($result)->toContain('[url='); expect($result)->toContain('doc.txt'); }); it('replaceAttachmentTags returns body when no attachments or map empty', function (): void { $controller = new PostController(); $ref = new ReflectionMethod($controller, 'replaceAttachmentTags'); $ref->setAccessible(true); expect($ref->invoke($controller, 'Body', []))->toBe('Body'); $attachment = new Attachment([ 'original_name' => '', ]); expect($ref->invoke($controller, 'Body', collect([$attachment])))->toBe('Body'); }); it('displayImagesInline defaults to true when missing setting', function (): void { Setting::where('key', 'attachments.display_images_inline')->delete(); $controller = new PostController(); $ref = new ReflectionMethod($controller, 'displayImagesInline'); $ref->setAccessible(true); expect($ref->invoke($controller))->toBeTrue(); }); it('displayImagesInline returns false for off values', function (): void { Setting::updateOrCreate(['key' => 'attachments.display_images_inline'], ['value' => 'off']); $controller = new PostController(); $ref = new ReflectionMethod($controller, 'displayImagesInline'); $ref->setAccessible(true); expect($ref->invoke($controller))->toBeFalse(); }); it('resolveGroupColor returns null for missing roles', function (): void { $controller = new PostController(); $user = User::factory()->create(); $user->setRelation('roles', null); $ref = new ReflectionMethod($controller, 'resolveGroupColor'); $ref->setAccessible(true); expect($ref->invoke($controller, $user))->toBeNull(); }); it('resolveGroupColor returns first sorted role color', function (): void { $controller = new PostController(); $user = User::factory()->create(); $roleB = Role::create(['name' => 'ROLE_B', 'color' => '#bbbbbb']); $roleA = Role::create(['name' => 'ROLE_A', 'color' => '#aaaaaa']); $user->roles()->attach([$roleB->id, $roleA->id]); $user->load('roles'); $ref = new ReflectionMethod($controller, 'resolveGroupColor'); $ref->setAccessible(true); expect($ref->invoke($controller, $user))->toBe('#aaaaaa'); });