hotmeteor / spectator

OpenAPI testing for PHP
MIT License
285 stars 53 forks source link

Responses using application/problem+json content type cannot be decoded #103

Closed ethanbray closed 2 years ago

ethanbray commented 2 years ago

I currently have several endpoints that can return possible errors/non-200 status codes. When these errors occur, the response Content-Type header is set to application/problem+json per RFC 7807.

Attempting to validate one of these responses causes this error to be thrown

[{
    "exception": "Spectator\\Exceptions\\ResponseValidationException",
    "message": "Unable to map [application/problem+json] to schema type [object].",
    "specErrors": []
}]

I tracked this down to the body method of ResponseValidator

   protected function body($contentType, $schemaType)
    {
        $body = $this->response->getContent();

        if (in_array($schemaType, ['object', 'array', 'allOf', 'anyOf', 'oneOf'], true)) {
            if (in_array($contentType, ['application/json', 'application/vnd.api+json'])) {
                return json_decode($body);
            } else {
                throw new ResponseValidationException("Unable to map [{$contentType}] to schema type [object].");
            }
        }

        return $body;
    }

From my perspective, application/problem+json should be added to the content type check and decoded as normal.

ethanbray commented 2 years ago

@hotmeteor Any chance you could review the linked pull request? This is currently stopping us from contract testing our error cases!

philsturgeon commented 2 years ago

@ethanbray I was getting a different error with problem JSON and your fork hasn't changed it.

^ {#5230
  +"exception": "Spectator\Exceptions\ResponseValidationException"
  +"message": """
    Response did not match any specified content type.\n
    \n
      Expected: application/json\n
      Actual: DNE\n
    \n
      ---
    """
  +"specErrors": []
}

Test:

it('returns a 404 for invalid record', function () {
    $non_existent_uuid = '53d4faeb-e046-4ab1-91ff-6b6e35c4c052';
    $this
        ->withHeaders(['Authorization' => 'Bearer '.$this->api_key])
        ->getJson(sprintf('/orders/%s', $non_existent_uuid))
        ->dump()
        ->assertValidResponse(404);
});

OpenAPI:


  '/orders/{order}':
    parameters:
      - in: path
        name: order
        required: true
        schema:
          type: string
          format: uuid
          example: 53f7bd7b-2ee2-4be4-a15b-e09c76a150f9
    get:
      operationId: get-orders-uuid
      summary: Get Order
      description: 'Orders are made by organizations to fund tree planting, and when trees are planted those orders will be marked as fullfilled. Orders may be partially fulfilled, and it is up to you if you wish to import those. Make sure to deduplicate the trees if you do.'
      tags:
        - Order
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '401':
          description: Unauthorized
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Generic_Problem'
        '404':
          description: Organization not found.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Generic_Problem'

I tried switching the $ref to just a type: object but thats not making any difference.