Skip to content

Commit f8c4056

Browse files
committed
fix(mock-server): fix mock response handling for 204 and 201 status codes
1 parent 53b9ae2 commit f8c4056

5 files changed

Lines changed: 160 additions & 7 deletions

File tree

data/openapi.yaml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,42 @@ paths:
4141
example:
4242
id: 1
4343
name: Alice
44+
/users/{id}/tasks:
45+
post:
46+
summary: Create a task for a user (async)
47+
parameters:
48+
- name: id
49+
in: path
50+
required: true
51+
schema:
52+
type: integer
53+
requestBody:
54+
required: true
55+
content:
56+
application/json:
57+
schema:
58+
$ref: '#/components/schemas/TaskInput'
59+
responses:
60+
'202':
61+
description: Task accepted for processing
62+
content:
63+
application/json:
64+
schema:
65+
$ref: '#/components/schemas/TaskAccepted'
66+
example:
67+
taskId: abc-123
68+
status: pending
69+
delete:
70+
summary: Delete all tasks for a user
71+
parameters:
72+
- name: id
73+
in: path
74+
required: true
75+
schema:
76+
type: integer
77+
responses:
78+
'204':
79+
description: Tasks deleted successfully
4480
components:
4581
schemas:
4682
User:
@@ -50,3 +86,17 @@ components:
5086
type: integer
5187
name:
5288
type: string
89+
TaskInput:
90+
type: object
91+
properties:
92+
title:
93+
type: string
94+
required:
95+
- title
96+
TaskAccepted:
97+
type: object
98+
properties:
99+
taskId:
100+
type: string
101+
status:
102+
type: string

src/Middleware/MockMiddleware/Faker/OpenAPIFaker.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,20 @@ public function setOptions(array $options): self
165165
return $this;
166166
}
167167

168+
public function hasResponse(
169+
string $path,
170+
string $method,
171+
string $statusCode = '200',
172+
): bool {
173+
try {
174+
$operation = $this->findOperation($path, HttpMethod::fromString($method));
175+
} catch (NoPath) {
176+
return false;
177+
}
178+
179+
return $operation->responses !== null && $operation->responses->hasResponse($statusCode);
180+
}
181+
168182
/**
169183
* @return list<string>
170184
*/

src/Middleware/MockMiddleware/Request/RequestHandler.php

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,26 @@
55
namespace WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Request;
66

77
use cebe\openapi\spec\OpenApi;
8-
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Exception\RoutingException;
9-
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Exception\SecurityException;
10-
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Exception\ValidationException;
11-
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Response\ResponseFaker;
12-
use Exception;
13-
use InvalidArgumentException;
148
use League\OpenAPIValidation\PSR7\Exception\NoOperation;
159
use League\OpenAPIValidation\PSR7\Exception\NoPath;
1610
use League\OpenAPIValidation\PSR7\Exception\NoResponseCode;
1711
use League\OpenAPIValidation\PSR7\Exception\Validation\InvalidSecurity;
1812
use League\OpenAPIValidation\PSR7\Exception\ValidationFailed;
1913
use League\OpenAPIValidation\PSR7\OperationAddress;
14+
use League\OpenAPIValidation\PSR7\SpecFinder;
2015
use Psr\Http\Message\ResponseInterface;
2116
use Throwable;
17+
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Exception\RoutingException;
18+
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Exception\SecurityException;
19+
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Exception\ValidationException;
2220
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Faker\Exception\NoPath as FakerNoPath;
21+
use WebProject\PhpOpenApiMockServer\Middleware\MockMiddleware\Response\ResponseFaker;
22+
23+
use function array_keys;
24+
use function preg_match;
25+
use function sort;
26+
27+
use InvalidArgumentException;
2328

2429
class RequestHandler
2530
{
@@ -39,7 +44,13 @@ public function handleValidRequest(
3944
?string $statusCode = null,
4045
?string $exampleName = null
4146
): ResponseInterface {
42-
return $this->responseFaker->mock($openApi, $operationAddress, $statusCode ?? ['200', '201'], $acceptedContentTypes, $exampleName);
47+
return $this->responseFaker->mock(
48+
$openApi,
49+
$operationAddress,
50+
$statusCode ?? $this->getSuccessStatusCodes($openApi, $operationAddress),
51+
$acceptedContentTypes,
52+
$exampleName
53+
);
4354
}
4455

4556
/**
@@ -133,4 +144,45 @@ public function handleValidationFailedRequest(
133144
return $this->responseFaker->handleException(ValidationException::forUnprocessableEntity($throwable), $acceptedContentTypes[0] ?? 'application/json');
134145
}
135146
}
147+
148+
/**
149+
* Extract all 2xx status codes defined in the spec for the given operation,
150+
* sorted ascending. Falls back to ['200', '201'] if none are found.
151+
*
152+
* @return list<string>
153+
*/
154+
private function getSuccessStatusCodes(OpenApi $openApi, OperationAddress $operationAddress): array
155+
{
156+
try {
157+
$operation = (new SpecFinder($openApi))
158+
->findOperationSpec($operationAddress);
159+
} catch (Throwable) {
160+
return ['200', '201'];
161+
}
162+
163+
if ($operation->responses === null) {
164+
return ['200', '201'];
165+
}
166+
167+
$codes = [];
168+
foreach (array_keys($operation->responses->getResponses()) as $code) {
169+
$code = (string) $code;
170+
if (preg_match('/^2\d{2}$/', $code) === 1) {
171+
$codes[] = $code;
172+
}
173+
}
174+
175+
sort($codes);
176+
177+
if ($codes === []) {
178+
// No 2xx codes defined, check for 'default' response
179+
if ($operation->responses->hasResponse('default')) {
180+
return ['default'];
181+
}
182+
183+
return ['200', '201'];
184+
}
185+
186+
return $codes;
187+
}
136188
}

src/Middleware/MockMiddleware/Response/ResponseFaker.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ private function mockResponse(
112112
$path = $operationAddress->path();
113113
$method = $operationAddress->method();
114114

115+
if ($this->isNoContentResponse($openAPIFaker, $path, $method, $statusCode)) {
116+
return $this->responseFactory->createResponse((int) $statusCode);
117+
}
118+
115119
$contentType = $this->negotiateContentType($openAPIFaker, $path, $method, $statusCode, $acceptedContentTypes);
116120

117121
$fakeData = null !== $exampleName
@@ -154,6 +158,22 @@ private function negotiateContentType(
154158
return $availableTypes[0];
155159
}
156160

161+
/**
162+
* Check if the response exists in the spec but defines no content (e.g. 204 No Content).
163+
*/
164+
private function isNoContentResponse(
165+
OpenAPIFaker $openAPIFaker,
166+
string $path,
167+
string $method,
168+
string $statusCode,
169+
): bool {
170+
if (! $openAPIFaker->hasResponse($path, $method, $statusCode)) {
171+
return false;
172+
}
173+
174+
return $openAPIFaker->getAvailableResponseContentTypes($path, $method, $statusCode) === [];
175+
}
176+
157177
private function createFaker(OpenApi $openApi): OpenAPIFaker
158178
{
159179
if ($this->openAPIFaker instanceof OpenAPIFaker) {

tests/Acceptance/MockServerCest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ public function testNotFoundInSpecButExistsInMezzio(AcceptanceTester $acceptance
4848
$acceptanceTester->seeResponseContainsJson(['message' => 'OpenAPI Mock Server is running!']);
4949
}
5050

51+
public function testPostReturns202Accepted(AcceptanceTester $acceptanceTester): void
52+
{
53+
$acceptanceTester->haveHttpHeader('Content-Type', 'application/json');
54+
$acceptanceTester->sendPost('/users/1/tasks', ['title' => 'My Task']);
55+
$acceptanceTester->seeResponseCodeIs(202);
56+
$acceptanceTester->seeResponseIsJson();
57+
58+
$response = json_decode($acceptanceTester->grabResponse(), true);
59+
$acceptanceTester->assertIsArray($response);
60+
}
61+
62+
public function testDeleteReturns204NoContent(AcceptanceTester $acceptanceTester): void
63+
{
64+
$acceptanceTester->sendDelete('/users/1/tasks');
65+
$acceptanceTester->seeResponseCodeIs(204);
66+
}
67+
5168
public function testDisableMockViaHeader(AcceptanceTester $acceptanceTester): void
5269
{
5370
// If we explicitly set it to false, it should fall through to Mezzio's root handler

0 commit comments

Comments
 (0)