Files
FOSSBilling/tests/Pest.php

246 lines
7.8 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 before the application bootstrap loads 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__ . '/Support/CombinedTwigLoader.php';
require_once __DIR__ . '/Support/PermissiveStub.php';
require_once __DIR__ . '/Support/ClientThemeRenderer.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';
/**
* Whether a Redis client extension is available. Cache-backend tests that need an
* environment without it (to exercise the connection-failure/fallback paths) skip
* themselves when this is true.
*/
function hasRedisExtension(): bool
{
return class_exists(Redis::class) || class_exists(Relay\Relay::class) || class_exists(RedisCluster::class);
}
/**
* Construct an API endpoint with the default test container.
*
* @template T of FOSSBilling\Api\AbstractApi
*
* @param T $api
*
* @return T
*/
function apiEndpoint(FOSSBilling\Api\AbstractApi $api): FOSSBilling\Api\AbstractApi
{
$api->setDi(Tests\Helpers\container());
return $api;
}
/**
* Runs $callback with APP_ENV set to $value (or unset entirely when $value is null),
* restoring the previous value afterward regardless of outcome.
*/
function withAppEnv(?string $value, callable $callback): mixed
{
$previous = getenv('APP_ENV');
if ($value === null) {
putenv('APP_ENV');
} else {
putenv('APP_ENV=' . $value);
}
try {
return $callback();
} finally {
putenv($previous === false ? 'APP_ENV' : 'APP_ENV=' . $previous);
}
}
// Define TestLogger class after autoloader is registered.
// This is deferred because FOSSBilling\Logger is loaded via the autoloader.
// @phpstan-ignore-next-line
if (!class_exists(Tests\Helpers\TestLogger::class)) {
// @phpstan-ignore-next-line
eval('
namespace Tests\Helpers;
class TestLogger extends \Psr\Log\AbstractLogger
{
public array $calls = [];
private string $channel = "application";
private array $context = [];
public function __construct()
{
$this->calls = [];
}
public function log($level, string|\\Stringable $message, array $context = []): void
{
$effectiveContext = [...$this->context, ...$context];
$params = [$message];
if ($effectiveContext !== []) {
$params[] = $effectiveContext;
}
$call = ["method" => $level, "params" => $params];
if ($this->channel !== "application") {
$call["channel"] = $this->channel;
}
$this->calls[] = $call;
}
public function withChannel(string $channel): static
{
$logger = clone $this;
$logger->calls =& $this->calls;
$logger->channel = $channel;
return $logger;
}
public function withContext(array $context): static
{
$logger = clone $this;
$logger->calls =& $this->calls;
$logger->context = [...$this->context, ...$context];
return $logger;
}
}
');
}
// 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.
|
*/