Guard chauffeur vehicles on legacy schemas

This commit is contained in:
Jeppe Bundgaard
2026-07-13 20:03:50 +02:00
parent 026492c3bd
commit 233133365d
2 changed files with 106 additions and 3 deletions
+11 -3
View File
@@ -249,7 +249,7 @@ class subusersRoute
}
$vehicle->getObjectProperties();
if ($vehicle->deleted_at->value() !== null) {
if ($this->customerVehiclesHaveDeletedAtColumn() && $vehicle->deleted_at->value() !== null) {
if ($strict) {
$response->error('Assigned vehicle not found', 404);
}
@@ -271,6 +271,11 @@ class subusersRoute
];
}
private function customerVehiclesHaveDeletedAtColumn(): bool
{
return (new customer_vehicles_o())->columnsExist(['deleted_at']);
}
private function validateAssignedVehicleIdForCustomer(mixed $value, int $customerNumber): ?int
{
$vehicleId = $this->normalizeAssignedVehicleId($value);
@@ -1267,10 +1272,13 @@ class subusersRoute
}
$whereSql = 'WHERE ' . implode(' AND ', $where);
$vehicleDeletedAtJoinSql = $this->customerVehiclesHaveDeletedAtColumn()
? ' AND cv.`deleted_at` IS NULL'
: '';
$fromSql = "
FROM `subuser_grants` g
INNER JOIN `subusers` s ON s.`id` = g.`subuser`
LEFT JOIN `customer_vehicles` cv ON cv.`id` = g.`assigned_vehicle_id` AND cv.`deleted_at` IS NULL
LEFT JOIN `customer_vehicles` cv ON cv.`id` = g.`assigned_vehicle_id`{$vehicleDeletedAtJoinSql}
";
$countSql = "SELECT COUNT(DISTINCT s.`id`) AS `count` $fromSql $whereSql";
@@ -1374,7 +1382,7 @@ class subusersRoute
g.`created_at` AS `grant_created_at`,
g.`updated_at` AS `grant_updated_at`
FROM `subuser_grants` g
LEFT JOIN `customer_vehicles` cv ON cv.`id` = g.`assigned_vehicle_id` AND cv.`deleted_at` IS NULL
LEFT JOIN `customer_vehicles` cv ON cv.`id` = g.`assigned_vehicle_id`{$vehicleDeletedAtJoinSql}
WHERE " . implode(' AND ', $grantWhere) . "
ORDER BY g.`enabled` DESC, g.`billing_customer_number` ASC, g.`id` DESC
";
@@ -4,6 +4,28 @@ declare(strict_types=1);
usesApiSuite();
function subusers_without_customer_vehicles_deleted_at(callable $callback): void
{
$db = api_test_runtime()->db();
$column = $db->query("SHOW COLUMNS FROM `customer_vehicles` LIKE 'deleted_at'");
if ($column === false) {
throw new RuntimeException('Unable to inspect customer_vehicles.deleted_at test column.');
}
$hadColumn = (int)$column->num_rows > 0;
if ($hadColumn) {
$db->query('ALTER TABLE `customer_vehicles` DROP COLUMN `deleted_at`');
}
try {
$callback();
} finally {
if ($hadColumn) {
$db->query('ALTER TABLE `customer_vehicles` ADD COLUMN `deleted_at` DATETIME NULL');
}
}
}
const SUBUSER_PASSWORD_POLICY_MESSAGE = 'Password must contain at least one uppercase letter, one lowercase letter, and one number';
it('lists subusers when an existing grant has legacy zero permissions', function (): void {
@@ -411,6 +433,79 @@ it('lists chauffeurs once with grouped grants across customers for superusers',
expect($secondRow['grants'][0]['customer_name'] ?? null)->toBe('Fleet Customer Beta');
});
it('lists superuser chauffeurs with assigned vehicles when customer vehicles has no deleted at column', function (): void {
subusers_without_customer_vehicles_deleted_at(function (): void {
api_test_covers('GET /superuser/subusers', 'happy');
api_test_covers('GET /superuser/users/{user_id}/subusers', 'happy');
$customerVehiclesDeletedAtColumn = api_test_runtime()->db()->query(
"SHOW COLUMNS FROM `customer_vehicles` LIKE 'deleted_at'"
);
expect($customerVehiclesDeletedAtColumn)->not->toBeFalse();
expect((int)$customerVehiclesDeletedAtColumn->num_rows)->toBe(0);
$session = api_fixtures()->createUserSession(['list_subusers']);
$suffix = (string)random_int(100000, 999999);
$customer = api_fixtures()->createUser([
'display_name' => 'Legacy Subuser Vehicle Customer ' . $suffix,
'economic_customer_name' => 'Legacy Subuser Vehicle Customer ' . $suffix,
]);
$subuser = api_fixtures()->createSubuser([
'name' => 'Legacy Vehicle Driver ' . $suffix,
]);
$vehicle = api_fixtures()->createVehicle([
'customer_id' => (int)$customer['customer_number'],
'type' => 1,
'reg' => 'legacy' . $suffix,
]);
$grantId = api_fixtures()->grantSubuser(
(int)$subuser['id'],
(int)$customer['customer_number'],
['VEHICLES_LIST'],
['assigned_vehicle_id' => (int)$vehicle['id']]
);
$list = api_client()->get('/superuser/subusers?page=1&limit=20', $session['headers']);
$search = api_client()->get(
'/superuser/subusers?page=1&limit=20&search=' . rawurlencode((string)$vehicle['reg']),
$session['headers']
);
$scoped = api_client()->get(
'/superuser/users/' . $customer['id'] . '/subusers?page=1&limit=20',
$session['headers']
);
foreach ([$list, $search, $scoped] as $response) {
$response
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
}
$matchingSearchRows = array_values(array_filter(
is_array($search->data()) ? $search->data() : [],
static fn (mixed $item): bool => is_array($item) && (int)($item['id'] ?? 0) === (int)$subuser['id']
));
expect($matchingSearchRows)->toHaveCount(1);
$matchingScopedRows = array_values(array_filter(
is_array($scoped->data()) ? $scoped->data() : [],
static fn (mixed $item): bool => is_array($item) && (int)($item['id'] ?? 0) === (int)$subuser['id']
));
expect($matchingScopedRows)->toHaveCount(1);
$grantsById = [];
foreach ($matchingScopedRows[0]['grants'] ?? [] as $grant) {
$grantsById[(int)$grant['grant_id']] = $grant;
}
expect($grantsById)->toHaveKey($grantId);
expect($grantsById[$grantId]['assigned_vehicle_id'])->toBe((int)$vehicle['id']);
expect($grantsById[$grantId]['assigned_vehicle_reg'])->toBe(strtoupper((string)$vehicle['reg']));
expect($scoped->meta()['pagination']['total'] ?? null)->toBeGreaterThanOrEqual(1);
});
});
it('paginates superuser chauffeur lists by unique chauffeur instead of grant count', function (): void {
$session = api_fixtures()->createUserSession(['list_subusers']);
$suffix = (string)random_int(100000, 999999);