Verified Commit 22fb1acb authored by Elias Häußler's avatar Elias Häußler 🐛
Browse files

[FEATURE] Use symfony/http-client and nyholm/psr-7 for requests

Resolves: #1
parent 064a4d3e
Pipeline #710 failed with stages
in 1 minute and 46 seconds
......@@ -20,16 +20,20 @@
},
"require": {
"php": "^7.1",
"ext-json": "*",
"composer-plugin-api": "^1.0 || ^2.0",
"eliashaeussler/composer-update-check": "~0.4",
"guzzlehttp/guzzle": "^6.0 || ^7.0",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^2.0",
"psr/http-client": "^1.0",
"spatie/emoji": "^2.0",
"symfony/http-client": "^4.4 || ^5.0",
"symfony/mailer": "^4.4 || ^5.0"
},
"require-dev": {
"composer/composer": "^1.0 || ^2.0",
"friendsofphp/php-cs-fixer": "^2.17",
"nyholm/psr7": "^1.3",
"guzzlehttp/guzzle": "^6.5 || ^7.0",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"rpkamp/mailhog-client": "^0.3 || ^0.4 || ^0.5"
},
......
......@@ -24,13 +24,14 @@ namespace EliasHaeussler\ComposerUpdateReporter\Service;
use Composer\IO\IOInterface;
use EliasHaeussler\ComposerUpdateCheck\Package\OutdatedPackage;
use EliasHaeussler\ComposerUpdateCheck\Package\UpdateCheckResult;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\RequestOptions;
use EliasHaeussler\ComposerUpdateReporter\Traits\RemoteServiceTrait;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Uri;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\UriInterface;
use Spatie\Emoji\Emoji;
use Spatie\Emoji\Exceptions\UnknownCharacter;
use Symfony\Component\HttpClient\Psr18Client;
/**
* GitLab
......@@ -40,6 +41,8 @@ use Spatie\Emoji\Exceptions\UnknownCharacter;
*/
class GitLab implements ServiceInterface
{
use RemoteServiceTrait;
/**
* @var UriInterface
*/
......@@ -50,11 +53,6 @@ class GitLab implements ServiceInterface
*/
private $authorizationKey;
/**
* @var Client
*/
private $client;
/**
* @var bool
*/
......@@ -64,12 +62,8 @@ class GitLab implements ServiceInterface
{
$this->uri = $uri;
$this->authorizationKey = $authorizationKey;
$this->client = new Client([
'base_uri' => (string) $this->uri,
RequestOptions::HEADERS => [
'Authorization' => 'Bearer ' . $this->authorizationKey,
],
]);
$this->requestFactory = new Psr17Factory();
$this->client = new Psr18Client();
$this->validateUri();
$this->validateAuthorizationKey();
......@@ -117,7 +111,7 @@ class GitLab implements ServiceInterface
/**
* @inheritDoc
* @throws GuzzleException
* @throws ClientExceptionInterface
*/
public function report(UpdateCheckResult $result, IOInterface $io): bool
{
......@@ -141,7 +135,7 @@ class GitLab implements ServiceInterface
if (!$this->json) {
$io->write(Emoji::rocket() . ' Sending report to GitLab...');
}
$response = $this->client->post('', [RequestOptions::JSON => $payload]);
$response = $this->sendRequest($payload, ['Authorization' => 'Bearer ' . $this->authorizationKey,]);
$successful = $response->getStatusCode() < 400;
// Print report state
......
......@@ -25,13 +25,14 @@ use Composer\IO\IOInterface;
use EliasHaeussler\ComposerUpdateCheck\Package\OutdatedPackage;
use EliasHaeussler\ComposerUpdateCheck\Package\UpdateCheckResult;
use EliasHaeussler\ComposerUpdateReporter\Traits\PackageProviderLinkTrait;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\RequestOptions;
use EliasHaeussler\ComposerUpdateReporter\Traits\RemoteServiceTrait;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Uri;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\UriInterface;
use Spatie\Emoji\Emoji;
use Spatie\Emoji\Exceptions\UnknownCharacter;
use Symfony\Component\HttpClient\Psr18Client;
/**
* Mattermost
......@@ -42,6 +43,7 @@ use Spatie\Emoji\Exceptions\UnknownCharacter;
class Mattermost implements ServiceInterface
{
use PackageProviderLinkTrait;
use RemoteServiceTrait;
/**
* @var UriInterface
......@@ -58,11 +60,6 @@ class Mattermost implements ServiceInterface
*/
private $username;
/**
* @var Client
*/
private $client;
/**
* @var bool
*/
......@@ -73,7 +70,8 @@ class Mattermost implements ServiceInterface
$this->uri = $uri;
$this->channelName = $channelName;
$this->username = $username;
$this->client = new Client(['base_uri' => (string) $this->uri]);
$this->requestFactory = new Psr17Factory();
$this->client = new Psr18Client();
$this->validateUri();
$this->validateChannelName();
......@@ -129,7 +127,7 @@ class Mattermost implements ServiceInterface
/**
* @inheritDoc
* @throws GuzzleException
* @throws ClientExceptionInterface
*/
public function report(UpdateCheckResult $result, IOInterface $io): bool
{
......@@ -161,7 +159,7 @@ class Mattermost implements ServiceInterface
if (!$this->json) {
$io->write(Emoji::rocket() . ' Sending report to Mattermost...');
}
$response = $this->client->post('', [RequestOptions::JSON => $payload]);
$response = $this->sendRequest($payload);
$successful = $response->getStatusCode() < 400;
// Print report state
......
......@@ -25,13 +25,14 @@ use Composer\IO\IOInterface;
use EliasHaeussler\ComposerUpdateCheck\Package\OutdatedPackage;
use EliasHaeussler\ComposerUpdateCheck\Package\UpdateCheckResult;
use EliasHaeussler\ComposerUpdateReporter\Traits\PackageProviderLinkTrait;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\RequestOptions;
use EliasHaeussler\ComposerUpdateReporter\Traits\RemoteServiceTrait;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Uri;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\UriInterface;
use Spatie\Emoji\Emoji;
use Spatie\Emoji\Exceptions\UnknownCharacter;
use Symfony\Component\HttpClient\Psr18Client;
/**
* Slack
......@@ -42,17 +43,13 @@ use Spatie\Emoji\Exceptions\UnknownCharacter;
class Slack implements ServiceInterface
{
use PackageProviderLinkTrait;
use RemoteServiceTrait;
/**
* @var UriInterface
*/
private $uri;
/**
* @var Client
*/
private $client;
/**
* @var bool
*/
......@@ -61,7 +58,8 @@ class Slack implements ServiceInterface
public function __construct(UriInterface $uri)
{
$this->uri = $uri;
$this->client = new Client(['base_uri' => (string) $this->uri]);
$this->requestFactory = new Psr17Factory();
$this->client = new Psr18Client();
$this->validateUri();
}
......@@ -96,7 +94,7 @@ class Slack implements ServiceInterface
/**
* @inheritDoc
* @throws GuzzleException
* @throws ClientExceptionInterface
*/
public function report(UpdateCheckResult $result, IOInterface $io): bool
{
......@@ -119,7 +117,7 @@ class Slack implements ServiceInterface
if (!$this->json) {
$io->write(Emoji::rocket() . ' Sending report to Slack...');
}
$response = $this->client->post('', [RequestOptions::JSON => $payload]);
$response = $this->sendRequest($payload);
$successful = $response->getStatusCode() < 400;
// Print report state
......
......@@ -25,12 +25,14 @@ use Composer\IO\IOInterface;
use EliasHaeussler\ComposerUpdateCheck\Package\OutdatedPackage;
use EliasHaeussler\ComposerUpdateCheck\Package\UpdateCheckResult;
use EliasHaeussler\ComposerUpdateReporter\Traits\PackageProviderLinkTrait;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\RequestOptions;
use EliasHaeussler\ComposerUpdateReporter\Traits\RemoteServiceTrait;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Uri;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\UriInterface;
use Spatie\Emoji\Emoji;
use Spatie\Emoji\Exceptions\UnknownCharacter;
use Symfony\Component\HttpClient\Psr18Client;
/**
* Teams
......@@ -41,17 +43,13 @@ use Spatie\Emoji\Exceptions\UnknownCharacter;
class Teams implements ServiceInterface
{
use PackageProviderLinkTrait;
use RemoteServiceTrait;
/**
* @var UriInterface
*/
private $uri;
/**
* @var Client
*/
private $client;
/**
* @var bool
*/
......@@ -60,7 +58,8 @@ class Teams implements ServiceInterface
public function __construct(UriInterface $uri)
{
$this->uri = $uri;
$this->client = new Client(['base_uri' => (string)$this->uri]);
$this->requestFactory = new Psr17Factory();
$this->client = new Psr18Client();
$this->validateUri();
}
......@@ -93,6 +92,10 @@ class Teams implements ServiceInterface
return is_array($extra) && (bool)($extra['enable'] ?? false);
}
/**
* @inheritDoc
* @throws ClientExceptionInterface
*/
public function report(UpdateCheckResult $result, IOInterface $io): bool
{
$outdatedPackages = $result->getOutdatedPackages();
......@@ -118,7 +121,7 @@ class Teams implements ServiceInterface
if (!$this->json) {
$io->write(Emoji::rocket() . ' Sending report to MS Teams...');
}
$response = $this->client->post('', [RequestOptions::JSON => $payload]);
$response = $this->sendRequest($payload);
$successful = $response->getStatusCode() < 400;
// Print report state
......
<?php
declare(strict_types=1);
/*
* This file is part of the Composer package "eliashaeussler/composer-update-reporter".
*
* Copyright (C) 2021 Elias Häußler <elias@haeussler.dev>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace EliasHaeussler\ComposerUpdateReporter\Traits;
use Nyholm\Psr7\Stream;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseInterface;
/**
* RemoteServiceTrait
*
* @author Elias Häußler <elias@haeussler.dev>
* @license GPL-3.0-or-later
*/
trait RemoteServiceTrait
{
/**
* @var RequestFactoryInterface
*/
private $requestFactory;
/**
* @var ClientInterface
*/
private $client;
/**
* @param array $payload
* @param array $headers
* @return ResponseInterface
* @throws ClientExceptionInterface
*/
protected function sendRequest(array $payload, array $headers = []): ResponseInterface
{
$request = $this->requestFactory->createRequest('POST', $this->uri)
->withBody(Stream::create(json_encode($payload)));
$headers = array_merge($headers, ['Accept' => 'application/json']);
foreach ($headers as $name => $value) {
$request = $request->withHeader($name, $value);
}
return $this->client->sendRequest($request);
}
public function setClient(ClientInterface $client): self
{
$this->client = $client;
return $this;
}
}
<?php
declare(strict_types=1);
/*
* This file is part of the Composer package "eliashaeussler/composer-update-reporter".
*
* Copyright (C) 2021 Elias Häußler <elias@haeussler.dev>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace EliasHaeussler\ComposerUpdateReporter\Tests\Unit;
use EliasHaeussler\ComposerUpdateReporter\Util;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* ClientMockTrait
*
* @author Elias Häußler <elias@haeussler.dev>
* @license GPL-3.0-or-later
*/
trait ClientMockTrait
{
/**
* @var MockHandler
*/
private $mockHandler;
/**
* @var array{request: RequestInterface, response: ResponseInterface|null, error: string|mixed, options: array}
*/
private $requestContainer = [];
private function getClient(): ClientInterface
{
$this->mockHandler = $this->mockHandler ?? new MockHandler();
$this->mockHandler->reset();
$history = Middleware::history($this->requestContainer);
$handlerStack = HandlerStack::create($this->mockHandler);
$handlerStack->push($history);
return new Client(['handler' => $handlerStack]);
}
protected function assertPayloadOfLastRequestContainsSubset(array $expectedPayloadSubset): void
{
$payload = $this->getPayloadOfLastRequest();
self::assertSame([], Util::arrayDiffRecursive($expectedPayloadSubset, $payload));
}
protected function getPayloadOfLastRequest(): array
{
self::assertNotEmpty($this->requestContainer, 'Unable to find last request');
/** @var RequestInterface $request */
$request = end($this->requestContainer)['request'];
self::assertInstanceOf(RequestInterface::class, $request);
$request->getBody()->rewind();
return json_decode($request->getBody()->getContents(), true);
}
}
......@@ -26,13 +26,11 @@ use EliasHaeussler\ComposerUpdateCheck\Package\OutdatedPackage;
use EliasHaeussler\ComposerUpdateCheck\Package\UpdateCheckResult;
use EliasHaeussler\ComposerUpdateReporter\Service\GitLab;
use EliasHaeussler\ComposerUpdateReporter\Tests\Unit\AbstractTestCase;
use EliasHaeussler\ComposerUpdateReporter\Tests\Unit\ClientMockTrait;
use EliasHaeussler\ComposerUpdateReporter\Tests\Unit\TestEnvironmentTrait;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\RequestOptions;
use Prophecy\Argument;
use Nyholm\Psr7\Response;
use Nyholm\Psr7\Uri;
use Psr\Http\Client\ClientExceptionInterface;
/**
* GitLabTest
......@@ -42,6 +40,7 @@ use Prophecy\Argument;
*/
class GitLabTest extends AbstractTestCase
{
use ClientMockTrait;
use TestEnvironmentTrait;
/**
......@@ -174,7 +173,7 @@ class GitLabTest extends AbstractTestCase
/**
* @test
* @throws GuzzleException
* @throws ClientExceptionInterface
*/
public function reportSkipsReportIfNoPackagesAreOutdated(): void
{
......@@ -190,8 +189,7 @@ class GitLabTest extends AbstractTestCase
* @dataProvider reportSendsUpdateReportSuccessfullyDataProvider
* @param bool $insecure
* @param string $expectedSecurityNotice
* @throws GuzzleException
* @throws \ReflectionException
* @throws ClientExceptionInterface
*/
public function reportSendsUpdateReportSuccessfully(bool $insecure, string $expectedSecurityNotice): void
{
......@@ -200,29 +198,22 @@ class GitLabTest extends AbstractTestCase
]);
$io = new BufferIO();
// Prophesize Client
$clientProphecy = $this->prophesize(Client::class);
$clientProphecy->post('', [
RequestOptions::JSON => [
'title' => '1 outdated package',
'foo/foo' => 'Outdated version: 1.0.0' . $expectedSecurityNotice . ', new version: 1.0.5',
],
])->willReturn(new Response())->shouldBeCalledOnce();
// Inject client prophecy into subject
$reflectionClass = new \ReflectionClass($this->subject);
$clientProperty = $reflectionClass->getProperty('client');
$clientProperty->setAccessible(true);
$clientProperty->setValue($this->subject, $clientProphecy->reveal());
$this->subject->setClient($this->getClient());
$this->mockHandler->append(new Response());
static::assertTrue($this->subject->report($result, $io));
static::assertStringContainsString('GitLab report was successful.', $io->getOutput());
$expectedPayloadSubset = [
'title' => '1 outdated package',
'foo/foo' => 'Outdated version: 1.0.0' . $expectedSecurityNotice . ', new version: 1.0.5',
];
$this->assertPayloadOfLastRequestContainsSubset($expectedPayloadSubset);
}
/**
* @test
* @throws GuzzleException
* @throws \ReflectionException
* @throws ClientExceptionInterface
*/
public function reportsPrintsErrorOnErroneousReport(): void
{
......@@ -231,17 +222,8 @@ class GitLabTest extends AbstractTestCase
]);
$io = new BufferIO();
// Prophesize Client
$clientProphecy = $this->prophesize(Client::class);
$clientProphecy->post('', Argument::type('array'))
->willReturn(new Response(404))
->shouldBeCalledOnce();
// Inject client prophecy into subject
$reflectionClass = new \ReflectionClass($this->subject);
$clientProperty = $reflectionClass->getProperty('client');
$clientProperty->setAccessible(true);
$clientProperty->setValue($this->subject, $clientProphecy->reveal());
$this->subject->setClient($this->getClient());
$this->mockHandler->append(new Response(404));
static::assertFalse($this->subject->report($result, $io));
static::assertStringContainsString('Error during GitLab report.', $io->getOutput());
......
......@@ -26,14 +26,11 @@ use EliasHaeussler\ComposerUpdateCheck\Package\OutdatedPackage;
use EliasHaeussler\ComposerUpdateCheck\Package\UpdateCheckResult;
use EliasHaeussler\ComposerUpdateReporter\Service\Mattermost;
use EliasHaeussler\ComposerUpdateReporter\Tests\Unit\AbstractTestCase;
use EliasHaeussler\ComposerUpdateReporter\Tests\Unit\ClientMockTrait;
use EliasHaeussler\ComposerUpdateReporter\Tests\Unit\TestEnvironmentTrait;
use EliasHaeussler\ComposerUpdateReporter\Util;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\RequestOptions;
use Prophecy\Argument;
use Nyholm\Psr7\Response;
use Nyholm\Psr7\Uri;
use Psr\Http\Client\ClientExceptionInterface;
/**
* MattermostTest
......@@ -43,6 +40,7 @@ use Prophecy\Argument;
*/
class MattermostTest extends AbstractTestCase
{
use ClientMockTrait;
use TestEnvironmentTrait;
/**
......@@ -180,7 +178,7 @@ class MattermostTest extends AbstractTestCase
/**
* @test
* @throws GuzzleException
* @throws ClientExceptionInterface
*/
public function reportSkipsReportIfNoPackagesAreOutdated(): void
{
......@@ -196,8 +194,7 @@ class MattermostTest extends AbstractTestCase
* @dataProvider reportSendsUpdateReportSuccessfullyDataProvider
* @param bool $insecure
* @param string $expectedSecurityNotice
* @throws GuzzleException
* @throws \ReflectionException
* @throws ClientExceptionInterface
*/
public function reportSendsUpdateReportSuccessfully(bool $insecure, string $expectedSecurityNotice): void
{
......@@ -206,45 +203,32 @@ class MattermostTest extends AbstractTestCase
]);
$io = new BufferIO();
// Prophesize Client
$clientProphecy = $this->prophesize(Client::class);
/** @noinspection PhpParamsInspection */
$clientProphecy->post('', Argument::allOf(
Argument::that(function (array $argument) {
return Util::arrayDiffRecursive([
RequestOptions::JSON => [
'channel' => 'foo',
'attachments' => [
[
'color' => '#EE0000',
],
],
'username' => 'baz',
],
], $argument) === [];
}),
Argument::that(function (array $argument) use ($expectedSecurityNotice) {
$text = $argument[RequestOptions::JSON]['attachments'][0]['text'] ?? null;
$expected = '[foo/foo](https://packagist.org/packages/foo/foo#1.0.5) | 1.0.0' . $expectedSecurityNotice . ' | **1.0.5**';
static::assertStringContainsString($expected, $text);
return true;
})
))->willReturn(new Response())->shouldBeCalledOnce();
// Inject client prophecy into subject
$reflectionClass = new \ReflectionClass($this->subject);
$clientProperty = $re