Files
FOSSBilling/tests/Modules/System/AdminTest.php
T
Adam Daley dd3dcbfd52 Enhance type safety, authentication checks, and testing framework (#3701)
* Improve type safety, auth checks and PHPStan fixes (#3250)

* Improve type safety, auth checks and PHPStan fixes

* Remove @phpstan-ignore annotations

* Improve type safety, auth checks and PHPStan fixes

* Remove @phpstan-ignore annotations

* Remove redundant @phpstan-ignore comment

* Improve code robustness and reliability (#3248)

* Harden code against null/undefined values

* Update dependency lockfiles

* Sanitize ticket input and use IDs in email list

* Restructure tests and add test utilities

* Move E2E tests to `Library\E2E` namespace

* Update `php-build` action ref in workflows

* Switch tests to Pest and add test scaffolding

* Refactor E2E tests to Pest-style tests

m

Skip E2E tests when env not configured

* Refactor Unit tests and update test config

Add test datasets and refine test helpers

* Add DEBUG guard, fix API JSON decode, update tests

* Refactor tests: add types and Mockery expectations

* Adjust tests, phpstan config, and composer script

* Instantiate test APIs locally, remove beforeEach

* Refactor runtime checks, types & logging

* Apply PHP Code Styling Changes

m

* Add PHPStan ignores, test renames & small fixes

* Enable Pest tests in CI workflow

* Remove legacy tests; add new unit tests/config

* Avoid undefined index in fingerprint comparison

* Ignore properties absent in both fingerprints

* Remove public_ticket_create API method

* Add Fingerprint unit tests

* Remove em dash from fingerprint test name

* Switch to Pest; assorted bug fixes & refactors

* Prefer getDi() and guard DEBUG define

* Refactor tests: DBAL, Antispam rename, helpers

* Refactor email tests to use Doctrine entities

* Update tests, use DBAL mocks & improve Formbuilder admin

* Validate plan_id and update unit tests

* Replace error_log with DI logger and update tests

* Expect db->store in Invoice Service test

* Use DI logger instead of error_log; fix tests

* Fix typo in Hook Service test

* Fix Staff test spelling fixture

* Complete Pest test migration

* Fix Product test spelling fixtures

* Fix Product test spelling fixtures

* Run live tests with Pest and load helpers

* Migrate tests to ApiClient; remove legacy APIHelper

* Fix Email test spelling

* Remove unused PHPUnit bridge

* Organize Cypress end-to-end tests

* Document Pest test directory root

* Remove noisy Pest comments
2026-06-02 07:43:01 +01:00

203 lines
7.0 KiB
PHP

<?php
declare(strict_types=1);
use Tests\Helpers\ApiClient;
const SYSTEM_MAX_RETRY_ATTEMPTS = 10;
const SYSTEM_RETRY_DELAY_MICROSECONDS = 200000;
const SYSTEM_DEFAULT_INTERFACE = '0';
function systemRetryUntil(callable $operation, int $maxAttempts = SYSTEM_MAX_RETRY_ATTEMPTS, int $delayMicroseconds = SYSTEM_RETRY_DELAY_MICROSECONDS): bool
{
for ($attempt = 0; $attempt < $maxAttempts; ++$attempt) {
if ($operation()) {
return true;
}
if ($attempt < $maxAttempts - 1) {
usleep($delayMicroseconds);
}
}
return false;
}
function systemIsIpLookupAvailable(): bool
{
$context = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => 2,
],
]);
$lookupErrorMessage = null;
$previousErrorHandler = set_error_handler(
static function (
int $severity,
string $message,
string $file = '',
int $line = 0,
) use (&$lookupErrorMessage, &$previousErrorHandler): bool {
if ($severity === E_WARNING) {
$lookupErrorMessage = $message;
return true;
}
if (is_callable($previousErrorHandler)) {
return (bool) $previousErrorHandler($severity, $message, $file, $line);
}
return false;
}
);
try {
$response = file_get_contents('https://api.ipify.org', false, $context);
} finally {
restore_error_handler();
}
if ($response === false && $lookupErrorMessage !== null) {
$singleLineLookupErrorMessage = str_replace(["\r", "\n"], ' ', $lookupErrorMessage);
error_log('IP lookup availability check failed: ' . $singleLineLookupErrorMessage);
}
return $response !== false;
}
function systemResetInterfaceConfiguration(): void
{
$resetResult = ApiClient::request(
'admin/system/set_interface_ip',
['custom_interface' => '', 'interface' => SYSTEM_DEFAULT_INTERFACE]
);
expect($resetResult->wasSuccessful())->toBeTrue();
}
test('clears cache', function (): void {
$beforeFirst = ApiClient::request('admin/system/error_reporting_enabled');
expect($beforeFirst->wasSuccessful())->toBeTrue()
->and($beforeFirst->getResult())->toBeBool();
$beforeSecond = ApiClient::request('admin/system/error_reporting_enabled');
expect($beforeSecond->wasSuccessful())->toBeTrue()
->and($beforeSecond->getResult())->toBeBool()
->and($beforeSecond->getResult())->toBe($beforeFirst->getResult());
$result = ApiClient::request('admin/system/clear_cache');
expect($result->wasSuccessful())->toBeTrue()
->and($result->getResult())->toBeBool();
$after = ApiClient::request('admin/system/error_reporting_enabled');
expect($after->wasSuccessful())->toBeTrue()
->and($after->getResult())->toBeBool()
->and($after->getResult())->toBe($beforeSecond->getResult());
});
test('toggles error reporting', function (): void {
$beforeResult = ApiClient::request('admin/system/error_reporting_enabled');
expect($beforeResult->wasSuccessful())->toBeTrue();
$before = $beforeResult->getResult();
expect($before)->toBeBool();
try {
$result = ApiClient::request('admin/system/toggle_error_reporting');
expect($result->wasSuccessful())->toBeTrue()
->and($result->getResult())->toBeTrue();
$afterResponse = ApiClient::request('admin/system/error_reporting_enabled');
expect($afterResponse->wasSuccessful())->toBeTrue();
$after = $afterResponse->getResult();
expect($after)->toBeBool()
->not->toBe($before);
} finally {
$currentResponse = ApiClient::request('admin/system/error_reporting_enabled');
expect($currentResponse->wasSuccessful())->toBeTrue();
$current = $currentResponse->getResult();
expect($current)->toBeBool();
if ($current !== $before) {
$restoreResult = ApiClient::request('admin/system/toggle_error_reporting');
expect($restoreResult->wasSuccessful())->toBeTrue()
->and($restoreResult->getResult())->toBeTrue();
}
}
});
test('gets and sets network interfaces', function (): void {
$result = ApiClient::request('admin/system/get_interface_ips');
expect($result->wasSuccessful())->toBeTrue()
->and($result->getResult())->toBeArray();
foreach ($result->getResult() as $ip) {
expect((bool) filter_var($ip, FILTER_VALIDATE_IP))->toBeTrue();
}
if (!systemIsIpLookupAvailable()) {
$this->markTestSkipped('Integration test requires ipify.org to be available.');
}
foreach ($result->getResult() as $ip) {
$testResult = ApiClient::request('admin/system/set_interface_ip', ['interface' => $ip]);
expect($testResult->wasSuccessful())->toBeTrue();
$isReady = systemRetryUntil(
static function (): bool {
$envResult = ApiClient::request('admin/system/env', ['ip' => true]);
return $envResult->wasSuccessful() && (bool) filter_var($envResult->getResult(), FILTER_VALIDATE_IP);
}
);
expect($isReady)->toBeTrue();
}
$cleanupResult = ApiClient::request('admin/system/set_interface_ip', ['interface' => SYSTEM_DEFAULT_INTERFACE]);
expect($cleanupResult->wasSuccessful())->toBeTrue();
});
test('rejects invalid interface', function (): void {
$result = ApiClient::request('admin/system/set_interface_ip', ['interface' => '12345']);
expect($result->wasSuccessful())->toBeFalse();
});
test('rejects invalid custom interface', function (): void {
$result = ApiClient::request('admin/system/set_interface_ip', ['custom_interface' => '!@#$%']);
expect($result->wasSuccessful())->toBeFalse();
});
test('rejects malicious interface values', function (string $payload): void {
$result = ApiClient::request('admin/system/set_interface_ip', ['interface' => $payload]);
expect($result->wasSuccessful())->toBeFalse();
})->with([
'quote-escape' => ["x\"; echo 'pwned'; //"],
'pipe-command' => ['eth0|id'],
'backticks' => ['eth0`id`'],
'sub-shell' => ['eth0$(id)'],
'and-command' => ['eth0 && whoami'],
'redirect' => ['eth0 > /tmp/pwned'],
'path-traversal-unix' => ['../etc/passwd'],
'path-traversal-windows' => ['..\\..\\windows\\system32\\drivers\\etc\\hosts'],
'null-byte' => ["eth0\0evil"],
'long-string' => [str_repeat('a', 1024)],
'newline-injection' => ["eth0\nwhoami"],
'leading-trailing-space' => [' eth0 '],
]);
test('rejects malicious custom interface', function (): void {
$result = ApiClient::request('admin/system/set_interface_ip', ['custom_interface' => "x\"; passthru('id'); //"]);
expect($result->wasSuccessful())->toBeFalse();
});
test('accepts valid custom interface hostname', function (): void {
try {
$result = ApiClient::request('admin/system/set_interface_ip', ['custom_interface' => 'eth0']);
expect($result->wasSuccessful())->toBeTrue();
} finally {
systemResetInterfaceConfiguration();
}
});