Files
admin-web/src/modules/iam/pages/InternalUserDialogs.test.ts
T

185 lines
14 KiB
TypeScript

import { createRequire } from 'node:module'
import { createPinia, setActivePinia } from 'pinia'
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useAuthStore } from '@/core/auth/auth'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import AppDialog from '@/shared/components/AppDialog.vue'
import InternalUsersView from './InternalUsersView.vue'
import { ApiResponseError } from '@/shared/api/http'
import * as api from '../api/platformIam'
import { listAllPlatformWarehouses } from '@/modules/warehouses'
const { default: ElementPlus, ElDialog, ElSelect, ElCheckbox, ElMessageBox } = createRequire(import.meta.url)('element-plus') as typeof import('element-plus')
vi.mock('../api/platformIam', () => ({ listPlatformManagedUsers: vi.fn(), getPlatformUserOptions: vi.fn(), listSharedReceivingOverseasWarehouseOptions: vi.fn(), createPlatformManagedUser: vi.fn(), updatePlatformManagedUser: vi.fn(), changePlatformManagedUserStatus: vi.fn(), resetPlatformManagedUserPassword: vi.fn() }))
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn() }))
const memberId = '9223372036854775807', warehouseId = '9223372036854775806'
let record: api.PlatformManagedUser
const table = { props: ['data'], template: '<div><slot name="toolbar-actions"/><div class="real-row"><slot v-if="data?.length" name="actions" :row="data[0]"/></div></div>' }
function button(label: string, selector = 'button') {
const found = Array.from(document.querySelectorAll<HTMLButtonElement>(selector)).find(node => node.textContent?.trim() === label)
expect(found, label).toBeDefined(); return found!
}
async function view() {
const wrapper = mount(InternalUsersView, { attachTo: document.body, global: { plugins: [ElementPlus], stubs: { DataTable: table, QueryToolbar: true } } }); await flushPromises(); return wrapper
}
async function editor(create = false) {
const wrapper = await view(); button(create ? '新增用户' : '编辑').click(); await flushPromises(); return wrapper
}
async function text(placeholder: string, value: string) {
const input = document.querySelector<HTMLInputElement>(`.el-dialog [placeholder="${placeholder}"]`)!
input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); await flushPromises()
}
function select(wrapper: Awaited<ReturnType<typeof view>>, placeholder: string) {
const found = wrapper.findAllComponents(ElSelect).find(node => node.props('placeholder') === placeholder)
expect(found, placeholder).toBeDefined(); return found!
}
async function fillCreate(wrapper: Awaited<ReturnType<typeof view>>, roles = ['ROLE-OP']) {
await text('登录用户名', 'new_operator'); await text('真实姓名', '新操作员'); await text('手机号码', '13800000000'); await text('邮箱地址', 'worker@example.test')
select(wrapper, '选择角色').vm.$emit('update:modelValue', roles)
await text('8-64 位,包含字母和数字', 'TestPassword123'); await text('再次输入密码', 'TestPassword123'); await flushPromises()
}
function permissions(values: string[]) { useAuthStore().permissions = ['platform:user:list', ...values]; useAccessPreviewStore().initializeAfterLogin() }
enableAutoUnmount(afterEach)
afterEach(() => ElMessageBox.close())
beforeEach(() => {
vi.resetAllMocks(); document.body.innerHTML = ''; setActivePinia(createPinia())
useAuthStore().token = 'test-session'
permissions(['platform:user:create','platform:user:edit','platform:user:status','platform:user:reset-password','platform:user:grant-role'])
record = { membershipId: memberId, userId: '9223372036854775805', authUserId: '9223372036854775804', username: 'test_operator', displayName: '测试操作员', phone: '13800000000', email: 'worker@example.test', tenantId: '0', tenantName: '平台', memberType: 'INTERNAL', memberNo: 'EMP-TEST', status: 'ACTIVE', roleIds: ['ROLE-OP'], roleNames: ['运营角色'], lastLoginAt: '2026-10-20T10:30:00', protectedAccount: false, overseasWarehouseId: null }
vi.mocked(api.listPlatformManagedUsers).mockImplementation(async () => ({ records: [record], total: 1, page: 1, size: 10, pages: 1 }))
vi.mocked(api.getPlatformUserOptions).mockResolvedValue({ tenants: [{ id: '0', name: '平台', status: 'ACTIVE' }], roles: [
{ id: 'ROLE-OP', tenantId: '0', roleCode: 'PLATFORM_OPERATOR', name: '运营角色', status: 'ACTIVE' },
{ id: 'ROLE-OVER', tenantId: '0', roleCode: 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', name: '海外PDA角色', status: 'ACTIVE' },
{ id: 'ROLE-DOM', tenantId: '0', roleCode: 'CN_FIRSTLEG_PDA_OPERATOR', name: '国内PDA角色', status: 'ACTIVE' },
] })
vi.mocked(api.listSharedReceivingOverseasWarehouseOptions).mockResolvedValue([{ id: warehouseId, warehouseCode: 'US-TEST', warehouseName: '海外收货仓', countryCode: 'US' }])
vi.mocked(listAllPlatformWarehouses).mockResolvedValue([{ id: warehouseId, tenantId: '0', warehouseCode: 'CN-TEST', internalCode: null, warehouseName: '国内作业仓', warehouseType: 'DOMESTIC', countryCode: 'CN', timezone: 'Asia/Shanghai', contactName: null, contactPhone: null, contactEmail: null, status: 'ACTIVE', remark: null }])
vi.mocked(api.createPlatformManagedUser).mockImplementation(async () => record)
vi.mocked(api.updatePlatformManagedUser).mockImplementation(async () => record)
vi.mocked(api.changePlatformManagedUserStatus).mockImplementation(async (_id,status) => ({ ...record, status }))
vi.mocked(api.resetPlatformManagedUserPassword).mockResolvedValue(undefined)
})
describe('internal user unified dialogs', () => {
it('creates a platform account and preserves draft through native fullscreen', async () => {
const wrapper = await editor(true); await fillCreate(wrapper)
const dialog = wrapper.findComponent(AppDialog)
expect(dialog.props()).toMatchObject({ width: 'min(650px, calc(100vw - 28px))', fullscreenable: true, title: '新增用户' })
document.querySelector<HTMLButtonElement>('[aria-label="展开全屏"]')!.click(); await flushPromises()
expect(dialog.findComponent(ElDialog).props('fullscreen')).toBe(true)
expect(document.querySelector<HTMLInputElement>('[placeholder="登录用户名"]')!.value).toBe('new_operator')
button('确认添加', '.el-dialog__footer button').click(); await flushPromises()
expect(api.createPlatformManagedUser).toHaveBeenCalledWith({ username: 'new_operator', displayName: '新操作员', phone: '13800000000', email: 'worker@example.test', tenantId: '0', roleIds: ['ROLE-OP'], overseasWarehouseId: null, password: 'TestPassword123', status: 'ACTIVE' })
})
it('enforces required fields and matching initial passwords before creating', async () => {
const wrapper = await editor(true)
button('确认添加', '.el-dialog__footer button').click(); await flushPromises()
expect(api.createPlatformManagedUser).not.toHaveBeenCalled()
await vi.waitFor(() => expect(document.querySelector('.el-dialog')?.textContent).toContain('请输入用户名'))
await fillCreate(wrapper); await text('再次输入密码', 'Mismatch123')
button('确认添加', '.el-dialog__footer button').click(); await flushPromises()
expect(api.createPlatformManagedUser).not.toHaveBeenCalled()
await vi.waitFor(() => expect(document.querySelector('.el-dialog')?.textContent).toContain('两次输入的密码不一致'))
})
it('edits the same membership with readonly login name and timestamp', async () => {
const wrapper = await editor()
expect(wrapper.findComponent(AppDialog).props('title')).toBe('编辑用户 · 测试操作员')
expect(document.querySelector('.el-dialog [placeholder="登录用户名"]')).toBeNull()
expect(document.querySelector('.el-dialog')?.textContent).toContain('test_operator')
expect(document.querySelector('.el-dialog')?.textContent).toContain('2026-10-20 10:30')
expect(document.querySelector('.el-dialog')?.textContent).not.toContain(memberId)
await text('真实姓名', '更名操作员')
button('保存修改', '.el-dialog__footer button').click(); await flushPromises()
expect(api.updatePlatformManagedUser).toHaveBeenCalledWith(memberId, { displayName: '更名操作员', phone: record.phone, email: record.email, tenantId: '0', roleIds: ['ROLE-OP'], overseasWarehouseId: null })
expect(api.resetPlatformManagedUserPassword).not.toHaveBeenCalled()
})
it.each(['ROLE-OVER','ROLE-DOM'] as const)('requires and submits the original %s long warehouse binding', async role => {
const wrapper = await editor(true); await fillCreate(wrapper, [role])
button('确认添加', '.el-dialog__footer button').click(); await flushPromises()
expect(api.createPlatformManagedUser).not.toHaveBeenCalled()
const placeholder = role === 'ROLE-OVER' ? '请选择平台共享海外收货仓' : '请选择国内作业仓'
select(wrapper, placeholder).vm.$emit('update:modelValue', warehouseId); await flushPromises()
button('确认添加', '.el-dialog__footer button').click(); await flushPromises()
expect(api.createPlatformManagedUser).toHaveBeenCalledWith(expect.objectContaining(role === 'ROLE-OVER' ? { overseasWarehouseId: warehouseId } : { overseasWarehouseId: null, domesticWarehouseId: warehouseId }))
if (role === 'ROLE-DOM') expect(listAllPlatformWarehouses).toHaveBeenCalledWith({ warehouseType: 'DOMESTIC', status: 'ACTIVE' })
})
it('recovers a shared warehouse load failure without discarding the user draft', async () => {
vi.mocked(api.listSharedReceivingOverseasWarehouseOptions).mockRejectedValueOnce(new ApiResponseError('目录暂不可用', 'A0500', 500, null, 'req-pda-options'))
const wrapper = await editor(true); await fillCreate(wrapper, ['ROLE-OVER'])
expect(document.querySelector('.el-dialog .load-error-state')?.textContent).toContain('目录暂不可用')
expect(document.querySelector('.el-dialog .load-error-state')?.textContent).toContain('req-pda-options')
button('重新加载', '.el-dialog button').click(); await flushPromises()
expect(api.listSharedReceivingOverseasWarehouseOptions).toHaveBeenCalledTimes(2)
expect(document.querySelector('.el-dialog .load-error-state')).toBeNull()
expect(document.querySelector<HTMLInputElement>('[placeholder="登录用户名"]')!.value).toBe('new_operator')
})
it('keeps protected role and status as readonly facts and disables the row status action', async () => {
record.protectedAccount = true
const wrapper = await editor()
expect(button('禁用', '.real-row button').disabled).toBe(true)
expect(wrapper.findAllComponents(ElSelect)).toHaveLength(0)
expect(document.querySelector('[aria-label="用户基本资料"]')?.textContent).toContain('运营角色')
expect(document.querySelector('[aria-label="用户基本资料"]')?.textContent).toContain('正常')
expect(api.changePlatformManagedUserStatus).not.toHaveBeenCalled()
})
it('allows only the original status command when editing with status permission alone', async () => {
permissions(['platform:user:status'])
const wrapper = await editor()
expect(document.querySelector('.el-dialog [placeholder="真实姓名"]')).toBeNull()
expect(document.querySelector('.el-dialog [placeholder="手机号码"]')).toBeNull()
expect(document.querySelector('[aria-label="用户登录密码"]')).toBeNull()
wrapper.findComponent(ElSelect).vm.$emit('update:modelValue', 'DISABLED'); await flushPromises()
button('保存修改', '.el-dialog__footer button').click(); await flushPromises()
expect(api.changePlatformManagedUserStatus).toHaveBeenCalledWith(memberId, 'DISABLED')
expect(api.updatePlatformManagedUser).not.toHaveBeenCalled(); expect(api.resetPlatformManagedUserPassword).not.toHaveBeenCalled()
})
it('uses the separate reset command without changing profile for reset-only permission', async () => {
permissions(['platform:user:reset-password'])
const wrapper = await editor()
expect(document.querySelector('.el-dialog [placeholder="真实姓名"]')).toBeNull()
await wrapper.findComponent(ElCheckbox).get('input').setValue(true); await flushPromises()
await text('8-64 位,包含字母和数字', 'ResetPassword123'); await text('再次输入密码', 'ResetPassword123')
button('保存修改', '.el-dialog__footer button').click(); await flushPromises()
expect(api.resetPlatformManagedUserPassword).toHaveBeenCalledWith(memberId, 'ResetPassword123')
expect(api.updatePlatformManagedUser).not.toHaveBeenCalled(); expect(api.changePlatformManagedUserStatus).not.toHaveBeenCalled()
})
it('locks all close paths during saving and retains editable values on failure', async () => {
let rejectSave!: (error: Error) => void
vi.mocked(api.updatePlatformManagedUser).mockImplementation(() => new Promise((_resolve,reject) => { rejectSave = reject }))
const wrapper = await editor(); await text('真实姓名', '失败草稿')
button('保存修改', '.el-dialog__footer button').click(); await flushPromises()
expect(wrapper.findComponent(ElDialog).props()).toMatchObject({ showClose: false, closeOnPressEscape: false, closeOnClickModal: false })
expect(button('取消', '.el-dialog__footer button').disabled).toBe(true)
expect(document.querySelector<HTMLInputElement>('[placeholder="真实姓名"]')!.disabled).toBe(true)
rejectSave(new Error('更新失败')); await flushPromises()
expect(wrapper.findComponent(AppDialog).props('modelValue')).toBe(true)
expect(document.querySelector<HTMLInputElement>('[placeholder="真实姓名"]')!.value).toBe('失败草稿')
})
it.each(['ACTIVE','DISABLED'] as const)('identifies %s status confirmation without exposing account IDs', async status => {
record.status = status
const verb = status === 'ACTIVE' ? '禁用' : '启用'
await view(); button(verb, '.real-row button').click(); await flushPromises()
expect(document.querySelector('.el-message-box__title')?.textContent).toBe(`${verb}用户 · 测试操作员`)
expect(document.querySelector('.el-message-box')?.textContent).not.toContain(memberId)
expect(api.changePlatformManagedUserStatus).not.toHaveBeenCalled()
button(`确认${verb}`, '.el-message-box button').click(); await flushPromises()
expect(api.changePlatformManagedUserStatus).toHaveBeenCalledWith(memberId, status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE')
})
it('keeps creation hidden without grant-role permission', async () => {
permissions(['platform:user:create'])
const wrapper = await view()
expect(wrapper.findAll('button').some(node => node.text() === '新增用户')).toBe(false)
expect(api.createPlatformManagedUser).not.toHaveBeenCalled()
})
})