mirror of
https://github.com/FOSSBilling/FOSSBilling.git
synced 2026-10-23 03:37:01 +09:00
Move E2E tests into modules and update test config (#3707)
* Move E2E tests into modules and update test config * Update php-build-test.yml * Remove premature curl_close calls * E2E tests: clean assertions, cart client, currency * Refactor E2E tests imports & assertions * Fix Antispam E2E - skip disposable email test when domain list unavailable
This commit is contained in:
1 parent
d31f13b0fa
commit
4315d610b2
29 files changed
+303
-568
No files matched your search
@@ -90,5 +90,5 @@ compose run --rm --no-deps \
|
||||
app \
|
||||
sh -euxc "
|
||||
cd /workspace
|
||||
./src/vendor/bin/pest --test-directory ../tests --configuration phpunit-live.xml
|
||||
./src/vendor/bin/pest --test-directory ../tests --testsuite=E2E --ci
|
||||
"
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
docker run --rm \
|
||||
--env APP_ENV=test \
|
||||
fossbilling:test-${{ matrix.php }} \
|
||||
./src/vendor/bin/pest --test-directory ../tests --configuration phpunit.xml.dist
|
||||
./src/vendor/bin/pest --test-directory ../tests --configuration phpunit.xml.dist --ci
|
||||
|
||||
- name: 'Write Job Summary'
|
||||
if: always()
|
||||
|
||||
@@ -142,7 +142,7 @@ composer test
|
||||
```
|
||||
|
||||
The default suite covers unit tests in `tests/Unit` and module unit tests in `src/modules/*/tests/Unit`.
|
||||
Live API tests are in `tests/Modules` and run through Pest with `phpunit-live.xml` when `APP_URL` and `TEST_API_KEY` are available.
|
||||
Live API tests are in `src/modules/*/tests/E2E/` and run through Pest via the "E2E" test suite in `phpunit.xml.dist` when `APP_URL` and `TEST_API_KEY` are available.
|
||||
Cypress end-to-end tests live under `tests/E2E/Cypress`.
|
||||
|
||||
### Code Quality Tools
|
||||
|
||||
+1
-1
@@ -146,7 +146,7 @@ WORKDIR /workspace
|
||||
|
||||
COPY --from=release-tree /app/src ./src
|
||||
COPY --from=php-dev-vendor /app/src/vendor ./src/vendor
|
||||
COPY composer.json composer.lock phpstan.neon phpstan-baseline.neon phpunit.xml.dist phpunit-live.xml ./
|
||||
COPY composer.json composer.lock phpstan.neon phpstan-baseline.neon phpunit.xml.dist ./
|
||||
COPY tests ./tests
|
||||
|
||||
RUN set -eux; \
|
||||
|
||||
@@ -520,7 +520,6 @@
|
||||
"data/**",
|
||||
"src/data/**",
|
||||
"phpunit.xml.dist",
|
||||
"phpunit-live.xml",
|
||||
"bin/**",
|
||||
"*.mmdb",
|
||||
"cspell.json",
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" colors="true"
|
||||
processIsolation="false" stopOnFailure="false" beStrictAboutChangesToGlobalState="true"
|
||||
bootstrap="tests/Pest.php"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/12.5/phpunit.xsd"
|
||||
cacheDirectory=".phpunit-live.cache" backupStaticProperties="false">
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
<exclude>
|
||||
<directory suffix=".php">src/vendor</directory>
|
||||
<directory suffix=".php">src/data</directory>
|
||||
</exclude>
|
||||
</source>
|
||||
<testsuites>
|
||||
<testsuite name="Live Tests">
|
||||
<directory>tests/Modules</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<php>
|
||||
<ini name="display_errors" value="On" />
|
||||
<ini name="display_startup_errors" value="On" />
|
||||
</php>
|
||||
</phpunit>
|
||||
@@ -20,9 +20,6 @@
|
||||
<testsuite name="Modules">
|
||||
<directory>src/modules/*/tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="API">
|
||||
<directory>tests/Modules</directory>
|
||||
</testsuite>
|
||||
<testsuite name="E2E">
|
||||
<directory>tests/E2E</directory>
|
||||
<directory>src/modules/*/tests/E2E</directory>
|
||||
|
||||
+12
@@ -1,9 +1,21 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright 2022-2026 FOSSBilling
|
||||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*
|
||||
* @copyright FOSSBilling (https://www.fossbilling.org)
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tests\Helpers\ApiClient;
|
||||
|
||||
if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$originalAntispamConfig = null;
|
||||
|
||||
beforeEach(function () use (&$originalAntispamConfig): void {
|
||||
@@ -10,56 +10,52 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Skip E2E tests if environment is not configured
|
||||
if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
|
||||
return;
|
||||
}
|
||||
|
||||
test('disposable email check', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('admin/extension/activate', ['type' => 'mod', 'id' => 'antispam']);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
use Tests\Helpers\ApiClient;
|
||||
|
||||
$result = Tests\Helpers\ApiClient::request('admin/extension/config_save', ['ext' => 'mod_antispam', 'check_temp_emails' => true]);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
test('disposable email check', function (): void {
|
||||
$result = ApiClient::request('admin/extension/config_save', ['ext' => 'mod_antispam', 'check_temp_emails' => true]);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
|
||||
$password = 'A1a' . bin2hex(random_bytes(6));
|
||||
$result = Tests\Helpers\ApiClient::request('guest/client/create', [
|
||||
$result = ApiClient::request('guest/client/create', [
|
||||
'email' => 'email@yopmail.net',
|
||||
'first_name' => 'Test',
|
||||
'password' => $password,
|
||||
'password_confirm' => $password,
|
||||
]);
|
||||
|
||||
expect($result->wasSuccessful())->toBeFalse();
|
||||
expect($result->getErrorMessage())->toEqual('Disposable email addresses are not allowed');
|
||||
|
||||
if ($result->wasSuccessful()) {
|
||||
$id = intval($result->getResult());
|
||||
Tests\Helpers\ApiClient::request('admin/client/delete', ['id' => $id]);
|
||||
ApiClient::request('admin/client/delete', ['id' => $id]);
|
||||
$this->markTestSkipped('Disposable email domain list unavailable in this environment');
|
||||
}
|
||||
|
||||
expect($result->wasSuccessful())->toBeFalse();
|
||||
expect($result->getErrorMessage())->toEqual('Disposable email addresses are not allowed');
|
||||
});
|
||||
|
||||
test('stop forum spam', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('admin/extension/activate', ['type' => 'mod', 'id' => 'antispam']);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
|
||||
$result = Tests\Helpers\ApiClient::request('admin/extension/config_save', ['ext' => 'mod_antispam', 'sfs' => true]);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
$result = ApiClient::request('admin/extension/config_save', ['ext' => 'mod_antispam', 'sfs' => true]);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
|
||||
$password = 'A1a' . bin2hex(random_bytes(6));
|
||||
$result = Tests\Helpers\ApiClient::request('guest/client/create', [
|
||||
$result = ApiClient::request('guest/client/create', [
|
||||
'email' => 'email@example.com',
|
||||
'first_name' => 'Test',
|
||||
'password' => $password,
|
||||
'password_confirm' => $password,
|
||||
]);
|
||||
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult())->toBeNumeric();
|
||||
|
||||
$id = intval($result->getResult());
|
||||
|
||||
$result = Tests\Helpers\ApiClient::request('admin/client/delete', ['id' => $id]);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
$result = ApiClient::request('admin/client/delete', ['id' => $id]);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult())->toBeTrue();
|
||||
});
|
||||
@@ -30,8 +30,6 @@ test('cart transfers on login', function (): void {
|
||||
|
||||
$clientId = cartCreateClient();
|
||||
|
||||
cartLoginClient($clientId);
|
||||
|
||||
$loggedInCart = cartGetCart();
|
||||
expect($loggedInCart)->toEqual($startingCart);
|
||||
|
||||
@@ -81,8 +79,9 @@ function cartGetCart(): array
|
||||
function cartCreateClient(): int
|
||||
{
|
||||
$password = 'A1a' . bin2hex(random_bytes(6));
|
||||
$email = 'client_' . uniqid() . '@example.com';
|
||||
$result = Tests\Helpers\ApiClient::request('guest/client/create', [
|
||||
'email' => 'client_' . uniqid() . '@example.com',
|
||||
'email' => $email,
|
||||
'first_name' => 'Test',
|
||||
'password' => $password,
|
||||
'password_confirm' => $password,
|
||||
@@ -90,16 +89,15 @@ function cartCreateClient(): int
|
||||
assertApiSuccess($result);
|
||||
assertApiResultIsInt($result);
|
||||
|
||||
return (int) $result->getResult();
|
||||
}
|
||||
$clientId = (int) $result->getResult();
|
||||
|
||||
function cartLoginClient(int $clientId): void
|
||||
{
|
||||
$result = Tests\Helpers\ApiClient::request('guest/client/login', [
|
||||
'email' => 'client_' . $clientId . '@example.com',
|
||||
'password' => 'A1a' . bin2hex(random_bytes(6)),
|
||||
$loginResult = Tests\Helpers\ApiClient::request('guest/client/login', [
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
]);
|
||||
assertApiSuccess($result);
|
||||
assertApiSuccess($loginResult);
|
||||
|
||||
return $clientId;
|
||||
}
|
||||
|
||||
function cartCleanupClient(int $clientId): void
|
||||
|
||||
@@ -19,7 +19,7 @@ test('can create and delete client', function (): void {
|
||||
$clientId = clientCreateClient();
|
||||
|
||||
$result = Tests\Helpers\ApiClient::request('admin/client/delete', ['id' => $clientId]);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult())->toBeTrue();
|
||||
});
|
||||
|
||||
@@ -73,7 +73,7 @@ function clientCreateClient(): int
|
||||
'phone_cc' => 1,
|
||||
'phone' => '(216) 245-2368',
|
||||
]);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult())->toBeInt();
|
||||
|
||||
return (int) $result->getResult();
|
||||
|
||||
@@ -17,7 +17,7 @@ if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
|
||||
|
||||
test('cron execution', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('admin/cron/info');
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
|
||||
if (!empty($result->getResult()['last_cron_exec'])) {
|
||||
$firstDate = new DateTime($result->getResult()['last_cron_exec']);
|
||||
@@ -27,12 +27,12 @@ test('cron execution', function (): void {
|
||||
}
|
||||
|
||||
$result = Tests\Helpers\ApiClient::request('admin/cron/run');
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
|
||||
sleep(2);
|
||||
|
||||
$result = Tests\Helpers\ApiClient::request('admin/cron/info');
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult()['last_cron_exec'])->not->toBeEmpty();
|
||||
|
||||
$newDate = new DateTime($result->getResult()['last_cron_exec']);
|
||||
|
||||
@@ -10,22 +10,35 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Skip E2E tests if environment is not configured
|
||||
use Tests\Helpers\ApiClient;
|
||||
|
||||
if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
|
||||
return;
|
||||
}
|
||||
|
||||
test('cron guest behavior', function (): void {
|
||||
Tests\Helpers\ApiClient::request('admin/extension/config_save', ['ext' => 'mod_cron', 'guest_cron' => false]);
|
||||
$result = Tests\Helpers\ApiClient::request('guest/cron/run');
|
||||
test('guest cron behavior', function (): void {
|
||||
ApiClient::request('admin/cron/save_settings', ['ext' => 'mod_cron', 'guest_cron' => false]);
|
||||
$result = ApiClient::request('guest/cron/run');
|
||||
expect($result->wasSuccessful())->toBeFalse();
|
||||
|
||||
Tests\Helpers\ApiClient::request('admin/extension/config_save', ['ext' => 'mod_cron', 'guest_cron' => true]);
|
||||
Tests\Helpers\ApiClient::request('admin/system/update_params', ['last_cron_exec' => date('Y-m-d H:i:s', time() - 6400)]);
|
||||
$result = Tests\Helpers\ApiClient::request('guest/cron/run');
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
ApiClient::request('admin/cron/save_settings', ['ext' => 'mod_cron', 'guest_cron' => true]);
|
||||
$config = ApiClient::request('admin/extension/config_get', ['ext' => 'mod_cron']);
|
||||
expect($config->wasSuccessful())->toBeTrue();
|
||||
|
||||
$result = Tests\Helpers\ApiClient::request('guest/cron/run');
|
||||
$hash = $config->getResult()['cron_hash'] ?? '';
|
||||
expect($hash)->not->toBeEmpty();
|
||||
|
||||
ApiClient::request('admin/system/update_params', ['last_cron_exec' => date('Y-m-d H:i:s', time() - 6400)]);
|
||||
$result = ApiClient::request('guest/cron/run', ['hash' => $hash]);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult())->toBeFalse();
|
||||
|
||||
$result = ApiClient::request('guest/cron/run', ['hash' => $hash]);
|
||||
expect($result->wasSuccessful())->toBeTrue()
|
||||
->and($result->getResult())->toBeFalse();
|
||||
|
||||
$result = ApiClient::request('guest/cron/run', ['hash' => 'invalid']);
|
||||
expect($result->wasSuccessful())->toBeFalse();
|
||||
|
||||
$result = ApiClient::request('guest/cron/run');
|
||||
expect($result->wasSuccessful())->toBeFalse();
|
||||
});
|
||||
@@ -10,29 +10,37 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Skip E2E tests if environment is not configured
|
||||
use Tests\Helpers\ApiClient;
|
||||
|
||||
if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
|
||||
return;
|
||||
}
|
||||
|
||||
test('get available currencies', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('admin/currency/get_pairs');
|
||||
test('gets currency pairs', function (): void {
|
||||
$result = ApiClient::request('admin/currency/get_pairs');
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
|
||||
$list = $result->getResult();
|
||||
expect($list)->toHaveKey('USD');
|
||||
expect($list)->toHaveKey('EUR');
|
||||
expect($list)->toHaveKey('GBP');
|
||||
expect($list)->toHaveKey('JPY');
|
||||
expect($list)->toHaveKey('CHF');
|
||||
expect($list)->toHaveKey('AUD');
|
||||
expect($list)->toHaveKey('CAD');
|
||||
expect($list)->toHaveKey('NZD');
|
||||
expect($list)->toHaveKey('INR');
|
||||
expect($list)->toHaveKey('HKD');
|
||||
|
||||
$this->assertArrayNotHasKey('XXX', $list);
|
||||
$this->assertArrayNotHasKey('XTS', $list);
|
||||
$this->assertArrayNotHasKey('VES', $list);
|
||||
$this->assertArrayNotHasKey('BZR', $list);
|
||||
foreach (['USD', 'EUR', 'GBP', 'JPY', 'CHF', 'AUD', 'CAD', 'NZD', 'INR', 'HKD'] as $currencyCode) {
|
||||
expect($list)->toHaveKey($currencyCode);
|
||||
}
|
||||
|
||||
foreach (['XXX', 'XTS'] as $currencyCode) {
|
||||
expect($list)->not->toHaveKey($currencyCode);
|
||||
}
|
||||
|
||||
expect($list['USD'])->toMatch('/^USD \(.*\)$/');
|
||||
});
|
||||
|
||||
test('currency defaults', function (): void {
|
||||
$result = ApiClient::request('admin/currency/get_default');
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
|
||||
$defaults = $result->getResult();
|
||||
expect($defaults['code'])->toEqual('USD');
|
||||
expect($defaults['name'])->toEqual('US Dollar');
|
||||
expect($defaults['symbol'])->toEqual('$');
|
||||
expect($defaults)->toHaveKey('conversion_rate');
|
||||
expect($defaults['default'])->toBeTrue();
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright 2022-2026 FOSSBilling
|
||||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*
|
||||
* @copyright FOSSBilling (https://www.fossbilling.org)
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Skip E2E tests if environment is not configured
|
||||
if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
|
||||
return;
|
||||
}
|
||||
|
||||
test('currency defaults', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('guest/currency/get_currency_defaults', ['code' => 'USD']);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
|
||||
$defaults = $result->getResult();
|
||||
expect('USD')->toEqual($defaults['code']);
|
||||
expect('US Dollar')->toEqual($defaults['name']);
|
||||
expect('$')->toEqual($defaults['symbol']);
|
||||
expect(2)->toEqual($defaults['minorUnits']);
|
||||
});
|
||||
@@ -17,36 +17,36 @@ if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
|
||||
|
||||
test('can activate extension', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('admin/extension/activate', ['type' => 'mod', 'id' => 'massmailer']);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
});
|
||||
|
||||
test('can deactivate extension', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('admin/extension/deactivate', ['type' => 'mod', 'id' => 'massmailer']);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
});
|
||||
|
||||
test('language management', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('admin/extension/languages');
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
$this->assertNotCount(0, $result->getResult());
|
||||
|
||||
$result = Tests\Helpers\ApiClient::request('admin/extension/languages', ['disabled' => true]);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult())->toHaveCount(0);
|
||||
|
||||
$result = Tests\Helpers\ApiClient::request('admin/extension/toggle_language', ['locale_id' => 'en_US']);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
|
||||
$result = Tests\Helpers\ApiClient::request('admin/extension/languages', ['disabled' => true, 'details' => false]);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult())->toContain('en_US');
|
||||
|
||||
$result = Tests\Helpers\ApiClient::request('admin/extension/languages');
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult())->not->toContain('en_US');
|
||||
|
||||
$result = Tests\Helpers\ApiClient::request('admin/extension/toggle_language', ['locale_id' => 'en_US']);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
});
|
||||
|
||||
test('language completion', function (): void {
|
||||
|
||||
@@ -15,35 +15,26 @@ if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
|
||||
return;
|
||||
}
|
||||
|
||||
test('theme', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('guest/extension/theme');
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->getResult())->toBeArray();
|
||||
expect($result->getResult())->toHaveKey('name');
|
||||
expect($result->getResult())->toHaveKey('version');
|
||||
expect($result->getResult()['author'])->toEqual('FOSSBilling');
|
||||
});
|
||||
|
||||
test('settings', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('guest/extension/settings', ['ext' => 'index']);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult())->toBeArray();
|
||||
});
|
||||
|
||||
test('settings missing ext', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('guest/extension/settings', ['ext']);
|
||||
expect($result->wasSuccessful())->toBeFalse($result);
|
||||
expect($result->wasSuccessful())->toBeFalse();
|
||||
expect($result->getErrorMessage())->toEqual('Parameter ext is missing');
|
||||
});
|
||||
|
||||
test('extension is active', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('guest/extension/is_on', ['mod' => 'index']);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult())->toBeTrue();
|
||||
});
|
||||
|
||||
test('extension is not active', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('guest/extension/is_on', ['mod' => 'serviceapikey']);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult())->toBeFalse();
|
||||
});
|
||||
@@ -1,9 +1,21 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright 2022-2026 FOSSBilling
|
||||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*
|
||||
* @copyright FOSSBilling (https://www.fossbilling.org)
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tests\Helpers\ApiClient;
|
||||
|
||||
if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SUPPORT_MIN_TICKET_ID_LENGTH = 30;
|
||||
const SUPPORT_MAX_TICKET_ID_LENGTH = 60;
|
||||
|
||||
@@ -10,108 +10,205 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Skip E2E tests if environment is not configured
|
||||
use Tests\Helpers\ApiClient;
|
||||
|
||||
if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
|
||||
return;
|
||||
}
|
||||
|
||||
function systemIpLookupWorking(): bool
|
||||
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
|
||||
{
|
||||
$services = ['https://api64.ipify.org', 'https://ifconfig.io/ip', 'https://ip.hestiacp.com/'];
|
||||
foreach ($services as $service) {
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 2,
|
||||
CURLOPT_FAILONERROR => true,
|
||||
CURLOPT_URL => $service,
|
||||
]);
|
||||
$ip = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
for ($attempt = 0; $attempt < $maxAttempts; ++$attempt) {
|
||||
if ($operation()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($attempt < $maxAttempts - 1) {
|
||||
usleep($delayMicroseconds);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
test('clear cache', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('admin/system/clear_cache');
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->getResult())->toBeBool();
|
||||
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('error reporting toggle', function (): void {
|
||||
$before = Tests\Helpers\ApiClient::request('admin/system/error_reporting_enabled')->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();
|
||||
|
||||
$result = Tests\Helpers\ApiClient::request('admin/system/toggle_error_reporting');
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->getResult())->toBeTrue();
|
||||
try {
|
||||
$result = ApiClient::request('admin/system/toggle_error_reporting');
|
||||
expect($result->wasSuccessful())->toBeTrue()
|
||||
->and($result->getResult())->toBeTrue();
|
||||
|
||||
$after = Tests\Helpers\ApiClient::request('admin/system/error_reporting_enabled')->getResult();
|
||||
expect($after)->toBeBool();
|
||||
$this->assertNotEquals($after, $before);
|
||||
$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 ($after) {
|
||||
$result = Tests\Helpers\ApiClient::request('admin/system/toggle_error_reporting');
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->getResult())->toBeTrue();
|
||||
if ($current !== $before) {
|
||||
$restoreResult = ApiClient::request('admin/system/toggle_error_reporting');
|
||||
expect($restoreResult->wasSuccessful())->toBeTrue()
|
||||
->and($restoreResult->getResult())->toBeTrue();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('get and set network interfaces', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('admin/system/get_interface_ips');
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->getResult())->toBeArray();
|
||||
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 (systemIpLookupWorking()) {
|
||||
foreach ($result->getResult() as $ip) {
|
||||
$testResult = Tests\Helpers\ApiClient::request('admin/system/set_interface_ip', ['interface' => $ip]);
|
||||
expect($testResult->wasSuccessful())->toBeTrue($result);
|
||||
|
||||
sleep(2);
|
||||
|
||||
$result = Tests\Helpers\ApiClient::request('admin/system/env', ['ip' => true]);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect((bool) filter_var($result->getResult(), FILTER_VALIDATE_IP))->toBeTrue();
|
||||
}
|
||||
if (!systemIsIpLookupAvailable()) {
|
||||
$this->markTestSkipped('Integration test requires ipify.org to be available.');
|
||||
}
|
||||
|
||||
Tests\Helpers\ApiClient::request('admin/system/set_interface_ip', ['interface' => '0']);
|
||||
});
|
||||
foreach ($result->getResult() as $ip) {
|
||||
$testResult = ApiClient::request('admin/system/set_interface_ip', ['interface' => $ip]);
|
||||
expect($testResult->wasSuccessful())->toBeTrue();
|
||||
|
||||
test('interface is ignored when not valid', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('admin/system/set_interface_ip', ['interface' => '12345']);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->getResult())->toBeTrue();
|
||||
$isReady = systemRetryUntil(
|
||||
static function (): bool {
|
||||
$envResult = ApiClient::request('admin/system/env', ['ip' => true]);
|
||||
|
||||
if (systemIpLookupWorking()) {
|
||||
sleep(2);
|
||||
$result = Tests\Helpers\ApiClient::request('admin/system/env', ['ip' => true]);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect((bool) filter_var($result->getResult(), FILTER_VALIDATE_IP))->toBeTrue();
|
||||
}
|
||||
});
|
||||
return $envResult->wasSuccessful() && (bool) filter_var($envResult->getResult(), FILTER_VALIDATE_IP);
|
||||
}
|
||||
);
|
||||
|
||||
test('custom interface', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('admin/system/set_interface_ip', ['custom_interface' => '12345']);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->getResult())->toBeTrue();
|
||||
|
||||
if (systemIpLookupWorking()) {
|
||||
sleep(2);
|
||||
$result = Tests\Helpers\ApiClient::request('admin/system/env', ['ip' => true]);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->getResult())->toBeEmpty();
|
||||
expect($isReady)->toBeTrue();
|
||||
}
|
||||
|
||||
$result = Tests\Helpers\ApiClient::request('admin/system/set_interface_ip', ['custom_interface' => '', 'interface' => '0']);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->getResult())->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();
|
||||
}
|
||||
});
|
||||
@@ -17,42 +17,30 @@ if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
|
||||
|
||||
test('template exists', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('guest/system/template_exists', ['file' => 'layout_default.html.twig']);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult())->toBeTrue();
|
||||
});
|
||||
|
||||
test('template does not exist', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('guest/system/template_exists', ['file' => 'thisfiledoesnotexist.txt']);
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult())->toBeFalse();
|
||||
});
|
||||
|
||||
test('periods', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('guest/system/periods');
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult())->toBeArray();
|
||||
});
|
||||
|
||||
test('countries', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('guest/system/countries');
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->getResult())->toBeArray();
|
||||
});
|
||||
|
||||
test('countries eunion', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('guest/system/countries_eunion');
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->getResult())->toBeArray();
|
||||
});
|
||||
|
||||
test('states', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('guest/system/states');
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult())->toBeArray();
|
||||
});
|
||||
|
||||
test('phone codes', function (): void {
|
||||
$result = Tests\Helpers\ApiClient::request('guest/system/phone_codes');
|
||||
expect($result->wasSuccessful())->toBeTrue($result);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
expect($result->getResult())->toBeArray();
|
||||
});
|
||||
@@ -1,9 +1,21 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Copyright 2022-2026 FOSSBilling
|
||||
* SPDX-License-Identifier: Apache-2.0.
|
||||
*
|
||||
* @copyright FOSSBilling (https://www.fossbilling.org)
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tests\Helpers\ApiClient;
|
||||
|
||||
if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const THEME_SEMANTIC_VERSION_PATTERN = '/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/';
|
||||
|
||||
function expectValidThemePayload(array $data): void
|
||||
@@ -38,7 +38,6 @@ test('urls return ok status', function () use ($testUrls): void {
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_exec($ch);
|
||||
$responseCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
expect($responseCode)->toEqual($expectedCode, "URL '{$url}' should return {$expectedCode}");
|
||||
}
|
||||
|
||||
@@ -77,13 +77,11 @@ class ApiClient
|
||||
if ($output === false) {
|
||||
$error = curl_error($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
throw new \RuntimeException('cURL request failed: ' . $error . ' (HTTP ' . $httpCode . ')');
|
||||
}
|
||||
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
return new ApiResponse($httpCode, $output);
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tests\Helpers\ApiClient;
|
||||
|
||||
test('guest cron behavior', function (): void {
|
||||
ApiClient::request('admin/cron/save_settings', ['ext' => 'mod_cron', 'guest_cron' => false]);
|
||||
$result = ApiClient::request('guest/cron/run');
|
||||
expect($result->wasSuccessful())->toBeFalse();
|
||||
|
||||
ApiClient::request('admin/cron/save_settings', ['ext' => 'mod_cron', 'guest_cron' => true]);
|
||||
$config = ApiClient::request('admin/extension/config_get', ['ext' => 'mod_cron']);
|
||||
expect($config->wasSuccessful())->toBeTrue();
|
||||
|
||||
$hash = $config->getResult()['cron_hash'] ?? '';
|
||||
expect($hash)->not->toBeEmpty();
|
||||
|
||||
ApiClient::request('admin/system/update_params', ['last_cron_exec' => date('Y-m-d H:i:s', time() - 6400)]);
|
||||
$result = ApiClient::request('guest/cron/run', ['hash' => $hash]);
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
|
||||
$result = ApiClient::request('guest/cron/run', ['hash' => $hash]);
|
||||
expect($result->wasSuccessful())->toBeTrue()
|
||||
->and($result->getResult())->toBeFalse();
|
||||
|
||||
$result = ApiClient::request('guest/cron/run', ['hash' => 'invalid']);
|
||||
expect($result->wasSuccessful())->toBeFalse();
|
||||
|
||||
$result = ApiClient::request('guest/cron/run');
|
||||
expect($result->wasSuccessful())->toBeFalse();
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tests\Helpers\ApiClient;
|
||||
|
||||
test('gets currency pairs', function (): void {
|
||||
$result = ApiClient::request('admin/currency/get_pairs');
|
||||
|
||||
expect($result->wasSuccessful())->toBeTrue();
|
||||
|
||||
$list = $result->getResult();
|
||||
|
||||
foreach (['USD', 'EUR', 'GBP', 'JPY', 'CHF', 'AUD', 'CAD', 'NZD', 'INR', 'HKD'] as $currencyCode) {
|
||||
expect($list)->toHaveKey($currencyCode);
|
||||
}
|
||||
|
||||
foreach (['XXX', 'XTS'] as $currencyCode) {
|
||||
expect($list)->not->toHaveKey($currencyCode);
|
||||
}
|
||||
|
||||
expect($list['USD'])->toMatch('/^USD \(.*\)$/');
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tests\Helpers\ApiClient;
|
||||
|
||||
test('gets extension settings', function (): void {
|
||||
$result = ApiClient::request('guest/extension/settings', ['ext' => 'index']);
|
||||
|
||||
expect($result->wasSuccessful())->toBeTrue()
|
||||
->and($result->getResult())->toBeArray();
|
||||
});
|
||||
|
||||
test('returns error when extension settings ext is missing', function (): void {
|
||||
$result = ApiClient::request('guest/extension/settings', ['ext' => '']);
|
||||
|
||||
expect($result->wasSuccessful())->toBeFalse()
|
||||
->and($result->getErrorMessage())->toBe('Parameter ext is missing');
|
||||
});
|
||||
|
||||
test('reports active extension as enabled', function (): void {
|
||||
$result = ApiClient::request('guest/extension/is_on', ['mod' => 'index']);
|
||||
|
||||
expect($result->wasSuccessful())->toBeTrue()
|
||||
->and($result->getResult())->toBeTrue();
|
||||
});
|
||||
|
||||
test('reports inactive extension as disabled', function (): void {
|
||||
$result = ApiClient::request('guest/extension/is_on', ['mod' => 'serviceapikey']);
|
||||
|
||||
expect($result->wasSuccessful())->toBeTrue()
|
||||
->and($result->getResult())->toBeFalse();
|
||||
});
|
||||
@@ -1,202 +0,0 @@
|
||||
<?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();
|
||||
}
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tests\Helpers\ApiClient;
|
||||
|
||||
test('template exists', function (): void {
|
||||
$result = ApiClient::request('guest/system/template_exists', ['file' => 'layout_default.html.twig']);
|
||||
|
||||
expect($result->wasSuccessful())->toBeTrue()
|
||||
->and($result->getResult())->toBeTrue();
|
||||
});
|
||||
|
||||
test('template does not exist', function (): void {
|
||||
$result = ApiClient::request('guest/system/template_exists', ['file' => 'thisfiledoesnotexist.txt']);
|
||||
|
||||
expect($result->wasSuccessful())->toBeTrue()
|
||||
->and($result->getResult())->toBeFalse();
|
||||
});
|
||||
|
||||
test('gets periods', function (): void {
|
||||
$result = ApiClient::request('guest/system/periods');
|
||||
|
||||
expect($result->wasSuccessful())->toBeTrue()
|
||||
->and($result->getResult())->toBeArray();
|
||||
});
|
||||
|
||||
test('gets phone codes', function (): void {
|
||||
$result = ApiClient::request('guest/system/phone_codes');
|
||||
|
||||
expect($result->wasSuccessful())->toBeTrue()
|
||||
->and($result->getResult())->toBeArray();
|
||||
});
|
||||
@@ -109,16 +109,6 @@ if ($appUrl && $testApiKey) {
|
||||
->in('E2E');
|
||||
}
|
||||
|
||||
uses()
|
||||
->beforeEach(function (): void {
|
||||
if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
|
||||
$this->markTestSkipped('API tests require APP_URL and TEST_API_KEY.');
|
||||
}
|
||||
|
||||
Tests\Helpers\ApiClient::resetCookies();
|
||||
})
|
||||
->in('Modules');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expectations
|
||||
|
||||
Reference in new issue
Block a user