-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
feat(rewards): expose GET /campaigns endpoint through RewardsController #27108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
74318d9
feat(rewards): add campaigns endpoint
VGR-GIT 29277be
fix(rewards): remove duplicate union member and fix test names
VGR-GIT 20dc97a
fix(rewards): add campaigns to state-logs snapshot
VGR-GIT 3e44f43
chore(rewards): fix prettier formatting in RewardsController.test.ts
VGR-GIT d413c28
feat(rewards): register getCampaigns in rewards controller messenger
VGR-GIT 0547350
fix(rewards): apply StateConstraint and type fixes for campaigns
VGR-GIT ddf3821
feat(rewards): add rewards-campaigns-enabled feature flag and useRewa…
VGR-GIT 6e17e12
fix(tests): add campaigns fields to RewardsState fixtures in reducer …
VGR-GIT 43287d5
fix(rewards): stabilize event arrays in useInvalidateByRewardEvents c…
VGR-GIT 2e5a30d
fix(rewards): accept readonly arrays in useInvalidateByRewardEvents
VGR-GIT c3608ec
test(rewards): add coverage for campaigns reducers and selectors
VGR-GIT d31ef7d
style(tests): format reducer test file with prettier
VGR-GIT File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
339 changes: 339 additions & 0 deletions
339
app/components/UI/Rewards/hooks/useRewardCampaigns.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,339 @@ | ||
| import { renderHook, act } from '@testing-library/react-hooks'; | ||
| import { useDispatch, useSelector } from 'react-redux'; | ||
| import { useFocusEffect } from '@react-navigation/native'; | ||
| import { useRewardCampaigns } from './useRewardCampaigns'; | ||
| import Engine from '../../../../core/Engine'; | ||
| import { | ||
| setCampaigns, | ||
| setCampaignsLoading, | ||
| setCampaignsError, | ||
| } from '../../../../reducers/rewards'; | ||
| import { | ||
| selectCampaigns, | ||
| selectCampaignsLoading, | ||
| selectCampaignsError, | ||
| } from '../../../../reducers/rewards/selectors'; | ||
| import { selectRewardsSubscriptionId } from '../../../../selectors/rewards'; | ||
| import { selectCampaignsRewardsEnabledFlag } from '../../../../selectors/featureFlagController/rewards'; | ||
| import { useInvalidateByRewardEvents } from './useInvalidateByRewardEvents'; | ||
| import type { | ||
| CampaignDto, | ||
| CampaignType, | ||
| } from '../../../../core/Engine/controllers/rewards-controller/types'; | ||
|
|
||
| // Mock dependencies | ||
| jest.mock('react-redux', () => ({ | ||
| useDispatch: jest.fn(), | ||
| useSelector: jest.fn(), | ||
| })); | ||
|
|
||
| jest.mock('../../../../core/Engine', () => ({ | ||
| controllerMessenger: { | ||
| call: jest.fn(), | ||
| }, | ||
| })); | ||
|
|
||
| jest.mock('../../../../reducers/rewards', () => ({ | ||
| setCampaigns: jest.fn(), | ||
| setCampaignsLoading: jest.fn(), | ||
| setCampaignsError: jest.fn(), | ||
| })); | ||
|
|
||
| jest.mock('../../../../reducers/rewards/selectors', () => ({ | ||
| selectCampaigns: jest.fn(), | ||
| selectCampaignsLoading: jest.fn(), | ||
| selectCampaignsError: jest.fn(), | ||
| })); | ||
|
|
||
| jest.mock('../../../../selectors/rewards', () => ({ | ||
| selectRewardsSubscriptionId: jest.fn(), | ||
| })); | ||
|
|
||
| jest.mock('../../../../selectors/featureFlagController/rewards', () => ({ | ||
| selectCampaignsRewardsEnabledFlag: jest.fn(), | ||
| })); | ||
|
|
||
| jest.mock('@react-navigation/native', () => ({ | ||
| useFocusEffect: jest.fn(), | ||
| })); | ||
|
|
||
| jest.mock('./useInvalidateByRewardEvents', () => ({ | ||
| useInvalidateByRewardEvents: jest.fn(), | ||
| })); | ||
|
|
||
| const createTestCampaign = ( | ||
| overrides: Partial<CampaignDto> = {}, | ||
| ): CampaignDto => ({ | ||
| id: 'campaign-1', | ||
| type: 'ONDO_HOLDING' as CampaignType, | ||
| name: 'ONDO Holding Campaign', | ||
| startDate: '2025-01-01T00:00:00.000Z', | ||
| endDate: '2027-01-01T00:00:00.000Z', | ||
| termsAndConditions: null, | ||
| excludedRegions: [], | ||
| statusLabel: 'Active', | ||
| ...overrides, | ||
| }); | ||
|
|
||
| describe('useRewardCampaigns', () => { | ||
| const mockDispatch = jest.fn(); | ||
| const mockEngineCall = Engine.controllerMessenger.call as jest.MockedFunction< | ||
| typeof Engine.controllerMessenger.call | ||
| >; | ||
| const mockUseDispatch = useDispatch as jest.MockedFunction< | ||
| typeof useDispatch | ||
| >; | ||
| const mockUseSelector = useSelector as jest.MockedFunction< | ||
| typeof useSelector | ||
| >; | ||
| const mockUseFocusEffect = useFocusEffect as jest.MockedFunction< | ||
| typeof useFocusEffect | ||
| >; | ||
| const mockUseInvalidateByRewardEvents = | ||
| useInvalidateByRewardEvents as jest.MockedFunction< | ||
| typeof useInvalidateByRewardEvents | ||
| >; | ||
| const mockSetCampaigns = setCampaigns as jest.MockedFunction< | ||
| typeof setCampaigns | ||
| >; | ||
| const mockSetCampaignsLoading = setCampaignsLoading as jest.MockedFunction< | ||
| typeof setCampaignsLoading | ||
| >; | ||
| const mockSetCampaignsError = setCampaignsError as jest.MockedFunction< | ||
| typeof setCampaignsError | ||
| >; | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| mockUseDispatch.mockReturnValue(mockDispatch); | ||
| mockSetCampaigns.mockReturnValue({ | ||
| type: 'rewards/setCampaigns', | ||
| payload: [], | ||
| }); | ||
| mockSetCampaignsLoading.mockReturnValue({ | ||
| type: 'rewards/setCampaignsLoading', | ||
| payload: false, | ||
| }); | ||
| mockSetCampaignsError.mockReturnValue({ | ||
| type: 'rewards/setCampaignsError', | ||
| payload: false, | ||
| }); | ||
| mockUseFocusEffect.mockClear(); | ||
| mockUseInvalidateByRewardEvents.mockClear(); | ||
| }); | ||
|
|
||
| const setupSelectorMocks = ( | ||
| options: { | ||
| subscriptionId?: string | null; | ||
| campaigns?: CampaignDto[]; | ||
| isLoading?: boolean; | ||
| hasError?: boolean; | ||
| isCampaignsEnabled?: boolean; | ||
| } = {}, | ||
| ) => { | ||
| const { | ||
| subscriptionId = 'subscription-1', | ||
| campaigns = [], | ||
| isLoading = false, | ||
| hasError = false, | ||
| isCampaignsEnabled = true, | ||
| } = options; | ||
|
|
||
| mockUseSelector.mockImplementation((selector) => { | ||
| if (selector === selectRewardsSubscriptionId) return subscriptionId; | ||
| if (selector === selectCampaigns) return campaigns; | ||
| if (selector === selectCampaignsLoading) return isLoading; | ||
| if (selector === selectCampaignsError) return hasError; | ||
| if (selector === selectCampaignsRewardsEnabledFlag) | ||
| return isCampaignsEnabled; | ||
| return undefined; | ||
| }); | ||
| }; | ||
|
|
||
| describe('initial state', () => { | ||
| it('returns initial state from selectors', () => { | ||
| const testCampaigns = [createTestCampaign()]; | ||
| setupSelectorMocks({ campaigns: testCampaigns }); | ||
|
|
||
| const { result } = renderHook(() => useRewardCampaigns()); | ||
|
|
||
| expect(result.current.campaigns).toEqual(testCampaigns); | ||
| expect(result.current.isLoading).toBe(false); | ||
| expect(result.current.hasError).toBe(false); | ||
| expect(typeof result.current.fetchCampaigns).toBe('function'); | ||
| }); | ||
|
|
||
| it('returns empty array when campaigns selector returns undefined', () => { | ||
| setupSelectorMocks({ campaigns: undefined as unknown as CampaignDto[] }); | ||
| mockUseSelector.mockImplementation((selector) => { | ||
| if (selector === selectCampaigns) return undefined; | ||
| if (selector === selectRewardsSubscriptionId) return 'subscription-1'; | ||
| if (selector === selectCampaignsLoading) return false; | ||
| if (selector === selectCampaignsError) return false; | ||
| if (selector === selectCampaignsRewardsEnabledFlag) return true; | ||
| return undefined; | ||
| }); | ||
|
|
||
| const { result } = renderHook(() => useRewardCampaigns()); | ||
|
|
||
| expect(result.current.campaigns).toEqual([]); | ||
| }); | ||
| }); | ||
|
|
||
| describe('fetchCampaigns', () => { | ||
| it('calls Engine controller when fetching campaigns', async () => { | ||
| setupSelectorMocks(); | ||
| const mockCampaignsData = [createTestCampaign()]; | ||
| mockEngineCall.mockResolvedValueOnce(mockCampaignsData); | ||
|
|
||
| const { result } = renderHook(() => useRewardCampaigns()); | ||
|
|
||
| await act(async () => { | ||
| await result.current.fetchCampaigns(); | ||
| }); | ||
|
|
||
| expect(mockEngineCall).toHaveBeenCalledWith( | ||
| 'RewardsController:getCampaigns', | ||
| 'subscription-1', | ||
| ); | ||
| }); | ||
|
|
||
| it('dispatches loading state before fetch', async () => { | ||
| setupSelectorMocks(); | ||
| mockEngineCall.mockResolvedValueOnce([]); | ||
|
|
||
| const { result } = renderHook(() => useRewardCampaigns()); | ||
|
|
||
| await act(async () => { | ||
| await result.current.fetchCampaigns(); | ||
| }); | ||
|
|
||
| expect(mockDispatch).toHaveBeenCalledWith(mockSetCampaignsLoading(true)); | ||
| expect(mockDispatch).toHaveBeenCalledWith(mockSetCampaignsError(false)); | ||
| }); | ||
|
|
||
| it('dispatches campaigns on successful fetch', async () => { | ||
| setupSelectorMocks(); | ||
| const mockCampaignsData = [createTestCampaign()]; | ||
| mockEngineCall.mockResolvedValueOnce(mockCampaignsData); | ||
|
|
||
| const { result } = renderHook(() => useRewardCampaigns()); | ||
|
|
||
| await act(async () => { | ||
| await result.current.fetchCampaigns(); | ||
| }); | ||
|
|
||
| expect(mockDispatch).toHaveBeenCalledWith( | ||
| mockSetCampaigns(mockCampaignsData), | ||
| ); | ||
| expect(mockDispatch).toHaveBeenCalledWith(mockSetCampaignsLoading(false)); | ||
| }); | ||
|
|
||
| it('dispatches error state on fetch failure', async () => { | ||
| setupSelectorMocks(); | ||
| const mockError = new Error('Network failed'); | ||
| mockEngineCall.mockRejectedValueOnce(mockError); | ||
|
|
||
| const { result } = renderHook(() => useRewardCampaigns()); | ||
|
|
||
| await act(async () => { | ||
| await result.current.fetchCampaigns(); | ||
| }); | ||
|
|
||
| expect(mockDispatch).toHaveBeenCalledWith(mockSetCampaignsError(true)); | ||
| expect(mockDispatch).toHaveBeenCalledWith(mockSetCampaignsLoading(false)); | ||
| }); | ||
|
|
||
| it('returns empty list and does not fetch when feature flag is disabled', async () => { | ||
| setupSelectorMocks({ isCampaignsEnabled: false }); | ||
|
|
||
| const { result } = renderHook(() => useRewardCampaigns()); | ||
|
|
||
| await act(async () => { | ||
| await result.current.fetchCampaigns(); | ||
| }); | ||
|
|
||
| expect(mockEngineCall).not.toHaveBeenCalled(); | ||
| expect(mockDispatch).toHaveBeenCalledWith(mockSetCampaigns([])); | ||
| expect(mockDispatch).toHaveBeenCalledWith(mockSetCampaignsLoading(false)); | ||
| expect(mockDispatch).toHaveBeenCalledWith(mockSetCampaignsError(false)); | ||
| }); | ||
|
|
||
| it('does not fetch when subscriptionId is null', async () => { | ||
| setupSelectorMocks({ subscriptionId: null }); | ||
|
|
||
| const { result } = renderHook(() => useRewardCampaigns()); | ||
|
|
||
| await act(async () => { | ||
| await result.current.fetchCampaigns(); | ||
| }); | ||
|
|
||
| expect(mockEngineCall).not.toHaveBeenCalled(); | ||
| expect(mockDispatch).toHaveBeenCalledWith(mockSetCampaigns([])); | ||
| expect(mockDispatch).toHaveBeenCalledWith(mockSetCampaignsLoading(false)); | ||
| expect(mockDispatch).toHaveBeenCalledWith(mockSetCampaignsError(false)); | ||
| }); | ||
| }); | ||
|
|
||
| describe('useFocusEffect integration', () => { | ||
| it('registers focus effect callback', () => { | ||
| setupSelectorMocks(); | ||
|
|
||
| renderHook(() => useRewardCampaigns()); | ||
|
|
||
| expect(mockUseFocusEffect).toHaveBeenCalledWith(expect.any(Function)); | ||
| }); | ||
|
|
||
| it('fetches campaigns when focus effect is triggered', async () => { | ||
| setupSelectorMocks(); | ||
| const mockCampaignsData = [createTestCampaign()]; | ||
| mockEngineCall.mockResolvedValueOnce(mockCampaignsData); | ||
|
|
||
| renderHook(() => useRewardCampaigns()); | ||
|
|
||
| const focusCallback = mockUseFocusEffect.mock.calls[0][0]; | ||
|
|
||
| await act(async () => { | ||
| focusCallback(); | ||
| }); | ||
|
|
||
| expect(mockEngineCall).toHaveBeenCalledWith( | ||
| 'RewardsController:getCampaigns', | ||
| 'subscription-1', | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('useInvalidateByRewardEvents integration', () => { | ||
| it('registers invalidation events', () => { | ||
| setupSelectorMocks(); | ||
|
|
||
| renderHook(() => useRewardCampaigns()); | ||
|
|
||
| expect(mockUseInvalidateByRewardEvents).toHaveBeenCalledWith( | ||
| ['RewardsController:accountLinked', 'RewardsController:balanceUpdated'], | ||
| expect.any(Function), | ||
| ); | ||
| }); | ||
|
|
||
| it('passes fetchCampaigns as callback to invalidation hook', async () => { | ||
| setupSelectorMocks(); | ||
| const mockCampaignsData = [createTestCampaign()]; | ||
| mockEngineCall.mockResolvedValueOnce(mockCampaignsData); | ||
|
|
||
| renderHook(() => useRewardCampaigns()); | ||
|
|
||
| const invalidationCallback = | ||
| mockUseInvalidateByRewardEvents.mock.calls[0][1]; | ||
|
|
||
| await act(async () => { | ||
| await invalidationCallback(); | ||
| }); | ||
|
|
||
| expect(mockEngineCall).toHaveBeenCalledWith( | ||
| 'RewardsController:getCampaigns', | ||
| 'subscription-1', | ||
| ); | ||
| }); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.