Skip to content

Commit e9b5b48

Browse files
committed
test: strengthen maxErrors assertions and add edge-case tests
- Use 50-item payloads to prove truncation works in maxErrors tests - Add capped vs unlimited comparison, boundary, and negative input tests - Add Laravel trait tests for max_errors config, string casting, and fallback
1 parent 6c247f7 commit e9b5b48

2 files changed

Lines changed: 164 additions & 7 deletions

File tree

tests/Unit/OpenApiResponseValidatorTest.php

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44

55
namespace Studio\OpenApiContractTesting\Tests\Unit;
66

7+
use InvalidArgumentException;
78
use PHPUnit\Framework\Attributes\Test;
89
use PHPUnit\Framework\TestCase;
910
use Studio\OpenApiContractTesting\OpenApiResponseValidator;
1011
use Studio\OpenApiContractTesting\OpenApiSpecLoader;
1112

13+
use function array_map;
1214
use function count;
15+
use function range;
1316

1417
class OpenApiResponseValidatorTest extends TestCase
1518
{
@@ -610,6 +613,40 @@ public function default_max_errors_reports_multiple_errors(): void
610613
$this->assertGreaterThan(1, count($result->errors()));
611614
}
612615

616+
#[Test]
617+
public function max_errors_caps_reported_errors_to_configured_limit(): void
618+
{
619+
$items = array_map(
620+
static fn(int $i) => ['id' => 'str-' . $i, 'name' => $i],
621+
range(1, 50),
622+
);
623+
624+
$capped = new OpenApiResponseValidator(maxErrors: 5);
625+
$cappedResult = $capped->validate(
626+
'petstore-3.0',
627+
'GET',
628+
'/v1/pets',
629+
200,
630+
['data' => $items],
631+
);
632+
633+
$unlimited = new OpenApiResponseValidator(maxErrors: 0);
634+
$unlimitedResult = $unlimited->validate(
635+
'petstore-3.0',
636+
'GET',
637+
'/v1/pets',
638+
200,
639+
['data' => $items],
640+
);
641+
642+
$this->assertFalse($cappedResult->isValid());
643+
$this->assertFalse($unlimitedResult->isValid());
644+
$this->assertLessThan(
645+
count($unlimitedResult->errors()),
646+
count($cappedResult->errors()),
647+
);
648+
}
649+
613650
#[Test]
614651
public function max_errors_one_limits_to_single_error(): void
615652
{
@@ -631,25 +668,55 @@ public function max_errors_one_limits_to_single_error(): void
631668
$this->assertCount(1, $result->errors());
632669
}
633670

671+
#[Test]
672+
public function max_errors_two_reports_more_than_one_error(): void
673+
{
674+
$items = array_map(
675+
static fn(int $i) => ['id' => 'str-' . $i, 'name' => $i],
676+
range(1, 50),
677+
);
678+
679+
$validator = new OpenApiResponseValidator(maxErrors: 2);
680+
$result = $validator->validate(
681+
'petstore-3.0',
682+
'GET',
683+
'/v1/pets',
684+
200,
685+
['data' => $items],
686+
);
687+
688+
$this->assertFalse($result->isValid());
689+
$this->assertGreaterThan(1, count($result->errors()));
690+
}
691+
634692
#[Test]
635693
public function max_errors_zero_reports_all_errors(): void
636694
{
695+
$items = array_map(
696+
static fn(int $i) => ['id' => 'str-' . $i, 'name' => $i],
697+
range(1, 50),
698+
);
699+
637700
$validator = new OpenApiResponseValidator(maxErrors: 0);
638701
$result = $validator->validate(
639702
'petstore-3.0',
640703
'GET',
641704
'/v1/pets',
642705
200,
643-
[
644-
'data' => [
645-
['id' => 'not-an-int', 'name' => 123],
646-
['id' => 'also-not-an-int', 'name' => 456],
647-
],
648-
],
706+
['data' => $items],
649707
);
650708

651709
$this->assertFalse($result->isValid());
652-
$this->assertGreaterThan(1, count($result->errors()));
710+
$this->assertGreaterThan(20, count($result->errors()));
711+
}
712+
713+
#[Test]
714+
public function negative_max_errors_throws_exception(): void
715+
{
716+
$this->expectException(InvalidArgumentException::class);
717+
$this->expectExceptionMessage('maxErrors must be 0 (unlimited) or a positive integer, got -1.');
718+
719+
new OpenApiResponseValidator(maxErrors: -1);
653720
}
654721

655722
// ========================================

tests/Unit/ValidatesOpenApiSchemaDefaultSpecTest.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
use Studio\OpenApiContractTesting\OpenApiSpecLoader;
1616
use Studio\OpenApiContractTesting\Tests\Helpers\CreatesTestResponse;
1717

18+
use function array_filter;
19+
use function count;
20+
use function explode;
1821
use function json_encode;
22+
use function str_starts_with;
23+
use function trim;
1924

2025
// Load namespace-level config() mock before the trait resolves the function call.
2126
require_once __DIR__ . '/../Helpers/LaravelConfigMock.php';
@@ -100,4 +105,89 @@ public function configured_default_spec_validates_successfully(): void
100105
'/v1/pets',
101106
);
102107
}
108+
109+
// ========================================
110+
// max_errors config tests
111+
// ========================================
112+
113+
#[Test]
114+
public function max_errors_config_limits_reported_errors(): void
115+
{
116+
$GLOBALS['__openapi_testing_config']['openapi-contract-testing.default_spec'] = 'petstore-3.0';
117+
$GLOBALS['__openapi_testing_config']['openapi-contract-testing.max_errors'] = 1;
118+
119+
$body = (string) json_encode(
120+
['data' => [['id' => 'bad', 'name' => 123], ['id' => 'bad', 'name' => 456]]],
121+
JSON_THROW_ON_ERROR,
122+
);
123+
$response = $this->makeTestResponse($body, 200);
124+
125+
try {
126+
$this->assertResponseMatchesOpenApiSchema(
127+
$response,
128+
HttpMethod::GET,
129+
'/v1/pets',
130+
);
131+
$this->fail('Expected AssertionFailedError was not thrown.');
132+
} catch (AssertionFailedError $e) {
133+
// With max_errors=1, the error message should contain exactly one schema error line.
134+
// The error format is "[path] message", so count lines starting with "[".
135+
$lines = explode("\n", $e->getMessage());
136+
$errorLines = array_filter($lines, static fn(string $line) => str_starts_with(trim($line), '['));
137+
$this->assertCount(1, $errorLines);
138+
}
139+
}
140+
141+
#[Test]
142+
public function string_numeric_max_errors_config_is_cast_to_int(): void
143+
{
144+
$GLOBALS['__openapi_testing_config']['openapi-contract-testing.default_spec'] = 'petstore-3.0';
145+
$GLOBALS['__openapi_testing_config']['openapi-contract-testing.max_errors'] = '1';
146+
147+
$body = (string) json_encode(
148+
['data' => [['id' => 'bad', 'name' => 123], ['id' => 'bad', 'name' => 456]]],
149+
JSON_THROW_ON_ERROR,
150+
);
151+
$response = $this->makeTestResponse($body, 200);
152+
153+
try {
154+
$this->assertResponseMatchesOpenApiSchema(
155+
$response,
156+
HttpMethod::GET,
157+
'/v1/pets',
158+
);
159+
$this->fail('Expected AssertionFailedError was not thrown.');
160+
} catch (AssertionFailedError $e) {
161+
$lines = explode("\n", $e->getMessage());
162+
$errorLines = array_filter($lines, static fn(string $line) => str_starts_with(trim($line), '['));
163+
$this->assertCount(1, $errorLines);
164+
}
165+
}
166+
167+
#[Test]
168+
public function non_numeric_max_errors_config_falls_back_to_default(): void
169+
{
170+
$GLOBALS['__openapi_testing_config']['openapi-contract-testing.default_spec'] = 'petstore-3.0';
171+
$GLOBALS['__openapi_testing_config']['openapi-contract-testing.max_errors'] = 'not-a-number';
172+
173+
$body = (string) json_encode(
174+
['data' => [['id' => 'bad', 'name' => 123], ['id' => 'bad', 'name' => 456]]],
175+
JSON_THROW_ON_ERROR,
176+
);
177+
$response = $this->makeTestResponse($body, 200);
178+
179+
try {
180+
$this->assertResponseMatchesOpenApiSchema(
181+
$response,
182+
HttpMethod::GET,
183+
'/v1/pets',
184+
);
185+
$this->fail('Expected AssertionFailedError was not thrown.');
186+
} catch (AssertionFailedError $e) {
187+
// Falls back to default of 20, so multiple errors should be reported
188+
$lines = explode("\n", $e->getMessage());
189+
$errorLines = array_filter($lines, static fn(string $line) => str_starts_with(trim($line), '['));
190+
$this->assertGreaterThan(1, count($errorLines));
191+
}
192+
}
103193
}

0 commit comments

Comments
 (0)