| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502 |
- import { setActivePinia, createPinia } from 'pinia'
- import { beforeEach, describe, it, expect, vi } from 'vitest'
- import { useUserStore } from '../../stores/user'
- import { authApi, profileApi } from '../../utils/api'
- // ── Mock API module ──────────────────────────────────────────────
- // vi.mock is hoisted to the top of the file, before any imports.
- vi.mock('../../utils/api', () => ({
- authApi: {
- login: vi.fn(),
- },
- profileApi: {
- info: vi.fn(),
- quota: vi.fn(),
- commissionSummary: vi.fn(),
- },
- }))
- describe('user store', () => {
- beforeEach(() => {
- setActivePinia(createPinia())
- // Reset API mocks between tests so return values don't leak
- authApi.login.mockReset()
- profileApi.info.mockReset()
- profileApi.quota.mockReset()
- profileApi.commissionSummary.mockReset()
- })
- // ── Initial state ──────────────────────────────────────────────
- describe('initial state', () => {
- it('creates with empty token and isLoggedIn=false when no saved token', () => {
- const store = useUserStore()
- expect(store.token).toBe('')
- expect(store.isLoggedIn).toBe(false)
- expect(store.nickname).toBe('')
- expect(store.avatarUrl).toBe('')
- expect(store.isVip).toBe(false)
- expect(store.vipType).toBeNull()
- expect(store.vipEndTime).toBeNull()
- expect(store.referralCode).toBe('')
- })
- it('loads token from storage and sets isLoggedIn=true', () => {
- uni.__mockStorage.token = 'saved-token-abc'
- const store = useUserStore()
- expect(store.token).toBe('saved-token-abc')
- expect(store.isLoggedIn).toBe(true)
- })
- it('loads empty string when storage returns falsy', () => {
- uni.__mockStorage.token = ''
- const store = useUserStore()
- expect(store.token).toBe('')
- expect(store.isLoggedIn).toBe(false)
- })
- })
- // ── login() ────────────────────────────────────────────────────
- describe('login()', () => {
- it('calls authApi.login with openid, nickname, avatarUrl', async () => {
- authApi.login.mockResolvedValue({ token: 'new-token', profileIncomplete: false })
- const store = useUserStore()
- await store.login('openid-123', 'Alice', 'http://avatar.url/1', 'ref-999')
- expect(authApi.login).toHaveBeenCalledWith({
- openid: 'openid-123',
- nickname: 'Alice',
- avatarUrl: 'http://avatar.url/1',
- referralCode: 'ref-999',
- })
- })
- it('calls authApi.login without referralCode when not provided', async () => {
- authApi.login.mockResolvedValue({ token: 'new-token', profileIncomplete: false })
- const store = useUserStore()
- await store.login('openid-123', 'Alice', 'http://avatar.url/1', '')
- expect(authApi.login).toHaveBeenCalledWith({
- openid: 'openid-123',
- nickname: 'Alice',
- avatarUrl: 'http://avatar.url/1',
- })
- })
- it('sets token, nickname, avatarUrl from login response', async () => {
- authApi.login.mockResolvedValue({ token: 'jwt-token-abc', profileIncomplete: false })
- const store = useUserStore()
- await store.login('oid', 'Bob', 'http://avatar/bob', '')
- expect(store.token).toBe('jwt-token-abc')
- expect(store.nickname).toBe('Bob')
- expect(store.avatarUrl).toBe('http://avatar/bob')
- })
- it('persists token to storage', async () => {
- authApi.login.mockResolvedValue({ token: 'persisted-token', profileIncomplete: false })
- const store = useUserStore()
- await store.login('oid', 'Bob', '', '')
- expect(uni.setStorageSync).toHaveBeenCalledWith('token', 'persisted-token')
- expect(uni.__mockStorage.token).toBe('persisted-token')
- })
- it('handles res being a raw string (no .token wrapper)', async () => {
- authApi.login.mockResolvedValue('raw-string-token')
- const store = useUserStore()
- await store.login('oid', 'Bob', '', '')
- expect(store.token).toBe('raw-string-token')
- })
- it('sets profileIncomplete from response', async () => {
- authApi.login.mockResolvedValue({ token: 't', profileIncomplete: true })
- const store = useUserStore()
- await store.login('oid', 'Bob', '', '')
- expect(store.profileIncomplete).toBe(true)
- })
- })
- // ── wechatLogin() ──────────────────────────────────────────────
- describe('wechatLogin()', () => {
- it('calls authApi.login with code instead of openid', async () => {
- authApi.login.mockResolvedValue({ token: 'wx-token', profileIncomplete: false })
- profileApi.info.mockResolvedValue({ nickname: 'wxuser', avatarUrl: '' })
- const store = useUserStore()
- await store.wechatLogin('code-abc', 'WeChatUser', 'http://avatar', 'ref')
- expect(authApi.login).toHaveBeenCalledWith({
- code: 'code-abc',
- nickname: 'WeChatUser',
- avatarUrl: 'http://avatar',
- referralCode: 'ref',
- })
- })
- it('falls back to default nickname when empty (before fetchProfile completes)', async () => {
- // The store sets nickname || '微信用户' before calling fetchProfile.
- // However, fetchProfile always overwrites it with the API value (even undefined).
- // This test verifies the code path exists and no error is thrown.
- authApi.login.mockResolvedValue({ token: 'wx-token', profileIncomplete: false })
- profileApi.info.mockResolvedValue({ nickname: 'ServerName', avatarUrl: '' })
- const store = useUserStore()
- await store.wechatLogin('code-abc', '', '', '')
- // fetchProfile response takes precedence over the temporary fallback
- expect(store.nickname).toBe('ServerName')
- expect(store.avatarUrl).toBe('')
- })
- it('fetches full profile after login', async () => {
- authApi.login.mockResolvedValue({ token: 'wx-token', profileIncomplete: false })
- profileApi.info.mockResolvedValue({
- nickname: 'ServerNick',
- avatarUrl: 'http://server/avatar',
- referralCode: 'CODE123',
- vipType: 'year',
- vipEndTime: '2027-06-08T00:00:00',
- gender: 1,
- birthDate: '1990-01-15',
- })
- const store = useUserStore()
- await store.wechatLogin('code-abc', 'WeChatUser', 'http://avatar', '')
- // Token set from login
- expect(store.token).toBe('wx-token')
- // Profile loaded from API
- expect(store.nickname).toBe('ServerNick')
- expect(store.referralCode).toBe('CODE123')
- expect(store.vipType).toBe('year')
- })
- it('does not throw when fetchProfile fails', async () => {
- authApi.login.mockResolvedValue({ token: 'wx-token', profileIncomplete: false })
- profileApi.info.mockRejectedValue(new Error('network error'))
- const store = useUserStore()
- await expect(store.wechatLogin('code', 'name', '', '')).resolves.not.toThrow()
- expect(store.token).toBe('wx-token')
- })
- })
- // ── fetchProfile() ─────────────────────────────────────────────
- describe('fetchProfile()', () => {
- const mockProfile = {
- nickname: 'TestUser',
- avatarUrl: 'http://avatar/test',
- referralCode: 'REF123',
- vipType: 'month',
- vipEndTime: '2026-12-31T23:59:59',
- gender: 1,
- birthDate: '1990-06-15',
- bio: 'Hello world',
- city: 'Shanghai',
- tags: 'reader,traveler',
- lookingFor: 2,
- profileComplete: true,
- birthYear: 1990,
- birthMonth: 6,
- birthDay: 15,
- }
- it('updates all user fields from API response', async () => {
- profileApi.info.mockResolvedValue(mockProfile)
- const store = useUserStore()
- await store.fetchProfile()
- expect(store.nickname).toBe('TestUser')
- expect(store.avatarUrl).toBe('http://avatar/test')
- expect(store.referralCode).toBe('REF123')
- expect(store.vipType).toBe('month')
- expect(store.vipEndTime).toBe('2026-12-31T23:59:59')
- expect(store.gender).toBe(1)
- expect(store.birthDate).toBe('1990-06-15')
- expect(store.bio).toBe('Hello world')
- expect(store.city).toBe('Shanghai')
- expect(store.tags).toBe('reader,traveler')
- expect(store.lookingFor).toBe(2)
- expect(store.profileComplete).toBe(true)
- expect(store.birthYear).toBe(1990)
- expect(store.birthMonth).toBe(6)
- expect(store.birthDay).toBe(15)
- })
- it('normalizes vipEndTime from Jackson array [y,m,d,h,m,s] to ISO string', async () => {
- profileApi.info.mockResolvedValue({
- ...mockProfile,
- vipEndTime: [2026, 6, 8, 23, 59, 59],
- })
- const store = useUserStore()
- await store.fetchProfile()
- expect(store.vipEndTime).toBe('2026-06-08T23:59:59')
- })
- it('normalizes vipEndTime with single-digit month/day padding', async () => {
- profileApi.info.mockResolvedValue({
- ...mockProfile,
- vipEndTime: [2026, 1, 5, 9, 5, 3],
- })
- const store = useUserStore()
- await store.fetchProfile()
- expect(store.vipEndTime).toBe('2026-01-05T09:05:03')
- })
- it('handles null vipEndTime', async () => {
- profileApi.info.mockResolvedValue({
- ...mockProfile,
- vipEndTime: null,
- })
- const store = useUserStore()
- await store.fetchProfile()
- expect(store.vipEndTime).toBeNull()
- expect(store.isVip).toBe(false)
- })
- it('handles undefined vipEndTime', async () => {
- profileApi.info.mockResolvedValue({
- ...mockProfile,
- vipEndTime: undefined,
- })
- const store = useUserStore()
- await store.fetchProfile()
- expect(store.vipEndTime).toBeNull()
- })
- it('sets isVip=true when vipEndTime is in the future', async () => {
- profileApi.info.mockResolvedValue({
- ...mockProfile,
- vipEndTime: '2099-01-01T00:00:00', // far future
- })
- const store = useUserStore()
- await store.fetchProfile()
- expect(store.isVip).toBe(true)
- expect(store.isVipActive).toBe(true)
- })
- it('sets isVip=false when vipEndTime is in the past', async () => {
- profileApi.info.mockResolvedValue({
- ...mockProfile,
- vipEndTime: '2020-01-01T00:00:00', // past
- })
- const store = useUserStore()
- await store.fetchProfile()
- expect(store.isVip).toBe(false)
- // isVipActive getter computes from vipEndTime
- expect(store.isVipActive).toBe(false)
- })
- it('handles missing optional fields with defaults', async () => {
- profileApi.info.mockResolvedValue({
- nickname: 'Minimal',
- avatarUrl: '',
- })
- const store = useUserStore()
- await store.fetchProfile()
- // referralCode is set directly (no || fallback), so stays undefined
- expect(store.referralCode).toBeUndefined()
- expect(store.vipType).toBeNull()
- expect(store.vipEndTime).toBeNull()
- expect(store.gender).toBe(0)
- expect(store.birthDate).toBeNull()
- expect(store.bio).toBe('')
- expect(store.city).toBe('')
- expect(store.tags).toBe('')
- expect(store.lookingFor).toBe(0)
- expect(store.profileComplete).toBe(false)
- expect(store.birthYear).toBe(0)
- expect(store.birthMonth).toBe(0)
- expect(store.birthDay).toBe(0)
- expect(store.profileIncomplete).toBe(true)
- })
- it('sets profileIncomplete to true when profileComplete is falsy', async () => {
- profileApi.info.mockResolvedValue({
- nickname: 'Test',
- avatarUrl: '',
- profileComplete: false,
- })
- const store = useUserStore()
- await store.fetchProfile()
- expect(store.profileIncomplete).toBe(true)
- })
- })
- // ── fetchQuota() ───────────────────────────────────────────────
- describe('fetchQuota()', () => {
- const mockQuota = {
- dailyChartCount: 2,
- chartLimit: 10,
- dailyChatCount: 5,
- chatLimit: 20,
- dailyShareCount: 3,
- shareLimit: 5,
- dailyAcademicCount: 0,
- academicLimit: 2,
- }
- it('updates quota limits from API response', async () => {
- profileApi.quota.mockResolvedValue(mockQuota)
- const store = useUserStore()
- await store.fetchQuota()
- expect(store.usedCharts).toBe(2)
- expect(store.maxCharts).toBe(10)
- expect(store.usedChats).toBe(5)
- expect(store.maxChats).toBe(20)
- expect(store.shareCount).toBe(3)
- expect(store.shareLimit).toBe(5)
- expect(store.usedAcademic).toBe(0)
- expect(store.maxAcademic).toBe(2)
- })
- it('falls back to 999 when chart/chats limits are 0 or negative', async () => {
- profileApi.quota.mockResolvedValue({
- dailyChartCount: 0,
- chartLimit: 0,
- dailyChatCount: 0,
- chatLimit: 0,
- dailyShareCount: 0,
- shareLimit: 0,
- dailyAcademicCount: 0,
- academicLimit: 0,
- })
- const store = useUserStore()
- await store.fetchQuota()
- expect(store.maxCharts).toBe(999)
- expect(store.maxChats).toBe(999)
- expect(store.shareLimit).toBe(1) // defaults to 1 when 0 or negative
- expect(store.maxAcademic).toBe(1) // defaults to 1 when 0 or negative
- })
- it('does not throw on API failure (silent catch)', async () => {
- profileApi.quota.mockRejectedValue(new Error('server error'))
- const store = useUserStore()
- await expect(store.fetchQuota()).resolves.not.toThrow()
- // Default values remain
- expect(store.usedCharts).toBe(0)
- expect(store.maxCharts).toBe(3)
- })
- })
- // ── fetchCommissionSummary() ───────────────────────────────────
- describe('fetchCommissionSummary()', () => {
- it('updates commission from API response', async () => {
- profileApi.commissionSummary.mockResolvedValue({
- totalEarnings: 1500,
- availableBalance: 800,
- })
- const store = useUserStore()
- await store.fetchCommissionSummary()
- expect(store.totalCommission).toBe(1500)
- expect(store.availableCommission).toBe(800)
- })
- it('does not throw on API failure (silent catch)', async () => {
- profileApi.commissionSummary.mockRejectedValue(new Error('error'))
- const store = useUserStore()
- await expect(store.fetchCommissionSummary()).resolves.not.toThrow()
- expect(store.totalCommission).toBe(0)
- expect(store.availableCommission).toBe(0)
- })
- it('handles missing response fields', async () => {
- profileApi.commissionSummary.mockResolvedValue({})
- const store = useUserStore()
- await store.fetchCommissionSummary()
- expect(store.totalCommission).toBe(0)
- expect(store.availableCommission).toBe(0)
- })
- })
- // ── logout() ───────────────────────────────────────────────────
- describe('logout()', () => {
- it('clears token, sets isLoggedIn=false', () => {
- const store = useUserStore()
- store.token = 'some-token'
- store.logout()
- expect(store.token).toBe('')
- expect(store.isLoggedIn).toBe(false)
- })
- it('removes token from storage', () => {
- uni.__mockStorage.token = 'some-token'
- const store = useUserStore()
- store.logout()
- expect(uni.removeStorageSync).toHaveBeenCalledWith('token')
- expect(uni.__mockStorage.token).toBeUndefined()
- })
- it('resets all state fields to defaults', () => {
- // Populate with non-default values
- const store = useUserStore()
- store.nickname = 'Test'
- store.avatarUrl = 'http://avatar'
- store.usedCharts = 5
- store.usedChats = 10
- store.usedAcademic = 3
- store.shareCount = 8
- store.shareLimit = 20
- store.maxAcademic = 10
- store.totalCommission = 500
- store.availableCommission = 200
- store.profileIncomplete = true
- store.gender = 1
- store.birthDate = '1990-01-01'
- store.bio = 'bio'
- store.city = 'city'
- store.tags = 'tag1,tag2'
- store.lookingFor = 2
- store.profileComplete = true
- store.birthYear = 1990
- store.birthMonth = 1
- store.birthDay = 1
- store.logout()
- expect(store.nickname).toBe('')
- expect(store.avatarUrl).toBe('')
- expect(store.usedCharts).toBe(0)
- expect(store.usedChats).toBe(0)
- expect(store.usedAcademic).toBe(0)
- expect(store.shareCount).toBe(0)
- expect(store.shareLimit).toBe(1)
- expect(store.maxAcademic).toBe(1)
- expect(store.totalCommission).toBe(0)
- expect(store.availableCommission).toBe(0)
- expect(store.profileIncomplete).toBe(false)
- expect(store.gender).toBe(0)
- expect(store.birthDate).toBeNull()
- expect(store.bio).toBe('')
- expect(store.city).toBe('')
- expect(store.tags).toBe('')
- expect(store.lookingFor).toBe(0)
- expect(store.profileComplete).toBe(false)
- expect(store.birthYear).toBe(0)
- expect(store.birthMonth).toBe(0)
- expect(store.birthDay).toBe(0)
- })
- })
- })
|