Skip to content

Commit 0be8008

Browse files
authored
Added endpoint that can return single spec files, #PG-4593 (#25)
* Added general endpoint to retrieve specs, either by plugin name or all * Removed matomo case and added plugin validation logic * Changed from spec to pluginName * Changed exception for backwards compatibility reasons
1 parent 6c51982 commit 0be8008

2 files changed

Lines changed: 83 additions & 27 deletions

File tree

API.php

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,30 @@
2424
class API extends \Piwik\Plugin\API
2525
{
2626
/**
27-
* Get the pre-generated single OpenAPI spec file if it exists. This endpoint only reads
27+
* Get a pre-generated OpenAPI spec file if it exists. This endpoint only reads
2828
* the generated JSON file and does not trigger spec generation.
2929
*
30-
* /index.php?module=API&method=OpenApiDocs.getMatomoOpenApiSpec
30+
* /index.php?module=API&method=OpenApiDocs.getOpenApiSpec&spec=CustomAlerts
3131
*
32+
* @param string $pluginName Plugin name used in the generated filename.
3233
* @param string $format Output format. Only `json` is supported.
3334
* @return array<string, mixed> The decoded OpenAPI specification payload.
3435
* @throws \Exception If the file is missing, unreadable, or contains invalid JSON.
3536
*/
36-
public function getMatomoOpenApiSpec(string $format = 'json'): array
37+
public function getOpenApiSpec(string $pluginName, string $format = 'json'): array
3738
{
3839
Piwik::checkUserHasSomeViewAccess();
3940

40-
if (strtolower($format) !== 'json') {
41-
throw new \Exception(
42-
Piwik::translate(
43-
'General_ExceptionInvalidReportRendererFormat',
44-
[$format, 'json']
45-
)
46-
);
47-
}
41+
$this->validateJsonFormat($format);
4842

49-
$filePath = $this->getMatomoSpecFilePath();
43+
if (
44+
!Manager::getInstance()->isValidPluginName($pluginName)
45+
|| !Manager::getInstance()->isPluginInFilesystem($pluginName)
46+
) {
47+
throw new \Exception('Invalid plugin name: ' . $pluginName);
48+
}
5049

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

68-
protected function getMatomoSpecFilePath(): string
68+
protected function getSpecFilePath(string $pluginName): string
6969
{
7070
$currentPluginDir = Manager::getInstance()::getPluginDirectory('OpenApiDocs');
7171

72-
return $currentPluginDir . OpenApiDocs::GENERATED_SPECS_PATH . 'matomo_openapi_spec_v' . OpenApiDocs::DEFAULT_SPEC_VERSION . '.json';
72+
return $currentPluginDir . OpenApiDocs::GENERATED_SPECS_PATH . $pluginName . '_openapi_spec_v' . OpenApiDocs::DEFAULT_SPEC_VERSION . '.json';
7373
}
7474

7575
protected function isSpecFileReadable(string $filePath): bool
@@ -86,6 +86,18 @@ protected function readSpecFile(string $filePath)
8686
return file_get_contents($filePath);
8787
}
8888

89+
protected function validateJsonFormat(string $format): void
90+
{
91+
if (strtolower($format) !== 'json') {
92+
throw new \Exception(
93+
Piwik::translate(
94+
'General_ExceptionInvalidReportRendererFormat',
95+
[$format, 'json']
96+
)
97+
);
98+
}
99+
}
100+
89101
/**
90102
* Get the generated API documentation data for the specified plugin.
91103
*

tests/Unit/APITest.php

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,54 +43,98 @@ protected function tearDown(): void
4343
parent::tearDown();
4444
}
4545

46-
public function testGetMatomoOpenApiSpecReturnsDecodedJson()
46+
public function testGetOpenApiSpecReturnsDecodedJsonForPlugin()
4747
{
4848
$expectedSpec = [
4949
'openapi' => '3.1.0',
5050
'info' => [
51-
'title' => 'Matomo Reporting API',
51+
'title' => 'Matomo Reporting API for CustomAlerts plugin',
5252
'version' => '1.0.0',
5353
],
5454
];
5555

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

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

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

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

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

70-
$api->getMatomoOpenApiSpec();
70+
$api->getOpenApiSpec('CustomAlerts');
7171
}
7272

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

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

80-
$api->getMatomoOpenApiSpec();
80+
$api->getOpenApiSpec('CustomAlerts');
8181
}
8282

83+
public function testGetOpenApiSpecThrowsExceptionWhenFormatIsInvalid()
84+
{
85+
$api = $this->buildApiMock('/tmp/CustomAlerts_openapi_spec_v1.0.0.json', true, '{}');
86+
87+
$this->expectException(\Exception::class);
88+
$this->expectExceptionMessage('General_ExceptionInvalidReportRendererFormat');
89+
90+
$api->getOpenApiSpec('CustomAlerts', 'yaml');
91+
}
92+
93+
public function testGetOpenApiSpecThrowsExceptionWhenSpecIsNotAValidPlugin()
94+
{
95+
$api = new API();
96+
97+
$this->expectException(\Exception::class);
98+
$this->expectExceptionMessage('Invalid plugin name: DefinitelyNotARealPlugin');
99+
100+
$api->getOpenApiSpec('DefinitelyNotARealPlugin');
101+
}
102+
103+
public function testGetSpecFilePathUsesPluginSpecificFileName()
104+
{
105+
$api = new API();
106+
107+
$this->assertSame(
108+
PIWIK_INCLUDE_PATH . '/plugins/OpenApiDocs/tmp/specs/CustomAlerts_openapi_spec_v1.0.0.json',
109+
$this->callProtectedMethod($api, 'getSpecFilePath', ['CustomAlerts'])
110+
);
111+
}
83112

84-
private function buildApiMock(bool $isReadable, $fileContents = false): API
113+
114+
private function buildApiMock(string $filePath, bool $isReadable, $fileContents = false): API
85115
{
86116
$api = $this->getMockBuilder(API::class)
87-
->onlyMethods(['getMatomoSpecFilePath', 'isSpecFileReadable', 'readSpecFile'])
117+
->onlyMethods(['getSpecFilePath', 'isSpecFileReadable', 'readSpecFile'])
88118
->getMock();
89119

90-
$api->method('getMatomoSpecFilePath')->willReturn('/tmp/matomo_openapi_spec_v1.0.0.json');
120+
$api->method('getSpecFilePath')->willReturn($filePath);
91121
$api->method('isSpecFileReadable')->willReturn($isReadable);
92122
$api->method('readSpecFile')->willReturn($fileContents);
93123

94124
return $api;
95125
}
126+
127+
/**
128+
* @param object $object
129+
* @param string $methodName
130+
* @param array<int, mixed> $arguments
131+
* @return mixed
132+
*/
133+
private function callProtectedMethod($object, string $methodName, array $arguments = [])
134+
{
135+
$reflection = new \ReflectionMethod($object, $methodName);
136+
$reflection->setAccessible(true);
137+
138+
return $reflection->invokeArgs($object, $arguments);
139+
}
96140
}

0 commit comments

Comments
 (0)