Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
dcd5c43d
Commit
dcd5c43d
authored
Apr 21, 2026
by
IanShaw027
Browse files
feat: complete email binding and pending oauth verification flows
parent
6da08262
Changes
29
Hide whitespace changes
Inline
Side-by-side
frontend/src/types/index.ts
View file @
dcd5c43d
...
@@ -118,6 +118,8 @@ export interface RegisterRequest {
...
@@ -118,6 +118,8 @@ export interface RegisterRequest {
export
interface
SendVerifyCodeRequest
{
export
interface
SendVerifyCodeRequest
{
email
:
string
email
:
string
turnstile_token
?:
string
turnstile_token
?:
string
pending_auth_token
?:
string
pending_oauth_token
?:
string
}
}
export
interface
SendVerifyCodeResponse
{
export
interface
SendVerifyCodeResponse
{
...
...
frontend/src/views/auth/EmailVerifyView.vue
View file @
dcd5c43d
...
@@ -176,7 +176,12 @@ import { AuthLayout } from '@/components/layout'
...
@@ -176,7 +176,12 @@ import { AuthLayout } from '@/components/layout'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
TurnstileWidget
from
'
@/components/TurnstileWidget.vue
'
import
TurnstileWidget
from
'
@/components/TurnstileWidget.vue
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
import
{
persistOAuthTokenContext
,
getPublicSettings
,
sendVerifyCode
}
from
'
@/api/auth
'
import
{
persistOAuthTokenContext
,
getPublicSettings
,
sendPendingOAuthVerifyCode
,
sendVerifyCode
,
}
from
'
@/api/auth
'
import
{
apiClient
}
from
'
@/api/client
'
import
{
apiClient
}
from
'
@/api/client
'
import
{
buildAuthErrorMessage
}
from
'
@/utils/authError
'
import
{
buildAuthErrorMessage
}
from
'
@/utils/authError
'
import
{
import
{
...
@@ -355,18 +360,21 @@ async function sendCode(): Promise<void> {
...
@@ -355,18 +360,21 @@ async function sendCode(): Promise<void> {
errorMessage
.
value
=
''
errorMessage
.
value
=
''
try
{
try
{
if
(
!
isRegistrationEmailSuffixAllowed
(
email
.
value
,
registrationEmailSuffixWhitelist
.
value
))
{
if
(
!
pendingAuthToken
.
value
&&
!
isRegistrationEmailSuffixAllowed
(
email
.
value
,
registrationEmailSuffixWhitelist
.
value
))
{
errorMessage
.
value
=
buildEmailSuffixNotAllowedMessage
()
errorMessage
.
value
=
buildEmailSuffixNotAllowedMessage
()
appStore
.
showError
(
errorMessage
.
value
)
appStore
.
showError
(
errorMessage
.
value
)
return
return
}
}
const
re
sponse
=
await
sendVerifyCode
(
{
const
re
questPayload
=
{
email
:
email
.
value
,
email
:
email
.
value
,
[
pendingAuthTokenField
.
value
]:
pendingAuthToken
.
value
||
undefined
,
[
pendingAuthTokenField
.
value
]:
pendingAuthToken
.
value
||
undefined
,
// 优先使用重发时新获取的 token(因为初始 token 可能已被使用)
// 优先使用重发时新获取的 token(因为初始 token 可能已被使用)
turnstile_token
:
resendTurnstileToken
.
value
||
initialTurnstileToken
.
value
||
undefined
turnstile_token
:
resendTurnstileToken
.
value
||
initialTurnstileToken
.
value
||
undefined
}
as
Parameters
<
typeof
sendVerifyCode
>
[
0
])
}
as
Parameters
<
typeof
sendVerifyCode
>
[
0
]
const
response
=
pendingAuthToken
.
value
?
await
sendPendingOAuthVerifyCode
(
requestPayload
)
:
await
sendVerifyCode
(
requestPayload
)
codeSent
.
value
=
true
codeSent
.
value
=
true
startCountdown
(
response
.
countdown
)
startCountdown
(
response
.
countdown
)
...
...
frontend/src/views/auth/LinuxDoCallbackView.vue
View file @
dcd5c43d
...
@@ -444,6 +444,28 @@ function getRequestErrorMessage(error: unknown, fallback: string): string {
...
@@ -444,6 +444,28 @@ function getRequestErrorMessage(error: unknown, fallback: string): string {
return
err
.
response
?.
data
?.
detail
||
err
.
response
?.
data
?.
message
||
err
.
message
||
fallback
return
err
.
response
?.
data
?.
detail
||
err
.
response
?.
data
?.
message
||
err
.
message
||
fallback
}
}
function
isCreateAccountRecoveryError
(
error
:
unknown
):
boolean
{
const
data
=
(
error
as
{
response
?:
{
data
?:
{
reason
?:
string
error
?:
string
code
?:
string
step
?:
string
intent
?:
string
}
}
}).
response
?.
data
const
states
=
[
data
?.
reason
,
data
?.
error
,
data
?.
code
,
data
?.
step
,
data
?.
intent
]
.
map
(
value
=>
value
?.
trim
().
toLowerCase
())
.
filter
((
value
):
value
is
string
=>
Boolean
(
value
))
return
states
.
includes
(
'
email_exists
'
)
||
states
.
includes
(
'
bind_login_required
'
)
||
states
.
includes
(
'
bind_login
'
)
||
states
.
includes
(
'
adopt_existing_user_by_email
'
)
}
async
function
finalizeCompletion
(
completion
:
PendingOAuthExchangeResponse
,
redirect
:
string
)
{
async
function
finalizeCompletion
(
completion
:
PendingOAuthExchangeResponse
,
redirect
:
string
)
{
if
(
getOAuthCompletionKind
(
completion
)
===
'
bind
'
)
{
if
(
getOAuthCompletionKind
(
completion
)
===
'
bind
'
)
{
const
bindRedirect
=
sanitizeRedirectPath
(
completion
.
redirect
||
'
/profile
'
)
const
bindRedirect
=
sanitizeRedirectPath
(
completion
.
redirect
||
'
/profile
'
)
...
@@ -540,10 +562,15 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
...
@@ -540,10 +562,15 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
email
:
payload
.
email
,
email
:
payload
.
email
,
password
:
payload
.
password
,
password
:
payload
.
password
,
verify_code
:
payload
.
verifyCode
||
undefined
,
verify_code
:
payload
.
verifyCode
||
undefined
,
invitation_code
:
payload
.
invitationCode
||
undefined
,
...
serializeAdoptionDecision
(
currentAdoptionDecision
())
...
serializeAdoptionDecision
(
currentAdoptionDecision
())
})
})
await
finalizePendingAccountResponse
(
data
)
await
finalizePendingAccountResponse
(
data
)
}
catch
(
e
:
unknown
)
{
}
catch
(
e
:
unknown
)
{
if
(
isCreateAccountRecoveryError
(
e
))
{
switchToBindLoginMode
(
payload
.
email
)
return
}
accountActionError
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
accountActionError
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
}
finally
{
}
finally
{
isSubmitting
.
value
=
false
isSubmitting
.
value
=
false
...
...
frontend/src/views/auth/OidcCallbackView.vue
View file @
dcd5c43d
...
@@ -488,6 +488,28 @@ function getRequestErrorMessage(error: unknown, fallback: string): string {
...
@@ -488,6 +488,28 @@ function getRequestErrorMessage(error: unknown, fallback: string): string {
return
err
.
response
?.
data
?.
detail
||
err
.
response
?.
data
?.
message
||
err
.
message
||
fallback
return
err
.
response
?.
data
?.
detail
||
err
.
response
?.
data
?.
message
||
err
.
message
||
fallback
}
}
function
isCreateAccountRecoveryError
(
error
:
unknown
):
boolean
{
const
data
=
(
error
as
{
response
?:
{
data
?:
{
reason
?:
string
error
?:
string
code
?:
string
step
?:
string
intent
?:
string
}
}
}
).
response
?.
data
const
states
=
[
data
?.
reason
,
data
?.
error
,
data
?.
code
,
data
?.
step
,
data
?.
intent
]
.
map
(
value
=>
value
?.
trim
().
toLowerCase
())
.
filter
((
value
):
value
is
string
=>
Boolean
(
value
))
return
states
.
includes
(
'
email_exists
'
)
||
states
.
includes
(
'
bind_login_required
'
)
||
states
.
includes
(
'
bind_login
'
)
||
states
.
includes
(
'
adopt_existing_user_by_email
'
)
}
async
function
finalizeCompletion
(
completion
:
PendingOAuthExchangeResponse
,
redirect
:
string
)
{
async
function
finalizeCompletion
(
completion
:
PendingOAuthExchangeResponse
,
redirect
:
string
)
{
if
(
getOAuthCompletionKind
(
completion
)
===
'
bind
'
)
{
if
(
getOAuthCompletionKind
(
completion
)
===
'
bind
'
)
{
const
bindRedirect
=
sanitizeRedirectPath
(
completion
.
redirect
||
'
/profile
'
)
const
bindRedirect
=
sanitizeRedirectPath
(
completion
.
redirect
||
'
/profile
'
)
...
@@ -584,10 +606,15 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
...
@@ -584,10 +606,15 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
email
:
payload
.
email
,
email
:
payload
.
email
,
password
:
payload
.
password
,
password
:
payload
.
password
,
verify_code
:
payload
.
verifyCode
||
undefined
,
verify_code
:
payload
.
verifyCode
||
undefined
,
invitation_code
:
payload
.
invitationCode
||
undefined
,
...
serializeAdoptionDecision
(
currentAdoptionDecision
())
...
serializeAdoptionDecision
(
currentAdoptionDecision
())
}
)
}
)
await
finalizePendingAccountResponse
(
data
)
await
finalizePendingAccountResponse
(
data
)
}
catch
(
e
:
unknown
)
{
}
catch
(
e
:
unknown
)
{
if
(
isCreateAccountRecoveryError
(
e
))
{
switchToBindLoginMode
(
payload
.
email
)
return
}
accountActionError
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
accountActionError
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
}
finally
{
}
finally
{
isSubmitting
.
value
=
false
isSubmitting
.
value
=
false
...
...
frontend/src/views/auth/WechatCallbackView.vue
View file @
dcd5c43d
...
@@ -647,6 +647,28 @@ function getRequestErrorMessage(error: unknown, fallback: string): string {
...
@@ -647,6 +647,28 @@ function getRequestErrorMessage(error: unknown, fallback: string): string {
return
err
.
response
?.
data
?.
detail
||
err
.
response
?.
data
?.
message
||
err
.
message
||
fallback
return
err
.
response
?.
data
?.
detail
||
err
.
response
?.
data
?.
message
||
err
.
message
||
fallback
}
}
function
isCreateAccountRecoveryError
(
error
:
unknown
):
boolean
{
const
data
=
(
error
as
{
response
?:
{
data
?:
{
reason
?:
string
error
?:
string
code
?:
string
step
?:
string
intent
?:
string
}
}
}
).
response
?.
data
const
states
=
[
data
?.
reason
,
data
?.
error
,
data
?.
code
,
data
?.
step
,
data
?.
intent
]
.
map
(
value
=>
value
?.
trim
().
toLowerCase
())
.
filter
((
value
):
value
is
string
=>
Boolean
(
value
))
return
states
.
includes
(
'
email_exists
'
)
||
states
.
includes
(
'
bind_login_required
'
)
||
states
.
includes
(
'
bind_login
'
)
||
states
.
includes
(
'
adopt_existing_user_by_email
'
)
}
async
function
finalizeCompletion
(
completion
:
PendingOAuthExchangeResponse
,
redirect
:
string
)
{
async
function
finalizeCompletion
(
completion
:
PendingOAuthExchangeResponse
,
redirect
:
string
)
{
if
(
getOAuthCompletionKind
(
completion
)
===
'
bind
'
)
{
if
(
getOAuthCompletionKind
(
completion
)
===
'
bind
'
)
{
const
bindRedirect
=
sanitizeRedirectPath
(
completion
.
redirect
||
'
/profile
'
)
const
bindRedirect
=
sanitizeRedirectPath
(
completion
.
redirect
||
'
/profile
'
)
...
@@ -739,10 +761,15 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
...
@@ -739,10 +761,15 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
email
:
payload
.
email
,
email
:
payload
.
email
,
password
:
payload
.
password
,
password
:
payload
.
password
,
verify_code
:
payload
.
verifyCode
||
undefined
,
verify_code
:
payload
.
verifyCode
||
undefined
,
invitation_code
:
payload
.
invitationCode
||
undefined
,
...
serializeAdoptionDecision
(
currentAdoptionDecision
())
...
serializeAdoptionDecision
(
currentAdoptionDecision
())
}
)
}
)
await
finalizePendingAccountResponse
(
data
)
await
finalizePendingAccountResponse
(
data
)
}
catch
(
e
:
unknown
)
{
}
catch
(
e
:
unknown
)
{
if
(
isCreateAccountRecoveryError
(
e
))
{
switchToBindLoginMode
(
payload
.
email
)
return
}
accountActionError
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
accountActionError
.
value
=
getRequestErrorMessage
(
e
,
t
(
'
auth.loginFailed
'
))
}
finally
{
}
finally
{
isSubmitting
.
value
=
false
isSubmitting
.
value
=
false
...
...
frontend/src/views/auth/__tests__/EmailVerifyView.spec.ts
View file @
dcd5c43d
...
@@ -11,6 +11,7 @@ const {
...
@@ -11,6 +11,7 @@ const {
clearPendingAuthSessionMock
,
clearPendingAuthSessionMock
,
getPublicSettingsMock
,
getPublicSettingsMock
,
sendVerifyCodeMock
,
sendVerifyCodeMock
,
sendPendingOAuthVerifyCodeMock
,
persistOAuthTokenContextMock
,
persistOAuthTokenContextMock
,
apiClientPostMock
,
apiClientPostMock
,
authStoreState
,
authStoreState
,
...
@@ -23,6 +24,7 @@ const {
...
@@ -23,6 +24,7 @@ const {
clearPendingAuthSessionMock
:
vi
.
fn
(),
clearPendingAuthSessionMock
:
vi
.
fn
(),
getPublicSettingsMock
:
vi
.
fn
(),
getPublicSettingsMock
:
vi
.
fn
(),
sendVerifyCodeMock
:
vi
.
fn
(),
sendVerifyCodeMock
:
vi
.
fn
(),
sendPendingOAuthVerifyCodeMock
:
vi
.
fn
(),
persistOAuthTokenContextMock
:
vi
.
fn
(),
persistOAuthTokenContextMock
:
vi
.
fn
(),
apiClientPostMock
:
vi
.
fn
(),
apiClientPostMock
:
vi
.
fn
(),
authStoreState
:
{
authStoreState
:
{
...
@@ -80,6 +82,7 @@ vi.mock('@/api/auth', async () => {
...
@@ -80,6 +82,7 @@ vi.mock('@/api/auth', async () => {
...
actual
,
...
actual
,
getPublicSettings
:
(...
args
:
any
[])
=>
getPublicSettingsMock
(...
args
),
getPublicSettings
:
(...
args
:
any
[])
=>
getPublicSettingsMock
(...
args
),
sendVerifyCode
:
(...
args
:
any
[])
=>
sendVerifyCodeMock
(...
args
),
sendVerifyCode
:
(...
args
:
any
[])
=>
sendVerifyCodeMock
(...
args
),
sendPendingOAuthVerifyCode
:
(...
args
:
any
[])
=>
sendPendingOAuthVerifyCodeMock
(...
args
),
persistOAuthTokenContext
:
(...
args
:
any
[])
=>
persistOAuthTokenContextMock
(...
args
),
persistOAuthTokenContext
:
(...
args
:
any
[])
=>
persistOAuthTokenContextMock
(...
args
),
}
}
})
})
...
@@ -100,6 +103,7 @@ describe('EmailVerifyView', () => {
...
@@ -100,6 +103,7 @@ describe('EmailVerifyView', () => {
clearPendingAuthSessionMock
.
mockReset
()
clearPendingAuthSessionMock
.
mockReset
()
getPublicSettingsMock
.
mockReset
()
getPublicSettingsMock
.
mockReset
()
sendVerifyCodeMock
.
mockReset
()
sendVerifyCodeMock
.
mockReset
()
sendPendingOAuthVerifyCodeMock
.
mockReset
()
persistOAuthTokenContextMock
.
mockReset
()
persistOAuthTokenContextMock
.
mockReset
()
apiClientPostMock
.
mockReset
()
apiClientPostMock
.
mockReset
()
authStoreState
.
pendingAuthSession
=
null
authStoreState
.
pendingAuthSession
=
null
...
@@ -112,9 +116,86 @@ describe('EmailVerifyView', () => {
...
@@ -112,9 +116,86 @@ describe('EmailVerifyView', () => {
registration_email_suffix_whitelist
:
[],
registration_email_suffix_whitelist
:
[],
})
})
sendVerifyCodeMock
.
mockResolvedValue
({
countdown
:
60
})
sendVerifyCodeMock
.
mockResolvedValue
({
countdown
:
60
})
sendPendingOAuthVerifyCodeMock
.
mockResolvedValue
({
countdown
:
60
})
setTokenMock
.
mockResolvedValue
({})
setTokenMock
.
mockResolvedValue
({})
})
})
it
(
'
uses the pending oauth verify-code endpoint when register data carries a pending auth session
'
,
async
()
=>
{
authStoreState
.
pendingAuthSession
=
{
token
:
'
pending-token-1
'
,
token_field
:
'
pending_auth_token
'
,
provider
:
'
wechat
'
,
redirect
:
'
/profile
'
,
}
sessionStorage
.
setItem
(
'
register_data
'
,
JSON
.
stringify
({
email
:
'
fresh@example.com
'
,
password
:
'
secret-123
'
,
})
)
mount
(
EmailVerifyView
,
{
global
:
{
stubs
:
{
AuthLayout
:
{
template
:
'
<div><slot /><slot name="footer" /></div>
'
},
Icon
:
true
,
TurnstileWidget
:
true
,
transition
:
false
,
},
},
})
await
flushPromises
()
expect
(
sendPendingOAuthVerifyCodeMock
).
toHaveBeenCalledWith
({
email
:
'
fresh@example.com
'
,
pending_auth_token
:
'
pending-token-1
'
,
})
expect
(
sendVerifyCodeMock
).
not
.
toHaveBeenCalled
()
})
it
(
'
skips the registration email suffix whitelist for pending oauth verification
'
,
async
()
=>
{
authStoreState
.
pendingAuthSession
=
{
token
:
'
pending-token-2
'
,
token_field
:
'
pending_auth_token
'
,
provider
:
'
oidc
'
,
redirect
:
'
/profile
'
,
}
getPublicSettingsMock
.
mockResolvedValue
({
turnstile_enabled
:
false
,
turnstile_site_key
:
''
,
site_name
:
'
Sub2API
'
,
registration_email_suffix_whitelist
:
[
'
allowed.com
'
],
})
sessionStorage
.
setItem
(
'
register_data
'
,
JSON
.
stringify
({
email
:
'
fresh@example.com
'
,
password
:
'
secret-123
'
,
})
)
mount
(
EmailVerifyView
,
{
global
:
{
stubs
:
{
AuthLayout
:
{
template
:
'
<div><slot /><slot name="footer" /></div>
'
},
Icon
:
true
,
TurnstileWidget
:
true
,
transition
:
false
,
},
},
})
await
flushPromises
()
expect
(
sendPendingOAuthVerifyCodeMock
).
toHaveBeenCalledWith
({
email
:
'
fresh@example.com
'
,
pending_auth_token
:
'
pending-token-2
'
,
})
expect
(
showErrorMock
).
not
.
toHaveBeenCalled
()
})
it
(
'
submits pending auth account creation when session storage has no pending metadata but auth store does
'
,
async
()
=>
{
it
(
'
submits pending auth account creation when session storage has no pending metadata but auth store does
'
,
async
()
=>
{
authStoreState
.
pendingAuthSession
=
{
authStoreState
.
pendingAuthSession
=
{
token
:
'
pending-token-1
'
,
token
:
'
pending-token-1
'
,
...
...
frontend/src/views/auth/__tests__/LinuxDoCallbackView.spec.ts
View file @
dcd5c43d
...
@@ -15,6 +15,7 @@ const getPublicSettings = vi.fn()
...
@@ -15,6 +15,7 @@ const getPublicSettings = vi.fn()
const
login2FA
=
vi
.
fn
()
const
login2FA
=
vi
.
fn
()
const
apiClientPost
=
vi
.
fn
()
const
apiClientPost
=
vi
.
fn
()
const
sendVerifyCode
=
vi
.
fn
()
const
sendVerifyCode
=
vi
.
fn
()
const
sendPendingOAuthVerifyCode
=
vi
.
fn
()
vi
.
mock
(
'
vue-router
'
,
()
=>
({
vi
.
mock
(
'
vue-router
'
,
()
=>
({
useRoute
:
()
=>
({
useRoute
:
()
=>
({
...
@@ -61,7 +62,8 @@ vi.mock('@/api/auth', async () => {
...
@@ -61,7 +62,8 @@ vi.mock('@/api/auth', async () => {
completeLinuxDoOAuthRegistration
:
(...
args
:
any
[])
=>
completeLinuxDoOAuthRegistration
(...
args
),
completeLinuxDoOAuthRegistration
:
(...
args
:
any
[])
=>
completeLinuxDoOAuthRegistration
(...
args
),
getPublicSettings
:
(...
args
:
any
[])
=>
getPublicSettings
(...
args
),
getPublicSettings
:
(...
args
:
any
[])
=>
getPublicSettings
(...
args
),
login2FA
:
(...
args
:
any
[])
=>
login2FA
(...
args
),
login2FA
:
(...
args
:
any
[])
=>
login2FA
(...
args
),
sendVerifyCode
:
(...
args
:
any
[])
=>
sendVerifyCode
(...
args
)
sendVerifyCode
:
(...
args
:
any
[])
=>
sendVerifyCode
(...
args
),
sendPendingOAuthVerifyCode
:
(...
args
:
any
[])
=>
sendPendingOAuthVerifyCode
(...
args
)
}
}
})
})
...
@@ -79,6 +81,7 @@ describe('LinuxDoCallbackView', () => {
...
@@ -79,6 +81,7 @@ describe('LinuxDoCallbackView', () => {
login2FA
.
mockReset
()
login2FA
.
mockReset
()
apiClientPost
.
mockReset
()
apiClientPost
.
mockReset
()
sendVerifyCode
.
mockReset
()
sendVerifyCode
.
mockReset
()
sendPendingOAuthVerifyCode
.
mockReset
()
getPublicSettings
.
mockResolvedValue
({
getPublicSettings
.
mockResolvedValue
({
turnstile_enabled
:
false
,
turnstile_enabled
:
false
,
turnstile_site_key
:
''
turnstile_site_key
:
''
...
@@ -334,6 +337,11 @@ describe('LinuxDoCallbackView', () => {
...
@@ -334,6 +337,11 @@ describe('LinuxDoCallbackView', () => {
})
})
it
(
'
collects email, password, and verify code 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
()
=>
{
getPublicSettings
.
mockResolvedValue
({
invitation_code_enabled
:
true
,
turnstile_enabled
:
false
,
turnstile_site_key
:
''
})
exchangePendingOAuthCompletion
.
mockResolvedValue
({
exchangePendingOAuthCompletion
.
mockResolvedValue
({
error
:
'
email_required
'
,
error
:
'
email_required
'
,
redirect
:
'
/welcome
'
,
redirect
:
'
/welcome
'
,
...
@@ -370,6 +378,7 @@ describe('LinuxDoCallbackView', () => {
...
@@ -370,6 +378,7 @@ describe('LinuxDoCallbackView', () => {
await
wrapper
.
get
(
'
[data-testid="linuxdo-create-account-email"]
'
).
setValue
(
'
new@example.com
'
)
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-password"]
'
).
setValue
(
'
secret-123
'
)
await
wrapper
.
get
(
'
[data-testid="linuxdo-create-account-verify-code"]
'
).
setValue
(
'
246810
'
)
await
wrapper
.
get
(
'
[data-testid="linuxdo-create-account-verify-code"]
'
).
setValue
(
'
246810
'
)
await
wrapper
.
get
(
'
[data-testid="linuxdo-create-account-invitation-code"]
'
).
setValue
(
'
INVITE123
'
)
await
wrapper
.
get
(
'
[data-testid="linuxdo-create-account-submit"]
'
).
trigger
(
'
click
'
)
await
wrapper
.
get
(
'
[data-testid="linuxdo-create-account-submit"]
'
).
trigger
(
'
click
'
)
await
flushPromises
()
await
flushPromises
()
...
@@ -377,6 +386,7 @@ describe('LinuxDoCallbackView', () => {
...
@@ -377,6 +386,7 @@ describe('LinuxDoCallbackView', () => {
email
:
'
new@example.com
'
,
email
:
'
new@example.com
'
,
password
:
'
secret-123
'
,
password
:
'
secret-123
'
,
verify_code
:
'
246810
'
,
verify_code
:
'
246810
'
,
invitation_code
:
'
INVITE123
'
,
adopt_display_name
:
true
,
adopt_display_name
:
true
,
adopt_avatar
:
false
adopt_avatar
:
false
})
})
...
@@ -384,12 +394,48 @@ describe('LinuxDoCallbackView', () => {
...
@@ -384,12 +394,48 @@ describe('LinuxDoCallbackView', () => {
expect
(
replace
).
toHaveBeenCalledWith
(
'
/welcome
'
)
expect
(
replace
).
toHaveBeenCalledWith
(
'
/welcome
'
)
})
})
it
(
'
switches to bind-login when create-account returns EMAIL_EXISTS
'
,
async
()
=>
{
exchangePendingOAuthCompletion
.
mockResolvedValue
({
error
:
'
email_required
'
,
redirect
:
'
/welcome
'
})
apiClientPost
.
mockRejectedValue
({
response
:
{
data
:
{
reason
:
'
EMAIL_EXISTS
'
,
message
:
'
email already exists
'
}
}
})
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
(
'
existing@example.com
'
)
await
wrapper
.
get
(
'
[data-testid="linuxdo-create-account-password"]
'
).
setValue
(
'
secret-123
'
)
await
wrapper
.
get
(
'
[data-testid="linuxdo-create-account-submit"]
'
).
trigger
(
'
click
'
)
await
flushPromises
()
expect
((
wrapper
.
get
(
'
[data-testid="linuxdo-bind-login-email"]
'
).
element
as
HTMLInputElement
).
value
).
toBe
(
'
existing@example.com
'
)
})
it
(
'
sends a verify code for pending oauth account creation
'
,
async
()
=>
{
it
(
'
sends a verify code for pending oauth account creation
'
,
async
()
=>
{
exchangePendingOAuthCompletion
.
mockResolvedValue
({
exchangePendingOAuthCompletion
.
mockResolvedValue
({
error
:
'
email_required
'
,
error
:
'
email_required
'
,
redirect
:
'
/welcome
'
redirect
:
'
/welcome
'
})
})
sendVerifyCode
.
mockResolvedValue
({
send
PendingOAuth
VerifyCode
.
mockResolvedValue
({
message
:
'
sent
'
,
message
:
'
sent
'
,
countdown
:
60
countdown
:
60
})
})
...
@@ -411,7 +457,7 @@ describe('LinuxDoCallbackView', () => {
...
@@ -411,7 +457,7 @@ describe('LinuxDoCallbackView', () => {
await
wrapper
.
get
(
'
[data-testid="linuxdo-create-account-send-code"]
'
).
trigger
(
'
click
'
)
await
wrapper
.
get
(
'
[data-testid="linuxdo-create-account-send-code"]
'
).
trigger
(
'
click
'
)
await
flushPromises
()
await
flushPromises
()
expect
(
sendVerifyCode
).
toHaveBeenCalledWith
({
expect
(
send
PendingOAuth
VerifyCode
).
toHaveBeenCalledWith
({
email
:
'
new@example.com
'
email
:
'
new@example.com
'
})
})
})
})
...
...
frontend/src/views/auth/__tests__/OidcCallbackView.spec.ts
View file @
dcd5c43d
...
@@ -15,6 +15,7 @@ const getPublicSettings = vi.fn()
...
@@ -15,6 +15,7 @@ const getPublicSettings = vi.fn()
const
login2FA
=
vi
.
fn
()
const
login2FA
=
vi
.
fn
()
const
apiClientPost
=
vi
.
fn
()
const
apiClientPost
=
vi
.
fn
()
const
sendVerifyCode
=
vi
.
fn
()
const
sendVerifyCode
=
vi
.
fn
()
const
sendPendingOAuthVerifyCode
=
vi
.
fn
()
vi
.
mock
(
'
vue-router
'
,
()
=>
({
vi
.
mock
(
'
vue-router
'
,
()
=>
({
useRoute
:
()
=>
({
useRoute
:
()
=>
({
...
@@ -66,7 +67,8 @@ vi.mock('@/api/auth', async () => {
...
@@ -66,7 +67,8 @@ vi.mock('@/api/auth', async () => {
completeOIDCOAuthRegistration
:
(...
args
:
any
[])
=>
completeOIDCOAuthRegistration
(...
args
),
completeOIDCOAuthRegistration
:
(...
args
:
any
[])
=>
completeOIDCOAuthRegistration
(...
args
),
getPublicSettings
:
(...
args
:
any
[])
=>
getPublicSettings
(...
args
),
getPublicSettings
:
(...
args
:
any
[])
=>
getPublicSettings
(...
args
),
login2FA
:
(...
args
:
any
[])
=>
login2FA
(...
args
),
login2FA
:
(...
args
:
any
[])
=>
login2FA
(...
args
),
sendVerifyCode
:
(...
args
:
any
[])
=>
sendVerifyCode
(...
args
)
sendVerifyCode
:
(...
args
:
any
[])
=>
sendVerifyCode
(...
args
),
sendPendingOAuthVerifyCode
:
(...
args
:
any
[])
=>
sendPendingOAuthVerifyCode
(...
args
)
}
}
})
})
...
@@ -84,6 +86,7 @@ describe('OidcCallbackView', () => {
...
@@ -84,6 +86,7 @@ describe('OidcCallbackView', () => {
login2FA
.
mockReset
()
login2FA
.
mockReset
()
apiClientPost
.
mockReset
()
apiClientPost
.
mockReset
()
sendVerifyCode
.
mockReset
()
sendVerifyCode
.
mockReset
()
sendPendingOAuthVerifyCode
.
mockReset
()
getPublicSettings
.
mockResolvedValue
({
getPublicSettings
.
mockResolvedValue
({
oidc_oauth_provider_name
:
'
ExampleID
'
,
oidc_oauth_provider_name
:
'
ExampleID
'
,
turnstile_enabled
:
false
,
turnstile_enabled
:
false
,
...
@@ -312,6 +315,12 @@ describe('OidcCallbackView', () => {
...
@@ -312,6 +315,12 @@ describe('OidcCallbackView', () => {
})
})
it
(
'
collects email, password, and verify code 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
()
=>
{
getPublicSettings
.
mockResolvedValue
({
oidc_oauth_provider_name
:
'
ExampleID
'
,
invitation_code_enabled
:
true
,
turnstile_enabled
:
false
,
turnstile_site_key
:
''
})
exchangePendingOAuthCompletion
.
mockResolvedValue
({
exchangePendingOAuthCompletion
.
mockResolvedValue
({
error
:
'
email_required
'
,
error
:
'
email_required
'
,
redirect
:
'
/welcome
'
,
redirect
:
'
/welcome
'
,
...
@@ -348,6 +357,7 @@ describe('OidcCallbackView', () => {
...
@@ -348,6 +357,7 @@ describe('OidcCallbackView', () => {
await
wrapper
.
get
(
'
[data-testid="oidc-create-account-email"]
'
).
setValue
(
'
new@example.com
'
)
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-password"]
'
).
setValue
(
'
secret-123
'
)
await
wrapper
.
get
(
'
[data-testid="oidc-create-account-verify-code"]
'
).
setValue
(
'
246810
'
)
await
wrapper
.
get
(
'
[data-testid="oidc-create-account-verify-code"]
'
).
setValue
(
'
246810
'
)
await
wrapper
.
get
(
'
[data-testid="oidc-create-account-invitation-code"]
'
).
setValue
(
'
INVITE123
'
)
await
wrapper
.
get
(
'
[data-testid="oidc-create-account-submit"]
'
).
trigger
(
'
click
'
)
await
wrapper
.
get
(
'
[data-testid="oidc-create-account-submit"]
'
).
trigger
(
'
click
'
)
await
flushPromises
()
await
flushPromises
()
...
@@ -355,6 +365,7 @@ describe('OidcCallbackView', () => {
...
@@ -355,6 +365,7 @@ describe('OidcCallbackView', () => {
email
:
'
new@example.com
'
,
email
:
'
new@example.com
'
,
password
:
'
secret-123
'
,
password
:
'
secret-123
'
,
verify_code
:
'
246810
'
,
verify_code
:
'
246810
'
,
invitation_code
:
'
INVITE123
'
,
adopt_display_name
:
true
,
adopt_display_name
:
true
,
adopt_avatar
:
false
adopt_avatar
:
false
})
})
...
@@ -362,12 +373,48 @@ describe('OidcCallbackView', () => {
...
@@ -362,12 +373,48 @@ describe('OidcCallbackView', () => {
expect
(
replace
).
toHaveBeenCalledWith
(
'
/welcome
'
)
expect
(
replace
).
toHaveBeenCalledWith
(
'
/welcome
'
)
})
})
it
(
'
switches to bind-login when create-account returns EMAIL_EXISTS
'
,
async
()
=>
{
exchangePendingOAuthCompletion
.
mockResolvedValue
({
error
:
'
email_required
'
,
redirect
:
'
/welcome
'
})
apiClientPost
.
mockRejectedValue
({
response
:
{
data
:
{
reason
:
'
EMAIL_EXISTS
'
,
message
:
'
email already exists
'
}
}
})
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
(
'
existing@example.com
'
)
await
wrapper
.
get
(
'
[data-testid="oidc-create-account-password"]
'
).
setValue
(
'
secret-123
'
)
await
wrapper
.
get
(
'
[data-testid="oidc-create-account-submit"]
'
).
trigger
(
'
click
'
)
await
flushPromises
()
expect
((
wrapper
.
get
(
'
[data-testid="oidc-bind-login-email"]
'
).
element
as
HTMLInputElement
).
value
).
toBe
(
'
existing@example.com
'
)
})
it
(
'
sends a verify code for pending oauth account creation
'
,
async
()
=>
{
it
(
'
sends a verify code for pending oauth account creation
'
,
async
()
=>
{
exchangePendingOAuthCompletion
.
mockResolvedValue
({
exchangePendingOAuthCompletion
.
mockResolvedValue
({
error
:
'
email_required
'
,
error
:
'
email_required
'
,
redirect
:
'
/welcome
'
redirect
:
'
/welcome
'
})
})
sendVerifyCode
.
mockResolvedValue
({
send
PendingOAuth
VerifyCode
.
mockResolvedValue
({
message
:
'
sent
'
,
message
:
'
sent
'
,
countdown
:
60
countdown
:
60
})
})
...
@@ -389,7 +436,7 @@ describe('OidcCallbackView', () => {
...
@@ -389,7 +436,7 @@ describe('OidcCallbackView', () => {
await
wrapper
.
get
(
'
[data-testid="oidc-create-account-send-code"]
'
).
trigger
(
'
click
'
)
await
wrapper
.
get
(
'
[data-testid="oidc-create-account-send-code"]
'
).
trigger
(
'
click
'
)
await
flushPromises
()
await
flushPromises
()
expect
(
sendVerifyCode
).
toHaveBeenCalledWith
({
expect
(
send
PendingOAuth
VerifyCode
).
toHaveBeenCalledWith
({
email
:
'
new@example.com
'
email
:
'
new@example.com
'
})
})
})
})
...
...
frontend/src/views/auth/__tests__/WechatCallbackView.spec.ts
View file @
dcd5c43d
...
@@ -8,6 +8,8 @@ const {
...
@@ -8,6 +8,8 @@ const {
login2FAMock
,
login2FAMock
,
apiClientPostMock
,
apiClientPostMock
,
sendVerifyCodeMock
,
sendVerifyCodeMock
,
sendPendingOAuthVerifyCodeMock
,
getPublicSettingsMock
,
prepareOAuthBindAccessTokenCookieMock
,
prepareOAuthBindAccessTokenCookieMock
,
getAuthTokenMock
,
getAuthTokenMock
,
replaceMock
,
replaceMock
,
...
@@ -24,6 +26,8 @@ const {
...
@@ -24,6 +26,8 @@ const {
login2FAMock
:
vi
.
fn
(),
login2FAMock
:
vi
.
fn
(),
apiClientPostMock
:
vi
.
fn
(),
apiClientPostMock
:
vi
.
fn
(),
sendVerifyCodeMock
:
vi
.
fn
(),
sendVerifyCodeMock
:
vi
.
fn
(),
sendPendingOAuthVerifyCodeMock
:
vi
.
fn
(),
getPublicSettingsMock
:
vi
.
fn
(),
prepareOAuthBindAccessTokenCookieMock
:
vi
.
fn
(),
prepareOAuthBindAccessTokenCookieMock
:
vi
.
fn
(),
getAuthTokenMock
:
vi
.
fn
(),
getAuthTokenMock
:
vi
.
fn
(),
replaceMock
:
vi
.
fn
(),
replaceMock
:
vi
.
fn
(),
...
@@ -130,6 +134,8 @@ vi.mock('@/api/auth', async () => {
...
@@ -130,6 +134,8 @@ vi.mock('@/api/auth', async () => {
completeWeChatOAuthRegistration
:
(...
args
:
any
[])
=>
completeWeChatOAuthRegistrationMock
(...
args
),
completeWeChatOAuthRegistration
:
(...
args
:
any
[])
=>
completeWeChatOAuthRegistrationMock
(...
args
),
login2FA
:
(...
args
:
any
[])
=>
login2FAMock
(...
args
),
login2FA
:
(...
args
:
any
[])
=>
login2FAMock
(...
args
),
sendVerifyCode
:
(...
args
:
any
[])
=>
sendVerifyCodeMock
(...
args
),
sendVerifyCode
:
(...
args
:
any
[])
=>
sendVerifyCodeMock
(...
args
),
sendPendingOAuthVerifyCode
:
(...
args
:
any
[])
=>
sendPendingOAuthVerifyCodeMock
(...
args
),
getPublicSettings
:
(...
args
:
any
[])
=>
getPublicSettingsMock
(...
args
),
prepareOAuthBindAccessTokenCookie
:
(...
args
:
any
[])
=>
prepareOAuthBindAccessTokenCookieMock
(...
args
),
prepareOAuthBindAccessTokenCookie
:
(...
args
:
any
[])
=>
prepareOAuthBindAccessTokenCookieMock
(...
args
),
getAuthToken
:
(...
args
:
any
[])
=>
getAuthTokenMock
(...
args
),
getAuthToken
:
(...
args
:
any
[])
=>
getAuthTokenMock
(...
args
),
}
}
...
@@ -142,6 +148,8 @@ describe('WechatCallbackView', () => {
...
@@ -142,6 +148,8 @@ describe('WechatCallbackView', () => {
login2FAMock
.
mockReset
()
login2FAMock
.
mockReset
()
apiClientPostMock
.
mockReset
()
apiClientPostMock
.
mockReset
()
sendVerifyCodeMock
.
mockReset
()
sendVerifyCodeMock
.
mockReset
()
sendPendingOAuthVerifyCodeMock
.
mockReset
()
getPublicSettingsMock
.
mockReset
()
replaceMock
.
mockReset
()
replaceMock
.
mockReset
()
setTokenMock
.
mockReset
()
setTokenMock
.
mockReset
()
showSuccessMock
.
mockReset
()
showSuccessMock
.
mockReset
()
...
@@ -167,6 +175,11 @@ describe('WechatCallbackView', () => {
...
@@ -167,6 +175,11 @@ describe('WechatCallbackView', () => {
configurable
:
true
,
configurable
:
true
,
value
:
'
Mozilla/5.0
'
,
value
:
'
Mozilla/5.0
'
,
})
})
getPublicSettingsMock
.
mockResolvedValue
({
invitation_code_enabled
:
false
,
turnstile_enabled
:
false
,
turnstile_site_key
:
''
,
})
})
})
it
(
'
overrides an incompatible query mode with the configured open capability during bind recovery
'
,
async
()
=>
{
it
(
'
overrides an incompatible query mode with the configured open capability during bind recovery
'
,
async
()
=>
{
...
@@ -478,6 +491,11 @@ describe('WechatCallbackView', () => {
...
@@ -478,6 +491,11 @@ describe('WechatCallbackView', () => {
})
})
it
(
'
collects email, password, and verify code 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
()
=>
{
getPublicSettingsMock
.
mockResolvedValue
({
invitation_code_enabled
:
true
,
turnstile_enabled
:
false
,
turnstile_site_key
:
''
,
})
exchangePendingOAuthCompletionMock
.
mockResolvedValue
({
exchangePendingOAuthCompletionMock
.
mockResolvedValue
({
error
:
'
email_required
'
,
error
:
'
email_required
'
,
redirect
:
'
/welcome
'
,
redirect
:
'
/welcome
'
,
...
@@ -514,6 +532,7 @@ describe('WechatCallbackView', () => {
...
@@ -514,6 +532,7 @@ describe('WechatCallbackView', () => {
await
wrapper
.
get
(
'
[data-testid="wechat-create-account-email"]
'
).
setValue
(
'
new@example.com
'
)
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-password"]
'
).
setValue
(
'
secret-123
'
)
await
wrapper
.
get
(
'
[data-testid="wechat-create-account-verify-code"]
'
).
setValue
(
'
246810
'
)
await
wrapper
.
get
(
'
[data-testid="wechat-create-account-verify-code"]
'
).
setValue
(
'
246810
'
)
await
wrapper
.
get
(
'
[data-testid="wechat-create-account-invitation-code"]
'
).
setValue
(
'
INVITE123
'
)
await
wrapper
.
get
(
'
[data-testid="wechat-create-account-submit"]
'
).
trigger
(
'
click
'
)
await
wrapper
.
get
(
'
[data-testid="wechat-create-account-submit"]
'
).
trigger
(
'
click
'
)
await
flushPromises
()
await
flushPromises
()
...
@@ -521,6 +540,7 @@ describe('WechatCallbackView', () => {
...
@@ -521,6 +540,7 @@ describe('WechatCallbackView', () => {
email
:
'
new@example.com
'
,
email
:
'
new@example.com
'
,
password
:
'
secret-123
'
,
password
:
'
secret-123
'
,
verify_code
:
'
246810
'
,
verify_code
:
'
246810
'
,
invitation_code
:
'
INVITE123
'
,
adopt_display_name
:
true
,
adopt_display_name
:
true
,
adopt_avatar
:
false
,
adopt_avatar
:
false
,
})
})
...
@@ -528,12 +548,48 @@ describe('WechatCallbackView', () => {
...
@@ -528,12 +548,48 @@ describe('WechatCallbackView', () => {
expect
(
replaceMock
).
toHaveBeenCalledWith
(
'
/welcome
'
)
expect
(
replaceMock
).
toHaveBeenCalledWith
(
'
/welcome
'
)
})
})
it
(
'
switches to bind-login when create-account returns EMAIL_EXISTS
'
,
async
()
=>
{
exchangePendingOAuthCompletionMock
.
mockResolvedValue
({
error
:
'
email_required
'
,
redirect
:
'
/welcome
'
,
})
apiClientPostMock
.
mockRejectedValue
({
response
:
{
data
:
{
reason
:
'
EMAIL_EXISTS
'
,
message
:
'
email already exists
'
,
},
},
})
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
(
'
existing@example.com
'
)
await
wrapper
.
get
(
'
[data-testid="wechat-create-account-password"]
'
).
setValue
(
'
secret-123
'
)
await
wrapper
.
get
(
'
[data-testid="wechat-create-account-submit"]
'
).
trigger
(
'
click
'
)
await
flushPromises
()
expect
((
wrapper
.
get
(
'
[data-testid="wechat-bind-login-email"]
'
).
element
as
HTMLInputElement
).
value
).
toBe
(
'
existing@example.com
'
)
})
it
(
'
sends a verify code for pending oauth account creation
'
,
async
()
=>
{
it
(
'
sends a verify code for pending oauth account creation
'
,
async
()
=>
{
exchangePendingOAuthCompletionMock
.
mockResolvedValue
({
exchangePendingOAuthCompletionMock
.
mockResolvedValue
({
error
:
'
email_required
'
,
error
:
'
email_required
'
,
redirect
:
'
/welcome
'
,
redirect
:
'
/welcome
'
,
})
})
sendVerifyCodeMock
.
mockResolvedValue
({
send
PendingOAuth
VerifyCodeMock
.
mockResolvedValue
({
message
:
'
sent
'
,
message
:
'
sent
'
,
countdown
:
60
,
countdown
:
60
,
})
})
...
@@ -555,7 +611,7 @@ describe('WechatCallbackView', () => {
...
@@ -555,7 +611,7 @@ describe('WechatCallbackView', () => {
await
wrapper
.
get
(
'
[data-testid="wechat-create-account-send-code"]
'
).
trigger
(
'
click
'
)
await
wrapper
.
get
(
'
[data-testid="wechat-create-account-send-code"]
'
).
trigger
(
'
click
'
)
await
flushPromises
()
await
flushPromises
()
expect
(
sendVerifyCodeMock
).
toHaveBeenCalledWith
({
expect
(
send
PendingOAuth
VerifyCodeMock
).
toHaveBeenCalledWith
({
email
:
'
new@example.com
'
,
email
:
'
new@example.com
'
,
})
})
})
})
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment