Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
* text=auto eol=lf

/.github export-ignore
/src export-ignore
/tests export-ignore
/.editorconfig export-ignore
/.gitattributes export-ignore
Expand Down
63 changes: 56 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,29 @@ See [this post](https://tech.osteel.me/posts/openapi-backed-api-testing-in-php-p

## Why?

[OpenAPI](https://swagger.io/specification/) is a specification intended to describe RESTful APIs in a way that is understood by humans and machines alike.
[OpenAPI](https://swagger.io/specification/) is a specification intended to describe RESTful APIs in a way that can be understood by both humans and machines.

By validating an API's requests and responses against the OpenAPI definition that describes it, we guarantee that the API is used correctly and behaves in accordance with the documentation we provide, thus making the OpenAPI definition the single source of truth.

The [HttpFoundation component](https://symfony.com/doc/current/components/http_foundation.html) is developed and maintained as part of the [Symfony framework](https://symfony.com/). It is used to handle HTTP requests and responses in projects such as Symfony, Laravel, Drupal, and [many others](https://symfony.com/components/HttpFoundation).

## How does it work?

This package is built upon the [OpenAPI PSR-7 Message Validator](https://github.com/thephpleague/openapi-psr7-validator) one, which validates [PSR-7 messages](https://www.php-fig.org/psr/psr-7/) against OpenAPI definitions.
This package is built on top of [OpenAPI PSR-7 Message Validator](https://github.com/thephpleague/openapi-psr7-validator), which validates [PSR-7 messages](https://www.php-fig.org/psr/psr-7/) against OpenAPI definitions.

It converts HttpFoundation request and response objects to PSR-7 messages using Symfony's [PSR-7 Bridge](https://symfony.com/doc/current/components/psr7.html) and [Tobias Nyholm](https://github.com/Nyholm)'s [PSR-7 implementation](https://github.com/Nyholm/psr7), before passing them on to OpenAPI PSR-7 Message Validator.

## Installation

> **Note**
> This package is mostly intended to be used as part of an API test suite.

Via Composer:

```bash
$ composer require --dev osteel/openapi-httpfoundation-testing
```

> **Note**
> This package is mostly intended to be used as part of an API test suite.

## Usage

Import the builder class:
Expand All @@ -45,7 +45,7 @@ Import the builder class:
use Osteel\OpenApi\Testing\ValidatorBuilder;
```

Use the builder to create a `\Osteel\OpenApi\Testing\Validator` object, using one of the available factory methods for YAML or JSON:
Use the builder to create a [`\Osteel\OpenApi\Testing\Validator`](/src/Validator.php) object, using one of the available factory methods for YAML or JSON:

```php
// From a file:
Expand All @@ -64,6 +64,9 @@ $validator = ValidatorBuilder::fromYaml($yamlFileOrString)->getValidator();
$validator = ValidatorBuilder::fromJson($jsonFileOrString)->getValidator();
```

> **Note**
> You can also use a dependency injection container to bind the `ValidatorBuilder` class to the [`ValidatorBuilderInterface`](/src/ValidatorBuilderInterface.php) interface it implements and inject the interface instead, which would also be useful for testing and mocking.

You can now validate `\Symfony\Component\HttpFoundation\Request` and `\Symfony\Component\HttpFoundation\Response` objects for a given [path](https://swagger.io/specification/#paths-object) and method:

```php
Expand All @@ -89,7 +92,53 @@ $validator->post($request, '/users');

In the example above, we check that the request matches the OpenAPI definition for a `POST` request on the `/users` path.

The `validate` method returns `true` in case of success, and throws `\Osteel\OpenApi\Testing\Exceptions\ValidationException` exceptions in case of error.
The `validate` method returns `true` in case of success, and throw a [`\Osteel\OpenApi\Testing\Exceptions\ValidationException`](/src/Exceptions/ValidationException.php) exception in case of error.

## Caching

This package supports caching to speed up the parsing of OpenAPI definitions. Simply pass your [PSR-6](https://www.php-fig.org/psr/psr-6/) or [PSR-16](https://www.php-fig.org/psr/psr-16/) cache object to the `setCache` method of the [`ValidatorBuilder`](/src/ValidatorBuilder.php) class.

Here is an example using Symfony's [Array Cache Adapter](https://symfony.com/doc/current/components/cache/adapters/array_cache_adapter.html "Array Cache Adapter"):

```php
use Osteel\OpenApi\Testing\ValidatorBuilder;
use Symfony\Component\Cache\Adapter\ArrayAdapter;

$cache = new ArrayAdapter();
$validator = ValidatorBuilder::fromYamlFile($yamlFile)->setCache($cache)->getValidator();
```

## Extending the package

There are two main extension points – message adapters and cache adapters.

### Message adapters

The [`ValidatorBuilder`](/src/ValidatorBuilder.php) class uses the [`HttpFoundationAdapter`](/src/Adapters/HttpFoundationAdapter.php) class as its default HTTP message adapter. This class converts HttpFoundation request and response objects to their PSR-7 counterparts.

If you need to change the adapter's logic, or if you need a new adapter altogether, create a class implementing the [`MessageAdapterInterface`](/src/Adapters/MessageAdapterInterface.php) interface and pass it to the `setMessageAdapter` method of the [`ValidatorBuilder`](/src/ValidatorBuilder.php) class:

```php
$validator = ValidatorBuilder::fromYamlFile($yamlFile)
->setMessageAdapter($yourAdapter)
->getValidator();
```

### Cache adapters

The [`ValidatorBuilder`](/src/ValidatorBuilder.php) class uses the [`Psr16Adapter`](/src/Cache/Psr16Adapter.php) class as its default cache adapter. This class converts PSR-16 cache objects to their PSR-6 counterparts.

If you need to change the adapter's logic, or if you need a new adapter altogether, create a class implementing the [`CacheAdapterInterface`](/src/Cache/CacheAdapterInterface.php) interface and pass it to the `setCacheAdapter` method of the [`ValidatorBuilder`](/src/ValidatorBuilder.php) class:

```php
$validator = ValidatorBuilder::fromYamlFile($yamlFile)
->setCacheAdapter($yourAdapter)
->getValidator();
```

### Other interfaces

The [`ValidatorBuilder`](/src/ValidatorBuilder.php) and [`Validator`](/src/Validator.php) classes are `final` but they implement the [`ValidatorBuilderInterface`](/src/ValidatorBuilderInterface.php) and [`ValidatorInterface`](/src/ValidatorInterface.php) interfaces respectively for which you can provide your own implementations if you need to.

## Change log

Expand Down
11 changes: 9 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@
"ext-json": "*",
"league/openapi-psr7-validator": "^0.21",
"nyholm/psr7": "^1.0",
"psr/cache": "^3.0",
"psr/http-message": "^1.0",
"psr/simple-cache": "^3.0",
"symfony/cache": "^6.0",
"symfony/http-foundation": "^4.0 || ^5.0 || ^6.0",
"symfony/psr-http-message-bridge": "^2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.17",
"phpunit/phpunit": "^9.0",
"phpunit/phpunit": "^9.6",
"rector/rector": "^0.17.1"
},
"autoload": {
Expand All @@ -47,8 +50,12 @@
}
},
"scripts": {
"test": "phpunit",
"fix": "php-cs-fixer fix -v",
"test": "phpunit",
"all": [
"@fix",
"@test"
],
"refactor": "rector process"
},
"extra": {
Expand Down
4 changes: 2 additions & 2 deletions src/Adapters/HttpFoundationAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

final class HttpFoundationAdapter implements AdapterInterface
final class HttpFoundationAdapter implements MessageAdapterInterface
{
/**
* @inheritDoc
* Convert a HttpFoundation object to a PSR-7 HTTP message.
*
* @param object $message the HTTP message to convert
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Psr\Http\Message\MessageInterface;

interface AdapterInterface
interface MessageAdapterInterface
{
/**
* Convert a HTTP message to a PSR-7 HTTP message.
Expand Down
17 changes: 17 additions & 0 deletions src/Cache/CacheAdapterInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Osteel\OpenApi\Testing\Cache;

use Psr\Cache\CacheItemPoolInterface;

interface CacheAdapterInterface
{
/**
* Convert a caching library to a PSR-6 caching library.
*
* @param object $cache the caching library to convert
*/
public function convert(object $cache): CacheItemPoolInterface;
}
31 changes: 31 additions & 0 deletions src/Cache/Psr16Adapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Osteel\OpenApi\Testing\Cache;

use InvalidArgumentException;
use Psr\Cache\CacheItemPoolInterface;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Cache\Adapter\Psr16Adapter as CacheAdapter;

final class Psr16Adapter implements CacheAdapterInterface
{
/**
* Convert a PSR-16 caching library to a PSR-6 caching library.
*
* @param object $cache the caching library to convert
*/
public function convert(object $cache): CacheItemPoolInterface
{
if ($cache instanceof CacheItemPoolInterface) {
return $cache;
}

if ($cache instanceof CacheInterface) {
return new CacheAdapter($cache);
}

throw new InvalidArgumentException(sprintf('Unsupported %s object received', $cache::class));
}
}
4 changes: 2 additions & 2 deletions src/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use League\OpenAPIValidation\PSR7\OperationAddress;
use League\OpenAPIValidation\PSR7\ResponseValidator;
use League\OpenAPIValidation\PSR7\RoutedServerRequestValidator;
use Osteel\OpenApi\Testing\Adapters\AdapterInterface;
use Osteel\OpenApi\Testing\Adapters\MessageAdapterInterface;
use Osteel\OpenApi\Testing\Exceptions\ValidationException;
use Psr\Http\Message\ResponseInterface;

Expand All @@ -17,7 +17,7 @@ final class Validator implements ValidatorInterface
public function __construct(
private RoutedServerRequestValidator $requestValidator,
private ResponseValidator $responseValidator,
private AdapterInterface $adapter
private MessageAdapterInterface $adapter
) {
}

Expand Down
55 changes: 43 additions & 12 deletions src/ValidatorBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

use InvalidArgumentException;
use League\OpenAPIValidation\PSR7\ValidatorBuilder as BaseValidatorBuilder;
use Osteel\OpenApi\Testing\Adapters\AdapterInterface;
use Osteel\OpenApi\Testing\Adapters\MessageAdapterInterface;
use Osteel\OpenApi\Testing\Adapters\HttpFoundationAdapter;
use Osteel\OpenApi\Testing\Cache\CacheAdapterInterface;
use Osteel\OpenApi\Testing\Cache\Psr16Adapter;

/**
* This class creates Validator objects based on OpenAPI definitions.
Expand All @@ -16,6 +18,8 @@ final class ValidatorBuilder implements ValidatorBuilderInterface
{
private string $adapter = HttpFoundationAdapter::class;

private string $cacheAdapter = Psr16Adapter::class;

public function __construct(private BaseValidatorBuilder $validatorBuilder)
{
}
Expand Down Expand Up @@ -97,6 +101,16 @@ private static function fromMethod(string $method, string $definition): Validato
return new ValidatorBuilder($builder);
}

/** @inheritDoc */
public function setCache(object $cache): ValidatorBuilderInterface
{
$adapter = new $this->cacheAdapter();

$this->validatorBuilder->setCache($adapter->convert($cache));

return $this;
}

/** @inheritDoc */
public function getValidator(): ValidatorInterface
{
Expand All @@ -108,25 +122,42 @@ public function getValidator(): ValidatorInterface
}

/**
* Change the adapter to use. The provided class must implement
* \Osteel\OpenApi\Testing\Adapters\AdapterInterface.
* Change the adapter to use. The provided class must implement \Osteel\OpenApi\Testing\Adapters\AdapterInterface.
*
* @param string $class the adapter's class
*
* @throws InvalidArgumentException
*/
public function setAdapter(string $class): ValidatorBuilder
public function setMessageAdapter(string $class): ValidatorBuilder
{
if (! is_subclass_of($class, AdapterInterface::class)) {
throw new InvalidArgumentException(sprintf(
'Class %s does not implement the %s interface',
$class,
AdapterInterface::class
));
if (is_subclass_of($class, MessageAdapterInterface::class)) {
$this->adapter = $class;

return $this;
}

$this->adapter = $class;
throw new InvalidArgumentException(
sprintf('Class %s does not implement the %s interface', $class, MessageAdapterInterface::class),
);
}

/**
* Change the cache adapter to use. The provided class must implement \Osteel\OpenApi\Testing\Cache\AdapterInterface.
*
* @param string $class the cache adapter's class
*
* @throws InvalidArgumentException
*/
public function setCacheAdapter(string $class): ValidatorBuilder
{
if (is_subclass_of($class, CacheAdapterInterface::class)) {
$this->cacheAdapter = $class;

return $this;
return $this;
}

throw new InvalidArgumentException(
sprintf('Class %s does not implement the %s interface', $class, CacheAdapterInterface::class),
);
}
}
3 changes: 3 additions & 0 deletions src/ValidatorBuilderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ public static function fromYamlString(string $definition): ValidatorBuilderInter
*/
public static function fromJsonString(string $definition): ValidatorBuilderInterface;

/** Set a cache library. */
public function setCache(object $cache): ValidatorBuilderInterface;

/** Return the validator. */
public function getValidator(): ValidatorInterface;
}
5 changes: 3 additions & 2 deletions tests/Adapters/HttpFoundationAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use stdClass;

class HttpFoundationAdapterTest extends TestCase
{
Expand All @@ -24,9 +25,9 @@ protected function setUp(): void
public function test_it_does_not_convert_the_message_because_the_type_is_not_supported()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Unsupported InvalidArgumentException object received');
$this->expectExceptionMessage('Unsupported stdClass object received');

$this->sut->convert(new InvalidArgumentException());
$this->sut->convert(new stdClass());
}

public function test_it_converts_the_http_foundation_request()
Expand Down
Loading