feat(auth): 后台登录、角色权限与会话访问策略
管理员与客服分角色登录,权限由后端判定;前端补齐登录、人员与组织管理页面。
This commit is contained in:
1 parent
5e0615f36d
commit
e24d3e2a65
123 files changed
+25293
No files matched your search
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright 2024-2026 ChatterMate
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const updateOrganization = vi.fn()
|
||||
|
||||
vi.mock('@/composables/useAuth', () => ({
|
||||
useAuth: () => ({ user: ref({ organization_id: 'org-1' }) }),
|
||||
}))
|
||||
|
||||
vi.mock('@/services/organization', () => ({
|
||||
organizationService: {
|
||||
getOrganization: vi.fn(),
|
||||
getOrganizationStats: vi.fn(),
|
||||
updateOrganization: (...args: unknown[]) => updateOrganization(...args),
|
||||
},
|
||||
}))
|
||||
|
||||
const { useOrganizationSettings } = await import('@/composables/useOrganizationSettings')
|
||||
|
||||
describe('useOrganizationSettings – save errors', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('shows the API detail when the domain is already taken', async () => {
|
||||
updateOrganization.mockRejectedValueOnce({
|
||||
message: 'Request failed with status code 409',
|
||||
response: { status: 409, data: { detail: 'That domain is already used by another organization.' } },
|
||||
})
|
||||
const settings = useOrganizationSettings()
|
||||
settings.formData.value.domain = 'localhost'
|
||||
|
||||
await settings.updateOrganization()
|
||||
|
||||
expect(updateOrganization).toHaveBeenCalledWith('org-1', expect.objectContaining({ domain: 'localhost' }))
|
||||
expect(settings.error.value).toBe('That domain is already used by another organization.')
|
||||
expect(settings.message.value).toBe('')
|
||||
expect(settings.hasChanges.value).toBe(true)
|
||||
})
|
||||
|
||||
it('falls back to a generic message when there is no API detail', async () => {
|
||||
updateOrganization.mockRejectedValueOnce({})
|
||||
const settings = useOrganizationSettings()
|
||||
settings.formData.value.name = 'Renamed'
|
||||
|
||||
await settings.updateOrganization()
|
||||
|
||||
expect(settings.error.value).toBe('更新组织信息失败')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
Copyright 2024-2026 ChatterMate
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import type { User } from '@/types/user'
|
||||
|
||||
const resetUserPassword = vi.fn()
|
||||
const toastSuccess = vi.fn()
|
||||
const toastError = vi.fn()
|
||||
|
||||
vi.mock('@/services/users', () => ({
|
||||
listUsers: vi.fn(),
|
||||
createUser: vi.fn(),
|
||||
updateUser: vi.fn(),
|
||||
deleteUser: vi.fn(),
|
||||
resetUserPassword: (...args: unknown[]) => resetUserPassword(...args),
|
||||
}))
|
||||
|
||||
vi.mock('vue-sonner', () => ({
|
||||
toast: {
|
||||
success: (...args: unknown[]) => toastSuccess(...args),
|
||||
error: (...args: unknown[]) => toastError(...args),
|
||||
},
|
||||
}))
|
||||
|
||||
const { useUsers } = await import('@/composables/useUsers')
|
||||
|
||||
const agent = { id: 'agent-1', full_name: 'Agent One', email: 'agent@test.com' } as User
|
||||
|
||||
describe('useUsers – admin password reset', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('opens the modal for the selected user', () => {
|
||||
const users = useUsers()
|
||||
users.handleResetPassword(agent)
|
||||
|
||||
expect(users.showResetPasswordModal.value).toBe(true)
|
||||
expect(users.selectedUser.value).toEqual(agent)
|
||||
})
|
||||
|
||||
it('resets the password and closes the modal', async () => {
|
||||
resetUserPassword.mockResolvedValueOnce(undefined)
|
||||
const users = useUsers()
|
||||
users.handleResetPassword(agent)
|
||||
|
||||
await users.confirmResetPassword('NewPassw0rd!')
|
||||
|
||||
expect(resetUserPassword).toHaveBeenCalledWith('agent-1', 'NewPassw0rd!')
|
||||
expect(users.showResetPasswordModal.value).toBe(false)
|
||||
expect(users.selectedUser.value).toBeNull()
|
||||
expect(toastSuccess).toHaveBeenCalled()
|
||||
expect(users.resettingPassword.value).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the modal open and surfaces the API message on failure', async () => {
|
||||
resetUserPassword.mockRejectedValueOnce({
|
||||
response: { data: { detail: [{ msg: 'Password must be at least 8 characters long' }] } },
|
||||
})
|
||||
const users = useUsers()
|
||||
users.handleResetPassword(agent)
|
||||
|
||||
await users.confirmResetPassword('short')
|
||||
|
||||
expect(users.showResetPasswordModal.value).toBe(true)
|
||||
expect(toastError).toHaveBeenCalledWith(
|
||||
'出错了',
|
||||
expect.objectContaining({
|
||||
description: '重置密码失败:Password must be at least 8 characters long',
|
||||
}),
|
||||
)
|
||||
expect(users.resettingPassword.value).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
Copyright 2024-2026 ChatterMate
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount, VueWrapper, flushPromises } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import { createRouter, createWebHistory, Router } from 'vue-router'
|
||||
import LoginView from '../../views/LoginView.vue'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
|
||||
// Mock the services
|
||||
vi.mock('@/services/auth', () => ({
|
||||
authService: {
|
||||
login: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
// The landing route is resolved from the real permission checks reading the
|
||||
// cached user, so this drives the user rather than mocking the checks away.
|
||||
const currentUser = vi.hoisted(() => ({ value: null as unknown }))
|
||||
|
||||
vi.mock('@/services/user', () => ({
|
||||
userService: {
|
||||
getCurrentUser: () => currentUser.value,
|
||||
setCurrentUser: vi.fn(),
|
||||
getUserId: () => 'user-1',
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock enterprise features
|
||||
vi.mock('@/composables/useEnterpriseFeatures', () => ({
|
||||
useEnterpriseFeatures: vi.fn(() => ({
|
||||
hasEnterpriseModule: false,
|
||||
loadModule: vi.fn().mockResolvedValue(null)
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock forgot password composable
|
||||
vi.mock('@/composables/useForgotPassword', () => ({
|
||||
useForgotPassword: vi.fn(() => ({
|
||||
isLoading: { value: false },
|
||||
error: { value: '' },
|
||||
success: { value: '' },
|
||||
currentStep: { value: 1 },
|
||||
email: { value: '' },
|
||||
otp: { value: '' },
|
||||
newPassword: { value: '' },
|
||||
confirmPassword: { value: '' },
|
||||
passwordValidation: { value: { score: 0, hasMinLength: false, hasUpperCase: false, hasLowerCase: false, hasNumber: false, hasSpecialChar: false } },
|
||||
requestPasswordReset: vi.fn(),
|
||||
verifyAndResetPassword: vi.fn(),
|
||||
resetForm: vi.fn(),
|
||||
goBackToEmailStep: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock Firebase services
|
||||
vi.mock('@/services/firebase', () => ({
|
||||
messaging: {},
|
||||
requestNotificationPermission: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useNotifications', () => ({
|
||||
useNotifications: vi.fn(() => ({
|
||||
requestPermission: vi.fn(),
|
||||
hasPermission: { value: false }
|
||||
}))
|
||||
}))
|
||||
|
||||
// Import the mocked modules
|
||||
import { authService } from '@/services/auth'
|
||||
import { userWithPermissions } from '../fixtures/permissions'
|
||||
|
||||
describe('LoginView', () => {
|
||||
let wrapper: VueWrapper
|
||||
let router: Router
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create a fresh router instance for each test
|
||||
router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'home', component: { template: '<div>Home</div>' } },
|
||||
{ path: '/ai-agents', name: 'ai-agents', component: { template: '<div>AI Agents</div>' } },
|
||||
{ path: '/conversations', name: 'conversations', component: { template: '<div>Conversations</div>' } },
|
||||
{ path: '/human-agents', name: 'human-agents', component: { template: '<div>Human Agents</div>' } },
|
||||
{ path: '/settings/organization', name: 'org-settings', component: { template: '<div>Organization Settings</div>' } },
|
||||
{ path: '/settings/ai-config', name: 'ai-config', component: { template: '<div>AI Config</div>' } },
|
||||
{ path: '/settings/user', name: 'user-settings', component: { template: '<div>User Settings</div>' } },
|
||||
{ path: '/403', name: 'forbidden', component: { template: '<div>403</div>' } }
|
||||
]
|
||||
})
|
||||
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Reset router to initial state and wait for it to be ready
|
||||
await router.push('/')
|
||||
await router.isReady()
|
||||
|
||||
// A user with no grants unless a test says otherwise
|
||||
currentUser.value = userWithPermissions([])
|
||||
|
||||
wrapper = mount(LoginView, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
RouterView: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Wait for component to be ready
|
||||
await nextTick()
|
||||
})
|
||||
|
||||
it('renders login form properly', () => {
|
||||
expect(wrapper.find('.auth-page').exists()).toBe(true)
|
||||
expect(wrapper.find('input[type="email"]').exists()).toBe(true)
|
||||
expect(wrapper.find('input[type="password"]').exists()).toBe(true)
|
||||
expect(wrapper.find('button[type="submit"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('updates email and password inputs', async () => {
|
||||
const emailInput = wrapper.find('input[type="email"]')
|
||||
const passwordInput = wrapper.find('input[type="password"]')
|
||||
|
||||
await emailInput.setValue('test@example.com')
|
||||
await passwordInput.setValue('password123')
|
||||
|
||||
expect((emailInput.element as HTMLInputElement).value).toBe('test@example.com')
|
||||
expect((passwordInput.element as HTMLInputElement).value).toBe('password123')
|
||||
})
|
||||
|
||||
it('shows loading state during login', async () => {
|
||||
const mockLogin = vi.fn(() => new Promise(resolve => setTimeout(resolve, 100)))
|
||||
;(authService.login as any) = mockLogin
|
||||
|
||||
await wrapper.find('input[type="email"]').setValue('test@example.com')
|
||||
await wrapper.find('input[type="password"]').setValue('password123')
|
||||
|
||||
const submitButton = wrapper.find('button[type="submit"]')
|
||||
await submitButton.trigger('submit')
|
||||
|
||||
expect(submitButton.text()).toBe('登录中…')
|
||||
expect(submitButton.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('handles successful login and redirects based on permissions', async () => {
|
||||
// Mock successful login
|
||||
;(authService.login as any).mockResolvedValue({ id: 1, email: 'test@example.com' })
|
||||
|
||||
// User can manage agents
|
||||
currentUser.value = userWithPermissions(['manage_agents'])
|
||||
|
||||
await wrapper.find('input[type="email"]').setValue('test@example.com')
|
||||
await wrapper.find('input[type="password"]').setValue('password123')
|
||||
await wrapper.find('form').trigger('submit')
|
||||
|
||||
// Verify login was called
|
||||
expect(authService.login).toHaveBeenCalledWith('test@example.com', 'password123')
|
||||
|
||||
// Wait for all promises to resolve
|
||||
await flushPromises()
|
||||
|
||||
expect(router.currentRoute.value.path).toBe('/ai-agents')
|
||||
})
|
||||
|
||||
it('shows the backend message authService.login rethrows', async () => {
|
||||
// What the real service throws: a plain Error carrying the API's detail.
|
||||
;(authService.login as any).mockRejectedValue(new Error('Incorrect email or password'))
|
||||
await wrapper.find('input[type="email"]').setValue('someone@example.com')
|
||||
await wrapper.find('input[type="password"]').setValue('wrong')
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('.auth-error').text()).toBe('Incorrect email or password')
|
||||
})
|
||||
|
||||
it('handles login error', async () => {
|
||||
const errorMessage = 'Invalid credentials'
|
||||
;(authService.login as any).mockRejectedValue({
|
||||
response: {
|
||||
data: {
|
||||
detail: errorMessage
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('input[type="email"]').setValue('test@example.com')
|
||||
await wrapper.find('input[type="password"]').setValue('wrong-password')
|
||||
await wrapper.find('form').trigger('submit')
|
||||
|
||||
// Wait for error to be displayed
|
||||
await flushPromises()
|
||||
expect(wrapper.find('.auth-error').text()).toBe(errorMessage)
|
||||
})
|
||||
|
||||
it('redirects to correct route based on permissions', async () => {
|
||||
;(authService.login as any).mockResolvedValue({ id: 1, email: 'test@example.com' })
|
||||
|
||||
const testCases = [
|
||||
{ permission: 'manage_agents', route: '/ai-agents' },
|
||||
{ permission: 'view_assigned_chats', route: '/conversations' },
|
||||
{ permission: 'manage_users', route: '/human-agents' },
|
||||
{ permission: 'view_organization', route: '/settings/organization' },
|
||||
{ permission: 'view_ai_config', route: '/settings/ai-config' },
|
||||
]
|
||||
|
||||
for (const testCase of testCases) {
|
||||
currentUser.value = userWithPermissions([testCase.permission])
|
||||
|
||||
// Reset router and wait for it to be ready
|
||||
await router.push('/')
|
||||
await router.isReady()
|
||||
await nextTick()
|
||||
|
||||
await wrapper.find('input[type="email"]').setValue('test@example.com')
|
||||
await wrapper.find('input[type="password"]').setValue('password123')
|
||||
await wrapper.find('form').trigger('submit')
|
||||
|
||||
// Wait for all promises to resolve
|
||||
await flushPromises()
|
||||
|
||||
expect(router.currentRoute.value.path).toBe(testCase.route)
|
||||
}
|
||||
})
|
||||
|
||||
// Used to land on /403 — which was not a registered route, so the user
|
||||
// silently ended up on /ai-agents, a page they had just been refused.
|
||||
it('lands a permissionless user on their own settings, never on 403', async () => {
|
||||
;(authService.login as any).mockResolvedValue({ id: 1, email: 'test@example.com' })
|
||||
currentUser.value = userWithPermissions([])
|
||||
|
||||
await wrapper.find('input[type="email"]').setValue('test@example.com')
|
||||
await wrapper.find('input[type="password"]').setValue('password123')
|
||||
await wrapper.find('form').trigger('submit')
|
||||
|
||||
// Wait for all promises to resolve
|
||||
await flushPromises()
|
||||
|
||||
expect(router.currentRoute.value.path).toBe('/settings/user')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
Copyright 2024-2026 ChatterMate
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount, VueWrapper } from '@vue/test-utils'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import SetupView from '../../views/SetupView.vue'
|
||||
import { nextTick } from 'vue'
|
||||
import { flushPromises } from '@vue/test-utils'
|
||||
|
||||
// Mock timezone-select-js
|
||||
vi.mock('timezone-select-js', () => ({
|
||||
listTz: () => [
|
||||
{ value: 'America/New_York', label: 'New York' },
|
||||
{ value: 'Europe/London', label: 'London' },
|
||||
{ value: 'Asia/Tokyo', label: 'Tokyo' }
|
||||
],
|
||||
clientTz: () => 'America/New_York'
|
||||
}))
|
||||
|
||||
// Mock the organization service
|
||||
vi.mock('@/services/organization', () => ({
|
||||
createOrganization: vi.fn(),
|
||||
getSetupStatus: vi.fn().mockResolvedValue(false)
|
||||
}))
|
||||
|
||||
// Mock Firebase services
|
||||
vi.mock('@/services/firebase', () => ({
|
||||
messaging: {},
|
||||
requestNotificationPermission: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useNotifications', () => ({
|
||||
useNotifications: vi.fn(() => ({
|
||||
requestPermission: vi.fn(),
|
||||
hasPermission: { value: false }
|
||||
}))
|
||||
}))
|
||||
|
||||
// Import mocked services
|
||||
import { createOrganization, getSetupStatus } from '@/services/organization'
|
||||
|
||||
describe('SetupView', () => {
|
||||
let wrapper: VueWrapper
|
||||
let router: any
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create a fresh pinia instance
|
||||
setActivePinia(createPinia())
|
||||
|
||||
// Create router instance
|
||||
router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'home', component: { template: '<div>Home</div>' } },
|
||||
{ path: '/ai-agents', name: 'ai-agents', component: { template: '<div>AI Agents</div>' } }
|
||||
]
|
||||
})
|
||||
|
||||
// Reset mocks
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Initialize router
|
||||
await router.push('/')
|
||||
await router.isReady()
|
||||
|
||||
wrapper = mount(SetupView, {
|
||||
global: {
|
||||
plugins: [router]
|
||||
}
|
||||
})
|
||||
|
||||
// Wait for initial organization check
|
||||
await nextTick()
|
||||
})
|
||||
|
||||
it('renders properly', () => {
|
||||
expect(wrapper.find('.setup').exists()).toBe(true)
|
||||
expect(wrapper.find('h1').text()).toBe('欢迎使用 ChatterMate')
|
||||
})
|
||||
|
||||
it('checks for existing organization on mount', async () => {
|
||||
expect(getSetupStatus).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('redirects to ai-agents if organization exists', async () => {
|
||||
// Mock organization exists
|
||||
vi.mocked(getSetupStatus).mockResolvedValueOnce(true)
|
||||
|
||||
// Remount component
|
||||
wrapper = mount(SetupView, {
|
||||
global: {
|
||||
plugins: [router]
|
||||
}
|
||||
})
|
||||
|
||||
// Wait for all promises to resolve and navigation to complete
|
||||
await flushPromises()
|
||||
await router.isReady()
|
||||
|
||||
expect(router.currentRoute.value.path).toBe('/login')
|
||||
expect(getSetupStatus).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('displays form fields with default values', () => {
|
||||
expect(wrapper.find('#orgName').exists()).toBe(true)
|
||||
expect(wrapper.find('#domain').exists()).toBe(true)
|
||||
expect(wrapper.find('#timezone').exists()).toBe(true)
|
||||
expect(wrapper.find('#adminName').exists()).toBe(true)
|
||||
expect(wrapper.find('#adminEmail').exists()).toBe(true)
|
||||
expect(wrapper.find('#adminPassword').exists()).toBe(true)
|
||||
expect(wrapper.find('#confirmPassword').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('validates organization name', async () => {
|
||||
const orgNameInput = wrapper.find('#orgName')
|
||||
|
||||
// Test invalid name
|
||||
await orgNameInput.setValue('a')
|
||||
await orgNameInput.trigger('input')
|
||||
expect(wrapper.find('.error-hint').text()).toContain('组织名称需为')
|
||||
|
||||
// Test valid name
|
||||
await orgNameInput.setValue('Valid Organization')
|
||||
await orgNameInput.trigger('input')
|
||||
expect(wrapper.find('.error-hint').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('validates domain', async () => {
|
||||
const domainInput = wrapper.find('#domain')
|
||||
|
||||
// Test invalid domain
|
||||
await domainInput.setValue('invalid')
|
||||
await domainInput.trigger('input')
|
||||
expect(wrapper.find('.error-hint').text()).toContain('请输入有效的域名')
|
||||
|
||||
// Test valid domain
|
||||
await domainInput.setValue('example.com')
|
||||
await domainInput.trigger('input')
|
||||
expect(wrapper.find('.error-hint').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('validates admin email', async () => {
|
||||
const emailInput = wrapper.find('#adminEmail')
|
||||
|
||||
// Test invalid email
|
||||
await emailInput.setValue('invalid-email')
|
||||
await emailInput.trigger('input')
|
||||
expect(wrapper.find('.error-hint').text()).toContain('请输入有效的邮箱地址')
|
||||
|
||||
// Test valid email
|
||||
await emailInput.setValue('admin@example.com')
|
||||
await emailInput.trigger('input')
|
||||
expect(wrapper.find('.error-hint').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('validates password strength', async () => {
|
||||
const passwordInput = wrapper.find('#adminPassword')
|
||||
|
||||
// Test weak password
|
||||
await passwordInput.setValue('weak')
|
||||
await passwordInput.trigger('input')
|
||||
expect(wrapper.find('.strength-bar').classes()).toContain('weak')
|
||||
|
||||
// Test strong password
|
||||
await passwordInput.setValue('StrongP@ss123')
|
||||
await passwordInput.trigger('input')
|
||||
expect(wrapper.find('.strength-bar').classes()).toContain('strong')
|
||||
})
|
||||
|
||||
it('validates password confirmation', async () => {
|
||||
// Fill in all required fields with valid data first
|
||||
await wrapper.find('#orgName').setValue('Test Organization')
|
||||
await wrapper.find('#domain').setValue('test.com')
|
||||
await wrapper.find('#adminName').setValue('Test Admin')
|
||||
await wrapper.find('#adminEmail').setValue('admin@test.com')
|
||||
|
||||
// Set different passwords
|
||||
const passwordInput = wrapper.find('#adminPassword')
|
||||
const confirmInput = wrapper.find('#confirmPassword')
|
||||
|
||||
await passwordInput.setValue('StrongP@ss123')
|
||||
await passwordInput.trigger('input')
|
||||
await confirmInput.setValue('DifferentP@ss123')
|
||||
|
||||
// Submit form
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.error-message').text()).toBe('两次输入的密码不一致')
|
||||
})
|
||||
|
||||
it('handles timezone selection', async () => {
|
||||
const timezoneSelect = wrapper.find('#timezone')
|
||||
await timezoneSelect.setValue('Europe/London')
|
||||
|
||||
expect((wrapper.vm as any).selectedTimezone).toBe('Europe/London')
|
||||
expect((wrapper.vm as any).orgData.timezone).toBe('Europe/London')
|
||||
})
|
||||
|
||||
it('handles business hours configuration', async () => {
|
||||
// Toggle Saturday to enabled
|
||||
const saturdayToggle = wrapper.findAll('.toggle input')[5]
|
||||
await saturdayToggle.setValue(true)
|
||||
|
||||
// Change Saturday hours
|
||||
const timeSelects = wrapper.findAll('.time-selects select')
|
||||
await timeSelects[10].setValue('10:00') // Start time
|
||||
await timeSelects[11].setValue('18:00') // End time
|
||||
|
||||
const businessHours = (wrapper.vm as any).orgData.business_hours
|
||||
expect(businessHours.saturday.enabled).toBe(true)
|
||||
expect(businessHours.saturday.start).toBe('10:00')
|
||||
expect(businessHours.saturday.end).toBe('18:00')
|
||||
})
|
||||
|
||||
it('submits form with valid data', async () => {
|
||||
// Fill in all required fields with valid data
|
||||
await wrapper.find('#orgName').setValue('Test Organization')
|
||||
await wrapper.find('#domain').setValue('test.com')
|
||||
await wrapper.find('#adminName').setValue('Test Admin')
|
||||
await wrapper.find('#adminEmail').setValue('admin@test.com')
|
||||
await wrapper.find('#adminPassword').setValue('StrongP@ss123')
|
||||
await wrapper.find('#confirmPassword').setValue('StrongP@ss123')
|
||||
|
||||
// Submit form
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await nextTick()
|
||||
|
||||
// Verify organization creation was called
|
||||
expect(createOrganization).toHaveBeenCalledWith(expect.objectContaining({
|
||||
name: 'Test Organization',
|
||||
domain: 'test.com',
|
||||
admin_name: 'Test Admin',
|
||||
admin_email: 'admin@test.com',
|
||||
admin_password: 'StrongP@ss123'
|
||||
}))
|
||||
})
|
||||
|
||||
it('handles organization creation error', async () => {
|
||||
const error = 'Failed to create organization'
|
||||
;(createOrganization as any).mockRejectedValueOnce(new Error(error))
|
||||
|
||||
// Fill in required fields
|
||||
await wrapper.find('#orgName').setValue('Test Organization')
|
||||
await wrapper.find('#domain').setValue('test.com')
|
||||
await wrapper.find('#adminName').setValue('Test Admin')
|
||||
await wrapper.find('#adminEmail').setValue('admin@test.com')
|
||||
await wrapper.find('#adminPassword').setValue('StrongP@ss123')
|
||||
await wrapper.find('#confirmPassword').setValue('StrongP@ss123')
|
||||
|
||||
// Submit form
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.error-message').text()).toBe(error)
|
||||
})
|
||||
|
||||
it('shows loading state during submission', async () => {
|
||||
// Mock slow organization creation
|
||||
;(createOrganization as any).mockImplementationOnce(() => new Promise(resolve => setTimeout(resolve, 100)))
|
||||
|
||||
// Fill in required fields
|
||||
await wrapper.find('#orgName').setValue('Test Organization')
|
||||
await wrapper.find('#domain').setValue('test.com')
|
||||
await wrapper.find('#adminName').setValue('Test Admin')
|
||||
await wrapper.find('#adminEmail').setValue('admin@test.com')
|
||||
await wrapper.find('#adminPassword').setValue('StrongP@ss123')
|
||||
await wrapper.find('#confirmPassword').setValue('StrongP@ss123')
|
||||
|
||||
// Submit form
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('button[type="submit"]').classes()).toContain('loading')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user