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 makeForum(): 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('creates a thread inside a forum', function (): void { $user = User::factory()->create(); Sanctum::actingAs($user); $forum = makeForum(); $response = $this->postJson('/api/threads', [ 'title' => 'First Thread', 'body' => 'Hello world', 'forum' => "/api/forums/{$forum->id}", ]); $response->assertStatus(201); $response->assertJsonFragment([ 'title' => 'First Thread', 'forum' => "/api/forums/{$forum->id}", ]); $this->assertDatabaseHas('threads', [ 'forum_id' => $forum->id, 'user_id' => $user->id, 'title' => 'First Thread', ]); }); it('rejects creating threads in a category', function (): void { $user = User::factory()->create(); Sanctum::actingAs($user); $category = Forum::create([ 'name' => 'Category Only', 'description' => null, 'type' => 'category', 'parent_id' => null, 'position' => 1, ]); $response = $this->postJson('/api/threads', [ 'title' => 'Nope', 'body' => 'Not allowed', 'forum' => "/api/forums/{$category->id}", ]); $response->assertStatus(422); $response->assertJsonFragment(['message' => 'Threads can only be created inside forums.']); }); it('requires authentication to update a thread', function (): void { $forum = makeForum(); $owner = User::factory()->create(); $thread = Thread::create([ 'forum_id' => $forum->id, 'user_id' => $owner->id, 'title' => 'Original', 'body' => '', ]); $response = $this->patchJson("/api/threads/{$thread->id}", [ 'title' => 'Updated', 'body' => 'Updated body', ]); $response->assertStatus(401); }); it('enforces thread update permissions', function (): void { $forum = makeForum(); $owner = User::factory()->create(); $other = User::factory()->create(); $thread = Thread::create([ 'forum_id' => $forum->id, 'user_id' => $owner->id, 'title' => 'Original', 'body' => '', ]); Sanctum::actingAs($other); $response = $this->patchJson("/api/threads/{$thread->id}", [ 'title' => 'Updated', 'body' => 'Updated body', ]); $response->assertStatus(403); Sanctum::actingAs($owner); $response = $this->patchJson("/api/threads/{$thread->id}", [ 'title' => 'Owner Update', 'body' => 'Owner body', ]); $response->assertOk(); $this->assertDatabaseHas('threads', [ 'id' => $thread->id, 'title' => 'Owner Update', 'body' => 'Owner body', ]); $admin = User::factory()->create(); $role = Role::create(['name' => 'ROLE_ADMIN', 'color' => '#111111']); $admin->roles()->attach($role); Sanctum::actingAs($admin); $response = $this->patchJson("/api/threads/{$thread->id}", [ 'title' => 'Admin Update', 'body' => 'Admin body', ]); $response->assertOk(); $this->assertDatabaseHas('threads', [ 'id' => $thread->id, 'title' => 'Admin Update', 'body' => 'Admin body', ]); }); it('enforces solved status permissions', function (): void { $forum = makeForum(); $owner = User::factory()->create(); $other = User::factory()->create(); $thread = Thread::create([ 'forum_id' => $forum->id, 'user_id' => $owner->id, 'title' => 'Original', 'body' => '', 'solved' => false, ]); Sanctum::actingAs($other); $response = $this->patchJson("/api/threads/{$thread->id}/solved", [ 'solved' => true, ]); $response->assertStatus(403); Sanctum::actingAs($owner); $response = $this->patchJson("/api/threads/{$thread->id}/solved", [ 'solved' => true, ]); $response->assertOk(); $this->assertDatabaseHas('threads', [ 'id' => $thread->id, 'solved' => 1, ]); }); it('filters threads by forum', function (): void { $forumA = makeForum(); $forumB = makeForum(); $threadA = Thread::create([ 'forum_id' => $forumA->id, 'user_id' => null, 'title' => 'Thread A', 'body' => '', ]); Thread::create([ 'forum_id' => $forumB->id, 'user_id' => null, 'title' => 'Thread B', 'body' => '', ]); $response = $this->getJson("/api/threads?forum=/api/forums/{$forumA->id}"); $response->assertOk(); $response->assertJsonCount(1); $response->assertJsonFragment([ 'id' => $threadA->id, 'title' => 'Thread A', ]); }); it('increments views count when showing a thread', function (): void { $forum = makeForum(); $thread = Thread::create([ 'forum_id' => $forum->id, 'user_id' => null, 'title' => 'Viewed Thread', 'body' => '', 'views_count' => 0, ]); $response = $this->getJson("/api/threads/{$thread->id}"); $response->assertOk(); $response->assertJsonFragment([ 'id' => $thread->id, 'views_count' => 1, ]); $thread->refresh(); expect($thread->views_count)->toBe(1); }); it('soft deletes a thread and tracks deleted_by', function (): void { $forum = makeForum(); $user = User::factory()->create(); $thread = Thread::create([ 'forum_id' => $forum->id, 'user_id' => $user->id, 'title' => 'Delete Me', 'body' => 'Body', ]); Sanctum::actingAs($user); $response = $this->deleteJson("/api/threads/{$thread->id}"); $response->assertStatus(204); $this->assertSoftDeleted('threads', [ 'id' => $thread->id, ]); $this->assertDatabaseHas('threads', [ 'id' => $thread->id, 'deleted_by' => $user->id, ]); });