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
40 changes: 26 additions & 14 deletions API.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,30 @@
class API extends \Piwik\Plugin\API
{
/**
* Get the pre-generated single OpenAPI spec file if it exists. This endpoint only reads
* Get a pre-generated OpenAPI spec file if it exists. This endpoint only reads
* the generated JSON file and does not trigger spec generation.
*
* /index.php?module=API&method=OpenApiDocs.getMatomoOpenApiSpec
* /index.php?module=API&method=OpenApiDocs.getOpenApiSpec&spec=CustomAlerts
*
* @param string $pluginName Plugin name used in the generated filename.
* @param string $format Output format. Only `json` is supported.
* @return array<string, mixed> The decoded OpenAPI specification payload.
* @throws \Exception If the file is missing, unreadable, or contains invalid JSON.
*/
public function getMatomoOpenApiSpec(string $format = 'json'): array
public function getOpenApiSpec(string $pluginName, string $format = 'json'): array
{
Piwik::checkUserHasSomeViewAccess();

if (strtolower($format) !== 'json') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also add a validation for $spec, it should be an valid pluginName

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AltamashShaikh I've changed that, also removed the single spec file logic from the endpoint. I don't think it's really required anymore.

throw new \Exception(
Piwik::translate(
'General_ExceptionInvalidReportRendererFormat',
[$format, 'json']
)
);
}
$this->validateJsonFormat($format);

$filePath = $this->getMatomoSpecFilePath();
if (
!Manager::getInstance()->isValidPluginName($pluginName)
|| !Manager::getInstance()->isPluginInFilesystem($pluginName)
) {
throw new \Exception('Invalid plugin name: ' . $pluginName);
}

$filePath = $this->getSpecFilePath($pluginName);
if (!$this->isSpecFileReadable($filePath)) {
throw new \Exception('OpenAPI spec file was not found. Generate it first via openapidocs:generate-spec-file.');
}
Expand All @@ -65,11 +65,11 @@ public function getMatomoOpenApiSpec(string $format = 'json'): array
return $decodedSpec;
}

protected function getMatomoSpecFilePath(): string
protected function getSpecFilePath(string $pluginName): string
{
$currentPluginDir = Manager::getInstance()::getPluginDirectory('OpenApiDocs');

return $currentPluginDir . OpenApiDocs::GENERATED_SPECS_PATH . 'matomo_openapi_spec_v' . OpenApiDocs::DEFAULT_SPEC_VERSION . '.json';
return $currentPluginDir . OpenApiDocs::GENERATED_SPECS_PATH . $pluginName . '_openapi_spec_v' . OpenApiDocs::DEFAULT_SPEC_VERSION . '.json';
}

protected function isSpecFileReadable(string $filePath): bool
Expand All @@ -86,6 +86,18 @@ protected function readSpecFile(string $filePath)
return file_get_contents($filePath);
}

protected function validateJsonFormat(string $format): void
{
if (strtolower($format) !== 'json') {
throw new \Exception(
Piwik::translate(
'General_ExceptionInvalidReportRendererFormat',
[$format, 'json']
)
);
}
}

/**
* Get the generated API documentation data for the specified plugin.
*
Expand Down
70 changes: 57 additions & 13 deletions tests/Unit/APITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,54 +43,98 @@ protected function tearDown(): void
parent::tearDown();
}

public function testGetMatomoOpenApiSpecReturnsDecodedJson()
public function testGetOpenApiSpecReturnsDecodedJsonForPlugin()
{
$expectedSpec = [
'openapi' => '3.1.0',
'info' => [
'title' => 'Matomo Reporting API',
'title' => 'Matomo Reporting API for CustomAlerts plugin',
'version' => '1.0.0',
],
];

$api = $this->buildApiMock(true, json_encode($expectedSpec));
$api = $this->buildApiMock('/tmp/CustomAlerts_openapi_spec_v1.0.0.json', true, json_encode($expectedSpec));

$result = $api->getMatomoOpenApiSpec();
$result = $api->getOpenApiSpec('CustomAlerts');

$this->assertSame($expectedSpec, $result);
}

public function testGetMatomoOpenApiSpecThrowsExceptionWhenFileMissing()
public function testGetOpenApiSpecThrowsExceptionWhenFileMissing()
{
$api = $this->buildApiMock(false);
$api = $this->buildApiMock('/tmp/CustomAlerts_openapi_spec_v1.0.0.json', false);

$this->expectException(\Exception::class);
$this->expectExceptionMessage('OpenAPI spec file was not found');

$api->getMatomoOpenApiSpec();
$api->getOpenApiSpec('CustomAlerts');
}

public function testGetMatomoOpenApiSpecThrowsExceptionWhenJsonIsInvalid()
public function testGetOpenApiSpecThrowsExceptionWhenJsonIsInvalid()
{
$api = $this->buildApiMock(true, '{invalid json}');
$api = $this->buildApiMock('/tmp/CustomAlerts_openapi_spec_v1.0.0.json', true, '{invalid json}');

$this->expectException(\Exception::class);
$this->expectExceptionMessage('OpenAPI spec file contains invalid JSON');

$api->getMatomoOpenApiSpec();
$api->getOpenApiSpec('CustomAlerts');
}

public function testGetOpenApiSpecThrowsExceptionWhenFormatIsInvalid()
{
$api = $this->buildApiMock('/tmp/CustomAlerts_openapi_spec_v1.0.0.json', true, '{}');

$this->expectException(\Exception::class);
$this->expectExceptionMessage('General_ExceptionInvalidReportRendererFormat');

$api->getOpenApiSpec('CustomAlerts', 'yaml');
}

public function testGetOpenApiSpecThrowsExceptionWhenSpecIsNotAValidPlugin()
{
$api = new API();

$this->expectException(\Exception::class);
$this->expectExceptionMessage('Invalid plugin name: DefinitelyNotARealPlugin');

$api->getOpenApiSpec('DefinitelyNotARealPlugin');
}

public function testGetSpecFilePathUsesPluginSpecificFileName()
{
$api = new API();

$this->assertSame(
PIWIK_INCLUDE_PATH . '/plugins/OpenApiDocs/tmp/specs/CustomAlerts_openapi_spec_v1.0.0.json',
$this->callProtectedMethod($api, 'getSpecFilePath', ['CustomAlerts'])
);
}

private function buildApiMock(bool $isReadable, $fileContents = false): API

private function buildApiMock(string $filePath, bool $isReadable, $fileContents = false): API
{
$api = $this->getMockBuilder(API::class)
->onlyMethods(['getMatomoSpecFilePath', 'isSpecFileReadable', 'readSpecFile'])
->onlyMethods(['getSpecFilePath', 'isSpecFileReadable', 'readSpecFile'])
->getMock();

$api->method('getMatomoSpecFilePath')->willReturn('/tmp/matomo_openapi_spec_v1.0.0.json');
$api->method('getSpecFilePath')->willReturn($filePath);
$api->method('isSpecFileReadable')->willReturn($isReadable);
$api->method('readSpecFile')->willReturn($fileContents);

return $api;
}

/**
* @param object $object
* @param string $methodName
* @param array<int, mixed> $arguments
* @return mixed
*/
private function callProtectedMethod($object, string $methodName, array $arguments = [])
{
$reflection = new \ReflectionMethod($object, $methodName);
$reflection->setAccessible(true);

return $reflection->invokeArgs($object, $arguments);
}
}
Loading