diff --git a/app/build.gradle b/app/build.gradle index fdeb8dd63f0da3601d3433db6aa123d2814bc72c..930cc22caa6886eb3f6fa66cd5d35808ae9bd449 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -69,6 +69,10 @@ android { } } + kotlinOptions { + jvmTarget = "11" + } + testOptions { unitTests { returnDefaultValues = true diff --git a/app/src/main/java/foundation/e/drive/contentScanner/FileDiffUtils.kt b/app/src/main/java/foundation/e/drive/contentScanner/FileDiffUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..e5a3f49846f988b30350b8bd9ce902b555925519 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/contentScanner/FileDiffUtils.kt @@ -0,0 +1,116 @@ +/* + * Copyright © MURENA SAS 2023. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ +package foundation.e.drive.contentScanner + +import androidx.annotation.VisibleForTesting +import com.owncloud.android.lib.resources.files.model.RemoteFile +import foundation.e.drive.models.SyncedFileState +import foundation.e.drive.utils.AppConstants +import java.io.File + +object FileDiffUtils { + enum class Action { Upload, Download, Skip, UpdateDB } + /** + * Define what to do of RemoteFile for which we know the Database equivalent + * @param remoteFile RemoteFile + * @param fileState SyncedFileState instance + * @return Action from Enum + */ + @JvmStatic + fun getActionForFileDiff(remoteFile: RemoteFile, fileState: SyncedFileState): Action { + if (!hasEtagChanged(remoteFile, fileState)) { + if (isCorruptedTimestamp(remoteFile.modifiedTimestamp)) return Action.Upload + + if (hasAlreadyBeenDownloaded(fileState)) return Action.Skip + } + val localFileSize = getLocalFileSize(fileState.localPath) + if (isRemoteAndLocalSizeEqual(remoteFile.size, localFileSize)) { + return Action.UpdateDB + } + return Action.Download + } + + /** + * Define what to do of local file for which we know the Database equivalent + * @param localFile File instance representing a file on the device + * @param fileState SyncedFileState instance. Containing data from Database + * @return Action from Enum + */ + @JvmStatic + fun getActionForFileDiff(localFile: File, fileState: SyncedFileState): Action { + val isFileMoreRecentThanDB = fileState.lastModified < localFile.lastModified() + + return if (isFileMoreRecentThanDB || !fileState.isLastEtagStored()) { + Action.Upload + } else Action.Skip + } + + /** + * Compare RemoteFile's eTag with the one stored in Database + * @param remoteFile RemoteFile + * @param fileState last store file's state + * @return true if ETag + */ + @VisibleForTesting + @JvmStatic + fun hasEtagChanged(remoteFile: RemoteFile, fileState: SyncedFileState): Boolean { + return fileState.isLastEtagStored() && remoteFile.etag != fileState.lastEtag + } + + /** + * Indicate if the file has already been downloaded + * or detected on the device + * @param fileState SyncedFileState containing data from Database + * @return true if localLastModified store in Database == 0 + */ + @VisibleForTesting + @JvmStatic + fun hasAlreadyBeenDownloaded(fileState: SyncedFileState): Boolean { + return fileState.lastModified > 0 + } + + /** + * Compare file size for remote file & local file + * @param remoteFileSize RemoteFile.size. Note: it's equal to getLength() except for + * folder where it also sum size of its content + * @param localFileSize File.length() + * @return true if remote file size is same as local file size + */ + @VisibleForTesting + @JvmStatic + fun isRemoteAndLocalSizeEqual(remoteFileSize: Long, localFileSize: Long): Boolean { + return remoteFileSize == localFileSize + } + + + /** + * Check if timestamp is equal to max of unsigned int 32 + * + * For yet unknown reason, some remote files have this value on cloud (DB & file system) + * the only way to fix them is to force re upload of the file with correct value + * @param timestampInSecond remote file timestamp (Long) + * @return true if the timestamp is equal to max of unsigned int 32 + */ + @VisibleForTesting + @JvmStatic + fun isCorruptedTimestamp(timestampInSecond: Long): Boolean { + return timestampInSecond >= AppConstants.CORRUPTED_TIMESTAMP_IN_SECOND + } + + /** + * Fetch file size on device + * @param path Path of the file + * @return 0 if path is empty or file size + */ + @VisibleForTesting + @JvmStatic + fun getLocalFileSize(path: String): Long { + if (path.isEmpty()) return 0 + return File(path).length() + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java b/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java index 0d94ae0a86893c29f42a924c154a37cc59cc2b8d..6e62d02d61f0a0a7f8f44fb62921f8c4d4552658 100644 --- a/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java +++ b/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java @@ -22,7 +22,6 @@ import foundation.e.drive.database.DbHelper; import foundation.e.drive.models.SyncRequest; import foundation.e.drive.models.SyncedFileState; import foundation.e.drive.models.SyncedFolder; -import foundation.e.drive.utils.FileDiffUtils; import foundation.e.drive.utils.FileUtils; import timber.log.Timber; diff --git a/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java b/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java index 0780d6a076cbc4b45d21aba64d90d15e239265fc..3848eb8931b81fdc3a4bbda4b09a3f0616cc5151 100644 --- a/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java +++ b/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java @@ -11,7 +11,7 @@ import static foundation.e.drive.models.SyncRequest.Type.DISABLE_SYNCING; import static foundation.e.drive.models.SyncedFileStateKt.DO_NOT_SCAN; import static foundation.e.drive.models.SyncedFileStateKt.SCAN_ON_CLOUD; import static foundation.e.drive.models.SyncedFileStateKt.SCAN_ON_DEVICE; -import static foundation.e.drive.utils.FileDiffUtils.getActionForFileDiff; +import static foundation.e.drive.contentScanner.FileDiffUtils.getActionForFileDiff; import android.content.Context; @@ -26,7 +26,6 @@ import foundation.e.drive.models.DownloadRequest; import foundation.e.drive.models.SyncRequest; import foundation.e.drive.models.SyncedFileState; import foundation.e.drive.models.SyncedFolder; -import foundation.e.drive.utils.FileDiffUtils; import foundation.e.drive.utils.FileUtils; import timber.log.Timber; @@ -47,19 +46,18 @@ public class RemoteContentScanner extends AbstractContentScanner { @Override protected void onKnownFileFound(@NonNull RemoteFile file, @NonNull SyncedFileState fileState) { if (fileState.getScanScope() == DO_NOT_SCAN) return; - final FileDiffUtils.Action action = getActionForFileDiff(file, fileState); - if (action == FileDiffUtils.Action.Download) { - - Timber.d("Add download SyncRequest for %s", file.getRemotePath()); - syncRequests.put(fileState.getId(), new DownloadRequest(file, fileState)); - - } else if (action == FileDiffUtils.Action.updateDB) { - - fileState.setLastEtag(file.getEtag()); - final int affectedRows = DbHelper.manageSyncedFileStateDB(fileState, "UPDATE", context); - if (affectedRows == 0) Timber.d("Error while updating eTag in DB for: %s", file.getRemotePath()); + switch (action) { + case Upload: + addUploadRequest(fileState); + break; + case Download: + addDownloadRequest(fileState, file); + break; + case UpdateDB: + addUpdateDBRequest(fileState, file); + break; } } @@ -108,4 +106,20 @@ public class RemoteContentScanner extends AbstractContentScanner { protected boolean isSyncedFolderParentOfFile(@NonNull SyncedFolder syncedFolder, @NonNull String dirPath) { return syncedFolder.getRemoteFolder().equals(dirPath); } -} + + private void addUploadRequest(SyncedFileState fileState) { + fileState.setLastEtag(""); //Force fake lastEtag value to bypass condition check on UploadFileOperation + syncRequests.put(fileState.getId(), new SyncRequest(fileState, SyncRequest.Type.UPLOAD)); + } + + private void addUpdateDBRequest(SyncedFileState fileState, RemoteFile file) { + fileState.setLastEtag(file.getEtag()); + final int affectedRows = DbHelper.manageSyncedFileStateDB(fileState, "UPDATE", context); + if (affectedRows == 0) Timber.d("Error while updating eTag in DB for: %s", file.getRemotePath()); + } + + private void addDownloadRequest(SyncedFileState fileState, RemoteFile file) { + Timber.d("Add download SyncRequest for %s", file.getRemotePath()); + syncRequests.put(fileState.getId(), new DownloadRequest(file, fileState)); + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/models/SyncedFileState.kt b/app/src/main/java/foundation/e/drive/models/SyncedFileState.kt index ccd10465557f0d1dcb0d5ccc99405096a6f6f63d..e43e7cc59c80d0af31f56ff5322ec64c89ac5aa4 100644 --- a/app/src/main/java/foundation/e/drive/models/SyncedFileState.kt +++ b/app/src/main/java/foundation/e/drive/models/SyncedFileState.kt @@ -56,7 +56,7 @@ data class SyncedFileState (var id: Int, parcel.readLong(), parcel.readByte() != 0.toByte(), parcel.readInt() - ) { } + ) override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeInt(id) @@ -85,7 +85,7 @@ data class SyncedFileState (var id: Int, } fun isLastEtagStored() : Boolean { - return this.lastEtag.isNotEmpty() + return !this.lastEtag.isNullOrEmpty() } fun hasBeenSynchronizedOnce(): Boolean { @@ -93,7 +93,7 @@ data class SyncedFileState (var id: Int, } fun disableScanning() { - this.scanScope == DO_NOT_SCAN + this.scanScope = DO_NOT_SCAN } override fun toString(): String { diff --git a/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java b/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java index 256487fc3044897b131cc82dc7bbc8e67d7e09b8..370e9edebc06a04ed8aa0d05d7324efa0f7d1454 100644 --- a/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java +++ b/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java @@ -18,6 +18,8 @@ import static com.owncloud.android.lib.common.operations.RemoteOperationResult.R import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static foundation.e.drive.utils.AppConstants.CORRUPTED_TIMESTAMP_IN_SECOND; + import android.content.Context; import android.media.MediaScannerConnection; @@ -154,7 +156,9 @@ public class DownloadFileOperation extends RemoteOperation { return FORBIDDEN; } - targetFile.setLastModified(remoteFile.getModifiedTimestamp()); + if (remoteFile.getModifiedTimestamp() < CORRUPTED_TIMESTAMP_IN_SECOND) { + targetFile.setLastModified(remoteFile.getModifiedTimestamp() * 1000); //android uses timestamp in MS + } syncedFileState.setLastModified(targetFile.lastModified()); syncedFileState.setLastEtag(remoteFile.getEtag()); diff --git a/app/src/main/java/foundation/e/drive/utils/AppConstants.kt b/app/src/main/java/foundation/e/drive/utils/AppConstants.kt index 9822b5b814fdbdd4215b8e133bf7bd60e2857202..fbf2dc62be05f4b615aeb72e19d59a7017970890 100644 --- a/app/src/main/java/foundation/e/drive/utils/AppConstants.kt +++ b/app/src/main/java/foundation/e/drive/utils/AppConstants.kt @@ -38,6 +38,8 @@ object AppConstants { const val notificationChannelID = "foundation.e.drive" const val WORK_GENERIC_TAG = "eDrive" const val WORK_INITIALIZATION_TAG = "eDrive-init" + const val CORRUPTED_TIMESTAMP_IN_SECOND = 4294967295L + @JvmField val USER_AGENT = "eos(" + buildTime + ")-eDrive(" + BuildConfig.VERSION_NAME + ")" diff --git a/app/src/main/java/foundation/e/drive/utils/FileDiffUtils.java b/app/src/main/java/foundation/e/drive/utils/FileDiffUtils.java deleted file mode 100644 index 7f5fe817b9a71706715bb152836247a733f7b003..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/utils/FileDiffUtils.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright © MURENA SAS 2022-2023. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package foundation.e.drive.utils; -import androidx.annotation.NonNull; - -import com.owncloud.android.lib.resources.files.model.RemoteFile; - -import java.io.File; - -import foundation.e.drive.models.SyncedFileState; - -/** - * This class encapsulate code to compare syncedFile & Remote file - * but also RemoteFolder and SyncedFolder - * @author vincent Bourgmayer - */ -public class FileDiffUtils { - - public enum Action { - Upload, - Download, - skip, - updateDB - } - - /** - * Define what to do of RemoteFile for which we know the Database equivalent - * @param remoteFile RemoteFile - * @param fileState SyncedFileState instance - * @return Action from Enum - */ - @NonNull - public static Action getActionForFileDiff(@NonNull RemoteFile remoteFile, @NonNull SyncedFileState fileState) { - if (hasAlreadyBeenDownloaded(fileState) && !hasEtagChanged(remoteFile, fileState)) { - return Action.skip; - } - - final File localFile = new File(fileState.getLocalPath()); - - if (isRemoteSizeSameAsLocalSize(remoteFile, localFile)) { - return Action.updateDB; - } - return Action.Download; - } - - - /** - * Define what to do of local file for which we know the Database equivalent - * @param localFile File instance representing a file on the device - * @param fileState SyncedFileState instance. Containing data from Database - * @return Action from Enum - */ - @NonNull - public static Action getActionForFileDiff(@NonNull File localFile, @NonNull SyncedFileState fileState) { - //If no etag is stored in sfs, the file hasn't been sync up to server. then do upload - if (fileState.getLastModified() < localFile.lastModified() || !fileState.isLastEtagStored()) { - return Action.Upload; - } - return Action.skip; - } - - /** - * Compare RemoteFile's eTag with the one stored in Database - * @param file RemoteFile - * @param fileState last store file's state - * @return true if ETag - */ - private static boolean hasEtagChanged(@NonNull RemoteFile file, @NonNull SyncedFileState fileState) { - //if SyncedFileState has no Etag then it hasn't been uploaded and so must not exist on server - return fileState.isLastEtagStored() && !file.getEtag().equals(fileState.getLastEtag()); - } - - /** - * Indicate if the file has already been downloaded - * or detected on the device - * @param fileState SyncedFileState containing data from Database - * @return true if localLastModified store in Database == 0 - */ - private static boolean hasAlreadyBeenDownloaded(@NonNull SyncedFileState fileState) { - return fileState.getLastModified() > 0L; - } - - /** - * - * @param remoteFile RemoteFile instance - * @param localFile File instance - * @return true if remote file size is same as local file size - */ - private static boolean isRemoteSizeSameAsLocalSize(@NonNull RemoteFile remoteFile, @NonNull File localFile) { - // if local file doesn't exist its size will be 0 - // remoteFile.getSize() : getSize() is equal to getLength() except that for folder is also sum the content of the folder! - return remoteFile.getSize() == localFile.length(); - } -} \ No newline at end of file diff --git a/app/src/test/java/foundation/e/drive/contentScanner/FileDiffUtilsTest.kt b/app/src/test/java/foundation/e/drive/contentScanner/FileDiffUtilsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..2478e14a8707691dc21e6ece80c0befcb0e983c5 --- /dev/null +++ b/app/src/test/java/foundation/e/drive/contentScanner/FileDiffUtilsTest.kt @@ -0,0 +1,314 @@ +/* + * Copyright © MURENA SAS 2023. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ +package foundation.e.drive.contentScanner + +import com.owncloud.android.lib.resources.files.model.RemoteFile +import foundation.e.drive.models.SyncedFileState +import org.junit.Assert +import org.junit.Test +import org.mockito.Mockito +import java.io.File + +/** + * Unit for test FileDiffUtils's method + * + * note: when test method name mention 'DB' it refers to SyncedFileState, because those object + * represent data fetched from DB in the scope of the class under test. + * + * 'mTime' means modification time, it is a shortcut for 'lastModified' + * + * @author Vincent Bourgmayer + */ +internal class FileDiffUtilsTest { + private val currentTimestampInSecond = System.currentTimeMillis()/1000 + + /* isRemoteAndLocalSizeEqual() */ + @Test + fun `isRemoteAndLocalSizeEqual() return false when local file's is size smaller`() { + val remoteSize: Long = 12346789 + val localSize: Long = 1234 + + Assert.assertFalse("isRemoteAndLocalSizeEqual($remoteSize, $localSize) returned true instead of false", + FileDiffUtils.isRemoteAndLocalSizeEqual(remoteSize, localSize)) + } + + @Test + fun `isRemoteAndLocalSizeEqual() return false when local file's size is bigger`() { + val remoteSize: Long = 1234 + val localSize: Long = 12346789 + + Assert.assertFalse("isRemoteAndLocalSizeEqual($remoteSize, $localSize) returned true instead of false", + FileDiffUtils.isRemoteAndLocalSizeEqual(remoteSize, localSize)) + } + + @Test + fun `isRemoteAndLocalSizeEqual() return true when remote & local file size are equal`() { + val remoteSize: Long = 12346789 + val localSize: Long = 12346789 + + Assert.assertTrue("isRemoteAndLocalSizeEqual($remoteSize, $localSize) returned false instead of true", + FileDiffUtils.isRemoteAndLocalSizeEqual(remoteSize, localSize)) + } + + /* hasAlreadyBeenDownloaded() */ + @Test + fun `hasAlreadyBeenDownloaded() return true when last modified is bigger than 0`() { + val fileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(fileState.lastModified).thenReturn(1) + + Assert.assertTrue("hasAlreadyBeenDownloaded() returned false instead of true", + FileDiffUtils.hasAlreadyBeenDownloaded(fileState)) + } + + @Test + fun `hasAlreadyBeenDownloaded() return false when last modified equal to 0`() { + val fileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(fileState.lastModified).thenReturn(0) + + Assert.assertFalse("hasAlreadyBeenDownloaded() returned true instead of false", + FileDiffUtils.hasAlreadyBeenDownloaded(fileState)) + } + + /* hasEtagChanged() */ + @Test + fun `hasEtagChanged() return false when DB doesn't have eTag return false`() { + val remoteFile = Mockito.mock(RemoteFile::class.java) + + val fileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(fileState.isLastEtagStored()).thenReturn(false) + + Assert.assertFalse("hasEtagChanged() returned true instead of false", + FileDiffUtils.hasEtagChanged(remoteFile, fileState)) + } + + @Test + fun `hasEtagChanged() return false when eTag from remoteFile & DB are the same`() { + val remoteFile = Mockito.mock(RemoteFile::class.java) + Mockito.`when`(remoteFile.etag).thenReturn("123456789") + + val fileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(fileState.lastEtag).thenReturn("123456789") + Mockito.`when`(fileState.isLastEtagStored()).thenReturn(true) + + Assert.assertFalse("hasEtagChanged() returned true instead of false", + FileDiffUtils.hasEtagChanged(remoteFile, fileState)) + } + + @Test + fun `hasEtagChanged() return true when Remote eTag is empty while DB is not`() { + val remoteFile = Mockito.mock(RemoteFile::class.java) + Mockito.`when`(remoteFile.etag).thenReturn("") + + val fileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(fileState.lastEtag).thenReturn("123456789") + Mockito.`when`(fileState.isLastEtagStored()).thenReturn(true) + + Assert.assertTrue("hasEtagChanged() returned false instead of true", + FileDiffUtils.hasEtagChanged(remoteFile, fileState)) + } + + @Test + fun `hasEtagChanged() return true when eTag from DB & Remote are different`() { + val remoteFile = Mockito.mock(RemoteFile::class.java) + //Mockito.`when`(remoteFile.etag).thenReturn("123456789") + remoteFile.etag = "123456789" + val fileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(fileState.lastEtag).thenReturn("987654321") + Mockito.`when`(fileState.isLastEtagStored()).thenReturn(true) + + Assert.assertTrue("hasEtagChanged() returned false instead of true", + FileDiffUtils.hasEtagChanged(remoteFile, fileState)) + } + + + /* getActionForFileDiff for localFile */ + + @Test + fun `getActionForFileDiff(File) return Upload when local file mTime is smaller & no eTag in DB`() { + val mockedLastModified: Long = 1 + val mockedFile = Mockito.spy(File("/local/path.txt")) + Mockito.`when`(mockedFile.lastModified()).thenReturn(mockedLastModified) + + val mockedLastModifiedFromDB: Long = 10 + val fileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(fileState.isLastEtagStored()).thenReturn(false) + Mockito.`when`(fileState.lastModified).thenReturn(mockedLastModifiedFromDB) + + val resultUnderTest = FileDiffUtils.getActionForFileDiff(mockedFile, fileState) + Assert.assertEquals("getActionForFileDiff() returned $resultUnderTest instead of Action.Upload", FileDiffUtils.Action.Upload, resultUnderTest) + } + + @Test + fun `getActionForFileDiff(File) return Skip when local file mTime is smaller & eTag is in DB`() { + val mockedLastModified: Long = 1 + val mockedFile = Mockito.spy(File("/local/path.txt")) + Mockito.`when`(mockedFile.lastModified()).thenReturn(mockedLastModified) + + val mockedLastModifiedFromDB: Long = 10 + val fileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(fileState.isLastEtagStored()).thenReturn(true) + Mockito.`when`(fileState.lastModified).thenReturn(mockedLastModifiedFromDB) + + val resultUnderTest = FileDiffUtils.getActionForFileDiff(mockedFile, fileState) + Assert.assertEquals("getActionForFileDiff() returned $resultUnderTest instead of Action.Skip", FileDiffUtils.Action.Skip, resultUnderTest) + } + + @Test + fun `getActionForFileDiff(File) return Upload when local file mTime is equal to DB & eTag not in DB`() { + val mockedLastModified: Long = 10 + val mockedFile = Mockito.spy(File("/local/path.txt")) + Mockito.`when`(mockedFile.lastModified()).thenReturn(mockedLastModified) + + val mockedLastModifiedFromDB: Long = 10 + val fileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(fileState.isLastEtagStored()).thenReturn(false) + Mockito.`when`(fileState.lastModified).thenReturn(mockedLastModifiedFromDB) + + val resultUnderTest = FileDiffUtils.getActionForFileDiff(mockedFile, fileState) + Assert.assertEquals("getActionForFileDiff() returned $resultUnderTest instead of Action.Upload", FileDiffUtils.Action.Upload, resultUnderTest) + } + + @Test + fun `getActionForFileDiff(File) return Skip when local file mTime is equal to DB & eTag is in DB`() { + val mockedLastModified: Long = 10 + val mockedFile = Mockito.spy(File("/local/path.txt")) + Mockito.`when`(mockedFile.lastModified()).thenReturn(mockedLastModified) + + val mockedLastModifiedFromDB: Long = 10 + val fileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(fileState.isLastEtagStored()).thenReturn(true) + Mockito.`when`(fileState.lastModified).thenReturn(mockedLastModifiedFromDB) + + val resultUnderTest = FileDiffUtils.getActionForFileDiff(mockedFile, fileState) + Assert.assertEquals("getActionForFileDiff() returned $resultUnderTest instead of Action.Skip", FileDiffUtils.Action.Skip, resultUnderTest) + } + + @Test + fun `getActionForFileDiff(File) return Upload when local file mTime is bigger & eTag not in DB`() { + val mockedLastModified: Long = 10 + val mockedFile = Mockito.spy(File("/local/path.txt")) + Mockito.`when`(mockedFile.lastModified()).thenReturn(mockedLastModified) + + val mockedLastModifiedFromDB: Long = 1 + val fileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(fileState.isLastEtagStored()).thenReturn(false) + Mockito.`when`(fileState.lastModified).thenReturn(mockedLastModifiedFromDB) + + val resultUnderTest = FileDiffUtils.getActionForFileDiff(mockedFile, fileState) + Assert.assertEquals("getActionForFileDiff() returned $resultUnderTest instead of Action.Upload", FileDiffUtils.Action.Upload, resultUnderTest) + } + + @Test + fun `getActionForFileDiff(File) return Upload when local file mTime is bigger & eTag is in DB`() { + val mockedLastModified: Long = 10 + val mockedFile = Mockito.spy(File("/local/path.txt")) + Mockito.`when`(mockedFile.lastModified()).thenReturn(mockedLastModified) + + val mockedLastModifiedFromDB: Long = 1 + val fileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(fileState.isLastEtagStored()).thenReturn(true) + Mockito.`when`(fileState.lastModified).thenReturn(mockedLastModifiedFromDB) + + val resultUnderTest = FileDiffUtils.getActionForFileDiff(mockedFile, fileState) + Assert.assertEquals("getActionForFileDiff() returned $resultUnderTest instead of Action.Upload", FileDiffUtils.Action.Upload, resultUnderTest) + } + + /* getActionForFileDiff for remoteFile */ + @Test + fun `getActionForFileDiff(RemoteFile) return Skip when file is up to date`() { + val mockedRemoteFile = Mockito.mock(RemoteFile::class.java) + Mockito.`when`(mockedRemoteFile.modifiedTimestamp).thenReturn(currentTimestampInSecond) + Mockito.`when`(mockedRemoteFile.etag).thenReturn("123456") + val mockedFileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(mockedFileState.lastModified).thenReturn(1L) + Mockito.`when`(mockedFileState.lastEtag).thenReturn("123456") + Mockito.`when`(mockedFileState.isLastEtagStored()).thenReturn(true) + + val resultUnderTest = FileDiffUtils.getActionForFileDiff(mockedRemoteFile, mockedFileState) + Assert.assertEquals("getActionForFileDiff(RemoteFile) returned $resultUnderTest instead of Action.Skip", FileDiffUtils.Action.Skip, resultUnderTest) + } + + @Test + fun `getActionForFileDiff(RemoteFile) return UpdateDB when file is up to date but not DB`() { + val mockedRemoteFile = Mockito.mock(RemoteFile::class.java) + Mockito.`when`(mockedRemoteFile.modifiedTimestamp).thenReturn(currentTimestampInSecond) + Mockito.`when`(mockedRemoteFile.etag).thenReturn("123456") + Mockito.`when`(mockedRemoteFile.size).thenReturn(0) //local file doesn't exist for test, so its size will be 0 + val mockedFileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(mockedFileState.lastModified).thenReturn(1L) + Mockito.`when`(mockedFileState.localPath).thenReturn("/local/file.txt") + Mockito.`when`(mockedFileState.lastEtag).thenReturn("12") + Mockito.`when`(mockedFileState.isLastEtagStored()).thenReturn(true) + + val resultUnderTest = FileDiffUtils.getActionForFileDiff(mockedRemoteFile, mockedFileState) + Assert.assertEquals("getActionForFileDiff(RemoteFile) returned $resultUnderTest instead of Action.UpdateDB", FileDiffUtils.Action.UpdateDB, resultUnderTest) + } + + + @Test + fun `getActionForFileDiff(RemoteFile) return Download when file is not up to date`() { + val mockedRemoteFile = Mockito.mock(RemoteFile::class.java) + Mockito.`when`(mockedRemoteFile.modifiedTimestamp).thenReturn(currentTimestampInSecond) + Mockito.`when`(mockedRemoteFile.etag).thenReturn("123456") + Mockito.`when`(mockedRemoteFile.size).thenReturn(1) //local file doesn't exist for test, so its size will be 0 + val mockedFileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(mockedFileState.lastModified).thenReturn(1L) + Mockito.`when`(mockedFileState.localPath).thenReturn("/local/file.txt") + Mockito.`when`(mockedFileState.lastEtag).thenReturn("12") + Mockito.`when`(mockedFileState.isLastEtagStored()).thenReturn(true) + + val resultUnderTest = FileDiffUtils.getActionForFileDiff(mockedRemoteFile, mockedFileState) + Assert.assertEquals("getActionForFileDiff(RemoteFile) returned $resultUnderTest instead of Action.Download", FileDiffUtils.Action.Download, resultUnderTest) + } + + @Test + fun `getActionForFileDiff(RemoteFile) return Upload when remote modifiedTimestamp is corrupted and eTag unchanged`() { + val mockedRemoteFile = Mockito.mock(RemoteFile::class.java) + Mockito.`when`(mockedRemoteFile.modifiedTimestamp).thenReturn(4294967295L) + Mockito.`when`(mockedRemoteFile.etag).thenReturn("123456") + val mockedFileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(mockedFileState.lastEtag).thenReturn("123456") + val resultUnderTest = FileDiffUtils.getActionForFileDiff(mockedRemoteFile, mockedFileState) + Assert.assertEquals("getActionForFileDiff(RemoteFile) returned $resultUnderTest instead of Action.Upload", FileDiffUtils.Action.Upload, resultUnderTest) + } + + @Test + fun `getActionForFileDiff(RemoteFile) don't return Upload when remote modifiedTimestamp is corrupted but eTag changed`() { + val mockedRemoteFile = Mockito.mock(RemoteFile::class.java) + Mockito.`when`(mockedRemoteFile.modifiedTimestamp).thenReturn(4294967295L) + Mockito.`when`(mockedRemoteFile.etag).thenReturn("654321") + val mockedFileState = Mockito.mock(SyncedFileState::class.java) + Mockito.`when`(mockedFileState.lastEtag).thenReturn("123456789") + Mockito.`when`(mockedFileState.isLastEtagStored()).thenReturn(true) + Mockito.`when`(mockedFileState.localPath).thenReturn("/local/file.txt") + val resultUnderTest = FileDiffUtils.getActionForFileDiff(mockedRemoteFile, mockedFileState) + Assert.assertNotEquals("getActionForFileDiff(RemoteFile) returned $resultUnderTest while it should not", FileDiffUtils.Action.Upload, resultUnderTest) + } + + /* isCorruptedTimestamp for localFile */ + @Test + fun `isCorruptedTimestamp() return true with timestamp equal to max of Int32 `() { + val corruptedTimestamp = 4294967295L + val resultUnderTest = FileDiffUtils.isCorruptedTimestamp(corruptedTimestamp) + Assert.assertTrue("isCorruptedTimestamp(4294967295L) returned $resultUnderTest instead of true", resultUnderTest) + } + + @Test + fun `isCorruptedTimestamp() return true with timestamp bigger than max of Int32 `() { + val corruptedTimestamp = 4294967295L + 1 + val resultUnderTest = FileDiffUtils.isCorruptedTimestamp(corruptedTimestamp) + Assert.assertTrue("isCorruptedTimestamp(4294967295L) returned $resultUnderTest instead of true", resultUnderTest) + } + + @Test + fun `isCorruptedTimestamp() return false with timestamp smaller than max of Int32 `() { + val corruptedTimestamp = 4294967295L - 1 + val resultUnderTest = FileDiffUtils.isCorruptedTimestamp(corruptedTimestamp) + Assert.assertFalse("isCorruptedTimestamp(4294967295L) returned $resultUnderTest instead of false", resultUnderTest) + } +} \ No newline at end of file