Files
api/services/nginx/app/tests/Unit/XLVask/XLVaskUsageHallScopeTest.php
T
Jeppe B ab6c3ba5b6 Fix route permission instance calls (#344)
## Root cause

`route_t::hasPermission()` and `requirePermission()` are instance
methods. Route code was invoking them with `self::`; the new XL Vask
hall-scope helper made that call from a genuinely static context,
causing PHP to throw:

`Non-static method routes\\xlvaskUsageLogsRoute::hasPermission() cannot
be called statically`

## Changes

- Invoke route permission methods through `$this` across all 273
executable legacy calls in 45 route classes.
- Make `xlvaskUsageLogsRoute::allowedHallIdsForUser()` an instance
helper and update all 13 callers.
- Preserve the existing all-scope and own-scope hall selection rules.
- Add a token-aware regression test that rejects executable
`self::hasPermission()` and `self::requirePermission()` calls, while
ignoring comments.
- Add focused XL Vask tests for global scanner hall scope and
group-limited own scope.
- Update affected route contract assertions to the instance-call form.

## Verification

- PHP lint: all 53 changed PHP files
- Focused PHPStan: changed XL Vask route and both new regression tests —
clean
- Focused regression slice: 58 passed, 748 assertions
- Full local unit suite: 1,300 passed, 9,442 assertions (1 unrelated
existing warning, 1 environment skip)
- Full local API suite: 285 passed, 11,704 assertions
- Exact-SHA GitHub Tests workflow: all 7 jobs passed (unit, API,
integration, legacy, edge gateway, and supporting checks)
- Independent exact-SHA QA gate: PASS, no findings
- Independent exact-SHA security gate: PASS, no findings
- Independent exact-SHA reviewer gate: PASS, no findings
- Remote comparison: exactly one commit ahead of
`40b104abed7723a7d1b7028190ecda0e7aeef829`; all 53 remote blob hashes
matched the reviewed worktree

## Delivery state

Draft only for human review. No merge or deployment is included. Qodana
is skipped while the PR remains draft and is therefore not represented
as a passed gate.
2026-08-04 16:04:41 +02:00

135 lines
3.7 KiB
PHP

<?php
require_once dirname(__DIR__, 3) . '/routes/xlvaskUsageLogsRoute.php';
class XLVaskUsageHallScopeRouteHarness extends \routes\xlvaskUsageLogsRoute
{
public bool $hasAllPermission = false;
public function __construct()
{
// Avoid request initialization in this focused unit test.
}
public function hasPermission(
string|\classes\permission_node $permission,
?int $customer_number = null
): bool {
$permissionKey = $permission instanceof \classes\permission_node
? (string)$permission->permission
: $permission;
return $this->hasAllPermission && $permissionKey === 'list_xlvask_usage_orders_all';
}
}
class XLVaskUsageHallScopeDbFake
{
public int $queryCount = 0;
/** @param array<int,array{HallId:mixed}> $rows */
public function __construct(private readonly array $rows)
{
}
public function query(string $sql): object
{
$this->queryCount++;
expect($sql)->toContain('SELECT DISTINCT HallId FROM plate_scanners');
return new stdClass();
}
/** @return array<int,array{HallId:mixed}> */
public function fetch_all(object $result): array
{
return $this->rows;
}
}
/**
* @param array<int,mixed> $groupHallIds
*/
function xlvask_usage_hall_scope_user(array $groupHallIds): object
{
return new class($groupHallIds) {
/** @param array<int,mixed> $groupHallIds */
public function __construct(private readonly array $groupHallIds)
{
}
public function getGroup(): object
{
return new class($this->groupHallIds) {
/** @param array<int,mixed> $groupHallIds */
public function __construct(private readonly array $groupHallIds)
{
}
/** @return array<int,mixed> */
public function getDepartmentsScannersHallIds(): array
{
return $this->groupHallIds;
}
};
}
};
}
/** @param array<int,mixed> $groupHallIds */
function resolve_xlvask_usage_hall_scope(
XLVaskUsageHallScopeRouteHarness $route,
XLVaskUsageHallScopeDbFake $dbFake,
array $groupHallIds
): array {
$hadDb = array_key_exists('db', $GLOBALS);
$originalDb = $GLOBALS['db'] ?? null;
$GLOBALS['db'] = $dbFake;
try {
$method = new ReflectionMethod(\routes\xlvaskUsageLogsRoute::class, 'allowedHallIdsForUser');
return $method->invoke($route, xlvask_usage_hall_scope_user($groupHallIds));
} finally {
if ($hadDb) {
$GLOBALS['db'] = $originalDb;
} else {
unset($GLOBALS['db']);
}
}
}
it('uses every normalized scanner hall for users with all-scope permission', function (): void {
$route = new XLVaskUsageHallScopeRouteHarness();
$route->hasAllPermission = true;
$dbFake = new XLVaskUsageHallScopeDbFake([
['HallId' => ' Hall-A '],
['HallId' => ''],
['HallId' => 'Hall-A'],
['HallId' => str_repeat('x', 192)],
['HallId' => 'Hall-B'],
]);
$hallIds = resolve_xlvask_usage_hall_scope($route, $dbFake, ['Own-Hall']);
expect($hallIds)->toBe(['Hall-A', 'Hall-B'])
->and($dbFake->queryCount)->toBe(1);
});
it('keeps own-scope users limited to normalized group halls', function (): void {
$route = new XLVaskUsageHallScopeRouteHarness();
$dbFake = new XLVaskUsageHallScopeDbFake([
['HallId' => 'Global-Hall'],
]);
$hallIds = resolve_xlvask_usage_hall_scope($route, $dbFake, [
' Own-Hall-A ',
'',
'Own-Hall-A',
'Own-Hall-B',
]);
expect($hallIds)->toBe(['Own-Hall-A', 'Own-Hall-B'])
->and($dbFake->queryCount)->toBe(0);
});