feat: complete pending oauth account creation UI

This commit is contained in:
IanShaw027
2026-04-21 00:02:51 +08:00
parent 7ef7fd19e7
commit 0fa47f18ed
8 changed files with 469 additions and 116 deletions

View File

@@ -0,0 +1,212 @@
<template>
<form class="space-y-3" @submit.prevent="handleSubmit">
<input
v-model="email"
:data-testid="`${testIdPrefix}-create-account-email`"
type="email"
class="input w-full"
placeholder="you@example.com"
:disabled="isSubmitting || isSendingCode"
/>
<input
v-model="password"
:data-testid="`${testIdPrefix}-create-account-password`"
type="password"
class="input w-full"
placeholder="Password"
:disabled="isSubmitting"
/>
<div class="flex gap-3">
<input
v-model="verifyCode"
:data-testid="`${testIdPrefix}-create-account-verify-code`"
type="text"
inputmode="numeric"
maxlength="6"
class="input min-w-0 flex-1"
placeholder="123456"
:disabled="isSubmitting"
/>
<button
:data-testid="`${testIdPrefix}-create-account-send-code`"
type="button"
class="btn btn-secondary shrink-0"
:disabled="isSubmitting || isSendingCode || countdown > 0 || !email.trim()"
@click="handleSendCode"
>
{{
isSendingCode
? t('auth.sendingCode')
: countdown > 0
? t('auth.resendCountdown', { countdown })
: t('auth.sendCode')
}}
</button>
</div>
<p v-if="sendCodeSuccess" class="text-sm text-green-600 dark:text-green-400">
{{ t('auth.codeSentSuccess') }}
</p>
<p v-else class="text-xs text-gray-500 dark:text-dark-400">
{{ t('auth.verificationCodeHint') }}
</p>
<button
:data-testid="`${testIdPrefix}-create-account-submit`"
type="button"
class="btn btn-primary w-full"
:disabled="isSubmitting || !email.trim() || password.length < 6"
@click="handleSubmit"
>
{{ isSubmitting ? t('common.processing') : 'Create account' }}
</button>
<button
type="button"
class="btn btn-secondary w-full"
:disabled="isSubmitting"
@click="emitSwitchToBind"
>
I already have an account
</button>
<transition name="fade">
<p v-if="sendCodeError" class="text-sm text-red-600 dark:text-red-400">
{{ sendCodeError }}
</p>
</transition>
<transition name="fade">
<p v-if="errorMessage" class="text-sm text-red-600 dark:text-red-400">
{{ errorMessage }}
</p>
</transition>
</form>
</template>
<script setup lang="ts">
import { onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { sendVerifyCode } from '@/api/auth'
export type PendingOAuthCreateAccountPayload = {
email: string
password: string
verifyCode: string
}
const props = defineProps<{
initialEmail: string
testIdPrefix: string
isSubmitting: boolean
errorMessage?: string
}>()
const emit = defineEmits<{
submit: [payload: PendingOAuthCreateAccountPayload]
switchToBind: [email: string]
}>()
const { t } = useI18n()
const email = ref('')
const password = ref('')
const verifyCode = ref('')
const isSendingCode = ref(false)
const sendCodeError = ref('')
const sendCodeSuccess = ref(false)
const countdown = ref(0)
let countdownTimer: ReturnType<typeof setInterval> | null = null
watch(
() => props.initialEmail,
value => {
email.value = value || ''
},
{ immediate: true }
)
function clearCountdown() {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
}
function startCountdown(seconds: number) {
clearCountdown()
countdown.value = Math.max(0, seconds)
if (countdown.value <= 0) {
return
}
countdownTimer = setInterval(() => {
if (countdown.value <= 1) {
countdown.value = 0
clearCountdown()
return
}
countdown.value -= 1
}, 1000)
}
function getRequestErrorMessage(error: unknown, fallback: string): string {
const err = error as { message?: string; response?: { data?: { detail?: string; message?: string } } }
return err.response?.data?.detail || err.response?.data?.message || err.message || fallback
}
async function handleSendCode() {
const trimmedEmail = email.value.trim()
if (!trimmedEmail) {
return
}
isSendingCode.value = true
sendCodeError.value = ''
sendCodeSuccess.value = false
try {
const response = await sendVerifyCode({
email: trimmedEmail
})
sendCodeSuccess.value = true
startCountdown(response.countdown)
} catch (error: unknown) {
sendCodeError.value = getRequestErrorMessage(error, t('auth.sendCodeFailed'))
} finally {
isSendingCode.value = false
}
}
function handleSubmit() {
const trimmedEmail = email.value.trim()
if (!trimmedEmail || password.value.length < 6) {
return
}
emit('submit', {
email: trimmedEmail,
password: password.value,
verifyCode: verifyCode.value.trim()
})
}
function emitSwitchToBind() {
emit('switchToBind', email.value.trim())
}
onUnmounted(() => {
clearCountdown()
})
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: all 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(-8px);
}
</style>

View File

@@ -0,0 +1,80 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import PendingOAuthCreateAccountForm from '../PendingOAuthCreateAccountForm.vue'
const sendVerifyCode = vi.fn()
vi.mock('vue-i18n', async () => {
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
return {
...actual,
useI18n: () => ({
t: (key: string) => key
})
}
})
vi.mock('@/api/auth', async () => {
const actual = await vi.importActual<typeof import('@/api/auth')>('@/api/auth')
return {
...actual,
sendVerifyCode: (...args: any[]) => sendVerifyCode(...args)
}
})
describe('PendingOAuthCreateAccountForm', () => {
beforeEach(() => {
sendVerifyCode.mockReset()
})
it('emits trimmed email, password, and verify code on submit', async () => {
const wrapper = mount(PendingOAuthCreateAccountForm, {
props: {
providerName: 'LinuxDo',
testIdPrefix: 'linuxdo',
initialEmail: 'prefill@example.com',
isSubmitting: false
}
})
await wrapper.get('[data-testid="linuxdo-create-account-email"]').setValue(' user@example.com ')
await wrapper.get('[data-testid="linuxdo-create-account-password"]').setValue('secret-123')
await wrapper.get('[data-testid="linuxdo-create-account-verify-code"]').setValue(' 246810 ')
await wrapper.get('form').trigger('submit.prevent')
expect(wrapper.emitted('submit')).toEqual([
[
{
email: 'user@example.com',
password: 'secret-123',
verifyCode: '246810'
}
]
])
})
it('sends a verify code for the trimmed email value', async () => {
sendVerifyCode.mockResolvedValue({
message: 'sent',
countdown: 60
})
const wrapper = mount(PendingOAuthCreateAccountForm, {
props: {
providerName: 'LinuxDo',
testIdPrefix: 'linuxdo',
initialEmail: '',
isSubmitting: false
}
})
await wrapper.get('[data-testid="linuxdo-create-account-email"]').setValue(' user@example.com ')
await wrapper.get('[data-testid="linuxdo-create-account-send-code"]').trigger('click')
await flushPromises()
expect(sendVerifyCode).toHaveBeenCalledWith({
email: 'user@example.com'
})
})
})

View File

@@ -113,37 +113,14 @@
<p class="text-sm text-gray-700 dark:text-gray-300">
Enter an email address to create your account and continue.
</p>
<div class="space-y-3">
<input
v-model="pendingAccountEmail"
data-testid="linuxdo-create-account-email"
type="email"
class="input w-full"
placeholder="you@example.com"
:disabled="isSubmitting"
@keyup.enter="handleCreateAccount"
/>
<button
data-testid="linuxdo-create-account-submit"
class="btn btn-primary w-full"
:disabled="isSubmitting || !pendingAccountEmail.trim()"
@click="handleCreateAccount"
>
{{ isSubmitting ? t('common.processing') : 'Create account' }}
</button>
<button
class="btn btn-secondary w-full"
:disabled="isSubmitting"
@click="switchToBindLoginMode"
>
I already have an account
</button>
</div>
<transition name="fade">
<p v-if="accountActionError" class="text-sm text-red-600 dark:text-red-400">
{{ accountActionError }}
</p>
</transition>
<PendingOAuthCreateAccountForm
test-id-prefix="linuxdo"
:initial-email="pendingAccountEmail"
:is-submitting="isSubmitting"
:error-message="accountActionError"
@submit="handleCreateAccount"
@switch-to-bind="switchToBindLoginMode"
/>
</template>
<template v-else-if="needsBindLogin">
@@ -258,6 +235,9 @@ import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { AuthLayout } from '@/components/layout'
import PendingOAuthCreateAccountForm, {
type PendingOAuthCreateAccountPayload
} from '@/components/auth/PendingOAuthCreateAccountForm.vue'
import Icon from '@/components/icons/Icon.vue'
import { apiClient } from '@/api/client'
import { useAuthStore, useAppStore } from '@/stores'
@@ -432,9 +412,9 @@ function applyTotpChallenge(completion: LinuxDoPendingActionResponse): boolean {
return true
}
function switchToBindLoginMode() {
function switchToBindLoginMode(nextEmail?: string) {
pendingAccountAction.value = 'bind_login'
bindLoginEmail.value = bindLoginEmail.value.trim() || pendingAccountEmail.value.trim()
bindLoginEmail.value = bindLoginEmail.value.trim() || nextEmail?.trim() || pendingAccountEmail.value.trim()
bindLoginPassword.value = ''
accountActionError.value = ''
canReturnToCreateAccount.value = true
@@ -533,15 +513,16 @@ async function handleContinueLogin() {
}
}
async function handleCreateAccount() {
async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
accountActionError.value = ''
const email = pendingAccountEmail.value.trim()
if (!email) return
if (!payload.email || !payload.password) return
isSubmitting.value = true
try {
const { data } = await apiClient.post<LinuxDoPendingActionResponse>('/auth/oauth/pending/create-account', {
email,
email: payload.email,
password: payload.password,
verify_code: payload.verifyCode || undefined,
...serializeAdoptionDecision(currentAdoptionDecision())
})
await finalizePendingAccountResponse(data)

View File

@@ -122,37 +122,14 @@
<p class="text-sm text-gray-700 dark:text-gray-300">
Enter an email address to create your account and continue.
</p>
<div class="space-y-3">
<input
v-model="pendingAccountEmail"
data-testid="oidc-create-account-email"
type="email"
class="input w-full"
placeholder="you@example.com"
:disabled="isSubmitting"
@keyup.enter="handleCreateAccount"
/>
<button
data-testid="oidc-create-account-submit"
class="btn btn-primary w-full"
:disabled="isSubmitting || !pendingAccountEmail.trim()"
@click="handleCreateAccount"
>
{{ isSubmitting ? t('common.processing') : 'Create account' }}
</button>
<button
class="btn btn-secondary w-full"
:disabled="isSubmitting"
@click="switchToBindLoginMode"
>
I already have an account
</button>
</div>
<transition name="fade">
<p v-if="accountActionError" class="text-sm text-red-600 dark:text-red-400">
{{ accountActionError }}
</p>
</transition>
<PendingOAuthCreateAccountForm
test-id-prefix="oidc"
:initial-email="pendingAccountEmail"
:is-submitting="isSubmitting"
:error-message="accountActionError"
@submit="handleCreateAccount"
@switch-to-bind="switchToBindLoginMode"
/>
</template>
<template v-else-if="needsBindLogin">
@@ -267,6 +244,9 @@ import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { AuthLayout } from '@/components/layout'
import PendingOAuthCreateAccountForm, {
type PendingOAuthCreateAccountPayload
} from '@/components/auth/PendingOAuthCreateAccountForm.vue'
import Icon from '@/components/icons/Icon.vue'
import { apiClient } from '@/api/client'
import { useAuthStore, useAppStore } from '@/stores'
@@ -476,9 +456,9 @@ function applyTotpChallenge(completion: PendingOidcCompletion): boolean {
return true
}
function switchToBindLoginMode() {
function switchToBindLoginMode(nextEmail?: string) {
pendingAccountAction.value = 'bind_login'
bindLoginEmail.value = bindLoginEmail.value.trim() || pendingAccountEmail.value.trim()
bindLoginEmail.value = bindLoginEmail.value.trim() || nextEmail?.trim() || pendingAccountEmail.value.trim()
bindLoginPassword.value = ''
accountActionError.value = ''
canReturnToCreateAccount.value = true
@@ -577,15 +557,16 @@ async function handleContinueLogin() {
}
}
async function handleCreateAccount() {
async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
accountActionError.value = ''
const email = pendingAccountEmail.value.trim()
if (!email) return
if (!payload.email || !payload.password) return
isSubmitting.value = true
try {
const { data } = await apiClient.post<PendingOidcCompletion>('/auth/oauth/pending/create-account', {
email,
email: payload.email,
password: payload.password,
verify_code: payload.verifyCode || undefined,
...serializeAdoptionDecision(currentAdoptionDecision())
})
await finalizePendingAccountResponse(data)

View File

@@ -160,37 +160,14 @@
<p class="text-sm text-gray-700 dark:text-gray-300">
Enter an email address to create your account and continue.
</p>
<div class="space-y-3">
<input
v-model="pendingAccountEmail"
data-testid="wechat-create-account-email"
type="email"
class="input w-full"
placeholder="you@example.com"
:disabled="isSubmitting"
@keyup.enter="handleCreateAccount"
/>
<button
data-testid="wechat-create-account-submit"
class="btn btn-primary w-full"
:disabled="isSubmitting || !pendingAccountEmail.trim()"
@click="handleCreateAccount"
>
{{ isSubmitting ? t('common.processing') : 'Create account' }}
</button>
<button
class="btn btn-secondary w-full"
:disabled="isSubmitting"
@click="switchToBindLoginMode"
>
I already have an account
</button>
</div>
<transition name="fade">
<p v-if="accountActionError" class="text-sm text-red-600 dark:text-red-400">
{{ accountActionError }}
</p>
</transition>
<PendingOAuthCreateAccountForm
test-id-prefix="wechat"
:initial-email="pendingAccountEmail"
:is-submitting="isSubmitting"
:error-message="accountActionError"
@submit="handleCreateAccount"
@switch-to-bind="switchToBindLoginMode"
/>
</template>
<template v-else-if="needsBindLogin">
@@ -305,6 +282,9 @@ import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { AuthLayout } from '@/components/layout'
import PendingOAuthCreateAccountForm, {
type PendingOAuthCreateAccountPayload
} from '@/components/auth/PendingOAuthCreateAccountForm.vue'
import Icon from '@/components/icons/Icon.vue'
import { apiClient } from '@/api/client'
import { useAuthStore, useAppStore } from '@/stores'
@@ -575,9 +555,9 @@ function applyTotpChallenge(completion: PendingWeChatCompletion): boolean {
return true
}
function switchToBindLoginMode() {
function switchToBindLoginMode(nextEmail?: string) {
pendingAccountAction.value = 'bind_login'
bindLoginEmail.value = bindLoginEmail.value.trim() || pendingAccountEmail.value.trim()
bindLoginEmail.value = bindLoginEmail.value.trim() || nextEmail?.trim() || pendingAccountEmail.value.trim()
bindLoginPassword.value = ''
accountActionError.value = ''
canReturnToCreateAccount.value = true
@@ -676,15 +656,16 @@ async function handleContinueLogin() {
}
}
async function handleCreateAccount() {
async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
accountActionError.value = ''
const email = pendingAccountEmail.value.trim()
if (!email) return
if (!payload.email || !payload.password) return
isSubmitting.value = true
try {
const { data } = await apiClient.post<PendingWeChatCompletion>('/auth/oauth/pending/create-account', {
email,
email: payload.email,
password: payload.password,
verify_code: payload.verifyCode || undefined,
...serializeAdoptionDecision(currentAdoptionDecision())
})
await finalizePendingAccountResponse(data)

View File

@@ -11,6 +11,7 @@ const exchangePendingOAuthCompletion = vi.fn()
const completeLinuxDoOAuthRegistration = vi.fn()
const login2FA = vi.fn()
const apiClientPost = vi.fn()
const sendVerifyCode = vi.fn()
vi.mock('vue-router', () => ({
useRoute: () => ({
@@ -53,7 +54,8 @@ vi.mock('@/api/auth', async () => {
...actual,
exchangePendingOAuthCompletion: (...args: any[]) => exchangePendingOAuthCompletion(...args),
completeLinuxDoOAuthRegistration: (...args: any[]) => completeLinuxDoOAuthRegistration(...args),
login2FA: (...args: any[]) => login2FA(...args)
login2FA: (...args: any[]) => login2FA(...args),
sendVerifyCode: (...args: any[]) => sendVerifyCode(...args)
}
})
@@ -67,6 +69,7 @@ describe('LinuxDoCallbackView', () => {
completeLinuxDoOAuthRegistration.mockReset()
login2FA.mockReset()
apiClientPost.mockReset()
sendVerifyCode.mockReset()
})
it('does not send adoption decisions during the initial exchange', async () => {
@@ -251,7 +254,7 @@ describe('LinuxDoCallbackView', () => {
})
})
it('collects email for pending oauth account creation and submits adoption decisions', async () => {
it('collects email, password, and verify code for pending oauth account creation and submits adoption decisions', async () => {
exchangePendingOAuthCompletion.mockResolvedValue({
error: 'email_required',
redirect: '/welcome',
@@ -286,11 +289,15 @@ describe('LinuxDoCallbackView', () => {
expect(checkboxes).toHaveLength(2)
await checkboxes[1].setValue(false)
await wrapper.get('[data-testid="linuxdo-create-account-email"]').setValue(' new@example.com ')
await wrapper.get('[data-testid="linuxdo-create-account-password"]').setValue('secret-123')
await wrapper.get('[data-testid="linuxdo-create-account-verify-code"]').setValue('246810')
await wrapper.get('[data-testid="linuxdo-create-account-submit"]').trigger('click')
await flushPromises()
expect(apiClientPost).toHaveBeenCalledWith('/auth/oauth/pending/create-account', {
email: 'new@example.com',
password: 'secret-123',
verify_code: '246810',
adopt_display_name: true,
adopt_avatar: false
})
@@ -298,6 +305,38 @@ describe('LinuxDoCallbackView', () => {
expect(replace).toHaveBeenCalledWith('/welcome')
})
it('sends a verify code for pending oauth account creation', async () => {
exchangePendingOAuthCompletion.mockResolvedValue({
error: 'email_required',
redirect: '/welcome'
})
sendVerifyCode.mockResolvedValue({
message: 'sent',
countdown: 60
})
const wrapper = mount(LinuxDoCallbackView, {
global: {
stubs: {
AuthLayout: { template: '<div><slot /></div>' },
Icon: true,
RouterLink: { template: '<a><slot /></a>' },
transition: false
}
}
})
await flushPromises()
await wrapper.get('[data-testid="linuxdo-create-account-email"]').setValue(' new@example.com ')
await wrapper.get('[data-testid="linuxdo-create-account-send-code"]').trigger('click')
await flushPromises()
expect(sendVerifyCode).toHaveBeenCalledWith({
email: 'new@example.com'
})
})
it('shows bind-login form for existing account binding and submits credentials with adoption decisions', async () => {
exchangePendingOAuthCompletion.mockResolvedValue({
error: 'bind_login_required',

View File

@@ -12,6 +12,7 @@ const completeOIDCOAuthRegistration = vi.fn()
const getPublicSettings = vi.fn()
const login2FA = vi.fn()
const apiClientPost = vi.fn()
const sendVerifyCode = vi.fn()
vi.mock('vue-router', () => ({
useRoute: () => ({
@@ -60,7 +61,8 @@ vi.mock('@/api/auth', async () => {
exchangePendingOAuthCompletion: (...args: any[]) => exchangePendingOAuthCompletion(...args),
completeOIDCOAuthRegistration: (...args: any[]) => completeOIDCOAuthRegistration(...args),
getPublicSettings: (...args: any[]) => getPublicSettings(...args),
login2FA: (...args: any[]) => login2FA(...args)
login2FA: (...args: any[]) => login2FA(...args),
sendVerifyCode: (...args: any[]) => sendVerifyCode(...args)
}
})
@@ -75,6 +77,7 @@ describe('OidcCallbackView', () => {
getPublicSettings.mockReset()
login2FA.mockReset()
apiClientPost.mockReset()
sendVerifyCode.mockReset()
getPublicSettings.mockResolvedValue({
oidc_oauth_provider_name: 'ExampleID'
})
@@ -234,7 +237,7 @@ describe('OidcCallbackView', () => {
})
})
it('collects email for pending oauth account creation and submits adoption decisions', async () => {
it('collects email, password, and verify code for pending oauth account creation and submits adoption decisions', async () => {
exchangePendingOAuthCompletion.mockResolvedValue({
error: 'email_required',
redirect: '/welcome',
@@ -269,11 +272,15 @@ describe('OidcCallbackView', () => {
expect(checkboxes).toHaveLength(2)
await checkboxes[1].setValue(false)
await wrapper.get('[data-testid="oidc-create-account-email"]').setValue(' new@example.com ')
await wrapper.get('[data-testid="oidc-create-account-password"]').setValue('secret-123')
await wrapper.get('[data-testid="oidc-create-account-verify-code"]').setValue('246810')
await wrapper.get('[data-testid="oidc-create-account-submit"]').trigger('click')
await flushPromises()
expect(apiClientPost).toHaveBeenCalledWith('/auth/oauth/pending/create-account', {
email: 'new@example.com',
password: 'secret-123',
verify_code: '246810',
adopt_display_name: true,
adopt_avatar: false
})
@@ -281,6 +288,38 @@ describe('OidcCallbackView', () => {
expect(replace).toHaveBeenCalledWith('/welcome')
})
it('sends a verify code for pending oauth account creation', async () => {
exchangePendingOAuthCompletion.mockResolvedValue({
error: 'email_required',
redirect: '/welcome'
})
sendVerifyCode.mockResolvedValue({
message: 'sent',
countdown: 60
})
const wrapper = mount(OidcCallbackView, {
global: {
stubs: {
AuthLayout: { template: '<div><slot /></div>' },
Icon: true,
RouterLink: { template: '<a><slot /></a>' },
transition: false
}
}
})
await flushPromises()
await wrapper.get('[data-testid="oidc-create-account-email"]').setValue(' new@example.com ')
await wrapper.get('[data-testid="oidc-create-account-send-code"]').trigger('click')
await flushPromises()
expect(sendVerifyCode).toHaveBeenCalledWith({
email: 'new@example.com'
})
})
it('shows bind-login form for existing account binding and submits credentials with adoption decisions', async () => {
exchangePendingOAuthCompletion.mockResolvedValue({
error: 'adopt_existing_user_by_email',

View File

@@ -7,6 +7,7 @@ const {
completeWeChatOAuthRegistrationMock,
login2FAMock,
apiClientPostMock,
sendVerifyCodeMock,
prepareOAuthBindAccessTokenCookieMock,
getAuthTokenMock,
replaceMock,
@@ -20,6 +21,7 @@ const {
completeWeChatOAuthRegistrationMock: vi.fn(),
login2FAMock: vi.fn(),
apiClientPostMock: vi.fn(),
sendVerifyCodeMock: vi.fn(),
prepareOAuthBindAccessTokenCookieMock: vi.fn(),
getAuthTokenMock: vi.fn(),
replaceMock: vi.fn(),
@@ -118,6 +120,7 @@ vi.mock('@/api/auth', async () => {
exchangePendingOAuthCompletion: (...args: any[]) => exchangePendingOAuthCompletionMock(...args),
completeWeChatOAuthRegistration: (...args: any[]) => completeWeChatOAuthRegistrationMock(...args),
login2FA: (...args: any[]) => login2FAMock(...args),
sendVerifyCode: (...args: any[]) => sendVerifyCodeMock(...args),
prepareOAuthBindAccessTokenCookie: (...args: any[]) => prepareOAuthBindAccessTokenCookieMock(...args),
getAuthToken: (...args: any[]) => getAuthTokenMock(...args),
}
@@ -129,6 +132,7 @@ describe('WechatCallbackView', () => {
completeWeChatOAuthRegistrationMock.mockReset()
login2FAMock.mockReset()
apiClientPostMock.mockReset()
sendVerifyCodeMock.mockReset()
replaceMock.mockReset()
setTokenMock.mockReset()
showSuccessMock.mockReset()
@@ -374,7 +378,7 @@ describe('WechatCallbackView', () => {
expect(locationState.current.href).toContain('mode=open')
})
it('collects email for pending oauth account creation and submits adoption decisions', async () => {
it('collects email, password, and verify code for pending oauth account creation and submits adoption decisions', async () => {
exchangePendingOAuthCompletionMock.mockResolvedValue({
error: 'email_required',
redirect: '/welcome',
@@ -409,11 +413,15 @@ describe('WechatCallbackView', () => {
expect(checkboxes).toHaveLength(2)
await checkboxes[1].setValue(false)
await wrapper.get('[data-testid="wechat-create-account-email"]').setValue(' new@example.com ')
await wrapper.get('[data-testid="wechat-create-account-password"]').setValue('secret-123')
await wrapper.get('[data-testid="wechat-create-account-verify-code"]').setValue('246810')
await wrapper.get('[data-testid="wechat-create-account-submit"]').trigger('click')
await flushPromises()
expect(apiClientPostMock).toHaveBeenCalledWith('/auth/oauth/pending/create-account', {
email: 'new@example.com',
password: 'secret-123',
verify_code: '246810',
adopt_display_name: true,
adopt_avatar: false,
})
@@ -421,6 +429,38 @@ describe('WechatCallbackView', () => {
expect(replaceMock).toHaveBeenCalledWith('/welcome')
})
it('sends a verify code for pending oauth account creation', async () => {
exchangePendingOAuthCompletionMock.mockResolvedValue({
error: 'email_required',
redirect: '/welcome',
})
sendVerifyCodeMock.mockResolvedValue({
message: 'sent',
countdown: 60,
})
const wrapper = mount(WechatCallbackView, {
global: {
stubs: {
AuthLayout: { template: '<div><slot /></div>' },
Icon: true,
RouterLink: { template: '<a><slot /></a>' },
transition: false,
},
},
})
await flushPromises()
await wrapper.get('[data-testid="wechat-create-account-email"]').setValue(' new@example.com ')
await wrapper.get('[data-testid="wechat-create-account-send-code"]').trigger('click')
await flushPromises()
expect(sendVerifyCodeMock).toHaveBeenCalledWith({
email: 'new@example.com',
})
})
it('shows bind-login form for existing account binding and submits credentials with adoption decisions', async () => {
exchangePendingOAuthCompletionMock.mockResolvedValue({
step: 'bind_login_required',