Skip to content

Commit bee639f

Browse files
committed
Added tests
1 parent 7e3bc78 commit bee639f

File tree

6 files changed

+311
-23
lines changed

6 files changed

+311
-23
lines changed

app/Providers/TUFServiceProvider.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace App\Providers;
44

55
use App\Models\TufMetadata;
6-
use App\TUF\DatabaseStorage;
6+
use App\TUF\EloquentModelStorage;
77
use App\TUF\HttpLoader;
88
use GuzzleHttp\Client;
99
use Illuminate\Support\Facades\App;
@@ -33,7 +33,7 @@ public function register()
3333
$sizeCheckingLoader = new SizeCheckingLoader($httpLoader);
3434

3535
// Setup storage
36-
$storage = new DatabaseStorage(TufMetadata::findOrFail(1));
36+
$storage = new EloquentModelStorage(TufMetadata::findOrFail(1));
3737

3838
// Create updater
3939
$updater = new Updater(

app/TUF/DatabaseStorage.php renamed to app/TUF/EloquentModelStorage.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
use App\Models\TufMetadata;
66
use Tuf\Metadata\StorageBase;
77

8-
class DatabaseStorage extends StorageBase
8+
class EloquentModelStorage extends StorageBase
99
{
1010
public const METADATA_COLUMNS = ['root', 'targets', 'snapshot', 'timestamp', 'mirrors'];
1111

1212
protected TufMetadata $model;
1313

14+
/**
15+
* @var array<string, string>
16+
*/
1417
protected array $container = [];
1518

1619
public function __construct(TufMetadata $model)
@@ -26,7 +29,6 @@ public function __construct(TufMetadata $model)
2629
}
2730
}
2831

29-
3032
public function read(string $name): ?string
3133
{
3234
return $this->container[$name] ?? null;
@@ -44,16 +46,14 @@ public function delete(string $name): void
4446

4547
public function persist(): bool
4648
{
47-
$data = [];
48-
4949
foreach (self::METADATA_COLUMNS as $column) {
5050
if (!\array_key_exists($column, $this->container)) {
5151
continue;
5252
}
5353

54-
$data[$column] = $this->container[$column];
54+
$this->model->$column = $this->container[$column];
5555
}
5656

57-
return $this->model->save($data);
57+
return $this->model->save();
5858
}
5959
}

app/TUF/TufFetcher.php

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,45 @@
33
namespace App\TUF;
44

55
use GuzzleHttp\Client;
6+
use Illuminate\Support\Collection;
67
use Illuminate\Support\Facades\App;
78
use Illuminate\Support\Facades\Cache;
89
use Tuf\Exception\MetadataException;
910
use Tuf\Metadata\StorageInterface;
1011

1112
class TufFetcher
1213
{
13-
protected Client $httpClient;
14-
1514
protected StorageInterface $updateStorage;
1615

1716
public function __construct()
1817
{
19-
$this->httpClient = App::make(Client::class);
2018
$this->updateStorage = App::make(StorageInterface::class);
2119
}
2220

23-
public function getReleases()
21+
public function getReleases(): mixed
2422
{
25-
return Cache::remember('cms_targets', config('autoupdates.tuf_repo_cachetime') * 60, function () {
26-
$targets = $this->updateStorage->getTargets();
27-
28-
if (is_null($targets)) {
29-
throw new MetadataException("Empty targetlist in metadata");
23+
// Cache response to avoid to make constant calls on the fly
24+
return Cache::remember(
25+
'cms_targets',
26+
(int) config('autoupdates.tuf_repo_cachetime') * 60, // @phpstan-ignore-line
27+
function () {
28+
$targets = $this->updateStorage->getTargets();
29+
30+
// Make sure we have a valid list of targets
31+
if (is_null($targets)) {
32+
throw new MetadataException("Empty targetlist in metadata");
33+
}
34+
35+
// Convert format
36+
return (new Collection($targets->getSigned()['targets']))
37+
->mapWithKeys(function (mixed $target) {
38+
if (!is_array($target) || empty($target['custom']) || !is_array($target['custom'])) {
39+
throw new MetadataException("Empty target custom attribute");
40+
}
41+
42+
return [$target['custom']['version'] => $target['custom']];
43+
});
3044
}
31-
32-
return collect($targets->getSigned()['targets'])
33-
->mapWithKeys(function ($target) {
34-
return [$target['custom']['version'] => $target['custom']];
35-
});
36-
});
45+
);
3746
}
3847
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
namespace Tests\Unit\TUF;
4+
5+
use App\Models\TufMetadata;
6+
use App\TUF\EloquentModelStorage;
7+
use Tests\TestCase;
8+
9+
class EloquentModelStorageTest extends TestCase
10+
{
11+
public function testConstructorWritesColumnMetadataToInternalStorage()
12+
{
13+
$model = $this->getModelMock(['root' => 'rootfoo']);
14+
$object = new EloquentModelStorage($model);
15+
16+
$this->assertEquals('rootfoo', $this->getInternalStorageValue($object)['root']);
17+
}
18+
19+
public function testConstructorIgnoresNonMetadataColumns()
20+
{
21+
$model = $this->getModelMock(['foobar' => 'aaa']);
22+
$object = new EloquentModelStorage($model);
23+
24+
$this->assertArrayNotHasKey('foobar', $this->getInternalStorageValue($object));
25+
}
26+
27+
public function testReadReturnsStorageValueForExistingColumns()
28+
{
29+
$object = new EloquentModelStorage($this->getModelMock(['root' => 'foobar']));
30+
$this->assertEquals('foobar', $object->read('root'));
31+
}
32+
33+
public function testReadReturnsNullForNonexistentColumns()
34+
{
35+
$object = new EloquentModelStorage($this->getModelMock([]));
36+
$this->assertNull($object->read('foobar'));
37+
}
38+
39+
public function testWriteUpdatesGivenInternalStorageValue()
40+
{
41+
$object = new EloquentModelStorage($this->getModelMock(['root' => 'foo']));
42+
$object->write('root', 'bar');
43+
44+
$this->assertEquals('bar', $this->getInternalStorageValue($object)['root']);
45+
}
46+
47+
public function testWriteCreatesNewInternalStorageValue()
48+
{
49+
$object = new EloquentModelStorage($this->getModelMock(['root' => 'foo']));
50+
$object->write('targets', 'bar');
51+
52+
$this->assertEquals('bar', $this->getInternalStorageValue($object)['targets']);
53+
}
54+
55+
public function testDeleteRemovesRowFromInternalStorage()
56+
{
57+
$object = new EloquentModelStorage($this->getModelMock(['root' => 'foo']));
58+
$object->delete('root');
59+
60+
$this->assertArrayNotHasKey('root', $this->getInternalStorageValue($object));
61+
}
62+
63+
public function testPersistUpdatesTableObjectState()
64+
{
65+
$modelMock = $this->getModelMock(['root' => 'foo', 'targets' => 'Joomla', 'nonexistent' => 'value']);
66+
67+
$modelMock
68+
->expects($this->once())
69+
->method('save')
70+
->willReturn(true);
71+
72+
$object = new EloquentModelStorage($modelMock);
73+
$this->assertTrue($object->persist());
74+
}
75+
76+
protected function getModelMock(array $mockData)
77+
{
78+
$model = $this->getMockBuilder(TufMetadata::class)
79+
->onlyMethods(['save'])
80+
->getMock();
81+
82+
// Write mock data to mock table
83+
foreach (EloquentModelStorage::METADATA_COLUMNS as $column) {
84+
$model->$column = (!empty($mockData[$column])) ? $mockData[$column] : null;
85+
}
86+
87+
return $model;
88+
}
89+
90+
protected function getInternalStorageValue($class)
91+
{
92+
$reflectionProperty = new \ReflectionProperty(EloquentModelStorage::class, 'container');
93+
94+
return $reflectionProperty->getValue($class);
95+
}
96+
}

tests/Unit/TUF/HttpLoaderTest.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
namespace Tests\Unit\TUF;
4+
5+
use App\TUF\HttpLoader;
6+
use GuzzleHttp\Client;
7+
use GuzzleHttp\Exception\RequestException;
8+
use GuzzleHttp\Psr7\Request;
9+
use GuzzleHttp\Psr7\Response;
10+
use GuzzleHttp\Psr7\Stream;
11+
use Tests\TestCase;
12+
use Tuf\Exception\RepoFileNotFound;
13+
14+
class HttpLoaderTest extends TestCase
15+
{
16+
protected const REPOPATHMOCK = 'https://example.org/tuftest/';
17+
18+
protected HttpLoader $object;
19+
20+
public function testLoaderQueriesCorrectUrl()
21+
{
22+
$responseBody = $this->createMock(Stream::class);
23+
24+
$object = new HttpLoader(
25+
self::REPOPATHMOCK,
26+
$this->getHttpClientMock(200, $responseBody, 'root.json')
27+
);
28+
29+
$object->load('root.json', 2048);
30+
}
31+
32+
public function testLoaderForwardsReturnedBodyFromHttpClient()
33+
{
34+
$responseBody = $this->createMock(Stream::class);
35+
36+
$object = new HttpLoader(
37+
self::REPOPATHMOCK,
38+
$this->getHttpClientMock(200, $responseBody, 'root.json')
39+
);
40+
41+
$this->assertSame(
42+
$responseBody,
43+
$object->load('root.json', 2048)->wait()
44+
);
45+
}
46+
47+
public function testLoaderThrowsExceptionForNon200Response()
48+
{
49+
$this->expectException(RepoFileNotFound::class);
50+
51+
$responseBody = $this->createMock(Stream::class);
52+
53+
$object = new HttpLoader(
54+
self::REPOPATHMOCK,
55+
$this->getHttpClientMock(400, $responseBody, 'root.json')
56+
);
57+
58+
$object->load('root.json', 2048);
59+
}
60+
61+
protected function getHttpClientMock(int $responseCode, Stream $responseBody, string $expectedFile)
62+
{
63+
$responseMock = $this->createMock(Response::class);
64+
$responseMock->method('getBody')->willReturn($responseBody);
65+
66+
$httpClientMock = $this->createMock(Client::class);
67+
68+
if ($responseCode !== 200) {
69+
$httpClientMock->expects($this->once())
70+
->method('get')
71+
->with(self::REPOPATHMOCK . $expectedFile)
72+
->willThrowException(new RequestException(
73+
"Request Exception",
74+
new Request('GET', self::REPOPATHMOCK . $expectedFile),
75+
new Response($responseCode)
76+
));
77+
} else {
78+
$httpClientMock->expects($this->once())
79+
->method('get')
80+
->with(self::REPOPATHMOCK . $expectedFile)
81+
->willReturn($responseMock);
82+
}
83+
84+
return $httpClientMock;
85+
}
86+
}

0 commit comments

Comments
 (0)