create(); $this->get(route('support.index'))->assertRedirect(route('login')); $this->post(route('support.store'))->assertRedirect(route('login')); $this->get(route('support.show', $conversation))->assertRedirect(route('login')); } public function test_authenticated_user_can_create_support_conversation_with_attachment(): void { Storage::fake('local'); $user = User::factory()->create(['can_create_channel' => false]); $attachment = UploadedFile::fake()->create('channel-request.pdf', 204800, 'application/pdf'); $jsonAttachment = UploadedFile::fake()->create('payload.json', 1, 'application/json'); $this->actingAs($user) ->post(route('support.store'), [ 'category' => SupportConversation::CATEGORY_CHANNEL_REQUEST, 'subject' => 'Please approve my creator channel', 'body' => 'I want to start streaming tutorials.', 'attachments' => [$attachment, $jsonAttachment], ]) ->assertRedirect() ->assertSessionHasNoErrors(); $conversation = SupportConversation::query()->firstOrFail(); $message = SupportMessage::query()->firstOrFail(); $storedAttachment = SupportAttachment::query()->firstOrFail(); $this->assertSame($user->id, $conversation->user_id); $this->assertSame(SupportConversation::CATEGORY_CHANNEL_REQUEST, $conversation->category); $this->assertSame('I want to start streaming tutorials.', $message->body); $this->assertFalse($user->refresh()->can_create_channel); $this->assertSame('channel-request.pdf', $storedAttachment->original_name); $this->assertDatabaseHas('support_attachments', [ 'support_message_id' => $message->id, 'original_name' => 'payload.json', 'mime_type' => 'application/json', ]); Storage::disk('local')->assertExists($storedAttachment->path); } public function test_support_conversation_validation_errors_are_returned(): void { $user = User::factory()->create(); $attachment = UploadedFile::fake()->create('script.exe', 1, 'application/x-msdownload'); $this->actingAs($user) ->post(route('support.store'), [ 'category' => 'billing', 'subject' => '', 'body' => '', 'attachments' => [$attachment], ]) ->assertSessionHasErrors([ 'category', 'subject', 'body', 'attachments.0' => __('Unsupported attachment type. Upload media, PDF, text/data, Office, OpenDocument, or archive files.'), ]); } public function test_media_attachment_must_not_exceed_two_hundred_megabytes(): void { $user = User::factory()->create(); $attachment = UploadedFile::fake()->image('too-large.jpg')->size(204801); $this->actingAs($user) ->post(route('support.store'), [ 'category' => SupportConversation::CATEGORY_BUG, 'subject' => 'Large upload', 'body' => 'This screenshot is too large.', 'attachments' => [$attachment], ]) ->assertSessionHasErrors([ 'attachments.0' => __('Attachments must be 200 MB or smaller.'), ]); } public function test_user_can_view_and_reply_to_own_conversation(): void { $user = User::factory()->create(); $conversation = SupportConversation::factory()->for($user)->create([ 'subject' => 'Playback problem', ]); SupportMessage::factory()->for($conversation, 'conversation')->for($user)->create([ 'body' => 'Video stalls after one minute.', ]); $this->actingAs($user) ->get(route('support.show', $conversation)) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('support/show') ->where('conversation.subject', 'Playback problem') ->where('conversation.messages.0.body', 'Video stalls after one minute.'), ); $this->actingAs($user) ->post(route('support.messages.store', $conversation), [ 'body' => 'It also happens in another browser.', ]) ->assertRedirect() ->assertSessionHasNoErrors(); $this->assertDatabaseHas('support_messages', [ 'support_conversation_id' => $conversation->id, 'user_id' => $user->id, 'body' => 'It also happens in another browser.', ]); } public function test_user_cannot_access_another_users_conversation(): void { $user = User::factory()->create(); $conversation = SupportConversation::factory()->create(); $this->actingAs($user) ->get(route('support.show', $conversation)) ->assertForbidden(); $this->actingAs($user) ->post(route('support.messages.store', $conversation), [ 'body' => 'Trying to reply.', ]) ->assertForbidden(); } public function test_admin_can_view_reply_and_close_conversation(): void { $admin = User::factory()->create(['is_admin' => true]); $user = User::factory()->create(['email' => 'viewer@example.com']); $conversation = SupportConversation::factory()->for($user)->create([ 'subject' => 'Bug report', ]); SupportMessage::factory()->for($conversation, 'conversation')->for($user)->create([ 'body' => 'The chat panel overlaps the video.', ]); $this->actingAs($admin) ->get(route('admin.support.index')) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('admin/support/index') ->where('conversations.0.user.email', 'viewer@example.com'), ); $this->actingAs($admin) ->get(route('admin.support.show', $conversation)) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('admin/support/show') ->where('conversation.messages.0.body', 'The chat panel overlaps the video.'), ); $this->actingAs($admin) ->post(route('admin.support.messages.store', $conversation), [ 'body' => 'Thanks, we are reviewing it.', ]) ->assertRedirect() ->assertSessionHasNoErrors(); $this->actingAs($admin) ->patch(route('admin.support.update', $conversation), [ 'status' => SupportConversation::STATUS_CLOSED, ]) ->assertRedirect() ->assertSessionHasNoErrors(); $this->assertDatabaseHas('support_messages', [ 'support_conversation_id' => $conversation->id, 'user_id' => $admin->id, 'body' => 'Thanks, we are reviewing it.', ]); $this->assertSame(SupportConversation::STATUS_CLOSED, $conversation->refresh()->status); } public function test_non_admin_cannot_access_admin_support_inbox(): void { $user = User::factory()->create(); $this->actingAs($user) ->get(route('admin.support.index')) ->assertForbidden(); } public function test_private_attachment_download_requires_conversation_access(): void { Storage::fake('local'); $owner = User::factory()->create(); $otherUser = User::factory()->create(); $admin = User::factory()->create(['is_admin' => true]); $conversation = SupportConversation::factory()->for($owner)->create(); $message = SupportMessage::factory()->for($conversation, 'conversation')->for($owner)->create(); $attachment = SupportAttachment::factory()->for($message, 'message')->create([ 'path' => 'support/testing/debug.log', 'original_name' => 'debug.log', ]); Storage::disk('local')->put($attachment->path, 'log contents'); $this->actingAs($owner) ->get(route('support.attachments.show', $attachment)) ->assertOk() ->assertDownload('debug.log'); $this->actingAs($otherUser) ->get(route('support.attachments.show', $attachment)) ->assertForbidden(); $this->actingAs($admin) ->get(route('support.attachments.show', $attachment)) ->assertOk() ->assertDownload('debug.log'); } }