pluck('key')->all())->toEqualCanonicalizing(['client', 'operator', 'admin']); }); test('a users roles are stored in the role_user pivot, not a JSON column', function () { $user = User::query()->create(['name' => 'Multi Role', 'email' => 'multirole@example.com', 'roles' => ['operator', 'admin']]); expect(Schema::hasColumn('users', 'roles'))->toBeFalse() ->and(DB::table('role_user')->where('user_id', $user->id)->count())->toBe(2) ->and($user->fresh()->roles)->toEqualCanonicalizing(['operator', 'admin']) ->and($user->isAdmin())->toBeTrue() ->and($user->isOperator())->toBeTrue() ->and($user->isClient())->toBeFalse(); }); test('admin toggling a users role in the panel updates role_user, not a JSON column', function () { $admin = adminUser(); $target = User::query()->create(['name' => 'Toggle Me', 'email' => 'toggle@example.com', 'roles' => ['client']]); Livewire::actingAs($admin)->test(Panel::class) ->call('toggleUserRole', $target->id, 'operator') ->assertOk(); expect($target->fresh()->roles)->toEqualCanonicalizing(['client', 'operator']); }); test('a users custom field values are stored in user_field_values, not a JSON column', function () { $field = UserField::query()->create(['label' => 'Dział', 'type' => 'text', 'sort_order' => 1]); $user = User::query()->create([ 'name' => 'Field Test', 'email' => 'fieldtest@example.com', 'roles' => ['client'], 'custom_field_values' => [$field->id => 'IT'], ]); expect(Schema::hasColumn('users', 'custom_field_values'))->toBeFalse() ->and(UserFieldValue::query()->where('user_id', $user->id)->where('user_field_id', $field->id)->value('value'))->toBe('IT') ->and($user->fresh()->custom_field_values)->toBe([$field->id => 'IT']); }); test('replacing a users field values drops the ones no longer present, matching the old JSON full-replace semantics', function () { $field = UserField::query()->create(['label' => 'Dział', 'type' => 'text', 'sort_order' => 1]); $user = User::query()->create([ 'name' => 'Field Test 2', 'email' => 'fieldtest2@example.com', 'roles' => ['client'], 'custom_field_values' => [$field->id => 'IT'], ]); $user->update(['custom_field_values' => []]); expect(UserFieldValue::query()->where('user_id', $user->id)->exists())->toBeFalse(); }); test('a client reply links the message to its author and the client role via ticket_message_authors', function () { seedStatusesAndPriorities(); $client = User::query()->create(['name' => 'Klient', 'email' => 'klient-msg@example.com', 'roles' => ['client']]); $ticket = makeTicket(['customer_id' => $client->id, 'email' => $client->email, 'name' => $client->name]); app(TicketService::class)->clientReply($ticket, $client, 'Odpowiedź klienta'); $message = $ticket->messages()->latest('id')->first(); expect(Schema::hasColumn('ticket_messages', 'author_id'))->toBeFalse() ->and(Schema::hasColumn('ticket_messages', 'role'))->toBeFalse() ->and($message->role)->toBe('client') ->and($message->author_id)->toBe($client->id) ->and($message->author->id)->toBe($client->id); }); test('an operator reply and note link to the operator role', function () { seedStatusesAndPriorities(); $operator = operatorUser('op-msg@example.com'); $ticket = makeTicket(); app(TicketService::class)->operatorReply($ticket, $operator, 'Odpowiedź operatora'); app(TicketService::class)->operatorNote($ticket, $operator, 'Notatka wewnętrzna'); $reply = $ticket->messages()->where('internal', false)->latest('id')->first(); $note = $ticket->messages()->where('internal', true)->latest('id')->first(); expect($reply->role)->toBe('operator')->and($reply->author_id)->toBe($operator->id) ->and($note->role)->toBe('operator')->and($note->author_id)->toBe($operator->id); }); test('a system-generated message (merge announcement) has no author and role defaults to "system"', function () { seedStatusesAndPriorities(); $primary = makeTicket(['number' => '2001']); $other = makeTicket(['number' => '2002']); app(TicketService::class)->merge([$primary->id, $other->id]); $announcement = $primary->messages()->where('body', 'like', '%Scalono zgłoszenia%')->firstOrFail(); expect($announcement->role)->toBe('system') ->and($announcement->author_id)->toBeNull() ->and(TicketMessageAuthor::query()->where('ticket_message_id', $announcement->id)->exists())->toBeFalse(); }); test('a client can edit their own message but not an operators message on the same ticket', function () { seedStatusesAndPriorities(); $client = User::query()->create(['name' => 'Owner', 'email' => 'owner@example.com', 'roles' => ['client']]); $operator = operatorUser('op-on-client-ticket@example.com'); $ticket = makeTicket(['customer_id' => $client->id, 'email' => $client->email, 'name' => $client->name]); app(TicketService::class)->clientReply($ticket, $client, 'Treść klienta'); $ownMessage = $ticket->messages()->latest('id')->first(); app(TicketService::class)->operatorReply($ticket, $operator, 'Odpowiedź operatora'); $operatorMessage = $ticket->messages()->latest('id')->first(); Livewire::actingAs($client)->test(ClientTicketShow::class, ['ticket' => $ticket]) ->call('startEdit', $ownMessage->id, 'zmieniona treść') ->assertSet('editingMessageId', $ownMessage->id); // abort_unless() inside an action call doesn't translate into a clean // HTTP response under Livewire's test harness (it disables the app's // exception handling for ->call()), so assert on the instance directly. $component = Livewire::actingAs($client)->test(ClientTicketShow::class, ['ticket' => $ticket])->instance(); expect(fn () => $component->startEdit($operatorMessage->id, 'zmieniona treść')) ->toThrow(Symfony\Component\HttpKernel\Exception\HttpException::class); }); test('an operator can edit an operator-authored message', function () { seedStatusesAndPriorities(); $operator = operatorUser('op-edit@example.com'); $ticket = makeTicket(); app(TicketService::class)->operatorReply($ticket, $operator, 'Treść operatora'); $message = $ticket->messages()->latest('id')->first(); Livewire::actingAs($operator)->test(OperatorTicketShow::class, ['ticket' => $ticket]) ->call('startEditMessage', $message->id, 'zmieniona treść') ->assertSet('editingMessageId', $message->id); });