mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
fix(frontend): support login autofill and reliable redirect
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp, defineComponent, h, nextTick, type App } from 'vue'
|
||||
|
||||
import LoginDialog from '../LoginDialog.vue'
|
||||
|
||||
const authStoreMock = vi.hoisted(() => ({
|
||||
loading: false,
|
||||
error: '',
|
||||
canAccessAdmin: false,
|
||||
login: vi.fn(),
|
||||
}))
|
||||
|
||||
const routerPushMock = vi.hoisted(() => vi.fn())
|
||||
const toastMocks = vi.hoisted(() => ({
|
||||
success: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}))
|
||||
|
||||
const authApiMocks = vi.hoisted(() => ({
|
||||
getRegistrationSettings: vi.fn(),
|
||||
getAuthSettings: vi.fn(),
|
||||
}))
|
||||
|
||||
const oauthApiMocks = vi.hoisted(() => ({
|
||||
getProviders: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({
|
||||
push: routerPushMock,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/auth', () => ({
|
||||
useAuthStore: () => authStoreMock,
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useToast', () => ({
|
||||
useToast: () => toastMocks,
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useSiteInfo', () => ({
|
||||
useSiteInfo: () => ({
|
||||
siteName: 'Aether',
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/config/demo', () => ({
|
||||
isDemoMode: () => false,
|
||||
DEMO_ACCOUNTS: {
|
||||
admin: { email: '[email protected]', password: 'demo123' },
|
||||
user: { email: '[email protected]', password: 'demo123' },
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/api/auth', () => ({
|
||||
authApi: authApiMocks,
|
||||
}))
|
||||
|
||||
vi.mock('@/api/oauth', () => ({
|
||||
oauthApi: oauthApiMocks,
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/deviceId', () => ({
|
||||
getClientDeviceId: () => 'device-123',
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/url', () => ({
|
||||
getApiUrl: (path: string) => path,
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/oauth-icons', () => ({
|
||||
getOAuthIcon: () => '',
|
||||
}))
|
||||
|
||||
vi.mock('../RegisterDialog.vue', () => ({
|
||||
default: defineComponent({
|
||||
name: 'RegisterDialogStub',
|
||||
setup() {
|
||||
return () => null
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
return {
|
||||
Dialog: defineComponent({
|
||||
name: 'DialogStub',
|
||||
props: {
|
||||
modelValue: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { slots }) {
|
||||
return () => props.modelValue ? h('div', { 'data-testid': 'dialog' }, slots.default?.()) : null
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/components/ui/button.vue', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
return {
|
||||
default: defineComponent({
|
||||
name: 'ButtonStub',
|
||||
props: {
|
||||
disabled: { type: Boolean, default: false },
|
||||
type: { type: String, default: 'button' },
|
||||
},
|
||||
setup(props, { attrs, slots }) {
|
||||
return () => h('button', {
|
||||
...attrs,
|
||||
type: props.type,
|
||||
disabled: props.disabled,
|
||||
}, slots.default?.())
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/components/ui/label.vue', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
return {
|
||||
default: defineComponent({
|
||||
name: 'LabelStub',
|
||||
setup(_props, { attrs, slots }) {
|
||||
return () => h('label', attrs, slots.default?.())
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
|
||||
|
||||
function mountLoginDialog() {
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
const app = createApp(LoginDialog, {
|
||||
modelValue: true,
|
||||
'onUpdate:modelValue': vi.fn(),
|
||||
})
|
||||
app.mount(root)
|
||||
mountedApps.push({ app, root })
|
||||
return root
|
||||
}
|
||||
|
||||
async function settle() {
|
||||
for (let index = 0; index < 4; index += 1) {
|
||||
await Promise.resolve()
|
||||
await nextTick()
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
authStoreMock.loading = false
|
||||
authStoreMock.error = ''
|
||||
authStoreMock.canAccessAdmin = false
|
||||
authStoreMock.login.mockReset()
|
||||
routerPushMock.mockReset()
|
||||
toastMocks.success.mockReset()
|
||||
toastMocks.warning.mockReset()
|
||||
toastMocks.error.mockReset()
|
||||
authApiMocks.getRegistrationSettings.mockResolvedValue({
|
||||
enable_registration: false,
|
||||
require_email_verification: false,
|
||||
email_configured: true,
|
||||
password_policy_level: 'weak',
|
||||
turnstile_enabled: false,
|
||||
turnstile_site_key: null,
|
||||
})
|
||||
authApiMocks.getAuthSettings.mockResolvedValue({
|
||||
local_enabled: true,
|
||||
ldap_enabled: false,
|
||||
ldap_exclusive: false,
|
||||
})
|
||||
oauthApiMocks.getProviders.mockResolvedValue([])
|
||||
sessionStorage.clear()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
for (const { app, root } of mountedApps.splice(0)) {
|
||||
app.unmount()
|
||||
root.remove()
|
||||
}
|
||||
document.body.innerHTML = ''
|
||||
sessionStorage.clear()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
describe('LoginDialog password manager contract', () => {
|
||||
it('exposes standard login form and field autocomplete metadata', async () => {
|
||||
const root = mountLoginDialog()
|
||||
await settle()
|
||||
|
||||
const form = root.querySelector('form')
|
||||
expect(form?.getAttribute('name')).toBe('login')
|
||||
expect(form?.getAttribute('action')).toBe('/api/auth/login')
|
||||
expect(form?.getAttribute('method')).toBe('post')
|
||||
expect(form?.getAttribute('autocomplete')).toBe('on')
|
||||
expect(form?.getAttribute('data-form-type')).toBe('login')
|
||||
|
||||
const username = root.querySelector<HTMLInputElement>('input[name="username"]')
|
||||
const password = root.querySelector<HTMLInputElement>('input[name="password"]')
|
||||
|
||||
expect(username?.id).toBe('username')
|
||||
expect(username?.getAttribute('autocomplete')).toBe('username')
|
||||
expect(username?.getAttribute('autocapitalize')).toBe('none')
|
||||
expect(username?.getAttribute('spellcheck')).toBe('false')
|
||||
expect(password?.id).toBe('password')
|
||||
expect(password?.type).toBe('password')
|
||||
expect(password?.getAttribute('autocomplete')).toBe('current-password')
|
||||
})
|
||||
|
||||
it('submits DOM-filled credentials and awaits router navigation without timer delay', async () => {
|
||||
authStoreMock.login.mockResolvedValue(true)
|
||||
routerPushMock.mockResolvedValue(undefined)
|
||||
sessionStorage.setItem('redirectPath', '/admin/dashboard')
|
||||
const root = mountLoginDialog()
|
||||
await settle()
|
||||
|
||||
const form = root.querySelector('form')
|
||||
const username = root.querySelector<HTMLInputElement>('input[name="username"]')
|
||||
const password = root.querySelector<HTMLInputElement>('input[name="password"]')
|
||||
expect(form).not.toBeNull()
|
||||
expect(username).not.toBeNull()
|
||||
expect(password).not.toBeNull()
|
||||
|
||||
username!.value = ' [email protected] '
|
||||
password!.value = 'secret-from-manager'
|
||||
form!.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
|
||||
await settle()
|
||||
|
||||
expect(authStoreMock.login).toHaveBeenCalledWith('[email protected]', 'secret-from-manager', 'local')
|
||||
expect(routerPushMock).toHaveBeenCalledWith('/admin/dashboard')
|
||||
expect(sessionStorage.getItem('redirectPath')).toBeNull()
|
||||
expect(toastMocks.success).toHaveBeenCalledWith('登录成功,正在跳转...')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user