Merge master into branch resolving self-serve lane command conflicts
This commit is contained in:
@@ -507,27 +507,21 @@ class moduleSelfServeRoute
|
||||
);
|
||||
break;
|
||||
case selfserve_lane_command::RESERVE:
|
||||
$this->requireSelfServeLaneCommandPermission(
|
||||
$this->requireOperatorLaneCommandPermission(
|
||||
$lane,
|
||||
$customer_number,
|
||||
'modules_selfserve_lane_command_execute_reserve',
|
||||
false
|
||||
'modules_selfserve_lane_command_execute_reserve'
|
||||
);
|
||||
break;
|
||||
case selfserve_lane_command::RELEASE:
|
||||
$this->requireSelfServeLaneCommandPermission(
|
||||
$this->requireOperatorLaneCommandPermission(
|
||||
$lane,
|
||||
$customer_number,
|
||||
'modules_selfserve_lane_command_execute_release',
|
||||
false
|
||||
'modules_selfserve_lane_command_execute_release'
|
||||
);
|
||||
break;
|
||||
case selfserve_lane_command::RESET:
|
||||
$this->requireSelfServeLaneCommandPermission(
|
||||
$this->requireOperatorLaneCommandPermission(
|
||||
$lane,
|
||||
$customer_number,
|
||||
'modules_selfserve_lane_command_execute_reset',
|
||||
false
|
||||
'modules_selfserve_lane_command_execute_reset'
|
||||
);
|
||||
break;
|
||||
case selfserve_lane_command::OPEN_PROPERTY_ACCESS_GATE:
|
||||
@@ -1484,7 +1478,7 @@ class moduleSelfServeRoute
|
||||
return;
|
||||
}
|
||||
|
||||
if ($allow_customer_self_serve) {
|
||||
if ($allow_customer_self_serve && $this->isSelfServeModuleEnabled()) {
|
||||
$customer_allowed = $requires_active_wash
|
||||
? $this->canCustomerUseActiveOperationalSelfServeLane($lane, $customer_number, $allow_department_active_wash)
|
||||
: $this->canCustomerUseSelfServeLane($lane, $customer_number);
|
||||
@@ -1501,6 +1495,18 @@ class moduleSelfServeRoute
|
||||
);
|
||||
}
|
||||
|
||||
private function requireOperatorLaneCommandPermission(selfserve_lane $lane, string $command_permission): void
|
||||
{
|
||||
if (empty($lane->department_lane) || empty($lane->department_lane->department)) {
|
||||
global $response;
|
||||
$response->error('Lane department not found', 404);
|
||||
}
|
||||
|
||||
self::requireDepartmentAccess((string)$lane->department_lane->department->value());
|
||||
self::requirePermission('modules_selfserve_lane_command_execute');
|
||||
self::requirePermission($command_permission);
|
||||
}
|
||||
|
||||
private function requirePropertyGateCommandPermission(string $permission, selfserve_lane $lane, int $customer_number): void
|
||||
{
|
||||
$elevated_permissions = [
|
||||
@@ -1518,6 +1524,15 @@ class moduleSelfServeRoute
|
||||
$this->emitForbidden([...$elevated_permissions, self::CUSTOMER_SELFSERVE_PERMISSION]);
|
||||
}
|
||||
|
||||
protected function isSelfServeModuleEnabled(): bool
|
||||
{
|
||||
try {
|
||||
return (bool)(new selfserve())->config->enabled->getVariableValue();
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function canCustomerUseSelfServeLane(selfserve_lane $lane, int $customer_number): bool
|
||||
{
|
||||
return $customer_number > 0
|
||||
|
||||
@@ -17,9 +17,12 @@ class SelfserveCustomerLaneAccessRouteHarness extends moduleSelfServeRoute
|
||||
'list_own_department_selfserve_vehicle_conditions' => true,
|
||||
];
|
||||
public bool $laneEnabled = true;
|
||||
public bool $moduleEnabled = true;
|
||||
public bool $activeWashResult = false;
|
||||
/** @var array<int,array{department_id:int,customer_number:int}> */
|
||||
public array $activeWashChecks = [];
|
||||
/** @var array<int,string> */
|
||||
public array $forbiddenPermissions = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -42,11 +45,33 @@ class SelfserveCustomerLaneAccessRouteHarness extends moduleSelfServeRoute
|
||||
return $this->canCustomerUseActiveSelfServeLane($lane, $customer_number);
|
||||
}
|
||||
|
||||
protected function isSelfServeModuleEnabled(): bool
|
||||
{
|
||||
return $this->moduleEnabled;
|
||||
}
|
||||
|
||||
protected function isLaneSelfServeOperationallyEnabled(selfserve_lane $lane): bool
|
||||
{
|
||||
return $this->laneEnabled;
|
||||
}
|
||||
|
||||
protected function emitForbidden(array $permissions): void
|
||||
{
|
||||
$this->forbiddenPermissions = array_values($permissions);
|
||||
throw new RuntimeException('forbidden');
|
||||
}
|
||||
|
||||
public function requireCommandPermissionForTest(
|
||||
selfserve_lane $lane,
|
||||
int $customer_number,
|
||||
string $command_permission,
|
||||
bool $allow_customer_self_serve,
|
||||
bool $requires_active_wash = false
|
||||
): void {
|
||||
$method = new ReflectionMethod(moduleSelfServeRoute::class, 'requireSelfServeLaneCommandPermission');
|
||||
$method->invoke($this, $lane, $customer_number, $command_permission, $allow_customer_self_serve, $requires_active_wash);
|
||||
}
|
||||
|
||||
protected function customerHasActiveSelfServeWashInDepartment(int $department_id, int $customer_number): bool
|
||||
{
|
||||
$this->activeWashChecks[] = [
|
||||
@@ -157,3 +182,62 @@ it('blocks active wash operations for other customers', function (): void {
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it('denies customer self-serve command fallback when the self-serve module is disabled', function (): void {
|
||||
$route = new SelfserveCustomerLaneAccessRouteHarness();
|
||||
$route->moduleEnabled = false;
|
||||
$lane = new SelfserveCustomerLaneAccessLaneFake(4, 12345679);
|
||||
|
||||
expect(fn() => $route->requireCommandPermissionForTest(
|
||||
$lane,
|
||||
12345679,
|
||||
'modules_selfserve_lane_command_execute_stop',
|
||||
true,
|
||||
true
|
||||
))->toThrow(RuntimeException::class, 'forbidden');
|
||||
});
|
||||
|
||||
it('returns deterministic forbidden permissions when disabled module blocks customer self-serve command fallback', function (): void {
|
||||
$route = new SelfserveCustomerLaneAccessRouteHarness();
|
||||
$route->moduleEnabled = false;
|
||||
$lane = new SelfserveCustomerLaneAccessLaneFake(4, 12345679);
|
||||
|
||||
try {
|
||||
$route->requireCommandPermissionForTest(
|
||||
$lane,
|
||||
12345679,
|
||||
'modules_selfserve_lane_command_execute_stop',
|
||||
true,
|
||||
true
|
||||
);
|
||||
} catch (RuntimeException $exception) {
|
||||
expect($exception->getMessage())->toBe('forbidden');
|
||||
}
|
||||
|
||||
expect($route->forbiddenPermissions)->toBe([
|
||||
'modules_selfserve_lane_command_execute',
|
||||
'modules_selfserve_lane_command_execute_stop',
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
});
|
||||
|
||||
it('continues allowing operator self-serve commands when the self-serve module is disabled', function (): void {
|
||||
$route = new SelfserveCustomerLaneAccessRouteHarness();
|
||||
$route->moduleEnabled = false;
|
||||
$route->permissions = [
|
||||
'modules_selfserve_lane_command_execute' => true,
|
||||
'modules_selfserve_lane_command_execute_stop' => true,
|
||||
];
|
||||
$lane = new SelfserveCustomerLaneAccessLaneFake(4, 12345679);
|
||||
|
||||
$route->requireCommandPermissionForTest(
|
||||
$lane,
|
||||
12345679,
|
||||
'modules_selfserve_lane_command_execute_stop',
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
expect($route->forbiddenPermissions)->toBe([]);
|
||||
});
|
||||
|
||||
@@ -465,3 +465,76 @@ it('keeps read-only self-serve preview and summary refreshes from touching relay
|
||||
expect($vehicleConditionsRoute)->toContain('$this->getWashFlow()->synchronizeSession($lane, $reg, $customer_id, $activate_machine, $vehicle_type_id, $sync_relay_state);');
|
||||
expect($vehicleConditionsRoute)->toContain('$this->getWashFlow()->synchronizeSession($lane_id, $reg, $customer_id, $activate_machine, $vehicle_type_id, $sync_relay_state);');
|
||||
});
|
||||
|
||||
it('classifies self-serve lane command route authorization by customer product behavior', function (): void {
|
||||
$moduleSelfServeRoute = file_get_contents(app_path('routes/moduleSelfServeRoute.php'));
|
||||
$productDocs = file_get_contents(app_path('modules/selfserve/selfserve.md'));
|
||||
$washFlow = file_get_contents(app_path('modules/selfserve/classes/selfserve_wash_flow.php'));
|
||||
|
||||
expect($moduleSelfServeRoute)->not->toBeFalse();
|
||||
expect($productDocs)->not->toBeFalse();
|
||||
expect($washFlow)->not->toBeFalse();
|
||||
|
||||
// Product flow confirms the customer UI sends STOP, while route docs expose START and property gates
|
||||
// as customer-scoped lane commands. RESERVE, RELEASE, and RESET remain absent from customer flow/docs.
|
||||
expect($productDocs)->toContain('Customer->>API: POST /modules/self-serve/lane/command (STOP)');
|
||||
expect($productDocs)->toContain('customer `list_own_department_selfserve_vehicle_conditions` for scoped `START`, scoped `STOP`, and property gate commands');
|
||||
expect($productDocs)->toContain('Customer `START` requires an enabled self-serve lane. Customer `STOP` and property gate commands require the customer\'s active wash in the lane department.');
|
||||
expect($washFlow)->toContain('$payload[\'command\'] = $relayRole === \'PROPERTY_ENTRANCE\' ? \'OPEN_PROPERTY_ACCESS_GATE\' : \'OPEN_PROPERTY_EXIT_GATE\'');
|
||||
expect($washFlow)->toContain('$signalType = \'studio_action_gate_open\'');
|
||||
|
||||
$commandCases = selfserve_lane_command_route_cases($moduleSelfServeRoute);
|
||||
|
||||
expect($commandCases['START'])
|
||||
->toContain('requireSelfServeLaneCommandPermission')
|
||||
->toContain("'modules_selfserve_lane_command_execute_start'")
|
||||
->toContain("true\n );")
|
||||
->not->toContain('requireDepartmentAccess')
|
||||
->not->toContain('requireOperatorLaneCommandPermission');
|
||||
|
||||
expect($commandCases['STOP'])
|
||||
->toContain('requireSelfServeLaneCommandPermission')
|
||||
->toContain("'modules_selfserve_lane_command_execute_stop'")
|
||||
->toContain("true,\n true")
|
||||
->not->toContain('requireDepartmentAccess')
|
||||
->not->toContain('requireOperatorLaneCommandPermission');
|
||||
|
||||
expect($commandCases['OPEN_PROPERTY_ACCESS_GATE'])
|
||||
->toContain('requirePropertyGateCommandPermission')
|
||||
->toContain("'modules_selfserve_lane_command_execute_open_property_access_gate'")
|
||||
->not->toContain('self::requirePermission')
|
||||
->not->toContain('requireDepartmentAccess');
|
||||
|
||||
expect($commandCases['OPEN_PROPERTY_EXIT_GATE'])
|
||||
->toContain('requirePropertyGateCommandPermission')
|
||||
->toContain("'modules_selfserve_lane_command_execute_open_property_exit_gate'")
|
||||
->not->toContain('self::requirePermission')
|
||||
->not->toContain('requireDepartmentAccess');
|
||||
|
||||
foreach (['RESERVE', 'RELEASE', 'RESET'] as $operatorOnlyCommand) {
|
||||
expect($commandCases[$operatorOnlyCommand])
|
||||
->toContain('requireOperatorLaneCommandPermission')
|
||||
->not->toContain('requireSelfServeLaneCommandPermission')
|
||||
->not->toContain('requirePropertyGateCommandPermission');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @return array<string,string>
|
||||
*/
|
||||
function selfserve_lane_command_route_cases(string $moduleSelfServeRoute): array
|
||||
{
|
||||
$cases = [];
|
||||
preg_match_all(
|
||||
'/case selfserve_lane_command::([A-Z_]+):(.*?)(?=\n\s*case selfserve_lane_command::|\n\s*}\n\s*\/\/ Execute the command)/s',
|
||||
$moduleSelfServeRoute,
|
||||
$matches,
|
||||
PREG_SET_ORDER
|
||||
);
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$cases[$match[1]] = $match[2];
|
||||
}
|
||||
|
||||
return $cases;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user