mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
228 lines
7.3 KiB
TypeScript
228 lines
7.3 KiB
TypeScript
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import PluginsView from '../PluginsView.vue'
|
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
|
import type { PluginUISession } from '@/api/admin'
|
|
|
|
enableAutoUnmount(afterEach)
|
|
|
|
const {
|
|
listPlugins,
|
|
uploadPlugin,
|
|
enablePlugin,
|
|
savePluginConfig,
|
|
createUISession,
|
|
stepUpRun,
|
|
} = vi.hoisted(() => ({
|
|
listPlugins: vi.fn(),
|
|
uploadPlugin: vi.fn(),
|
|
enablePlugin: vi.fn(),
|
|
savePluginConfig: vi.fn(),
|
|
createUISession: vi.fn(),
|
|
stepUpRun: vi.fn((action: () => Promise<unknown>) => action()),
|
|
}))
|
|
|
|
vi.mock('@/api/admin', () => ({
|
|
adminAPI: {
|
|
plugins: {
|
|
list: listPlugins,
|
|
upload: uploadPlugin,
|
|
enable: enablePlugin,
|
|
disable: vi.fn(),
|
|
remove: vi.fn(),
|
|
getConfig: vi.fn().mockResolvedValue({}),
|
|
saveConfig: savePluginConfig,
|
|
test: vi.fn().mockResolvedValue({ success: true, message: 'ok', latency_ms: 1 }),
|
|
createUISession,
|
|
},
|
|
},
|
|
}))
|
|
|
|
vi.mock('@/stores', () => ({
|
|
useAppStore: () => ({
|
|
showError: vi.fn(),
|
|
showSuccess: vi.fn(),
|
|
showInfo: vi.fn(),
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/composables/useStepUp', () => ({
|
|
useStepUp: () => ({ run: stepUpRun }),
|
|
isStepUpBlocked: () => false,
|
|
isStepUpCancelled: () => false,
|
|
stepUpBlockReason: () => '',
|
|
}))
|
|
|
|
vi.mock('vue-i18n', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('vue-i18n')>()),
|
|
useI18n: () => ({ t: (key: string) => key }),
|
|
}))
|
|
|
|
const plugin = {
|
|
id: 7,
|
|
plugin_key: 'local.test.transport',
|
|
name: 'Test Transport',
|
|
version: '1.0.0',
|
|
description: '',
|
|
author: 'test',
|
|
manifest: {
|
|
schema_version: 1,
|
|
id: 'local.test.transport',
|
|
name: 'Test Transport',
|
|
version: '1.0.0',
|
|
requires: {
|
|
sub2api: '>=0.1.0',
|
|
plugin_protocol: 1,
|
|
transport_api: 1,
|
|
ui_bridge: 1,
|
|
},
|
|
capabilities: [],
|
|
ui: { entrypoint: 'ui/index.html' },
|
|
},
|
|
binary_sha256: 'a'.repeat(64),
|
|
signature_status: 'trusted' as const,
|
|
state: 'disabled' as const,
|
|
last_error: '',
|
|
installed_at: '2026-08-22T00:00:00Z',
|
|
updated_at: '2026-08-22T00:00:00Z',
|
|
bindings: [
|
|
{
|
|
id: 1,
|
|
plugin_id: 7,
|
|
capability: 'openai.oauth.outbound_transport.v1',
|
|
platform: 'openai',
|
|
account_type: 'oauth',
|
|
enabled: false,
|
|
rollout_percent: 100,
|
|
},
|
|
],
|
|
compatibility: {
|
|
compatible: true,
|
|
tested: true,
|
|
status: 'compatible' as const,
|
|
message: '',
|
|
current_sub2api_version: '0.1.0',
|
|
required_sub2api_version: '>=0.1.0',
|
|
recommended_sub2api_version: '0.1.0',
|
|
plugin_protocol: 1,
|
|
transport_api: 1,
|
|
ui_bridge: 1,
|
|
},
|
|
runtime_healthy: false,
|
|
runtime_message: '',
|
|
}
|
|
|
|
function mountView() {
|
|
return mount(PluginsView, {
|
|
global: {
|
|
stubs: {
|
|
AppLayout: { template: '<div><slot /></div>' },
|
|
BaseDialog: { template: '<div><slot /></div>' },
|
|
Icon: true,
|
|
TotpStepUpDialog: true,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
describe('管理员插件页二次验证', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
stepUpRun.mockImplementation((action: () => Promise<unknown>) => action())
|
|
listPlugins.mockResolvedValue([plugin])
|
|
uploadPlugin.mockResolvedValue(plugin)
|
|
enablePlugin.mockResolvedValue(plugin)
|
|
savePluginConfig.mockResolvedValue({ enabled: true })
|
|
createUISession.mockResolvedValue({
|
|
url: '/api/v1/plugin-ui/token/index.html#bridge_token=bridge',
|
|
bridge_token: 'bridge',
|
|
ui_bridge_version: 1,
|
|
expires_at: '2026-08-22T01:00:00Z',
|
|
})
|
|
})
|
|
|
|
it('启用插件通过 step-up 控制器执行', async () => {
|
|
const wrapper = mountView()
|
|
await flushPromises()
|
|
|
|
const button = wrapper.findAll('button').find((item) => item.text().includes('admin.plugins.enable'))
|
|
expect(button).toBeDefined()
|
|
await button!.trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(stepUpRun).toHaveBeenCalledTimes(1)
|
|
expect(enablePlugin).toHaveBeenCalledWith(7, 100, false)
|
|
})
|
|
|
|
it.each([7, 8])('keeps the latest session when reopening configuration for plugin %s', async (id) => {
|
|
let resolveOld!: (session: PluginUISession) => void
|
|
createUISession.mockReturnValueOnce(new Promise<PluginUISession>((resolve) => { resolveOld = resolve }))
|
|
listPlugins.mockResolvedValue([plugin, { ...plugin, id: 8, name: 'Other Transport' }])
|
|
const wrapper = mountView()
|
|
await flushPromises()
|
|
const buttons = wrapper.findAll('button').filter(item => item.text() === 'admin.plugins.configure')
|
|
await buttons[0].trigger('click')
|
|
wrapper.getComponent(BaseDialog).vm.$emit('close')
|
|
await buttons[id === 7 ? 0 : 1].trigger('click')
|
|
await flushPromises()
|
|
const currentURL = wrapper.get('iframe').attributes('src')
|
|
|
|
resolveOld({ url: '/obsolete-session', bridge_token: 'old', ui_bridge_version: 1, expires_at: '' })
|
|
await flushPromises()
|
|
expect(wrapper.get('iframe').attributes('src')).toBe(currentURL)
|
|
})
|
|
|
|
it('ignores an old session error while the reopened configuration is loading', async () => {
|
|
let rejectOld!: (error: Error) => void
|
|
let resolveCurrent!: (session: PluginUISession) => void
|
|
createUISession
|
|
.mockReturnValueOnce(new Promise((_, reject) => { rejectOld = reject }))
|
|
.mockReturnValueOnce(new Promise<PluginUISession>((resolve) => { resolveCurrent = resolve }))
|
|
const wrapper = mountView()
|
|
await flushPromises()
|
|
const button = wrapper.findAll('button').find(item => item.text() === 'admin.plugins.configure')!
|
|
await button.trigger('click')
|
|
wrapper.getComponent(BaseDialog).vm.$emit('close')
|
|
await button.trigger('click')
|
|
rejectOld(new Error('obsolete failure'))
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).not.toContain('obsolete failure')
|
|
expect(wrapper.text()).toContain('admin.plugins.loadingUI')
|
|
resolveCurrent({ url: '/current-session', bridge_token: 'current', ui_bridge_version: 1, expires_at: '' })
|
|
await flushPromises()
|
|
await wrapper.get('iframe').trigger('load')
|
|
expect(wrapper.get('iframe').attributes('src')).toBe('/current-session')
|
|
expect(wrapper.text()).not.toContain('admin.plugins.loadingUI')
|
|
})
|
|
|
|
it('does not restore a session after the configuration dialog closes', async () => {
|
|
let resolveSession!: (session: PluginUISession) => void
|
|
createUISession.mockReturnValueOnce(new Promise<PluginUISession>((resolve) => { resolveSession = resolve }))
|
|
const wrapper = mountView()
|
|
await flushPromises()
|
|
await wrapper.findAll('button').find(item => item.text() === 'admin.plugins.configure')!.trigger('click')
|
|
wrapper.getComponent(BaseDialog).vm.$emit('close')
|
|
resolveSession({ url: '/closed-session', bridge_token: 'old', ui_bridge_version: 1, expires_at: '' })
|
|
await flushPromises()
|
|
expect(wrapper.find('iframe').exists()).toBe(false)
|
|
})
|
|
|
|
it('上传插件通过 step-up 控制器执行', async () => {
|
|
const wrapper = mountView()
|
|
await flushPromises()
|
|
const input = wrapper.get('input[type="file"]')
|
|
Object.defineProperty(input.element, 'files', {
|
|
configurable: true,
|
|
value: [new File(['plugin'], 'transport.s2plugin', { type: 'application/zip' })],
|
|
})
|
|
|
|
await input.trigger('change')
|
|
await flushPromises()
|
|
|
|
expect(stepUpRun).toHaveBeenCalledTimes(1)
|
|
expect(uploadPlugin).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|