Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
uses: shivammathur/setup-php@728c6c6b8cf02c2e48117716a91ee48313958a19 # v2
with:
php-version: ${{ matrix.php }}
coverage: none
coverage: pcov
tools: composer:v2

- name: Install dependencies
Expand All @@ -35,8 +35,14 @@ jobs:
- name: Static analysis
run: composer phpstan

- name: Tests
run: composer test
# Coverage gate. Floor is the currently-measured 98% (KendoReports.php:122,
# the string-path file_get_contents attach branch, is the sole uncovered
# line — a parked M-1 Medic item). Aspiration: --min=100 once that branch
# is covered; ratchet the floor up then. Teeth are the pest --coverage --min
# CLI flag (a phpunit.xml <coverage> block alone is a silent no-op on
# PHPUnit 10+).
- name: Tests (coverage gate)
run: composer test:coverage
Comment thread
Goosterhof marked this conversation as resolved.

# Aggregate check tracked by the town-crier trial (TC-0069) — intended as the
# single required status check (the check (8.4)/(8.5) matrix legs stop being
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
uses: shivammathur/setup-php@728c6c6b8cf02c2e48117716a91ee48313958a19 # v2
with:
php-version: ${{ matrix.php }}
coverage: none
coverage: pcov
tools: composer:v2

- name: Install dependencies
Expand All @@ -45,8 +45,10 @@ jobs:
- name: Static analysis
run: composer phpstan

- name: Tests
run: composer test
# Coverage gate — mirrors ci.yml. Floor 98% (KendoReports.php:122 parked
# M-1 branch); aspiration --min=100. Teeth = pest --coverage --min CLI flag.
- name: Tests (coverage gate)
run: composer test:coverage

release:
needs: verify
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and

## [Unreleased]

### Added

- CI coverage gate: `pest --coverage --min=98` with the `pcov` driver on the PHP 8.4/8.5 matrix (CI) and the release-verify job. Floor is the currently-measured 98% (the string-path `file_get_contents` attach branch in `KendoReports` is the sole uncovered line, a parked item); aspiration `--min=100`, ratcheted up once that branch is covered.
- Token-confidentiality regression tests: assert the `report:create` Bearer token never surfaces in a `ReportSubmissionException` message (rejection or transport failure) or in the `error_log` swallow path.

## [0.1.0] — 2026-06-08

Inaugural public release — the kendo report-submission client library.
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
},
"scripts": {
"test": "pest",
"test:coverage": "pest --coverage --min=98",
"phpstan": "phpstan analyse",
"format": "pint",
"format:check": "pint --test"
Expand Down
71 changes: 71 additions & 0 deletions tests/Feature/KendoReportsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,77 @@ function created(): array
expect($client->submit('Broken', 'It broke'))->toBe(created());
});

/**
* Token-confidentiality regression pin (Sapper durability caveat).
*
* The report:create Bearer token must never surface in a failure message.
* Today ReportSubmissionException carries only the HTTP status (rejected) or a
* transport reason (failed) — never the token. These pins lock that in: a future
* ->throw()/->retry() (which would fold the request — headers, Authorization —
Comment thread
Goosterhof marked this conversation as resolved.
* into the thrown/logged message) would fail here before it could leak the token
* into a caller-surfaced exception or the error_log swallow path.
*/
$secretToken = 'super-secret-report-create-token-value';

it('never leaks the report:create token in the exception surfaced on a rejection', function() use ($secretToken): void {
config()->set('report-tool.token', $secretToken);
Http::fake(['kendo.test/*' => Http::response(['message' => 'nope'], 422)]);

try {
app(KendoReports::class)->submit('Broken', 'It broke');

throw new RuntimeException('Expected a ReportSubmissionException to be thrown.');
} catch (ReportSubmissionException $e) {
expect($e->getMessage())
->toContain('422')
->not->toContain($secretToken);
}
});

it('never leaks the report:create token in the exception surfaced on a transport failure', function() use ($secretToken): void {
config()->set('report-tool.token', $secretToken);
Http::fake(function(): void {
// A realistic transport failure — Guzzle's ConnectionException message
// carries the curl error, never the request's Authorization header. The
// pin proves the client never sources the token into the failure reason
// it surfaces (a future ->throw()/->retry() that folded the request into
// the message would trip this).
throw new ConnectionException('cURL error 28: Operation timed out after 5000 milliseconds');
});

try {
app(KendoReports::class)->submit('Broken', 'It broke');

throw new RuntimeException('Expected a ReportSubmissionException to be thrown.');
} catch (ReportSubmissionException $e) {
expect($e->getMessage())->not->toContain($secretToken);
}
});

it('never leaks the report:create token into the error_log swallow path', function() use ($secretToken): void {
config()->set('report-tool.token', $secretToken);
config()->set('report-tool.swallow', true);
Http::fake(['kendo.test/*' => Http::response(['message' => 'nope'], 422)]);

$logFile = (string) tempnam(sys_get_temp_dir(), 'krt-errorlog-');
$previous = ini_set('error_log', $logFile);

try {
app(KendoReports::class)->submit('Broken', 'It broke');

$logged = (string) file_get_contents($logFile);

expect($logged)
->toContain('422')
->not->toContain($secretToken);
} finally {
if ($previous !== false) {
ini_set('error_log', $previous);
}
@unlink($logFile);
}
});

it('falls back to the default timeouts when the configured values are non-positive', function(): void {
config()->set('report-tool.connect_timeout', 0);
config()->set('report-tool.timeout', 'nonsense');
Expand Down