getVariableValue(); $publicBrokerConfig->setVariableValue(''); } catch (Throwable) { $publicBrokerConfig = null; } try { $callback(); } finally { $_SERVER = $originalServer; if ($originalPublicApiUrl === false) { putenv('EDGE_PUBLIC_API_URL'); } else { putenv('EDGE_PUBLIC_API_URL=' . $originalPublicApiUrl); } if ($originalPublicBrokerUrl === false) { putenv('EDGE_PUBLIC_BROKER_URL'); } else { putenv('EDGE_PUBLIC_BROKER_URL=' . $originalPublicBrokerUrl); } if ($publicBrokerConfig !== null && $originalPublicBrokerConfig !== null) { try { $publicBrokerConfig->setVariableValue($originalPublicBrokerConfig); } catch (Throwable) { } } } } function invoke_edge_gateway_private(object $instance, string $method, mixed ...$arguments): mixed { $reflection = new ReflectionMethod($instance, $method); return $reflection->invokeArgs($instance, $arguments); } it('builds install script urls with the compose edge gateway artifacts and forwarded https scheme', function (): void { with_edge_gateway_server_state([ 'HTTP_HOST' => 'api.truckwash.io:4433', 'HTTP_X_FORWARDED_PROTO' => 'https', ], function (): void { $manager = new EdgeGatewayManagerUrlHarness(); $script = $manager->buildInstallScript('abc123'); expect($manager->getApiBaseUrl())->toBe('https://api.truckwash.io:4433'); expect($manager->buildInstallTokenVerifyUrl('abc123'))->toBe('https://api.truckwash.io:4433/edge-agent/install-token/verify?token=abc123'); expect($manager->buildInstallScriptUrl('abc123'))->toBe('https://api.truckwash.io:4433/edge-agent/install.sh?token=abc123'); expect($manager->buildInstallCommand('abc123'))->toContain("https://api.truckwash.io:4433/edge-agent/install.sh?token=abc123"); expect($script)->toContain('fetch_http "Verify install token" "https://api.truckwash.io:4433/edge-agent/install-token/verify?token=abc123"'); expect($script)->toContain('INSTALL_STATUS_URL="https://api.truckwash.io:4433/edge-agent/install-token/status"'); expect($script)->toContain('fetch_http "Download artifact manifest" "https://api.truckwash.io:4433/edge-agent/artifacts/manifest.json" "$INSTALL_DIR/manifest.json"'); expect($script)->toContain('fetch_http "Download PHP edge agent" "https://api.truckwash.io:4433/edge-agent/artifacts/agent.php" "$INSTALL_DIR/agent.php"'); expect($script)->toContain('fetch_http "Download LAN worker" "https://api.truckwash.io:4433/edge-agent/artifacts/lan-worker.php" "$INSTALL_DIR/lan-worker.php"'); expect($script)->toContain('fetch_http "Download compose stack" "https://api.truckwash.io:4433/edge-agent/artifacts/docker-compose.gateway.yml" "$INSTALL_DIR/docker-compose.gateway.yml"'); expect($script)->toContain('fetch_http "Download compose stack service unit" "https://api.truckwash.io:4433/edge-agent/artifacts/truckwash-edge-gateway-stack.service" "$INSTALL_DIR/truckwash-edge-gateway-stack.service"'); expect($script)->toContain('begin_install_phase "VERIFY_ARTIFACTS" "Verifying edge gateway artifacts"'); expect($script)->toContain('verify_manifest_artifact "$INSTALL_DIR/manifest.json" "agent.php" "$INSTALL_DIR/agent.php"'); expect($script)->toContain('report_install_status() {'); expect($script)->toContain('begin_install_phase "VERIFY_TOKEN" "Verifying install token"'); expect($script)->toContain('begin_install_phase "WAIT_FOR_CLAIM" "Waiting for gateway heartbeat and claim"'); expect($script)->toContain('report_install_status "FAILED" "FAILED" "$failure_message" "$diagnostics_json" "$gateway_id"'); expect($script)->toContain('report_install_status "CLAIMED" "CLAIMED"'); expect($script)->toContain('Installer failed during step ${CURRENT_STEP_CODE:-FAILED}: ${CURRENT_STEP:-unknown}'); expect($script)->toContain('Last request: ${CURRENT_METHOD} ${CURRENT_URL}'); expect($script)->toContain('Response body preview (first 400 bytes):'); expect($script)->toContain('begin_install_phase "REMOVE_EXISTING_INSTALL" "Removing existing edge gateway installation"'); expect($script)->toContain('run_step "Removing existing edge gateway installation" cleanup_existing_installation'); expect($script)->toContain('wait_for_gateway_claim "$CONFIG_PATH" "$HEARTBEAT_MARKER_PATH" "$INSTALL_STARTED_AT" 180'); expect($script)->not->toContain('wait_for_post_restart_heartbeat "$HEARTBEAT_MARKER_PATH" "$INSTALL_STARTED_AT" 180'); expect($script)->not->toContain('Gateway reconnected using preserved credentials.'); expect($script)->toContain('"apiUrl":"https://api.truckwash.io:4433"'); expect($script)->toContain('"serviceName":"truckwash-edge-agent.service"'); expect($script)->toContain('"stackServiceName":"truckwash-edge-gateway-stack.service"'); expect($script)->toContain('"composeFileName":"docker-compose.gateway.yml"'); expect($script)->toContain('"stateDatabasePath":"/opt/truckwash-edge-agent/runtime/gateway-state.sqlite"'); expect($script)->toContain('"operationPollTimeoutSeconds":20'); expect($script)->toContain('"installedVersion":"compose-php-agent-v3"'); expect($script)->toContain('"brokerUrl":"https://api.truckwash.io:4433/edge-broker"'); expect($script)->not->toContain('"shellActionPollTimeoutSeconds"'); }); }); it('appends forwarded ports when the forwarded host omits them', function (): void { with_edge_gateway_server_state([ 'HTTP_X_FORWARDED_PROTO' => 'https', 'HTTP_X_FORWARDED_HOST' => 'api.truckwash.io', 'HTTP_X_FORWARDED_PORT' => '4433', ], function (): void { $manager = new EdgeGatewayManagerUrlHarness(); expect($manager->getApiBaseUrl())->toBe('https://api.truckwash.io:4433'); }); }); it('prefers EDGE_PUBLIC_API_URL when explicitly configured', function (): void { with_edge_gateway_server_state([ 'HTTP_HOST' => 'localhost', 'HTTP_X_FORWARDED_PROTO' => 'http', 'HTTP_X_FORWARDED_PREFIX' => '/api', ], function (): void { putenv('EDGE_PUBLIC_API_URL=https://edge.example.test/api'); $manager = new EdgeGatewayManagerUrlHarness(); $script = $manager->buildInstallScript('token-1'); expect($manager->getApiBaseUrl())->toBe('https://edge.example.test/api'); expect($manager->buildInstallScriptUrl('token-1'))->toBe('https://edge.example.test/api/edge-agent/install.sh?token=token-1'); expect($script)->toContain('"apiUrl":"https://edge.example.test/api"'); expect($script)->toContain('"brokerUrl":"https://edge.example.test/api/edge-broker"'); }); }); it('builds websocket broker urls on the traefik broker path', function (): void { with_edge_gateway_server_state([ 'HTTP_HOST' => 'api.truckwash.io:4433', 'HTTP_X_FORWARDED_PROTO' => 'https', ], function (): void { $manager = new EdgeGatewayManagerUrlHarness(); expect(invoke_edge_gateway_private($manager, 'buildBrokerPublicUrl')) ->toBe('https://api.truckwash.io:4433/edge-broker'); expect(invoke_edge_gateway_private($manager, 'buildBrokerPublicWebSocketUrl', '/ws/browser-shell')) ->toBe('wss://api.truckwash.io:4433/edge-broker/ws/browser-shell'); }); }); it('builds localhost websocket broker urls on the local traefik api prefix', function (): void { with_edge_gateway_server_state([ 'HTTP_HOST' => 'localhost', 'HTTP_X_FORWARDED_PROTO' => 'http', 'HTTP_X_FORWARDED_PREFIX' => '/api', ], function (): void { $manager = new EdgeGatewayManagerUrlHarness(); expect($manager->getApiBaseUrl()) ->toBe('http://localhost/api'); expect(invoke_edge_gateway_private($manager, 'buildBrokerPublicUrl')) ->toBe('http://localhost/api/edge-broker'); expect(invoke_edge_gateway_private($manager, 'buildBrokerPublicWebSocketUrl', '/ws/browser-gateway-stream')) ->toBe('ws://localhost/api/edge-broker/ws/browser-gateway-stream'); expect(invoke_edge_gateway_private($manager, 'buildBrokerPublicWebSocketUrl', '/ws/browser-shell')) ->toBe('ws://localhost/api/edge-broker/ws/browser-shell'); }); }); it('keeps root-host api urls unprefixed when the request is not under the local api alias', function (): void { with_edge_gateway_server_state([ 'HTTP_HOST' => 'localhost', 'HTTP_X_FORWARDED_PROTO' => 'http', 'REQUEST_URI' => '/edge-gateways/84/stream-session', ], function (): void { $manager = new EdgeGatewayManagerUrlHarness(); expect($manager->getApiBaseUrl()) ->toBe('http://localhost'); expect(invoke_edge_gateway_private($manager, 'buildBrokerPublicUrl')) ->toBe('http://localhost/edge-broker'); }); });