Files
iNaturalistReactNative/tests/unit/uploaders/utils/realmSync.test.js
Johannes Klein 9cab2a4008 Fix: sounds getting deleted during upload (#3598)
* Rename for consistency with obs photos

* Refactor to be similar to obs photos

* Include Sound as type

* Revert "Include Sound as type"

This reverts commit 3db88ae031.

* Revert "Refactor to be similar to obs photos"

This reverts commit 8ed7454be0.

* Revert "Rename for consistency with obs photos"

This reverts commit 674b921bab.

* Git mv without changes

* Add types

* Type api sound param

* Type fields s const

* Type fields as const

* Type api param

* Type new param

* The only call site of this function passes in only defined strings

* Adapted from Photo check for local path before deleting

* Type fct params

* Refactor constructor to only have a uri as param

* Refactor Sound constructor to have string as param

* Update Sound.ts

* Remove unused param

* Type fct params

* Type realm

* Also collect unsyncedSounds

* Create operations for sound uploads based on Sound only

* Add Evidence type

* Change a sound upload operation to work with a RealmSound

* Update realmSync to also just pass through Sounds

* Use server ID for attaching obs_sound to obs

* Also split photo pipeline for clarity

This is not a functional change. Also previously, only RealmPhoto s are uploaded, only RealmObservationPhoto s are attached or modified. Same as for sounds now.

* Update realmSync.ts

* Remove log

* Update realmSync.test.js

* Update mocks

* Update Sound map test

* Update prepareMediaForUpload.test.js

* Update prepareMediaForUpload.test.js

* Update mediaUploader.test.js
2026-05-07 21:22:27 +02:00

149 lines
5.1 KiB
JavaScript

import safeRealmWrite from "sharedHelpers/safeRealmWrite";
import { markRecordUploaded } from "uploaders";
jest.mock( "sharedHelpers/safeRealmWrite" );
describe( "markRecordUploaded", () => {
let mockRealm;
let mockObservation;
let mockObsPhoto;
let mockObsSound;
let mockPhoto;
let mockSound;
let mockResponse;
beforeEach( () => {
jest.clearAllMocks();
mockObsPhoto = { uuid: "photo123", id: null, _synced_at: null };
mockObsSound = { uuid: "sound123", id: null, _synced_at: null };
mockPhoto = { id: null, _synced_at: null };
mockSound = { id: null, _synced_at: null };
mockObservation = {
uuid: "obs123",
id: null,
_synced_at: null,
needs_sync: true,
observationPhotos: [mockObsPhoto],
observationSounds: [mockObsSound],
};
mockResponse = {
results: [{ id: 12345 }],
};
mockRealm = {
isClosed: false,
objectForPrimaryKey: jest.fn().mockReturnValue( mockObservation ),
};
// Mock the safeRealmWrite implementation
safeRealmWrite.mockImplementation( ( realm, callback ) => {
callback();
} );
} );
test( "should do nothing if realm is closed", () => {
mockRealm.isClosed = true;
markRecordUploaded( mockObservation.uuid, null, "Observation", mockResponse, mockRealm );
expect( safeRealmWrite ).not.toHaveBeenCalled();
} );
test( "should update an Observation record correctly", () => {
markRecordUploaded( "obs123", null, "Observation", mockResponse, mockRealm );
expect( mockRealm.objectForPrimaryKey ).toHaveBeenCalledWith( "Observation", "obs123" );
expect( safeRealmWrite ).toHaveBeenCalledTimes( 1 );
expect( mockObservation.id ).toBe( 12345 );
expect( mockObservation._synced_at ).toBeInstanceOf( Date );
expect( mockObservation.needs_sync ).toBe( false );
} );
test( "should update an ObservationPhoto record correctly", () => {
markRecordUploaded( "obs123", "photo123", "ObservationPhoto", mockResponse, mockRealm );
expect( mockRealm.objectForPrimaryKey ).toHaveBeenCalledWith( "Observation", "obs123" );
expect( safeRealmWrite ).toHaveBeenCalledTimes( 1 );
expect( mockObsPhoto.id ).toBe( 12345 );
expect( mockObsPhoto._synced_at ).toBeInstanceOf( Date );
// needs_sync should not be modified for ObservationPhoto
expect( mockObsPhoto.needs_sync ).toBeUndefined();
} );
test( "should update an ObservationSound record correctly", () => {
markRecordUploaded( "obs123", "sound123", "ObservationSound", mockResponse, mockRealm );
expect( mockRealm.objectForPrimaryKey ).toHaveBeenCalledWith( "Observation", "obs123" );
expect( safeRealmWrite ).toHaveBeenCalledTimes( 1 );
expect( mockObsSound.id ).toBe( 12345 );
expect( mockObsSound._synced_at ).toBeInstanceOf( Date );
// needs_sync should not be modified for ObservationSound
expect( mockObsSound.needs_sync ).toBeUndefined();
} );
test( "should update a Photo record correctly with options.record", () => {
const options = { record: mockPhoto };
markRecordUploaded( "obs123", null, "Photo", mockResponse, mockRealm, options );
expect( safeRealmWrite ).toHaveBeenCalledTimes( 1 );
expect( mockPhoto.id ).toBe( 12345 );
expect( mockPhoto._synced_at ).toBeInstanceOf( Date );
} );
test( "should update a Sound record correctly with options.record", () => {
const options = { record: mockSound };
markRecordUploaded( "obs123", null, "Sound", mockResponse, mockRealm, options );
expect( safeRealmWrite ).toHaveBeenCalledTimes( 1 );
expect( mockSound.id ).toBe( 12345 );
expect( mockSound._synced_at ).toBeInstanceOf( Date );
} );
test( "should throw error when record is not found", () => {
expect( () => {
markRecordUploaded( "obs123", "nonexistent", "ObservationPhoto", mockResponse, mockRealm );
} ).toThrow( "Cannot find local Realm object to mark as updated" );
} );
test( "should retry on invalidated object error", () => {
safeRealmWrite.mockImplementationOnce( () => {
throw new Error( "Object has been invalidated or deleted" );
} );
markRecordUploaded( "obs123", null, "Observation", mockResponse, mockRealm );
expect( mockRealm.objectForPrimaryKey ).toHaveBeenCalledTimes( 2 );
expect( safeRealmWrite ).toHaveBeenCalledTimes( 2 );
expect( mockObservation.id ).toBe( 12345 );
expect( mockObservation._synced_at ).toBeInstanceOf( Date );
expect( mockObservation.needs_sync ).toBe( false );
} );
test( "should attempt to retry on non-invalidation errors", () => {
// Mock safeRealmWrite to throw a non-invalidation error
safeRealmWrite.mockImplementationOnce( ( realm, callback, description ) => {
const error = new Error( "Some other error" );
error.message = `${description}: ${error.message}`;
throw error;
} );
expect( () => {
markRecordUploaded( "obs123", null, "Observation", mockResponse, mockRealm );
} ).toThrow( /Some other error/ );
// Only called once because error doesn't match invalidated pattern
expect( safeRealmWrite ).toHaveBeenCalledTimes( 1 );
} );
} );