import { renderHook, waitFor } from "@testing-library/react-native"; import useSyncObservations from "components/MyObservations/hooks/useSyncObservations"; import inatjs from "inaturalistjs"; import safeRealmWrite from "sharedHelpers/safeRealmWrite"; import { BEGIN_AUTOMATIC_SYNC, } from "stores/createSyncObservationsSlice"; import useStore from "stores/useStore"; import factory, { makeResponse } from "tests/factory"; import faker from "tests/helpers/faker"; import setupUniqueRealm from "tests/helpers/uniqueRealm"; const initialStoreState = useStore.getState( ); const currentUserId = 1; const syncedObservations = [ factory( "LocalObservation", { _deleted_at: faker.date.past( ), _synced_at: faker.date.past( ), } ), ]; const unsyncedObservations = [ factory( "LocalObservation", { _deleted_at: faker.date.past( ), _synced_at: null, } ), factory( "LocalObservation", { _deleted_at: faker.date.past( ), _synced_at: null, } ), ]; const obsToDelete = unsyncedObservations[0]; const syncingStore = { currentDeleteCount: 1, deleteQueue: [obsToDelete.uuid], syncingStatus: BEGIN_AUTOMATIC_SYNC, deleteError: null, deletionsCompletedAt: null, }; const mockDeletedIds = [ faker.number.int( ), ]; const mockRemoteObservation = factory( "RemoteObservation", { taxon: factory.states( "genus" )( "RemoteTaxon" ), } ); const mockMutateAsync = jest.fn(); jest.mock( "sharedHooks/useAuthenticatedMutation", ( ) => ( { __esModule: true, default: ( ) => ( { mutateAsync: mockMutateAsync, } ), } ) ); const mockUpload = jest.fn( ); jest.mock( "components/MyObservations/hooks/useUploadObservations", ( ) => ( { __esModule: true, default: ( ) => ( { startUploadObservations: mockUpload, } ), } ) ); // UNIQUE REALM SETUP const mockRealmIdentifier = __filename; const { mockRealmModelsIndex, uniqueRealmBeforeAll, uniqueRealmAfterAll } = setupUniqueRealm( mockRealmIdentifier, ); jest.mock( "realmModels/index", ( ) => mockRealmModelsIndex ); jest.mock( "providers/contexts", ( ) => { const originalModule = jest.requireActual( "providers/contexts" ); return { __esModule: true, ...originalModule, RealmContext: { ...originalModule.RealmContext, useRealm: ( ) => global.mockRealms[mockRealmIdentifier], useQuery: ( ) => [], }, }; } ); beforeAll( uniqueRealmBeforeAll ); afterAll( uniqueRealmAfterAll ); // /UNIQUE REALM SETUP const getLocalObservation = uuid => global.mockRealms[__filename] .objectForPrimaryKey( "Observation", uuid ); const createObservations = ( observations, comment ) => { const realm = global.mockRealms[__filename]; safeRealmWrite( realm, ( ) => { observations.forEach( observation => { realm.create( "Observation", observation ); } ); }, comment, ); }; beforeAll( async () => { useStore.setState( initialStoreState, true ); } ); describe( "automatic sync while user is logged out", ( ) => { beforeEach( () => { const realm = global.mockRealms[__filename]; safeRealmWrite( realm, ( ) => { realm.create( "Observation", obsToDelete ); }, "write Observation to delete, useSyncObservations" ); } ); it( "should not fetch remote observations or deletions when user is logged out", async ( ) => { useStore.setState( { ...syncingStore, } ); renderHook( ( ) => useSyncObservations( null ) ); await waitFor( ( ) => { expect( inatjs.observations.deleted ).not.toHaveBeenCalled( ); } ); await waitFor( ( ) => { expect( inatjs.observations.search ).not.toHaveBeenCalled( ); } ); } ); it( "should delete local observations without making a server call", async ( ) => { const realm = global.mockRealms[__filename]; const obsForDeletion = realm.objectForPrimaryKey( "Observation", obsToDelete.uuid ); expect( obsForDeletion ).toBeTruthy( ); useStore.setState( { ...syncingStore, } ); renderHook( ( ) => useSyncObservations( null ) ); const deletedObs = realm.objectForPrimaryKey( "Observation", obsToDelete.uuid ); await waitFor( ( ) => { expect( deletedObs ).toBeFalsy( ); } ); expect( mockMutateAsync ).not.toHaveBeenCalled( ); } ); } ); describe( "automatic sync while user is logged in", ( ) => { describe( "fetch remote deletions", ( ) => { inatjs.observations.deleted.mockResolvedValue( makeResponse( mockDeletedIds ) ); it( "should fetch deleted observations from server", async ( ) => { useStore.setState( { ...syncingStore, } ); renderHook( ( ) => useSyncObservations( currentUserId ) ); await waitFor( ( ) => { expect( inatjs.observations.deleted ).toHaveBeenCalled( ); } ); } ); } ); describe( "handle local deletions", ( ) => { it( "should not make deletion API call for unsynced observations", async ( ) => { createObservations( unsyncedObservations, "write unsyncedObservations, useSyncObservations test", ); const unsyncedObservation = getLocalObservation( unsyncedObservations[0].uuid, ); expect( unsyncedObservation._synced_at ).toBeNull( ); useStore.setState( { ...syncingStore, deleteQueue: [unsyncedObservations[0].uuid], } ); renderHook( ( ) => useSyncObservations( currentUserId ) ); await waitFor( ( ) => { expect( mockMutateAsync ).not.toHaveBeenCalled( ); } ); } ); it( "should make deletion API call for previously synced observations", async ( ) => { createObservations( syncedObservations, "write syncedObservations, useSyncObservations test", ); const syncedObservation = getLocalObservation( syncedObservations[0].uuid ); expect( syncedObservation._synced_at ).not.toBeNull( ); useStore.setState( { ...syncingStore, deleteQueue: [syncedObservations[0].uuid], } ); renderHook( ( ) => useSyncObservations( currentUserId ) ); await waitFor( ( ) => { expect( mockMutateAsync ) .toHaveBeenCalledWith( { uuid: syncedObservations[0].uuid } ); } ); } ); } ); it( "should fetch remote observations from server in automatic sync", async ( ) => { inatjs.observations.search.mockResolvedValue( makeResponse( [mockRemoteObservation] ) ); useStore.setState( { ...syncingStore, } ); renderHook( ( ) => useSyncObservations( currentUserId ) ); await waitFor( ( ) => { expect( inatjs.observations.search ).toHaveBeenCalled( ); } ); } ); it( "should not create upload queue", async ( ) => { useStore.setState( { ...syncingStore, } ); renderHook( ( ) => useSyncObservations( currentUserId, mockUpload ) ); await waitFor( ( ) => { expect( mockUpload ).not.toHaveBeenCalled( ); } ); } ); } );