diff --git a/.travis.yml b/.travis.yml
index 8177ec8c..ee82e47b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,7 +6,6 @@ git:
cache:
directories:
- vendor
- - plugins
- node_modules
env:
diff --git a/app/Http/Controllers/MarketController.php b/app/Http/Controllers/MarketController.php
new file mode 100644
index 00000000..4c96c626
--- /dev/null
+++ b/app/Http/Controllers/MarketController.php
@@ -0,0 +1,156 @@
+guzzle = $guzzle;
+ $this->guzzleConfig = [
+ 'headers' => ['User-Agent' => config('secure.user_agent')],
+ 'verify' => config('secure.certificates')
+ ];
+ }
+
+ public function marketData()
+ {
+ $plugins = collect($this->getAllAvailablePlugins())->map(function ($item) {
+ $plugin = plugin($item['name']);
+ $manager = app('plugins');
+
+ if ($plugin) {
+ $item['enabled'] = $plugin->isEnabled();
+ $item['installed'] = $plugin->version;
+ $item['update_available'] = Comparator::greaterThan($item['version'], $item['installed']);
+ } else {
+ $item['installed'] = false;
+ }
+
+ $requirements = array_get($item, 'require', []);
+ unset($item['require']);
+
+ $item['dependencies'] = [
+ 'isRequirementsSatisfied' => $manager->isRequirementsSatisfied($requirements),
+ 'requirements' => $requirements,
+ 'unsatisfiedRequirements' => $manager->getUnsatisfiedRequirements($requirements)
+ ];
+
+ return $item;
+ });
+
+ return $plugins;
+ }
+
+ public function checkUpdates()
+ {
+ $pluginsHaveUpdate = collect($this->getAllAvailablePlugins())->filter(function ($item) {
+ $plugin = plugin($item['name']);
+ return $plugin && Comparator::greaterThan($item['version'], $plugin->version);
+ });
+
+ return json([
+ 'available' => $pluginsHaveUpdate->isNotEmpty(),
+ 'plugins' => $pluginsHaveUpdate->values()->all()
+ ]);
+ }
+
+ public function download(Request $request, PluginManager $manager)
+ {
+ $name = $request->get('name');
+ $metadata = $this->getPluginMetadata($name);
+
+ if (! $metadata) {
+ return json(trans('admin.plugins.market.non-existent', ['plugin' => $name]), 1);
+ }
+
+ // Gather plugin distribution URL
+ $url = $metadata['dist']['url'];
+ $filename = array_last(explode('/', $url));
+ $plugins_dir = $manager->getPluginsDir();
+ $tmp_path = $plugins_dir.DIRECTORY_SEPARATOR.$filename;
+
+ // Download
+ try {
+ $this->guzzle->request('GET', $url, array_merge($this->guzzleConfig, [
+ 'sink' => $tmp_path
+ ]));
+ } catch (Exception $e) {
+ report($e);
+ return json(trans('admin.plugins.market.download-failed', ['error' => $e->getMessage()]), 2);
+ }
+
+ // Check file's sha1 hash
+ if (sha1_file($tmp_path) !== $metadata['dist']['shasum']) {
+ @unlink($tmp_path);
+ return json(trans('admin.plugins.market.shasum-failed'), 3);
+ }
+
+ // Unzip
+ $zip = new ZipArchive();
+ $res = $zip->open($tmp_path);
+
+ if ($res === true) {
+ if ($zip->extractTo($plugins_dir) === false) {
+ return json(trans('admin.plugins.market.unzip-failed', ['error' => 'Unable to extract the file.']), 4);
+ }
+ } else {
+ return json(trans('admin.plugins.market.unzip-failed', ['error' => $res]), 4);
+ }
+ $zip->close();
+ @unlink($tmp_path);
+
+ return json(trans('admin.plugins.market.install-success'), 0);
+ }
+
+ protected function getPluginMetadata($name)
+ {
+ return collect($this->getAllAvailablePlugins())->where('name', $name)->first();
+ }
+
+ protected function getAllAvailablePlugins()
+ {
+ if (app()->environment('testing') || ! $this->registryCache) {
+ try {
+ $pluginsJson = $this->guzzle->request(
+ 'GET', config('plugins.registry'), $this->guzzleConfig
+ )->getBody();
+ } catch (Exception $e) {
+ throw new Exception(trans('admin.plugins.market.connection-error', [
+ 'error' => htmlentities($e->getMessage())
+ ]));
+ }
+
+ $this->registryCache = json_decode($pluginsJson, true);
+ }
+
+ return array_get($this->registryCache, 'packages', []);
+ }
+}
diff --git a/app/Http/Controllers/PluginController.php b/app/Http/Controllers/PluginController.php
index 56ef1760..8b3641df 100644
--- a/app/Http/Controllers/PluginController.php
+++ b/app/Http/Controllers/PluginController.php
@@ -11,7 +11,7 @@ use App\Services\PluginManager;
class PluginController extends Controller
{
- public function config($name, Request $request)
+ public function config($name)
{
$plugin = plugin($name);
diff --git a/app/Services/PluginManager.php b/app/Services/PluginManager.php
index 1b0ad671..2698429f 100644
--- a/app/Services/PluginManager.php
+++ b/app/Services/PluginManager.php
@@ -277,20 +277,24 @@ class PluginManager
/**
* Get the unsatisfied requirements of plugin.
*
- * @param string|Plugin $plugin
+ * @param string|Plugin|array $plugin
* @return array
*/
public function getUnsatisfiedRequirements($plugin)
{
- if (! $plugin instanceof Plugin) {
- $plugin = $this->getPlugin($plugin);
- }
+ if (is_array($plugin)) {
+ $requirements = $plugin;
+ } else {
+ if (! $plugin instanceof Plugin) {
+ $plugin = $this->getPlugin($plugin);
+ }
- if (! $plugin) {
- throw new \InvalidArgumentException('Plugin with given name does not exist.');
- }
+ if (! $plugin) {
+ throw new \InvalidArgumentException('Plugin with given name does not exist.');
+ }
- $requirements = $plugin->getRequirements();
+ $requirements = $plugin->getRequirements();
+ }
$unsatisfied = [];
@@ -334,7 +338,7 @@ class PluginManager
/**
* Whether the plugin's requirements are satisfied.
*
- * @param string|Plugin $plugin
+ * @param string|Plugin|array $plugin
* @return bool
*/
public function isRequirementsSatisfied($plugin)
@@ -347,7 +351,7 @@ class PluginManager
*
* @return string
*/
- protected function getPluginsDir()
+ public function getPluginsDir()
{
return config('plugins.directory') ?: base_path('plugins');
}
diff --git a/config/plugins.php b/config/plugins.php
index bd499d11..9dc2a713 100644
--- a/config/plugins.php
+++ b/config/plugins.php
@@ -22,4 +22,14 @@ return [
|
*/
'url' => menv('PLUGINS_URL'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Plugins Market Source
+ |--------------------------------------------------------------------------
+ |
+ | Specify where to get plugins' metadata for plugin market.
+ |
+ */
+ 'registry' => menv('PLUGINS_REGISTRY', 'https://coding.net/u/printempw/p/bs-plugins-archive/git/raw/master/plugins.json'),
];
diff --git a/resources/assets/src/components/admin/Market.vue b/resources/assets/src/components/admin/Market.vue
new file mode 100644
index 00000000..e6443d1e
--- /dev/null
+++ b/resources/assets/src/components/admin/Market.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+ {{ props.formattedRow[props.column.field] }}
+ {{ props.row.name }}
+
+
+
+
+
+ {{ dep }}: {{ semver }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/assets/src/components/route.js b/resources/assets/src/components/route.js
index 869fabf9..158d35cd 100644
--- a/resources/assets/src/components/route.js
+++ b/resources/assets/src/components/route.js
@@ -34,6 +34,11 @@ export default [
component: () => import('./admin/Customization'),
el: '#change-color'
},
+ {
+ path: 'admin/plugins/market',
+ component: () => import('./admin/Market'),
+ el: '.content'
+ },
{
path: 'auth/login',
component: () => import('./auth/Login'),
diff --git a/resources/assets/tests/components/admin/Market.test.js b/resources/assets/tests/components/admin/Market.test.js
new file mode 100644
index 00000000..9401c1eb
--- /dev/null
+++ b/resources/assets/tests/components/admin/Market.test.js
@@ -0,0 +1,131 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import Market from '@/components/admin/Market';
+import { flushPromises } from '../../utils';
+import { swal } from '@/js/notify';
+
+jest.mock('@/js/notify');
+
+test('render dependencies', async () => {
+ Vue.prototype.$http.get.mockResolvedValue([
+ { name: 'a', dependencies: { requirements: [] } },
+ { name: 'b', dependencies: {
+ requirements: { 'a': '^1.0.0', 'c': '^2.0.0' }, unsatisfiedRequirements: { 'c': {} }
+ } }
+ ]);
+ const wrapper = mount(Market);
+ await flushPromises();
+
+ expect(wrapper.text()).toContain('admin.noDependencies');
+ expect(wrapper.find('span.label.bg-green').text()).toBe('a: ^1.0.0');
+ expect(wrapper.find('span.label.bg-red').text()).toBe('c: ^2.0.0');
+});
+
+test('render operation buttons', async () => {
+ Vue.prototype.$http.get.mockResolvedValue([
+ { name: 'a', dependencies: { requirements: [] }, installed: true, update_available: true },
+ { name: 'b', dependencies: { requirements: [] }, installed: true, enabled: true },
+ { name: 'c', dependencies: { requirements: [] }, installed: true },
+ { name: 'd', dependencies: { requirements: [] }, installed: false },
+ ]);
+ const wrapper = mount(Market);
+ await flushPromises();
+ const tbody = wrapper.find('tbody');
+
+ expect(tbody.find('tr:nth-child(1)').text()).toContain('admin.updatePlugin');
+ expect(tbody.find('tr:nth-child(2)').text()).toContain('admin.statusEnabled');
+ expect(tbody.find('tr:nth-child(3)').text()).toContain('admin.enablePlugin');
+ expect(tbody.find('tr:nth-child(4)').text()).toContain('admin.installPlugin');
+});
+
+test('install plugin', async () => {
+ Vue.prototype.$http.get.mockResolvedValue([
+ { name: 'd', dependencies: { requirements: [] }, installed: false },
+ ]);
+ Vue.prototype.$http.post
+ .mockResolvedValueOnce({ errno: 1, msg: '1' })
+ .mockResolvedValueOnce({ errno: 0, msg: '0' });
+ const wrapper = mount(Market);
+ await flushPromises();
+ const button = wrapper.find('button');
+
+ button.trigger('click');
+ await flushPromises();
+ expect(Vue.prototype.$http.post).toBeCalledWith(
+ '/admin/plugins/market/download',
+ { name: 'd' }
+ );
+
+ button.trigger('click');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.text()).toContain('admin.enablePlugin');
+});
+
+test('update plugin', async () => {
+ Vue.prototype.$http.get.mockResolvedValue([
+ {
+ name: 'a',
+ version: '2.0.0',
+ dependencies: { requirements: [] },
+ installed: '1.0.0',
+ update_available: true
+ },
+ ]);
+ Vue.prototype.$http.post
+ .mockResolvedValueOnce({ errno: 1, msg: '1' });
+ swal.mockResolvedValueOnce({ dismiss: 1 })
+ .mockResolvedValue({});
+ const wrapper = mount(Market);
+ await flushPromises();
+ const button = wrapper.find('button');
+
+ button.trigger('click');
+ await flushPromises();
+ expect(Vue.prototype.$http.post).not.toBeCalled();
+
+ button.trigger('click');
+ await flushPromises();
+ expect(Vue.prototype.$http.post).toBeCalledWith(
+ '/admin/plugins/market/download',
+ { name: 'a' }
+ );
+});
+
+test('enable installed plugin', async () => {
+ Vue.prototype.$http.get.mockResolvedValue([
+ { name: 'a', dependencies: { requirements: [] }, installed: true },
+ { name: 'b', dependencies: { requirements: {} }, installed: true },
+ ]);
+ Vue.prototype.$http.post
+ .mockResolvedValueOnce({ errno: 1, msg: '1', reason: ['`a
`b'] })
+ .mockResolvedValue({ errno: 0, msg: '0' });
+ swal.mockResolvedValueOnce({ dismiss: 1 })
+ .mockResolvedValueOnce({});
+ const wrapper = mount(Market);
+ await flushPromises();
+ const buttons = wrapper.findAll('button');
+
+ buttons.at(0).trigger('click');
+ await flushPromises();
+ expect(swal).toBeCalledWith({
+ text: 'admin.noDependenciesNotice',
+ type: 'warning',
+ showCancelButton: true
+ });
+ expect(Vue.prototype.$http.post).not.toBeCalled();
+
+ buttons.at(0).trigger('click');
+ await flushPromises();
+ expect(Vue.prototype.$http.post).toBeCalledWith(
+ '/admin/plugins/manage',
+ { action: 'enable', name: 'a' }
+ );
+ expect(swal).toBeCalledWith({
+ type: 'warning',
+ html: '1
'
+ });
+
+ buttons.at(1).trigger('click');
+ await flushPromises();
+ expect(wrapper.text()).toContain('admin.statusEnabled');
+});
diff --git a/resources/lang/en/admin.yml b/resources/lang/en/admin.yml
index 08749fb6..29caf8b8 100644
--- a/resources/lang/en/admin.yml
+++ b/resources/lang/en/admin.yml
@@ -102,23 +102,26 @@ plugins:
version: Version
dependencies: Dependencies
- status:
- title: Status
- enabled: Enabled
- disabled: Disabled
-
operations:
title: Operations
enabled: :plugin has been enabled.
unsatisfied:
notice: There are unsatisfied dependencies in the plugin, therefore we can't enable it. Please install or update the plugins listed below.
- disabled: The :name plugin is not enabled
- version: The version of :name does not satisfies the constraint :constraint
+ disabled: "The `:name` plugin is not enabled"
+ version: "The version of `:name` does not satisfies the constraint `:constraint`"
disabled: :plugin has been disabled.
deleted: The plugin was deleted successfully.
no-config-notice: The plugin is not installed or doesn't provide configuration page.
not-found: No such plugin.
+ market:
+ connection-error: Unable to connect to the plugins registry. :error
+ non-existent: The plugin :plugin does not exist.
+ download-failed: Unable to download the plugin. :error
+ shasum-failed: The downloaded file failed hash check, please retry.
+ unzip-failed: Unable to extract the plugin. :error
+ install-success: Plugin was installed.
+
empty: No result
update:
diff --git a/resources/lang/en/front-end.yml b/resources/lang/en/front-end.yml
index 3e151009..0e68a971 100644
--- a/resources/lang/en/front-end.yml
+++ b/resources/lang/en/front-end.yml
@@ -271,6 +271,17 @@ admin:
whyDependencies: What's this?
statusEnabled: Enabled
statusDisabled: Disabled
+ pluginTitle: Plugin
+ pluginAuthor: Author
+ pluginVersion: Version
+ pluginName: Name
+ pluginDescription: Description
+ pluginDependencies: Dependencies
+ installPlugin: Install
+ pluginInstalling: Installing...
+ updatePlugin: Update
+ pluginUpdating: Updating...
+ confirmUpdate: Are you sure to update ":plugin" from :old to :new?
enablePlugin: Enable
disablePlugin: Disable
confirmDeletion: Are you sure to delete this plugin?
diff --git a/resources/lang/zh_CN/admin.yml b/resources/lang/zh_CN/admin.yml
index 21cab487..a1cf571f 100644
--- a/resources/lang/zh_CN/admin.yml
+++ b/resources/lang/zh_CN/admin.yml
@@ -112,13 +112,21 @@ plugins:
enabled: :plugin 已启用
unsatisfied:
notice: 无法启用此插件,因为其仍有未满足的依赖关系。请检查以下插件的版本,更新或安装它们:
- disabled: :name 插件未启用
- version: :name 的版本不符合要求 :constraint
+ disabled: "`:name` 插件未启用"
+ version: "`:name` 的版本不符合要求 `:constraint`"
disabled: :plugin 已禁用
deleted: 插件已被成功删除
no-config-notice: 插件未安装或未提供配置页面
not-found: 插件不存在
+ market:
+ connection-error: 无法连接至插件市场源,错误信息::error
+ non-existent: 插件 :plugin 不存在
+ download-failed: 插件下载失败,错误信息::error
+ shasum-failed: 文件校验失败,请尝试重新下载
+ unzip-failed: 插件解压缩失败,错误信息::error
+ install-success: 插件安装成功
+
empty: 无结果
update:
diff --git a/resources/lang/zh_CN/front-end.yml b/resources/lang/zh_CN/front-end.yml
index 97bf0ae8..b89d525c 100644
--- a/resources/lang/zh_CN/front-end.yml
+++ b/resources/lang/zh_CN/front-end.yml
@@ -267,6 +267,17 @@ admin:
whyDependencies: 为什么会这样?
statusEnabled: 已启用
statusDisabled: 已禁用
+ pluginTitle: 插件
+ pluginAuthor: 作者
+ pluginVersion: 版本
+ pluginName: 插件标识
+ pluginDescription: 描述
+ pluginDependencies: 依赖关系
+ installPlugin: 安装
+ pluginInstalling: 正在安装...
+ updatePlugin: 更新
+ pluginUpdating: 正在更新...
+ confirmUpdate: 确定将「:plugin」从 :old 升级至 :new?
enablePlugin: 启用插件
disablePlugin: 禁用插件
confirmDeletion: 真的要删除这个插件吗?
diff --git a/resources/views/admin/market.blade.php b/resources/views/admin/market.blade.php
new file mode 100644
index 00000000..333ef15a
--- /dev/null
+++ b/resources/views/admin/market.blade.php
@@ -0,0 +1,20 @@
+@extends('admin.master')
+
+@section('title', trans('general.plugin-market'))
+
+@section('content')
+
+
+
+
+
+
+
+
+
+
+@endsection
diff --git a/routes/web.php b/routes/web.php
index d35c78b5..6c0e7f94 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -123,11 +123,15 @@ Route::group(['middleware' => ['auth', 'admin'], 'prefix' => 'admin'], function
Route::group(['prefix' => 'plugins'], function () {
Route::get ('/data', 'PluginController@getPluginData');
- Route::redirect('/market', 'https://github.com/printempw/blessing-skin-server/wiki/Plugins');
Route::view('/manage', 'admin.plugins');
Route::post('/manage', 'PluginController@manage');
Route::any ('/config/{name}', 'PluginController@config');
+
+ Route::view('/market', 'admin.market');
+ Route::get ('/market-data', 'MarketController@marketData');
+ Route::get ('/market/check', 'MarketController@checkUpdates');
+ Route::post('/market/download', 'MarketController@download');
});
Route::group(['prefix' => 'update'], function () {
diff --git a/tests/Concerns/GeneratesFakePlugins.php b/tests/Concerns/GeneratesFakePlugins.php
new file mode 100644
index 00000000..886ecad2
--- /dev/null
+++ b/tests/Concerns/GeneratesFakePlugins.php
@@ -0,0 +1,143 @@
+ str_random(10),
+ 'version' => '0.0.'.rand(1, 9),
+ 'title' => str_random(20),
+ 'description' => str_random(60),
+ 'author' => array_get($info, 'author', str_random(10)),
+ 'url' => 'https://'.str_random(10).'.test',
+ 'namespace' => str_random(10),
+ 'require' => [
+ 'blessing-skin-server' => '^3.4.0'
+ ]
+ ], $info);
+ }
+
+ /**
+ * Generate plugin information for plugins registry (with "dist" field).
+ *
+ * @param array $info
+ * @return array
+ */
+ protected function generateFakePluginsRegistryPackage($info = [])
+ {
+ return $this->generateFakePlguinInfo(array_replace([
+ 'dist' => [
+ 'type' => 'zip',
+ 'url' => 'https://plugins-registry.test/'.str_random(10).'.zip',
+ 'shasum' => strtolower(str_random(40))
+ ]
+ ], $info));
+ }
+
+ /**
+ * Generate fake content of a plugins registry.
+ * You can also pass two arguments (name and version) as a shortcut.
+ * If no argument is passed, we will randomly generate 10 fake plugins.
+ *
+ * @param array $plugins An array of plugin information.
+ * @return string JSON encoded content.
+ */
+ protected function generateFakePluginsRegistry($plugins = [])
+ {
+ if (func_num_args() == 2) {
+ $plugins = [
+ [
+ 'name' => func_get_arg(0),
+ 'version' => func_get_arg(1)
+ ]
+ ];
+ }
+
+ $packages = [];
+
+ if (count($plugins) == 0) {
+ // Randomly generate 10 fake plugins
+ for ($i = 0; $i < 10; $i++) {
+ $packages[] = $this->generateFakePluginsRegistryPackage();
+ }
+ } else {
+ foreach ($plugins as $info) {
+ $packages[] = $this->generateFakePluginsRegistryPackage($info);
+ }
+ }
+
+ return json_encode([
+ 'packages' => $packages
+ ]);
+ }
+
+ /**
+ * Generate a fake plugin in plugins directory with given information.
+ *
+ * @param array $info The "name" field is required.
+ * @return void
+ */
+ protected function generateFakePlugin($info)
+ {
+ $plugin_dir = base_path("plugins/{$info['name']}");
+
+ if (! is_dir($plugin_dir)) {
+ mkdir($plugin_dir);
+ }
+
+ // Generate fake config view
+ if ($config = array_get($info, 'config')) {
+ $views_path = "$plugin_dir/views";
+
+ if (! is_dir($views_path)) {
+ mkdir($views_path);
+ }
+
+ file_put_contents("$views_path/$config", str_random(64));
+ }
+
+ file_put_contents("$plugin_dir/package.json", json_encode(
+ $this->generateFakePlguinInfo($info)
+ ));
+
+ file_put_contents("$plugin_dir/bootstrap.php", "open($zipPath, ZipArchive::CREATE);
+ $zip->addEmptyDir($name);
+ $zip->addFromString("$name/package.json", json_encode(
+ $this->generateFakePlguinInfo($info)
+ ));
+ $zip->close();
+
+ return $zipPath;
+ }
+}
diff --git a/tests/MarketControllerTest.php b/tests/MarketControllerTest.php
new file mode 100644
index 00000000..e4519014
--- /dev/null
+++ b/tests/MarketControllerTest.php
@@ -0,0 +1,195 @@
+actAs('superAdmin');
+ }
+
+ public function testDownload()
+ {
+ $this->setupGuzzleClientMock();
+
+ // Try to download a non-existent plugin
+ $this->appendToGuzzleQueue(200, [], $this->generateFakePluginsRegistry());
+ $this->postJson('/admin/plugins/market/download', [
+ 'name' => 'non-existent-plugin'
+ ])->assertJson([
+ 'errno' => 1,
+ 'msg' => trans('admin.plugins.market.non-existent', ['plugin' => 'non-existent-plugin'])
+ ]);
+
+ // Can't download due to connection error
+ $this->appendToGuzzleQueue([
+ new Response(200, [], $this->generateFakePluginsRegistry('fake-test-download', '0.0.1')),
+ new RequestException('Connection Error', new Request('GET', 'whatever')),
+ ]);
+ $this->postJson('/admin/plugins/market/download', [
+ 'name' => 'fake-test-download'
+ ])->assertJson([
+ 'errno' => 2,
+ 'msg' => trans('admin.plugins.market.download-failed', ['error' => 'Connection Error'])
+ ]);
+
+ // Downloaded plugin archive was tampered
+ $fakeArchive = $this->generateFakePluginArchive(['name' => 'fake-test-download', 'version' => '0.0.1']);
+ $this->appendToGuzzleQueue([
+ new Response(200, [], $this->generateFakePluginsRegistry('fake-test-download', '0.0.1')),
+ new Response(200, [], fopen($fakeArchive, 'r')),
+ ]);
+ $this->postJson('/admin/plugins/market/download', [
+ 'name' => 'fake-test-download'
+ ])->assertJson([
+ 'errno' => 3,
+ 'msg' => trans('admin.plugins.market.shasum-failed')
+ ]);
+
+ // Download and extract plugin
+ $shasum = sha1_file($fakeArchive);
+ $this->appendToGuzzleQueue([
+ new Response(200, [], $this->generateFakePluginsRegistry([
+ [
+ 'name' => 'fake-test-download',
+ 'version' => '0.0.1',
+ 'dist' => [
+ 'url' => 'whatever',
+ 'shasum' => $shasum
+ ]
+ ]
+ ])),
+ new Response(200, [], fopen($fakeArchive, 'r')),
+ ]);
+ $this->postJson('/admin/plugins/market/download', [
+ 'name' => 'fake-test-download'
+ ])->assertJson([
+ 'errno' => 0,
+ 'msg' => trans('admin.plugins.market.install-success')
+ ]);
+ $this->assertTrue(is_dir(base_path('plugins/fake-test-download')));
+ $this->assertTrue(empty(glob(base_path('plugins/fake-test-download_*.zip'))));
+
+ // Broken archive
+ file_put_contents($fakeArchive, 'broken');
+ $shasum = sha1_file($fakeArchive);
+ $this->appendToGuzzleQueue([
+ new Response(200, [], $this->generateFakePluginsRegistry([
+ [
+ 'name' => 'fake-test-download',
+ 'version' => '0.0.1',
+ 'dist' => [
+ 'url' => 'whatever',
+ 'shasum' => $shasum
+ ]
+ ]
+ ])),
+ new Response(200, [], fopen($fakeArchive, 'r')),
+ ]);
+ $this->postJson('/admin/plugins/market/download', [
+ 'name' => 'fake-test-download'
+ ])->assertJson([
+ 'errno' => 4,
+ 'msg' => trans('admin.plugins.market.unzip-failed', ['error' => 19])
+ ]);
+ }
+
+ public function testCheckUpdates()
+ {
+ $this->setupGuzzleClientMock();
+
+ // Not installed
+ $this->appendToGuzzleQueue(200, [], $this->generateFakePluginsRegistry('fake-test-update', '0.0.1'));
+ $this->getJson('/admin/plugins/market/check')
+ ->assertJson([
+ 'available' => false,
+ 'plugins' => []
+ ]);
+
+ // Generate fake plugin and refresh plugin manager
+ $this->generateFakePlugin(['name' => 'fake-test-update', 'version' => '0.0.1']);
+ $this->app->singleton('plugins', \App\Services\PluginManager::class);
+
+ // Plugin up-to-date
+ $this->appendToGuzzleQueue(200, [], $this->generateFakePluginsRegistry('fake-test-update', '0.0.1'));
+ $this->getJson('/admin/plugins/market/check')
+ ->assertJson([
+ 'available' => false,
+ 'plugins' => []
+ ]);
+
+ // New version available
+ $this->appendToGuzzleQueue(200, [], $this->generateFakePluginsRegistry('fake-test-update', '2.3.3'));
+ $this->getJson('/admin/plugins/market/check')
+ ->assertJson([
+ 'available' => true,
+ 'plugins' => [[
+ 'name' => 'fake-test-update'
+ ]]
+ ]);
+ }
+
+ public function testMarketData()
+ {
+ $this->setupGuzzleClientMock([
+ new RequestException('Connection Error', new Request('POST', 'whatever')),
+ new Response(200, [], $this->generateFakePluginsRegistry()),
+ ]);
+
+ // Expected an exception, but unable to be asserted.
+ $this->getJson('/admin/plugins/market-data');
+
+ $this->getJson('/admin/plugins/market-data')
+ ->assertJsonStructure([
+ [
+ 'name',
+ 'title',
+ 'version',
+ 'installed',
+ 'description',
+ 'author',
+ 'dist',
+ 'dependencies'
+ ]
+ ]);
+
+ // Detect installed plugins
+ $this->appendToGuzzleQueue(200, [], $this->generateFakePluginsRegistry('fake', '0.0.1'));
+ $this->generateFakePlugin(['name' => 'fake', 'version' => '0.0.1']);
+ $this->getJson('/admin/plugins/market-data')
+ ->assertJsonStructure([
+ [
+ 'name',
+ 'title',
+ 'version',
+ 'installed',
+ 'description',
+ 'author',
+ 'dist',
+ 'dependencies'
+ ]
+ ]);
+ }
+
+ protected function tearDown()
+ {
+ // Clean fake plugins
+ File::deleteDirectory(base_path('plugins/fake-test-download'));
+ File::deleteDirectory(base_path('plugins/fake-test-update'));
+ File::deleteDirectory(base_path('plugins/fake'));
+
+ parent::tearDown();
+ }
+}