Skip to content

Commit

Permalink
ENH Work with official list of supported modules
Browse files Browse the repository at this point in the history
  • Loading branch information
emteknetnz committed Mar 13, 2023
1 parent 9495e2b commit 9b2d015
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 67 deletions.
33 changes: 19 additions & 14 deletions src/Util/ApiLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
use SilverStripe\Core\Injector\Injector;

/**
* Handles fetching supported addon details from addons.silverstripe.org
* Handles fetching supported module details
*/
abstract class ApiLoader
{
Expand Down Expand Up @@ -71,29 +71,35 @@ public function doRequest($endpoint, callable $callback)
throw new RuntimeException($failureMessage . 'Error code ' . $response->getStatusCode());
}

if (!in_array('application/json', $response->getHeader('Content-Type') ?? [])) {
throw new RuntimeException($failureMessage . 'Response is not JSON');
}

$responseBody = json_decode($response->getBody()->getContents(), true);

if (empty($responseBody)) {
throw new RuntimeException($failureMessage . 'Response could not be parsed');
}
$responseJson = $this->parseResponseContents($response->getBody()->getContents(), $failureMessage);

if (!isset($responseBody['success']) || !$responseBody['success']) {
throw new RuntimeException($failureMessage . 'Response returned unsuccessfully');
if (str_contains($endpoint, 'addons.silverstripe.org')) {
if (!isset($responseJson['success']) || !$responseJson['success']) {
throw new RuntimeException($failureMessage . 'Response returned unsuccessfully');
}
}

// Allow callback to handle processing of the response body
$result = $callback($responseBody);
$result = $callback($responseJson);

// Setting the value to the cache for subsequent requests
$this->handleCacheFromResponse($response, $result);

return $result;
}

private function parseResponseContents(string $contents, string $failureMessage): array
{
if (empty($contents)) {
throw new RuntimeException($failureMessage . 'Response was empty');
}
$responseJson = json_decode($contents, true);
if (empty($responseJson)) {
throw new RuntimeException($failureMessage . json_last_error_msg());
}
return $responseJson;
}

/**
* @return Client
*/
Expand Down Expand Up @@ -157,7 +163,6 @@ protected function setToCache($cacheKey, $value, $ttl = null)
{
// Seralize as JSON to ensure array etc can be stored
$value = json_encode($value);

return $this->getCache()->set($cacheKey, $value, $ttl);
}

Expand Down
8 changes: 4 additions & 4 deletions src/Util/SupportedAddonsLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
class SupportedAddonsLoader extends ApiLoader
{
/**
* Return the list of supported addons as provided by addons.silverstripe.org
* Return the list of supported modules
*
* @return array
*/
public function getAddonNames()
{
$endpoint = 'addons.silverstripe.org/api/supported-addons';
return $this->doRequest($endpoint, function ($responseBody) {
return isset($responseBody['addons']) ? $responseBody['addons'] : [];
$endpoint = 'https://raw.githubusercontent.com/silverstripe/supported-modules/5/modules.json';
return $this->doRequest($endpoint, function ($responseJson) {
return array_map(fn(array $item) => $item['composer'], $responseJson);
});
}

Expand Down
104 changes: 59 additions & 45 deletions tests/Util/ApiLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use GuzzleHttp\Psr7\Response;
use SilverStripe\Dev\SapphireTest;
use Psr\SimpleCache\CacheInterface;
use ReflectionMethod;

class ApiLoaderTest extends SapphireTest
{
Expand All @@ -25,35 +26,16 @@ public function testNon200ErrorCodesAreHandled()
});
}

public function testNonJsonResponsesAreHandled()
private function getFakeJson(): array
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Could not obtain information about module. Response is not JSON');
$loader = $this->getLoader();
$loader->setGuzzleClient($this->getMockClient(new Response(
200,
['Content-Type' => 'text/html; charset=utf-8']
)));

$loader->doRequest('foo', function () {
// noop
});
}

public function testUnsuccessfulResponsesAreHandled()
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Could not obtain information about module. Response returned unsuccessfully');
$loader = $this->getLoader();
$loader->setGuzzleClient($this->getMockClient(new Response(
200,
['Content-Type' => 'application/json'],
json_encode(['success' => false])
)));

$loader->doRequest('foo', function () {
// noop
});
return [
[
'composer' => 'foo/bar',
],
[
'composer' => 'bin/baz',
],
];
}

/**
Expand All @@ -63,20 +45,18 @@ public function testUnsuccessfulResponsesAreHandled()
*/
public function testAddonsAreParsedAndReturnedCorrectly()
{
$fakeAddons = ['foo/bar', 'bin/baz'];

$loader = $this->getLoader();
$loader->setGuzzleClient($this->getMockClient(new Response(
200,
['Content-Type' => 'application/json'],
json_encode(['success' => true, 'addons' => $fakeAddons])
json_encode($this->getFakeJson())
)));

$addons = $loader->doRequest('foo', function ($responseBody) {
return $responseBody['addons'];
$addons = $loader->doRequest('foo', function ($responseJson) {
return array_map(fn(array $item) => $item['composer'], $responseJson);
});

$this->assertSame($fakeAddons, $addons);
$this->assertSame(['foo/bar', 'bin/baz'], $addons);
}

/**
Expand All @@ -86,35 +66,31 @@ public function testAddonsAreParsedAndReturnedCorrectly()
*/
public function testCacheControlSettingsAreRespected()
{
$fakeAddons = ['foo/bar', 'bin/baz'];

$cacheMock = $this->getMockCacheInterface();

$cacheMock->expects($this->once())->method('get')->will($this->returnValue(false));
$cacheMock->expects($this->once())
->method('set')
->with($this->anything(), json_encode($fakeAddons), 5000)
->with($this->anything(), '["foo\/bar","bin\/baz"]', 5000)
->will($this->returnValue(true));

$loader = $this->getLoader($cacheMock);
$loader->setGuzzleClient($this->getMockClient(new Response(
200,
['Content-Type' => 'application/json', 'Cache-Control' => 'max-age=5000'],
json_encode(['success' => true, 'addons' => $fakeAddons])
json_encode($this->getFakeJson())
)));

$loader->doRequest('foo', function ($responseBody) {
return $responseBody['addons'];
$loader->doRequest('foo', function ($responseJson) {
return array_map(fn(array $item) => $item['composer'], $responseJson);
});
}

public function testCachedAddonsAreUsedWhenAvailable()
{
$fakeAddons = ['foo/bar', 'bin/baz'];

$cacheMock = $this->getMockCacheInterface();

$cacheMock->expects($this->once())->method('get')->will($this->returnValue(json_encode($fakeAddons)));
$cacheMock->expects($this->once())->method('get')->will($this->returnValue(json_encode($this->getFakeJson())));
$loader = $this->getLoader($cacheMock);

$mockClient = $this->getMockBuilder(Client::class)->setMethods(['send'])->getMock();
Expand All @@ -125,7 +101,45 @@ public function testCachedAddonsAreUsedWhenAvailable()
// noop
});

$this->assertSame($fakeAddons, $addons);
$this->assertSame($this->getFakeJson(), $addons);
}

public function testParseResponseContentsEmpty()
{
// ApiLoader is an abstract class
$inst = new class extends ApiLoader {
protected function getCacheKey()
{
return 'abc';
}
};
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('FAILURE_MESSAGE. Response was empty');
$refMethod = new ReflectionMethod(ApiLoader::class, 'parseResponseContents');
$refMethod->setAccessible(true);
$contents = '';
$failureMessage = 'FAILURE_MESSAGE. ';
$refMethod->invoke($inst, $contents, $failureMessage);
$inst->parseResponseContents($contents, $failureMessage);
}

public function testParseResponseContentsInvalid()
{
// ApiLoader is an abstract class
$inst = new class extends ApiLoader {
protected function getCacheKey()
{
return 'abc';
}
};
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('FAILURE_MESSAGE. Syntax error');
$refMethod = new ReflectionMethod(ApiLoader::class, 'parseResponseContents');
$refMethod->setAccessible(true);
$contents = '[ malformed }';
$failureMessage = 'FAILURE_MESSAGE. ';
$refMethod->invoke($inst, $contents, $failureMessage);
$inst->parseResponseContents($contents, $failureMessage);
}

/**
Expand Down Expand Up @@ -159,7 +173,7 @@ protected function getLoader($cacheMock = false)
->getMockForAbstractClass();

$loader->setCache($cacheMock);
$loader->expects($this->any())->method('getCacheKey')->will($this->returnValue('cachKey'));
$loader->expects($this->any())->method('getCacheKey')->will($this->returnValue('cacheKey'));

return $loader;
}
Expand Down
13 changes: 9 additions & 4 deletions tests/Util/SupportedAddonsLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ protected function setUp(): void

public function testCallsSupportedAddonsEndpoint()
{
$endpoint = 'https://raw.githubusercontent.com/silverstripe/supported-modules/5/modules.json';
$this->loader->expects($this->once())
->method('doRequest')
->with('addons.silverstripe.org/api/supported-addons', function () {
->with($endpoint, function () {
// no-op
});

Expand All @@ -41,11 +42,15 @@ public function testCallbackReturnsAddonsFromBody()

$result = $this->loader->getAddonNames();
$mockResponse = [
'foo' => 'bar',
'addons' => 'baz',
[
'composer' => 'foo/bar'
],
[
'composer' => 'bin/baz'
],
];

$this->assertSame('baz', $result($mockResponse));
$this->assertSame(['foo/bar', 'bin/baz'], $result($mockResponse));
}

public function testValueOfDoRequestIsReturned()
Expand Down

0 comments on commit 9b2d015

Please sign in to comment.