368 lines
12 KiB
PHP
368 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use classes\edge_gateway_manager;
|
|
|
|
usesApiSuite();
|
|
|
|
it('serves installer artifacts and recovers gateway runtime status after fresh heartbeats', function (): void {
|
|
$department = api_fixtures()->createDepartment([
|
|
'name' => 'Edge Agent Department',
|
|
]);
|
|
$session = api_fixtures()->createEdgeOperatorSession((int)$department['id']);
|
|
$installTokenResponse = api_client()->post('/edge-gateways/install-token', [
|
|
'department_id' => (int)$department['id'],
|
|
'label' => 'Edge Agent Install',
|
|
], $session['headers']);
|
|
|
|
$installTokenResponse
|
|
->assertStatus(201)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$installToken = $installTokenResponse->data();
|
|
|
|
$installScript = api_client()->get('/edge-agent/install.sh?token=' . urlencode((string)$installToken['token']));
|
|
|
|
expect($installScript->status)->toBe(200)
|
|
->and($installScript->body)
|
|
->toContain('/edge-agent/install-token/status')
|
|
->toContain('agent.php')
|
|
->toContain((string)$installToken['token']);
|
|
|
|
$artifact = api_client()->get('/edge-agent/artifacts/agent.php');
|
|
expect($artifact->status)->toBe(200)
|
|
->and($artifact->body)
|
|
->toContain('<?php');
|
|
|
|
$claimResponse = api_client()->post('/edge-agent/claim', [
|
|
'token' => (string)$installToken['token'],
|
|
'hostname' => 'edge-agent-api',
|
|
'installed_version' => 'php-agent-v1',
|
|
]);
|
|
|
|
$claimResponse
|
|
->assertStatus(201)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$gatewayId = (int)($claimResponse->data()['gateway']['id'] ?? 0);
|
|
$agentToken = (string)($claimResponse->data()['agent_token'] ?? '');
|
|
|
|
expect($gatewayId)->toBeGreaterThan(0)
|
|
->and($agentToken)->not->toBe('');
|
|
|
|
edge_agent_test_set_heartbeat_age($gatewayId, edge_gateway_manager::HEARTBEAT_DEGRADED_AFTER_SECONDS + 1);
|
|
|
|
$degradedDetail = api_client()->get('/edge-gateways/' . $gatewayId, $session['headers']);
|
|
|
|
$degradedDetail
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($degradedDetail->data())
|
|
->toHaveKey('status', 'DEGRADED');
|
|
|
|
edge_agent_test_set_heartbeat_age($gatewayId, edge_gateway_manager::HEARTBEAT_OFFLINE_AFTER_SECONDS + 1);
|
|
|
|
$offlineDetail = api_client()->get('/edge-gateways/' . $gatewayId, $session['headers']);
|
|
|
|
$offlineDetail
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($offlineDetail->data())
|
|
->toHaveKey('status', 'OFFLINE');
|
|
|
|
$heartbeatResponse = api_client()->post('/edge-agent/gateways/' . $gatewayId . '/heartbeat', [
|
|
'agent_token' => $agentToken,
|
|
'status' => 'ONLINE',
|
|
'hostname' => 'edge-agent-api',
|
|
'metadata' => [
|
|
'agent_instance_id' => 'edge-agent-api-test',
|
|
'broker_connected' => true,
|
|
'broker_url' => 'wss://broker.example.test/edge-broker',
|
|
'broker_last_connected_at' => '2026-04-08T10:05:00+00:00',
|
|
'system_metrics' => [
|
|
'cpu_percent' => 21,
|
|
'memory_mb' => 128,
|
|
],
|
|
],
|
|
'inventory' => edge_agent_test_inventory('heartbeat'),
|
|
]);
|
|
|
|
$heartbeatResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($heartbeatResponse->data())
|
|
->toHaveKey('status', 'ONLINE')
|
|
->and($heartbeatResponse->data()['broker_url'] ?? null)
|
|
->toBeString()
|
|
->toContain('/edge-broker');
|
|
|
|
$recoveredDetail = api_client()->get('/edge-gateways/' . $gatewayId, $session['headers']);
|
|
|
|
$recoveredDetail
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($recoveredDetail->data())
|
|
->toHaveKey('status', 'ONLINE')
|
|
->and($recoveredDetail->data()['metadata']['system_metrics']['cpu_percent'] ?? null)
|
|
->toBe(21)
|
|
->and($recoveredDetail->data()['metadata']['broker_presence']['connected'] ?? null)
|
|
->toBeTrue()
|
|
->and($recoveredDetail->data()['channel_status']['broker']['connected'] ?? null)
|
|
->toBeTrue()
|
|
->and($recoveredDetail->data()['channel_status']['command']['preferred'] ?? null)
|
|
->toBe(edge_gateway_manager::DELIVERY_CHANNEL_BROKER);
|
|
});
|
|
|
|
it('polls operations and commands, submits results, and records broker presence for task pages', function (): void {
|
|
$department = api_fixtures()->createDepartment([
|
|
'name' => 'Edge Agent Operations Department',
|
|
]);
|
|
$session = api_fixtures()->createEdgeOperatorSession((int)$department['id']);
|
|
$gateway = api_fixtures()->createClaimedEdgeGateway([
|
|
'department_id' => (int)$department['id'],
|
|
'label' => 'Edge Agent Runtime',
|
|
]);
|
|
|
|
$operationResponse = api_client()->post('/edge-gateways/' . (int)$gateway['id'] . '/operations', [
|
|
'type' => 'DISCOVERY',
|
|
'request' => [
|
|
'inventory' => edge_agent_test_inventory('operation'),
|
|
],
|
|
], $session['headers']);
|
|
|
|
$operationResponse
|
|
->assertStatus(201)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$operationId = (int)($operationResponse->data()['operation']['id'] ?? 0);
|
|
expect($operationId)->toBeGreaterThan(0);
|
|
|
|
$operationLease = api_client()->post('/edge-agent/gateways/' . (int)$gateway['id'] . '/operations/next', [
|
|
'agent_token' => (string)$gateway['agent_token'],
|
|
'wait_seconds' => 0,
|
|
'agent_instance_id' => 'edge-agent-api-test',
|
|
]);
|
|
|
|
$operationLease
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($operationLease->data())
|
|
->toHaveKey('id', $operationId)
|
|
->toHaveKey('status', 'IN_PROGRESS');
|
|
|
|
$eventResponse = api_client()->post(
|
|
'/edge-agent/gateways/' . (int)$gateway['id'] . '/operations/' . $operationId . '/events',
|
|
[
|
|
'agent_token' => (string)$gateway['agent_token'],
|
|
'level' => 'INFO',
|
|
'code' => 'DISCOVERY_RUNNING',
|
|
'message' => 'Discovery is running through the agent API.',
|
|
'context' => [
|
|
'progress' => 55,
|
|
'label' => 'Discovery running',
|
|
],
|
|
]
|
|
);
|
|
|
|
$eventResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$completeResponse = api_client()->post(
|
|
'/edge-agent/gateways/' . (int)$gateway['id'] . '/operations/' . $operationId . '/complete',
|
|
[
|
|
'agent_token' => (string)$gateway['agent_token'],
|
|
'ok' => true,
|
|
'result' => [
|
|
'inventory' => edge_agent_test_inventory('completed'),
|
|
],
|
|
]
|
|
);
|
|
|
|
$completeResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($completeResponse->data())
|
|
->toHaveKey('status', 'COMPLETED');
|
|
|
|
$job = api_fixtures()->createEdgeCommandJob([
|
|
'gateway_id' => (int)$gateway['id'],
|
|
'command_type' => 'GET_RELAY_STATUS',
|
|
'request' => [
|
|
'relayId' => 'relay-main',
|
|
'localIp' => '10.0.0.18',
|
|
],
|
|
]);
|
|
|
|
$commandPoll = api_client()->post('/edge-agent/gateways/' . (int)$gateway['id'] . '/commands/poll', [
|
|
'agent_token' => (string)$gateway['agent_token'],
|
|
'wait_seconds' => 0,
|
|
]);
|
|
|
|
$commandPoll
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($commandPoll->data())
|
|
->toHaveKey('id', (int)$job['id'])
|
|
->toHaveKey('command_type', 'GET_RELAY_STATUS');
|
|
|
|
$commandResult = api_client()->post(
|
|
'/edge-agent/gateways/' . (int)$gateway['id'] . '/commands/' . (int)$job['id'] . '/result',
|
|
[
|
|
'agent_token' => (string)$gateway['agent_token'],
|
|
'ok' => true,
|
|
'result' => [
|
|
'relayId' => 'relay-main',
|
|
'online' => true,
|
|
],
|
|
]
|
|
);
|
|
|
|
$commandResult
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($commandResult->data())
|
|
->toHaveKey('acknowledged', true)
|
|
->and($commandResult->data()['job']['status'] ?? null)
|
|
->toBe('COMPLETED');
|
|
|
|
$presenceResponse = api_client()->post('/edge-agent/gateways/' . (int)$gateway['id'] . '/presence', [
|
|
'agent_token' => (string)$gateway['agent_token'],
|
|
'status' => 'connected',
|
|
'connection_id' => 'broker-connection-1',
|
|
'metadata' => [
|
|
'transport' => 'ws',
|
|
],
|
|
]);
|
|
|
|
$presenceResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($presenceResponse->data())
|
|
->toHaveKey('connected', true);
|
|
|
|
$tasksPage = api_client()->get('/edge-gateways/' . (int)$gateway['id'] . '/tasks', $session['headers']);
|
|
|
|
$tasksPage
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($tasksPage->data()['operations'] ?? [])
|
|
->toBeArray()
|
|
->not->toBeEmpty()
|
|
->and($tasksPage->data()['recent_commands'] ?? [])
|
|
->toBeArray()
|
|
->not->toBeEmpty();
|
|
|
|
expect($tasksPage->data()['recent_operations_summary']['completed'] ?? 0)
|
|
->toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('rejects missing and invalid edge agent tokens', function (): void {
|
|
$department = api_fixtures()->createDepartment([
|
|
'name' => 'Edge Agent Auth Department',
|
|
]);
|
|
$gateway = api_fixtures()->createClaimedEdgeGateway([
|
|
'department_id' => (int)$department['id'],
|
|
]);
|
|
|
|
$missingToken = api_client()->get('/edge-agent/install-token/verify');
|
|
|
|
$missingToken
|
|
->assertStatus(400)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMessage('Missing token');
|
|
|
|
$missingAgentToken = api_client()->post('/edge-agent/gateways/' . (int)$gateway['id'] . '/heartbeat', [
|
|
'status' => 'ONLINE',
|
|
]);
|
|
|
|
$missingAgentToken
|
|
->assertStatus(401)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMessage('Missing edge gateway agent token');
|
|
|
|
$invalidAgentToken = api_client()->post('/edge-agent/gateways/' . (int)$gateway['id'] . '/operations/next', [
|
|
'agent_token' => 'invalid-token',
|
|
'wait_seconds' => 0,
|
|
]);
|
|
|
|
$invalidAgentToken
|
|
->assertStatus(401)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMessage('Invalid edge gateway token');
|
|
});
|
|
|
|
function edge_agent_test_set_heartbeat_age(int $gatewayId, int $secondsAgo): void
|
|
{
|
|
$db = api_test_runtime()->db();
|
|
$row = api_fixtures()->fetchRowById('edge_gateways', $gatewayId) ?? [];
|
|
$referenceTimestamp = strtotime((string)($row['last_heartbeat_at'] ?? $row['updated_at'] ?? $row['created_at'] ?? ''));
|
|
if ($referenceTimestamp === false || $referenceTimestamp <= 0) {
|
|
$referenceTimestamp = time();
|
|
}
|
|
$timestamp = date('Y-m-d H:i:s', $referenceTimestamp - max(0, $secondsAgo));
|
|
$escapedTimestamp = $db->real_escape_string($timestamp);
|
|
$db->query("UPDATE edge_gateways SET last_heartbeat_at = '{$escapedTimestamp}', status = 'ONLINE' WHERE id = " . (int)$gatewayId);
|
|
|
|
$redis = api_test_runtime()->redis();
|
|
if ($redis !== null) {
|
|
foreach ([
|
|
'obj_prop:*:' . $gatewayId . ':status',
|
|
'obj_prop:*:' . $gatewayId . ':last_heartbeat_at',
|
|
'obj_prop:*:' . $gatewayId . ':updated_at',
|
|
] as $pattern) {
|
|
$keys = $redis->keys($pattern);
|
|
if (is_array($keys) && $keys !== []) {
|
|
$redis->del($keys);
|
|
}
|
|
}
|
|
}
|
|
|
|
api_fixtures()->clearEdgeGatewayViewCache();
|
|
}
|
|
|
|
function edge_agent_test_inventory(string $suffix): array
|
|
{
|
|
return [[
|
|
'device_id' => 'agent-' . $suffix,
|
|
'local_ip' => '10.30.40.50',
|
|
'model' => 'TruckWash Edge Agent',
|
|
'channel_count' => 1,
|
|
'online' => true,
|
|
'capabilities' => [
|
|
'gateway_management_v2' => true,
|
|
],
|
|
'metadata' => [
|
|
'hostname' => 'edge-' . $suffix,
|
|
],
|
|
]];
|
|
}
|