Skip to content

Commit abbf969

Browse files
committed
Merge remote-tracking branch 'origin/develop' into test/mutation-bundle-extension
# Conflicts: # CHANGELOG.md
2 parents a147524 + 643aa7b commit abbf969

9 files changed

Lines changed: 169 additions & 1 deletion

File tree

.github/workflows/php.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,37 @@ jobs:
8585
# binary's GPG signature endpoint failing) must not fail the build.
8686
fail_ci_if_error: false
8787
flags: unittests
88+
89+
mutation-tests:
90+
name: Mutation tests
91+
runs-on: ubuntu-latest
92+
steps:
93+
- uses: actions/checkout@v6
94+
95+
- name: Create docker network
96+
run: |
97+
docker network create frontend
98+
99+
# The GITHUB_* variables are passed into the container so Infection's
100+
# CI detection works: --logger-github emits inline PR annotations for
101+
# escaped mutants, and the Stryker dashboard logger (configured in
102+
# infection.json5) publishes the report on pushes to develop. The
103+
# minimum mutation score is enforced via minCoveredMsi in
104+
# infection.json5.
105+
- name: Run Infection
106+
env:
107+
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
108+
run: |
109+
docker compose run --rm phpfpm composer install
110+
docker compose run --rm \
111+
-e XDEBUG_MODE=coverage \
112+
-e GITHUB_ACTIONS \
113+
-e GITHUB_REF \
114+
-e GITHUB_HEAD_REF \
115+
-e GITHUB_SHA \
116+
-e GITHUB_REPOSITORY \
117+
-e GITHUB_EVENT_NAME \
118+
-e GITHUB_RUN_ID \
119+
-e GITHUB_SERVER_URL \
120+
-e STRYKER_DASHBOARD_API_KEY \
121+
phpfpm vendor/bin/infection --logger-github

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,8 @@ unit.xml
2121

2222
# Local Claude Code instructions (not part of the published package)
2323
/CLAUDE.md
24+
25+
###> infection/infection ###
26+
/infection.log
27+
/infection.html
28+
###< infection/infection ###

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Dev: mutation testing with [Infection](https://infection.github.io/)
13+
(`task test:mutation`). The minimum mutation score is configured in
14+
`infection.json5` and enforced in CI; escaped mutants are annotated inline
15+
on pull requests, and results for `develop` are published to the Stryker
16+
dashboard (mutation score badge in the README). No effect on the published
17+
package.
18+
1019
### Changed
1120

1221
- Dev: added a test for `ItkDevOpenIdConnectBundle::getContainerExtension()`
1322
asserting the custom extension is created and memoized (same instance on
1423
repeated calls), prompted by mutation testing findings. No effect on the
1524
published package.
25+
- Dev: strengthened DependencyInjection tests based on mutation testing
26+
findings — the extension's container wiring (cache pool reference,
27+
provider options mapping, CLI login route arguments) is now asserted
28+
explicitly, and the documented invariant that provider keys are not
29+
normalized (`my-provider``my_provider`) is covered by a test. No
30+
effect on the published package.
1631

1732
- CI: bumped `codecov/codecov-action` from `v5` to `v7` (restores Codecov's
1833
GPG signing key after the `codecovsecurity` account was removed, and moves

README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
[![PHP Version](https://img.shields.io/packagist/php-v/itk-dev/openid-connect-bundle.svg?style=flat-square&colorB=%238892BF)](https://www.php.net/downloads)
66
[![Build Status](https://img.shields.io/github/actions/workflow/status/itk-dev/openid-connect-bundle/php.yaml?branch=develop&label=CI&logo=github&style=flat-square)](https://github.com/itk-dev/openid-connect-bundle/actions/workflows/php.yaml?query=branch%3Adevelop)
77
[![Codecov Code Coverage](https://img.shields.io/codecov/c/gh/itk-dev/openid-connect-bundle?label=codecov&logo=codecov&style=flat-square)](https://codecov.io/gh/itk-dev/openid-connect-bundle)
8+
[![Mutation Score](https://img.shields.io/endpoint?style=flat-square&label=mutation%20score&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fitk-dev%2Fopenid-connect-bundle%2Fdevelop)](https://dashboard.stryker-mutator.io/reports/github.com/itk-dev/openid-connect-bundle/develop)
89
[![Read License](https://img.shields.io/packagist/l/itk-dev/openid-connect-bundle.svg?style=flat-square&colorB=darkcyan)](https://github.com/itk-dev/openid-connect-bundle/blob/master/LICENSE.md)
910
[![Package downloads on Packagist](https://img.shields.io/packagist/dt/itk-dev/openid-connect-bundle.svg?style=flat-square&colorB=darkmagenta)](https://packagist.org/packages/itk-dev/openid-connect-bundle/stats)
1011

@@ -472,6 +473,27 @@ task test:matrix
472473
This runs PHPUnit with coverage for each combination and prints a summary of
473474
pass/fail results.
474475

476+
### Mutation Testing
477+
478+
Line coverage shows which code the tests *execute*; mutation testing shows
479+
which code they actually *verify*. [Infection](https://infection.github.io/)
480+
applies small changes (mutants) to the source code — flipping a comparison,
481+
removing a method call — and runs the test suite against each one. If the
482+
tests still pass, the mutant "escaped": a potential bug the tests would not
483+
catch.
484+
485+
```shell
486+
task test:mutation
487+
```
488+
489+
The minimum mutation score (`minCoveredMsi`) is defined in `infection.json5`
490+
and enforced both locally and in CI — no command line flags needed. CI
491+
annotates escaped mutants inline on pull requests, and results for `develop`
492+
are published to the
493+
[Stryker dashboard](https://dashboard.stryker-mutator.io/reports/github.com/itk-dev/openid-connect-bundle/develop),
494+
which also feeds the mutation score badge above. Detailed reports are written
495+
to `infection.log` and `infection.html` on each run.
496+
475497
### PHPStan Static Analysis
476498

477499
```shell
@@ -510,7 +532,8 @@ Run `task --list` to see all available tasks.
510532

511533
## CI
512534

513-
GitHub Actions are used to run the test suite and code style checks on all PRs.
535+
GitHub Actions are used to run the test suite, mutation tests and code style
536+
checks on all PRs.
514537

515538
## Versioning
516539

Taskfile.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ tasks:
144144
cmds:
145145
- "{{.DOCKER_COMPOSE}} exec -e XDEBUG_MODE=coverage phpfpm vendor/bin/phpunit --coverage-text --coverage-clover=coverage/unit.xml"
146146

147+
test:mutation:
148+
desc: Run Infection mutation testing (threshold configured in infection.json5)
149+
cmds:
150+
- "{{.DOCKER_COMPOSE}} exec -e XDEBUG_MODE=coverage phpfpm vendor/bin/infection"
151+
147152
test:run:
148153
desc: "Run tests for a PHP version and dependency set (e.g. task test:run PHP=8.4 DEPS=lowest)"
149154
vars:
@@ -250,3 +255,4 @@ tasks:
250255
- task: lint
251256
- task: analyze:php
252257
- task: test:matrix
258+
- task: test:mutation

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"require-dev": {
2929
"ergebnis/composer-normalize": "^2.28",
3030
"friendsofphp/php-cs-fixer": "^3.11",
31+
"infection/infection": "*",
3132
"phpstan/phpstan": "^2.1.41",
3233
"phpstan/phpstan-deprecation-rules": "^2.0",
3334
"phpstan/phpstan-phpunit": "^2.0",
@@ -51,6 +52,7 @@
5152
"config": {
5253
"allow-plugins": {
5354
"ergebnis/composer-normalize": true,
55+
"infection/extension-installer": true,
5456
"symfony/runtime": true
5557
},
5658
"sort-packages": true

infection.json5

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "vendor/infection/infection/resources/schema.json",
3+
"source": {
4+
"directories": ["src"]
5+
},
6+
"threads": "max",
7+
// Minimum mutation score for covered code; enforced locally and in CI.
8+
// Baseline measured at 80% — ratchet up as surviving mutants are killed.
9+
"minCoveredMsi": 78,
10+
"logs": {
11+
"text": "infection.log",
12+
"html": "infection.html",
13+
// Publishes results for the develop branch to the Stryker dashboard
14+
// (feeds the README badge). Requires STRYKER_DASHBOARD_API_KEY; only
15+
// active on CI, skipped locally and on non-matching branches.
16+
"stryker": {
17+
"report": "develop"
18+
}
19+
},
20+
"mutators": {
21+
"@default": true
22+
}
23+
}

tests/DependencyInjection/ConfigurationTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,24 @@ public function testHttpClientOptionsRejectsUnknownKey(): void
163163
);
164164
}
165165

166+
public function testProviderKeysAreNotNormalized(): void
167+
{
168+
$input = $this->getMinimalConfig();
169+
$input['openid_providers']['my-provider'] = $input['openid_providers']['provider1'];
170+
unset($input['openid_providers']['provider1']);
171+
172+
$config = $this->processor->processConfiguration(
173+
$this->configuration,
174+
[$input]
175+
);
176+
177+
// Provider keys are part of the public contract ('my-provider' and
178+
// 'my_provider' are distinct providers), so dashes must survive
179+
// config processing instead of being normalized to underscores.
180+
$this->assertArrayHasKey('my-provider', $config['openid_providers']);
181+
$this->assertArrayNotHasKey('my_provider', $config['openid_providers']);
182+
}
183+
166184
public function testMultipleProviders(): void
167185
{
168186
$input = $this->getMinimalConfig();

tests/DependencyInjection/ItkDevOpenIdConnectExtensionTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,48 @@ public function testLoad(): void
4848
$this->assertTrue($container->hasDefinition(CliLoginTokenAuthenticator::class));
4949
}
5050

51+
public function testLoadWiresProviderManagerConfig(): void
52+
{
53+
$extension = new ItkDevOpenIdConnectExtension();
54+
$container = new ContainerBuilder();
55+
56+
$extension->load([$this->getBaseConfig()], $container);
57+
58+
$config = $container->getDefinition(OpenIdConfigurationProviderManager::class)->getArgument('$config');
59+
$this->assertIsArray($config);
60+
61+
$defaultOptions = $config['default_providers_options'] ?? null;
62+
$this->assertIsArray($defaultOptions);
63+
$cacheItemPool = $defaultOptions['cacheItemPool'] ?? null;
64+
$this->assertInstanceOf(Reference::class, $cacheItemPool);
65+
$this->assertSame('cache.app', (string) $cacheItemPool);
66+
67+
// Provider options must be keyed by provider name with the
68+
// intermediate 'options' level stripped.
69+
$providers = $config['providers'] ?? null;
70+
$this->assertIsArray($providers);
71+
$this->assertSame(['test_provider'], array_keys($providers));
72+
$provider = $providers['test_provider'];
73+
$this->assertIsArray($provider);
74+
$this->assertArrayNotHasKey('options', $provider);
75+
$this->assertSame('test_id', $provider['client_id']);
76+
}
77+
78+
public function testLoadWiresCacheAndCliLoginRoute(): void
79+
{
80+
$extension = new ItkDevOpenIdConnectExtension();
81+
$container = new ContainerBuilder();
82+
83+
$extension->load([$this->getBaseConfig()], $container);
84+
85+
$cache = $container->getDefinition(CliLoginHelper::class)->getArgument('$cache');
86+
$this->assertInstanceOf(Reference::class, $cache);
87+
$this->assertSame('cache.app', (string) $cache);
88+
89+
$this->assertSame('test_route', $container->getDefinition(UserLoginCommand::class)->getArgument('$cliLoginRoute'));
90+
$this->assertSame('test_route', $container->getDefinition(CliLoginTokenAuthenticator::class)->getArgument('$cliLoginRoute'));
91+
}
92+
5193
public function testLoadWithUserProvider(): void
5294
{
5395
$extension = new ItkDevOpenIdConnectExtension();

0 commit comments

Comments
 (0)