Reactivate existing relay binding rows when a gateway/relay pair is re-added after soft deletion, avoiding duplicate uniq_edge_gateway_binding inserts. Add regression coverage for the reactivation path.
162 lines
11 KiB
PHP
162 lines
11 KiB
PHP
<?php
|
|
|
|
app_require('classes/edge_gateway_manager.php');
|
|
|
|
use classes\edge_gateway_manager;
|
|
|
|
it('keeps relay dispatch and discovery queueing on the edge gateway manager', function (): void {
|
|
$managerSource = file_get_contents(app_path('classes/edge_gateway_manager.php'));
|
|
$operationServiceSource = file_get_contents(app_path('classes/edge_gateway_operation_service.php'));
|
|
|
|
expect($managerSource)->not->toBeFalse();
|
|
expect($managerSource)->toContain('public function queueDiscovery');
|
|
expect($managerSource)->toContain('public function dispatchRelayStatus');
|
|
expect($managerSource)->toContain('public function dispatchRelayStatusLocalOnly');
|
|
expect($managerSource)->toContain('public function dispatchRelaySwitch');
|
|
expect($managerSource)->toContain('public function dispatchRelaySwitchLocalOnly');
|
|
expect($managerSource)->toContain('public function dispatchRelaySwitchWithTimer');
|
|
expect($managerSource)->toContain('public function dispatchRelaySwitchLocalOnlyWithTimer');
|
|
expect($managerSource)->toContain('public function queueRelayStatusBatch');
|
|
expect($managerSource)->toContain('public function queueRelaySwitchBatch');
|
|
expect($managerSource)->toContain('public function relayBatchStatus');
|
|
expect($managerSource)->toContain('private function createCommandJob');
|
|
expect($managerSource)->toContain('private function queueRelayBatch');
|
|
expect($managerSource)->toContain("'GET_RELAY_STATUS'");
|
|
expect($managerSource)->toContain("'SET_RELAY_STATE'");
|
|
expect($managerSource)->toContain("'relayId' => \$logicalRelayId");
|
|
expect($managerSource)->toContain("'deviceId' => \$binding['device_id']");
|
|
expect($managerSource)->toContain("'localIp' => \$binding['local_ip']");
|
|
expect($managerSource)->toContain("'channel' => (int)\$binding['channel']");
|
|
expect($managerSource)->toContain("'on' => \$on");
|
|
expect($managerSource)->toContain("'require_fast_path' => \$requireFastLocalPath");
|
|
expect($managerSource)->toContain('private function resolveRelayBindingDeviceGeneration');
|
|
expect($managerSource)->toContain('private function normalizeDeviceCapabilities');
|
|
expect($managerSource)->toContain("\$request['device_generation'] = \$deviceGeneration;");
|
|
expect($managerSource)->toContain("\$request['toggle_after'] = \$toggleAfterSeconds;");
|
|
expect($managerSource)->toContain('private function expireTimedOutRelayStatusCommandJobs');
|
|
expect($managerSource)->toContain("AND command_type = 'GET_RELAY_STATUS'");
|
|
expect($managerSource)->toContain("'Edge gateway command timed out', null, 'TIMED_OUT'");
|
|
expect($managerSource)->toContain('private function normalizeCommandFailureStatus');
|
|
expect($managerSource)->toContain('private function isCommandTimeoutError');
|
|
expect($managerSource)->toContain("'batch_id' => \$batchId");
|
|
expect($managerSource)->toContain("'batch_dedupe_key' => \$batchDedupeKey");
|
|
expect($managerSource)->toContain("'idempotency_key' => \$batchId . ':' . (string)\$command['target']");
|
|
expect($managerSource)->toContain("'no_auto_retry' => \$isGatePulse");
|
|
expect($managerSource)->toContain('private function commandDisallowsAutomaticRetry');
|
|
expect($managerSource)->toContain('private function requeueCommandForApiPolling');
|
|
expect($managerSource)->toContain("'preferred_channel' => self::DELIVERY_CHANNEL_API");
|
|
expect($managerSource)->toContain("'fallback_reason' => 'broker_dispatch_failed'");
|
|
expect($managerSource)->toContain("\$this->requeueCommandForApiPolling(\$job, \$exception->getMessage());");
|
|
expect($managerSource)->toContain('self::COMMAND_POLL_TIMEOUT_SECONDS + 5');
|
|
expect($managerSource)->toContain('return $this->waitForCommandResult((int)$job->id, $waitTimeoutSeconds);');
|
|
expect($managerSource)->toContain('private function buildRelayBatchDedupeKey');
|
|
expect($managerSource)->toContain('private function findRecentRelayBatchByDedupeKey');
|
|
expect($managerSource)->toContain('private function enforceRelayCommandRateLimit');
|
|
expect($managerSource)->toContain('RELAY_GATE_RATE_LIMIT_PER_MINUTE');
|
|
expect($managerSource)->toContain("JSON_EXTRACT(delivery_json, \\'$.no_auto_retry\\')");
|
|
expect($managerSource)->toContain('not retrying non-idempotent gate pulse');
|
|
expect($managerSource)->toContain('UNKNOWN_OUTCOME');
|
|
expect($managerSource)->toContain('recent_unknown_gate_outcomes');
|
|
expect($managerSource)->toContain('recent_relay_failure_rate');
|
|
expect($managerSource)->toContain('local_transport_override');
|
|
expect($managerSource)->toContain('private function resolveRelayBindingLocalIp');
|
|
expect($managerSource)->toContain('private function findShellyCloudRelayLocalIp');
|
|
expect($managerSource)->toContain('private function backfillRelayBindingLocalIpFromInventory');
|
|
expect($managerSource)->toContain('$bindingObject->local_ip->set($localIp);');
|
|
expect($managerSource)->toContain("'local_ip' => \$localIp");
|
|
expect($managerSource)->toContain('(new shelly_relay_inventory())->listRelayOptions(true)');
|
|
expect($managerSource)->toContain('public function buildUpdateOperationRequest');
|
|
expect($managerSource)->toContain('public function rotateGatewayCredentials');
|
|
expect($operationServiceSource)->toContain('public function queueOperation');
|
|
expect($operationServiceSource)->toContain('public function cancelOperation');
|
|
expect($operationServiceSource)->toContain('public function claimNextOperation');
|
|
expect($operationServiceSource)->toContain('public function completeAgentOperation');
|
|
expect($operationServiceSource)->toContain("public const STATUS_CANCEL_REQUESTED = 'CANCEL_REQUESTED';");
|
|
expect($operationServiceSource)->toContain("public const STATUS_CANCELLED = 'CANCELLED';");
|
|
expect($operationServiceSource)->toContain("public const ERROR_CANCELLED = 'EDGE_GATEWAY_CANCELLED';");
|
|
expect($operationServiceSource)->toContain('public const OPERATION_LEASE_SECONDS = 45;');
|
|
expect($operationServiceSource)->toContain('private function refreshOperationLease');
|
|
expect($operationServiceSource)->toContain('agent_instance_id');
|
|
expect($operationServiceSource)->toContain('lease_expires_at');
|
|
expect($operationServiceSource)->toContain("'operation_type' => \$type");
|
|
expect($managerSource)->toContain("command_type");
|
|
});
|
|
|
|
it('resolves relay bindings without requiring a primary department gateway first', function (): void {
|
|
$managerSource = file_get_contents(app_path('classes/edge_gateway_manager.php'));
|
|
|
|
expect($managerSource)->not->toBeFalse();
|
|
preg_match(
|
|
'/public function resolveRelayBinding\(int \$departmentId, string \$logicalRelayId\): array\s*\{(?P<body>.*?)\n \}\n\n \/\*\*/s',
|
|
(string)$managerSource,
|
|
$matches
|
|
);
|
|
|
|
expect($matches)->toHaveKey('body');
|
|
$body = (string)$matches['body'];
|
|
|
|
expect($body)->toContain("'department_id' => \$departmentId")
|
|
->and($body)->toContain("'relay_id' => \$logicalRelayId")
|
|
->and($body)->not->toContain('getPrimaryGatewayForDepartment')
|
|
->and($body)->toContain('(new edge_gateways_o())->select((int)$row[\'gateway_id\'])')
|
|
->and($body)->toContain('No active edge gateway found for relay')
|
|
->and($body)->toContain('statusPriority')
|
|
->and($body)->toContain('heartbeatTimestamp');
|
|
});
|
|
|
|
it('reactivates soft-deleted relay bindings before inserting replacements', function (): void {
|
|
$managerSource = file_get_contents(app_path('classes/edge_gateway_manager.php'));
|
|
|
|
expect($managerSource)->not->toBeFalse();
|
|
preg_match(
|
|
'/public function setRelayBindings\(int \$gatewayId, array \$bindings, \?int \$userId = null\): array\s*\{(?P<body>.*?)\n \}\n\n \/\*\*/s',
|
|
(string)$managerSource,
|
|
$matches
|
|
);
|
|
|
|
expect($matches)->toHaveKey('body');
|
|
$body = (string)$matches['body'];
|
|
|
|
expect($body)->toContain("'gateway_id' => \$gatewayId")
|
|
->and($body)->toContain("'relay_id' => \$relayId")
|
|
->and($body)->toContain('$bindingObject->deleted_at->set(null);')
|
|
->and($body)->not->toContain("'deleted_at' => null,\n ], ['id']);");
|
|
});
|
|
|
|
it('loads relay command helpers on the manager and gateway operations on the dedicated service', function (): void {
|
|
$reflection = new ReflectionClass(edge_gateway_manager::class);
|
|
$operationServiceReflection = new ReflectionClass(\classes\edge_gateway_operation_service::class);
|
|
|
|
expect($reflection->hasMethod('queueDiscovery'))->toBeTrue();
|
|
expect($reflection->hasMethod('pollCommand'))->toBeTrue();
|
|
expect($reflection->hasMethod('submitCommandResult'))->toBeTrue();
|
|
expect($reflection->hasMethod('dispatchRelayStatus'))->toBeTrue();
|
|
expect($reflection->hasMethod('dispatchRelayStatusLocalOnly'))->toBeTrue();
|
|
expect($reflection->hasMethod('dispatchRelaySwitch'))->toBeTrue();
|
|
expect($reflection->hasMethod('dispatchRelaySwitchLocalOnly'))->toBeTrue();
|
|
expect($reflection->hasMethod('dispatchRelaySwitchWithTimer'))->toBeTrue();
|
|
expect($reflection->hasMethod('dispatchRelaySwitchLocalOnlyWithTimer'))->toBeTrue();
|
|
expect($reflection->hasMethod('queueRelayStatusBatch'))->toBeTrue();
|
|
expect($reflection->hasMethod('queueRelaySwitchBatch'))->toBeTrue();
|
|
expect($reflection->hasMethod('relayBatchStatus'))->toBeTrue();
|
|
expect($reflection->hasMethod('claimNextCommandJob'))->toBeTrue();
|
|
expect($reflection->getMethod('claimNextCommandJob')->isPrivate())->toBeTrue();
|
|
expect($reflection->hasMethod('syncDeviceInventory'))->toBeTrue();
|
|
expect($reflection->getMethod('syncDeviceInventory')->isPrivate())->toBeTrue();
|
|
expect($reflection->hasMethod('resolveRelayBindingDeviceGeneration'))->toBeTrue();
|
|
expect($reflection->getMethod('resolveRelayBindingDeviceGeneration')->isPrivate())->toBeTrue();
|
|
expect($reflection->hasMethod('normalizeDeviceCapabilities'))->toBeTrue();
|
|
expect($reflection->getMethod('normalizeDeviceCapabilities')->isPrivate())->toBeTrue();
|
|
expect($reflection->hasMethod('resolveRelayBindingLocalIp'))->toBeTrue();
|
|
expect($reflection->getMethod('resolveRelayBindingLocalIp')->isPrivate())->toBeTrue();
|
|
expect($reflection->hasMethod('backfillRelayBindingLocalIpFromInventory'))->toBeTrue();
|
|
expect($reflection->getMethod('backfillRelayBindingLocalIpFromInventory')->isPrivate())->toBeTrue();
|
|
expect($reflection->hasMethod('syncGatewayInventory'))->toBeTrue();
|
|
expect($operationServiceReflection->hasMethod('queueOperation'))->toBeTrue();
|
|
expect($operationServiceReflection->hasMethod('cancelOperation'))->toBeTrue();
|
|
expect($operationServiceReflection->hasMethod('listOperations'))->toBeTrue();
|
|
expect($operationServiceReflection->hasMethod('claimNextOperation'))->toBeTrue();
|
|
expect($operationServiceReflection->hasMethod('appendAgentOperationEvent'))->toBeTrue();
|
|
expect($operationServiceReflection->hasMethod('completeAgentOperation'))->toBeTrue();
|
|
});
|