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');
});