diff --git a/public/contexts/__mocks__/core_context.tsx b/public/contexts/__mocks__/core_context.tsx new file mode 100644 index 00000000..3d6d1427 --- /dev/null +++ b/public/contexts/__mocks__/core_context.tsx @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BehaviorSubject } from 'rxjs'; +import { coreMock } from '../../../../../src/core/public/mocks'; + +export const useCore = jest.fn(() => { + const useCoreMock = { + services: { + ...coreMock.createStart(), + sessions: { + sessions$: new BehaviorSubject({ + objects: [ + { + id: '1', + title: 'foo', + }, + ], + total: 1, + }), + status$: new BehaviorSubject('idle'), + load: jest.fn(), + }, + sessionLoad: {}, + }, + }; + useCoreMock.services.http.delete.mockReturnValue(Promise.resolve()); + useCoreMock.services.http.put.mockReturnValue(Promise.resolve()); + return useCoreMock; +}); diff --git a/public/hooks/__tests__/fetch_reducer.test.ts b/public/hooks/__tests__/fetch_reducer.test.ts new file mode 100644 index 00000000..232d232d --- /dev/null +++ b/public/hooks/__tests__/fetch_reducer.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { genericReducer } from '../fetch_reducer'; + +describe('genericReducer', () => { + it('should return original state', () => { + expect( + genericReducer( + { data: { foo: 'bar' }, loading: false }, + // mock not supported type + { type: ('not-supported-type' as unknown) as 'request' } + ) + ).toEqual({ + data: { foo: 'bar' }, + loading: false, + }); + }); + + it('should return state follow request action', () => { + expect(genericReducer({ data: { foo: 'bar' }, loading: false }, { type: 'request' })).toEqual({ + data: { foo: 'bar' }, + loading: true, + }); + }); + + it('should return state follow success action', () => { + expect( + genericReducer( + { data: { foo: 'bar' }, loading: false }, + { type: 'success', payload: { foo: 'baz' } } + ) + ).toEqual({ + data: { foo: 'baz' }, + loading: false, + }); + }); + + it('should return state follow failure action', () => { + const error = new Error(); + expect( + genericReducer({ data: { foo: 'bar' }, loading: false }, { type: 'failure', error }) + ).toEqual({ + error, + loading: false, + }); + expect( + genericReducer( + { data: { foo: 'bar' }, loading: false }, + { type: 'failure', error: { body: error } } + ) + ).toEqual({ + error, + loading: false, + }); + }); +}); diff --git a/public/hooks/__tests__/use_sessions.test.ts b/public/hooks/__tests__/use_sessions.test.ts new file mode 100644 index 00000000..53f1768d --- /dev/null +++ b/public/hooks/__tests__/use_sessions.test.ts @@ -0,0 +1,180 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useDeleteSession, usePatchSession } from '../use_sessions'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { useCore } from '../../contexts/core_context'; +import { HttpHandler } from '../../../../../src/core/public'; + +jest.mock('../../contexts/core_context'); +const useCoreMocked = useCore as jest.MockedFunction; + +describe('useDeleteSession', () => { + it('should call delete with path and signal', async () => { + const { result } = renderHook(() => useDeleteSession()); + + await act(async () => { + await result.current.deleteSession('foo'); + }); + expect(useCoreMocked.mock.results[0].value.services.http.delete).toHaveBeenCalledWith( + '/api/assistant/session/foo', + expect.objectContaining({ + signal: expect.any(Object), + }) + ); + }); + + it('should be loading after deleteSession called', async () => { + const { result, waitFor } = renderHook(() => useDeleteSession()); + useCoreMocked.mock.results[0].value.services.http.delete.mockReturnValue(new Promise(() => {})); + + act(() => { + result.current.deleteSession('foo'); + }); + + await waitFor(() => { + expect(result.current.loading).toBe(true); + }); + }); + + it('should return data after delete success', async () => { + const { result, waitFor } = renderHook(() => useDeleteSession()); + useCoreMocked.mock.results[0].value.services.http.delete.mockReturnValue( + Promise.resolve('deleted') + ); + + act(() => { + result.current.deleteSession('foo'); + }); + + await waitFor(() => { + expect(result.current.data).toBe('deleted'); + expect(result.current.loading).toBe(false); + }); + }); + + it('should throw error after abort', async () => { + const { result, waitFor } = renderHook(() => useDeleteSession()); + const abortErrorMock = new Error('Abort'); + useCoreMocked.mock.results[0].value.services.http.delete.mockImplementation((( + _path, + options + ) => { + return new Promise((_resolve, reject) => { + if (options?.signal) { + options.signal.onabort = () => { + reject(abortErrorMock); + }; + } + }); + }) as HttpHandler); + + let deleteSessionPromise: Promise; + act(() => { + deleteSessionPromise = result.current.deleteSession('foo'); + }); + + let deleteSessionError; + await act(async () => { + result.current.abort(); + try { + await deleteSessionPromise; + } catch (error) { + deleteSessionError = error; + } + }); + + expect(result.current.isAborted()).toBe(true); + expect(deleteSessionError).toBe(abortErrorMock); + + await waitFor(() => { + expect(result.current.error).toBe(abortErrorMock); + }); + }); +}); + +describe('usePatchSession', () => { + it('should call put with path, query and signal', async () => { + const { result } = renderHook(() => usePatchSession()); + + await act(async () => { + await result.current.patchSession('foo', 'new-title'); + }); + expect(useCoreMocked.mock.results[0].value.services.http.put).toHaveBeenCalledWith( + '/api/assistant/session/foo', + expect.objectContaining({ + signal: expect.any(Object), + body: JSON.stringify({ title: 'new-title' }), + }) + ); + }); + + it('should be loading after patchSession called', async () => { + const { result, waitFor } = renderHook(() => usePatchSession()); + useCoreMocked.mock.results[0].value.services.http.put.mockReturnValue(new Promise(() => {})); + + act(() => { + result.current.patchSession('foo', 'new-title'); + }); + + await waitFor(() => { + expect(result.current.loading).toBe(true); + }); + }); + + it('should return data after patch session success', async () => { + const { result, waitFor } = renderHook(() => usePatchSession()); + useCoreMocked.mock.results[0].value.services.http.put.mockReturnValue( + Promise.resolve({ + title: 'new-title', + }) + ); + + act(() => { + result.current.patchSession('foo', 'new-title'); + }); + + await waitFor(() => { + expect(result.current.data).toEqual({ title: 'new-title' }); + expect(result.current.loading).toBe(false); + }); + }); + + it('should throw error after abort', async () => { + const { result, waitFor } = renderHook(() => usePatchSession()); + const abortErrorMock = new Error('Abort'); + useCoreMocked.mock.results[0].value.services.http.put.mockImplementation(((_path, options) => { + return new Promise((_resolve, reject) => { + if (options?.signal) { + options.signal.onabort = () => { + reject(abortErrorMock); + }; + } + }); + }) as HttpHandler); + + let patchSessionPromise: Promise; + act(() => { + patchSessionPromise = result.current.patchSession('foo', 'new-title'); + }); + + let patchSessionError; + await act(async () => { + result.current.abort(); + try { + await patchSessionPromise; + } catch (error) { + patchSessionError = error; + } + }); + + expect(result.current.isAborted()).toBe(true); + expect(patchSessionError).toBe(abortErrorMock); + + await waitFor(() => { + expect(result.current.error).toBe(abortErrorMock); + }); + }); +});