diff --git a/CLAUDE.md b/CLAUDE.md index bf94134..69242ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,6 +60,10 @@ All errors and warnings are `SecurityTxtSpecViolation` subclasses in `src/Violat Tests use [Nette Tester](https://tester.nette.org/) (`.phpt` files). Each test file is a standalone PHP script ending with `(new FooTest())->run()`. The `tests/bootstrap.php` sets up the autoloader and provides a `needsInternet()` helper that skips network-dependent tests unless `TEST_CASE_RUNNER_INCLUDE_SKIPPED=1`. +When an assertion fails, Tester writes the full actual and expected values to `tests/*/output/FooTest.actual` and `tests/*/output/FooTest.expected` and prints a `diff` command to compare them — use that instead of reading the truncated terminal output. These files are regenerated on every run; do not edit them. + +Inline heredoc expected strings in `.phpt` files contain **raw ANSI escape characters** (not `\033[` literals). Text editors and the Edit tool show them as plain `[1;32m` etc., but the bytes are real ESC (0x1b) sequences. Replacing them requires handling the actual bytes — use Python with `ESC = '\033'` and string concatenation rather than text substitution. + ### Constraints - `parse_url()` is banned — use `Uri\WhatWg\Url` (PHP 8.5 built-in, WhatWG URL standard). Enforced by PHPStan via `phpstan.neon`. diff --git a/src/Check/ConsolePrinter.php b/src/Check/ConsolePrinter.php index a9b8a96..cc8e560 100644 --- a/src/Check/ConsolePrinter.php +++ b/src/Check/ConsolePrinter.php @@ -18,6 +18,12 @@ public function enableColors(): void } + public function ok(string $message): void + { + $this->print($this->colorGreen('[OK]'), $message); + } + + public function info(string $message): void { $this->print($this->colorDarkGray('[Info]'), $message); diff --git a/src/Check/SecurityTxtCheckHostCli.php b/src/Check/SecurityTxtCheckHostCli.php index 15125b5..b8e6674 100644 --- a/src/Check/SecurityTxtCheckHostCli.php +++ b/src/Check/SecurityTxtCheckHostCli.php @@ -52,7 +52,7 @@ function (string $url): void { ); $this->checkHost->addOnFinalUrl( function (string $url): void { - $this->consolePrinter->info('Selecting security.txt located at ' . $this->consolePrinter->colorBold($url) . ' for further tests'); + $this->consolePrinter->info('Using ' . $this->consolePrinter->colorBold($url)); }, ); $this->checkHost->addOnRedirect( @@ -72,7 +72,7 @@ function (int $daysAgo, DateTimeImmutable $expiryDate): void { ); $this->checkHost->addOnExpires( function (int $inDays, DateTimeImmutable $expiryDate): void { - $this->consolePrinter->info($this->consolePrinter->colorGreen("The file will expire in {$inDays} " . ($inDays === 1 ? 'day' : 'days')) . " ({$expiryDate->format(DATE_RFC3339)})"); + $this->consolePrinter->ok("The file will expire in {$inDays} " . ($inDays === 1 ? 'day' : 'days') . " ({$expiryDate->format(DATE_RFC3339)})"); }, ); $this->checkHost->addOnHost( @@ -82,9 +82,8 @@ function (string $host): void { ); $this->checkHost->addOnValidSignature( function (string $keyFingerprint, DateTimeImmutable $signatureDate): void { - $this->consolePrinter->info(sprintf( - '%s, key %s, signed on %s', - $this->consolePrinter->colorGreen('Signature valid'), + $this->consolePrinter->ok(sprintf( + 'Signature valid, key %s, signed on %s', $keyFingerprint, $signatureDate->format(DATE_RFC3339), )); @@ -126,9 +125,10 @@ function (string $keyFingerprint, DateTimeImmutable $signatureDate): void { $noIpv6, ); if (!$checkResult->isValid()) { - $this->consolePrinter->error($this->consolePrinter->colorRed('Please update the file!')); + $this->consolePrinter->error($this->consolePrinter->colorRed('The file is invalid')); $this->exit(CheckExitStatus::Error); } else { + $this->consolePrinter->ok($this->consolePrinter->colorGreen('The file is valid')); $this->exit(CheckExitStatus::Ok); } } catch (SecurityTxtFetcherException $e) { diff --git a/tests/Check/ConsolePrinterTest.phpt b/tests/Check/ConsolePrinterTest.phpt index 3973bfa..668b36b 100644 --- a/tests/Check/ConsolePrinterTest.phpt +++ b/tests/Check/ConsolePrinterTest.phpt @@ -14,26 +14,20 @@ require __DIR__ . '/../bootstrap.php'; final class ConsolePrinterTest extends TestCase { - private ConsolePrinter $printer; - - - public function __construct() - { - $this->printer = new ConsolePrinter(); - } - - - public function testPrinter(): void + public function testPrinterColorsOn(): void { - $this->printer->enableColors(); + $printer = new ConsolePrinter(); + $printer->enableColors(); ob_start(); - $this->printer->info('Never'); - $this->printer->error('gonna'); - $this->printer->warning('give'); - $this->printer->info($this->printer->colorGreen('you')); - $this->printer->info($this->printer->colorBold('up')); + $printer->ok('🕺'); + $printer->info('Never'); + $printer->error('gonna'); + $printer->warning('give'); + $printer->info($printer->colorGreen('you')); + $printer->info($printer->colorBold('up')); $output = ob_get_clean(); $expected = <<< EOT + [OK] 🕺 [Info] Never [Error] gonna [Warning] give @@ -43,6 +37,29 @@ final class ConsolePrinterTest extends TestCase Assert::same($expected . "\n", $output); } + + public function testPrinterColorsOff(): void + { + $printer = new ConsolePrinter(); + ob_start(); + $printer->ok('🕺'); + $printer->info('Never'); + $printer->error('gonna'); + $printer->warning('give'); + $printer->info($printer->colorGreen('you')); + $printer->info($printer->colorBold('up')); + $output = ob_get_clean(); + $expected = <<< EOT + [OK] 🕺 + [Info] Never + [Error] gonna + [Warning] give + [Info] you + [Info] up + EOT; + Assert::same($expected . "\n", $output); + } + } (new ConsolePrinterTest())->run(); diff --git a/tests/Check/SecurityTxtCheckHostCliTest.phpt b/tests/Check/SecurityTxtCheckHostCliTest.phpt index 8b77683..49fb739 100644 --- a/tests/Check/SecurityTxtCheckHostCliTest.phpt +++ b/tests/Check/SecurityTxtCheckHostCliTest.phpt @@ -62,12 +62,12 @@ final class SecurityTxtCheckHostCliTest extends TestCase [Info] Loading security.txt from https://example.com/security.txt [Info] Redirected from https://example.com/security.txt to https://nah.example/ [Info] Not found https://nah.example/ - [Info] Selecting security.txt located at https://example.com/.well-known/security.txt for further tests + [Info] Using https://example.com/.well-known/security.txt [Error] The file at https://example.com/.well-known/security.txt has a correct Content-Type of text/plain but the charset=utf-8 parameter is missing (How to fix: Add a charset=utf-8 parameter, e.g. text/plain; charset=utf-8) [Error] on line 2: The file is considered stale and should not be used (How to fix: The Expires field should contain a date and time in the future formatted according to the Internet profile of ISO 8601 as defined in RFC 3339, e.g. {$this->getExpiresExample()}) [Warning] security.txt not found at the top-level path (How to fix: Redirect the top-level file to the one under the /.well-known/ path) [Error] The file has expired 42 days ago ({$this->expires}) - [Error] Please update the file! + [Error] The file is invalid EOT; Assert::same($expected . "\n", $output); Assert::same(CheckExitStatus::Error->value, $this->exitStatus); @@ -93,10 +93,10 @@ final class SecurityTxtCheckHostCliTest extends TestCase [Info] Loading security.txt from https://example.com/.well-known/security.txt [Info] Loading security.txt from https://example.com/security.txt [Info] Not found https://example.com/security.txt - [Info] Selecting security.txt located at https://example.com/.well-known/security.txt for further tests + [Info] Using https://example.com/.well-known/security.txt [Warning] on line 2: The file will be considered stale in 5 days (How to fix: Update the value of the Expires field, e.g. {$this->getExpiresExample()}) - [Info] The file will expire in 5 days ({$expires}) - [Error] Please update the file! + [OK] The file will expire in 5 days ({$expires}) + [Error] The file is invalid EOT; Assert::same($expected . "\n", $output); Assert::same(CheckExitStatus::Error->value, $this->exitStatus); @@ -137,9 +137,10 @@ final class SecurityTxtCheckHostCliTest extends TestCase [Info] Loading security.txt from https://example.com/.well-known/security.txt [Info] Loading security.txt from https://example.com/security.txt [Info] Not found https://example.com/security.txt - [Info] Selecting security.txt located at https://example.com/.well-known/security.txt for further tests - [Info] The file will expire in 5 days ({$expires}) - [Info] Signature valid, key AF6E1775E311FF78E911E7DC7F879001A9C8F50A, signed on 2025-07-22T02:10:07+00:00 + [Info] Using https://example.com/.well-known/security.txt + [OK] The file will expire in 5 days ({$expires}) + [OK] Signature valid, key AF6E1775E311FF78E911E7DC7F879001A9C8F50A, signed on 2025-07-22T02:10:07+00:00 + [OK] The file is valid EOT; Assert::same($expected . "\n", $output); Assert::same(CheckExitStatus::Ok->value, $this->exitStatus);