Files
sub2api/frontend/src/__tests__/integration/data-import.spec.ts
T

215 lines
6.2 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import ImportDataModal from '@/components/admin/account/ImportDataModal.vue'
const showError = vi.fn()
const showSuccess = vi.fn()
const showWarning = vi.fn()
vi.mock('@/stores/app', () => ({
useAppStore: () => ({
showError,
showSuccess,
showWarning
})
}))
vi.mock('@/api/admin', () => ({
adminAPI: {
accounts: {
importData: vi.fn()
}
}
}))
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key: string) => key
})
}))
const mountModal = () =>
mount(ImportDataModal, {
props: { show: true },
global: {
stubs: {
BaseDialog: { template: '<div><slot /><slot name="footer" /></div>' }
}
}
})
const makeJsonFile = (name: string, content: string, type = 'application/json') => {
const file = new File([content], name, { type })
Object.defineProperty(file, 'text', {
value: () => Promise.resolve(content)
})
return file
}
const setInputFiles = (element: Element, files: File[]) => {
Object.defineProperty(element, 'files', {
value: files,
configurable: true
})
}
describe('ImportDataModal', () => {
beforeEach(async () => {
showError.mockReset()
showSuccess.mockReset()
showWarning.mockReset()
const { adminAPI } = await import('@/api/admin')
vi.mocked(adminAPI.accounts.importData).mockReset()
})
it('未选择文件时提示错误', async () => {
const wrapper = mountModal()
await wrapper.find('form').trigger('submit')
expect(showError).toHaveBeenCalledWith('admin.accounts.dataImportSelectFile')
})
it('无效 JSON 时按文件名提示解析失败', async () => {
const { adminAPI } = await import('@/api/admin')
const wrapper = mountModal()
const input = wrapper.find('input[type="file"]')
setInputFiles(input.element, [makeJsonFile('data.json', 'invalid json')])
await input.trigger('change')
await wrapper.find('form').trigger('submit')
await flushPromises()
expect(showError).toHaveBeenCalledWith('admin.accounts.dataImportParseFailedFile')
expect(adminAPI.accounts.importData).not.toHaveBeenCalled()
})
it('不是导出数据的 JSON 按文件名拒绝', async () => {
const { adminAPI } = await import('@/api/admin')
const wrapper = mountModal()
const input = wrapper.find('input[type="file"]')
setInputFiles(input.element, [makeJsonFile('random.json', JSON.stringify({ name: 'test' }))])
await input.trigger('change')
await wrapper.find('form').trigger('submit')
await flushPromises()
expect(showError).toHaveBeenCalledWith('admin.accounts.dataImportInvalidFile')
expect(adminAPI.accounts.importData).not.toHaveBeenCalled()
})
it('无有效 JSON 的选择不清空已有选择', async () => {
const { adminAPI } = await import('@/api/admin')
vi.mocked(adminAPI.accounts.importData).mockResolvedValue({
proxy_created: 0,
proxy_reused: 0,
proxy_failed: 0,
account_created: 1,
account_failed: 0
})
const wrapper = mountModal()
const input = wrapper.find('input[type="file"]')
const valid = makeJsonFile(
'valid.json',
JSON.stringify({ exported_at: '2026-07-05T00:00:00Z', proxies: [], accounts: [{ name: 'a' }] })
)
setInputFiles(input.element, [valid])
await input.trigger('change')
setInputFiles(input.element, [new File(['hello'], 'notes.txt', { type: 'text/plain' })])
await input.trigger('change')
expect(showError).toHaveBeenCalledWith('admin.accounts.dataImportSelectFile')
await wrapper.find('form').trigger('submit')
await flushPromises()
expect(adminAPI.accounts.importData).toHaveBeenCalledWith({
data: expect.objectContaining({
accounts: [{ name: 'a' }]
}),
skip_default_group_bind: true
})
})
it('merges multiple selected JSON files before importing', async () => {
const { adminAPI } = await import('@/api/admin')
vi.mocked(adminAPI.accounts.importData).mockResolvedValue({
proxy_created: 0,
proxy_reused: 0,
proxy_failed: 0,
account_created: 2,
account_failed: 0
})
const wrapper = mountModal()
const input = wrapper.find('input[type="file"]')
const first = makeJsonFile(
'first.json',
JSON.stringify({ exported_at: '2026-07-05T00:00:00Z', proxies: [], accounts: [{ name: 'a' }] })
)
const second = makeJsonFile(
'second.json',
JSON.stringify({
exported_at: '2026-07-05T00:00:01Z',
proxies: [{ proxy_key: 'p' }],
accounts: [{ name: 'b' }]
})
)
setInputFiles(input.element, [first, second])
await input.trigger('change')
await wrapper.find('form').trigger('submit')
await flushPromises()
expect(adminAPI.accounts.importData).toHaveBeenCalledWith({
data: expect.objectContaining({
proxies: [{ proxy_key: 'p' }],
accounts: [{ name: 'a' }, { name: 'b' }]
}),
skip_default_group_bind: true
})
expect(showSuccess).toHaveBeenCalledWith('admin.accounts.dataImportSuccess')
})
it('部分成功时关闭弹窗仍通知父组件刷新', async () => {
const { adminAPI } = await import('@/api/admin')
vi.mocked(adminAPI.accounts.importData).mockResolvedValue({
proxy_created: 0,
proxy_reused: 0,
proxy_failed: 0,
account_created: 1,
account_failed: 1
})
const wrapper = mountModal()
const input = wrapper.find('input[type="file"]')
setInputFiles(input.element, [
makeJsonFile(
'mixed.json',
JSON.stringify({
exported_at: '2026-07-05T00:00:00Z',
proxies: [],
accounts: [{ name: 'a' }, { name: 'b' }]
})
)
])
await input.trigger('change')
await wrapper.find('form').trigger('submit')
await flushPromises()
expect(showError).toHaveBeenCalledWith('admin.accounts.dataImportCompletedWithErrors')
expect(wrapper.emitted('imported')).toBeUndefined()
// 第二个 btn-secondary 是 footer 的取消按钮(第一个是选择文件)
await wrapper.findAll('button.btn-secondary')[1]!.trigger('click')
expect(wrapper.emitted('imported')).toHaveLength(1)
expect(wrapper.emitted('close')).toHaveLength(1)
})
})