mirror of
https://github.com/FOSSBilling/FOSSBilling.git
synced 2026-11-09 13:36:07 +09:00
* Guard Activity index template against missing client/staff nested data
The activity log API at Activity/Api/Admin.php:38-49 only injects a
nested 'client' or 'staff' key when the corresponding *_id is set
(via isset() which returns false for null). System events and
client-only events therefore have no 'client' key, while staff-only
events have no 'client' key.
Under strict_variables, '{% if event.client %}' (line 191) throws
"Key 'client' ... does not exist" for any event without a client.
Switch the conditional to event.client_id / event.staff_id (always
present as SQL columns) and add '|default("")' to the nested
event.client.email / event.client.name / event.staff.email / event.staff.name
accesses so deleted-but-referenced records render gracefully.
* Guard Security iplookup template against empty country/asn data
When the GeoIP or ASN lookup fails (IncompleteRecord from MaxMind), the
Service catches and substitutes [] for the missing data. Under
strict_variables, the empty mapping at record.asn / record.country
causes line 48 to throw "sequence/mapping is empty".
Twig's |default filter on a GetAttrExpression compiles to a safe
is-defined check, so it correctly falls through to the placeholder
when the underlying array is empty or the key is missing.
Verified:
- /admin/security/iplookup?ip=192.168.1.1 (private IP, empty asn)
renders the row with 'Unknown' ASN cells.
- /admin/security/iplookup?ip=8.8.8.8 still renders
'15169' / 'GOOGLE' / 'United States'.
* Initialize notes/rel arrays in Support toApiArray so templates render without errors
Two related issues surfaced under strict_variables on the admin support
ticket page:
1. toApiArray only set $data['notes'] inside the foreach loop over
ticket notes. Tickets without any notes therefore had no 'notes' key
and 'ticket.notes|length' in mod_support_ticket.html.twig:42 threw
'Key "notes" does not exist'.
2. _getRelDetails returned [] (empty array) when a ticket has no
related entity. mod_support_ticket.html.twig sets {% set task =
ticket.rel %} and then accesses task.task, task.type, task.status
etc. on the next lines. Any access into an empty mapping throws
under strict_variables.
Fixes:
- Service::toApiArray: always initialize $data['notes'] = [] for admin
identity so the key is present even when empty.
- Service::_getRelDetails: always return the base structure with all
fields (id, type, task, new_value, status) set to null when there
is no related entity. Skip the DB lookups for the non-rel case.
Template hardening (defense-in-depth, in case other code paths return
a different shape):
- mod_support_ticket.html.twig:42: 'ticket.notes|length' -> 'is defined and
ticket.notes|length'.
- mod_support_ticket.html.twig:222: 'ticket.notes' -> 'ticket.notes|default([])'.
- mod_support_tickets.html.twig:238: same is defined guard.
Verified on dev:
- /admin/support/ticket/1 renders without errors.
- API now returns rel as {id: null, type: null, ...} and notes: [] for
tickets without related data.
- 1777 local tests pass, PHPStan clean.
* Harden tests and add Twig/ToApi linters
* Remove redundant setAccessible calls in tests
* Update cspell.json word list
* Add UrlAwarePermissiveContainer & trim errors
* Add geoip stub and fix ParserFactory usage
* Enforce and apply parentheses for new expressions
169 lines
5.7 KiB
PHP
169 lines
5.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Test Case
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| The closure you provide to your test functions is always bound to a specific
|
|
| PHPUnit test case class. By default, that class is "PHPUnit\Framework\TestCase".
|
|
|
|
|
| You can define custom test case classes in this file and use them via the
|
|
| `uses()` function.
|
|
|
|
|
*/
|
|
|
|
// Global test configuration
|
|
putenv('APP_ENV=test');
|
|
define('PATH_TESTS', __DIR__);
|
|
|
|
// Bump the memory limit. The strict-variables render-everything harness
|
|
// parses every FOSSBilling template and allocates many stubs and AST nodes,
|
|
// which exceeds the default 128M limit and causes the pest process to exit
|
|
// with 255 mid-suite. 2G is comfortably above peak usage (~185M).
|
|
ini_set('memory_limit', '2G');
|
|
|
|
// Disable DEBUG mode to prevent FOSSBilling\Exception from logging stack traces
|
|
// Must be defined BEFORE load.php is included
|
|
if (!defined('DEBUG')) {
|
|
define('DEBUG', false);
|
|
}
|
|
|
|
// Pre-declare translation functions to prevent Box_Translate from trying to redefine them
|
|
// These stubs will be used if the full translation system isn't initialized
|
|
if (!function_exists('__trans')) {
|
|
function __trans(string $msgid, ?array $values = null): string
|
|
{
|
|
return $values ? strtr($msgid, $values) : $msgid;
|
|
}
|
|
}
|
|
if (!function_exists('__pluralTrans')) {
|
|
function __pluralTrans(string $msgid, string $msgidPlural, int $number, ?array $values = null): string
|
|
{
|
|
return $values ? strtr($msgid, $values) : $msgid;
|
|
}
|
|
}
|
|
|
|
// Load application bootstrap
|
|
require_once __DIR__ . '/../src/load.php';
|
|
require_once __DIR__ . '/../src/vendor/autoload.php';
|
|
|
|
// Load test helpers
|
|
require_once __DIR__ . '/Helpers/Container.php';
|
|
require_once __DIR__ . '/Helpers/Factories.php';
|
|
require_once __DIR__ . '/Helpers/Api.php';
|
|
require_once __DIR__ . '/Helpers/DummyBean.php';
|
|
require_once __DIR__ . '/Support/CombinedTwigLoader.php';
|
|
require_once __DIR__ . '/Support/PermissiveStub.php';
|
|
require_once __DIR__ . '/Support/PermissiveContainer.php';
|
|
require_once __DIR__ . '/Support/VariableCollectorVisitor.php';
|
|
require_once __DIR__ . '/Support/StrictTemplateRenderer.php';
|
|
require_once __DIR__ . '/Support/TwigAstLinter.php';
|
|
require_once __DIR__ . '/Support/ToApiArrayAuditor.php';
|
|
|
|
// Load test datasets
|
|
require_once __DIR__ . '/Datasets/PeriodCodes.php';
|
|
require_once __DIR__ . '/Datasets/ValidationData.php';
|
|
require_once __DIR__ . '/Datasets/GeographicData.php';
|
|
|
|
// Define TestLogger class after autoloader is registered
|
|
// This must be done here because it extends Box_Log which is loaded via the autoloader
|
|
// Using eval() to defer class definition until runtime when Box_Log is available
|
|
// @phpstan-ignore-next-line
|
|
if (!class_exists(Tests\Helpers\TestLogger::class)) {
|
|
// @phpstan-ignore-next-line
|
|
eval('
|
|
namespace Tests\Helpers;
|
|
|
|
class TestLogger extends \Box_Log
|
|
{
|
|
public array $calls = [];
|
|
|
|
public function __construct()
|
|
{
|
|
$this->calls = [];
|
|
}
|
|
|
|
public function __call($method, $params): void
|
|
{
|
|
$this->calls[] = ["method" => $method, "params" => $params];
|
|
}
|
|
}
|
|
');
|
|
}
|
|
|
|
// Redirect error_log to /dev/null during tests to prevent "PHPUnit controlled exception" clutter
|
|
ini_set('error_log', '/dev/null');
|
|
|
|
// Configure Unit tests base with Mockery integration
|
|
uses(MockeryPHPUnitIntegration::class)
|
|
->beforeEach(function (): void {
|
|
// Unit test setup
|
|
})
|
|
->in('Unit');
|
|
|
|
// Configure E2E tests - requires live instance with API access
|
|
// Run with: ./vendor/bin/pest --testsuite=E2E
|
|
// Requires APP_URL and TEST_API_KEY environment variables
|
|
$appUrl = getenv('APP_URL');
|
|
$testApiKey = getenv('TEST_API_KEY');
|
|
|
|
if ($appUrl && $testApiKey) {
|
|
uses()
|
|
->beforeEach(function () use ($appUrl, $testApiKey): void {
|
|
Tests\Helpers\ApiClient::setBaseUrl($appUrl);
|
|
Tests\Helpers\ApiClient::setApiKey($testApiKey);
|
|
})
|
|
->in('E2E');
|
|
}
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Expectations
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| When you're writing tests, you often need to check that values meet certain
|
|
| conditions. The "expect()" function gives you access to a set of useful
|
|
| expectation methods.
|
|
|
|
|
*/
|
|
|
|
expect()->extend('toBeDomain', function (): void {
|
|
$this->not->toBeEmpty();
|
|
expect($this->value)->toBeString();
|
|
expect(filter_var($this->value, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME))->not->toBeFalse();
|
|
});
|
|
|
|
expect()->extend('toBeEmail', function (): void {
|
|
$this->not->toBeEmpty();
|
|
expect($this->value)->toBeString();
|
|
expect(filter_var($this->value, FILTER_VALIDATE_EMAIL))->not->toBeFalse();
|
|
});
|
|
|
|
expect()->extend('toBeUrl', function (): void {
|
|
$this->not->toBeEmpty();
|
|
expect($this->value)->toBeString();
|
|
expect(filter_var($this->value, FILTER_VALIDATE_URL))->not->toBeFalse();
|
|
});
|
|
|
|
expect()->extend('toBeUuid', function (): void {
|
|
$this->not->toBeEmpty();
|
|
expect($this->value)->toBeString();
|
|
expect(preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $this->value))->toBe(1);
|
|
});
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Functions
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| While Pest is very powerful out-of-the-box, you may have some testing code
|
|
| specific to your project that you don't want to repeat in every file.
|
|
| Here you can define custom helper functions and expect() extensions.
|
|
|
|
|
*/
|