Skip to content

Commit 3cab28a

Browse files
committed
Implemented files uploading/replacing via HTTP
1 parent 8bdba61 commit 3cab28a

3 files changed

Lines changed: 225 additions & 5 deletions

File tree

src/Adapter/Http/HttpAdapter.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use GuzzleHttp\Client;
66
use GuzzleHttp\Exception\GuzzleException;
7+
use GuzzleHttp\Psr7\Utils;
78
use Psr\Log\LoggerInterface;
89
use Psr\Log\NullLogger;
910
use Sal\Seven\Model\Http\Authentication\HttpAuthentication;
@@ -285,6 +286,76 @@ public function patch(string $uri, array $parameters = [], ?string $json = null)
285286
);
286287
}
287288

289+
/**
290+
* Upload a file via POST request.
291+
*
292+
* @throws GuzzleException
293+
* @throws \RuntimeException
294+
*/
295+
public function upload(string $uri, \SplFileInfo $file): HttpResponse
296+
{
297+
$uri = empty($this->baseUri) ? $uri : "{$this->baseUri}{$uri}";
298+
299+
try {
300+
$stream = Utils::tryFopen($file->getPathname(), 'r');
301+
} catch (\RuntimeException) {
302+
throw new \RuntimeException("Cannot open file '{$file->getPathname()}' for reading.");
303+
}
304+
305+
$options = array_merge(
306+
$this->buildOptions(), [
307+
'body' => $stream,
308+
]
309+
);
310+
311+
$this->logger->debug('UPLOAD', [
312+
'uri' => $uri,
313+
'options' => $options,
314+
]);
315+
316+
$response = $this->client->post($uri, $options);
317+
318+
return new HttpResponse(
319+
$response->getStatusCode(),
320+
$response->getBody()
321+
);
322+
}
323+
324+
/**
325+
* Upload a file via PUT request, prepending to replace it if exists.
326+
*
327+
* @throws GuzzleException
328+
* @throws \RuntimeException
329+
*/
330+
public function replace(string $uri, \SplFileInfo $file): HttpResponse
331+
{
332+
$uri = empty($this->baseUri) ? $uri : "{$this->baseUri}{$uri}";
333+
334+
try {
335+
$stream = Utils::tryFopen($file->getPathname(), 'r');
336+
} catch (\RuntimeException) {
337+
throw new \RuntimeException("Cannot open file '{$file->getPathname()}' for reading.");
338+
}
339+
340+
$options = array_merge(
341+
$this->buildOptions(), [
342+
'body' => $stream,
343+
]
344+
);
345+
346+
$this->logger->debug('REPLACE', [
347+
'uri' => $uri,
348+
'options' => $options,
349+
]);
350+
351+
$response = $this->client->put($uri, $options);
352+
353+
return new HttpResponse(
354+
$response->getStatusCode(),
355+
$response->getBody()
356+
);
357+
}
358+
288359
public function getBaseUri(): ?string
289360
{
290361
return $this->baseUri;

src/Adapter/Http/HttpAdapterInterface.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ public function put(string $uri, array $parameters = [], ?string $json = null):
4545
*/
4646
public function patch(string $uri, array $parameters = [], ?string $json = null): HttpResponse;
4747

48+
/**
49+
* @throws GuzzleException
50+
* @throws \RuntimeException
51+
*/
52+
public function upload(string $uri, \SplFileInfo $file): HttpResponse;
53+
54+
/**
55+
* @throws GuzzleException
56+
* @throws \RuntimeException
57+
*/
58+
public function replace(string $uri, \SplFileInfo $file): HttpResponse;
59+
4860
public function getBaseUri(): ?string;
4961

5062
public function setBaseUri(string $baseUri): void;

tests/Adapter/Http/HttpAdapterTest.php

Lines changed: 142 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,18 +209,155 @@ public function testPatchWithFormParameters()
209209
$this->assertEquals('ok', (string) $response->getBody());
210210
}
211211

212-
public function testBearerAuthenticationHeader()
212+
public function testUploadFile(): void
213+
{
214+
$mockResponse = new Response(201, [], 'uploaded');
215+
216+
$filePath = sys_get_temp_dir().'/test_upload.txt';
217+
file_put_contents($filePath, 'test content');
218+
$file = new \SplFileInfo($filePath);
219+
220+
$client = $this->createMock(Client::class);
221+
$client->expects($this->once())
222+
->method('post')
223+
->with(
224+
'https://example.com/upload',
225+
$this->callback(
226+
function (mixed $options) use ($file): bool {
227+
return isset($options['body'])
228+
&& stream_get_contents($options['body']) === file_get_contents($file->getPathname());
229+
}
230+
)
231+
)
232+
->willReturn($mockResponse);
233+
234+
$adapter = new HttpAdapter($client);
235+
$adapter->setBaseUri('https://example.com');
236+
237+
$response = $adapter->upload('/upload', $file);
238+
239+
$this->assertEquals(201, $response->getStatusCode());
240+
$this->assertEquals('uploaded', (string) $response->getBody());
241+
242+
unlink($filePath);
243+
}
244+
245+
public function testReplaceFile(): void
246+
{
247+
$mockResponse = new Response(200, [], 'replaced');
248+
249+
$filePath = sys_get_temp_dir().'/test_replace.txt';
250+
file_put_contents($filePath, 'replace content');
251+
$file = new \SplFileInfo($filePath);
252+
253+
$client = $this->createMock(Client::class);
254+
$client->expects($this->once())
255+
->method('put')
256+
->with(
257+
'https://example.com/replace',
258+
$this->callback(
259+
function (mixed $options) use ($file): bool {
260+
return isset($options['body'])
261+
&& stream_get_contents($options['body']) === file_get_contents($file->getPathname());
262+
}
263+
)
264+
)
265+
->willReturn($mockResponse);
266+
267+
$adapter = new HttpAdapter($client);
268+
$adapter->setBaseUri('https://example.com');
269+
270+
$response = $adapter->replace('/replace', $file);
271+
272+
$this->assertEquals(200, $response->getStatusCode());
273+
$this->assertEquals('replaced', (string) $response->getBody());
274+
275+
unlink($filePath);
276+
}
277+
278+
public function testUploadFileThrowsExceptionIfFileCannotBeOpened(): void
279+
{
280+
$this->expectException(\RuntimeException::class);
281+
$this->expectExceptionMessage('Cannot open file');
282+
283+
$file = new \SplFileInfo('/path/to/nonexistent/file.txt');
284+
285+
$client = $this->createMock(Client::class);
286+
$adapter = new HttpAdapter($client);
287+
288+
$adapter->upload('/upload', $file);
289+
}
290+
291+
public function testReplaceFileThrowsExceptionIfFileCannotBeOpened(): void
292+
{
293+
$this->expectException(\RuntimeException::class);
294+
$this->expectExceptionMessage('Cannot open file');
295+
296+
$file = new \SplFileInfo('/path/to/nonexistent/file.txt');
297+
298+
$client = $this->createMock(Client::class);
299+
$adapter = new HttpAdapter($client);
300+
301+
$adapter->replace('/replace', $file);
302+
}
303+
304+
public function testUploadFileWithGuzzleException(): void
305+
{
306+
$this->expectException(\GuzzleHttp\Exception\GuzzleException::class);
307+
308+
$filePath = sys_get_temp_dir().'/test_upload.txt';
309+
file_put_contents($filePath, 'test content');
310+
$file = new \SplFileInfo($filePath);
311+
312+
$client = $this->createMock(Client::class);
313+
$client->method('post')->willThrowException(
314+
new class extends \Exception implements \GuzzleHttp\Exception\GuzzleException {}
315+
);
316+
317+
$adapter = new HttpAdapter($client);
318+
319+
$adapter->upload('/upload', $file);
320+
321+
unlink($filePath);
322+
}
323+
324+
public function testReplaceFileWithGuzzleException(): void
325+
{
326+
$this->expectException(\GuzzleHttp\Exception\GuzzleException::class);
327+
328+
$filePath = sys_get_temp_dir().'/test_replace.txt';
329+
file_put_contents($filePath, 'replace content');
330+
$file = new \SplFileInfo($filePath);
331+
332+
$client = $this->createMock(Client::class);
333+
$client->method('put')->willThrowException(
334+
new class extends \Exception implements \GuzzleHttp\Exception\GuzzleException {}
335+
);
336+
337+
$adapter = new HttpAdapter($client);
338+
339+
$adapter->replace('/replace', $file);
340+
341+
unlink($filePath);
342+
}
343+
344+
public function testBearerAuthenticationHeader(): void
213345
{
214346
$mockResponse = new Response(200, [], 'ok');
215347

216348
/** @var Client|MockObject $client */
217349
$client = $this->createMock(Client::class);
218350
$client->expects($this->once())
219351
->method('get')
220-
->with('https://example.com/test', $this->callback(function ($options) {
221-
return isset($options['headers']['Authorization'])
222-
&& 'Bearer TOKEN123' === $options['headers']['Authorization'];
223-
}))
352+
->with(
353+
'https://example.com/test',
354+
$this->callback(
355+
function (mixed $options): bool {
356+
return isset($options['headers']['Authorization'])
357+
&& 'Bearer TOKEN123' === $options['headers']['Authorization'];
358+
}
359+
)
360+
)
224361
->willReturn($mockResponse);
225362

226363
$adapter = new HttpAdapter($client);

0 commit comments

Comments
 (0)