Files
api/services/nginx/app/tests/Api/EdgeGatewayAgentApiTest.php
T

377 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');
$manifest = api_client()->get('/edge-agent/artifacts/manifest.json');
expect($manifest->status)->toBe(200);
$manifestPayload = json_decode($manifest->body, true);
expect($manifestPayload)->toBeArray()
->and($manifestPayload['version'] ?? null)->toBe(edge_gateway_manager::DEFAULT_INSTALL_VERSION);
$manifestArtifacts = array_column((array)($manifestPayload['artifacts'] ?? []), null, 'name');
expect($manifestArtifacts)->toHaveKey('agent.php')
->and($manifestArtifacts['agent.php']['sha256'] ?? null)->toBe(hash('sha256', $artifact->body));
$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,
],
]];
}