add two new ways to install plugin

This commit is contained in:
Pig Fang 2020-03-12 10:41:41 +08:00
parent ba757d98a8
commit 271c950ad9
10 changed files with 440 additions and 77 deletions

View File

@ -4,7 +4,10 @@ namespace App\Http\Controllers;
use App\Services\Plugin; use App\Services\Plugin;
use App\Services\PluginManager; use App\Services\PluginManager;
use App\Services\Unzip;
use Composer\CaBundle\CaBundle;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Parsedown; use Parsedown;
class PluginController extends Controller class PluginController extends Controller
@ -105,4 +108,33 @@ class PluginController extends Controller
}) })
->values(); ->values();
} }
public function upload(Request $request, PluginManager $manager, Unzip $unzip)
{
$request->validate(['file' => 'required|file|mimetypes:application/zip']);
$path = $request->file('file')->getPathname();
$unzip->extract($path, $manager->getPluginsDirs()->first());
return json(trans('admin.plugins.market.install-success'), 0);
}
public function wget(Request $request, PluginManager $manager, Unzip $unzip)
{
$data = $request->validate(['url' => 'required|url']);
$path = tempnam(sys_get_temp_dir(), 'wget-plugin');
$response = Http::withOptions([
'sink' => $path,
'verify' => CaBundle::getSystemCaRootBundlePath(),
])->get($data['url']);
if ($response->ok()) {
$unzip->extract($path, $manager->getPluginsDirs()->first());
return json(trans('admin.plugins.market.install-success'), 0);
} else {
return json(trans('admin.download.errors.download', ['error' => $response->status()]), 1);
}
}
} }

View File

@ -1,43 +1,39 @@
@use '../../../styles/utils';
.box { .box {
cursor: default; cursor: default;
transition-property: box-shadow; transition-property: box-shadow;
transition-duration: 0.3s; transition-duration: 0.3s;
width: 32%;
@media (max-width: 1280px) {
width: 47%;
}
@media (max-width: 768px) {
width: 100%;
}
&:hover { &:hover {
box-shadow: 0 0.5rem 1rem rgba(#000, 0.15); box-shadow: 0 0.5rem 1rem rgba(#000, 0.15);
} }
} }
.content { .content {
max-width: 85%; max-width: calc(100% - 70px);
} }
.actions { .header {
margin-top: -7px; max-width: calc(100% - 40px);
}
a { .title {
transition-property: color; @include utils.truncate-text;
transition-duration: 0.3s; }
color: #000;
&:hover { .actions a {
color: #999; transition-property: color;
} transition-duration: 0.3s;
&:not(:last-child) { color: #000;
margin-right: 9px; &:hover {
} color: #999;
}
&:not(:last-child) {
margin-right: 9px;
} }
} }
.description { .description {
font-size: 14px; font-size: 14px;
white-space: nowrap; @include utils.truncate-text;
overflow: hidden;
text-overflow: ellipsis;
} }

View File

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { trans } from '../../../scripts/i18n' import { t } from '@/scripts/i18n'
import { Plugin } from './types' import { Plugin } from './types'
import styles from './InfoBox.scss' import styles from './InfoBox.scss'
@ -33,26 +33,30 @@ const InfoBox: React.FC<Props> = props => {
</span> </span>
<div className={`info-box-content ${styles.content}`}> <div className={`info-box-content ${styles.content}`}>
<div className="d-flex justify-content-between"> <div className="d-flex justify-content-between">
<div> <div className={`d-flex ${styles.header}`}>
<input <input
className="mr-2" className="mr-2 d-inline-block"
type="checkbox" type="checkbox"
checked={plugin.enabled} checked={plugin.enabled}
title={ title={
plugin.enabled plugin.enabled
? trans('admin.disablePlugin') ? t('admin.disablePlugin')
: trans('admin.enablePlugin') : t('admin.enablePlugin')
} }
onChange={handleChange} onChange={handleChange}
/> />
<strong className="mr-2">{plugin.title}</strong> <strong className={`d-inline-block mr-2 ${styles.title}`}>
<span className="text-gray">v{plugin.version}</span> {plugin.title}
</strong>
<span className="d-none d-sm-inline-block text-gray">
v{plugin.version}
</span>
</div> </div>
<div className={styles.actions}> <div className={styles.actions}>
{plugin.readme && ( {plugin.readme && (
<a <a
href={`${props.baseUrl}/admin/plugins/readme/${plugin.name}`} href={`${props.baseUrl}/admin/plugins/readme/${plugin.name}`}
title={trans('admin.pluginReadme')} title={t('admin.pluginReadme')}
> >
<i className="fas fa-question" /> <i className="fas fa-question" />
</a> </a>
@ -60,16 +64,12 @@ const InfoBox: React.FC<Props> = props => {
{plugin.enabled && plugin.config && ( {plugin.enabled && plugin.config && (
<a <a
href={`${props.baseUrl}/admin/plugins/config/${plugin.name}`} href={`${props.baseUrl}/admin/plugins/config/${plugin.name}`}
title={trans('admin.configurePlugin')} title={t('admin.configurePlugin')}
> >
<i className="fas fa-cog" /> <i className="fas fa-cog" />
</a> </a>
)} )}
<a <a href="#" title={t('admin.deletePlugin')} onClick={handleDelete}>
href="#"
title={trans('admin.deletePlugin')}
onClick={handleDelete}
>
<i className="fas fa-trash" /> <i className="fas fa-trash" />
</a> </a>
</div> </div>
@ -80,4 +80,4 @@ const InfoBox: React.FC<Props> = props => {
) )
} }
export default React.memo(InfoBox) export default InfoBox

View File

@ -1,15 +1,21 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { hot } from 'react-hot-loader/root' import { hot } from 'react-hot-loader/root'
import { trans } from '../../../scripts/i18n' import { t } from '@/scripts/i18n'
import * as fetch from '../../../scripts/net' import * as fetch from '@/scripts/net'
import { toast, showModal } from '../../../scripts/notify' import { toast, showModal } from '@/scripts/notify'
import Loading from '../../../components/Loading' import FileInput from '@/components/FileInput'
import Loading from '@/components/Loading'
import InfoBox from './InfoBox' import InfoBox from './InfoBox'
import { Plugin } from './types' import { Plugin } from './types'
const PluginsManagement: React.FC = () => { const PluginsManagement: React.FC = () => {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [plugins, setPlugins] = useState<Plugin[]>([]) const [plugins, setPlugins] = useState<Plugin[]>([])
const [file, setFile] = useState<File | null>(null)
const [isUploading, setIsUploading] = useState(false)
const [url, setUrl] = useState('')
const [isDownloading, setIsDownloading] = useState(false)
useEffect(() => { useEffect(() => {
const getPlugins = async () => { const getPlugins = async () => {
setLoading(true) setLoading(true)
@ -73,7 +79,7 @@ const PluginsManagement: React.FC = () => {
try { try {
await showModal({ await showModal({
title: plugin.title, title: plugin.title,
text: trans('admin.confirmDeletion'), text: t('admin.confirmDeletion'),
okButtonType: 'danger', okButtonType: 'danger',
}) })
} catch { } catch {
@ -93,20 +99,132 @@ const PluginsManagement: React.FC = () => {
} }
} }
return loading ? ( const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
<Loading /> setFile(event.target.files![0])
) : ( }
<div className="d-flex flex-wrap">
{plugins.map((plugin, i) => ( const handleUrlChange = (event: React.ChangeEvent<HTMLInputElement>) => {
<InfoBox setUrl(event.target.value)
key={plugin.name} }
plugin={plugin}
onEnable={plugin => handleEnable(plugin, i)} const handleUpload = async () => {
onDisable={plugin => handleDisable(plugin, i)} if (!file) {
onDelete={handleDelete} return
baseUrl={blessing.base_url} }
/>
))} setIsUploading(true)
const formData = new FormData()
formData.append('file', file, file.name)
const { code, message } = await fetch.post<fetch.ResponseBody>(
'/admin/plugins/upload',
formData,
)
setIsUploading(false)
if (code === 0) {
toast.success(message)
setFile(null)
setPlugins(await fetch.get('/admin/plugins/data'))
} else {
toast.error(message)
}
}
const handleSubmitUrl = async () => {
setIsDownloading(true)
const { code, message } = await fetch.post<fetch.ResponseBody>(
'/admin/plugins/wget',
{ url },
)
setIsDownloading(false)
if (code === 0) {
toast.success(message)
setUrl('')
setPlugins(await fetch.get('/admin/plugins/data'))
} else {
toast.error(message)
}
}
const chunks = Array(Math.ceil(plugins.length / 2))
.fill(null)
.map((_, i) => plugins.slice(i * 2, (i + 1) * 2))
return (
<div className="row">
<div className="col-lg-8">
{loading ? (
<Loading />
) : (
chunks.map((chunk, i) => (
<div className="row" key={`${chunk[0].name}&${chunk[1]?.name}`}>
{chunk.map((plugin, j) => (
<div className="col-md-6" key={plugin.name}>
<InfoBox
plugin={plugin}
onEnable={plugin => handleEnable(plugin, i * 2 + j)}
onDisable={plugin => handleDisable(plugin, i * 2 + j)}
onDelete={handleDelete}
baseUrl={blessing.base_url}
/>
</div>
))}
</div>
))
)}
</div>
<div className="col-lg-4">
<div className="card card-primary card-outline">
<div className="card-header">
<h3 className="card-title">{t('admin.uploadArchive')}</h3>
</div>
<div className="card-body">
<p>{t('admin.uploadArchiveNotice')}</p>
<FileInput
file={file}
accept="application/zip"
onChange={handleFileChange}
/>
</div>
<div className="card-footer">
<button
className="btn btn-primary float-right"
disabled={isUploading}
onClick={handleUpload}
>
{isUploading ? <Loading /> : t('general.submit')}
</button>
</div>
</div>
<div className="card card-primary card-outline">
<div className="card-header">
<h3 className="card-title">{t('admin.downloadRemote')}</h3>
</div>
<div className="card-body">
<p>{t('admin.downloadRemoteNotice')}</p>
<div className="form-group">
<label htmlFor="zip-url">URL</label>
<input
type="text"
id="zip-url"
className="form-control"
value={url}
onChange={handleUrlChange}
/>
</div>
</div>
<div className="card-footer">
<button
className="btn btn-primary float-right"
disabled={isDownloading}
onClick={handleSubmitUrl}
>
{isDownloading ? <Loading /> : t('general.submit')}
</button>
</div>
</div>
</div>
</div> </div>
) )
} }

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import { render, wait, fireEvent } from '@testing-library/react' import { render, wait, fireEvent } from '@testing-library/react'
import { trans } from '@/scripts/i18n' import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net' import * as fetch from '@/scripts/net'
import PluginsManagement from '@/views/admin/PluginsManagement' import PluginsManagement from '@/views/admin/PluginsManagement'
@ -24,13 +24,23 @@ test('plugin info box', async () => {
icon: {}, icon: {},
enabled: true, enabled: true,
}, },
{
name: 'b',
title: 'Another Plugin',
version: '0.1.0',
description: '',
config: true,
readme: true,
icon: {},
enabled: true,
},
]) ])
const { queryByTitle, queryByText } = render(<PluginsManagement />) const { queryByTitle, queryByText } = render(<PluginsManagement />)
await wait() await wait()
expect(queryByTitle(trans('admin.configurePlugin'))).not.toBeNull() expect(queryByTitle(t('admin.configurePlugin'))).not.toBeNull()
expect(queryByTitle(trans('admin.pluginReadme'))).not.toBeNull() expect(queryByTitle(t('admin.pluginReadme'))).not.toBeNull()
expect(queryByText('My Plugin')).not.toBeNull() expect(queryByText('My Plugin')).not.toBeNull()
expect(queryByText('v1.0.0')).not.toBeNull() expect(queryByText('v1.0.0')).not.toBeNull()
expect(queryByText('desc')).not.toBeNull() expect(queryByText('desc')).not.toBeNull()
@ -47,7 +57,7 @@ describe('enable plugin', () => {
]) ])
}) })
it('successfully', async () => { it('succeeded', async () => {
fetch.get.mockResolvedValue([ fetch.get.mockResolvedValue([
{ {
name: 'a', name: 'a',
@ -60,7 +70,7 @@ describe('enable plugin', () => {
const { getByTitle, getByRole, queryByText } = render(<PluginsManagement />) const { getByTitle, getByRole, queryByText } = render(<PluginsManagement />)
await wait() await wait()
fireEvent.click(getByTitle(trans('admin.enablePlugin'))) fireEvent.click(getByTitle(t('admin.enablePlugin')))
await wait() await wait()
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', { expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
@ -69,7 +79,7 @@ describe('enable plugin', () => {
}) })
expect(queryByText('success')).toBeInTheDocument() expect(queryByText('success')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success') expect(getByRole('status')).toHaveClass('alert-success')
expect(getByTitle(trans('admin.disablePlugin'))).toBeChecked() expect(getByTitle(t('admin.disablePlugin'))).toBeChecked()
}) })
it('failed', async () => { it('failed', async () => {
@ -82,7 +92,7 @@ describe('enable plugin', () => {
const { getByTitle, getByText, queryByText } = render(<PluginsManagement />) const { getByTitle, getByText, queryByText } = render(<PluginsManagement />)
await wait() await wait()
fireEvent.click(getByTitle(trans('admin.enablePlugin'))) fireEvent.click(getByTitle(t('admin.enablePlugin')))
await wait() await wait()
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', { expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
@ -92,7 +102,7 @@ describe('enable plugin', () => {
expect(queryByText('unresolved')).toBeInTheDocument() expect(queryByText('unresolved')).toBeInTheDocument()
expect(queryByText('abc')).toBeInTheDocument() expect(queryByText('abc')).toBeInTheDocument()
fireEvent.click(getByText(trans('general.confirm'))) fireEvent.click(getByText(t('general.confirm')))
}) })
}) })
@ -107,13 +117,13 @@ describe('disable plugin', () => {
]) ])
}) })
it('successfully', async () => { it('succeeded', async () => {
fetch.post.mockResolvedValue({ code: 0, message: 'success' }) fetch.post.mockResolvedValue({ code: 0, message: 'success' })
const { getByTitle, getByRole, queryByText } = render(<PluginsManagement />) const { getByTitle, getByRole, queryByText } = render(<PluginsManagement />)
await wait() await wait()
fireEvent.click(getByTitle(trans('admin.disablePlugin'))) fireEvent.click(getByTitle(t('admin.disablePlugin')))
await wait() await wait()
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', { expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
@ -122,7 +132,7 @@ describe('disable plugin', () => {
}) })
expect(queryByText('success')).toBeInTheDocument() expect(queryByText('success')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success') expect(getByRole('status')).toHaveClass('alert-success')
expect(getByTitle(trans('admin.enablePlugin'))).not.toBeChecked() expect(getByTitle(t('admin.enablePlugin'))).not.toBeChecked()
}) })
it('failed', async () => { it('failed', async () => {
@ -131,7 +141,7 @@ describe('disable plugin', () => {
const { getByTitle, getByRole, queryByText } = render(<PluginsManagement />) const { getByTitle, getByRole, queryByText } = render(<PluginsManagement />)
await wait() await wait()
fireEvent.click(getByTitle(trans('admin.disablePlugin'))) fireEvent.click(getByTitle(t('admin.disablePlugin')))
await wait() await wait()
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', { expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
@ -159,14 +169,14 @@ describe('delete plugin', () => {
const { getByTitle, getByText } = render(<PluginsManagement />) const { getByTitle, getByText } = render(<PluginsManagement />)
await wait() await wait()
fireEvent.click(getByTitle(trans('admin.deletePlugin'))) fireEvent.click(getByTitle(t('admin.deletePlugin')))
fireEvent.click(getByText(trans('general.cancel'))) fireEvent.click(getByText(t('general.cancel')))
await wait() await wait()
expect(fetch.post).not.toBeCalled() expect(fetch.post).not.toBeCalled()
}) })
it('successfully', async () => { it('succeeded', async () => {
fetch.post.mockResolvedValue({ code: 0, message: 'success' }) fetch.post.mockResolvedValue({ code: 0, message: 'success' })
const { getByTitle, getByText, getByRole, queryByText } = render( const { getByTitle, getByText, getByRole, queryByText } = render(
@ -174,8 +184,8 @@ describe('delete plugin', () => {
) )
await wait() await wait()
fireEvent.click(getByTitle(trans('admin.deletePlugin'))) fireEvent.click(getByTitle(t('admin.deletePlugin')))
fireEvent.click(getByText(trans('general.confirm'))) fireEvent.click(getByText(t('general.confirm')))
await wait() await wait()
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', { expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
@ -195,8 +205,8 @@ describe('delete plugin', () => {
) )
await wait() await wait()
fireEvent.click(getByTitle(trans('admin.deletePlugin'))) fireEvent.click(getByTitle(t('admin.deletePlugin')))
fireEvent.click(getByText(trans('general.confirm'))) fireEvent.click(getByText(t('general.confirm')))
await wait() await wait()
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', { expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
@ -208,3 +218,129 @@ describe('delete plugin', () => {
expect(queryByText('My Plugin')).not.toBeNull() expect(queryByText('My Plugin')).not.toBeNull()
}) })
}) })
describe('upload plugin archive', () => {
it('no selected file', async () => {
fetch.get.mockResolvedValue([])
const { getAllByText } = render(<PluginsManagement />)
await wait()
fireEvent.click(getAllByText(t('general.submit'))[0])
expect(fetch.post).not.toBeCalled()
})
it('succeeded', async () => {
fetch.get.mockResolvedValue([])
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
const { getByTitle, getAllByText, getByRole, queryByText } = render(
<PluginsManagement />,
)
await wait()
const file = new File([], 'plugin.zip')
fireEvent.change(getByTitle(t('skinlib.upload.select-file')), {
target: { files: [file] },
})
fireEvent.click(getAllByText(t('general.submit'))[0])
await wait()
expect(fetch.get).toBeCalledTimes(2)
expect(fetch.post).toBeCalledWith(
'/admin/plugins/upload',
expect.any(FormData),
)
const formData: FormData = fetch.post.mock.calls[0][1]
expect(formData.get('file')).toStrictEqual(file)
expect(queryByText('plugin.zip')).not.toBeInTheDocument()
expect(queryByText('ok')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
})
it('failed', async () => {
fetch.get.mockResolvedValue([])
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const { getByTitle, getAllByText, getByRole, queryByText } = render(
<PluginsManagement />,
)
await wait()
const file = new File([], 'plugin.zip')
fireEvent.change(getByTitle(t('skinlib.upload.select-file')), {
target: { files: [file] },
})
fireEvent.click(getAllByText(t('general.submit'))[0])
await wait()
expect(fetch.get).toBeCalledTimes(1)
expect(fetch.post).toBeCalledWith(
'/admin/plugins/upload',
expect.any(FormData),
)
expect(queryByText('plugin.zip')).toBeInTheDocument()
expect(queryByText('failed')).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-danger')
})
})
describe('submit remote URL', () => {
it('succeeded', async () => {
fetch.get.mockResolvedValue([])
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
const {
getByLabelText,
getAllByText,
getByRole,
queryByText,
queryByDisplayValue,
} = render(<PluginsManagement />)
await wait()
fireEvent.input(getByLabelText('URL'), {
target: { value: 'https://example.com/a.zip' },
})
fireEvent.click(getAllByText(t('general.submit'))[1])
await wait()
expect(fetch.get).toBeCalledTimes(2)
expect(fetch.post).toBeCalledWith('/admin/plugins/wget', {
url: 'https://example.com/a.zip',
})
expect(
queryByDisplayValue('https://example.com/a.zip'),
).not.toBeInTheDocument()
expect(queryByText('ok')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
})
it('failed', async () => {
fetch.get.mockResolvedValue([])
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const {
getByLabelText,
getAllByText,
getByRole,
queryByText,
queryByDisplayValue,
} = render(<PluginsManagement />)
await wait()
fireEvent.input(getByLabelText('URL'), {
target: { value: 'https://example.com/a.zip' },
})
fireEvent.click(getAllByText(t('general.submit'))[1])
await wait()
expect(fetch.get).toBeCalledTimes(1)
expect(fetch.post).toBeCalledWith('/admin/plugins/wget', {
url: 'https://example.com/a.zip',
})
expect(queryByDisplayValue('https://example.com/a.zip')).toBeInTheDocument()
expect(queryByText('failed')).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-danger')
})
})

View File

@ -237,6 +237,10 @@ admin:
enablePlugin: Enable enablePlugin: Enable
disablePlugin: Disable disablePlugin: Disable
confirmDeletion: Are you sure to delete this plugin? confirmDeletion: Are you sure to delete this plugin?
uploadArchive: Upload Archive
uploadArchiveNotice: Install a plugin by uploading a Zip archive.
downloadRemote: Download From Remote
downloadRemoteNotice: Install a plugin by downloading a Zip archive from remote URL.
noDependenciesNotice: >- noDependenciesNotice: >-
There is no dependency definition in the plugin. It means that the plugin There is no dependency definition in the plugin. It means that the plugin
may be not compatible with the current version of Blessing Skin, and may be not compatible with the current version of Blessing Skin, and

View File

@ -24,6 +24,8 @@
- Added Blessing Skin Shell. - Added Blessing Skin Shell.
- Support specifying "from" email address and name when sending email. - Support specifying "from" email address and name when sending email.
- 3D skin viewer can be with background now. - 3D skin viewer can be with background now.
- Added support of installing plugin by uploading archive.
- Added support of installing plugin by submitting remote URL.
## Tweaked ## Tweaked

View File

@ -24,6 +24,8 @@
- 新增 Blessing Skin Shell - 新增 Blessing Skin Shell
- 支持单独指定邮件发件人的地址和名称 - 支持单独指定邮件发件人的地址和名称
- 3D 皮肤预览现在是带背景的 - 3D 皮肤预览现在是带背景的
- 可通过上传压缩包来安装插件
- 可通过提交 URL 来安装插件
## 调整 ## 调整

View File

@ -161,6 +161,8 @@ Route::prefix('admin')
Route::post('manage', 'PluginController@manage'); Route::post('manage', 'PluginController@manage');
Route::any('config/{name}', 'PluginController@config'); Route::any('config/{name}', 'PluginController@config');
Route::get('readme/{name}', 'PluginController@readme'); Route::get('readme/{name}', 'PluginController@readme');
Route::post('upload', 'PluginController@upload');
Route::post('wget', 'PluginController@wget');
Route::prefix('market')->group(function () { Route::prefix('market')->group(function () {
Route::view('', 'admin.market'); Route::view('', 'admin.market');

View File

@ -4,7 +4,11 @@ namespace Tests;
use App\Services\Plugin; use App\Services\Plugin;
use App\Services\PluginManager; use App\Services\PluginManager;
use App\Services\Unzip;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Http;
use Mockery\MockInterface;
class PluginControllerTest extends TestCase class PluginControllerTest extends TestCase
{ {
@ -263,4 +267,71 @@ class PluginControllerTest extends TestCase
], ],
]); ]);
} }
public function testUpload()
{
// Missing file.
$this->postJson('/admin/plugins/upload')->assertJsonValidationErrors('file');
// Not a file.
$this->postJson('/admin/plugins/upload', ['file' => 'f'])
->assertJsonValidationErrors('file');
// Not a zip.
$file = UploadedFile::fake()->create('plugin.zip', 0, 'application/x-tar');
$this->postJson('/admin/plugins/upload', ['file' => $file])
->assertJsonValidationErrors('file');
// Success.
$file = UploadedFile::fake()->create('plugin.zip', 0, 'application/zip');
$this->mock(Unzip::class, function (MockInterface $mock) {
$mock->shouldReceive('extract')->withArgs(function ($path, $dest) {
$this->assertEquals(
resolve(PluginManager::class)->getPluginsDirs()->first(),
$dest
);
return true;
})->once();
});
$this->postJson('/admin/plugins/upload', ['file' => $file])
->assertJson([
'code' => 0,
'message' => trans('admin.plugins.market.install-success'),
]);
}
public function testWget()
{
// Missing url.
$this->postJson('/admin/plugins/wget')->assertJsonValidationErrors('url');
// Not a url.
$this->postJson('/admin/plugins/wget', ['url' => 'f'])
->assertJsonValidationErrors('url');
Http::fakeSequence()->pushStatus(404)->pushStatus(200);
$this->postJson('/admin/plugins/wget', ['url' => 'https://down.org/a.zip'])
->assertJson([
'code' => 1,
'message' => trans('admin.download.errors.download', ['error' => 404]),
]);
$this->mock(Unzip::class, function (MockInterface $mock) {
$mock->shouldReceive('extract')->withArgs(function ($path, $dest) {
$this->assertEquals(
resolve(PluginManager::class)->getPluginsDirs()->first(),
$dest
);
return true;
})->once();
});
$this->postJson('/admin/plugins/wget', ['url' => 'https://down.org/a.zip'])
->assertJson([
'code' => 0,
'message' => trans('admin.plugins.market.install-success'),
]);
}
} }