fix(public): 修复用户可见性、额度、验证与 Codex 探测

This commit is contained in:
Entropy.Xu
2026-05-16 00:51:44 +08:00
parent 8eb4c029b2
commit bbd3c30b0e
50 changed files with 2321 additions and 347 deletions
@@ -0,0 +1,205 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createApp, defineComponent, h, nextTick, type App } from 'vue'
import RegisterDialog from '../RegisterDialog.vue'
const authApiMocks = vi.hoisted(() => ({
sendVerificationCode: vi.fn(),
getVerificationStatus: vi.fn(),
verifyEmail: vi.fn(),
register: vi.fn(),
}))
vi.mock('@/api/auth', () => ({
authApi: authApiMocks,
}))
vi.mock('@/composables/useToast', () => ({
useToast: () => ({
success: vi.fn(),
error: vi.fn(),
}),
}))
vi.mock('@/utils/errorParser', () => ({
parseApiError: (_error: unknown, fallback: string) => fallback,
}))
vi.mock('../TurnstileWidget.vue', () => ({
default: defineComponent({
name: 'TurnstileWidgetStub',
props: {
modelValue: { type: String, default: '' },
siteKey: { type: String, required: true },
},
emits: ['update:modelValue'],
setup(_props, { emit, expose }) {
expose({ reset: vi.fn() })
return () =>
h('button', {
type: 'button',
'data-testid': 'turnstile-widget',
onClick: () => emit('update:modelValue', 'turnstile-token-123'),
}, 'Turnstile')
},
}),
}))
vi.mock('@/components/ui', async () => {
const { defineComponent, h } = await import('vue')
return {
Dialog: defineComponent({
name: 'DialogStub',
props: { open: { type: Boolean, default: false } },
emits: ['update:open'],
setup(props, { slots }) {
return () => props.open
? h('div', [slots.default?.(), slots.footer?.()])
: 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' },
},
emits: ['click'],
setup(props, { attrs, emit, slots }) {
return () => h('button', {
...attrs,
type: props.type,
disabled: props.disabled,
onClick: (event: MouseEvent) => emit('click', event),
}, slots.default?.())
},
}),
}
})
vi.mock('@/components/ui/input.vue', async () => {
const { defineComponent, h } = await import('vue')
return {
default: defineComponent({
name: 'InputStub',
props: {
modelValue: { type: [String, Number], default: '' },
disabled: { type: Boolean, default: false },
type: { type: String, default: 'text' },
id: { type: String, default: undefined },
},
emits: ['update:modelValue'],
setup(props, { attrs, emit }) {
return () => h('input', {
...attrs,
id: props.id,
type: props.type,
disabled: props.disabled,
value: props.modelValue,
onInput: (event: Event) => emit('update:modelValue', (event.target as HTMLInputElement).value),
})
},
}),
}
})
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 mountRegisterDialog() {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp(RegisterDialog, {
open: true,
requireEmailVerification: true,
emailConfigured: true,
turnstileEnabled: true,
turnstileSiteKey: 'site-key-123',
'onUpdate:open': 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(() => {
authApiMocks.sendVerificationCode.mockReset()
authApiMocks.getVerificationStatus.mockReset()
authApiMocks.verifyEmail.mockReset()
authApiMocks.register.mockReset()
authApiMocks.getVerificationStatus.mockResolvedValue({
has_pending_code: false,
is_verified: false,
cooldown_remaining: null,
code_expires_in: null,
})
authApiMocks.sendVerificationCode.mockResolvedValue({
success: true,
message: 'ok',
expire_minutes: 5,
})
})
afterEach(() => {
for (const { app, root } of mountedApps.splice(0)) {
app.unmount()
root.remove()
}
document.body.innerHTML = ''
})
describe('RegisterDialog Turnstile verification flow', () => {
it('shows Turnstile before sending code and includes the token in the request', async () => {
const root = mountRegisterDialog()
await settle()
expect(root.textContent).toContain('Turnstile')
const emailInput = root.querySelector('#reg-email') as HTMLInputElement
emailInput.value = '[email protected]'
emailInput.dispatchEvent(new Event('input'))
await settle()
const sendButtonBeforeToken = Array.from(root.querySelectorAll('button'))
.find((button) => button.textContent?.includes('请先完成人机验证')) as HTMLButtonElement
expect(sendButtonBeforeToken.disabled).toBe(true)
const turnstileButton = root.querySelector('[data-testid="turnstile-widget"]') as HTMLButtonElement
turnstileButton.click()
await settle()
const sendButton = Array.from(root.querySelectorAll('button'))
.find((button) => button.textContent?.includes('发送验证码')) as HTMLButtonElement
expect(sendButton.disabled).toBe(false)
sendButton.click()
await settle()
expect(authApiMocks.sendVerificationCode).toHaveBeenCalledWith(
'[email protected]',
'turnstile-token-123'
)
})
})