419 lines
12 KiB
PHP
419 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
usesApiSuite();
|
|
|
|
it('creates install tokens, tracks installer status, and exposes claimed gateway detail to authorized operators', function (): void {
|
|
$department = api_fixtures()->createDepartment([
|
|
'name' => 'Edge Operator Department',
|
|
]);
|
|
$session = api_fixtures()->createEdgeOperatorSession((int)$department['id']);
|
|
|
|
$createResponse = api_client()->post('/edge-gateways/install-token', [
|
|
'department_id' => (int)$department['id'],
|
|
'label' => 'Dock 7 Gateway',
|
|
], $session['headers']);
|
|
|
|
$createResponse
|
|
->assertStatus(201)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$installToken = $createResponse->data();
|
|
expect($installToken)
|
|
->toBeArray()
|
|
->toHaveKeys(['claim_token_id', 'token', 'install_command']);
|
|
|
|
$statusResponse = api_client()->get(
|
|
'/edge-gateways/install-token/' . (int)$installToken['claim_token_id'] . '/status',
|
|
$session['headers']
|
|
);
|
|
|
|
$statusResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($statusResponse->data())
|
|
->toHaveKey('status', 'PENDING')
|
|
->toHaveKey('gateway_id', null);
|
|
|
|
$verifyResponse = api_client()->get('/edge-agent/install-token/verify?token=' . urlencode((string)$installToken['token']));
|
|
|
|
$verifyResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($verifyResponse->data())
|
|
->toHaveKey('valid', true)
|
|
->toHaveKey('claim_token_id', (int)$installToken['claim_token_id']);
|
|
|
|
$runningStatus = api_client()->post('/edge-agent/install-token/status', [
|
|
'token' => (string)$installToken['token'],
|
|
'status' => 'RUNNING',
|
|
'step' => 'BOOTSTRAP',
|
|
'message' => 'Installer is downloading gateway artifacts.',
|
|
'diagnostics' => [
|
|
'download-1',
|
|
'download-2',
|
|
'download-3',
|
|
'download-4',
|
|
'download-5',
|
|
'download-6',
|
|
'download-7',
|
|
],
|
|
]);
|
|
|
|
$runningStatus
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$runningStatusDetail = api_client()->get(
|
|
'/edge-gateways/install-token/' . (int)$installToken['claim_token_id'] . '/status',
|
|
$session['headers']
|
|
);
|
|
|
|
$runningStatusDetail
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($runningStatusDetail->data())
|
|
->toHaveKey('status', 'RUNNING')
|
|
->toHaveKey('step', 'BOOTSTRAP')
|
|
->and($runningStatusDetail->data()['diagnostics'] ?? [])
|
|
->toBeArray()
|
|
->toHaveCount(6);
|
|
|
|
$claimResponse = api_client()->post('/edge-agent/claim', [
|
|
'token' => (string)$installToken['token'],
|
|
'hostname' => 'edge-operator-api',
|
|
'installed_version' => 'php-agent-v1',
|
|
'metadata' => [
|
|
'agent_runtime' => 'compose-php',
|
|
],
|
|
]);
|
|
|
|
$claimResponse
|
|
->assertStatus(201)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$claimedPayload = $claimResponse->data();
|
|
$gatewayId = (int)($claimedPayload['gateway']['id'] ?? 0);
|
|
|
|
expect($claimedPayload)
|
|
->toHaveKey('agent_token')
|
|
->and($gatewayId)
|
|
->toBeGreaterThan(0)
|
|
->and($claimedPayload['gateway']['status'] ?? null)
|
|
->toBe('ONLINE');
|
|
|
|
$claimedStatus = api_client()->get(
|
|
'/edge-gateways/install-token/' . (int)$installToken['claim_token_id'] . '/status',
|
|
$session['headers']
|
|
);
|
|
|
|
$claimedStatus
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($claimedStatus->data())
|
|
->toHaveKey('status', 'CLAIMED')
|
|
->toHaveKey('gateway_id', $gatewayId)
|
|
->toHaveKey('last_error', null);
|
|
|
|
$listResponse = api_client()->get('/edge-gateways?department_id=' . (int)$department['id'], $session['headers']);
|
|
|
|
$listResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect(collect_gateway_ids_from_api_response($listResponse->data()))
|
|
->toContain($gatewayId);
|
|
|
|
$detailResponse = api_client()->get('/edge-gateways/' . $gatewayId, $session['headers']);
|
|
|
|
$detailResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($detailResponse->data())
|
|
->toHaveKey('id', $gatewayId)
|
|
->toHaveKey('department_id', (int)$department['id'])
|
|
->toHaveKey('status', 'ONLINE');
|
|
});
|
|
|
|
it('manages edge gateway metadata, bindings, operations, sessions, rotation, cutover, and deletion', function (): void {
|
|
$department = api_fixtures()->createDepartment([
|
|
'name' => 'Edge Operator Control Department',
|
|
]);
|
|
$session = api_fixtures()->createEdgeOperatorSession((int)$department['id']);
|
|
$gateway = api_fixtures()->createClaimedEdgeGateway([
|
|
'department_id' => (int)$department['id'],
|
|
'label' => 'Original Gateway Label',
|
|
]);
|
|
api_fixtures()->createClaimedEdgeGateway([
|
|
'department_id' => (int)$department['id'],
|
|
'label' => 'Alternate Gateway Label',
|
|
'is_primary' => 0,
|
|
]);
|
|
|
|
$updateResponse = api_client()->put('/edge-gateways/' . (int)$gateway['id'], [
|
|
'label' => 'Renamed Gateway',
|
|
'is_primary' => false,
|
|
], $session['headers']);
|
|
|
|
$updateResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($updateResponse->data())
|
|
->toHaveKey('label', 'Renamed Gateway')
|
|
->toHaveKey('is_primary', false);
|
|
|
|
$bindingsResponse = api_client()->put('/edge-gateways/' . (int)$gateway['id'] . '/bindings', [
|
|
'bindings' => [[
|
|
'relay_id' => 'relay-main',
|
|
'device_id' => 'device-main',
|
|
'local_ip' => '10.0.0.18',
|
|
'channel' => 0,
|
|
'binding_source' => 'MANUAL',
|
|
]],
|
|
], $session['headers']);
|
|
|
|
$bindingsResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($bindingsResponse->data()['bindings'] ?? [])
|
|
->toBeArray()
|
|
->toHaveCount(1)
|
|
->and(($bindingsResponse->data()['bindings'][0]['relay_id'] ?? null))
|
|
->toBe('relay-main');
|
|
|
|
$operationResponse = api_client()->post('/edge-gateways/' . (int)$gateway['id'] . '/operations', [
|
|
'type' => 'DISCOVERY',
|
|
'request' => [
|
|
'inventory' => edge_operator_test_inventory('operator'),
|
|
],
|
|
], $session['headers']);
|
|
|
|
$operationResponse
|
|
->assertStatus(201)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$operationId = (int)($operationResponse->data()['operation']['id'] ?? 0);
|
|
expect($operationId)->toBeGreaterThan(0);
|
|
|
|
$operationsList = api_client()->get('/edge-gateways/' . (int)$gateway['id'] . '/operations', $session['headers']);
|
|
|
|
$operationsList
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect(collect_gateway_ids_from_api_response($operationsList->data(), 'id'))
|
|
->toContain($operationId);
|
|
|
|
$eventsResponse = api_client()->get(
|
|
'/edge-gateways/' . (int)$gateway['id'] . '/operations/' . $operationId . '/events',
|
|
$session['headers']
|
|
);
|
|
|
|
$eventsResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($eventsResponse->data())
|
|
->toBeArray()
|
|
->not->toBeEmpty()
|
|
->and($eventsResponse->data()[0]['code'] ?? null)
|
|
->toBe('OPERATION_QUEUED');
|
|
|
|
$cancelResponse = api_client()->post(
|
|
'/edge-gateways/' . (int)$gateway['id'] . '/operations/' . $operationId . '/cancel',
|
|
[],
|
|
$session['headers']
|
|
);
|
|
|
|
$cancelResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($cancelResponse->data()['operation']['status'] ?? null)
|
|
->toBe('CANCELLED');
|
|
|
|
$streamSession = api_client()->post(
|
|
'/edge-gateways/' . (int)$gateway['id'] . '/stream-session',
|
|
['scopes' => ['logs', 'tasks']],
|
|
$session['headers']
|
|
);
|
|
|
|
$streamSession
|
|
->assertStatus(201)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($streamSession->data())
|
|
->toHaveKey('token')
|
|
->toHaveKey('ws_url');
|
|
|
|
$shellSession = api_client()->post(
|
|
'/edge-gateways/' . (int)$gateway['id'] . '/shell-sessions',
|
|
[
|
|
'reason' => 'Operator smoke session',
|
|
'cwd' => '/opt/truckwash-edge-agent',
|
|
'cols' => 120,
|
|
'rows' => 40,
|
|
],
|
|
$session['headers']
|
|
);
|
|
|
|
$shellSession
|
|
->assertStatus(201)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($shellSession->data())
|
|
->toHaveKey('token')
|
|
->toHaveKey('session')
|
|
->and($shellSession->data()['session']['reason'] ?? null)
|
|
->toBe('Operator smoke session');
|
|
|
|
$rotateResponse = api_client()->post(
|
|
'/edge-gateways/' . (int)$gateway['id'] . '/rotate-credentials',
|
|
[],
|
|
$session['headers']
|
|
);
|
|
|
|
$rotateResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($rotateResponse->data())
|
|
->toHaveKey('gateway_id', (int)$gateway['id'])
|
|
->toHaveKey('agent_token')
|
|
->toHaveKey('config_json');
|
|
|
|
$cutoverResponse = api_client()->post(
|
|
'/departments/' . (int)$department['id'] . '/gateway-cutover',
|
|
['transport_mode' => 'gateway'],
|
|
$session['headers']
|
|
);
|
|
|
|
$cutoverResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($cutoverResponse->data())
|
|
->toHaveKey('department_id', (int)$department['id'])
|
|
->toHaveKey('transport_mode', 'gateway');
|
|
|
|
$deleteResponse = api_client()->delete('/edge-gateways/' . (int)$gateway['id'], null, $session['headers']);
|
|
|
|
$deleteResponse
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($deleteResponse->data())
|
|
->toHaveKey('deleted', true)
|
|
->toHaveKey('gateway_id', (int)$gateway['id']);
|
|
});
|
|
|
|
it('rejects operator edge routes when module permission or department access is missing', function (): void {
|
|
$department = api_fixtures()->createDepartment([
|
|
'name' => 'Edge Operator Access Department',
|
|
]);
|
|
$allowed = api_fixtures()->createEdgeOperatorSession((int)$department['id']);
|
|
$token = api_client()->post('/edge-gateways/install-token', [
|
|
'department_id' => (int)$department['id'],
|
|
'label' => 'Restricted Gateway',
|
|
], $allowed['headers']);
|
|
|
|
$token
|
|
->assertStatus(201)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
$missingModule = api_fixtures()->createUserSession([
|
|
'department_access_' . (int)$department['id'],
|
|
]);
|
|
|
|
$missingModuleResponse = api_client()->get(
|
|
'/edge-gateways?department_id=' . (int)$department['id'],
|
|
$missingModule['headers']
|
|
);
|
|
|
|
$missingModuleResponse
|
|
->assertStatus(403)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMissingPermissions(['modules_shelly_config']);
|
|
|
|
$missingDepartment = api_fixtures()->createUserSession(['modules_shelly_config']);
|
|
|
|
$missingDepartmentResponse = api_client()->get(
|
|
'/edge-gateways/install-token/' . (int)$token->data()['claim_token_id'] . '/status',
|
|
$missingDepartment['headers']
|
|
);
|
|
|
|
$missingDepartmentResponse
|
|
->assertStatus(403)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMissingPermissions(['department_access_' . (int)$department['id']]);
|
|
});
|
|
|
|
function edge_operator_test_inventory(string $suffix): array
|
|
{
|
|
return [[
|
|
'device_id' => 'gateway-' . $suffix,
|
|
'local_ip' => '10.20.30.40',
|
|
'model' => 'TruckWash Edge Test',
|
|
'channel_count' => 2,
|
|
'online' => true,
|
|
'capabilities' => [
|
|
'local_discovery' => true,
|
|
'relay_commands' => true,
|
|
],
|
|
'metadata' => [
|
|
'hostname' => 'edge-' . $suffix,
|
|
],
|
|
]];
|
|
}
|
|
|
|
/**
|
|
* @param mixed $items
|
|
* @return array<int, int>
|
|
*/
|
|
function collect_gateway_ids_from_api_response(mixed $items, string $key = 'id'): array
|
|
{
|
|
if (!is_array($items)) {
|
|
return [];
|
|
}
|
|
|
|
$ids = [];
|
|
foreach ($items as $item) {
|
|
if (is_array($item) && isset($item[$key]) && is_numeric($item[$key])) {
|
|
$ids[] = (int)$item[$key];
|
|
}
|
|
}
|
|
|
|
return $ids;
|
|
}
|