Files
iNaturalistReactNative/tests/unit/uploaders/observationUploader.test.js
Ryan Stelly b78be9243d lint rule & autofix for "trailing comma" (#3299)
* (lint) MOB-1063 enforce trailing commas

* autofix trailing commas

* manually fix newly introduced maxlen violations

* add trailing comma convention to i18n build
2025-12-22 20:17:13 -06:00

228 lines
7.8 KiB
JavaScript

import * as apiObservations from "api/observations";
import * as authService from "components/LoginSignUp/AuthenticationService";
import factory from "tests/factory";
import * as uploaders from "uploaders";
import * as mediaUploader from "uploaders/mediaUploader";
import uploadObservation from "uploaders/observationUploader";
import * as progressTracker from "uploaders/utils/progressTracker";
jest.mock( "components/LoginSignUp/AuthenticationService" );
jest.mock( "uploaders/utils/progressTracker" );
jest.mock( "uploaders/mediaUploader" );
jest.mock( "uploaders" );
jest.mock( "api/observations" );
jest.mock( "sharedHelpers/safeRealmWrite", () => jest.fn() );
jest.mock( "realmModels/Observation", () => {
const actual = jest.requireActual( "realmModels/Observation" );
actual.default.upsertRemoteObservations = jest.fn();
return actual;
} );
describe( "uploadObservation", () => {
let mockObservation;
let mockRealm;
let mockProgressTracker;
let mockMediaItems;
beforeEach( () => {
jest.clearAllMocks();
mockObservation = factory( "LocalObservation", {
uuid: "test-uuid-123",
} );
mockObservation.wasSynced = jest.fn( () => false );
mockRealm = {
objectForPrimaryKey: jest.fn( () => factory( "LocalObservation" ) ),
};
mockProgressTracker = {
start: jest.fn(),
complete: jest.fn(),
error: jest.fn(),
};
mockMediaItems = {
unsyncedObservationPhotos: [],
modifiedObservationPhotos: [],
unsyncedObservationSounds: [],
};
authService.getJWT.mockResolvedValue( "test-json-web-token" );
progressTracker.trackObservationUpload.mockReturnValue( mockProgressTracker );
// Mock the new functions in mediaUploader
mediaUploader.uploadObservationMedia.mockResolvedValue( mockMediaItems );
mediaUploader.attachMediaToObservation.mockResolvedValue( undefined );
uploaders.prepareObservationForUpload.mockReturnValue( {
uuid: mockObservation.uuid,
taxon_id: 12345,
} );
apiObservations.createObservation.mockResolvedValue( {
results: [{ uuid: mockObservation.uuid }],
} );
apiObservations.updateObservation.mockResolvedValue( {
results: [{ uuid: mockObservation.uuid }],
} );
} );
it( "should start and complete progress tracking", async () => {
await uploadObservation( mockObservation, mockRealm );
expect( progressTracker.trackObservationUpload ).toHaveBeenCalledWith( mockObservation.uuid );
expect( mockProgressTracker.start ).toHaveBeenCalled();
expect( mockProgressTracker.complete ).toHaveBeenCalled();
} );
it( "should validate the API token", async () => {
await uploadObservation( mockObservation, mockRealm );
expect( authService.getJWT ).toHaveBeenCalled();
} );
it( "should throw an error if no API token is available", async () => {
authService.getJWT.mockResolvedValue( null );
await expect( uploadObservation( mockObservation, mockRealm ) )
.rejects.toThrow( "Gack, tried to upload an observation without API token!" );
} );
it( "should prepare the observation for upload", async () => {
await uploadObservation( mockObservation, mockRealm );
expect( uploaders.prepareObservationForUpload ).toHaveBeenCalledWith( mockObservation );
} );
it( "should create a new observation if it was not previously synced", async () => {
mockObservation.wasSynced.mockReturnValue( false );
await uploadObservation( mockObservation, mockRealm );
expect( apiObservations.createObservation ).toHaveBeenCalledWith(
expect.objectContaining( {
observation: expect.anything(),
fields: { id: true },
} ),
expect.objectContaining( { api_token: "test-json-web-token" } ),
);
expect( apiObservations.updateObservation ).not.toHaveBeenCalled();
} );
it( "should update an existing observation if it was previously synced", async () => {
mockObservation.wasSynced.mockReturnValue( true );
await uploadObservation( mockObservation, mockRealm );
expect( apiObservations.updateObservation ).toHaveBeenCalledWith(
expect.objectContaining( {
observation: expect.anything(),
fields: { id: true },
id: mockObservation.uuid,
ignore_photos: true,
} ),
expect.objectContaining( { api_token: "test-json-web-token" } ),
);
expect( apiObservations.createObservation ).not.toHaveBeenCalled();
} );
it( "should call uploadObservationMedia with correct parameters", async () => {
await uploadObservation( mockObservation, mockRealm );
expect( mediaUploader.uploadObservationMedia ).toHaveBeenCalledWith(
mockObservation,
expect.objectContaining( { api_token: "test-json-web-token" } ),
mockRealm,
);
} );
it(
"should call attachMediaToObservation with correct parameters after observation creation",
async () => {
await uploadObservation( mockObservation, mockRealm );
expect( mediaUploader.attachMediaToObservation ).toHaveBeenCalledWith(
mockObservation.uuid,
mockMediaItems,
expect.objectContaining( { api_token: "test-json-web-token" } ),
mockRealm,
);
},
);
it( "should mark the record as uploaded after media is attached", async () => {
await uploadObservation( mockObservation, mockRealm );
expect( uploaders.markRecordUploaded ).toHaveBeenCalledWith(
mockObservation.uuid,
null,
"Observation",
expect.anything(),
mockRealm,
);
} );
it( "should return the API response", async () => {
const mockResponse = { results: [{ uuid: mockObservation.uuid }] };
apiObservations.createObservation.mockResolvedValue( mockResponse );
const result = await uploadObservation( mockObservation, mockRealm );
expect( result ).toEqual( mockResponse );
} );
it( "should throw an error if observation creation fails", async () => {
apiObservations.createObservation.mockRejectedValue( new Error( "API Error" ) );
await expect( uploadObservation( mockObservation, mockRealm ) )
.rejects.toThrow( "API Error" );
} );
it( "should throw an error if media upload fails", async () => {
mediaUploader.uploadObservationMedia.mockRejectedValue( new Error( "Media Upload Error" ) );
await expect( uploadObservation( mockObservation, mockRealm ) )
.rejects.toThrow( "Media Upload Error" );
} );
it( "should throw an error if media attachment fails", async () => {
mediaUploader.attachMediaToObservation
.mockRejectedValue( new Error( "Media Attachment Error" ) );
await expect( uploadObservation( mockObservation, mockRealm ) )
.rejects.toThrow( "Media Attachment Error" );
} );
it( "should not complete progress tracking when errors occur", async () => {
apiObservations.createObservation.mockRejectedValue( new Error( "API Error" ) );
await expect( uploadObservation( mockObservation, mockRealm ) ).rejects.toThrow( "API Error" );
expect( mockProgressTracker.complete ).not.toHaveBeenCalled();
} );
it( "should throw an error when observation creation returns null response", async () => {
apiObservations.createObservation.mockResolvedValue( null );
await expect( uploadObservation( mockObservation, mockRealm ) )
.rejects.toThrow( "No response from observation upload" );
// Should not attempt to attach media or mark as uploaded
expect( mediaUploader.attachMediaToObservation ).not.toHaveBeenCalled();
expect( uploaders.markRecordUploaded ).not.toHaveBeenCalled();
} );
it( "should pass custom options to API calls", async () => {
const customOpts = { locale: "es-ES" };
await uploadObservation( mockObservation, mockRealm, customOpts );
expect( apiObservations.createObservation ).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining( {
api_token: "test-json-web-token",
locale: "es-ES",
} ),
);
} );
} );