From 76209d72ae300c594da2fd77422fe7341beea48f Mon Sep 17 00:00:00 2001 From: Faisat-Creator <2.72928242e+08+Faisat-Creator@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:45:27 +0000 Subject: [PATCH] test: cover sentry streams and mock data Closes #734\nCloses #736\nCloses #737\nCloses #738 --- __tests__/hooks/use-archived-streams.test.ts | 75 +++++++++++++++++++ __tests__/hooks/use-hidden-streams.test.ts | 59 +++++++++++++++ __tests__/lib/mock-data.test.ts | 77 ++++++++++++++++++++ __tests__/lib/sentry.test.ts | 55 ++++++++++++++ 4 files changed, 266 insertions(+) create mode 100644 __tests__/hooks/use-archived-streams.test.ts create mode 100644 __tests__/hooks/use-hidden-streams.test.ts create mode 100644 __tests__/lib/mock-data.test.ts create mode 100644 __tests__/lib/sentry.test.ts diff --git a/__tests__/hooks/use-archived-streams.test.ts b/__tests__/hooks/use-archived-streams.test.ts new file mode 100644 index 0000000..b163e76 --- /dev/null +++ b/__tests__/hooks/use-archived-streams.test.ts @@ -0,0 +1,75 @@ +import { act, renderHook, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const fetchSent = vi.hoisted(() => vi.fn()) +const fetchReceived = vi.hoisted(() => vi.fn()) +const captureError = vi.hoisted(() => vi.fn()) + +vi.mock('@/lib/contract', () => ({ + fetchArchivedSentStreamIds: fetchSent, + fetchArchivedReceivedStreamIds: fetchReceived, +})) +vi.mock('@/components/providers/network-provider', () => ({ + useNetwork: () => ({ network: 'testnet' }), +})) +vi.mock('@/lib/sentry', () => ({ captureError })) + +import { useArchivedStreams } from '@/hooks/use-archived-streams' + +describe('useArchivedStreams', () => { + beforeEach(() => { + vi.clearAllMocks() + fetchSent.mockResolvedValue(['sent-1', 'sent-2']) + fetchReceived.mockResolvedValue(['received-1']) + }) + + it('loads archived sent and received IDs for an address', async () => { + const { result } = renderHook(() => useArchivedStreams('GADDRESS')) + + await waitFor(() => expect(result.current.loading).toBe(false)) + + expect(fetchSent).toHaveBeenCalledWith('testnet', 'GADDRESS') + expect(fetchReceived).toHaveBeenCalledWith('testnet', 'GADDRESS') + expect(result.current.sent).toEqual(['sent-1', 'sent-2']) + expect(result.current.received).toEqual(['received-1']) + }) + + it('clears results and does not fetch when the address is null', async () => { + const { result } = renderHook(() => useArchivedStreams(null)) + + await waitFor(() => expect(result.current.loading).toBe(false)) + + expect(result.current.sent).toEqual([]) + expect(result.current.received).toEqual([]) + expect(fetchSent).not.toHaveBeenCalled() + expect(fetchReceived).not.toHaveBeenCalled() + }) + + it('refetches both archive lists on demand', async () => { + const { result } = renderHook(() => useArchivedStreams('GADDRESS')) + await waitFor(() => expect(result.current.loading).toBe(false)) + fetchSent.mockResolvedValueOnce(['sent-3']) + fetchReceived.mockResolvedValueOnce(['received-2']) + + await act(async () => { + await result.current.refetch() + }) + + expect(result.current.sent).toEqual(['sent-3']) + expect(result.current.received).toEqual(['received-2']) + expect(fetchSent).toHaveBeenCalledTimes(2) + expect(fetchReceived).toHaveBeenCalledTimes(2) + }) + + it('reports a failed archive query and clears loading', async () => { + const error = new Error('RPC unavailable') + fetchSent.mockRejectedValueOnce(error) + + const { result } = renderHook(() => useArchivedStreams('GADDRESS')) + await waitFor(() => expect(result.current.loading).toBe(false)) + + expect(captureError).toHaveBeenCalledWith(error, { + operation: 'use-archived-streams:fetch', + }) + }) +}) diff --git a/__tests__/hooks/use-hidden-streams.test.ts b/__tests__/hooks/use-hidden-streams.test.ts new file mode 100644 index 0000000..1a225c5 --- /dev/null +++ b/__tests__/hooks/use-hidden-streams.test.ts @@ -0,0 +1,59 @@ +import { act, renderHook, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, it } from 'vitest' +import { useHiddenStreams } from '@/hooks/use-hidden-streams' + +const HIDDEN_KEY = 'flowstar:hidden-streams' +const BLOCKED_KEY = 'flowstar:blocked-senders' +const PINNED_KEY = 'flowstar:pinned-streams' + +describe('useHiddenStreams', () => { + beforeEach(() => localStorage.clear()) + + it('hydrates hidden, blocked, and pinned values from localStorage', async () => { + localStorage.setItem(HIDDEN_KEY, JSON.stringify(['stream-1'])) + localStorage.setItem(BLOCKED_KEY, JSON.stringify(['GSENDER'])) + localStorage.setItem(PINNED_KEY, JSON.stringify(['stream-2'])) + + const { result } = renderHook(() => useHiddenStreams()) + await waitFor(() => expect(result.current.hiddenIds.has('stream-1')).toBe(true)) + + expect(result.current.blockedSenders).toEqual(new Set(['GSENDER'])) + expect(result.current.pinnedIds).toEqual(new Set(['stream-2'])) + expect(result.current.isHidden('stream-1')).toBe(true) + expect(result.current.isBlocked('GSENDER')).toBe(true) + expect(result.current.isPinned('stream-2')).toBe(true) + }) + + it('reacts to hide, block, and pin mutations made through the hook', async () => { + const { result } = renderHook(() => useHiddenStreams()) + + act(() => { + result.current.hideStream('stream-1') + result.current.blockSender('GSENDER') + result.current.pinStream('stream-1') + }) + await waitFor(() => expect(result.current.isHidden('stream-1')).toBe(true)) + + expect(result.current.isBlocked('GSENDER')).toBe(true) + expect(result.current.isPinned('stream-1')).toBe(true) + expect(JSON.parse(localStorage.getItem(HIDDEN_KEY)!)).toEqual(['stream-1']) + }) + + it('updates state when values are removed', async () => { + localStorage.setItem(HIDDEN_KEY, JSON.stringify(['stream-1'])) + localStorage.setItem(BLOCKED_KEY, JSON.stringify(['GSENDER'])) + localStorage.setItem(PINNED_KEY, JSON.stringify(['stream-1'])) + const { result } = renderHook(() => useHiddenStreams()) + await waitFor(() => expect(result.current.isHidden('stream-1')).toBe(true)) + + act(() => { + result.current.unhideStream('stream-1') + result.current.unblockSender('GSENDER') + result.current.unpinStream('stream-1') + }) + await waitFor(() => expect(result.current.isHidden('stream-1')).toBe(false)) + + expect(result.current.isBlocked('GSENDER')).toBe(false) + expect(result.current.isPinned('stream-1')).toBe(false) + }) +}) diff --git a/__tests__/lib/mock-data.test.ts b/__tests__/lib/mock-data.test.ts new file mode 100644 index 0000000..724590b --- /dev/null +++ b/__tests__/lib/mock-data.test.ts @@ -0,0 +1,77 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { DEMO_ADDRESS, mockStore } from '@/lib/mock-data' +import type { CreateStreamInput } from '@/types/stream' + +const token = { address: 'CTOKEN', symbol: 'TEST', decimals: 7 } +const makeInput = (overrides: Partial = {}): CreateStreamInput => ({ + recipient: 'GRECIPIENT', + token, + totalAmount: 10_000n, + startTime: 100n, + endTime: 200n, + cliffTime: 100n, + cliffAmount: 1_000n, + ...overrides, +}) + +function cleanup(id: string) { + mockStore.cleanup(id) +} + +describe('mockStore', () => { + const createdIds: string[] = [] + afterEach(() => { + createdIds.splice(0).forEach(cleanup) + }) + + it('creates a stream with derived duration, linear amount, and rate', () => { + const stream = mockStore.create(makeInput(), 'GSENDER') + createdIds.push(stream.id) + + expect(stream.sender).toBe('GSENDER') + expect(stream.depositedAmount).toBe(10_000n) + expect(stream.duration).toBe(100n) + expect(stream.linearAmount).toBe(9_000n) + expect(stream.amountPerSecond).toBe(90n) + expect(mockStore.getById(stream.id)).toEqual(stream) + expect(mockStore.getAll()).toContainEqual(stream) + }) + + it('emits updates for create, withdraw, cancel, and cleanup', () => { + const listener = vi.fn() + const unsubscribe = mockStore.subscribe(listener) + const stream = mockStore.create(makeInput(), 'GSENDER') + createdIds.push(stream.id) + expect(listener).toHaveBeenCalledTimes(1) + + mockStore.withdraw(stream.id, 500n) + mockStore.cancel(stream.id) + expect(listener).toHaveBeenCalledTimes(3) + expect(mockStore.getById(stream.id)).toMatchObject({ withdrawnAmount: 500n, cancelled: true }) + + mockStore.cleanup(stream.id) + expect(listener).toHaveBeenCalledTimes(4) + expect(mockStore.getById(stream.id)).toBeUndefined() + unsubscribe() + mockStore.cancel(stream.id) + expect(listener).toHaveBeenCalledTimes(4) + }) + + it('returns archived streams for either participant and excludes unrelated streams', () => { + const senderStream = mockStore.create(makeInput({ recipient: 'GRECIPIENT' }), 'GSENDER') + const recipientStream = mockStore.create(makeInput({ recipient: DEMO_ADDRESS }), 'GOTHER') + const activeStream = mockStore.create(makeInput({ recipient: 'GACTIVE' }), 'GSENDER') + createdIds.push(senderStream.id, recipientStream.id, activeStream.id) + mockStore.cancel(senderStream.id) + mockStore.cancel(recipientStream.id) + + expect(mockStore.getArchived('GSENDER').map((stream) => stream.id)).toContain(senderStream.id) + expect(mockStore.getArchived(DEMO_ADDRESS).map((stream) => stream.id)).toContain( + recipientStream.id, + ) + expect(mockStore.getArchived('GUNRELATED')).toEqual([]) + expect(mockStore.getArchived('GSENDER').map((stream) => stream.id)).not.toContain( + activeStream.id, + ) + }) +}) diff --git a/__tests__/lib/sentry.test.ts b/__tests__/lib/sentry.test.ts new file mode 100644 index 0000000..0acd7be --- /dev/null +++ b/__tests__/lib/sentry.test.ts @@ -0,0 +1,55 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const sentry = vi.hoisted(() => ({ + withScope: vi.fn(), + captureException: vi.fn(), + setUser: vi.fn(), +})) + +vi.mock('@sentry/nextjs', () => sentry) + +import { captureError, setSentryUser } from '@/lib/sentry' + +describe('lib/sentry', () => { + beforeEach(() => { + vi.clearAllMocks() + sentry.withScope.mockImplementation((callback: (scope: unknown) => void) => + callback({ setUser: vi.fn(), setTag: vi.fn(), setExtra: vi.fn() }), + ) + }) + + it('captures an error through the SDK', async () => { + const error = new Error('request failed') + await captureError(error) + + expect(sentry.withScope).toHaveBeenCalledOnce() + expect(sentry.captureException).toHaveBeenCalledWith(error) + }) + + it('adds redacted wallet, operation, and extra context to the scope', async () => { + const scope = { setUser: vi.fn(), setTag: vi.fn(), setExtra: vi.fn() } + sentry.withScope.mockImplementation((callback: (value: typeof scope) => void) => + callback(scope), + ) + + await captureError('failure', { + walletAddress: 'GABCDEF123456789', + operation: 'withdraw', + extra: { streamId: '42', retryable: true }, + }) + + expect(scope.setUser).toHaveBeenCalledWith({ id: 'GABCDEF1…' }) + expect(scope.setTag).toHaveBeenCalledWith('operation', 'withdraw') + expect(scope.setExtra).toHaveBeenCalledWith('streamId', '42') + expect(scope.setExtra).toHaveBeenCalledWith('retryable', true) + expect(sentry.captureException).toHaveBeenCalledWith('failure') + }) + + it('sets and clears the connected wallet user', async () => { + await setSentryUser('GABCDEF123456789') + await setSentryUser(null) + + expect(sentry.setUser).toHaveBeenNthCalledWith(1, { id: 'GABCDEF1…' }) + expect(sentry.setUser).toHaveBeenNthCalledWith(2, null) + }) +})