refactor: use HTTP client wrapper of Laravel

This commit is contained in:
Pig Fang 2020-03-09 17:18:36 +08:00
parent 6463a33744
commit 5d426e8c92
9 changed files with 146 additions and 253 deletions

View File

@ -8,16 +8,16 @@ use App\Services\Unzip;
use Composer\CaBundle\CaBundle;
use Composer\Semver\Comparator;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
class MarketController extends Controller
{
public function marketData(PluginManager $manager, Client $client)
public function marketData(PluginManager $manager)
{
$plugins = $this->fetch($client)->map(function ($item) use ($manager) {
$plugins = $this->fetch()->map(function ($item) use ($manager) {
$plugin = $manager->get($item['name']);
if ($plugin) {
@ -40,14 +40,10 @@ class MarketController extends Controller
return $plugins;
}
public function download(
Request $request,
PluginManager $manager,
Client $client,
Unzip $unzip
) {
public function download(Request $request, PluginManager $manager, Unzip $unzip)
{
$name = $request->input('name');
$plugins = $this->fetch($client);
$plugins = $this->fetch();
$metadata = $plugins->firstWhere('name', $name);
if (!$metadata) {
@ -64,34 +60,33 @@ class MarketController extends Controller
}
$path = tempnam(sys_get_temp_dir(), $name);
try {
$client->get($metadata['dist']['url'], [
'sink' => $path,
'verify' => CaBundle::getSystemCaRootBundlePath(),
]);
$response = Http::withOptions([
'sink' => $path,
'verify' => CaBundle::getSystemCaRootBundlePath(),
])->get($metadata['dist']['url']);
if ($response->ok()) {
$unzip->extract($path, $manager->getPluginsDirs()->first());
} catch (Exception $e) {
report($e);
return json(trans('admin.download.errors.download', ['error' => $e->getMessage()]), 1);
return json(trans('admin.plugins.market.install-success'), 0);
} else {
return json(trans('admin.download.errors.download', ['error' => $response->status()]), 1);
}
return json(trans('admin.plugins.market.install-success'), 0);
}
protected function fetch(Client $client): Collection
protected function fetch(): Collection
{
$plugins = collect(explode(',', config('plugins.registry')))
->map(function ($registry) use ($client) {
try {
$body = $client->get(trim($registry), [
'verify' => CaBundle::getSystemCaRootBundlePath(),
])->getBody();
} catch (Exception $e) {
throw new Exception(trans('admin.plugins.market.connection-error', ['error' => $e->getMessage()]));
}
->map(function ($registry) {
$response = Http::withOptions([
'verify' => CaBundle::getSystemCaRootBundlePath(),
])->get(trim($registry));
return Arr::get(json_decode($body, true), 'packages', []);
if ($response->ok()) {
return $response->json()['packages'];
} else {
throw new Exception(trans('admin.plugins.market.connection-error', ['error' => $response->status()]));
}
})
->flatten(1);

View File

@ -6,21 +6,19 @@ use App\Services\Unzip;
use Cache;
use Composer\CaBundle\CaBundle;
use Composer\Semver\Comparator;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Contracts\Console\Kernel as Artisan;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use Symfony\Component\Finder\SplFileInfo;
class UpdateController extends Controller
{
const SPEC = 2;
public function showUpdatePage(Client $client)
public function showUpdatePage()
{
$info = $this->getUpdateInfo($client);
$info = $this->getUpdateInfo();
$canUpdate = $this->canUpdate(Arr::get($info, 'info'));
return view('admin.update', [
@ -33,30 +31,30 @@ class UpdateController extends Controller
]);
}
public function download(Unzip $unzip, Filesystem $filesystem, Client $client)
public function download(Unzip $unzip, Filesystem $filesystem)
{
$info = $this->getUpdateInfo($client);
$info = $this->getUpdateInfo();
if (!$info['ok'] || !$this->canUpdate($info['info'])['can']) {
return json(trans('admin.update.info.up-to-date'), 1);
}
$info = $info['info'];
$path = tempnam(sys_get_temp_dir(), 'bs');
try {
$client->get($info['url'], [
'sink' => $path,
'verify' => CaBundle::getSystemCaRootBundlePath(),
]);
$response = Http::withOptions([
'sink' => $path,
'verify' => CaBundle::getSystemCaRootBundlePath(),
])->get($info['url']);
if ($response->ok()) {
$unzip->extract($path, base_path());
// Delete options cache. This allows us to update the version.
$filesystem->delete(storage_path('options.php'));
return json(trans('admin.update.complete'), 0);
} catch (Exception $e) {
report($e);
return json(trans('admin.download.errors.download', ['error' => $e->getMessage()]), 1);
} else {
return json(trans('admin.download.errors.download', ['error' => $response->status()]), 1);
}
}
@ -82,21 +80,21 @@ class UpdateController extends Controller
return view('setup.updates.success');
}
protected function getUpdateInfo(Client $client)
protected function getUpdateInfo()
{
try {
$response = $client->request('GET', config('app.update_source'), [
'verify' => CaBundle::getSystemCaRootBundlePath(),
]);
$info = json_decode($response->getBody(), true);
$response = Http::withOptions([
'verify' => CaBundle::getSystemCaRootBundlePath(),
])->get(config('app.update_source'));
if ($response->ok()) {
$info = $response->json();
if (Arr::get($info, 'spec') === self::SPEC) {
return ['ok' => true, 'info' => $info];
} else {
return ['ok' => false, 'error' => trans('admin.update.errors.spec')];
}
} catch (RequestException $e) {
return ['ok' => false, 'error' => $e->getMessage()];
} else {
return ['ok' => false, 'error' => 'HTTP status code: '.$response->status()];
}
}

View File

@ -2,38 +2,24 @@
namespace App\Rules;
use Composer\CaBundle\CaBundle;
use Gregwar\Captcha\CaptchaBuilder;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Facades\Http;
class Captcha implements Rule
{
protected $client;
public function __construct(\GuzzleHttp\Client $client)
{
$this->client = $client;
}
public function passes($attribute, $value)
{
$secretkey = option('recaptcha_secretkey');
if ($secretkey) {
try {
$response = $this->client->post('https://www.recaptcha.net/recaptcha/api/siteverify', [
'form_params' => [
'secret' => $secretkey,
'response' => $value,
],
'verify' => \Composer\CaBundle\CaBundle::getSystemCaRootBundlePath(),
]);
if ($response->getStatusCode() == 200) {
$body = json_decode((string) $response->getBody());
return $body->success;
}
} catch (\GuzzleHttp\Exception\RequestException $e) {
return false;
}
return Http::asForm()
->withOptions(['verify' => CaBundle::getSystemCaRootBundlePath()])
->post('https://www.recaptcha.net/recaptcha/api/siteverify', [
'secret' => $secretkey,
'response' => $value,
])
->json()['success'];
}
$builder = new CaptchaBuilder(session()->pull('captcha'));

View File

@ -217,6 +217,7 @@ return [
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Http' => Illuminate\Support\Facades\Http::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,

View File

@ -1,69 +0,0 @@
<?php
namespace Tests\Concerns;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
/**
* @see http://docs.guzzlephp.org/en/stable/testing.html
* @see https://christrombley.me/blog/testing-guzzle-6-responses-with-laravel
*/
trait MocksGuzzleClient
{
/**
* @var MockHandler
*/
public $guzzleMockHandler;
/**
* Set up for mocking Guzzle HTTP client.
*
* @param Response|RequestException $responses
*
* @return void
*/
public function setupGuzzleClientMock($responses = null)
{
$this->guzzleMockHandler = new MockHandler($responses);
$handler = HandlerStack::create($this->guzzleMockHandler);
$client = new Client(['handler' => $handler]);
// Inject to Laravel service container
$this->app->instance(Client::class, $client);
}
/**
* Add responses to Guzzle client's mock queue.
* Pass a Response or RequestException instance, or an array of them.
*
* @param array|Response|RequestException|int $response
* @param array $headers
* @param string $body
* @param string $version
* @param string|null $reason
*/
public function appendToGuzzleQueue($response = 200, $headers = [], $body = '', $version = '1.1', $reason = null)
{
if (!$this->guzzleMockHandler) {
$this->setupGuzzleClientMock();
}
if (is_array($response)) {
foreach ($response as $single) {
$this->appendToGuzzleQueue($single);
}
return;
}
if ($response instanceof Response || $response instanceof RequestException) {
return $this->guzzleMockHandler->append($response);
}
return $this->guzzleMockHandler->append(new Response($response, $headers, $body, $version, $reason));
}
}

View File

@ -6,6 +6,7 @@ use App\Events;
use App\Mail\ForgotPassword;
use App\Models\Player;
use App\Models\User;
use App\Rules\Captcha;
use App\Services\Facades\Option;
use Cache;
use Event;
@ -25,11 +26,7 @@ class AuthControllerTest extends TestCase
protected function setUp(): void
{
parent::setUp();
app()->instance(\App\Rules\Captcha::class, new class() extends \App\Rules\Captcha {
public function __construct(\GuzzleHttp\Client $client = null)
{
}
app()->instance(Captcha::class, new class() extends Captcha {
public function passes($attribute, $value)
{
return true;

View File

@ -5,15 +5,10 @@ namespace Tests;
use App\Services\Plugin;
use App\Services\PluginManager;
use App\Services\Unzip;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Tests\Concerns\MocksGuzzleClient;
use Illuminate\Support\Facades\Http;
class MarketControllerTest extends TestCase
{
use MocksGuzzleClient;
protected function setUp(): void
{
parent::setUp();
@ -22,12 +17,37 @@ class MarketControllerTest extends TestCase
public function testDownload()
{
$this->setupGuzzleClientMock();
Http::fake([
config('plugins.registry') => Http::sequence()
->push(['version' => 1, 'packages' => []])
->push([
'version' => 1,
'packages' => [
[
'name' => 'fake',
'version' => '0.0.0',
'require' => ['a' => '^4.0.0'],
],
],
])
->whenEmpty([
'version' => 1,
'packages' => [
[
'name' => 'fake',
'version' => '0.0.0',
'dist' => [
'url' => 'http://nowhere.test/',
'shasum' => 'deadbeef',
],
],
],
]),
'http://nowhere.test/' => Http::sequence()
->pushStatus(404)
->pushStatus(200),
]);
$this->appendToGuzzleQueue(200, [], json_encode([
'version' => 1,
'packages' => [],
]));
$this->postJson('/admin/plugins/market/download', ['name' => 'nope'])
->assertJson([
'code' => 1,
@ -35,14 +55,6 @@ class MarketControllerTest extends TestCase
]);
// Unresolved plugin.
$fakeRegistry = json_encode(['packages' => [
[
'name' => 'fake',
'version' => '0.0.0',
'require' => ['a' => '^4.0.0'],
],
]]);
$this->appendToGuzzleQueue([new Response(200, [], $fakeRegistry)]);
$this->postJson('/admin/plugins/market/download', ['name' => 'fake'])
->assertJson([
'message' => trans('admin.plugins.market.unresolved'),
@ -55,24 +67,9 @@ class MarketControllerTest extends TestCase
]);
// Download
$fakeRegistry = json_encode(['packages' => [
[
'name' => 'fake',
'version' => '0.0.0',
'dist' => ['url' => 'http://nowhere.test/', 'shasum' => 'deadbeef'],
],
]]);
$this->appendToGuzzleQueue([
new Response(200, [], $fakeRegistry),
new Response(404),
]);
$this->postJson('/admin/plugins/market/download', ['name' => 'fake'])
->assertJson(['code' => 1]);
$this->appendToGuzzleQueue([
new Response(200, [], $fakeRegistry),
new Response(200),
]);
$this->mock(Unzip::class, function ($mock) {
$mock->shouldReceive('extract')->once();
});
@ -85,29 +82,31 @@ class MarketControllerTest extends TestCase
public function testMarketData()
{
$this->setupGuzzleClientMock([
new RequestException('Connection Error', new Request('POST', '')),
new Response(200, [], json_encode(['version' => 1, 'packages' => [
[
'name' => 'fake1',
'title' => 'Fake',
'version' => '1.0.0',
'description' => '',
'author' => '',
'dist' => [],
'require' => [],
Http::fakeSequence()
->pushStatus(404)
->push([
'version' => 1,
'packages' => [
[
'name' => 'fake1',
'title' => 'Fake',
'version' => '1.0.0',
'description' => '',
'author' => '',
'dist' => [],
'require' => [],
],
[
'name' => 'fake2',
'title' => 'Fake',
'version' => '0.0.0',
'description' => '',
'author' => '',
'dist' => [],
'require' => [],
],
],
[
'name' => 'fake2',
'title' => 'Fake',
'version' => '0.0.0',
'description' => '',
'author' => '',
'dist' => [],
'require' => [],
],
]])),
]);
]);
$this->getJson('/admin/plugins/market/list')->assertStatus(500);

View File

@ -3,63 +3,55 @@
namespace Tests;
use App\Services\Unzip;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Illuminate\Contracts\Console\Kernel as Artisan;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Http;
use Symfony\Component\Finder\SplFileInfo;
use Tests\Concerns\MocksGuzzleClient;
class UpdateControllerTest extends TestCase
{
use DatabaseTransactions;
use MocksGuzzleClient;
protected function setUp(): void
{
parent::setUp();
$this->actAs('superAdmin');
$this->actingAs(factory(\App\Models\User::class)->states('superAdmin')->create());
}
public function testShowUpdatePage()
{
$this->setupGuzzleClientMock();
Http::fakeSequence()
->pushStatus(404)
->push($this->fakeUpdateInfo('8.9.3', ['spec' => 0]))
->push($this->fakeUpdateInfo('8.9.3', ['php' => '100.0.0']))
->push($this->fakeUpdateInfo('8.9.3'));
// Can't connect to update source
$this->appendToGuzzleQueue([
new RequestException('Connection Error', new Request('GET', 'whatever')),
]);
$this->get('/admin/update')->assertSee(config('app.version'));
// Missing `spec` field
$this->appendToGuzzleQueue([
new Response(200, [], $this->mockFakeUpdateInfo('8.9.3', ['spec' => 0])),
]);
$this->get('/admin/update')->assertSee(trans('admin.update.errors.spec'));
// Low PHP version
$this->appendToGuzzleQueue([
new Response(200, [], $this->mockFakeUpdateInfo('8.9.3', ['php' => '100.0.0'])),
]);
$this->get('/admin/update')->assertSee(trans('admin.update.errors.php', ['version' => '100.0.0']));
// New version available
$this->appendToGuzzleQueue([
new Response(200, [], $this->mockFakeUpdateInfo('8.9.3')),
]);
$this->get('/admin/update')->assertSee(config('app.version'))->assertSee('8.9.3');
}
public function testDownload()
{
$this->setupGuzzleClientMock();
Http::fake([
config('app.update_source') => Http::sequence()
->push($this->fakeUpdateInfo('1.2.3'))
->whenEmpty($this->fakeUpdateInfo('8.9.3')),
'https://whatever.test/8.9.3/update.zip' => Http::sequence()
->pushStatus(404)
->pushStatus(200),
]);
// Already up-to-date
$this->appendToGuzzleQueue([
new Response(200, [], $this->mockFakeUpdateInfo('1.2.3')),
]);
$this->postJson('/admin/update/download')
->assertJson([
'code' => 1,
@ -67,12 +59,6 @@ class UpdateControllerTest extends TestCase
]);
// Download
$this->appendToGuzzleQueue([
new Response(200, [], $this->mockFakeUpdateInfo('8.9.3')),
new Response(404),
new Response(200, [], $this->mockFakeUpdateInfo('8.9.3')),
new Response(200),
]);
$this->postJson('/admin/update/download')->assertJson(['code' => 1]);
$this->mock(Unzip::class, function ($mock) {
$mock->shouldReceive('extract')->once()->andReturn();
@ -130,13 +116,13 @@ class UpdateControllerTest extends TestCase
$this->assertEquals('100.0.0', option('version'));
}
protected function mockFakeUpdateInfo(string $version, $extra = [])
protected function fakeUpdateInfo(string $version, $extra = [])
{
return json_encode(array_merge([
return array_merge([
'spec' => 2,
'php' => '7.2.5',
'latest' => $version,
'url' => "https://whatever.test/$version/update.zip",
], $extra));
], $extra);
}
}

View File

@ -3,23 +3,21 @@
namespace Tests;
use App\Rules\Captcha;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
class CaptchaTest extends TestCase
{
public function testCharactersCaptcha()
{
session(['captcha' => 'abc']);
$rule = resolve(Captcha::class);
$rule = new Captcha();
$this->assertFalse($rule->passes('captcha', 'abcd'));
$this->assertEquals(trans('validation.captcha'), $rule->message());
$this->assertNull(session('captcha'));
session(['captcha' => 'abc']);
$rule = resolve(Captcha::class);
$rule = new Captcha();
$this->assertTrue($rule->passes('captcha', 'abc'));
$this->assertNull(session('captcha'));
}
@ -27,16 +25,18 @@ class CaptchaTest extends TestCase
public function testRecaptcha()
{
option(['recaptcha_secretkey' => 'secret']);
$mock = new MockHandler([
new Response(403),
new Response(200, [], json_encode(['success' => true])),
]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
Http::fake(Http::response(['success' => true]));
$rule = new Captcha($client);
$this->assertFalse($rule->passes('captcha', 'value'));
$rule = new Captcha();
$this->assertTrue($rule->passes('captcha', 'value'));
$this->assertEquals(trans('validation.recaptcha'), $rule->message());
Http::assertSent(function (Request $request) {
$this->assertEquals(
['secret' => 'secret', 'response' => 'value'],
$request->data()
);
return true;
});
}
}