diff --git a/src/pages/login/wxLoginFlow.ts b/src/pages/login/wxLoginFlow.ts new file mode 100644 index 0000000..85a3e0f --- /dev/null +++ b/src/pages/login/wxLoginFlow.ts @@ -0,0 +1,24 @@ +import Taro from '@tarojs/taro' +import { wxMiniappLogin, type WxLoginResult } from '@/api/auth' +import { storage } from '@/utils/storage' +import { WX_OPENID_STORAGE_KEY, FALLBACK_WX_APP_ID } from '@/constants/auth' + +// 取小程序 appId:正常环境走 accountInfo,异常(部分基座不支持)回退常量 +function resolveAppId(): string { + try { + return Taro.getAccountInfoSync().miniProgram.appId || FALLBACK_WX_APP_ID + } catch { + return FALLBACK_WX_APP_ID + } +} + +// 微信一键登录主流程(spec D3): +// Taro.login 换一次性 code → 组装 appId/openId → 调 miniapp/login → 缓存响应中的 openId +export async function performWxLogin(): Promise { + const { code } = await Taro.login() + if (!code) throw new Error('获取微信登录凭证失败') + const openId = storage.get(WX_OPENID_STORAGE_KEY) ?? undefined + const result = await wxMiniappLogin({ appId: resolveAppId(), code, openId }) + if (result.openId) storage.set(WX_OPENID_STORAGE_KEY, result.openId) + return result +} diff --git a/tests/unit/login/wxLoginFlow.spec.ts b/tests/unit/login/wxLoginFlow.spec.ts new file mode 100644 index 0000000..845e814 --- /dev/null +++ b/tests/unit/login/wxLoginFlow.spec.ts @@ -0,0 +1,93 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' + +const mockLogin = vi.hoisted(() => vi.fn()) +const mockGetAccountInfo = vi.hoisted(() => vi.fn()) +vi.mock('@tarojs/taro', () => ({ + default: { login: mockLogin, getAccountInfoSync: mockGetAccountInfo } +})) + +const mockWxMiniappLogin = vi.hoisted(() => vi.fn()) +vi.mock('@/api/auth', () => ({ wxMiniappLogin: mockWxMiniappLogin })) + +const mem = new Map() +vi.mock('@/utils/storage', () => ({ + storage: { + get: (key: string): T | null => (mem.has(key) ? (mem.get(key) as T) : null), + set: (key: string, value: T): void => { mem.set(key, value) }, + remove: (key: string): void => { mem.delete(key) } + } +})) + +import { performWxLogin } from '@/pages/login/wxLoginFlow' + +const result = (over: Record = {}) => ({ + userInfo: { + userId: 'u-1', userType: 1, avatar: '', nickName: '张三', mobile: '', + gender: 1, region: '', loginFrom: 'miniapp' + }, + code: 'tk-abc', + ...over +}) + +beforeEach(() => { + mem.clear() + mockLogin.mockReset() + mockGetAccountInfo.mockReset() + mockWxMiniappLogin.mockReset() + mockLogin.mockResolvedValue({ code: 'wx-code-1' }) + mockGetAccountInfo.mockReturnValue({ miniProgram: { appId: 'wx-appid-live' } }) +}) + +describe('performWxLogin', () => { + it('完整流程:Taro.login → appId/openId 组装 → 返回登录结果', async () => { + mockWxMiniappLogin.mockResolvedValue(result()) + const res = await performWxLogin() + expect(res.code).toBe('tk-abc') + expect(mockWxMiniappLogin).toHaveBeenCalledWith({ + appId: 'wx-appid-live', + code: 'wx-code-1', + openId: undefined + }) + }) + + it('首次登录(本地无 openId)请求不带 openId,结果含 openId 时写入缓存', async () => { + mockWxMiniappLogin.mockResolvedValue(result({ openId: 'o-abc' })) + await performWxLogin() + expect(mockWxMiniappLogin.mock.calls[0][0].openId).toBeUndefined() + expect(mem.get('wx_openid')).toBe('o-abc') + }) + + it('二次登录:本地已有 openId 则随请求回传', async () => { + mem.set('wx_openid', 'o-saved') + mockWxMiniappLogin.mockResolvedValue(result()) + await performWxLogin() + expect(mockWxMiniappLogin.mock.calls[0][0].openId).toBe('o-saved') + }) + + it('结果不含 openId 时不覆盖本地缓存', async () => { + mem.set('wx_openid', 'o-saved') + mockWxMiniappLogin.mockResolvedValue(result()) + await performWxLogin() + expect(mem.get('wx_openid')).toBe('o-saved') + }) + + it('Taro.login 返回空 code 抛错(不发起登录请求)', async () => { + mockLogin.mockResolvedValue({ code: '' }) + await expect(performWxLogin()).rejects.toThrow('获取微信登录凭证失败') + expect(mockWxMiniappLogin).not.toHaveBeenCalled() + }) + + it('getAccountInfoSync 抛错时回退常量 appId', async () => { + mockGetAccountInfo.mockImplementation(() => { throw new Error('not supported') }) + mockWxMiniappLogin.mockResolvedValue(result()) + await performWxLogin() + expect(mockWxMiniappLogin.mock.calls[0][0].appId).toBe('wxe9773bb6f025ab00') + }) + + it('接口业务错(BusinessError)原样透传', async () => { + const err = new Error('code 已被使用') + err.name = 'BusinessError' + mockWxMiniappLogin.mockRejectedValue(err) + await expect(performWxLogin()).rejects.toThrow('code 已被使用') + }) +})