From b036533f5423fdf0c98fe15b1527cb23a07622c1 Mon Sep 17 00:00:00 2001 From: wadakatu Date: Fri, 20 Feb 2026 10:05:02 +0900 Subject: [PATCH 1/3] feat(laravel): add config-based default spec with service provider Replace the abstract openApiSpec() method with a concrete method that reads from Laravel config, allowing users to set a project-wide default spec via the openapi-contract-testing.default_spec config key. Add a service provider with publishable config for Laravel auto-discovery. --- composer.json | 7 ++++++ phpstan.neon.dist | 8 ++++++- .../OpenApiContractTestingServiceProvider.php | 22 +++++++++++++++++++ src/Laravel/ValidatesOpenApiSchema.php | 5 ++++- src/Laravel/config.php | 7 ++++++ 5 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 src/Laravel/OpenApiContractTestingServiceProvider.php create mode 100644 src/Laravel/config.php diff --git a/composer.json b/composer.json index c95d050..3a8395a 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,13 @@ "dealerdirect/phpcodesniffer-composer-installer": true } }, + "extra": { + "laravel": { + "providers": [ + "Studio\\OpenApiContractTesting\\Laravel\\OpenApiContractTestingServiceProvider" + ] + } + }, "minimum-stability": "stable", "prefer-stable": true } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 2d8f8fc..cc43028 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -11,11 +11,17 @@ parameters: - message: '#Call to an undefined method Illuminate\\Testing\\TestResponse::#' path: src/Laravel/ValidatesOpenApiSchema.php + - + message: '#Function config_path not found#' + path: src/Laravel/OpenApiContractTestingServiceProvider.php - message: '#does not specify its types: TResponse#' paths: - src/Laravel/ValidatesOpenApiSchema.php - tests/Unit/ValidatesOpenApiSchemaTest.php + - tests/Unit/ValidatesOpenApiSchemaDefaultSpecTest.php - message: '#expects TResponse of Symfony\\Component\\HttpFoundation\\Response#' - path: tests/Unit/ValidatesOpenApiSchemaTest.php + paths: + - tests/Unit/ValidatesOpenApiSchemaTest.php + - tests/Unit/ValidatesOpenApiSchemaDefaultSpecTest.php diff --git a/src/Laravel/OpenApiContractTestingServiceProvider.php b/src/Laravel/OpenApiContractTestingServiceProvider.php new file mode 100644 index 0000000..c946e05 --- /dev/null +++ b/src/Laravel/OpenApiContractTestingServiceProvider.php @@ -0,0 +1,22 @@ +mergeConfigFrom(__DIR__ . '/config.php', 'openapi-contract-testing'); + } + + public function boot(): void + { + $this->publishes([ + __DIR__ . '/config.php' => config_path('openapi-contract-testing.php'), + ], 'openapi-contract-testing'); + } +} diff --git a/src/Laravel/ValidatesOpenApiSchema.php b/src/Laravel/ValidatesOpenApiSchema.php index 52e405f..30ff3cb 100644 --- a/src/Laravel/ValidatesOpenApiSchema.php +++ b/src/Laravel/ValidatesOpenApiSchema.php @@ -11,7 +11,10 @@ trait ValidatesOpenApiSchema { - abstract protected function openApiSpec(): string; + protected function openApiSpec(): string + { + return config('openapi-contract-testing.default_spec', ''); + } protected function assertResponseMatchesOpenApiSchema( TestResponse $response, diff --git a/src/Laravel/config.php b/src/Laravel/config.php new file mode 100644 index 0000000..d9cf9f4 --- /dev/null +++ b/src/Laravel/config.php @@ -0,0 +1,7 @@ + '', +]; From 9ca80d800bb29b861de627238f81ec2665185749 Mon Sep 17 00:00:00 2001 From: wadakatu Date: Fri, 20 Feb 2026 10:11:19 +0900 Subject: [PATCH 2/3] test(laravel): add tests for config-based default openApiSpec() behavior Add namespace-level config() mock helper and unit tests verifying that the default openApiSpec() method reads from Laravel config, returns an empty string when unconfigured, and fails with a clear message when the spec name is empty. --- tests/Helpers/LaravelConfigMock.php | 17 +++ .../ValidatesOpenApiSchemaDefaultSpecTest.php | 111 ++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 tests/Helpers/LaravelConfigMock.php create mode 100644 tests/Unit/ValidatesOpenApiSchemaDefaultSpecTest.php diff --git a/tests/Helpers/LaravelConfigMock.php b/tests/Helpers/LaravelConfigMock.php new file mode 100644 index 0000000..90024f8 --- /dev/null +++ b/tests/Helpers/LaravelConfigMock.php @@ -0,0 +1,17 @@ +assertSame('', $this->openApiSpec()); + } + + #[Test] + public function default_open_api_spec_returns_config_value(): void + { + $GLOBALS['__openapi_testing_config']['openapi-contract-testing.default_spec'] = 'petstore-3.0'; + + $this->assertSame('petstore-3.0', $this->openApiSpec()); + } + + #[Test] + public function empty_config_default_spec_fails_with_clear_message(): void + { + $response = $this->makeTestResponse('{}', 200); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('openApiSpec() must return a non-empty spec name'); + + $this->assertResponseMatchesOpenApiSchema( + $response, + HttpMethod::GET, + '/v1/pets', + ); + } + + #[Test] + public function configured_default_spec_validates_successfully(): void + { + $GLOBALS['__openapi_testing_config']['openapi-contract-testing.default_spec'] = 'petstore-3.0'; + + $body = (string) json_encode( + ['data' => [['id' => 1, 'name' => 'Fido', 'tag' => null]]], + JSON_THROW_ON_ERROR, + ); + $response = $this->makeTestResponse($body, 200); + + $this->assertResponseMatchesOpenApiSchema( + $response, + HttpMethod::GET, + '/v1/pets', + ); + } + + private function makeTestResponse(string $content, int $statusCode): TestResponse + { + $baseResponse = new class ($content, $statusCode) { + public function __construct( + private readonly string $content, + private readonly int $statusCode, + ) {} + + public function getContent(): string + { + return $this->content; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } + }; + + return new TestResponse($baseResponse); + } +} From 65943329b869ce2bc53fe67906970d603004c636 Mon Sep 17 00:00:00 2001 From: wadakatu Date: Fri, 20 Feb 2026 10:46:01 +0900 Subject: [PATCH 3/3] fix(laravel): address PR review feedback for config-based default spec - Validate config() return type with is_string guard for type safety - Improve error message to mention config key and override option - Update CLAUDE.md Laravel Integration section for new architecture - Improve LaravelConfigMock docblock (correct package attribution, explain namespace mocking rationale, warn about use function imports) - Extract shared CreatesTestResponse trait from duplicated helpers - Add null config value test and update error message assertions - Add explicit config() ignore in phpstan.neon.dist for trait file --- CLAUDE.md | 3 +- phpstan.neon.dist | 2 +- src/Laravel/ValidatesOpenApiSchema.php | 16 +++++++- tests/Helpers/CreatesTestResponse.php | 32 +++++++++++++++ tests/Helpers/LaravelConfigMock.php | 16 +++++++- .../ValidatesOpenApiSchemaDefaultSpecTest.php | 40 ++++++++----------- tests/Unit/ValidatesOpenApiSchemaTest.php | 25 +----------- 7 files changed, 81 insertions(+), 53 deletions(-) create mode 100644 tests/Helpers/CreatesTestResponse.php diff --git a/CLAUDE.md b/CLAUDE.md index bf8d936..118a045 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,7 +49,8 @@ This is a PHP library (`studio-design/openapi-contract-testing`) that validates ### Laravel Integration -- **`ValidatesOpenApiSchema`** trait — Used in Laravel test cases. Provides `assertResponseMatchesOpenApiSchema()` which auto-resolves method/path from the current request and records coverage. Requires implementing `openApiSpec(): string` to specify which spec to validate against. +- **`ValidatesOpenApiSchema`** trait — Used in Laravel test cases. Provides `assertResponseMatchesOpenApiSchema()` which auto-resolves method/path from the current request and records coverage. The default spec name is read from `config('openapi-contract-testing.default_spec')`; override `openApiSpec(): string` per-test-class if needed. +- **`OpenApiContractTestingServiceProvider`** — Auto-discovered service provider that registers and publishes the `openapi-contract-testing` config file (`default_spec` key). ### Key Enums diff --git a/phpstan.neon.dist b/phpstan.neon.dist index cc43028..33debca 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -6,7 +6,7 @@ parameters: tmpDir: .phpstan.cache ignoreErrors: - - message: '#Function app not found#' + message: '#Function (app|config) not found#' path: src/Laravel/ValidatesOpenApiSchema.php - message: '#Call to an undefined method Illuminate\\Testing\\TestResponse::#' diff --git a/src/Laravel/ValidatesOpenApiSchema.php b/src/Laravel/ValidatesOpenApiSchema.php index 30ff3cb..fdc5e01 100644 --- a/src/Laravel/ValidatesOpenApiSchema.php +++ b/src/Laravel/ValidatesOpenApiSchema.php @@ -9,11 +9,19 @@ use Studio\OpenApiContractTesting\OpenApiCoverageTracker; use Studio\OpenApiContractTesting\OpenApiResponseValidator; +use function is_string; + trait ValidatesOpenApiSchema { protected function openApiSpec(): string { - return config('openapi-contract-testing.default_spec', ''); + $spec = config('openapi-contract-testing.default_spec'); + + if (!is_string($spec) || $spec === '') { + return ''; + } + + return $spec; } protected function assertResponseMatchesOpenApiSchema( @@ -23,7 +31,11 @@ protected function assertResponseMatchesOpenApiSchema( ): void { $specName = $this->openApiSpec(); if ($specName === '') { - $this->fail('openApiSpec() must return a non-empty spec name, but an empty string was returned.'); + $this->fail( + 'openApiSpec() must return a non-empty spec name, but an empty string was returned. ' + . 'Either override openApiSpec() in your test class, or set the "default_spec" key ' + . 'in config/openapi-contract-testing.php.', + ); } $resolvedMethod = $method !== null ? $method->value : app('request')->getMethod(); diff --git a/tests/Helpers/CreatesTestResponse.php b/tests/Helpers/CreatesTestResponse.php new file mode 100644 index 0000000..06afbb7 --- /dev/null +++ b/tests/Helpers/CreatesTestResponse.php @@ -0,0 +1,32 @@ +content; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } + }; + + return new TestResponse($baseResponse); + } +} diff --git a/tests/Helpers/LaravelConfigMock.php b/tests/Helpers/LaravelConfigMock.php index 90024f8..eefe1ac 100644 --- a/tests/Helpers/LaravelConfigMock.php +++ b/tests/Helpers/LaravelConfigMock.php @@ -7,9 +7,21 @@ /** * Namespace-level config() mock for unit testing. * + * This library does not depend on laravel/framework, so the global \config() + * helper is unavailable during unit tests. This namespaced function acts as a + * lightweight substitute. + * * PHP resolves unqualified function calls by checking the current namespace first, - * so this takes priority over the global \config() from illuminate/support - * when called from within the Studio\OpenApiContractTesting\Laravel namespace. + * then falling back to the global namespace. Because the ValidatesOpenApiSchema trait + * lives in Studio\OpenApiContractTesting\Laravel and calls config() without a leading + * backslash, this function takes priority over any global \config() that might exist + * at runtime. + * + * IMPORTANT: This relies on config() being called as an unqualified function + * in ValidatesOpenApiSchema.php (i.e., no "use function config" import). + * Adding such an import would bypass namespace resolution and break this mock. + * + * Test values are read from $GLOBALS['__openapi_testing_config']. */ function config(string $key, mixed $default = null): mixed { diff --git a/tests/Unit/ValidatesOpenApiSchemaDefaultSpecTest.php b/tests/Unit/ValidatesOpenApiSchemaDefaultSpecTest.php index b55ec73..a6eb5fc 100644 --- a/tests/Unit/ValidatesOpenApiSchemaDefaultSpecTest.php +++ b/tests/Unit/ValidatesOpenApiSchemaDefaultSpecTest.php @@ -6,7 +6,6 @@ use const JSON_THROW_ON_ERROR; -use Illuminate\Testing\TestResponse; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -14,13 +13,16 @@ use Studio\OpenApiContractTesting\Laravel\ValidatesOpenApiSchema; use Studio\OpenApiContractTesting\OpenApiCoverageTracker; use Studio\OpenApiContractTesting\OpenApiSpecLoader; +use Studio\OpenApiContractTesting\Tests\Helpers\CreatesTestResponse; use function json_encode; +// Load namespace-level config() mock before the trait resolves the function call. require_once __DIR__ . '/../Helpers/LaravelConfigMock.php'; class ValidatesOpenApiSchemaDefaultSpecTest extends TestCase { + use CreatesTestResponse; use ValidatesOpenApiSchema; protected function setUp(): void @@ -54,13 +56,25 @@ public function default_open_api_spec_returns_config_value(): void $this->assertSame('petstore-3.0', $this->openApiSpec()); } + #[Test] + public function null_config_value_returns_empty_string(): void + { + $GLOBALS['__openapi_testing_config']['openapi-contract-testing.default_spec'] = null; + + $this->assertSame('', $this->openApiSpec()); + } + #[Test] public function empty_config_default_spec_fails_with_clear_message(): void { $response = $this->makeTestResponse('{}', 200); $this->expectException(AssertionFailedError::class); - $this->expectExceptionMessage('openApiSpec() must return a non-empty spec name'); + $this->expectExceptionMessage( + 'openApiSpec() must return a non-empty spec name, but an empty string was returned. ' + . 'Either override openApiSpec() in your test class, or set the "default_spec" key ' + . 'in config/openapi-contract-testing.php.', + ); $this->assertResponseMatchesOpenApiSchema( $response, @@ -86,26 +100,4 @@ public function configured_default_spec_validates_successfully(): void '/v1/pets', ); } - - private function makeTestResponse(string $content, int $statusCode): TestResponse - { - $baseResponse = new class ($content, $statusCode) { - public function __construct( - private readonly string $content, - private readonly int $statusCode, - ) {} - - public function getContent(): string - { - return $this->content; - } - - public function getStatusCode(): int - { - return $this->statusCode; - } - }; - - return new TestResponse($baseResponse); - } } diff --git a/tests/Unit/ValidatesOpenApiSchemaTest.php b/tests/Unit/ValidatesOpenApiSchemaTest.php index 3d3466e..6038d34 100644 --- a/tests/Unit/ValidatesOpenApiSchemaTest.php +++ b/tests/Unit/ValidatesOpenApiSchemaTest.php @@ -6,7 +6,6 @@ use const JSON_THROW_ON_ERROR; -use Illuminate\Testing\TestResponse; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -14,11 +13,13 @@ use Studio\OpenApiContractTesting\Laravel\ValidatesOpenApiSchema; use Studio\OpenApiContractTesting\OpenApiCoverageTracker; use Studio\OpenApiContractTesting\OpenApiSpecLoader; +use Studio\OpenApiContractTesting\Tests\Helpers\CreatesTestResponse; use function json_encode; class ValidatesOpenApiSchemaTest extends TestCase { + use CreatesTestResponse; use ValidatesOpenApiSchema; protected function setUp(): void @@ -146,26 +147,4 @@ protected function openApiSpec(): string { return 'petstore-3.0'; } - - private function makeTestResponse(string $content, int $statusCode): TestResponse - { - $baseResponse = new class ($content, $statusCode) { - public function __construct( - private readonly string $content, - private readonly int $statusCode, - ) {} - - public function getContent(): string - { - return $this->content; - } - - public function getStatusCode(): int - { - return $this->statusCode; - } - }; - - return new TestResponse($baseResponse); - } }