package com.jagsaund.rxuploader; import com.jagsaund.rxuploader.job.ErrorType; import com.jagsaund.rxuploader.job.Job; import com.jagsaund.rxuploader.job.Status; import com.jagsaund.rxuploader.job.StatusType; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import rx.Observable; import rx.observers.TestSubscriber; import rx.schedulers.TestScheduler; import rx.subjects.TestSubject; import static com.jagsaund.rxuploader.job.Status.createQueued; import static com.jagsaund.rxuploader.job.Status.createSending; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class UploadManagerTest { private static final Job TEST_JOB = Job.builder() .setId("job-id") .setFilepath("filepath") .setMetadata(Collections.emptyMap()) .setStatus(createQueued("job-id")) .setMimeType("text/plain") .build(); @Mock private UploadInteractor uploadInteractor; @Mock private UploadErrorAdapter uploadErrorAdapter; private TestScheduler testScheduler; private TestSubject<Status> statusSubject; private TestSubject<Job> jobSubject; private UploadManager uploadManager; @Before public void setUp() throws Exception { testScheduler = new TestScheduler(); statusSubject = TestSubject.create(testScheduler); jobSubject = TestSubject.create(testScheduler); when(uploadInteractor.getAll()).thenReturn(Observable.empty()); uploadManager = new UploadManager(uploadInteractor, uploadErrorAdapter, jobSubject, statusSubject, false); } @Test public void testEnqueue() throws Exception { when(uploadInteractor.save(TEST_JOB)) .thenReturn(Observable.just(TEST_JOB)); when(uploadInteractor.update(TEST_JOB.status())) .thenReturn(Observable.just(TEST_JOB)); final Status completed = Status.createCompleted(TEST_JOB.id(), "Finished"); when(uploadInteractor.update(completed)) .thenReturn(Observable.just(TEST_JOB.withStatus(completed))); final Status[] statuses = new Status[] { Status.createSending(TEST_JOB.id(), 0), Status.createSending(TEST_JOB.id(), 50), Status.createSending(TEST_JOB.id(), 100), completed, }; when(uploadInteractor.upload(TEST_JOB.id())) .thenReturn(Observable.from(statuses)); when(uploadInteractor.delete(TEST_JOB.id())) .thenReturn(Observable.just(TEST_JOB.withStatus(completed))); // when a new job is queued to be uploaded uploadManager.enqueue(TEST_JOB); testScheduler.triggerActions(); // it should first be saved to the job repository verify(uploadInteractor).save(TEST_JOB); // then the job should be passed to the status queue where the job status is updated verify(uploadInteractor).update(TEST_JOB.status()); // then the status is filtered for queued items which should be sent to upload verify(uploadInteractor).upload(TEST_JOB.id()); // then the status should be updated once upload completes verify(uploadInteractor).update(completed); } @Test public void testEnqueueUploadFailure() throws Exception { when(uploadErrorAdapter.fromThrowable(any(IOException.class))) .thenReturn(ErrorType.NETWORK); when(uploadInteractor.save(TEST_JOB)) .thenReturn(Observable.just(TEST_JOB)); when(uploadInteractor.update(TEST_JOB.status())) .thenReturn(Observable.just(TEST_JOB)); final Status failed = Status.createFailed(TEST_JOB.id(), ErrorType.NETWORK); when(uploadInteractor.update(failed)) .thenReturn(Observable.just(TEST_JOB.withStatus(failed))); final Status[] statuses = new Status[] { Status.createSending(TEST_JOB.id(), 0), Status.createSending(TEST_JOB.id(), 50), }; when(uploadInteractor.upload(TEST_JOB.id())) .thenReturn(Observable.from(statuses) .concatWith(Observable.error(new IOException()))); // when a new job is queued to be uploaded uploadManager.enqueue(TEST_JOB); testScheduler.triggerActions(); // it should first be saved to the job repository verify(uploadInteractor).save(TEST_JOB); // then the job should be passed to the status queue where the job status is updated verify(uploadInteractor).update(TEST_JOB.status()); // then the status is filtered for queued items which should be sent to upload verify(uploadInteractor).upload(TEST_JOB.id()); // then the status should be updated once upload failed verify(uploadInteractor).update(failed); // make sure the job was not deleted verify(uploadInteractor, times(0)).delete(anyString()); } @Test public void testConcurrentUpload() throws Exception { // Expect uploads to be performed one at a time final String jobId1 = "job-id-1"; final String jobId2 = "job-id-2"; final Job job1 = Job.builder() .setId(jobId1) .setFilepath("filepath") .setMetadata(Collections.emptyMap()) .setStatus(createQueued(jobId1)) .setMimeType("text/plain") .build(); final Job job2 = Job.builder() .setId(jobId2) .setFilepath("filepath") .setMetadata(Collections.emptyMap()) .setStatus(createQueued(jobId2)) .setMimeType("text/plain") .build(); final Status[] statusesJob1 = new Status[]{ Status.createSending(jobId1, 0), Status.createSending(jobId1, 20), Status.createSending(jobId1, 40), Status.createSending(jobId1, 60), Status.createSending(jobId1, 80), Status.createSending(jobId1, 100), }; final Status[] statusesJob2 = new Status[]{ Status.createSending(jobId2, 0), Status.createSending(jobId2, 20), Status.createSending(jobId2, 40), Status.createSending(jobId2, 60), Status.createSending(jobId2, 80), Status.createSending(jobId2, 100), }; when(uploadInteractor.getAll()) .thenReturn(Observable.from(Collections.emptyList())); when(uploadInteractor.save(job1)) .thenReturn(Observable.just(job1)); when(uploadInteractor.save(job2)) .thenReturn(Observable.just(job2)); when(uploadInteractor.update(job1.status())) .thenReturn(Observable.just(job1)); when(uploadInteractor.update(job2.status())) .thenReturn(Observable.just(job2)); // independently control execution of status emissions // simulate inherent delay of upload operation TestScheduler delayTestScheduler = new TestScheduler(); when(uploadInteractor.upload(jobId1)).thenReturn(Observable.from(statusesJob1) .concatMap(s -> Observable.just(s) .delay(50, TimeUnit.MILLISECONDS, delayTestScheduler))); // explicitly want the second upload job to emit items faster than the first // after all, we are trying to confirm that all the emissions from the first job will be // emitted before the second job begins when(uploadInteractor.upload(jobId2)) .thenReturn(Observable.from(statusesJob2)); final Status queuedJob1 = Status.createQueued(jobId1); final Status queuedJob2 = Status.createQueued(jobId2); // directly inject the two queued jobs -- these will propagate through the system to the // uploader statusSubject.onNext(queuedJob1); statusSubject.onNext(queuedJob2); final Status[] expected = new Status[statusesJob1.length + statusesJob2.length]; System.arraycopy(statusesJob1, 0, expected, 0, statusesJob1.length); System.arraycopy(statusesJob2, 0, expected, statusesJob1.length, statusesJob2.length); // only interested in the sending progress updates final TestSubscriber<Status> ts = TestSubscriber.create(); uploadManager.status() .filter(status -> status.statusType() == StatusType.SENDING) .subscribe(ts); testScheduler.triggerActions(); // confirm that after the first delay we received just the first emission from job1 delayTestScheduler.advanceTimeBy(50, TimeUnit.MILLISECONDS); delayTestScheduler.triggerActions(); testScheduler.triggerActions(); ts.assertValuesAndClear(expected[0]); // confirm that after the next set of delays, still only the first job is emitting status // updates delayTestScheduler.advanceTimeBy(50 * 4, TimeUnit.MILLISECONDS); delayTestScheduler.triggerActions(); testScheduler.triggerActions(); ts.assertValuesAndClear(expected[1], expected[2], expected[3], expected[4]); // complete the delay and verify that the last status is emitted from the first upload // operation and then receive the remaining status emissions from the 2nd upload delayTestScheduler.advanceTimeBy(50, TimeUnit.MILLISECONDS); delayTestScheduler.triggerActions(); testScheduler.triggerActions(); ts.assertValues(Arrays.copyOfRange(expected, 5, expected.length)); } @Test public void testDanglingUpload() { final String jobId1 = "job-id-1"; final String jobId2 = "job-id-2"; final Job job1 = Job.builder() .setId(jobId1) .setFilepath("filepath") .setMetadata(Collections.emptyMap()) .setStatus(createQueued(jobId1)) .setMimeType("text/plain") .build(); final Job job2 = Job.builder() .setId(jobId2) .setFilepath("filepath") .setMetadata(Collections.emptyMap()) .setStatus(createSending(jobId2, 0)) .setMimeType("text/plain") .build(); when(uploadInteractor.getAll()) .thenReturn(Observable.from(Arrays.asList(job1, job2))); final TestScheduler testScheduler = new TestScheduler(); final TestSubject<Status> statusSubject = TestSubject.create(testScheduler); final TestSubject<Job> jobSubject = TestSubject.create(testScheduler); new UploadManager(uploadInteractor, uploadErrorAdapter, jobSubject, statusSubject, false); verify(uploadInteractor, times(1)).update(any(Status.class)); final Status expectedStatus = Status.createFailed(job2.id(), ErrorType.TERMINATED); verify(uploadInteractor).update(expectedStatus); } }