@@ -41,11 +41,14 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher
4141import kotlinx.coroutines.test.advanceTimeBy
4242import kotlinx.coroutines.test.runCurrent
4343import kotlinx.coroutines.test.runTest
44+ import okhttp3.ResponseBody.Companion.toResponseBody
4445import okhttp3.internal.http.RealResponseBody
4546import okio.Buffer
47+ import okio.ByteString.Companion.toByteString
4648import okio.buffer
4749import okio.source
4850import org.cru.godtools.api.AttachmentsApi
51+ import org.cru.godtools.api.CdnApi
4952import org.cru.godtools.api.TranslationsApi
5053import org.cru.godtools.base.ToolFileSystem
5154import org.cru.godtools.db.repository.AttachmentsRepository
@@ -55,6 +58,7 @@ import org.cru.godtools.downloadmanager.work.WORK_NAME_DOWNLOAD_ATTACHMENT
5558import org.cru.godtools.model.Attachment
5659import org.cru.godtools.model.DownloadedFile
5760import org.cru.godtools.model.DownloadedTranslationFile
61+ import org.cru.godtools.model.Translation
5862import org.cru.godtools.model.TranslationKey
5963import org.cru.godtools.model.randomTranslation
6064import org.cru.godtools.shared.tool.parser.ManifestParser
@@ -75,6 +79,9 @@ class GodToolsDownloadManagerTest {
7579
7680 private val attachmentsApi = mockk<AttachmentsApi >()
7781 private val attachmentsRepository: AttachmentsRepository = mockk(relaxUnitFun = true )
82+ private val cdnApi: CdnApi = mockk {
83+ coEvery { downloadPublishedFile(any()) } returns Response .error(404 , " " .toResponseBody())
84+ }
7885 private val downloadedFilesRepository: DownloadedFilesRepository = mockk(relaxUnitFun = true ) {
7986 coEvery { findDownloadedFile(any()) } returns null
8087 coEvery { getDownloadedFiles() } returns emptyList()
@@ -90,9 +97,13 @@ class GodToolsDownloadManagerTest {
9097 every { defaultConfig } returns ParserConfig ()
9198 excludeRecords { defaultConfig }
9299 }
93- private val translationsApi = mockk<TranslationsApi >()
100+ private val translationsApi: TranslationsApi = mockk {
101+ coEvery { download(any()) } returns Response .error(404 , " " .toResponseBody())
102+ coEvery { downloadFile(any()) } returns Response .error(404 , " " .toResponseBody())
103+ }
94104 private val translationsRepository: TranslationsRepository = mockk {
95105 coEvery { markStaleTranslationsAsNotDownloaded() } returns false
106+ coEvery { markTranslationDownloaded(any(), any()) } just Runs
96107 }
97108 private val workManager: WorkManager = mockk {
98109 every { enqueueUniqueWork(any(), any(), any<OneTimeWorkRequest >()) } returns mockk()
@@ -102,6 +113,7 @@ class GodToolsDownloadManagerTest {
102113 private val downloadManager = GodToolsDownloadManager (
103114 attachmentsApi,
104115 attachmentsRepository,
116+ cdnApi = cdnApi,
105117 downloadedFilesRepository,
106118 fs,
107119 manifestParser,
@@ -434,6 +446,121 @@ class GodToolsDownloadManagerTest {
434446 }
435447 // endregion downloadLatestPublishedTranslation()
436448
449+ // region downloadPublishedFileIfNecessary
450+ private fun setupTranslationFilesDownload (translation : Translation , manifest : Manifest ) {
451+ coEvery { translationsRepository.findLatestTranslation(translation.toolCode, translation.languageCode) }
452+ .returns(translation)
453+ coEvery { manifestParser.parseManifest(translation.manifestFileName!! , any()) }
454+ .returns(ParserResult .Data (manifest))
455+ coEvery { translationsApi.downloadFile(translation.manifestFileName!! ) }
456+ .returns(Response .success(RealResponseBody (null , 0 , Buffer ().writeUtf8(" manifest" ))))
457+ }
458+
459+ @Test
460+ fun `downloadPublishedFileIfNecessary - prefers CDN over API` () = testScope.runTest {
461+ downloadManager.cleanupActor.close()
462+ val fileContent = " a" .repeat(1024 )
463+ val translation = randomTranslation(manifestFileName = " manifest.xml" , isDownloaded = false )
464+ val manifest = Manifest (pageXmlFiles = listOf (XmlFile (" file.xml" , " file.xml" )))
465+ setupTranslationFilesDownload(translation, manifest)
466+ coEvery { cdnApi.downloadPublishedFile(" file.xml" ) } returns Response .success(fileContent.toResponseBody())
467+
468+ assertTrue(downloadManager.downloadLatestPublishedTranslation(TranslationKey (translation)))
469+ assertEquals(fileContent, files[" file.xml" ]!! .readText())
470+ coVerify { cdnApi.downloadPublishedFile(" file.xml" ) }
471+ coVerify(exactly = 0 ) { translationsApi.downloadFile(" file.xml" ) }
472+ }
473+
474+ @Test
475+ fun `downloadPublishedFileIfNecessary - falls back to API on CDN 404` () = testScope.runTest {
476+ downloadManager.cleanupActor.close()
477+ val fileContent = " a" .repeat(1024 )
478+ val translation = randomTranslation(manifestFileName = " manifest.xml" , isDownloaded = false )
479+ val manifest = Manifest (pageXmlFiles = listOf (XmlFile (" file.xml" , " file.xml" )))
480+ setupTranslationFilesDownload(translation, manifest)
481+ coEvery { translationsApi.downloadFile(" file.xml" ) } returns Response .success(fileContent.toResponseBody())
482+
483+ assertTrue(downloadManager.downloadLatestPublishedTranslation(TranslationKey (translation)))
484+ assertEquals(fileContent, files[" file.xml" ]!! .readText())
485+ coVerify { cdnApi.downloadPublishedFile(" file.xml" ) }
486+ coVerify { translationsApi.downloadFile(" file.xml" ) }
487+ }
488+
489+ @Test
490+ fun `downloadPublishedFileIfNecessary - falls back to API on CDN IOException` () = testScope.runTest {
491+ downloadManager.cleanupActor.close()
492+ val fileContent = " a" .repeat(1024 )
493+ val translation = randomTranslation(manifestFileName = " manifest.xml" , isDownloaded = false )
494+ val manifest = Manifest (pageXmlFiles = listOf (XmlFile (" file.xml" , " file.xml" )))
495+ setupTranslationFilesDownload(translation, manifest)
496+ coEvery { cdnApi.downloadPublishedFile(" file.xml" ) } throws IOException ()
497+ coEvery { translationsApi.downloadFile(" file.xml" ) } returns Response .success(fileContent.toResponseBody())
498+
499+ assertTrue(downloadManager.downloadLatestPublishedTranslation(TranslationKey (translation)))
500+ assertEquals(fileContent, files[" file.xml" ]!! .readText())
501+ coVerify { cdnApi.downloadPublishedFile(" file.xml" ) }
502+ coVerify { translationsApi.downloadFile(" file.xml" ) }
503+ }
504+
505+ @Test
506+ fun `downloadPublishedFileIfNecessary - returns false when both CDN and API fail` () = testScope.runTest {
507+ downloadManager.cleanupActor.close()
508+ val translation = randomTranslation(manifestFileName = " manifest.xml" , isDownloaded = false )
509+ val manifest = Manifest (pageXmlFiles = listOf (XmlFile (" file.xml" , " file.xml" )))
510+ setupTranslationFilesDownload(translation, manifest)
511+
512+ assertFalse(downloadManager.downloadLatestPublishedTranslation(TranslationKey (translation)))
513+ coVerify { cdnApi.downloadPublishedFile(" file.xml" ) }
514+ coVerify { translationsApi.downloadFile(" file.xml" ) }
515+ }
516+
517+ @Test
518+ fun `downloadPublishedFileIfNecessary - accepts file with matching sha256` () = testScope.runTest {
519+ downloadManager.cleanupActor.close()
520+ val fileContent = " a" .repeat(1024 )
521+ val sha256Hex = fileContent.toByteArray().toByteString().sha256().hex()
522+ val translation = randomTranslation(manifestFileName = " manifest.xml" , isDownloaded = false )
523+ val manifest = Manifest (pageXmlFiles = listOf (XmlFile (" file.xml" , " file.xml" , checksumSha256 = sha256Hex)))
524+ setupTranslationFilesDownload(translation, manifest)
525+ coEvery { cdnApi.downloadPublishedFile(" file.xml" ) } returns Response .success(fileContent.toResponseBody())
526+
527+ assertTrue(downloadManager.downloadLatestPublishedTranslation(TranslationKey (translation)))
528+ coVerify { downloadedFilesRepository.insertOrIgnore(DownloadedFile (" file.xml" )) }
529+ }
530+
531+ @Test
532+ fun `downloadPublishedFileIfNecessary - rejects file with wrong sha256` () = testScope.runTest {
533+ downloadManager.cleanupActor.close()
534+ val fileContent = " a" .repeat(1024 )
535+ val translation = randomTranslation(manifestFileName = " manifest.xml" , isDownloaded = false )
536+ val manifest = Manifest (
537+ pageXmlFiles = listOf (XmlFile (" file.xml" , " file.xml" , checksumSha256 = " 00" .repeat(32 ))),
538+ )
539+ setupTranslationFilesDownload(translation, manifest)
540+ coEvery { cdnApi.downloadPublishedFile(" file.xml" ) } returns Response .success(fileContent.toResponseBody())
541+ coEvery { translationsApi.downloadFile(" file.xml" ) } returns Response .success(fileContent.toResponseBody())
542+
543+ assertFalse(downloadManager.downloadLatestPublishedTranslation(TranslationKey (translation)))
544+ coVerify(exactly = 0 ) { downloadedFilesRepository.insertOrIgnore(DownloadedFile (" file.xml" )) }
545+ assertFalse(files[" file.xml" ]!! .exists())
546+ }
547+
548+ @Test
549+ fun `downloadPublishedFileIfNecessary - rejects file with wrong size` () = testScope.runTest {
550+ downloadManager.cleanupActor.close()
551+ val fileContent = " a" .repeat(1024 )
552+ val translation = randomTranslation(manifestFileName = " manifest.xml" , isDownloaded = false )
553+ val manifest = Manifest (pageXmlFiles = listOf (XmlFile (" file.xml" , " file.xml" , size = 9999 )))
554+ setupTranslationFilesDownload(translation, manifest)
555+ coEvery { cdnApi.downloadPublishedFile(" file.xml" ) } returns Response .success(fileContent.toResponseBody())
556+ coEvery { translationsApi.downloadFile(" file.xml" ) } returns Response .success(fileContent.toResponseBody())
557+
558+ assertFalse(downloadManager.downloadLatestPublishedTranslation(TranslationKey (translation)))
559+ coVerify(exactly = 0 ) { downloadedFilesRepository.insertOrIgnore(DownloadedFile (" file.xml" )) }
560+ assertFalse(files[" file.xml" ]!! .exists())
561+ }
562+ // endregion downloadPublishedFileIfNecessary
563+
437564 @Test
438565 fun verifyImportTranslation () = testScope.runTest {
439566 coEvery { translationsRepository.findLatestTranslation(any(), any(), any()) } returns null
0 commit comments