Skip to content

Commit

Permalink
Publicly accessible version of api docs (#264)
Browse files Browse the repository at this point in the history
* Publicly-accessible version of docs endpoint, with updated unit tests
* Update the dkan_api docs service
  • Loading branch information
thierrydallacroce authored Dec 4, 2019
1 parent 6274d9a commit 51970e9
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 100 deletions.
1 change: 1 addition & 0 deletions modules/custom/dkan_api/dkan_api.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ services:
class: \Drupal\dkan_api\Controller\Docs
arguments:
- '@module_handler'
- '@request_stack'
113 changes: 92 additions & 21 deletions modules/custom/dkan_api/src/Controller/Docs.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Drupal\Core\Serialization\Yaml;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RequestStack;

/**
* Serves openapi spec for dataset-related endpoints.
Expand All @@ -29,6 +30,13 @@ class Docs implements ContainerInjectionInterface {
*/
private $moduleHandler;

/**
* Request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
private $requestStack;

/**
* Serializer to translate yaml to json.
*
Expand All @@ -40,25 +48,29 @@ class Docs implements ContainerInjectionInterface {
* Inherited.
*
* @{inheritdocs}
*
* @codeCoverageIgnore
*/
public static function create(ContainerInterface $container) {
$moduleHandler = $container->get('module_handler');
return new Docs($moduleHandler);
return new Docs(
$container->get('module_handler'),
$container->get('request_stack')
);
}

/**
* Constructor.
*/
public function __construct(ModuleHandlerInterface $moduleHandler) {
public function __construct(
ModuleHandlerInterface $moduleHandler,
RequestStack $requestStack
) {
$this->moduleHandler = $moduleHandler;
$this->requestStack = $requestStack;
$this->serializer = new Yaml();
$this->spec = $this->getJsonFromYmlFile();
}

/**
* Ger version.
* Get version.
*/
public function getVersions() {
return new JsonResponse(["version" => 1, "url" => "/api/1"]);
Expand All @@ -73,36 +85,95 @@ public function getVersions() {
public function getJsonFromYmlFile() {
$modulePath = $this->moduleHandler->getModule('dkan_api')->getPath();
$ymlSpecPath = $modulePath . '/docs/dkan_api_openapi_spec.yml';
$ymlSpec = $this->fileGetContents($ymlSpecPath);
$ymlSpec = file_get_contents($ymlSpecPath);

return $this->serializer->decode($ymlSpec);
}

/**
* Wrapper around file_get_contents to facilitate testing.
* Returns the complete API spec.
*
* @param string $path
* Path for our yml spec.
* @return \Symfony\Component\HttpFoundation\JsonResponse
* OpenAPI spec response.
*/
public function getComplete() {
if ($this->requestStack->getCurrentRequest()->get('authentication') === "false") {
$spec = $this->getPublic();
}
else {
$spec = $this->spec;
}

$jsonSpec = json_encode($spec);
return $this->sendResponse($jsonSpec);
}

/**
* Return a publicly-accessible version of the API spec.
*
* @return false|string
* Our yml file, or FALSE.
* Remove any endpoint requiring authentication, as well as the security
* schemes components from the api spec.
*
* @codeCoverageIgnore
* @return array
* The modified API spec, without authentication-related items.
*/
private function fileGetContents($path) {
return file_get_contents($path);
private function getPublic() {
$publicSpec = $this->removeAuthenticatedEndpoints($this->spec);
$cleanSpec = $this->cleanUpEndpoints($publicSpec);
unset($cleanSpec['components']['securitySchemes']);
return $cleanSpec;
}

/**
* Returns the complete API spec.
* Remove API spec endpoints requiring authentication.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* OpenAPI spec response.
* @param array $spec
* The original spec.
*
* @return array
* The modified API spec, without authenticated endpoints.
*/
public function getComplete() {
$jsonSpec = json_encode($this->spec);
private function removeAuthenticatedEndpoints(array $spec) {
foreach ($spec['paths'] as $path => $operations) {
$this->removeAuthenticatedOperations($operations, $path, $spec);
}
return $spec;
}

return $this->sendResponse($jsonSpec);
/**
* Within a path, remove operations requiring authentication.
*
* @param array $operations
* Operations for the current path.
* @param string $path
* The path being processed.
* @param array $spec
* Our modified dataset-specific openapi spec.
*/
private function removeAuthenticatedOperations(array $operations, string $path, array &$spec) {
foreach ($operations as $operation => $details) {
if (isset($spec['paths'][$path][$operation]['security'])) {
unset($spec['paths'][$path][$operation]);
}
}
}

/**
* Clean up empty endpoints from the spec.
*
* @param array $spec
* The original spec.
*
* @return array
* The cleaned up API spec.
*/
private function cleanUpEndpoints(array $spec) {
foreach ($spec['paths'] as $path => $operations) {
if (empty($operations)) {
unset($spec['paths'][$path]);
}
}
return $spec;
}

/**
Expand Down
115 changes: 36 additions & 79 deletions modules/custom/dkan_api/tests/src/Unit/Controller/DocsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,108 +6,65 @@
use Drupal\dkan_api\Controller\Docs;
use Drupal\dkan_common\Tests\DkanTestBase;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\dkan_common\Tests\Mock\Chain;
use Drupal\dkan_common\Tests\Mock\Options;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\dkan_data\Storage\Data;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

/**
* Class.
* Test class Docs.
*/
class DocsTest extends DkanTestBase {

/**
*
*/
public function getContainer() {
// TODO: Change the autogenerated stub.
parent::setUp();

$container = $this->getMockBuilder(ContainerInterface::class)
->setMethods(['get'])
->disableOriginalConstructor()
->getMockForAbstractClass();

$container->method('get')
->with(
$this->logicalOr(
$this->equalTo('module_handler'),
$this->equalTo('dkan_data.storage')
)
)
->will($this->returnCallback([$this, 'containerGet']));

return $container;
}

/**
*
*/
public function containerGet($input) {
switch ($input) {
case 'module_handler':
return $this->getModuleHandlerMock();
public function testGetVersions() {
$mockChain = $this->getCommonMockChain();
$controller = Docs::create($mockChain->getMock());
$response = $controller->getVersions();

case 'dkan_data.storage':
return $this->getDataMock();
$spec = '{"version":1,"url":"\/api\/1"}';

break;
}
$this->assertEquals($spec, $response->getContent());
}

/**
*
*/
public function testGetComplete() {
$controller = Docs::create($this->getContainer());
$response = $controller->getComplete();
$mock = $this->getCommonMockChain()
->add(RequestStack::class, 'getCurrentRequest',Request::class)
->add(Request::class, 'get', NULL);

$spec = '{"openapi":"3.0.1","info":{"title":"API Documentation","version":"Alpha"},"components":{"securitySchemes":{"basicAuth":{"type":"http","scheme":"basic"}}},"paths":{"\/api\/1\/metastore\/schemas\/dataset\/items\/{identifier}":{"get":{"summary":"Get this dataset","tags":["Dataset"],"parameters":[{"name":"identifier","in":"path","description":"Dataset uuid","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Ok"}}},"delete":{"summary":"This operation should not be present in dataset-specific docs.","security":[{"basicAuth":[]}],"responses":{"200":{"description":"Ok"}}},"post":null},"\/api\/1\/some\/other\/path":{"patch":{"summary":"This path and operation should not be present in dataset-specific docs.","security":[{"basicAuth":[]}],"responses":{"200":{"description":"Ok"}}}}}}';

$spec = '{"openapi":"3.0.1","info":{"title":"API Documentation","version":"Alpha"},"paths":{"\/api\/1\/metastore\/schemas\/dataset\/items\/{identifier}":{"get":{"summary":"Get this dataset","tags":["Dataset"],"parameters":[{"name":"identifier","in":"path","description":"Dataset uuid","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Ok"}}}}}}';
$controller = Docs::create($mock->getMock());
$response = $controller->getComplete();

$this->assertEquals($spec, $response->getContent());
}

/**
*
*/
private function getModuleHandlerMock() {
$mock = $this->getMockBuilder(ModuleHandlerInterface::class)
->disableOriginalConstructor()
->setMethods(['getModule'])
->getMockForAbstractClass();
public function testGetPublic() {
$mock = $this->getCommonMockChain()
->add(RequestStack::class, 'getCurrentRequest',Request::class)
->add(Request::class, 'get', 'false');

$mock->method('getModule')
->willReturn($this->getExtensionMock());
$spec = '{"openapi":"3.0.1","info":{"title":"API Documentation","version":"Alpha"},"components":[],"paths":{"\/api\/1\/metastore\/schemas\/dataset\/items\/{identifier}":{"get":{"summary":"Get this dataset","tags":["Dataset"],"parameters":[{"name":"identifier","in":"path","description":"Dataset uuid","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Ok"}}},"post":null}}}';

return $mock;
}

/**
*
*/
private function getExtensionMock() {
$mock = $this->getMockBuilder(Extension::class)
->disableOriginalConstructor()
->setMethods(['getPath'])
->getMock();

$mock->method('getPath')
->willReturn(__DIR__);
$controller = Docs::create($mock->getMock());
$response = $controller->getComplete();

return $mock;
$this->assertEquals($spec, $response->getContent());
}

/**
*
*/
private function getDataMock() {
$mock = $this->getMockBuilder(Data::class)
->disableOriginalConstructor()
->setMethods(['retrieve'])
->getMock();
public function getCommonMockChain() {
$options = (new Options())
->add('module_handler', ModuleHandlerInterface::class)
->add('request_stack', RequestStack::class);

$mock->method('retrieve')
->willReturn("{}");
$mockChain = (new Chain($this))
->add(ContainerInterface::class, 'get', $options)
->add(ModuleHandlerInterface::class, 'getModule', Extension::class)
->add(Extension::class, 'getPath', __DIR__);

return $mock;
return $mockChain;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ openapi: 3.0.1
info:
title: API Documentation
version: Alpha
components:
securitySchemes:
basicAuth:
type: http
scheme: basic
paths:
/api/1/metastore/schemas/dataset/items/{identifier}:
get:
Expand All @@ -19,3 +24,20 @@ paths:
responses:
200:
description: Ok
delete:
summary: This operation should not be present in dataset-specific docs.
security:
- basicAuth: []
responses:
200:
description: Ok
# Though an empty verb invalidates the spec, test its removal by dataset-specific docs.
post:
/api/1/some/other/path:
patch:
summary: This path and operation should not be present in dataset-specific docs.
security:
- basicAuth: []
responses:
200:
description: Ok

0 comments on commit 51970e9

Please sign in to comment.