mirror of
https://github.com/FOSSBilling/FOSSBilling.git
synced 2026-11-09 13:36:07 +09:00
* 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
203 lines
7.0 KiB
PHP
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();
|
|
}
|
|
});
|