mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-03 20:02:29 +02:00
Imported from Rosmi720/XC_VM@260ca7ab, with the scoping and permission gaps closed before merge. Original work: - Keep tickets listed through a LEFT JOIN so one does not vanish when its author's user row is edited or deleted; username falls back to 'Unknown'. - Stop double-escaping message bodies: the repository returned htmlspecialchars() output that both ticket_view templates escape again, so markup showed up as entities. The repository now returns raw text and the views keep their nl2br(htmlspecialchars()). - Drop the str_repeat(' ', ...) padding hack that rendered literal ' ' inside short replies. - Resolve last_reply and the derived status flags without undefined-key warnings on tickets that have no replies yet. - Add Close / Re-Open to the admin ticket view with a confirmation dialog, a quick reply form under the thread, and a Reply entry in the tickets table. - Add English and Arabic strings for the confirmation dialogs. Changed on import: - Tenant isolation: the original widened the admin branch to every ticket on the server whenever $rAdmin was set, and admin/tickets.php always sets it. Since users_groups.is_admin is a flag several groups can carry, that exposed every tenant's tickets to any admin-panel user. Only the super-admin group (member_group_id = 1) now gets the unscoped list; any other admin stays scoped to the users it owns. TicketVisibilityTest covers all three scopes. - Gate Close / Re-Open, the quick reply form and the Reply links behind the same Authorization::check('adv', 'ticket') the tickets table already used. - Quote 'Unknown' as a string literal, not "Unknown", which ANSI_QUOTES would read as an identifier. - Escape the textarea placeholder. - Drop the admin/functions.php hunk: that file no longer exists after the PSR-4 move and AdminApiStage already populates $GLOBALS['rAdminUserInfo']. The post.php fallback is kept, since post.php can be served standalone. Verified: 857 tests, make gates, CRAP gate.
123 lines
3.9 KiB
PHP
123 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace XcVm\Tests\Unit;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use TestDb;
|
|
use XcVm\Domain\User\TicketRepository;
|
|
use XcVm\Infrastructure\Database\DatabaseFactory;
|
|
|
|
/**
|
|
* Tenant isolation for the ticket list.
|
|
*
|
|
* `users_groups`.`is_admin` is a group flag that more than one group can carry,
|
|
* so reaching the admin panel must not mean seeing every tenant's tickets: only
|
|
* the super-admin group (member_group_id = 1) gets the whole server, while any
|
|
* other admin stays scoped to the users it owns.
|
|
*
|
|
* Hierarchy seeded: super-admin(1); sub-admin(2) owns reseller(3) who owns
|
|
* sub-reseller(4); stranger-admin(90) owns stranger-reseller(91), an unrelated
|
|
* branch that must stay invisible to the sub-admin.
|
|
*/
|
|
final class TicketVisibilityTest extends TestCase {
|
|
|
|
private TestDb $db;
|
|
|
|
protected function setUp(): void {
|
|
$this->db = new TestDb();
|
|
$this->db->exec(
|
|
'CREATE TABLE `users` (
|
|
`id` INTEGER PRIMARY KEY,
|
|
`username` TEXT,
|
|
`owner_id` INTEGER,
|
|
`member_group_id` INTEGER
|
|
);
|
|
INSERT INTO `users` (`id`,`username`,`owner_id`,`member_group_id`) VALUES
|
|
(1,"superadmin",0,1),
|
|
(2,"subadmin",1,3),
|
|
(3,"reseller",2,4),
|
|
(4,"subreseller",3,4),
|
|
(90,"strangeradmin",1,3),
|
|
(91,"strangerreseller",90,4);
|
|
CREATE TABLE `tickets` (
|
|
`id` INTEGER PRIMARY KEY,
|
|
`member_id` INTEGER,
|
|
`title` TEXT,
|
|
`status` INTEGER DEFAULT 1,
|
|
`admin_read` INTEGER DEFAULT 0,
|
|
`user_read` INTEGER DEFAULT 0
|
|
);
|
|
INSERT INTO `tickets` (`id`,`member_id`,`title`) VALUES
|
|
(10,2,"from subadmin"),
|
|
(11,3,"from reseller"),
|
|
(12,4,"from subreseller"),
|
|
(13,91,"from stranger branch");
|
|
CREATE TABLE `tickets_replies` (
|
|
`id` INTEGER PRIMARY KEY,
|
|
`ticket_id` INTEGER,
|
|
`date` INTEGER,
|
|
`admin_reply` INTEGER DEFAULT 0,
|
|
`message` TEXT
|
|
);'
|
|
);
|
|
DatabaseFactory::set($this->db);
|
|
$GLOBALS['rPermissions'] = ['all_reports' => []];
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
DatabaseFactory::reset();
|
|
unset($GLOBALS['rUserInfo'], $GLOBALS['rPermissions']);
|
|
}
|
|
|
|
public function testSuperAdminSeesEveryTicketOnTheServer(): void {
|
|
$GLOBALS['rUserInfo'] = ['id' => 1, 'member_group_id' => 1];
|
|
|
|
$this->assertSame([10, 11, 12, 13], $this->ticketIds(TicketRepository::getAll(1, true)));
|
|
}
|
|
|
|
public function testNonSuperAdminDoesNotSeeAnotherAdminsBranch(): void {
|
|
// Regression guard: an `$rAdmin` call used to widen to every ticket on the
|
|
// server, exposing other admins' tenants to any admin-panel user.
|
|
$GLOBALS['rUserInfo'] = ['id' => 2, 'member_group_id' => 3];
|
|
|
|
$rIds = $this->ticketIds(TicketRepository::getAll(2, true));
|
|
|
|
$this->assertSame([10, 11], $rIds, 'own ticket plus the users it owns');
|
|
$this->assertNotContains(13, $rIds, 'stranger branch must stay hidden');
|
|
}
|
|
|
|
public function testResellerSeesOwnTicketsPlusReports(): void {
|
|
$GLOBALS['rUserInfo'] = ['id' => 3, 'member_group_id' => 4];
|
|
$GLOBALS['rPermissions'] = ['all_reports' => [4]];
|
|
|
|
$this->assertSame([11, 12], $this->ticketIds(TicketRepository::getAll(3)));
|
|
}
|
|
|
|
public function testMissingIdStillListsEverything(): void {
|
|
$GLOBALS['rUserInfo'] = ['id' => 1, 'member_group_id' => 1];
|
|
|
|
$this->assertSame([10, 11, 12, 13], $this->ticketIds(TicketRepository::getAll(null, true)));
|
|
}
|
|
|
|
public function testTicketSurvivesItsAuthorBeingDeleted(): void {
|
|
$this->db->exec('DELETE FROM `users` WHERE `id` = 91;');
|
|
$GLOBALS['rUserInfo'] = ['id' => 1, 'member_group_id' => 1];
|
|
|
|
$rRows = TicketRepository::getAll(1, true);
|
|
$rOrphan = array_values(array_filter($rRows, static fn(array $r): bool => (int) $r['id'] === 13));
|
|
|
|
$this->assertCount(1, $rOrphan, 'the ticket must not vanish with its author');
|
|
$this->assertSame('Unknown', $rOrphan[0]['username']);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $rRows
|
|
* @return int[] Ticket ids, ascending.
|
|
*/
|
|
private function ticketIds(array $rRows): array {
|
|
$rIds = array_map(static fn(array $r): int => (int) $r['id'], $rRows);
|
|
sort($rIds);
|
|
return $rIds;
|
|
}
|
|
}
|