build OAuth mgmt page with React
This commit is contained in:
parent
de64694002
commit
93f64b034f
14
resources/assets/src/components/ButtonEdit.tsx
Normal file
14
resources/assets/src/components/ButtonEdit.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string
|
||||||
|
onClick: React.MouseEventHandler<HTMLAnchorElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonEdit: React.FC<Props> = props => (
|
||||||
|
<a href="#" title={props.title} className="ml-2" onClick={props.onClick}>
|
||||||
|
<i className="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ButtonEdit
|
||||||
|
|
@ -36,7 +36,7 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'user/oauth/manage',
|
path: 'user/oauth/manage',
|
||||||
component: () => import('../views/user/OAuth.vue'),
|
react: () => import('../views/user/OAuth'),
|
||||||
el: '.content > .container-fluid',
|
el: '.content > .container-fluid',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,207 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="container-fluid">
|
|
||||||
<button
|
|
||||||
type="primary"
|
|
||||||
class="btn-create-app btn btn-primary"
|
|
||||||
data-toggle="modal"
|
|
||||||
data-target="#modal-create"
|
|
||||||
>
|
|
||||||
{{ $t('user.oauth.create') }}
|
|
||||||
</button>
|
|
||||||
<vue-good-table
|
|
||||||
:rows="clients"
|
|
||||||
:columns="columns"
|
|
||||||
:search-options="tableOptions.search"
|
|
||||||
:pagination-options="tableOptions.pagination"
|
|
||||||
style-class="vgt-table striped"
|
|
||||||
>
|
|
||||||
<template #table-row="props">
|
|
||||||
<span v-if="props.column.field === 'name'">
|
|
||||||
{{ props.formattedRow[props.column.field] }}
|
|
||||||
<a
|
|
||||||
:title="$t('user.oauth.modifyName')"
|
|
||||||
href="#"
|
|
||||||
data-test="name"
|
|
||||||
@click="modifyName(props.row)"
|
|
||||||
>
|
|
||||||
<i class="fas fa-edit btn-edit" />
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<span v-else-if="props.column.field === 'redirect'">
|
|
||||||
{{ props.formattedRow[props.column.field] }}
|
|
||||||
<a
|
|
||||||
:title="$t('user.oauth.modifyUrl')"
|
|
||||||
href="#"
|
|
||||||
data-test="callback"
|
|
||||||
@click="modifyCallback(props.row)"
|
|
||||||
>
|
|
||||||
<i class="fas fa-edit btn-edit" />
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<span v-else-if="props.column.field === 'operations'">
|
|
||||||
<button class="btn btn-danger" data-test="remove" @click="remove(props.row)">
|
|
||||||
{{ $t('report.delete') }}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
{{ props.formattedRow[props.column.field] }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</vue-good-table>
|
|
||||||
|
|
||||||
<modal id="modal-create" :title="$t('user.oauth.create')" @confirm="create">
|
|
||||||
<table class="table">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td v-t="'user.oauth.name'" class="key" />
|
|
||||||
<td class="value">
|
|
||||||
<input v-model="name" class="form-control" type="text">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td v-t="'user.oauth.redirect'" class="key" />
|
|
||||||
<td class="value">
|
|
||||||
<input v-model="callback" class="form-control" type="text">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { VueGoodTable } from 'vue-good-table'
|
|
||||||
import 'vue-good-table/dist/vue-good-table.min.css'
|
|
||||||
import Modal from '../../components/Modal.vue'
|
|
||||||
import tableOptions from '../../components/mixins/tableOptions'
|
|
||||||
import emitMounted from '../../components/mixins/emitMounted'
|
|
||||||
import { walkFetch, init } from '../../scripts/net'
|
|
||||||
import { showModal, toast } from '../../scripts/notify'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'OAuthApps',
|
|
||||||
components: {
|
|
||||||
Modal,
|
|
||||||
VueGoodTable,
|
|
||||||
},
|
|
||||||
mixins: [
|
|
||||||
emitMounted,
|
|
||||||
tableOptions,
|
|
||||||
],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
name: '',
|
|
||||||
callback: '',
|
|
||||||
clients: [],
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'id', label: this.$t('user.oauth.id'), type: 'number',
|
|
||||||
},
|
|
||||||
{ field: 'name', label: this.$t('user.oauth.name') },
|
|
||||||
{
|
|
||||||
field: 'secret',
|
|
||||||
label: this.$t('user.oauth.secret'),
|
|
||||||
sortable: false,
|
|
||||||
globalSearchDisabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'redirect',
|
|
||||||
label: this.$t('user.oauth.redirect'),
|
|
||||||
sortable: false,
|
|
||||||
globalSearchDisabled: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'operations',
|
|
||||||
label: this.$t('admin.operationsTitle'),
|
|
||||||
sortable: false,
|
|
||||||
globalSearchDisabled: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async fetchData() {
|
|
||||||
this.clients = await this.$http.get('/oauth/clients')
|
|
||||||
},
|
|
||||||
async create() {
|
|
||||||
const client = await this.$http.post('/oauth/clients', {
|
|
||||||
name: this.name,
|
|
||||||
redirect: this.callback,
|
|
||||||
})
|
|
||||||
if (client.id) {
|
|
||||||
this.clients.unshift(client)
|
|
||||||
} else {
|
|
||||||
toast.error(client.message)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async modifyName(client) {
|
|
||||||
let name
|
|
||||||
try {
|
|
||||||
({ value: name } = await showModal({
|
|
||||||
mode: 'prompt',
|
|
||||||
title: this.$t('user.oauth.name'),
|
|
||||||
input: client.name,
|
|
||||||
}))
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await this.modify(client, { name })
|
|
||||||
},
|
|
||||||
async modifyCallback(client) {
|
|
||||||
let redirect
|
|
||||||
try {
|
|
||||||
({ value: redirect } = await showModal({
|
|
||||||
mode: 'prompt',
|
|
||||||
title: this.$t('user.oauth.redirect'),
|
|
||||||
input: client.redirect,
|
|
||||||
}))
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await this.modify(client, { redirect })
|
|
||||||
},
|
|
||||||
async modify(client, modified) {
|
|
||||||
const request = new Request(
|
|
||||||
`/oauth/clients/${client.id}`,
|
|
||||||
Object.assign({}, init, {
|
|
||||||
body: JSON.stringify(Object.assign({ name: client.name, redirect: client.redirect }, modified)),
|
|
||||||
method: 'PUT',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
request.headers.set('Content-Type', 'application/json')
|
|
||||||
const result = await walkFetch(request)
|
|
||||||
if (result.id) {
|
|
||||||
Object.assign(client, modified)
|
|
||||||
} else {
|
|
||||||
toast.error(result.message)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async remove(client) {
|
|
||||||
try {
|
|
||||||
await showModal({
|
|
||||||
text: this.$t('user.oauth.confirmRemove'),
|
|
||||||
okButtonType: 'danger',
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const request = new Request(
|
|
||||||
`/oauth/clients/${client.id}`,
|
|
||||||
Object.assign({}, init, { method: 'DELETE' }),
|
|
||||||
)
|
|
||||||
await walkFetch(request)
|
|
||||||
this.$delete(this.clients, this.clients.findIndex(({ id }) => id === client.id))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
.btn-create-app
|
|
||||||
margin-bottom 5px
|
|
||||||
margin-right 10px
|
|
||||||
</style>
|
|
||||||
70
resources/assets/src/views/user/OAuth/ModalCreate.tsx
Normal file
70
resources/assets/src/views/user/OAuth/ModalCreate.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import Modal from '../../../components/Modal'
|
||||||
|
import { trans } from '../../../scripts/i18n'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onCreate(name: string, redirect: string): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalCreate: React.FC<Props> = props => {
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [url, setUrl] = useState('')
|
||||||
|
|
||||||
|
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setName(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUrlChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setUrl(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleComplete = () => {
|
||||||
|
props.onCreate(name, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDismiss = () => {
|
||||||
|
setName('')
|
||||||
|
setUrl('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
id="modal-create"
|
||||||
|
onConfirm={handleComplete}
|
||||||
|
onDismiss={handleDismiss}
|
||||||
|
>
|
||||||
|
<table className="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td className="key">{trans('user.oauth.name')}</td>
|
||||||
|
<td className="value">
|
||||||
|
<input
|
||||||
|
value={name}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
className="form-control"
|
||||||
|
placeholder={trans('user.oauth.name')}
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="key">{trans('user.oauth.redirect')}</td>
|
||||||
|
<td className="value">
|
||||||
|
<input
|
||||||
|
value={url}
|
||||||
|
onChange={handleUrlChange}
|
||||||
|
className="form-control"
|
||||||
|
placeholder={trans('user.oauth.redirect')}
|
||||||
|
type="url"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalCreate
|
||||||
43
resources/assets/src/views/user/OAuth/Row.tsx
Normal file
43
resources/assets/src/views/user/OAuth/Row.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { trans } from '../../../scripts/i18n'
|
||||||
|
import ButtonEdit from '../../../components/ButtonEdit'
|
||||||
|
import { App } from './types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
app: App
|
||||||
|
onEditName: React.MouseEventHandler<HTMLAnchorElement>
|
||||||
|
onEditRedirect: React.MouseEventHandler<HTMLAnchorElement>
|
||||||
|
onDelete: React.MouseEventHandler<HTMLButtonElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
const Row: React.FC<Props> = props => {
|
||||||
|
const { app } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>{app.id}</td>
|
||||||
|
<td>
|
||||||
|
<span>{app.name}</span>
|
||||||
|
<ButtonEdit
|
||||||
|
title={trans('user.oauth.modifyName')}
|
||||||
|
onClick={props.onEditName}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>{app.secret}</td>
|
||||||
|
<td>
|
||||||
|
<span>{app.redirect}</span>
|
||||||
|
<ButtonEdit
|
||||||
|
title={trans('user.oauth.modifyUrl')}
|
||||||
|
onClick={props.onEditRedirect}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button className="btn btn-danger" onClick={props.onDelete}>
|
||||||
|
{trans('report.delete')}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Row
|
||||||
155
resources/assets/src/views/user/OAuth/index.tsx
Normal file
155
resources/assets/src/views/user/OAuth/index.tsx
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
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 { showModal, toast } from '../../../scripts/notify'
|
||||||
|
import Loading from '../../../components/Loading'
|
||||||
|
import Row from './Row'
|
||||||
|
import ModalCreate from './ModalCreate'
|
||||||
|
import { App } from './types'
|
||||||
|
|
||||||
|
type Exception = {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const OAuth: React.FC = () => {
|
||||||
|
const [apps, setApps] = useState<App[]>([])
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getApps = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
const allApps = await fetch.get<App[]>('/oauth/clients')
|
||||||
|
setApps(allApps)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
getApps()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleAdd = async (name: string, redirect: string) => {
|
||||||
|
const result = await fetch.post<App | Exception>('/oauth/clients', {
|
||||||
|
name,
|
||||||
|
redirect,
|
||||||
|
})
|
||||||
|
if ('id' in result) {
|
||||||
|
setApps(apps => [...apps, result])
|
||||||
|
} else {
|
||||||
|
toast.error(result.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editName = async (app: App, index: number) => {
|
||||||
|
let name: string
|
||||||
|
try {
|
||||||
|
;({ value: name } = await showModal({
|
||||||
|
mode: 'prompt',
|
||||||
|
title: trans('user.oauth.name'),
|
||||||
|
input: app.name,
|
||||||
|
}))
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await fetch.put<App | Exception>(
|
||||||
|
`/oauth/clients/${app.id}`,
|
||||||
|
{ ...app, name },
|
||||||
|
)
|
||||||
|
if ('id' in result) {
|
||||||
|
setApps(apps => {
|
||||||
|
apps[index].name = name
|
||||||
|
return apps.slice()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast.error(result.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editRedirect = async (app: App, index: number) => {
|
||||||
|
let redirect: string
|
||||||
|
try {
|
||||||
|
;({ value: redirect } = await showModal({
|
||||||
|
mode: 'prompt',
|
||||||
|
title: trans('user.oauth.redirect'),
|
||||||
|
input: app.redirect,
|
||||||
|
}))
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await fetch.put<App | Exception>(
|
||||||
|
`/oauth/clients/${app.id}`,
|
||||||
|
{ ...app, redirect },
|
||||||
|
)
|
||||||
|
if ('id' in result) {
|
||||||
|
setApps(apps => {
|
||||||
|
apps[index].redirect = redirect
|
||||||
|
return apps.slice()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast.error(result.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (app: App) => {
|
||||||
|
try {
|
||||||
|
await showModal({
|
||||||
|
text: trans('user.oauth.confirmRemove'),
|
||||||
|
okButtonType: 'danger',
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetch.del(`/oauth/clients/${app.id}`)
|
||||||
|
setApps(apps => apps.filter(a => a.id !== app.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
data-toggle="modal"
|
||||||
|
data-target="#modal-create"
|
||||||
|
>
|
||||||
|
{trans('user.oauth.create')}
|
||||||
|
</button>
|
||||||
|
<div className="card mt-2">
|
||||||
|
<div className="card-body p-0">
|
||||||
|
<table className="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{trans('user.oauth.id')}</th>
|
||||||
|
<th>{trans('user.oauth.name')}</th>
|
||||||
|
<th>{trans('user.oauth.secret')}</th>
|
||||||
|
<th>{trans('user.oauth.redirect')}</th>
|
||||||
|
<th>{trans('admin.operationsTitle')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{apps.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td className="text-center" colSpan={5}>
|
||||||
|
{isLoading ? <Loading /> : 'Nothing here.'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
apps.map((app, i) => (
|
||||||
|
<Row
|
||||||
|
key={app.id}
|
||||||
|
app={app}
|
||||||
|
onEditName={() => editName(app, i)}
|
||||||
|
onEditRedirect={() => editRedirect(app, i)}
|
||||||
|
onDelete={() => handleDelete(app)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ModalCreate onCreate={handleAdd} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hot(OAuth)
|
||||||
6
resources/assets/src/views/user/OAuth/types.ts
Normal file
6
resources/assets/src/views/user/OAuth/types.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export type App = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
secret: string
|
||||||
|
redirect: string
|
||||||
|
}
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
import Vue from 'vue'
|
|
||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import { flushPromises } from '../../utils'
|
|
||||||
import { walkFetch } from '@/scripts/net'
|
|
||||||
import { showModal, toast } from '@/scripts/notify'
|
|
||||||
import Modal from '@/components/Modal.vue'
|
|
||||||
import OAuth from '@/views/user/OAuth.vue'
|
|
||||||
|
|
||||||
jest.mock('@/scripts/notify')
|
|
||||||
|
|
||||||
jest.mock('@/scripts/net', () => ({
|
|
||||||
walkFetch: jest.fn(),
|
|
||||||
init: {},
|
|
||||||
}))
|
|
||||||
|
|
||||||
test('basic render', async () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue([
|
|
||||||
{ id: 1 },
|
|
||||||
])
|
|
||||||
const wrapper = mount(OAuth)
|
|
||||||
await flushPromises()
|
|
||||||
expect(wrapper.findAll('[data-test=remove]')).toHaveLength(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('create app', async () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue([])
|
|
||||||
Vue.prototype.$http.post
|
|
||||||
.mockResolvedValueOnce({ message: 'fail' })
|
|
||||||
.mockResolvedValueOnce({ id: 1, name: 'name' })
|
|
||||||
const wrapper = mount(OAuth)
|
|
||||||
await flushPromises()
|
|
||||||
|
|
||||||
const modal = wrapper.find(Modal)
|
|
||||||
const inputs = wrapper.findAll('.value')
|
|
||||||
inputs.at(0).find('input')
|
|
||||||
.setValue('name')
|
|
||||||
inputs.at(1).find('input')
|
|
||||||
.setValue('https://example.com/')
|
|
||||||
|
|
||||||
modal.vm.$emit('confirm')
|
|
||||||
await flushPromises()
|
|
||||||
expect(Vue.prototype.$http.post).toBeCalledWith(
|
|
||||||
'/oauth/clients',
|
|
||||||
{ name: 'name', redirect: 'https://example.com/' },
|
|
||||||
)
|
|
||||||
expect(toast.error).toBeCalledWith('fail')
|
|
||||||
|
|
||||||
modal.vm.$emit('confirm')
|
|
||||||
await flushPromises()
|
|
||||||
expect(wrapper.text()).toContain('name')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('modify name', async () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue([
|
|
||||||
{ id: 1, name: 'old' },
|
|
||||||
])
|
|
||||||
walkFetch
|
|
||||||
.mockResolvedValueOnce({ message: 'fail' })
|
|
||||||
.mockResolvedValueOnce({ id: 1, name: 'new-name' })
|
|
||||||
showModal
|
|
||||||
.mockRejectedValueOnce(null)
|
|
||||||
.mockResolvedValue({ value: 'new-name' })
|
|
||||||
const wrapper = mount(OAuth)
|
|
||||||
await flushPromises()
|
|
||||||
const button = wrapper.find('[data-test=name]')
|
|
||||||
|
|
||||||
button.trigger('click')
|
|
||||||
await flushPromises()
|
|
||||||
expect(walkFetch).not.toBeCalled()
|
|
||||||
|
|
||||||
button.trigger('click')
|
|
||||||
await flushPromises()
|
|
||||||
expect(walkFetch).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
url: '/oauth/clients/1',
|
|
||||||
body: JSON.stringify({ name: 'new-name' }),
|
|
||||||
method: 'PUT',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
expect(toast.error).toBeCalledWith('fail')
|
|
||||||
|
|
||||||
button.trigger('click')
|
|
||||||
await flushPromises()
|
|
||||||
expect(wrapper.text()).toContain('new-name')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('modify redirect', async () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue([
|
|
||||||
{ id: 1, redirect: 'https://example.com/' },
|
|
||||||
])
|
|
||||||
walkFetch
|
|
||||||
.mockResolvedValueOnce({ message: 'fail' })
|
|
||||||
.mockResolvedValueOnce({ id: 1, redirect: 'https://example.net/' })
|
|
||||||
showModal
|
|
||||||
.mockRejectedValueOnce(null)
|
|
||||||
.mockResolvedValue({ value: 'https://example.net/' })
|
|
||||||
const wrapper = mount(OAuth)
|
|
||||||
await flushPromises()
|
|
||||||
const button = wrapper.find('[data-test=callback]')
|
|
||||||
|
|
||||||
button.trigger('click')
|
|
||||||
await flushPromises()
|
|
||||||
expect(walkFetch).not.toBeCalled()
|
|
||||||
|
|
||||||
button.trigger('click')
|
|
||||||
await flushPromises()
|
|
||||||
expect(walkFetch).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
url: '/oauth/clients/1',
|
|
||||||
body: JSON.stringify({ redirect: 'https://example.net/' }),
|
|
||||||
method: 'PUT',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
expect(toast.error).toBeCalledWith('fail')
|
|
||||||
|
|
||||||
button.trigger('click')
|
|
||||||
await flushPromises()
|
|
||||||
expect(wrapper.text()).toContain('https://example.net/')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('remove app', async () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue([
|
|
||||||
{ id: 1, name: 'name' },
|
|
||||||
])
|
|
||||||
showModal
|
|
||||||
.mockRejectedValueOnce(null)
|
|
||||||
.mockResolvedValue({ value: '' })
|
|
||||||
|
|
||||||
const wrapper = mount(OAuth)
|
|
||||||
await flushPromises()
|
|
||||||
const button = wrapper.find('[data-test=remove]')
|
|
||||||
|
|
||||||
button.trigger('click')
|
|
||||||
await flushPromises()
|
|
||||||
expect(walkFetch).not.toBeCalled()
|
|
||||||
|
|
||||||
button.trigger('click')
|
|
||||||
await flushPromises()
|
|
||||||
expect(wrapper.text()).toContain('No data')
|
|
||||||
})
|
|
||||||
238
resources/assets/tests/views/user/OAuth.test.tsx
Normal file
238
resources/assets/tests/views/user/OAuth.test.tsx
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { render, fireEvent, wait } from '@testing-library/react'
|
||||||
|
import * as fetch from '@/scripts/net'
|
||||||
|
import { trans } from '@/scripts/i18n'
|
||||||
|
import { toast, showModal } from '@/scripts/notify'
|
||||||
|
import OAuth from '@/views/user/OAuth'
|
||||||
|
import { App } from '@/views/user/OAuth/types'
|
||||||
|
|
||||||
|
jest.mock('@/scripts/net')
|
||||||
|
jest.mock('@/scripts/notify')
|
||||||
|
|
||||||
|
const example: App = {
|
||||||
|
id: 1,
|
||||||
|
name: 'My App',
|
||||||
|
redirect: 'http://url.test/',
|
||||||
|
secret: 'abc',
|
||||||
|
}
|
||||||
|
|
||||||
|
test('loading data', () => {
|
||||||
|
fetch.get.mockResolvedValue([])
|
||||||
|
const { queryByTitle } = render(<OAuth />)
|
||||||
|
expect(queryByTitle('Loading...')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('create app', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch.get.mockResolvedValue([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('succeeded', async () => {
|
||||||
|
fetch.post.mockResolvedValue(example)
|
||||||
|
const { getByPlaceholderText, getByText, queryByText } = render(<OAuth />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(trans('user.oauth.create')))
|
||||||
|
fireEvent.input(getByPlaceholderText(trans('user.oauth.name')), {
|
||||||
|
target: { value: 'My App' },
|
||||||
|
})
|
||||||
|
fireEvent.input(getByPlaceholderText(trans('user.oauth.redirect')), {
|
||||||
|
target: { value: 'http://url.test/' },
|
||||||
|
})
|
||||||
|
fireEvent.click(getByText(trans('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).toBeCalledWith('/oauth/clients', {
|
||||||
|
name: 'My App',
|
||||||
|
redirect: 'http://url.test/',
|
||||||
|
})
|
||||||
|
expect(queryByText(example.id.toString())).toBeInTheDocument()
|
||||||
|
expect(queryByText(example.name)).toBeInTheDocument()
|
||||||
|
expect(queryByText(example.redirect)).toBeInTheDocument()
|
||||||
|
expect(queryByText(example.secret)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('failed', async () => {
|
||||||
|
fetch.post.mockResolvedValue({ message: 'exception' })
|
||||||
|
const { getByPlaceholderText, getByText, queryByText } = render(<OAuth />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(trans('user.oauth.create')))
|
||||||
|
fireEvent.input(getByPlaceholderText(trans('user.oauth.name')), {
|
||||||
|
target: { value: 'My App' },
|
||||||
|
})
|
||||||
|
fireEvent.input(getByPlaceholderText(trans('user.oauth.redirect')), {
|
||||||
|
target: { value: 'http://url.test/' },
|
||||||
|
})
|
||||||
|
fireEvent.click(getByText(trans('general.confirm')))
|
||||||
|
|
||||||
|
await wait()
|
||||||
|
expect(fetch.post).toBeCalledWith('/oauth/clients', {
|
||||||
|
name: 'My App',
|
||||||
|
redirect: 'http://url.test/',
|
||||||
|
})
|
||||||
|
expect(toast.error).toBeCalledWith('exception')
|
||||||
|
expect(queryByText(example.name)).not.toBeInTheDocument()
|
||||||
|
expect(queryByText(example.redirect)).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancel dialog', async () => {
|
||||||
|
const { getByPlaceholderText, getByText } = render(<OAuth />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(trans('user.oauth.create')))
|
||||||
|
fireEvent.input(getByPlaceholderText(trans('user.oauth.name')), {
|
||||||
|
target: { value: 'My App' },
|
||||||
|
})
|
||||||
|
fireEvent.input(getByPlaceholderText(trans('user.oauth.redirect')), {
|
||||||
|
target: { value: 'http://url.test/' },
|
||||||
|
})
|
||||||
|
fireEvent.click(getByText(trans('general.cancel')))
|
||||||
|
|
||||||
|
await wait()
|
||||||
|
expect(fetch.post).not.toBeCalled()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(trans('user.oauth.create')))
|
||||||
|
expect(getByPlaceholderText(trans('user.oauth.name'))).toHaveValue('')
|
||||||
|
expect(getByPlaceholderText(trans('user.oauth.redirect'))).toHaveValue('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('edit app', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch.get.mockResolvedValue([example])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('edit name', () => {
|
||||||
|
it('succeeded', async () => {
|
||||||
|
fetch.put.mockResolvedValue({ ...example, name: 'new name' })
|
||||||
|
showModal.mockResolvedValue({ value: 'new name' })
|
||||||
|
|
||||||
|
const { getByTitle, queryByText } = render(<OAuth />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByTitle(trans('user.oauth.modifyName')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.put).toBeCalledWith(`/oauth/clients/${example.id}`, {
|
||||||
|
...example,
|
||||||
|
name: 'new name',
|
||||||
|
})
|
||||||
|
expect(queryByText('new name')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('failed', async () => {
|
||||||
|
fetch.put.mockResolvedValue({ message: 'exception' })
|
||||||
|
showModal.mockResolvedValue({ value: 'new name' })
|
||||||
|
|
||||||
|
const { getByTitle, queryByText } = render(<OAuth />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByTitle(trans('user.oauth.modifyName')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.put).toBeCalledWith(`/oauth/clients/${example.id}`, {
|
||||||
|
...example,
|
||||||
|
name: 'new name',
|
||||||
|
})
|
||||||
|
expect(queryByText(example.name)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancel dialog', async () => {
|
||||||
|
showModal.mockRejectedValue(null)
|
||||||
|
|
||||||
|
const { getByTitle, queryByText } = render(<OAuth />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByTitle(trans('user.oauth.modifyName')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.put).not.toBeCalled()
|
||||||
|
expect(queryByText(example.name)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('edit redirect url', () => {
|
||||||
|
it('succeeded', async () => {
|
||||||
|
fetch.put.mockResolvedValue({ ...example, redirect: 'http://new.test/' })
|
||||||
|
showModal.mockResolvedValue({ value: 'http://new.test/' })
|
||||||
|
|
||||||
|
const { getByTitle, queryByText } = render(<OAuth />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByTitle(trans('user.oauth.modifyUrl')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.put).toBeCalledWith(`/oauth/clients/${example.id}`, {
|
||||||
|
...example,
|
||||||
|
redirect: 'http://new.test/',
|
||||||
|
})
|
||||||
|
expect(queryByText('http://new.test/')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('failed', async () => {
|
||||||
|
fetch.put.mockResolvedValue({ message: 'exception' })
|
||||||
|
showModal.mockResolvedValue({ value: 'http://new.test/' })
|
||||||
|
|
||||||
|
const { getByTitle, queryByText } = render(<OAuth />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByTitle(trans('user.oauth.modifyUrl')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.put).toBeCalledWith(`/oauth/clients/${example.id}`, {
|
||||||
|
...example,
|
||||||
|
redirect: 'http://new.test/',
|
||||||
|
})
|
||||||
|
expect(toast.error).toBeCalledWith('exception')
|
||||||
|
expect(queryByText(example.redirect)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancel dialog', async () => {
|
||||||
|
showModal.mockRejectedValue(null)
|
||||||
|
|
||||||
|
const { getByTitle, queryByText } = render(<OAuth />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByTitle(trans('user.oauth.modifyUrl')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.put).not.toBeCalled()
|
||||||
|
expect(queryByText(example.redirect)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('delete app', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch.get.mockResolvedValue([example])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('succeeded', async () => {
|
||||||
|
showModal.mockResolvedValue({ value: '' })
|
||||||
|
|
||||||
|
const { getByText, queryByText } = render(<OAuth />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(trans('report.delete')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.del).toBeCalledWith(`/oauth/clients/${example.id}`)
|
||||||
|
expect(queryByText(example.name)).not.toBeInTheDocument()
|
||||||
|
expect(queryByText(example.redirect)).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancel dialog', async () => {
|
||||||
|
showModal.mockRejectedValue(null)
|
||||||
|
|
||||||
|
const { getByText, queryByText } = render(<OAuth />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(trans('report.delete')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).not.toBeCalled()
|
||||||
|
expect(queryByText(example.name)).toBeInTheDocument()
|
||||||
|
expect(queryByText(example.redirect)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user