renderCallback = $render; } public function render($fileName, $variableArray = [], $ext = 'html.twig'): string { return ($this->renderCallback)($fileName, $variableArray, $ext); } }; $di = new Pimple\Container(); $di['logger'] = new class { public function withChannel(string $channel): self { return $this; } public function error(string|Stringable $message, array $context = []): void { } public function info(string|Stringable $message, array $context = []): void { } }; $extensionService = new class { public function isExtensionActive(): bool { return false; } }; $di['request'] = Request::create('http://localhost/test'); $di['mod_service'] = $di->protect(static fn (): object => $extensionService); $di['auth'] = Mockery::mock(Box_Authorization::class) ->shouldReceive('isClientLoggedIn')->andReturn($clientLoggedIn)->getMock(); $di['url'] = Mockery::mock(FOSSBilling\Url::class) ->shouldReceive('link')->andReturnArg(0)->getMock(); $app->setDi($di); $app->setUrl('/test'); return $app; } test('get_custom_page returns 200 on successful render', function (): void { $app = appClientWithRender(fn (): string => 'ok'); $response = $app->get_custom_page('signup'); expect($response->getStatusCode())->toBe(200); expect($response->getContent())->toBe('ok'); }); test('get_custom_page returns XML content type for sitemap', function (): void { $app = appClientWithRender(fn (): string => ''); $response = $app->get_custom_page('sitemap.xml'); expect($response->getStatusCode())->toBe(200) ->and($response->headers->get('Content-Type'))->toContain('application/xml') ->and($response->getContent())->toBe(''); }); test('get_custom_page redirects authenticated clients away from login', function (): void { $app = appClientWithRender( static fn (): never => throw new RuntimeException('The login page should not be rendered.'), clientLoggedIn: true, ); $response = $app->get_custom_page('login'); expect($response->getStatusCode())->toBe(302) ->and($response->headers->get('Location'))->toBe('/'); }); test('get_custom_page returns 500 (not 404) when the template throws a RuntimeError (regression for #3818)', function (): void { $app = appClientWithRender(function (string $fileName): string { if ($fileName === 'error') { return 'error body'; } throw new Twig\Error\RuntimeError('Variable "company" does not exist.'); }); $response = $app->get_custom_page('signup'); expect($response->getStatusCode())->toBe(500); }); test('get_custom_page returns 500 (not 404) when the template throws a SyntaxError', function (): void { $app = appClientWithRender(function (string $fileName): string { if ($fileName === 'error') { return 'error body'; } throw new Twig\Error\SyntaxError('Unexpected token.'); }); $response = $app->get_custom_page('signup'); expect($response->getStatusCode())->toBe(500); }); test('get_custom_page returns 500 (not 404) when a nested template is missing during render', function (): void { $app = appClientWithRender(function (string $fileName): string { if ($fileName === 'error') { return 'error body'; } throw new Twig\Error\LoaderError('Template "missing-partial.html.twig" is not defined.'); }); $response = $app->get_custom_page('signup'); expect($response->getStatusCode())->toBe(500); }); test('get_custom_page still returns 404 when the top-level template is missing', function (): void { $app = appClientWithRender(function (string $fileName): string { if ($fileName === 'error') { return 'error body'; } throw new FOSSBilling\InformationException('Page not found', null, 404); }); $response = $app->get_custom_page('signup'); expect($response->getStatusCode())->toBe(404); }); test('render() converts a Twig cache write failure into a report:false exception (regression for FOSSBILLING-EBW)', function (string $message): void { $app = new class extends Box_AppClient { public function triggerCacheWriteFailure(RuntimeException $e): never { $this->convertCacheWriteFailure($e); } }; $di = new Pimple\Container(); $di['request'] = Request::create('http://localhost/test'); $di['logger'] = new class { public function withChannel(string $channel): self { return $this; } public function error(string|Stringable $message, array $context = []): void { } }; $app->setDi($di); try { $app->triggerCacheWriteFailure(new RuntimeException($message)); expect(false)->toBeTrue('Expected a FOSSBilling\Exception to be thrown.'); } catch (FOSSBilling\Exception $e) { expect($e->getCode())->toBe(5002) ->and(FOSSBilling\ErrorPage::getCodeInfo($e->getCode())['report'])->toBeFalse(); } })->with([ 'directory does not exist and cannot be created' => ['Unable to create the cache directory (/var/www/data/cache/7d).'], 'directory exists but is not writable' => ['Unable to write in the cache directory (/var/www/data/cache/7d).'], 'cache file itself could not be written' => ['Failed to write cache file "/var/www/data/cache/7d/abc123.php".'], ]); test('render() rethrows a RuntimeException unrelated to the Twig cache unchanged', function (): void { $app = new class extends Box_AppClient { public function triggerCacheWriteFailure(RuntimeException $e): never { $this->convertCacheWriteFailure($e); } }; $di = new Pimple\Container(); $di['request'] = Request::create('http://localhost/test'); $app->setDi($di); expect(fn () => $app->triggerCacheWriteFailure(new RuntimeException('Something else entirely.'))) ->toThrow(RuntimeException::class, 'Something else entirely.'); }); test('numeric custom page paths return a themed 404', function (): void { $app = appClientWithRender(static function (string $fileName): string { if ($fileName === 'error') { return 'error body'; } throw new FOSSBilling\InformationException('Page not found', null, 404); }); $app->setUrl('/12345'); $response = $app->run(); expect($response->getStatusCode())->toBe(404) ->and($response->getContent())->toBe('error body'); });