Files
coolify/tests/Unit/Api/LogEndpointHelpersTest.php
T
Andras Bacsai 7d11bc92f7 feat(logs): accept all for log lines and keep -1
API log endpoints and the GetLogs UI now treat `all` as unbounded
output, with `-1` remaining as a compatibility alias. MCP still
falls back to a positive default. Traefik version checks run from
CheckForUpdatesJob instead of a weekly schedule.
2026-09-13 15:25:20 +02:00

157 lines
6.1 KiB
PHP

<?php
use App\Mcp\Concerns\ResolvesResource;
use App\Models\Server;
it('normalizes requested log line counts', function () {
if (! function_exists('normalizeLogLines')) {
expect(function_exists('normalizeLogLines'))->toBeTrue();
return;
}
expect(normalizeLogLines(null))->toBe(100)
->and(normalizeLogLines(''))->toBe(100)
->and(normalizeLogLines('abc'))->toBe(100)
->and(normalizeLogLines('all'))->toBe('all')
->and(normalizeLogLines('ALL'))->toBe(100)
->and(normalizeLogLines('-1'))->toBe('all')
->and(normalizeLogLines('0'))->toBe(0)
->and(normalizeLogLines('-5'))->toBe(100)
->and(normalizeLogLines('50'))->toBe(50)
->and(normalizeLogLines('50000'))->toBe(10000);
});
it('keeps MCP log requests bounded', function () {
$normalizer = new class
{
use ResolvesResource;
public function normalize(mixed $lines): int
{
return $this->normalizeMcpLogLines($lines);
}
};
expect($normalizer->normalize('all'))->toBe(100)
->and($normalizer->normalize('-1'))->toBe(100)
->and($normalizer->normalize('0'))->toBe(100)
->and($normalizer->normalize('501'))->toBe(500);
});
it('documents the named all logs option on every REST log endpoint', function (string $controller) {
$source = file_get_contents(__DIR__."/../../../app/Http/Controllers/Api/{$controller}.php");
expect($source)->toContain('Use `all` to return all logs. `-1` remains available as a compatibility alias.');
})->with([
'applications' => 'ApplicationsController',
'databases' => 'DatabasesController',
'services' => 'ServicesController',
'service applications' => 'ServiceApplicationsController',
'service databases' => 'ServiceDatabasesController',
]);
it('normalizes service resource log line counts before invoking Docker', function (string $controller, mixed $lines, int $expectedLines) {
$source = file_get_contents(__DIR__."/../../../app/Http/Controllers/Api/{$controller}.php");
expect($source)->toContain('$lines = normalizeLogLines($request->query(\'lines\'));')
->and(normalizeLogLines($lines))->toBe($expectedLines);
})->with([
'application invalid value' => ['ServiceApplicationsController', 'invalid', 100],
'application negative value' => ['ServiceApplicationsController', '-5', 100],
'application value above limit' => ['ServiceApplicationsController', '50000', 10000],
'database invalid value' => ['ServiceDatabasesController', 'invalid', 100],
'database negative value' => ['ServiceDatabasesController', '-5', 100],
'database value above limit' => ['ServiceDatabasesController', '50000', 10000],
]);
it('parses show_timestamps query values as booleans', function () {
if (! function_exists('parseLogTimestampFlag')) {
expect(function_exists('parseLogTimestampFlag'))->toBeTrue();
return;
}
expect(parseLogTimestampFlag('true'))->toBeTrue()
->and(parseLogTimestampFlag('1'))->toBeTrue()
->and(parseLogTimestampFlag(true))->toBeTrue()
->and(parseLogTimestampFlag('false'))->toBeFalse()
->and(parseLogTimestampFlag('0'))->toBeFalse()
->and(parseLogTimestampFlag(false))->toBeFalse()
->and(parseLogTimestampFlag('not-a-bool'))->toBeFalse()
->and(parseLogTimestampFlag(null))->toBeFalse();
});
it('builds docker log commands with options before an escaped container id', function () {
if (! function_exists('buildContainerLogsCommand')) {
expect(function_exists('buildContainerLogsCommand'))->toBeTrue();
return;
}
$server = new Server;
$server->settings = ['is_swarm_manager' => false];
expect(buildContainerLogsCommand($server, 'container-1', 25, true))
->toBe("docker logs -n 25 --timestamps 'container-1' 2>&1");
});
it('builds docker log commands for all and zero lines', function () {
$server = new Server;
$server->settings = ['is_swarm_manager' => false];
expect(buildContainerLogsCommand($server, 'container-1', 'all'))
->toBe("docker logs -n all 'container-1' 2>&1")
->and(buildContainerLogsCommand($server, 'container-1', 0))
->toBe("docker logs -n 0 'container-1' 2>&1");
});
it('builds swarm service log commands with options before an escaped service id', function () {
if (! function_exists('buildContainerLogsCommand')) {
expect(function_exists('buildContainerLogsCommand'))->toBeTrue();
return;
}
$server = new Server;
$server->settings = ['is_swarm_manager' => true];
expect(buildContainerLogsCommand($server, "service'name", 25, true))
->toBe("docker service logs -n 25 --timestamps 'service'\\''name' 2>&1");
});
it('filters service sub containers in PHP instead of using user input in shell filters', function () {
if (! function_exists('filterServiceSubContainersByName')) {
expect(function_exists('filterServiceSubContainersByName'))->toBeTrue();
return;
}
$containers = collect([
['ID' => 'first', 'Labels' => 'coolify.serviceId=10,coolify.name=app-service-uuid,coolify.type=service'],
['ID' => 'second', 'Labels' => 'coolify.serviceId=10,coolify.name=db-service-uuid,coolify.type=service'],
['ID' => 'third', 'Labels' => ['coolify.name' => 'app-service-uuid']],
]);
expect(filterServiceSubContainersByName($containers, 'app-service-uuid')->pluck('ID')->all())
->toBe(['first', 'third']);
});
it('filters service containers when another Docker label contains commas', function () {
$containers = collect([
[
'ID' => 'app',
'Labels' => 'traefik.http.routers.app.rule=Host(`one.example.com`,`two.example.com`),coolify.name=app-service-uuid',
],
]);
expect(filterServiceSubContainersByName($containers, 'app-service-uuid')->pluck('ID')->all())
->toBe(['app']);
});
it('does not interpolate the requested service name into the docker ps shell command', function () {
$source = file_get_contents(__DIR__.'/../../../bootstrap/helpers/docker.php');
expect($source)->not->toContain('coolify.name={$name}');
});