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:
Adam Daley authored and GitHub committed 2026-06-03 11:18:29 +01:00
1 parent d31f13b0fa
commit 4315d610b2
29 files changed
+303 -568

No files matched your search

+1 -1
View File
@@ -90,5 +90,5 @@ compose run --rm --no-deps \
app \ app \
sh -euxc " sh -euxc "
cd /workspace cd /workspace
./src/vendor/bin/pest --test-directory ../tests --configuration phpunit-live.xml ./src/vendor/bin/pest --test-directory ../tests --testsuite=E2E --ci
" "
+1 -1
View File
@@ -81,7 +81,7 @@ jobs:
docker run --rm \ docker run --rm \
--env APP_ENV=test \ --env APP_ENV=test \
fossbilling:test-${{ matrix.php }} \ 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' - name: 'Write Job Summary'
if: always() if: always()
+1 -1
View File
@@ -142,7 +142,7 @@ composer test
``` ```
The default suite covers unit tests in `tests/Unit` and module unit tests in `src/modules/*/tests/Unit`. 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`. Cypress end-to-end tests live under `tests/E2E/Cypress`.
### Code Quality Tools ### Code Quality Tools
+1 -1
View File
@@ -146,7 +146,7 @@ WORKDIR /workspace
COPY --from=release-tree /app/src ./src COPY --from=release-tree /app/src ./src
COPY --from=php-dev-vendor /app/src/vendor ./src/vendor 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 COPY tests ./tests
RUN set -eux; \ RUN set -eux; \
-1
View File
@@ -520,7 +520,6 @@
"data/**", "data/**",
"src/data/**", "src/data/**",
"phpunit.xml.dist", "phpunit.xml.dist",
"phpunit-live.xml",
"bin/**", "bin/**",
"*.mmdb", "*.mmdb",
"cspell.json", "cspell.json",
-25
View File
@@ -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>
-3
View File
@@ -20,9 +20,6 @@
<testsuite name="Modules"> <testsuite name="Modules">
<directory>src/modules/*/tests/Unit</directory> <directory>src/modules/*/tests/Unit</directory>
</testsuite> </testsuite>
<testsuite name="API">
<directory>tests/Modules</directory>
</testsuite>
<testsuite name="E2E"> <testsuite name="E2E">
<directory>tests/E2E</directory> <directory>tests/E2E</directory>
<directory>src/modules/*/tests/E2E</directory> <directory>src/modules/*/tests/E2E</directory>
@@ -1,9 +1,21 @@
<?php <?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); declare(strict_types=1);
use Tests\Helpers\ApiClient; use Tests\Helpers\ApiClient;
if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
return;
}
$originalAntispamConfig = null; $originalAntispamConfig = null;
beforeEach(function () use (&$originalAntispamConfig): void { beforeEach(function () use (&$originalAntispamConfig): void {
+16 -20
View File
@@ -10,56 +10,52 @@
declare(strict_types=1); declare(strict_types=1);
// Skip E2E tests if environment is not configured
if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) { if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
return; return;
} }
test('disposable email check', function (): void { use Tests\Helpers\ApiClient;
$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', 'check_temp_emails' => true]); test('disposable email check', function (): void {
expect($result->wasSuccessful())->toBeTrue($result); $result = ApiClient::request('admin/extension/config_save', ['ext' => 'mod_antispam', 'check_temp_emails' => true]);
expect($result->wasSuccessful())->toBeTrue();
$password = 'A1a' . bin2hex(random_bytes(6)); $password = 'A1a' . bin2hex(random_bytes(6));
$result = Tests\Helpers\ApiClient::request('guest/client/create', [ $result = ApiClient::request('guest/client/create', [
'email' => 'email@yopmail.net', 'email' => 'email@yopmail.net',
'first_name' => 'Test', 'first_name' => 'Test',
'password' => $password, 'password' => $password,
'password_confirm' => $password, 'password_confirm' => $password,
]); ]);
expect($result->wasSuccessful())->toBeFalse();
expect($result->getErrorMessage())->toEqual('Disposable email addresses are not allowed');
if ($result->wasSuccessful()) { if ($result->wasSuccessful()) {
$id = intval($result->getResult()); $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 { test('stop forum spam', function (): void {
$result = Tests\Helpers\ApiClient::request('admin/extension/activate', ['type' => 'mod', 'id' => 'antispam']); $result = ApiClient::request('admin/extension/config_save', ['ext' => 'mod_antispam', 'sfs' => true]);
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue();
$result = Tests\Helpers\ApiClient::request('admin/extension/config_save', ['ext' => 'mod_antispam', 'sfs' => true]);
expect($result->wasSuccessful())->toBeTrue($result);
$password = 'A1a' . bin2hex(random_bytes(6)); $password = 'A1a' . bin2hex(random_bytes(6));
$result = Tests\Helpers\ApiClient::request('guest/client/create', [ $result = ApiClient::request('guest/client/create', [
'email' => 'email@example.com', 'email' => 'email@example.com',
'first_name' => 'Test', 'first_name' => 'Test',
'password' => $password, 'password' => $password,
'password_confirm' => $password, 'password_confirm' => $password,
]); ]);
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue();
expect($result->getResult())->toBeNumeric(); expect($result->getResult())->toBeNumeric();
$id = intval($result->getResult()); $id = intval($result->getResult());
$result = Tests\Helpers\ApiClient::request('admin/client/delete', ['id' => $id]); $result = ApiClient::request('admin/client/delete', ['id' => $id]);
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue();
expect($result->getResult())->toBeTrue(); expect($result->getResult())->toBeTrue();
}); });
+9 -11
View File
@@ -30,8 +30,6 @@ test('cart transfers on login', function (): void {
$clientId = cartCreateClient(); $clientId = cartCreateClient();
cartLoginClient($clientId);
$loggedInCart = cartGetCart(); $loggedInCart = cartGetCart();
expect($loggedInCart)->toEqual($startingCart); expect($loggedInCart)->toEqual($startingCart);
@@ -81,8 +79,9 @@ function cartGetCart(): array
function cartCreateClient(): int function cartCreateClient(): int
{ {
$password = 'A1a' . bin2hex(random_bytes(6)); $password = 'A1a' . bin2hex(random_bytes(6));
$email = 'client_' . uniqid() . '@example.com';
$result = Tests\Helpers\ApiClient::request('guest/client/create', [ $result = Tests\Helpers\ApiClient::request('guest/client/create', [
'email' => 'client_' . uniqid() . '@example.com', 'email' => $email,
'first_name' => 'Test', 'first_name' => 'Test',
'password' => $password, 'password' => $password,
'password_confirm' => $password, 'password_confirm' => $password,
@@ -90,16 +89,15 @@ function cartCreateClient(): int
assertApiSuccess($result); assertApiSuccess($result);
assertApiResultIsInt($result); assertApiResultIsInt($result);
return (int) $result->getResult(); $clientId = (int) $result->getResult();
}
function cartLoginClient(int $clientId): void $loginResult = Tests\Helpers\ApiClient::request('guest/client/login', [
{ 'email' => $email,
$result = Tests\Helpers\ApiClient::request('guest/client/login', [ 'password' => $password,
'email' => 'client_' . $clientId . '@example.com',
'password' => 'A1a' . bin2hex(random_bytes(6)),
]); ]);
assertApiSuccess($result); assertApiSuccess($loginResult);
return $clientId;
} }
function cartCleanupClient(int $clientId): void function cartCleanupClient(int $clientId): void
@@ -19,7 +19,7 @@ test('can create and delete client', function (): void {
$clientId = clientCreateClient(); $clientId = clientCreateClient();
$result = Tests\Helpers\ApiClient::request('admin/client/delete', ['id' => $clientId]); $result = Tests\Helpers\ApiClient::request('admin/client/delete', ['id' => $clientId]);
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue();
expect($result->getResult())->toBeTrue(); expect($result->getResult())->toBeTrue();
}); });
@@ -73,7 +73,7 @@ function clientCreateClient(): int
'phone_cc' => 1, 'phone_cc' => 1,
'phone' => '(216) 245-2368', 'phone' => '(216) 245-2368',
]); ]);
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue();
expect($result->getResult())->toBeInt(); expect($result->getResult())->toBeInt();
return (int) $result->getResult(); return (int) $result->getResult();
+3 -3
View File
@@ -17,7 +17,7 @@ if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
test('cron execution', function (): void { test('cron execution', function (): void {
$result = Tests\Helpers\ApiClient::request('admin/cron/info'); $result = Tests\Helpers\ApiClient::request('admin/cron/info');
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue();
if (!empty($result->getResult()['last_cron_exec'])) { if (!empty($result->getResult()['last_cron_exec'])) {
$firstDate = new DateTime($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'); $result = Tests\Helpers\ApiClient::request('admin/cron/run');
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue();
sleep(2); sleep(2);
$result = Tests\Helpers\ApiClient::request('admin/cron/info'); $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(); expect($result->getResult()['last_cron_exec'])->not->toBeEmpty();
$newDate = new DateTime($result->getResult()['last_cron_exec']); $newDate = new DateTime($result->getResult()['last_cron_exec']);
+23 -10
View File
@@ -10,22 +10,35 @@
declare(strict_types=1); declare(strict_types=1);
// Skip E2E tests if environment is not configured use Tests\Helpers\ApiClient;
if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) { if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
return; return;
} }
test('cron guest behavior', function (): void { test('guest cron behavior', function (): void {
Tests\Helpers\ApiClient::request('admin/extension/config_save', ['ext' => 'mod_cron', 'guest_cron' => false]); ApiClient::request('admin/cron/save_settings', ['ext' => 'mod_cron', 'guest_cron' => false]);
$result = Tests\Helpers\ApiClient::request('guest/cron/run'); $result = ApiClient::request('guest/cron/run');
expect($result->wasSuccessful())->toBeFalse(); expect($result->wasSuccessful())->toBeFalse();
Tests\Helpers\ApiClient::request('admin/extension/config_save', ['ext' => 'mod_cron', 'guest_cron' => true]); ApiClient::request('admin/cron/save_settings', ['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)]); $config = ApiClient::request('admin/extension/config_get', ['ext' => 'mod_cron']);
$result = Tests\Helpers\ApiClient::request('guest/cron/run'); expect($config->wasSuccessful())->toBeTrue();
expect($result->wasSuccessful())->toBeTrue($result);
$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->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();
}); });
+25 -17
View File
@@ -10,29 +10,37 @@
declare(strict_types=1); declare(strict_types=1);
// Skip E2E tests if environment is not configured use Tests\Helpers\ApiClient;
if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) { if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
return; return;
} }
test('get available currencies', function (): void { test('gets currency pairs', function (): void {
$result = Tests\Helpers\ApiClient::request('admin/currency/get_pairs'); $result = ApiClient::request('admin/currency/get_pairs');
expect($result->wasSuccessful())->toBeTrue(); expect($result->wasSuccessful())->toBeTrue();
$list = $result->getResult(); $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); foreach (['USD', 'EUR', 'GBP', 'JPY', 'CHF', 'AUD', 'CAD', 'NZD', 'INR', 'HKD'] as $currencyCode) {
$this->assertArrayNotHasKey('XTS', $list); expect($list)->toHaveKey($currencyCode);
$this->assertArrayNotHasKey('VES', $list); }
$this->assertArrayNotHasKey('BZR', $list);
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 { test('can activate extension', function (): void {
$result = Tests\Helpers\ApiClient::request('admin/extension/activate', ['type' => 'mod', 'id' => 'massmailer']); $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 { test('can deactivate extension', function (): void {
$result = Tests\Helpers\ApiClient::request('admin/extension/deactivate', ['type' => 'mod', 'id' => 'massmailer']); $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 { test('language management', function (): void {
$result = Tests\Helpers\ApiClient::request('admin/extension/languages'); $result = Tests\Helpers\ApiClient::request('admin/extension/languages');
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue();
$this->assertNotCount(0, $result->getResult()); $this->assertNotCount(0, $result->getResult());
$result = Tests\Helpers\ApiClient::request('admin/extension/languages', ['disabled' => true]); $result = Tests\Helpers\ApiClient::request('admin/extension/languages', ['disabled' => true]);
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue();
expect($result->getResult())->toHaveCount(0); expect($result->getResult())->toHaveCount(0);
$result = Tests\Helpers\ApiClient::request('admin/extension/toggle_language', ['locale_id' => 'en_US']); $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]); $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'); expect($result->getResult())->toContain('en_US');
$result = Tests\Helpers\ApiClient::request('admin/extension/languages'); $result = Tests\Helpers\ApiClient::request('admin/extension/languages');
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue();
expect($result->getResult())->not->toContain('en_US'); expect($result->getResult())->not->toContain('en_US');
$result = Tests\Helpers\ApiClient::request('admin/extension/toggle_language', ['locale_id' => '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 { test('language completion', function (): void {
@@ -15,35 +15,26 @@ if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
return; 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 { test('settings', function (): void {
$result = Tests\Helpers\ApiClient::request('guest/extension/settings', ['ext' => 'index']); $result = Tests\Helpers\ApiClient::request('guest/extension/settings', ['ext' => 'index']);
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue();
expect($result->getResult())->toBeArray(); expect($result->getResult())->toBeArray();
}); });
test('settings missing ext', function (): void { test('settings missing ext', function (): void {
$result = Tests\Helpers\ApiClient::request('guest/extension/settings', ['ext']); $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'); expect($result->getErrorMessage())->toEqual('Parameter ext is missing');
}); });
test('extension is active', function (): void { test('extension is active', function (): void {
$result = Tests\Helpers\ApiClient::request('guest/extension/is_on', ['mod' => 'index']); $result = Tests\Helpers\ApiClient::request('guest/extension/is_on', ['mod' => 'index']);
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue();
expect($result->getResult())->toBeTrue(); expect($result->getResult())->toBeTrue();
}); });
test('extension is not active', function (): void { test('extension is not active', function (): void {
$result = Tests\Helpers\ApiClient::request('guest/extension/is_on', ['mod' => 'serviceapikey']); $result = Tests\Helpers\ApiClient::request('guest/extension/is_on', ['mod' => 'serviceapikey']);
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue();
expect($result->getResult())->toBeFalse(); expect($result->getResult())->toBeFalse();
}); });
@@ -1,9 +1,21 @@
<?php <?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); declare(strict_types=1);
use Tests\Helpers\ApiClient; use Tests\Helpers\ApiClient;
if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
return;
}
const SUPPORT_MIN_TICKET_ID_LENGTH = 30; const SUPPORT_MIN_TICKET_ID_LENGTH = 30;
const SUPPORT_MAX_TICKET_ID_LENGTH = 60; const SUPPORT_MAX_TICKET_ID_LENGTH = 60;
+168 -71
View File
@@ -10,108 +10,205 @@
declare(strict_types=1); declare(strict_types=1);
// Skip E2E tests if environment is not configured use Tests\Helpers\ApiClient;
if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) { if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
return; 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/']; for ($attempt = 0; $attempt < $maxAttempts; ++$attempt) {
foreach ($services as $service) { if ($operation()) {
$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)) {
return true; return true;
} }
if ($attempt < $maxAttempts - 1) {
usleep($delayMicroseconds);
}
} }
return false; return false;
} }
test('clear cache', function (): void { function systemIsIpLookupAvailable(): bool
$result = Tests\Helpers\ApiClient::request('admin/system/clear_cache'); {
expect($result->wasSuccessful())->toBeTrue($result); $context = stream_context_create([
expect($result->getResult())->toBeBool(); '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 { test('toggles error reporting', function (): void {
$before = Tests\Helpers\ApiClient::request('admin/system/error_reporting_enabled')->getResult(); $beforeResult = ApiClient::request('admin/system/error_reporting_enabled');
expect($beforeResult->wasSuccessful())->toBeTrue();
$before = $beforeResult->getResult();
expect($before)->toBeBool(); expect($before)->toBeBool();
$result = Tests\Helpers\ApiClient::request('admin/system/toggle_error_reporting'); try {
expect($result->wasSuccessful())->toBeTrue($result); $result = ApiClient::request('admin/system/toggle_error_reporting');
expect($result->getResult())->toBeTrue(); expect($result->wasSuccessful())->toBeTrue()
->and($result->getResult())->toBeTrue();
$after = Tests\Helpers\ApiClient::request('admin/system/error_reporting_enabled')->getResult(); $afterResponse = ApiClient::request('admin/system/error_reporting_enabled');
expect($after)->toBeBool(); expect($afterResponse->wasSuccessful())->toBeTrue();
$this->assertNotEquals($after, $before); $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) { if ($current !== $before) {
$result = Tests\Helpers\ApiClient::request('admin/system/toggle_error_reporting'); $restoreResult = ApiClient::request('admin/system/toggle_error_reporting');
expect($result->wasSuccessful())->toBeTrue($result); expect($restoreResult->wasSuccessful())->toBeTrue()
expect($result->getResult())->toBeTrue(); ->and($restoreResult->getResult())->toBeTrue();
}
} }
}); });
test('get and set network interfaces', function (): void { test('gets and sets network interfaces', function (): void {
$result = Tests\Helpers\ApiClient::request('admin/system/get_interface_ips'); $result = ApiClient::request('admin/system/get_interface_ips');
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue()
expect($result->getResult())->toBeArray(); ->and($result->getResult())->toBeArray();
foreach ($result->getResult() as $ip) { foreach ($result->getResult() as $ip) {
expect((bool) filter_var($ip, FILTER_VALIDATE_IP))->toBeTrue(); expect((bool) filter_var($ip, FILTER_VALIDATE_IP))->toBeTrue();
} }
if (systemIpLookupWorking()) { if (!systemIsIpLookupAvailable()) {
foreach ($result->getResult() as $ip) { $this->markTestSkipped('Integration test requires ipify.org to be available.');
$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();
}
} }
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 { $isReady = systemRetryUntil(
$result = Tests\Helpers\ApiClient::request('admin/system/set_interface_ip', ['interface' => '12345']); static function (): bool {
expect($result->wasSuccessful())->toBeTrue($result); $envResult = ApiClient::request('admin/system/env', ['ip' => true]);
expect($result->getResult())->toBeTrue();
if (systemIpLookupWorking()) { return $envResult->wasSuccessful() && (bool) filter_var($envResult->getResult(), FILTER_VALIDATE_IP);
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();
}
});
test('custom interface', function (): void { expect($isReady)->toBeTrue();
$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();
} }
$result = Tests\Helpers\ApiClient::request('admin/system/set_interface_ip', ['custom_interface' => '', 'interface' => '0']); $cleanupResult = ApiClient::request('admin/system/set_interface_ip', ['interface' => SYSTEM_DEFAULT_INTERFACE]);
expect($result->wasSuccessful())->toBeTrue($result); expect($cleanupResult->wasSuccessful())->toBeTrue();
expect($result->getResult())->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();
}
}); });
+5 -17
View File
@@ -17,42 +17,30 @@ if (!getenv('APP_URL') || !getenv('TEST_API_KEY')) {
test('template exists', function (): void { test('template exists', function (): void {
$result = Tests\Helpers\ApiClient::request('guest/system/template_exists', ['file' => 'layout_default.html.twig']); $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(); expect($result->getResult())->toBeTrue();
}); });
test('template does not exist', function (): void { test('template does not exist', function (): void {
$result = Tests\Helpers\ApiClient::request('guest/system/template_exists', ['file' => 'thisfiledoesnotexist.txt']); $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(); expect($result->getResult())->toBeFalse();
}); });
test('periods', function (): void { test('periods', function (): void {
$result = Tests\Helpers\ApiClient::request('guest/system/periods'); $result = Tests\Helpers\ApiClient::request('guest/system/periods');
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue();
expect($result->getResult())->toBeArray(); expect($result->getResult())->toBeArray();
}); });
test('countries', function (): void { test('countries', function (): void {
$result = Tests\Helpers\ApiClient::request('guest/system/countries'); $result = Tests\Helpers\ApiClient::request('guest/system/countries');
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue();
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->getResult())->toBeArray(); expect($result->getResult())->toBeArray();
}); });
test('phone codes', function (): void { test('phone codes', function (): void {
$result = Tests\Helpers\ApiClient::request('guest/system/phone_codes'); $result = Tests\Helpers\ApiClient::request('guest/system/phone_codes');
expect($result->wasSuccessful())->toBeTrue($result); expect($result->wasSuccessful())->toBeTrue();
expect($result->getResult())->toBeArray(); expect($result->getResult())->toBeArray();
}); });
@@ -1,9 +1,21 @@
<?php <?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); declare(strict_types=1);
use Tests\Helpers\ApiClient; 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-]+)*)?$/'; 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 function expectValidThemePayload(array $data): void
-1
View File
@@ -38,7 +38,6 @@ test('urls return ok status', function () use ($testUrls): void {
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch); curl_exec($ch);
$responseCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $responseCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
expect($responseCode)->toEqual($expectedCode, "URL '{$url}' should return {$expectedCode}"); expect($responseCode)->toEqual($expectedCode, "URL '{$url}' should return {$expectedCode}");
} }
-2
View File
@@ -77,13 +77,11 @@ class ApiClient
if ($output === false) { if ($output === false) {
$error = curl_error($ch); $error = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
throw new \RuntimeException('cURL request failed: ' . $error . ' (HTTP ' . $httpCode . ')'); throw new \RuntimeException('cURL request failed: ' . $error . ' (HTTP ' . $httpCode . ')');
} }
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return new ApiResponse($httpCode, $output); return new ApiResponse($httpCode, $output);
} }
-32
View File
@@ -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();
});
-23
View File
@@ -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 \(.*\)$/');
});
-33
View File
@@ -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();
});
-202
View File
@@ -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();
}
});
-33
View File
@@ -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();
});
-10
View File
@@ -109,16 +109,6 @@ if ($appUrl && $testApiKey) {
->in('E2E'); ->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 | Expectations