feat: add oauth callback email binding ui

This commit is contained in:
IanShaw027
2026-04-20 19:30:19 +08:00
parent 6a75bd77e3
commit 6ea3f42e2f
10 changed files with 916 additions and 36 deletions

View File

@@ -5,14 +5,19 @@ import WechatCallbackView from '@/views/auth/WechatCallbackView.vue'
const {
exchangePendingOAuthCompletionMock,
completeWeChatOAuthRegistrationMock,
prepareOAuthBindAccessTokenCookieMock,
getAuthTokenMock,
replaceMock,
setTokenMock,
showSuccessMock,
showErrorMock,
routeState,
locationState,
} = vi.hoisted(() => ({
exchangePendingOAuthCompletionMock: vi.fn(),
completeWeChatOAuthRegistrationMock: vi.fn(),
prepareOAuthBindAccessTokenCookieMock: vi.fn(),
getAuthTokenMock: vi.fn(),
replaceMock: vi.fn(),
setTokenMock: vi.fn(),
showSuccessMock: vi.fn(),
@@ -20,6 +25,14 @@ const {
routeState: {
query: {} as Record<string, unknown>,
},
locationState: {
current: {
href: 'http://localhost/auth/wechat/callback',
hash: '',
search: '',
pathname: '/auth/wechat/callback'
} as { href: string; hash: string; search: string; pathname: string },
},
}))
vi.mock('vue-router', () => ({
@@ -94,6 +107,8 @@ vi.mock('@/api/auth', async () => {
...actual,
exchangePendingOAuthCompletion: (...args: any[]) => exchangePendingOAuthCompletionMock(...args),
completeWeChatOAuthRegistration: (...args: any[]) => completeWeChatOAuthRegistrationMock(...args),
prepareOAuthBindAccessTokenCookie: (...args: any[]) => prepareOAuthBindAccessTokenCookieMock(...args),
getAuthToken: (...args: any[]) => getAuthTokenMock(...args),
}
})
@@ -105,8 +120,24 @@ describe('WechatCallbackView', () => {
setTokenMock.mockReset()
showSuccessMock.mockReset()
showErrorMock.mockReset()
prepareOAuthBindAccessTokenCookieMock.mockReset()
getAuthTokenMock.mockReset()
routeState.query = {}
localStorage.clear()
locationState.current = {
href: 'http://localhost/auth/wechat/callback',
hash: '',
search: '',
pathname: '/auth/wechat/callback'
}
Object.defineProperty(window, 'location', {
configurable: true,
value: locationState.current,
})
Object.defineProperty(window.navigator, 'userAgent', {
configurable: true,
value: 'Mozilla/5.0',
})
})
it('does not send adoption decisions during the initial exchange', async () => {
@@ -269,4 +300,61 @@ describe('WechatCallbackView', () => {
expect(setTokenMock).toHaveBeenCalledWith('wechat-invite-token')
expect(replaceMock).toHaveBeenCalledWith('/subscriptions')
})
it('offers existing-account email collection during invitation flow', async () => {
exchangePendingOAuthCompletionMock.mockResolvedValue({
error: 'invitation_required',
redirect: '/usage',
})
getAuthTokenMock.mockReturnValue(null)
const wrapper = mount(WechatCallbackView, {
global: {
stubs: {
AuthLayout: { template: '<div><slot /></div>' },
Icon: true,
RouterLink: { template: '<a><slot /></a>' },
transition: false,
},
},
})
await flushPromises()
const emailInput = wrapper.get('[data-testid="existing-account-email"]')
await emailInput.setValue('user@example.com')
await wrapper.get('[data-testid="existing-account-submit"]').trigger('click')
expect(replaceMock).toHaveBeenCalledTimes(1)
expect(replaceMock.mock.calls[0]?.[0]).toContain('/login?')
expect(replaceMock.mock.calls[0]?.[0]).toContain('wechat_bind_existing%3D1')
expect(replaceMock.mock.calls[0]?.[0]).toContain('email=user%40example.com')
})
it('restarts the current-user bind flow after returning from login', async () => {
routeState.query = {
wechat_bind_existing: '1',
redirect: '/profile'
}
getAuthTokenMock.mockReturnValue('existing-auth-token')
mount(WechatCallbackView, {
global: {
stubs: {
AuthLayout: { template: '<div><slot /></div>' },
Icon: true,
RouterLink: { template: '<a><slot /></a>' },
transition: false,
},
},
})
await flushPromises()
expect(exchangePendingOAuthCompletionMock).not.toHaveBeenCalled()
expect(prepareOAuthBindAccessTokenCookieMock).toHaveBeenCalledTimes(1)
expect(locationState.current.href).toContain('/api/v1/auth/oauth/wechat/start?')
expect(locationState.current.href).toContain('intent=bind_current_user')
expect(locationState.current.href).toContain('redirect=%2Fprofile')
})
})