import { renderHook, act, waitFor } from '@testing-library/react-native' import { useMembership } from './use-membership' import { subscription, useSession } from '@/lib/auth' import { useRouter } from 'expo-router' import { Linking, Alert } from 'react-native' jest.mock('@/lib/auth', () => ({ subscription: { plans: jest.fn(), list: jest.fn(), create: jest.fn(), upgrade: jest.fn(), restore: jest.fn(), cancel: jest.fn(), credit: { topup: jest.fn(), }, }, useSession: jest.fn(), })) jest.mock('expo-router', () => ({ useRouter: jest.fn(), })) jest.mock('react-native', () => ({ Linking: { openURL: jest.fn(), }, Alert: { alert: jest.fn(), }, })) const mockUseQuery = jest.fn() const mockUseMutation = jest.fn() jest.mock('@tanstack/react-query', () => ({ useQuery: (...args: any[]) => mockUseQuery(args[0]), useMutation: (...args: any[]) => mockUseMutation(args[0]), })) describe('useMembership', () => { const mockRouter = { push: jest.fn() } const mockSession = { user: { id: 'user-123' }, } beforeEach(() => { jest.clearAllMocks() ;(useRouter as jest.Mock).mockReturnValue(mockRouter) ;(useSession as jest.Mock).mockReturnValue({ data: mockSession }) mockUseQuery.mockReturnValue({ data: null, isLoading: false, refetch: jest.fn(), }) mockUseMutation.mockReturnValue({ mutate: jest.fn(), }) }) describe('获取 Stripe 定价表数据', () => { it('应该成功获取定价表数据', async () => { const mockPricingData = { pricing_table_items: [ { amount: '1000', recurring: { interval: 'month' }, metadata: { grant_token: '1000' }, is_highlight: true, }, ], } mockUseQuery.mockReturnValue({ data: mockPricingData, isLoading: false, refetch: jest.fn(), }) const { result } = renderHook(() => useMembership()) expect(result.current.stripePricingData).toEqual(mockPricingData) }) }) describe('获取用户订阅列表', () => { it('应该成功获取订阅列表', async () => { const mockSubscriptions = [ { type: 'licenced', status: 'active', cancelAtPeriodEnd: false, stripeSubscriptionId: 'sub-123', }, ] let queryCallCount = 0 mockUseQuery.mockImplementation((config) => { queryCallCount++ if (queryCallCount === 2) { return { data: mockSubscriptions, isLoading: false, refetch: jest.fn(), } } return { data: null, isLoading: false, refetch: jest.fn(), } }) const { result } = renderHook(() => useMembership()) expect(result.current.isLoadingSubscriptions).toBe(false) }) it('应该在没有用户会话时不获取订阅', () => { ;(useSession as jest.Mock).mockReturnValue({ data: null }) mockUseQuery.mockImplementation((config) => { if (config.enabled === false) { return { data: null, isLoading: false, refetch: jest.fn(), } } return { data: null, isLoading: false, refetch: jest.fn(), } }) renderHook(() => useMembership()) expect(mockUseQuery).toHaveBeenCalled() }) }) describe('创建订阅', () => { it('应该成功创建订阅并打开支付链接', async () => { const mockUrl = 'https://checkout.stripe.com/test' ;(subscription.create as jest.Mock).mockResolvedValue({ data: { url: mockUrl }, error: null, }) const mutateFn = jest.fn(async (variables) => { const { data, error } = await subscription.create({ priceId: variables.priceId, productId: variables.productId, successUrl: expect.stringContaining('/membership'), cancelUrl: expect.stringContaining('/membership?canceled=true'), }) if (error) throw new Error(error.message) if (data?.url) await Linking.openURL(data.url) return data }) mockUseMutation.mockReturnValue({ mutate: mutateFn, }) const { result } = renderHook(() => useMembership()) await act(async () => { await result.current.createSubscription({ priceId: 'price-123', productId: 'prod-123', }) }) expect(mutateFn).toHaveBeenCalled() expect(Linking.openURL).toHaveBeenCalledWith(mockUrl) }) it('应该处理创建订阅错误', async () => { ;(subscription.create as jest.Mock).mockResolvedValue({ data: null, error: { message: 'Failed to create subscription' }, }) const mutateFn = jest.fn(async (variables) => { try { const { data, error } = await subscription.create(variables) if (error) throw new Error(error.message) return data } catch (error) { Alert.alert('错误', '订阅创建失败') throw error } }) mockUseMutation.mockReturnValue({ mutate: mutateFn, }) const { result } = renderHook(() => useMembership()) await act(async () => { try { await result.current.createSubscription({ priceId: 'price-123', productId: 'prod-123', }) } catch (e) { // Expected error } }) expect(Alert.alert).toHaveBeenCalledWith('错误', '订阅创建失败') }) }) describe('升级订阅', () => { it('应该成功升级订阅', async () => { const mockSubscriptions = [ { type: 'licenced', status: 'active', cancelAtPeriodEnd: false, stripeSubscriptionId: 'sub-123', }, ] let queryCallCount = 0 mockUseQuery.mockImplementation(() => { queryCallCount++ if (queryCallCount === 2) { return { data: mockSubscriptions, isLoading: false, refetch: jest.fn(), } } return { data: null, isLoading: false, refetch: jest.fn(), } }) ;(subscription.upgrade as jest.Mock).mockResolvedValue({ data: { success: true }, error: null, }) const mutateFn = jest.fn() mockUseMutation.mockReturnValue({ mutate: mutateFn, }) const { result } = renderHook(() => useMembership()) await act(async () => { result.current.upgradeSubscription({ credits: 1000 }) }) expect(mutateFn).toHaveBeenCalled() }) }) describe('恢复订阅', () => { it('应该成功恢复订阅', async () => { const mockSubscriptions = [ { type: 'licenced', status: 'active', cancelAtPeriodEnd: true, stripeSubscriptionId: 'sub-123', referenceId: 'user-123', }, ] let queryCallCount = 0 mockUseQuery.mockImplementation(() => { queryCallCount++ if (queryCallCount === 2) { return { data: mockSubscriptions, isLoading: false, refetch: jest.fn(), } } return { data: null, isLoading: false, refetch: jest.fn(), } }) ;(subscription.restore as jest.Mock).mockResolvedValue({ data: { success: true }, error: null, }) const mutateFn = jest.fn() mockUseMutation.mockReturnValue({ mutate: mutateFn, }) const { result } = renderHook(() => useMembership()) await act(async () => { result.current.restoreSubscription() }) expect(mutateFn).toHaveBeenCalled() }) }) describe('充值积分', () => { it('应该成功充值积分并打开支付链接', async () => { const mockUrl = 'https://checkout.stripe.com/topup' const mockSubscriptions = [ { type: 'metered', priceId: 'price-metered-123', }, ] let queryCallCount = 0 mockUseQuery.mockImplementation(() => { queryCallCount++ if (queryCallCount === 2) { return { data: mockSubscriptions, isLoading: false, refetch: jest.fn(), } } return { data: null, isLoading: false, refetch: jest.fn(), } }) ;(subscription.credit.topup as jest.Mock).mockResolvedValue({ data: { url: mockUrl }, error: null, }) const mutateFn = jest.fn(async (amount) => { const { data, error } = await subscription.credit.topup({ amount, priceId: 'price-metered-123', successUrl: expect.stringContaining('/membership'), cancelUrl: expect.stringContaining('/membership?canceled=true'), }) if (error) throw new Error(error.message) if (data?.url) await Linking.openURL(data.url) return data }) mockUseMutation.mockReturnValue({ mutate: mutateFn, }) const { result } = renderHook(() => useMembership()) await act(async () => { await result.current.rechargeToken(1000) }) expect(mutateFn).toHaveBeenCalledWith(1000) expect(Linking.openURL).toHaveBeenCalledWith(mockUrl) }) it('应该处理无效充值金额', async () => { const mutateFn = jest.fn(async (amount) => { try { if (!amount || amount <= 0) throw new Error('充值金额无效') } catch (error) { Alert.alert('错误', '充值失败') throw error } }) mockUseMutation.mockReturnValue({ mutate: mutateFn, }) const { result } = renderHook(() => useMembership()) await act(async () => { try { await result.current.rechargeToken(0) } catch (e) { // Expected error } }) expect(Alert.alert).toHaveBeenCalledWith('错误', '充值失败') }) }) })