setDi(Tests\Helpers\container()); try { $api->getIdentity(); } catch (Throwable) { $realClass = $api::class; // Mockery mocks have names like Mockery_2_Box_Mod_..._Api_Admin; unwrap to the mocked class. if (str_starts_with($realClass, 'Mockery_') && ($parent = get_parent_class($api)) !== false) { $realClass = $parent; } // Normalize both namespace separators for checks (mocks use underscores). $normalized = str_replace(['\\', '_'], '\\', $realClass); $api->setIdentity(match (true) { str_contains($normalized, '\\Api\\Admin') => \Tests\Helpers\admin(), str_contains($normalized, '\\Api\\Client') => \Tests\Helpers\client(), default => new FOSSBilling\Core\Identity\Guest(), }); } 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\Core\Logging\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. | */