mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-12 15:53:46 -04:00
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
129 lines
5.8 KiB
PHP
129 lines
5.8 KiB
PHP
<?php
|
|
|
|
use App\Services\TraefikAcmeService;
|
|
|
|
function traefikAcmeFixture(): string
|
|
{
|
|
return json_encode([
|
|
'letsencrypt' => [
|
|
'Account' => ['Email' => 'admin@example.com'],
|
|
'Certificates' => [
|
|
[
|
|
'domain' => [
|
|
'main' => 'example.com',
|
|
'sans' => ['www.example.com', '*.example.com'],
|
|
],
|
|
'certificate' => base64_encode('first-certificate'),
|
|
'key' => base64_encode('first-key'),
|
|
'Store' => 'default',
|
|
],
|
|
[
|
|
'domain' => ['main' => 'api.example.net'],
|
|
'certificate' => base64_encode('second-certificate'),
|
|
'key' => base64_encode('second-key'),
|
|
],
|
|
],
|
|
],
|
|
'zerossl' => [
|
|
'Account' => ['Email' => 'admin@example.org'],
|
|
'Certificates' => [[
|
|
'domain' => ['main' => 'example.org', 'sans' => []],
|
|
'certificate' => base64_encode('third-certificate'),
|
|
'key' => base64_encode('third-key'),
|
|
]],
|
|
],
|
|
], JSON_THROW_ON_ERROR);
|
|
}
|
|
|
|
it('lists every certificate from every ACME resolver without exposing secrets', function () {
|
|
$certificates = app(TraefikAcmeService::class)->certificates(traefikAcmeFixture());
|
|
|
|
expect($certificates)->toHaveCount(3)
|
|
->and($certificates[0])->toMatchArray([
|
|
'resolver' => 'letsencrypt',
|
|
'main_domain' => 'example.com',
|
|
'sans' => ['www.example.com', '*.example.com'],
|
|
'store' => 'default',
|
|
])
|
|
->and($certificates[0])->toHaveKeys(['id', 'expires_at'])
|
|
->and($certificates[0])->not->toHaveKeys(['certificate', 'key', 'account']);
|
|
});
|
|
|
|
it('removes only the selected certificate and preserves all other ACME data', function () {
|
|
$service = app(TraefikAcmeService::class);
|
|
$certificates = $service->certificates(traefikAcmeFixture());
|
|
|
|
$updated = json_decode($service->deleteCertificates(
|
|
traefikAcmeFixture(),
|
|
[$certificates[0]['id']],
|
|
), true, flags: JSON_THROW_ON_ERROR);
|
|
|
|
expect($updated['letsencrypt']['Account']['Email'])->toBe('admin@example.com')
|
|
->and($updated['letsencrypt']['Certificates'])->toHaveCount(1)
|
|
->and($updated['letsencrypt']['Certificates'][0]['domain']['main'])->toBe('api.example.net')
|
|
->and($updated['zerossl']['Certificates'])->toHaveCount(1);
|
|
});
|
|
|
|
it('rejects invalid files and unknown certificate identifiers', function () {
|
|
$service = app(TraefikAcmeService::class);
|
|
|
|
expect(fn () => $service->certificates('{invalid'))
|
|
->toThrow(RuntimeException::class, 'invalid JSON')
|
|
->and(fn () => $service->deleteCertificates(traefikAcmeFixture(), ['missing']))
|
|
->toThrow(RuntimeException::class, 'could not be found');
|
|
});
|
|
|
|
it('reads the expiry date from the base64-encoded PEM chain Traefik stores', function () {
|
|
$key = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']);
|
|
$certificate = openssl_csr_sign(openssl_csr_new(['commonName' => 'example.com'], $key), null, $key, 30);
|
|
openssl_x509_export($certificate, $pem);
|
|
$expiresAt = gmdate('Y-m-d H:i:s', openssl_x509_parse($pem)['validTo_time_t']);
|
|
|
|
$contents = json_encode(['letsencrypt' => ['Certificates' => [[
|
|
'domain' => ['main' => 'example.com'],
|
|
'certificate' => base64_encode($pem),
|
|
'key' => base64_encode('key'),
|
|
]]]], JSON_THROW_ON_ERROR);
|
|
|
|
expect(app(TraefikAcmeService::class)->certificates($contents)[0]['expires_at'])->toBe($expiresAt);
|
|
});
|
|
|
|
it('removes several certificates of different resolvers in one pass', function () {
|
|
$service = app(TraefikAcmeService::class);
|
|
$certificates = $service->certificates(traefikAcmeFixture());
|
|
|
|
$updated = json_decode($service->deleteCertificates(
|
|
traefikAcmeFixture(),
|
|
[$certificates[1]['id'], $certificates[2]['id']],
|
|
), true, flags: JSON_THROW_ON_ERROR);
|
|
|
|
expect(collect($updated['letsencrypt']['Certificates'])->pluck('domain.main')->all())->toBe(['example.com'])
|
|
->and($updated['zerossl']['Certificates'])->toBe([])
|
|
->and($updated['zerossl']['Account']['Email'])->toBe('admin@example.org')
|
|
->and(fn () => $service->deleteCertificates(traefikAcmeFixture(), [$certificates[0]['id'], 'missing']))
|
|
->toThrow(RuntimeException::class, 'could not be found');
|
|
});
|
|
|
|
it('finds the host names in Traefik labels, configuration files, and resource URLs', function () {
|
|
$text = '{"traefik.http.routers.http-0-abc.rule":"Host(`App.Example.com`) && PathPrefix(`/`)"}'."\n"
|
|
."rule: Host(`api.example.net`)\ndomains:\n - main: '*.wild.example.org'\nhttps://shop.example.com:8443/path,http://www.example.com";
|
|
|
|
expect(app(TraefikAcmeService::class)->hostnames($text))
|
|
->toHaveKeys(['app.example.com', 'api.example.net', '*.wild.example.org', 'shop.example.com', 'www.example.com']);
|
|
});
|
|
|
|
it('treats a certificate as used when a host matches its domain, an alternative name, or its wildcard', function (array $hosts, bool $inUse) {
|
|
$certificate = ['main_domain' => 'Example.com', 'sans' => ['www.example.com', '*.apps.example.com']];
|
|
|
|
expect(app(TraefikAcmeService::class)->isInUse($certificate, array_fill_keys($hosts, true)))->toBe($inUse);
|
|
})->with([
|
|
'main domain' => [['example.com'], true],
|
|
'alternative name' => [['www.example.com'], true],
|
|
'host below the wildcard' => [['shop.apps.example.com'], true],
|
|
'wildcard named in the configuration' => [['*.apps.example.com'], true],
|
|
'host two levels below the wildcard' => [['a.shop.apps.example.com'], false],
|
|
'parent of the wildcard' => [['apps.example.com'], false],
|
|
'other domain' => [['example.net', 'other.example.com'], false],
|
|
'no hosts' => [[], false],
|
|
]);
|