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');
});