816 lines
31 KiB
PHP
816 lines
31 KiB
PHP
<?php
|
|
|
|
app_require('classes/superuser_system_status_service.php');
|
|
|
|
use classes\superuser_system_status_service;
|
|
|
|
function system_status_module_value(mixed $value, string $type = 'string'): array
|
|
{
|
|
return [
|
|
'raw' => $value,
|
|
'parsed' => $value,
|
|
'type' => $type,
|
|
];
|
|
}
|
|
|
|
class SuperuserSystemStatusServiceProbeDouble extends superuser_system_status_service
|
|
{
|
|
public array $httpProbeCalls = [];
|
|
public array $nextHttpProbeResult = [
|
|
'status' => 'ok',
|
|
'status_reason' => 'Probe connectivity confirmed.',
|
|
'status_reason_key' => 'http_ok',
|
|
'status_reason_params' => ['label' => 'Probe'],
|
|
'checked_at' => '2026-04-08T17:00:00+00:00',
|
|
'latency_ms' => 12.34,
|
|
'http_status' => 200,
|
|
];
|
|
public ?array $moduleConfigRowsOverride = null;
|
|
public bool $backupStoreValidationShouldFail = false;
|
|
public string $backupStoreFailureMessage = 'Backup bucket is missing.';
|
|
public array $backupHealthSummary = [
|
|
'latest_verified_backup' => ['backup_uuid' => 'verified-backup'],
|
|
'latest_verified_age_seconds' => 60,
|
|
'fresh' => true,
|
|
'encryption' => ['available' => true, 'key_id' => 'test-key'],
|
|
'verification_required' => true,
|
|
'restore_enabled' => false,
|
|
];
|
|
public bool $selfserveBootstrapShouldFail = false;
|
|
public string $selfserveBootstrapFailureMessage = 'Schema bootstrap failed.';
|
|
public bool $selfserveMinuteProductExistsValue = true;
|
|
public ?int $selfserveMinuteProductCheckedId = null;
|
|
public ?string $shellyProbeDeviceId = null;
|
|
|
|
public function collectModulesPublic(bool $force, array &$warnings): array
|
|
{
|
|
return $this->collectModules($force, $warnings);
|
|
}
|
|
|
|
public function classifyHttpProbeResultPublic(array $httpResponse, string $label): array
|
|
{
|
|
return $this->classifyHttpProbeResult($httpResponse, $label);
|
|
}
|
|
|
|
public function evaluateRecaptchaProbeResponsePublic(array $httpResponse, string $label): array
|
|
{
|
|
return $this->evaluateRecaptchaProbeResponse($httpResponse, $label);
|
|
}
|
|
|
|
public function normalizeWarningEntriesPublic(array $warnings): array
|
|
{
|
|
return $this->normalizeWarningEntries($warnings);
|
|
}
|
|
|
|
public function probeEconomicModulePublic(array $config): array
|
|
{
|
|
return $this->probeEconomicModule($config);
|
|
}
|
|
|
|
public function probeRecaptchaModulePublic(array $config): array
|
|
{
|
|
return $this->probeRecaptchaModule($config);
|
|
}
|
|
|
|
public function probeEmailModulePublic(array $config): array
|
|
{
|
|
return $this->probeEmailModule($config);
|
|
}
|
|
|
|
public function probeBackupsModulePublic(array $config): array
|
|
{
|
|
return $this->probeBackupsModule($config);
|
|
}
|
|
|
|
public function probeMotorApiModulePublic(array $config): array
|
|
{
|
|
return $this->probeMotorApiModule($config);
|
|
}
|
|
|
|
public function probeFxRatesApiModulePublic(array $config): array
|
|
{
|
|
return $this->probeFxRatesApiModule($config);
|
|
}
|
|
|
|
public function probeWeatherApiModulePublic(array $config): array
|
|
{
|
|
return $this->probeWeatherApiModule($config);
|
|
}
|
|
|
|
public function probeWorkfeedModulePublic(array $config): array
|
|
{
|
|
return $this->probeWorkfeedModule($config);
|
|
}
|
|
|
|
public function probeGatewayApiModulePublic(array $config): array
|
|
{
|
|
return $this->probeGatewayApiModule($config);
|
|
}
|
|
|
|
public function probeXlVaskModulePublic(array $config): array
|
|
{
|
|
return $this->probeXlVaskModule($config);
|
|
}
|
|
|
|
public function probeLimbleModulePublic(array $config): array
|
|
{
|
|
return $this->probeLimbleModule($config);
|
|
}
|
|
|
|
public function probeLicensePlateRecognizerModulePublic(array $config): array
|
|
{
|
|
return $this->probeLicensePlateRecognizerModule($config);
|
|
}
|
|
|
|
public function probeShellyModulePublic(array $config): array
|
|
{
|
|
return $this->probeShellyModule($config);
|
|
}
|
|
|
|
public function probeSelfserveModulePublic(array $config): array
|
|
{
|
|
return $this->probeSelfserveModule($config);
|
|
}
|
|
|
|
public function probeBirdModulePublic(array $config): array
|
|
{
|
|
return $this->probeBirdModule($config);
|
|
}
|
|
|
|
protected function performHttpProbe(
|
|
string $url,
|
|
array $headers,
|
|
string $label,
|
|
?string $basicAuth = null,
|
|
string $method = 'GET',
|
|
?string $body = null,
|
|
?callable $responseEvaluator = null
|
|
): array {
|
|
$this->httpProbeCalls[] = [
|
|
'url' => $url,
|
|
'headers' => $headers,
|
|
'label' => $label,
|
|
'basic_auth' => $basicAuth,
|
|
'method' => $method,
|
|
'body' => $body,
|
|
'has_evaluator' => $responseEvaluator !== null,
|
|
];
|
|
|
|
return $this->nextHttpProbeResult;
|
|
}
|
|
|
|
protected function loadModuleConfigRows(array $moduleNames): array
|
|
{
|
|
if ($this->moduleConfigRowsOverride !== null) {
|
|
return $this->moduleConfigRowsOverride;
|
|
}
|
|
|
|
return parent::loadModuleConfigRows($moduleNames);
|
|
}
|
|
|
|
protected function validateBackupsStore(): void
|
|
{
|
|
if ($this->backupStoreValidationShouldFail) {
|
|
throw new RuntimeException($this->backupStoreFailureMessage);
|
|
}
|
|
}
|
|
|
|
protected function backupHealthSummary(): array
|
|
{
|
|
return $this->backupHealthSummary;
|
|
}
|
|
|
|
protected function bootstrapSelfserveSchema(): void
|
|
{
|
|
if ($this->selfserveBootstrapShouldFail) {
|
|
throw new RuntimeException($this->selfserveBootstrapFailureMessage);
|
|
}
|
|
}
|
|
|
|
protected function selfserveMinuteProductExists(int $productId): bool
|
|
{
|
|
$this->selfserveMinuteProductCheckedId = $productId;
|
|
return $this->selfserveMinuteProductExistsValue;
|
|
}
|
|
|
|
protected function findShellyProbeDeviceId(): ?string
|
|
{
|
|
return $this->shellyProbeDeviceId;
|
|
}
|
|
}
|
|
|
|
beforeEach(function (): void {
|
|
$this->previousEconomicApi = $GLOBALS['ECONOMIC_API'] ?? null;
|
|
});
|
|
|
|
afterEach(function (): void {
|
|
if ($this->previousEconomicApi === null) {
|
|
unset($GLOBALS['ECONOMIC_API']);
|
|
return;
|
|
}
|
|
|
|
$GLOBALS['ECONOMIC_API'] = $this->previousEconomicApi;
|
|
});
|
|
|
|
it('reduces overall status using down and degraded precedence', function (): void {
|
|
expect(superuser_system_status_service::reduceOverallStatus(['ok', 'configured']))->toBe('ok');
|
|
expect(superuser_system_status_service::reduceOverallStatus(['ok', 'not_configured']))->toBe('degraded');
|
|
expect(superuser_system_status_service::reduceOverallStatus(['configured', 'down']))->toBe('down');
|
|
});
|
|
|
|
it('classifies runtime usage percentages consistently', function (): void {
|
|
expect(superuser_system_status_service::statusFromUsagePercent(42.5))->toBe('ok');
|
|
expect(superuser_system_status_service::statusFromUsagePercent(91.0, 90, 99))->toBe('degraded');
|
|
expect(superuser_system_status_service::statusFromUsagePercent(99.1, 90, 99))->toBe('down');
|
|
expect(superuser_system_status_service::statusFromUsagePercent(null))->toBe('down');
|
|
});
|
|
|
|
it('reuses cached module probes only when the ttl is still valid and force is false', function (): void {
|
|
$freshCachedProbe = ['checked_at' => date('c', time() - 15)];
|
|
$expiredCachedProbe = ['checked_at' => date('c', time() - 120)];
|
|
|
|
expect(superuser_system_status_service::shouldReuseCachedModuleProbe($freshCachedProbe, false, time(), 60))->toBeTrue();
|
|
expect(superuser_system_status_service::shouldReuseCachedModuleProbe($freshCachedProbe, true, time(), 60))->toBeFalse();
|
|
expect(superuser_system_status_service::shouldReuseCachedModuleProbe($expiredCachedProbe, false, time(), 60))->toBeFalse();
|
|
});
|
|
|
|
it('classifies authenticated http probe responses conservatively', function (int $httpStatus, string $expectedStatus, string $reasonFragment, string $expectedReasonKey): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
|
|
$result = $service->classifyHttpProbeResultPublic([
|
|
'checked_at' => '2026-04-08T17:10:00+00:00',
|
|
'latency_ms' => 23.5,
|
|
'http_status' => $httpStatus,
|
|
'error' => '',
|
|
], 'Example API');
|
|
|
|
expect($result['status'])->toBe($expectedStatus);
|
|
expect($result['status_reason'])->toContain($reasonFragment);
|
|
expect($result['status_reason_key'])->toBe($expectedReasonKey);
|
|
})->with([
|
|
'success' => [200, 'ok', 'connectivity confirmed', 'http_ok'],
|
|
'unauthorized' => [401, 'down', 'HTTP 401', 'http_status'],
|
|
'forbidden' => [403, 'down', 'HTTP 403', 'http_status'],
|
|
'rate limited' => [429, 'degraded', 'rate limited', 'http_rate_limited'],
|
|
'server error' => [503, 'degraded', 'HTTP 503', 'http_status'],
|
|
'no response' => [0, 'down', 'did not return an HTTP response', 'http_no_response'],
|
|
]);
|
|
|
|
it('classifies transport errors as down', function (): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
|
|
$result = $service->classifyHttpProbeResultPublic([
|
|
'checked_at' => '2026-04-08T17:10:00+00:00',
|
|
'latency_ms' => 7.5,
|
|
'http_status' => 0,
|
|
'error' => 'Connection refused',
|
|
], 'Example API');
|
|
|
|
expect($result['status'])->toBe('down');
|
|
expect($result['status_reason'])->toContain('Connection refused');
|
|
expect($result['status_reason_key'])->toBe('http_probe_failed');
|
|
});
|
|
|
|
it('normalizes and deduplicates warning entries for snapshots', function (): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
|
|
$entries = $service->normalizeWarningEntriesPublic([
|
|
'Redis is unavailable; module probe caching is bypassed.',
|
|
[
|
|
'key' => 'redis_cache_bypass',
|
|
'params' => [],
|
|
'message' => 'Redis is unavailable; module probe caching is bypassed.',
|
|
],
|
|
[
|
|
'key' => 'modules_without_probes',
|
|
'params' => ['modules' => 'ocrspace, virkdata', 'module_keys' => ['ocrspace', 'virkdata']],
|
|
'message' => 'Some modules expose configuration-only status because no safe read-only probe exists: ocrspace, virkdata.',
|
|
],
|
|
[
|
|
'key' => 'modules_without_probes',
|
|
'params' => ['modules' => 'ocrspace, virkdata', 'module_keys' => ['ocrspace', 'virkdata']],
|
|
'message' => 'Some modules expose configuration-only status because no safe read-only probe exists: ocrspace, virkdata.',
|
|
],
|
|
]);
|
|
|
|
expect($entries)->toHaveCount(3);
|
|
expect($entries[0])->toBe([
|
|
'key' => null,
|
|
'params' => [],
|
|
'message' => 'Redis is unavailable; module probe caching is bypassed.',
|
|
]);
|
|
expect($entries[1]['key'])->toBe('redis_cache_bypass');
|
|
expect($entries[2]['params']['module_keys'])->toBe(['ocrspace', 'virkdata']);
|
|
});
|
|
|
|
it('interprets recaptcha probe payloads safely', function (array $payload, string $expectedStatus, string $reasonFragment, string $expectedReasonKey): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
|
|
$result = $service->evaluateRecaptchaProbeResponsePublic([
|
|
'checked_at' => '2026-04-08T17:10:00+00:00',
|
|
'latency_ms' => 11.4,
|
|
'http_status' => 200,
|
|
'body' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
'error' => '',
|
|
], 'reCAPTCHA');
|
|
|
|
expect($result['status'])->toBe($expectedStatus);
|
|
expect($result['status_reason'])->toContain($reasonFragment);
|
|
expect($result['status_reason_key'])->toBe($expectedReasonKey);
|
|
})->with([
|
|
'dummy token rejected but credentials valid' => [
|
|
['success' => false, 'error-codes' => ['invalid-input-response']],
|
|
'ok',
|
|
'connectivity confirmed',
|
|
'http_ok',
|
|
],
|
|
'invalid secret is down' => [
|
|
['success' => false, 'error-codes' => ['invalid-input-secret']],
|
|
'down',
|
|
'credentials were rejected',
|
|
'recaptcha_credentials_rejected',
|
|
],
|
|
'unexpected validation errors degrade' => [
|
|
['success' => false, 'error-codes' => ['bad-request']],
|
|
'degraded',
|
|
'unexpected validation errors',
|
|
'recaptcha_validation_errors',
|
|
],
|
|
]);
|
|
|
|
it('builds the expected http probe requests for newly supported modules', function (
|
|
string $methodName,
|
|
array $config,
|
|
string $expectedUrl,
|
|
array $expectedHeaders,
|
|
?string $expectedBasicAuth,
|
|
string $expectedMethod,
|
|
?string $expectedBodyFragment,
|
|
bool $expectsEvaluator
|
|
): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
|
|
$service->{$methodName}($config);
|
|
|
|
expect($service->httpProbeCalls)->toHaveCount(1);
|
|
$call = $service->httpProbeCalls[0];
|
|
|
|
expect($call['url'])->toBe($expectedUrl);
|
|
expect($call['basic_auth'])->toBe($expectedBasicAuth);
|
|
expect($call['method'])->toBe($expectedMethod);
|
|
expect($call['has_evaluator'])->toBe($expectsEvaluator);
|
|
|
|
foreach ($expectedHeaders as $expectedHeader) {
|
|
expect($call['headers'])->toContain($expectedHeader);
|
|
}
|
|
|
|
if ($expectedBodyFragment !== null) {
|
|
expect((string)$call['body'])->toContain($expectedBodyFragment);
|
|
} else {
|
|
expect($call['body'])->toBeNull();
|
|
}
|
|
})->with([
|
|
'recaptcha' => [
|
|
'probeRecaptchaModulePublic',
|
|
['secret_key_v2' => system_status_module_value('recaptcha-secret')],
|
|
'https://www.google.com/recaptcha/api/siteverify',
|
|
['Content-Type: application/x-www-form-urlencoded'],
|
|
null,
|
|
'POST',
|
|
'secret=recaptcha-secret',
|
|
true,
|
|
],
|
|
'email' => [
|
|
'probeEmailModulePublic',
|
|
['mailersend_api_key' => system_status_module_value('mailersend-key')],
|
|
'https://api.mailersend.com/v1/api-quota',
|
|
['Authorization: Bearer mailersend-key', 'Accept: application/json'],
|
|
null,
|
|
'GET',
|
|
null,
|
|
false,
|
|
],
|
|
'motorapi' => [
|
|
'probeMotorApiModulePublic',
|
|
['secret_key' => system_status_module_value('motorapi-key')],
|
|
'https://v1.motorapi.dk/usage',
|
|
['X-AUTH-TOKEN: motorapi-key'],
|
|
null,
|
|
'GET',
|
|
null,
|
|
false,
|
|
],
|
|
'fxratesapi' => [
|
|
'probeFxRatesApiModulePublic',
|
|
['secret_key' => system_status_module_value('fxrates-key')],
|
|
'https://api.fxratesapi.com/latest?base=EUR¤cies=DKK',
|
|
['X-AUTH-TOKEN: fxrates-key'],
|
|
null,
|
|
'GET',
|
|
null,
|
|
false,
|
|
],
|
|
'weatherapi' => [
|
|
'probeWeatherApiModulePublic',
|
|
['secret_key' => system_status_module_value('weather-key')],
|
|
'https://api.weatherapi.com/v1/current.json?key=weather-key&q=Copenhagen',
|
|
['Accept: application/json'],
|
|
null,
|
|
'GET',
|
|
null,
|
|
false,
|
|
],
|
|
'workfeed' => [
|
|
'probeWorkfeedModulePublic',
|
|
[
|
|
'api_url' => system_status_module_value('https://api.workfeed.test'),
|
|
'CompanyID' => system_status_module_value('company-123'),
|
|
'api_key' => system_status_module_value('workfeed-token'),
|
|
],
|
|
'https://api.workfeed.test/companies/company-123/departments',
|
|
['Accept: application/json', 'Authorization: workfeed-token'],
|
|
null,
|
|
'GET',
|
|
null,
|
|
false,
|
|
],
|
|
'gatewayapi' => [
|
|
'probeGatewayApiModulePublic',
|
|
['api_token' => system_status_module_value('gateway-token')],
|
|
'https://gatewayapi.eu/rest/me',
|
|
['Authorization: Token gateway-token', 'Accept: application/json'],
|
|
null,
|
|
'GET',
|
|
null,
|
|
false,
|
|
],
|
|
'xlvask' => [
|
|
'probeXlVaskModulePublic',
|
|
[
|
|
'username' => system_status_module_value('xl-user'),
|
|
'password' => system_status_module_value('xl-pass'),
|
|
],
|
|
'https://api.xlwash.com/customers',
|
|
['Accept: application/json'],
|
|
'xl-user:xl-pass',
|
|
'GET',
|
|
null,
|
|
false,
|
|
],
|
|
'limble' => [
|
|
'probeLimbleModulePublic',
|
|
[
|
|
'client_id' => system_status_module_value('limble-id'),
|
|
'client_secret' => system_status_module_value('limble-secret'),
|
|
],
|
|
'https://api.limblecmms.com:443/v2/tasks?limit=1&page=1',
|
|
['Accept: application/json', 'Content-Type: application/json'],
|
|
'limble-id:limble-secret',
|
|
'GET',
|
|
null,
|
|
false,
|
|
],
|
|
'license plate recognizer' => [
|
|
'probeLicensePlateRecognizerModulePublic',
|
|
['api_key' => system_status_module_value('lpr-key')],
|
|
'https://vs4sws0kg4sog4ssw8kwowk4.coolify.truckwash.dk/info/',
|
|
['Authorization: Token lpr-key', 'Accept: application/json'],
|
|
null,
|
|
'GET',
|
|
null,
|
|
false,
|
|
],
|
|
'bird' => [
|
|
'probeBirdModulePublic',
|
|
[
|
|
'server_url' => system_status_module_value('https://api.bird.com'),
|
|
'api_key' => system_status_module_value('bird-key'),
|
|
'channelId' => system_status_module_value('channel-123'),
|
|
'workplaceId' => system_status_module_value('workspace-456'),
|
|
],
|
|
'https://api.bird.com/workspaces/workspace-456/channels/channel-123/calls?limit=1',
|
|
['Authorization: AccessKey bird-key', 'Accept: application/json'],
|
|
null,
|
|
'GET',
|
|
null,
|
|
false,
|
|
],
|
|
]);
|
|
|
|
it('uses runtime economic credentials for the economic probe', function (): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
$GLOBALS['ECONOMIC_API'] = [
|
|
'app_secret_token' => 'economic-secret',
|
|
'app_access_grant' => 'economic-grant',
|
|
];
|
|
|
|
$service->probeEconomicModulePublic([]);
|
|
|
|
expect($service->httpProbeCalls)->toHaveCount(1);
|
|
$call = $service->httpProbeCalls[0];
|
|
|
|
expect($call['url'])->toBe('https://restapi.e-conomic.com/layouts/');
|
|
expect($call['headers'])->toContain('X-AppSecretToken: economic-secret');
|
|
expect($call['headers'])->toContain('X-AgreementGrantToken: economic-grant');
|
|
});
|
|
|
|
it('returns down without attempting economic http calls when runtime credentials are missing', function (): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
unset($GLOBALS['ECONOMIC_API']);
|
|
|
|
$result = $service->probeEconomicModulePublic([]);
|
|
|
|
expect($result['status'])->toBe('down');
|
|
expect($result['status_reason'])->toContain('credentials are missing');
|
|
expect($result['status_reason_key'])->toBe('economic_credentials_missing');
|
|
expect($service->httpProbeCalls)->toBeEmpty();
|
|
});
|
|
|
|
it('reports backup probe failures from local validation', function (): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
$service->backupStoreValidationShouldFail = true;
|
|
$service->backupStoreFailureMessage = 'Backups bucket is unavailable.';
|
|
|
|
$result = $service->probeBackupsModulePublic([]);
|
|
|
|
expect($result['status'])->toBe('down');
|
|
expect($result['status_reason'])->toContain('Backups bucket is unavailable');
|
|
expect($result['status_reason_key'])->toBe('backup_probe_failed');
|
|
});
|
|
|
|
it('reports backups down when encryption key is missing', function (): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
$service->backupHealthSummary['encryption'] = [
|
|
'available' => false,
|
|
'error' => 'BACKUP_ENCRYPTION_KEY_V1 is not configured.',
|
|
];
|
|
|
|
$result = $service->probeBackupsModulePublic([]);
|
|
|
|
expect($result['status'])->toBe('down');
|
|
expect($result['status_reason_key'])->toBe('backup_encryption_key_missing');
|
|
});
|
|
|
|
it('reports backups degraded when no verified backup is available', function (): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
$service->backupHealthSummary['latest_verified_backup'] = null;
|
|
$service->backupHealthSummary['fresh'] = false;
|
|
|
|
$result = $service->probeBackupsModulePublic([]);
|
|
|
|
expect($result['status'])->toBe('degraded');
|
|
expect($result['status_reason_key'])->toBe('backup_no_verified_backup');
|
|
});
|
|
|
|
it('reports backups degraded when the latest verified backup is stale', function (): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
$service->backupHealthSummary['fresh'] = false;
|
|
$service->backupHealthSummary['latest_verified_age_seconds'] = 7200;
|
|
|
|
$result = $service->probeBackupsModulePublic([]);
|
|
|
|
expect($result['status'])->toBe('degraded');
|
|
expect($result['status_reason_key'])->toBe('backup_latest_verified_stale');
|
|
});
|
|
|
|
it('returns configured for shelly when no known device id is available for probing', function (): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
|
|
$result = $service->probeShellyModulePublic([
|
|
'server_url' => system_status_module_value('https://shelly.example'),
|
|
'secret_key' => system_status_module_value('shelly-secret'),
|
|
]);
|
|
|
|
expect($result['status'])->toBe('configured');
|
|
expect($result['status_reason'])->toContain('no known device id');
|
|
expect($result['status_reason_key'])->toBe('shelly_no_device_id');
|
|
expect($service->httpProbeCalls)->toBeEmpty();
|
|
});
|
|
|
|
it('builds an authenticated shelly status probe when a known device id exists', function (): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
$service->shellyProbeDeviceId = 'device-123';
|
|
|
|
$service->probeShellyModulePublic([
|
|
'server_url' => system_status_module_value('https://shelly.example'),
|
|
'secret_key' => system_status_module_value('shelly-secret'),
|
|
]);
|
|
|
|
expect($service->httpProbeCalls)->toHaveCount(1);
|
|
$call = $service->httpProbeCalls[0];
|
|
expect($call['url'])->toBe('https://shelly.example/device/status?id=device-123&auth_key=shelly-secret');
|
|
expect($call['headers'])->toContain('Accept: application/json');
|
|
expect($call['method'])->toBe('GET');
|
|
});
|
|
|
|
it('validates selfserve schema and minute product configuration', function (): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
|
|
$result = $service->probeSelfserveModulePublic([
|
|
'machine_wash_minutes_included' => system_status_module_value('15'),
|
|
'minute_product' => system_status_module_value('77'),
|
|
]);
|
|
|
|
expect($result['status'])->toBe('ok');
|
|
expect($service->selfserveMinuteProductCheckedId)->toBe(77);
|
|
});
|
|
|
|
it('reports invalid selfserve minute configuration before touching the schema', function (): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
|
|
$result = $service->probeSelfserveModulePublic([
|
|
'machine_wash_minutes_included' => system_status_module_value('abc'),
|
|
'minute_product' => system_status_module_value('77'),
|
|
]);
|
|
|
|
expect($result['status'])->toBe('down');
|
|
expect($result['status_reason'])->toContain('minutes configuration is invalid');
|
|
expect($result['status_reason_key'])->toBe('selfserve_minutes_invalid');
|
|
expect($service->selfserveMinuteProductCheckedId)->toBeNull();
|
|
});
|
|
|
|
it('reports missing selfserve minute products', function (): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
$service->selfserveMinuteProductExistsValue = false;
|
|
|
|
$result = $service->probeSelfserveModulePublic([
|
|
'machine_wash_minutes_included' => system_status_module_value('15'),
|
|
'minute_product' => system_status_module_value('88'),
|
|
]);
|
|
|
|
expect($result['status'])->toBe('down');
|
|
expect($result['status_reason'])->toContain('does not exist');
|
|
expect($result['status_reason_key'])->toBe('selfserve_minute_product_missing');
|
|
expect($result['status_reason_params'])->toBe(['productId' => 88]);
|
|
expect($service->selfserveMinuteProductCheckedId)->toBe(88);
|
|
});
|
|
|
|
it('surfaces selfserve bootstrap failures', function (): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
$service->selfserveBootstrapShouldFail = true;
|
|
$service->selfserveBootstrapFailureMessage = 'Migration failed.';
|
|
|
|
$result = $service->probeSelfserveModulePublic([
|
|
'machine_wash_minutes_included' => system_status_module_value('15'),
|
|
'minute_product' => system_status_module_value('88'),
|
|
]);
|
|
|
|
expect($result['status'])->toBe('down');
|
|
expect($result['status_reason'])->toContain('Migration failed');
|
|
expect($result['status_reason_key'])->toBe('selfserve_probe_failed');
|
|
});
|
|
|
|
it('adds localization metadata for disabled and missing-config modules', function (): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
$service->moduleConfigRowsOverride = [
|
|
'reCAPTCHA' => [
|
|
'enabled' => system_status_module_value(false, 'bool'),
|
|
],
|
|
'Email' => [
|
|
'enabled' => system_status_module_value(true, 'bool'),
|
|
'mailersend_enabled' => system_status_module_value(false, 'bool'),
|
|
],
|
|
];
|
|
|
|
$warnings = [];
|
|
$modules = $service->collectModulesPublic(false, $warnings);
|
|
$moduleMap = [];
|
|
foreach ($modules as $module) {
|
|
$moduleMap[$module['key']] = $module;
|
|
}
|
|
|
|
expect($moduleMap['reCAPTCHA']['status'])->toBe('disabled');
|
|
expect($moduleMap['reCAPTCHA']['status_reason_key'])->toBe('module_disabled');
|
|
expect($moduleMap['email']['status'])->toBe('not_configured');
|
|
expect($moduleMap['email']['status_reason_key'])->toBe('email_delivery_not_implemented');
|
|
expect($moduleMap['email']['status_reason_params'])->toBe([]);
|
|
expect($warnings)->toBeEmpty();
|
|
});
|
|
|
|
it('marks newly supported modules as probe backed and leaves only truly unsupported modules as configuration only', function (): void {
|
|
$service = new SuperuserSystemStatusServiceProbeDouble();
|
|
$GLOBALS['ECONOMIC_API'] = [
|
|
'app_secret_token' => 'economic-secret',
|
|
'app_access_grant' => 'economic-grant',
|
|
];
|
|
|
|
$service->moduleConfigRowsOverride = [
|
|
'economic' => [
|
|
'invoiceLayoutNumber' => system_status_module_value('1'),
|
|
'paymentTermsNumber' => system_status_module_value('2'),
|
|
'adminFeeMonthly' => system_status_module_value('3'),
|
|
'adminFeeOrder' => system_status_module_value('4'),
|
|
'feeProductId' => system_status_module_value('5'),
|
|
],
|
|
'Email' => [
|
|
'enabled' => system_status_module_value(true, 'bool'),
|
|
'mailersend_enabled' => system_status_module_value(true, 'bool'),
|
|
'mailersend_api_key' => system_status_module_value('mailersend-key'),
|
|
'smtp_from' => system_status_module_value('from@example.com'),
|
|
'smtp_from_name' => system_status_module_value('Truck Wash'),
|
|
'smtp_reply_to' => system_status_module_value('reply@example.com'),
|
|
'smtp_reply_to_name' => system_status_module_value('Reply'),
|
|
],
|
|
'Backups' => [
|
|
'enabled' => system_status_module_value(true, 'bool'),
|
|
],
|
|
'motorapi' => [
|
|
'enabled' => system_status_module_value(true, 'bool'),
|
|
'secret_key' => system_status_module_value('motorapi-key'),
|
|
],
|
|
'fxratesapi' => [
|
|
'enabled' => system_status_module_value(true, 'bool'),
|
|
'secret_key' => system_status_module_value('fxrates-key'),
|
|
],
|
|
'weatherapi' => [
|
|
'enabled' => system_status_module_value(true, 'bool'),
|
|
'secret_key' => system_status_module_value('weather-key'),
|
|
],
|
|
'workfeed' => [
|
|
'enabled' => system_status_module_value(true, 'bool'),
|
|
'api_url' => system_status_module_value('https://api.workfeed.test'),
|
|
'api_key' => system_status_module_value('workfeed-token'),
|
|
'CompanyID' => system_status_module_value('company-123'),
|
|
],
|
|
'GatewayAPI' => [
|
|
'enabled' => system_status_module_value(true, 'bool'),
|
|
'api_secret' => system_status_module_value('gateway-secret'),
|
|
'api_token' => system_status_module_value('gateway-token'),
|
|
'sender' => system_status_module_value('TruckWash'),
|
|
],
|
|
'xlvask' => [
|
|
'enabled' => system_status_module_value(true, 'bool'),
|
|
'username' => system_status_module_value('xl-user'),
|
|
'password' => system_status_module_value('xl-pass'),
|
|
],
|
|
'limble' => [
|
|
'enabled' => system_status_module_value(true, 'bool'),
|
|
'client_id' => system_status_module_value('limble-id'),
|
|
'client_secret' => system_status_module_value('limble-secret'),
|
|
],
|
|
'licenseplaterecognizer' => [
|
|
'enabled' => system_status_module_value(true, 'bool'),
|
|
'api_key' => system_status_module_value('lpr-key'),
|
|
],
|
|
'shelly' => [
|
|
'enabled' => system_status_module_value(true, 'bool'),
|
|
'server_url' => system_status_module_value('https://shelly.example'),
|
|
'secret_key' => system_status_module_value('shelly-secret'),
|
|
],
|
|
'selfserve' => [
|
|
'enabled' => system_status_module_value(true, 'bool'),
|
|
'machine_wash_minutes_included' => system_status_module_value('15'),
|
|
'minute_product' => system_status_module_value('88'),
|
|
],
|
|
'bird' => [
|
|
'enabled' => system_status_module_value(true, 'bool'),
|
|
'server_url' => system_status_module_value('https://api.bird.com'),
|
|
'api_key' => system_status_module_value('bird-key'),
|
|
'channelId' => system_status_module_value('channel-123'),
|
|
'workplaceId' => system_status_module_value('workspace-456'),
|
|
],
|
|
'ocrSpace' => [
|
|
'enabled' => system_status_module_value(true, 'bool'),
|
|
'api_key' => system_status_module_value('ocr-key'),
|
|
],
|
|
'virkdata' => [
|
|
'enabled' => system_status_module_value(true, 'bool'),
|
|
'secret_key' => system_status_module_value('virk-key'),
|
|
],
|
|
];
|
|
|
|
$warnings = [];
|
|
$modules = $service->collectModulesPublic(false, $warnings);
|
|
$moduleMap = [];
|
|
foreach ($modules as $module) {
|
|
$moduleMap[$module['key']] = $module;
|
|
}
|
|
|
|
expect($moduleMap['email']['probe_supported'])->toBeTrue();
|
|
expect($moduleMap['email']['status'])->toBe('ok');
|
|
expect($moduleMap['email']['status_reason_key'])->toBe('http_ok');
|
|
expect($moduleMap['backups']['probe_supported'])->toBeTrue();
|
|
expect($moduleMap['backups']['status'])->toBe('ok');
|
|
expect($moduleMap['backups']['status_reason_key'])->toBe('backup_connectivity_confirmed');
|
|
expect($moduleMap['motorapi']['probe_supported'])->toBeTrue();
|
|
expect($moduleMap['motorapi']['status'])->toBe('ok');
|
|
expect($moduleMap['bird']['probe_supported'])->toBeTrue();
|
|
expect($moduleMap['bird']['status'])->toBe('ok');
|
|
expect($moduleMap['shelly']['probe_supported'])->toBeTrue();
|
|
expect($moduleMap['shelly']['status'])->toBe('configured');
|
|
expect($moduleMap['shelly']['status_reason_key'])->toBe('shelly_no_device_id');
|
|
expect($moduleMap['selfserve']['probe_supported'])->toBeTrue();
|
|
expect($moduleMap['selfserve']['status'])->toBe('ok');
|
|
expect($moduleMap['selfserve']['status_reason_key'])->toBe('selfserve_configuration_confirmed');
|
|
expect($moduleMap['ocrspace']['probe_supported'])->toBeFalse();
|
|
expect($moduleMap['ocrspace']['status'])->toBe('configured');
|
|
expect($moduleMap['ocrspace']['status_reason_key'])->toBe('safe_probe_unavailable');
|
|
expect($moduleMap['virkdata']['probe_supported'])->toBeFalse();
|
|
expect($moduleMap['virkdata']['status'])->toBe('configured');
|
|
expect($moduleMap['virkdata']['status_reason_key'])->toBe('safe_probe_unavailable');
|
|
expect($warnings)->toHaveCount(1);
|
|
expect($warnings[0]['key'])->toBe('modules_without_probes');
|
|
expect($warnings[0]['params']['module_keys'])->toBe(['ocrspace', 'virkdata']);
|
|
expect($warnings[0]['message'])->toContain('ocrspace, virkdata');
|
|
});
|