2020import static org .datatransferproject .datatransfer .google .media .GoogleMediaExporter .MEDIA_TOKEN_PREFIX ;
2121
2222import static org .junit .jupiter .api .Assertions .assertNull ;
23+ import static org .junit .jupiter .api .Assertions .assertThrows ;
2324import static org .mockito .ArgumentMatchers .any ;
2425import static org .mockito .ArgumentMatchers .anyString ;
2526import static org .mockito .ArgumentMatchers .eq ;
3031
3132import com .fasterxml .jackson .databind .ObjectMapper ;
3233import com .google .api .client .json .gson .GsonFactory ;
34+ import com .google .common .collect .ImmutableList ;
3335import java .io .IOException ;
3436import java .io .InputStream ;
3537import java .util .Collection ;
4951import org .datatransferproject .datatransfer .google .photos .GooglePhotosInterface ;
5052import org .datatransferproject .spi .cloud .storage .TemporaryPerJobDataStore ;
5153import org .datatransferproject .spi .cloud .storage .TemporaryPerJobDataStore .InputStreamWrapper ;
54+ import org .datatransferproject .spi .transfer .idempotentexecutor .RetryingInMemoryIdempotentImportExecutor ;
5255import org .datatransferproject .spi .transfer .provider .ExportResult ;
5356import org .datatransferproject .spi .transfer .types .ContinuationData ;
5457import org .datatransferproject .spi .transfer .types .InvalidTokenException ;
5558import org .datatransferproject .spi .transfer .types .PermissionDeniedException ;
5659import org .datatransferproject .spi .transfer .types .TempMediaData ;
5760import org .datatransferproject .spi .transfer .types .UploadErrorException ;
61+ import org .datatransferproject .types .common .ExportInformation ;
5862import org .datatransferproject .types .common .PaginationData ;
5963import org .datatransferproject .types .common .StringPaginationToken ;
6064import org .datatransferproject .types .common .models .ContainerResource ;
6165import org .datatransferproject .types .common .models .IdOnlyContainerResource ;
6266import org .datatransferproject .types .common .models .photos .PhotoAlbum ;
6367import org .datatransferproject .types .common .models .photos .PhotoModel ;
6468import org .datatransferproject .types .common .models .photos .PhotosContainerResource ;
65- import org .datatransferproject .types .common .models .videos .VideoAlbum ;
6669import org .datatransferproject .types .common .models .videos .VideoModel ;
67- import org .datatransferproject .types .common .models .videos .VideosContainerResource ;
6870import org .datatransferproject .types .common .models .media .MediaAlbum ;
6971import org .datatransferproject .types .common .models .media .MediaContainerResource ;
72+ import org .datatransferproject .types .transfer .auth .TokensAndUrlAuthData ;
73+ import org .datatransferproject .types .transfer .retry .RetryStrategyLibrary ;
74+ import org .datatransferproject .types .transfer .retry .UniformRetryStrategy ;
7075import org .junit .jupiter .api .BeforeEach ;
7176import org .junit .jupiter .api .Test ;
7277import org .mockito .ArgumentCaptor ;
@@ -78,9 +83,14 @@ public class GoogleMediaExporterTest {
7883 private static String ALBUM_TOKEN = "some-upstream-generated-album-token" ;
7984 private static String MEDIA_TOKEN = "some-upstream-generated-media-token" ;
8085
81- private static UUID uuid = UUID .randomUUID ();
86+ private static long RETRY_INTERVAL_MILLIS = 100L ;
87+ private static int RETRY_MAX_ATTEMPTS = 1 ;
8288
89+ private static UUID uuid = UUID .randomUUID ();
90+ private TokensAndUrlAuthData authData ;
91+ private RetryingInMemoryIdempotentImportExecutor retryingExecutor ;
8392 private GoogleMediaExporter googleMediaExporter ;
93+ private GoogleMediaExporter retryingGoogleMediaExporter ;
8494 private TemporaryPerJobDataStore jobStore ;
8595 private GooglePhotosInterface photosInterface ;
8696
@@ -99,10 +109,20 @@ public void setup()
99109 mediaItemSearchResponse = mock (MediaItemSearchResponse .class );
100110
101111 Monitor monitor = mock (Monitor .class );
112+ authData = mock (TokensAndUrlAuthData .class );
113+
114+ retryingExecutor = new RetryingInMemoryIdempotentImportExecutor (monitor ,
115+ new RetryStrategyLibrary (ImmutableList .of (), new UniformRetryStrategy (RETRY_MAX_ATTEMPTS , RETRY_INTERVAL_MILLIS , "identifier" ))
116+ );
102117
103118 googleMediaExporter =
104119 new GoogleMediaExporter (
105- credentialFactory , jobStore , GsonFactory .getDefaultInstance (), photosInterface , monitor );
120+ credentialFactory , jobStore , GsonFactory .getDefaultInstance (), monitor , photosInterface );
121+
122+ retryingGoogleMediaExporter = new GoogleMediaExporter (
123+ credentialFactory , jobStore , GsonFactory .getDefaultInstance (), monitor , photosInterface ,
124+ retryingExecutor ,
125+ true );
106126
107127 when (photosInterface .listAlbums (any (Optional .class ))).thenReturn (albumListResponse );
108128 when (photosInterface .listMediaItems (any (Optional .class ), any (Optional .class )))
@@ -354,6 +374,92 @@ public void onlyExportAlbumlessPhoto()
354374 .containsExactly (albumlessPhotoUri + "=d" ); // download
355375 }
356376
377+ @ Test
378+ public void testGetGoogleMediaItemSucceeds () throws IOException , InvalidTokenException , PermissionDeniedException {
379+ String mediaItemID = "media_id" ;
380+ when (photosInterface .getMediaItem (any ())).thenReturn (setUpSingleMediaItem (mediaItemID , mediaItemID , null ));
381+
382+ assertThat (retryingGoogleMediaExporter .getGoogleMediaItem (mediaItemID , mediaItemID , mediaItemID , authData )).isInstanceOf (GoogleMediaItem .class );
383+
384+ }
385+
386+ @ Test
387+ public void testExportPhotosContainerRetrying () throws IOException , InvalidTokenException , PermissionDeniedException , UploadErrorException {
388+ String PHOTO_ID_TO_FAIL_1 = "photo3" ;
389+ String PHOTO_ID_TO_FAIL_2 = "photo5" ;
390+
391+ ImmutableList <PhotoAlbum > albums = ImmutableList .of ();
392+ ImmutableList <PhotoModel > photos = ImmutableList .of (
393+ setUpSinglePhotoModel ("" , "photo1" ),
394+ setUpSinglePhotoModel ("" , "photo2" ),
395+ setUpSinglePhotoModel ("" , PHOTO_ID_TO_FAIL_1 ),
396+ setUpSinglePhotoModel ("" , "photo4" ),
397+ setUpSinglePhotoModel ("" , PHOTO_ID_TO_FAIL_2 ),
398+ setUpSinglePhotoModel ("" , "photo6" )
399+ );
400+
401+ PhotosContainerResource container = new PhotosContainerResource (albums , photos );
402+ ExportInformation exportInfo = new ExportInformation (null , container );
403+
404+ MediaMetadata photoMediaMetadata = new MediaMetadata ();
405+ photoMediaMetadata .setPhoto (new Photo ());
406+
407+
408+ // For the photo_id_to_fail photos, throw an exception.
409+ when (photosInterface .getMediaItem (PHOTO_ID_TO_FAIL_1 )).thenThrow (IOException .class );
410+ when (photosInterface .getMediaItem (PHOTO_ID_TO_FAIL_2 )).thenThrow (IOException .class );
411+ // For all other photos, return a media item.
412+ for (PhotoModel photoModel : photos ) {
413+ if (photoModel .getDataId ().equals (PHOTO_ID_TO_FAIL_1 ) || photoModel .getDataId ().equals (PHOTO_ID_TO_FAIL_2 )) {
414+ continue ;
415+ }
416+ when (photosInterface .getMediaItem (photoModel .getDataId ())).thenReturn (
417+ setUpSingleMediaItem (photoModel .getDataId (), photoModel .getDataId (), photoMediaMetadata )
418+ );
419+ }
420+
421+ ExportResult <MediaContainerResource > result = retryingGoogleMediaExporter .export (
422+ uuid , authData , Optional .of (exportInfo )
423+ );
424+ assertThat (
425+ result .getExportedData ().getPhotos ().stream ().map (x -> x .getDataId ()).collect (Collectors .toList ())
426+ ).isEqualTo (
427+ photos .stream ().map (
428+ x -> x .getDataId ()
429+ ).filter (
430+ dataId -> !(dataId .equals (PHOTO_ID_TO_FAIL_1 ) || dataId .equals (PHOTO_ID_TO_FAIL_2 ))
431+ ).collect (
432+ Collectors .toList ()
433+ )
434+ );
435+ assertThat (result .getExportedData ().getPhotos ().size ()).isEqualTo (photos .size () - 2 );
436+ assertThat (retryingExecutor .getErrors ().size ()).isEqualTo (2 );
437+ assertThat (retryingExecutor .getErrors ().stream ().findFirst ().toString ().contains ("IOException" )).isTrue ();
438+ }
439+
440+ @ Test
441+ public void testGetGoogleMediaItemFailed () throws IOException , InvalidTokenException , PermissionDeniedException {
442+ String mediaItemID = "media_id" ;
443+ when (photosInterface .getMediaItem (mediaItemID )).thenThrow (IOException .class );
444+
445+ long start = System .currentTimeMillis ();
446+ assertThat (retryingGoogleMediaExporter .getGoogleMediaItem (mediaItemID , mediaItemID , mediaItemID , authData )).isNull ();
447+ long end = System .currentTimeMillis ();
448+
449+ // If retrying occurred, then the retry_interval must have been waited at least max_attempts
450+ // amount of times.
451+ assertThat (end - start ).isAtLeast (RETRY_INTERVAL_MILLIS * RETRY_MAX_ATTEMPTS );
452+ assertThat (retryingExecutor .getErrors ().size ()).isEqualTo (1 );
453+ assertThat (retryingExecutor .getErrors ().stream ().findFirst ().toString ()).contains ("IOException" );
454+
455+
456+ start = System .currentTimeMillis ();
457+ assertThrows (IOException .class , () -> googleMediaExporter .getGoogleMediaItem (mediaItemID , mediaItemID , mediaItemID , authData ));
458+ end = System .currentTimeMillis ();
459+
460+ assertThat (end - start ).isLessThan (RETRY_INTERVAL_MILLIS * RETRY_MAX_ATTEMPTS );
461+ }
462+
357463 /** Sets up a response with a single album, containing a single photo */
358464 private void setUpSingleAlbum () {
359465 GoogleAlbum albumEntry = new GoogleAlbum ();
@@ -363,6 +469,10 @@ private void setUpSingleAlbum() {
363469 when (albumListResponse .getAlbums ()).thenReturn (new GoogleAlbum [] {albumEntry });
364470 }
365471
472+ private static PhotoModel setUpSinglePhotoModel (String albumId , String dataId ) {
473+ return new PhotoModel ("Title" , "fetchableUrl" , "description" , "photo" , dataId , albumId , false );
474+ }
475+
366476 /** Sets up a response for a single photo */
367477 // TODO(zacsh) delete this helper in favor of explicitly setting the fields that an assertion will
368478 // _actually_ use (and doing so _inlined, visibly_ in the arrange phase).
0 commit comments