Skip to content

Commit 3871d65

Browse files
committed
refactor: more robust download handling and improved tests
1 parent 0ef6746 commit 3871d65

File tree

2 files changed

+81
-44
lines changed

2 files changed

+81
-44
lines changed

phpmyfaq/src/phpMyFAQ/Setup/Upgrade.php

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
3535
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
3636
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
37+
use Symfony\Contracts\HttpClient\HttpClientInterface;
3738
use ZipArchive;
3839

3940
class Upgrade extends AbstractSetup
@@ -48,14 +49,19 @@ class Upgrade extends AbstractSetup
4849

4950
private bool $isNightly;
5051

52+
private HttpClientInterface $httpClient;
53+
5154
public function __construct(
5255
protected System $system,
5356
private readonly Configuration $configuration,
57+
?HttpClientInterface $httpClient = null,
5458
) {
5559
parent::__construct($this->system);
5660

5761
$this->isNightly =
5862
$this->configuration->get(item: 'upgrade.releaseEnvironment') === ReleaseType::NIGHTLY->value;
63+
64+
$this->httpClient = $httpClient ?? HttpClient::create(['timeout' => 60]);
5965
}
6066

6167
/**
@@ -125,25 +131,40 @@ public function downloadPackage(string $version): string
125131
{
126132
$url = $this->getDownloadHost() . $this->getPath() . $this->getFilename($version);
127133

128-
$httpClient = HttpClient::create(['timeout' => 60]);
134+
$attempts = 3;
135+
$lastExceptionMessage = null;
129136

130-
try {
131-
$response = $httpClient->request('GET', $url);
137+
for ($i = 0; $i < $attempts; $i++) {
138+
try {
139+
$response = $this->httpClient->request('GET', $url);
132140

133-
if ($response->getStatusCode() !== 200) {
134-
throw new Exception('Cannot download package (HTTP Status: ' . $response->getStatusCode() . ').');
135-
}
141+
if ($response->getStatusCode() !== 200) {
142+
throw new Exception('Cannot download package (HTTP Status: ' . $response->getStatusCode() . ').');
143+
}
136144

137-
$package = $response->getContent();
145+
$package = $response->getContent();
138146

139-
file_put_contents($this->upgradeDirectory . DIRECTORY_SEPARATOR . $this->getFilename($version), $package);
147+
$targetPath = $this->upgradeDirectory . DIRECTORY_SEPARATOR . $this->getFilename($version);
148+
file_put_contents($targetPath, $package);
140149

141-
return $this->upgradeDirectory . DIRECTORY_SEPARATOR . $this->getFilename($version);
142-
} catch (
143-
TransportExceptionInterface|ClientExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface $e
144-
) {
145-
throw new Exception($e->getMessage());
150+
return $targetPath;
151+
} catch (
152+
TransportExceptionInterface|ClientExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface $exception
153+
) {
154+
$lastExceptionMessage = $exception->getMessage();
155+
156+
// After the last attempt, throw the exception outward
157+
if ($i === ($attempts - 1)) {
158+
throw new Exception('Download failed after ' . $attempts . ' attempts: ' . $lastExceptionMessage);
159+
}
160+
161+
// Short sleep to mitigate transient network issues
162+
usleep(250000); // 250ms
163+
}
146164
}
165+
166+
// Should not be reached, but for safety
167+
throw new Exception('Download failed: ' . ($lastExceptionMessage ?? 'unknown error'));
147168
}
148169

149170
/**
@@ -155,8 +176,7 @@ public function downloadPackage(string $version): string
155176
*/
156177
public function verifyPackage(string $path, string $version): bool
157178
{
158-
$httpClient = HttpClient::create(['timeout' => 30]);
159-
$response = $httpClient->request('GET', DownloadHostType::PHPMYFAQ->value . 'info/' . $version);
179+
$response = $this->httpClient->request('GET', DownloadHostType::PHPMYFAQ->value . 'info/' . $version);
160180

161181
try {
162182
$responseContent = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);
@@ -267,8 +287,9 @@ public function installPackage(callable $progressCallback): bool
267287
$currentFile = 0;
268288

269289
foreach ($sourceDirIterator as $item) {
270-
$source = $item->getPathName();
271-
$destination = $destinationDir . DIRECTORY_SEPARATOR . $sourceDirIterator->getSubPathName();
290+
$source = $item->getRealPath();
291+
$relativePath = str_replace($sourceDir, '', $source);
292+
$destination = $destinationDir . DIRECTORY_SEPARATOR . $relativePath;
272293

273294
if ($item->isDir()) {
274295
if (!is_dir($destination)) {

tests/phpMyFAQ/Setup/UpgradeTest.php

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,42 +7,67 @@
77
use phpMyFAQ\Database\Sqlite3;
88
use phpMyFAQ\Enums\DownloadHostType;
99
use phpMyFAQ\System;
10+
use PHPUnit\Framework\MockObject\MockObject;
1011
use PHPUnit\Framework\TestCase;
12+
use Symfony\Contracts\HttpClient\HttpClientInterface;
13+
use Symfony\Contracts\HttpClient\ResponseInterface;
1114

1215
class UpgradeTest extends TestCase
1316
{
1417
private Upgrade $upgrade;
18+
private HttpClientInterface|MockObject $httpClientMock;
19+
1520
protected function setUp(): void
1621
{
1722
parent::setUp();
1823

1924
$dbHandle = new Sqlite3();
2025
$dbHandle->connect(PMF_TEST_DIR . '/test.db', '', '');
2126
$configuration = new Configuration($dbHandle);
22-
$this->upgrade = new Upgrade(new System(), $configuration);
27+
28+
$this->httpClientMock = $this->createMock(HttpClientInterface::class);
29+
$this->upgrade = new Upgrade(new System(), $configuration, $this->httpClientMock);
2330
$this->upgrade->setUpgradeDirectory(PMF_CONTENT_DIR . '/upgrades');
2431
}
2532

2633
/**
2734
* @throws Exception
2835
*/
29-
public function testDownloadPackage(): void
36+
public function testDownloadPackageSuccessful(): void
37+
{
38+
$response = $this->createMock(ResponseInterface::class);
39+
$response->method('getStatusCode')->willReturn(200);
40+
$response->method('getContent')->willReturn('zip-binary-content');
41+
42+
$this->httpClientMock
43+
->expects($this->once())
44+
->method('request')
45+
->with('GET', $this->isString())
46+
->willReturn($response);
47+
48+
$path = $this->upgrade->downloadPackage('3.1.15');
49+
50+
$this->assertIsString($path);
51+
$this->assertFileExists($path);
52+
$this->assertSame('zip-binary-content', file_get_contents($path));
53+
}
54+
55+
/**
56+
* @throws Exception
57+
*/
58+
public function testDownloadPackageThrowsOnHttpError(): void
3059
{
31-
if (!$this->isNetworkAvailable()) {
32-
$this->markTestSkipped('Network not available');
33-
}
34-
35-
try {
36-
$actual = $this->upgrade->downloadPackage('3.1.15');
37-
$this->assertIsString($actual);
38-
} catch (Exception $e) {
39-
if (str_contains($e->getMessage(), 'timeout') || str_contains($e->getMessage(), 'connection')) {
40-
$this->markTestSkipped('Network timeout: ' . $e->getMessage());
41-
}
42-
throw $e;
43-
}
44-
45-
$this->expectException('phpMyFAQ\Core\Exception');
60+
$response = $this->createMock(ResponseInterface::class);
61+
$response->method('getStatusCode')->willReturn(404);
62+
63+
$this->httpClientMock
64+
->expects($this->once())
65+
->method('request')
66+
->willReturn($response);
67+
68+
$this->expectException(Exception::class);
69+
$this->expectExceptionMessage('Cannot download package (HTTP Status: 404).');
70+
4671
$this->upgrade->downloadPackage('1.2.3');
4772
}
4873

@@ -63,7 +88,7 @@ public function testCheckFilesystemValid(): void
6388
*/
6489
public function testCheckFilesystemMissingConfigFiles(): void
6590
{
66-
$this->expectException('phpMyFAQ\Core\Exception');
91+
$this->expectException('phpMyFAQ\\Core\\Exception');
6792
$this->expectExceptionMessage(
6893
'The files /content/core/config/constant.php and /content/core/config/database.php are missing.'
6994
);
@@ -98,13 +123,4 @@ public function testGetPathForNonNightly(): void
98123

99124
$this->assertEquals('', $this->upgrade->getPath());
100125
}
101-
102-
private function isNetworkAvailable(): bool
103-
{
104-
$context = stream_context_create([
105-
'http' => ['timeout' => 5]
106-
]);
107-
108-
return @file_get_contents('https://download.phpmyfaq.de', false, $context) !== false;
109-
}
110126
}

0 commit comments

Comments
 (0)