add two new ways to install plugin
This commit is contained in:
parent
ba757d98a8
commit
271c950ad9
@ -4,7 +4,10 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\Plugin;
|
||||
use App\Services\PluginManager;
|
||||
use App\Services\Unzip;
|
||||
use Composer\CaBundle\CaBundle;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Parsedown;
|
||||
|
||||
class PluginController extends Controller
|
||||
@ -105,4 +108,33 @@ class PluginController extends Controller
|
||||
})
|
||||
->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,43 +1,39 @@
|
||||
@use '../../../styles/utils';
|
||||
|
||||
.box {
|
||||
cursor: default;
|
||||
transition-property: box-shadow;
|
||||
transition-duration: 0.3s;
|
||||
width: 32%;
|
||||
@media (max-width: 1280px) {
|
||||
width: 47%;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0.5rem 1rem rgba(#000, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 85%;
|
||||
max-width: calc(100% - 70px);
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: -7px;
|
||||
.header {
|
||||
max-width: calc(100% - 40px);
|
||||
}
|
||||
|
||||
a {
|
||||
transition-property: color;
|
||||
transition-duration: 0.3s;
|
||||
color: #000;
|
||||
&:hover {
|
||||
color: #999;
|
||||
}
|
||||
&:not(:last-child) {
|
||||
margin-right: 9px;
|
||||
}
|
||||
.title {
|
||||
@include utils.truncate-text;
|
||||
}
|
||||
|
||||
.actions a {
|
||||
transition-property: color;
|
||||
transition-duration: 0.3s;
|
||||
color: #000;
|
||||
&:hover {
|
||||
color: #999;
|
||||
}
|
||||
&:not(:last-child) {
|
||||
margin-right: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@include utils.truncate-text;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { trans } from '../../../scripts/i18n'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import { Plugin } from './types'
|
||||
import styles from './InfoBox.scss'
|
||||
|
||||
@ -33,26 +33,30 @@ const InfoBox: React.FC<Props> = props => {
|
||||
</span>
|
||||
<div className={`info-box-content ${styles.content}`}>
|
||||
<div className="d-flex justify-content-between">
|
||||
<div>
|
||||
<div className={`d-flex ${styles.header}`}>
|
||||
<input
|
||||
className="mr-2"
|
||||
className="mr-2 d-inline-block"
|
||||
type="checkbox"
|
||||
checked={plugin.enabled}
|
||||
title={
|
||||
plugin.enabled
|
||||
? trans('admin.disablePlugin')
|
||||
: trans('admin.enablePlugin')
|
||||
? t('admin.disablePlugin')
|
||||
: t('admin.enablePlugin')
|
||||
}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<strong className="mr-2">{plugin.title}</strong>
|
||||
<span className="text-gray">v{plugin.version}</span>
|
||||
<strong className={`d-inline-block mr-2 ${styles.title}`}>
|
||||
{plugin.title}
|
||||
</strong>
|
||||
<span className="d-none d-sm-inline-block text-gray">
|
||||
v{plugin.version}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
{plugin.readme && (
|
||||
<a
|
||||
href={`${props.baseUrl}/admin/plugins/readme/${plugin.name}`}
|
||||
title={trans('admin.pluginReadme')}
|
||||
title={t('admin.pluginReadme')}
|
||||
>
|
||||
<i className="fas fa-question" />
|
||||
</a>
|
||||
@ -60,16 +64,12 @@ const InfoBox: React.FC<Props> = props => {
|
||||
{plugin.enabled && plugin.config && (
|
||||
<a
|
||||
href={`${props.baseUrl}/admin/plugins/config/${plugin.name}`}
|
||||
title={trans('admin.configurePlugin')}
|
||||
title={t('admin.configurePlugin')}
|
||||
>
|
||||
<i className="fas fa-cog" />
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
href="#"
|
||||
title={trans('admin.deletePlugin')}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<a href="#" title={t('admin.deletePlugin')} onClick={handleDelete}>
|
||||
<i className="fas fa-trash" />
|
||||
</a>
|
||||
</div>
|
||||
@ -80,4 +80,4 @@ const InfoBox: React.FC<Props> = props => {
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(InfoBox)
|
||||
export default InfoBox
|
||||
|
@ -1,15 +1,21 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { hot } from 'react-hot-loader/root'
|
||||
import { trans } from '../../../scripts/i18n'
|
||||
import * as fetch from '../../../scripts/net'
|
||||
import { toast, showModal } from '../../../scripts/notify'
|
||||
import Loading from '../../../components/Loading'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import * as fetch from '@/scripts/net'
|
||||
import { toast, showModal } from '@/scripts/notify'
|
||||
import FileInput from '@/components/FileInput'
|
||||
import Loading from '@/components/Loading'
|
||||
import InfoBox from './InfoBox'
|
||||
import { Plugin } from './types'
|
||||
|
||||
const PluginsManagement: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
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(() => {
|
||||
const getPlugins = async () => {
|
||||
setLoading(true)
|
||||
@ -73,7 +79,7 @@ const PluginsManagement: React.FC = () => {
|
||||
try {
|
||||
await showModal({
|
||||
title: plugin.title,
|
||||
text: trans('admin.confirmDeletion'),
|
||||
text: t('admin.confirmDeletion'),
|
||||
okButtonType: 'danger',
|
||||
})
|
||||
} catch {
|
||||
@ -93,20 +99,132 @@ const PluginsManagement: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
return loading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<div className="d-flex flex-wrap">
|
||||
{plugins.map((plugin, i) => (
|
||||
<InfoBox
|
||||
key={plugin.name}
|
||||
plugin={plugin}
|
||||
onEnable={plugin => handleEnable(plugin, i)}
|
||||
onDisable={plugin => handleDisable(plugin, i)}
|
||||
onDelete={handleDelete}
|
||||
baseUrl={blessing.base_url}
|
||||
/>
|
||||
))}
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFile(event.target.files![0])
|
||||
}
|
||||
|
||||
const handleUrlChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUrl(event.target.value)
|
||||
}
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from '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 PluginsManagement from '@/views/admin/PluginsManagement'
|
||||
|
||||
@ -24,13 +24,23 @@ test('plugin info box', async () => {
|
||||
icon: {},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
title: 'Another Plugin',
|
||||
version: '0.1.0',
|
||||
description: '',
|
||||
config: true,
|
||||
readme: true,
|
||||
icon: {},
|
||||
enabled: true,
|
||||
},
|
||||
])
|
||||
|
||||
const { queryByTitle, queryByText } = render(<PluginsManagement />)
|
||||
await wait()
|
||||
|
||||
expect(queryByTitle(trans('admin.configurePlugin'))).not.toBeNull()
|
||||
expect(queryByTitle(trans('admin.pluginReadme'))).not.toBeNull()
|
||||
expect(queryByTitle(t('admin.configurePlugin'))).not.toBeNull()
|
||||
expect(queryByTitle(t('admin.pluginReadme'))).not.toBeNull()
|
||||
expect(queryByText('My Plugin')).not.toBeNull()
|
||||
expect(queryByText('v1.0.0')).not.toBeNull()
|
||||
expect(queryByText('desc')).not.toBeNull()
|
||||
@ -47,7 +57,7 @@ describe('enable plugin', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('successfully', async () => {
|
||||
it('succeeded', async () => {
|
||||
fetch.get.mockResolvedValue([
|
||||
{
|
||||
name: 'a',
|
||||
@ -60,7 +70,7 @@ describe('enable plugin', () => {
|
||||
const { getByTitle, getByRole, queryByText } = render(<PluginsManagement />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByTitle(trans('admin.enablePlugin')))
|
||||
fireEvent.click(getByTitle(t('admin.enablePlugin')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
|
||||
@ -69,7 +79,7 @@ describe('enable plugin', () => {
|
||||
})
|
||||
expect(queryByText('success')).toBeInTheDocument()
|
||||
expect(getByRole('status')).toHaveClass('alert-success')
|
||||
expect(getByTitle(trans('admin.disablePlugin'))).toBeChecked()
|
||||
expect(getByTitle(t('admin.disablePlugin'))).toBeChecked()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
@ -82,7 +92,7 @@ describe('enable plugin', () => {
|
||||
const { getByTitle, getByText, queryByText } = render(<PluginsManagement />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByTitle(trans('admin.enablePlugin')))
|
||||
fireEvent.click(getByTitle(t('admin.enablePlugin')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
|
||||
@ -92,7 +102,7 @@ describe('enable plugin', () => {
|
||||
expect(queryByText('unresolved')).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' })
|
||||
|
||||
const { getByTitle, getByRole, queryByText } = render(<PluginsManagement />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByTitle(trans('admin.disablePlugin')))
|
||||
fireEvent.click(getByTitle(t('admin.disablePlugin')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
|
||||
@ -122,7 +132,7 @@ describe('disable plugin', () => {
|
||||
})
|
||||
expect(queryByText('success')).toBeInTheDocument()
|
||||
expect(getByRole('status')).toHaveClass('alert-success')
|
||||
expect(getByTitle(trans('admin.enablePlugin'))).not.toBeChecked()
|
||||
expect(getByTitle(t('admin.enablePlugin'))).not.toBeChecked()
|
||||
})
|
||||
|
||||
it('failed', async () => {
|
||||
@ -131,7 +141,7 @@ describe('disable plugin', () => {
|
||||
const { getByTitle, getByRole, queryByText } = render(<PluginsManagement />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByTitle(trans('admin.disablePlugin')))
|
||||
fireEvent.click(getByTitle(t('admin.disablePlugin')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
|
||||
@ -159,14 +169,14 @@ describe('delete plugin', () => {
|
||||
const { getByTitle, getByText } = render(<PluginsManagement />)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByTitle(trans('admin.deletePlugin')))
|
||||
fireEvent.click(getByText(trans('general.cancel')))
|
||||
fireEvent.click(getByTitle(t('admin.deletePlugin')))
|
||||
fireEvent.click(getByText(t('general.cancel')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).not.toBeCalled()
|
||||
})
|
||||
|
||||
it('successfully', async () => {
|
||||
it('succeeded', async () => {
|
||||
fetch.post.mockResolvedValue({ code: 0, message: 'success' })
|
||||
|
||||
const { getByTitle, getByText, getByRole, queryByText } = render(
|
||||
@ -174,8 +184,8 @@ describe('delete plugin', () => {
|
||||
)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByTitle(trans('admin.deletePlugin')))
|
||||
fireEvent.click(getByText(trans('general.confirm')))
|
||||
fireEvent.click(getByTitle(t('admin.deletePlugin')))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
|
||||
@ -195,8 +205,8 @@ describe('delete plugin', () => {
|
||||
)
|
||||
await wait()
|
||||
|
||||
fireEvent.click(getByTitle(trans('admin.deletePlugin')))
|
||||
fireEvent.click(getByText(trans('general.confirm')))
|
||||
fireEvent.click(getByTitle(t('admin.deletePlugin')))
|
||||
fireEvent.click(getByText(t('general.confirm')))
|
||||
await wait()
|
||||
|
||||
expect(fetch.post).toBeCalledWith('/admin/plugins/manage', {
|
||||
@ -208,3 +218,129 @@ describe('delete plugin', () => {
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
@ -237,6 +237,10 @@ admin:
|
||||
enablePlugin: Enable
|
||||
disablePlugin: Disable
|
||||
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: >-
|
||||
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
|
||||
|
@ -24,6 +24,8 @@
|
||||
- Added Blessing Skin Shell.
|
||||
- Support specifying "from" email address and name when sending email.
|
||||
- 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
|
||||
|
||||
|
@ -24,6 +24,8 @@
|
||||
- 新增 Blessing Skin Shell
|
||||
- 支持单独指定邮件发件人的地址和名称
|
||||
- 3D 皮肤预览现在是带背景的
|
||||
- 可通过上传压缩包来安装插件
|
||||
- 可通过提交 URL 来安装插件
|
||||
|
||||
## 调整
|
||||
|
||||
|
@ -161,6 +161,8 @@ Route::prefix('admin')
|
||||
Route::post('manage', 'PluginController@manage');
|
||||
Route::any('config/{name}', 'PluginController@config');
|
||||
Route::get('readme/{name}', 'PluginController@readme');
|
||||
Route::post('upload', 'PluginController@upload');
|
||||
Route::post('wget', 'PluginController@wget');
|
||||
|
||||
Route::prefix('market')->group(function () {
|
||||
Route::view('', 'admin.market');
|
||||
|
@ -4,7 +4,11 @@ namespace Tests;
|
||||
|
||||
use App\Services\Plugin;
|
||||
use App\Services\PluginManager;
|
||||
use App\Services\Unzip;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Mockery\MockInterface;
|
||||
|
||||
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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user