From 8e2d99ebd933f7d301814d95960f43805ca9bbc8 Mon Sep 17 00:00:00 2001 From: AbuTuraab Date: Tue, 28 Jul 2026 23:13:16 +0100 Subject: [PATCH 1/2] added unit test suite for payment stream creation hook --- apps/web/src/hooks/use-create-stream.test.ts | 117 +++++++++++++++++++ apps/web/src/hooks/use-create-stream.ts | 4 +- 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/hooks/use-create-stream.test.ts diff --git a/apps/web/src/hooks/use-create-stream.test.ts b/apps/web/src/hooks/use-create-stream.test.ts new file mode 100644 index 0000000..410dda1 --- /dev/null +++ b/apps/web/src/hooks/use-create-stream.test.ts @@ -0,0 +1,117 @@ +import { act, renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { createElement, type ReactNode } from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const { createStreamMock, toastErrorMock, walletMock } = vi.hoisted(() => ({ + createStreamMock: vi.fn(), + toastErrorMock: vi.fn(), + walletMock: { + address: 'GSENDER' as string | null, + signTransaction: vi.fn(), + }, +})); + +vi.mock('@/lib/api', () => ({ createStream: createStreamMock })); +vi.mock('@/providers/StellarWalletProvider', () => ({ useWallet: () => walletMock })); +vi.mock('react-hot-toast', () => ({ default: { error: toastErrorMock } })); + +import { useCreateStream } from './use-create-stream'; + +const streamInput = { + recipient: 'GRECIPIENT', + token: 'CTOKEN', + amount: 100n, + startTime: 1_700_000_000, + endTime: 1_700_003_600, +}; + +function createWrapper(queryClient: QueryClient) { + return ({ children }: { children: ReactNode }) => + createElement(QueryClientProvider, { client: queryClient }, children); +} + +describe('useCreateStream', () => { + let queryClient: QueryClient; + + beforeEach(() => { + vi.clearAllMocks(); + walletMock.address = 'GSENDER'; + queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + }); + + it('optimistically appends an active stream and refetches after creation', async () => { + let resolveCreate!: (streamId: number) => void; + createStreamMock.mockImplementation( + () => new Promise((resolve) => { resolveCreate = resolve; }) + ); + queryClient.setQueryData(['streams', 'GSENDER'], [{ id: 7, status: 'Active' }]); + const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); + const { result } = renderHook(() => useCreateStream(), { + wrapper: createWrapper(queryClient), + }); + + let mutation!: Promise; + act(() => { + mutation = result.current.mutateAsync(streamInput); + }); + + await waitFor(() => { + expect(queryClient.getQueryData(['streams', 'GSENDER'])).toEqual([ + { id: 7, status: 'Active' }, + { ...streamInput, id: -1, status: 'Active' }, + ]); + }); + expect(createStreamMock).toHaveBeenCalledWith({ + ...streamInput, + sender: 'GSENDER', + signTransaction: walletMock.signTransaction, + }); + + resolveCreate(8); + await expect(mutation).resolves.toBe(8); + await waitFor(() => { + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['streams'] }); + }); + }); + + it('restores all stream caches and notifies the user when creation fails', async () => { + createStreamMock.mockRejectedValue(new Error('Network unavailable')); + const userStreams = [{ id: 7, status: 'Active' }]; + const dashboardStreams = [{ id: 3, status: 'Paused' }]; + queryClient.setQueryData(['streams', 'GSENDER'], userStreams); + queryClient.setQueryData(['streams', 'dashboard'], dashboardStreams); + const { result } = renderHook(() => useCreateStream(), { + wrapper: createWrapper(queryClient), + }); + + await expect(result.current.mutateAsync(streamInput)).rejects.toThrow('Network unavailable'); + + expect(queryClient.getQueryData(['streams', 'GSENDER'])).toEqual(userStreams); + expect(queryClient.getQueryData(['streams', 'dashboard'])).toEqual(dashboardStreams); + expect(toastErrorMock).toHaveBeenCalledWith('Failed to create stream. Refreshing latest data.'); + }); + + it('uses an explicit sender instead of the connected wallet address', async () => { + createStreamMock.mockResolvedValue(8); + const { result } = renderHook(() => useCreateStream(), { + wrapper: createWrapper(queryClient), + }); + + await expect(result.current.mutateAsync({ ...streamInput, sender: 'GOVERRIDE' })).resolves.toBe(8); + + expect(createStreamMock).toHaveBeenCalledWith(expect.objectContaining({ sender: 'GOVERRIDE' })); + }); + + it('rejects stream creation when neither an input sender nor a wallet is available', async () => { + walletMock.address = null; + const { result } = renderHook(() => useCreateStream(), { + wrapper: createWrapper(queryClient), + }); + + await expect(result.current.mutateAsync(streamInput)).rejects.toThrow('Wallet not connected'); + expect(createStreamMock).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/hooks/use-create-stream.ts b/apps/web/src/hooks/use-create-stream.ts index 2b6ee49..166ab8a 100644 --- a/apps/web/src/hooks/use-create-stream.ts +++ b/apps/web/src/hooks/use-create-stream.ts @@ -3,7 +3,9 @@ import toast from 'react-hot-toast'; import { createStream } from '@/lib/api'; import { useWallet } from '@/providers/StellarWalletProvider'; -type CreateStreamInput = Parameters[0]; +type CreateStreamInput = Omit[0], 'sender' | 'signTransaction'> & { + sender?: string; +}; interface CreateStreamMutationContext { previousStreams: Array<[QueryKey, unknown]>; From 65d785a318ecaa89e67a2b58d9dc43b4ce8cafdf Mon Sep 17 00:00:00 2001 From: AbuTuraab Date: Wed, 29 Jul 2026 10:25:22 +0100 Subject: [PATCH 2/2] Update apps/web/src/hooks/use-create-stream.test.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- apps/web/src/hooks/use-create-stream.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/web/src/hooks/use-create-stream.test.ts b/apps/web/src/hooks/use-create-stream.test.ts index 410dda1..d50b12c 100644 --- a/apps/web/src/hooks/use-create-stream.test.ts +++ b/apps/web/src/hooks/use-create-stream.test.ts @@ -27,8 +27,12 @@ const streamInput = { }; function createWrapper(queryClient: QueryClient) { - return ({ children }: { children: ReactNode }) => - createElement(QueryClientProvider, { client: queryClient }, children); + function QueryClientWrapper({ children }: { children: ReactNode }) { + return createElement(QueryClientProvider, { client: queryClient }, children); + } + + return QueryClientWrapper; +} } describe('useCreateStream', () => {