mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
fix(redeem): preserve success when account refresh fails
This commit is contained in:
@@ -756,6 +756,7 @@ export default {
|
||||
days: ' days',
|
||||
codeRedeemSuccess: 'Code redeemed successfully!',
|
||||
failedToRedeem: 'Failed to redeem code. Please check the code and try again.',
|
||||
userRefreshFailed: 'Redeemed successfully, but failed to refresh account information.',
|
||||
subscriptionRefreshFailed: 'Redeemed successfully, but failed to refresh subscription status.',
|
||||
pleaseEnterCode: 'Please enter a redeem code'
|
||||
},
|
||||
|
||||
@@ -760,6 +760,7 @@ export default {
|
||||
days: '天',
|
||||
codeRedeemSuccess: '兑换成功!',
|
||||
failedToRedeem: '兑换失败,请检查兑换码后重试。',
|
||||
userRefreshFailed: '兑换成功,但账户信息刷新失败。',
|
||||
subscriptionRefreshFailed: '兑换成功,但订阅状态刷新失败。',
|
||||
pleaseEnterCode: '请输入兑换码'
|
||||
},
|
||||
|
||||
@@ -447,7 +447,12 @@ const handleRedeem = async () => {
|
||||
redeemResult.value = result
|
||||
|
||||
// Refresh user data to get updated balance/concurrency
|
||||
await authStore.refreshUser()
|
||||
try {
|
||||
await authStore.refreshUser()
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh user after redeem:', error)
|
||||
appStore.showWarning(t('redeem.userRefreshFailed'))
|
||||
}
|
||||
|
||||
// If subscription type, immediately refresh subscription status
|
||||
if (result.type === 'subscription') {
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import RedeemView from '../RedeemView.vue'
|
||||
|
||||
const { redeem, getHistory, refreshUser, fetchActiveSubscriptions, showError, showWarning, showSuccess } = vi.hoisted(() => ({
|
||||
redeem: vi.fn(),
|
||||
getHistory: vi.fn(),
|
||||
refreshUser: vi.fn(),
|
||||
fetchActiveSubscriptions: vi.fn(),
|
||||
showError: vi.fn(),
|
||||
showWarning: vi.fn(),
|
||||
showSuccess: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api', () => ({
|
||||
redeemAPI: { redeem, getHistory },
|
||||
authAPI: { getPublicSettings: vi.fn().mockResolvedValue({}) },
|
||||
}))
|
||||
vi.mock('@/stores/auth', () => ({
|
||||
useAuthStore: () => ({ user: { balance: 10, concurrency: 2 }, refreshUser }),
|
||||
}))
|
||||
vi.mock('@/stores/subscriptions', () => ({
|
||||
useSubscriptionStore: () => ({ fetchActiveSubscriptions }),
|
||||
}))
|
||||
vi.mock('@/stores/app', () => ({
|
||||
useAppStore: () => ({ showError, showWarning, showSuccess }),
|
||||
}))
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
|
||||
return { ...actual, useI18n: () => ({ t: (key: string) => key }) }
|
||||
})
|
||||
|
||||
async function submitCode() {
|
||||
const wrapper = mount(RedeemView, {
|
||||
global: { stubs: { AppLayout: { template: '<div><slot /></div>' }, Icon: true } },
|
||||
})
|
||||
await flushPromises()
|
||||
await wrapper.get('input#code').setValue(' REDEEM-CODE ')
|
||||
await wrapper.get('form').trigger('submit')
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
describe('RedeemView refresh after redemption', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
redeem.mockResolvedValue({ type: 'balance', value: 20, message: 'Code applied' })
|
||||
getHistory.mockResolvedValue([])
|
||||
refreshUser.mockResolvedValue({ balance: 30, concurrency: 2 })
|
||||
fetchActiveSubscriptions.mockResolvedValue([])
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it.each(['balance', 'concurrency', 'subscription'])(
|
||||
'keeps a successful %s redemption when profile refresh fails', async (type) => {
|
||||
redeem.mockResolvedValue({ type, value: 20, message: 'Code applied' })
|
||||
refreshUser.mockRejectedValue({ status: 503, message: 'Service unavailable' })
|
||||
getHistory.mockResolvedValueOnce([]).mockResolvedValueOnce([{
|
||||
id: 1, code: 'REDEEM-CODE', type, value: 20, used_at: '2026-03-08T00:00:00Z',
|
||||
}])
|
||||
|
||||
const wrapper = await submitCode()
|
||||
|
||||
expect(redeem).toHaveBeenCalledWith('REDEEM-CODE')
|
||||
expect(showError).not.toHaveBeenCalled()
|
||||
expect(showWarning).toHaveBeenCalledWith('redeem.userRefreshFailed')
|
||||
expect(showSuccess).toHaveBeenCalledWith('redeem.codeRedeemSuccess')
|
||||
expect(wrapper.text()).toContain('Code applied')
|
||||
expect(wrapper.text()).not.toContain('redeem.failedToRedeem')
|
||||
expect((wrapper.get('input#code').element as HTMLInputElement).value).toBe('')
|
||||
expect((wrapper.get('input#code').element as HTMLInputElement).disabled).toBe(false)
|
||||
expect(getHistory).toHaveBeenCalledTimes(2)
|
||||
expect(wrapper.text()).toContain('REDEEM-C...')
|
||||
if (type === 'subscription') {
|
||||
expect(fetchActiveSubscriptions).toHaveBeenCalledWith(true)
|
||||
} else {
|
||||
expect(fetchActiveSubscriptions).not.toHaveBeenCalled()
|
||||
}
|
||||
wrapper.unmount()
|
||||
}
|
||||
)
|
||||
|
||||
it('finishes normally without a warning when profile refresh succeeds', async () => {
|
||||
const wrapper = await submitCode()
|
||||
|
||||
expect(refreshUser).toHaveBeenCalledOnce()
|
||||
expect(showWarning).not.toHaveBeenCalled()
|
||||
expect(showError).not.toHaveBeenCalled()
|
||||
expect(showSuccess).toHaveBeenCalledWith('redeem.codeRedeemSuccess')
|
||||
expect((wrapper.get('input#code').element as HTMLInputElement).value).toBe('')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('preserves the existing subscription refresh warning after successful redemption', async () => {
|
||||
redeem.mockResolvedValue({ type: 'subscription', value: 20, message: 'Code applied' })
|
||||
fetchActiveSubscriptions.mockRejectedValue(new Error('Network Error'))
|
||||
const wrapper = await submitCode()
|
||||
|
||||
expect(showWarning).toHaveBeenCalledWith('redeem.subscriptionRefreshFailed')
|
||||
expect(showError).not.toHaveBeenCalled()
|
||||
expect(showSuccess).toHaveBeenCalledWith('redeem.codeRedeemSuccess')
|
||||
expect(getHistory).toHaveBeenCalledTimes(2)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('keeps the code and reports failure when the redemption request itself fails', async () => {
|
||||
redeem.mockRejectedValue({ response: { data: { detail: 'Invalid code' } } })
|
||||
const wrapper = await submitCode()
|
||||
|
||||
expect(showError).toHaveBeenCalledWith('redeem.redeemFailed')
|
||||
expect(wrapper.text()).toContain('Invalid code')
|
||||
expect(wrapper.text()).not.toContain('Code applied')
|
||||
expect((wrapper.get('input#code').element as HTMLInputElement).value).toBe(' REDEEM-CODE ')
|
||||
expect(refreshUser).not.toHaveBeenCalled()
|
||||
expect(fetchActiveSubscriptions).not.toHaveBeenCalled()
|
||||
expect(getHistory).toHaveBeenCalledOnce()
|
||||
expect(showSuccess).not.toHaveBeenCalled()
|
||||
expect(showWarning).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user