1
+ /*
2
+ * Nextcloud - Android Client
3
+ *
4
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
5
+ * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
6
+ */
7
+ package com.nextcloud.client.jobs.upload
8
+
9
+ import android.content.Context
10
+ import androidx.test.platform.app.InstrumentationRegistry
11
+ import androidx.test.ext.junit.runners.AndroidJUnit4
12
+ import com.nextcloud.client.account.UserAccountManagerImpl
13
+ import com.owncloud.android.datamodel.UploadsStorageManager
14
+ import com.owncloud.android.db.OCUpload
15
+ import com.owncloud.android.files.services.NameCollisionPolicy
16
+ import com.owncloud.android.operations.FixedChunkUploadRemoteOperation
17
+ import org.junit.After
18
+ import org.junit.Assert.*
19
+ import org.junit.Before
20
+ import org.junit.Test
21
+ import org.junit.runner.RunWith
22
+ import java.io.File
23
+ import java.io.FileOutputStream
24
+
25
+ @RunWith(AndroidJUnit4 ::class )
26
+ class FileUploadWorkerInstrumentedTest {
27
+
28
+ private lateinit var context: Context
29
+ private lateinit var uploadsStorageManager: UploadsStorageManager
30
+ private lateinit var tempDir: File
31
+
32
+ @Before
33
+ fun setUp () {
34
+ context = InstrumentationRegistry .getInstrumentation().targetContext
35
+
36
+ // Initialize UploadsStorageManager with real database
37
+ uploadsStorageManager = UploadsStorageManager (
38
+ UserAccountManagerImpl .fromContext(context),
39
+ context.contentResolver
40
+ )
41
+
42
+ // Create temp directory for test files
43
+ tempDir = File (context.cacheDir, " file_upload_worker_test" )
44
+ if (! tempDir.exists()) {
45
+ tempDir.mkdirs()
46
+ }
47
+
48
+ // Clean up any existing uploads
49
+ uploadsStorageManager.removeAllUploads()
50
+ }
51
+
52
+ @After
53
+ fun tearDown () {
54
+ // Clean up test files
55
+ if (tempDir.exists()) {
56
+ deleteRecursive(tempDir)
57
+ }
58
+
59
+ // Clean up uploads
60
+ uploadsStorageManager.removeAllUploads()
61
+ }
62
+
63
+ @Test
64
+ fun testFileUploadWorkerConstants () {
65
+ // Test that FileUploadWorker constants are correctly defined
66
+ assertEquals(" ACCOUNT constant" , " data_account" , FileUploadWorker .ACCOUNT )
67
+ assertEquals(" UPLOAD_IDS constant" , " uploads_ids" , FileUploadWorker .UPLOAD_IDS )
68
+ assertEquals(" LOCAL_BEHAVIOUR_COPY" , 0 , FileUploadWorker .LOCAL_BEHAVIOUR_COPY )
69
+ assertEquals(" LOCAL_BEHAVIOUR_MOVE" , 1 , FileUploadWorker .LOCAL_BEHAVIOUR_MOVE )
70
+ assertEquals(" LOCAL_BEHAVIOUR_FORGET" , 2 , FileUploadWorker .LOCAL_BEHAVIOUR_FORGET )
71
+ assertEquals(" LOCAL_BEHAVIOUR_DELETE" , 3 , FileUploadWorker .LOCAL_BEHAVIOUR_DELETE )
72
+ }
73
+
74
+ @Test
75
+ fun testDeterministicNotificationIdGeneration () {
76
+ // Test notification ID generation with real file system
77
+ val testFile1 = createTestFile(" notification_test1.txt" , 1024L )
78
+ val testFile2 = createTestFile(" notification_test2.txt" , 2048L )
79
+
80
+ // Test same file produces same notification ID
81
+ val id1a = generateTestNotificationId(testFile1.absolutePath, testFile1.length())
82
+ val id1b = generateTestNotificationId(testFile1.absolutePath, testFile1.length())
83
+ assertEquals(" Same file should generate same notification ID" , id1a, id1b)
84
+
85
+ // Test different files produce different notification IDs
86
+ val id2 = generateTestNotificationId(testFile2.absolutePath, testFile2.length())
87
+ assertNotEquals(" Different files should generate different notification IDs" , id1a, id2)
88
+
89
+ // Test different file sizes produce different notification IDs
90
+ val id1c = generateTestNotificationId(testFile1.absolutePath, 4096L )
91
+ assertNotEquals(" Different file sizes should generate different notification IDs" , id1a, id1c)
92
+
93
+ // Verify all IDs are positive
94
+ assertTrue(" Notification IDs should be positive" , id1a > 0 )
95
+ assertTrue(" Notification IDs should be positive" , id1b > 0 )
96
+ assertTrue(" Notification IDs should be positive" , id2 > 0 )
97
+ assertTrue(" Notification IDs should be positive" , id1c > 0 )
98
+ }
99
+
100
+ @Test
101
+ fun testRealDatabaseOperations () {
102
+ // Test FileUploadWorker integration with real UploadsStorageManager database
103
+ val testFile = createTestFile(" database_test.txt" , 1024L )
104
+
105
+ // Create upload entry
106
+ val upload = OCUpload (
107
+ testFile.absolutePath,
108
+ " /remote/database_test.txt" ,
109
+
110
+ ).apply {
111
+ nameCollisionPolicy = NameCollisionPolicy .DEFAULT
112
+ localAction = FileUploadWorker .LOCAL_BEHAVIOUR_COPY
113
+ isUseWifiOnly = false
114
+ isWhileChargingOnly = false
115
+ }
116
+
117
+ // Store in database
118
+ val uploadId = uploadsStorageManager.storeUpload(upload)
119
+ assertTrue(" Upload should be stored successfully" , uploadId > 0 )
120
+
121
+ // Verify upload can be retrieved
122
+ val retrievedUpload = uploadsStorageManager.getUploadById(uploadId)
123
+ assertNotNull(" Upload should be retrievable" , retrievedUpload)
124
+ assertEquals(" Local paths should match" , upload.localPath, retrievedUpload?.localPath)
125
+ assertEquals(" Remote paths should match" , upload.remotePath, retrievedUpload?.remotePath)
126
+ assertEquals(" Account names should match" , upload.accountName, retrievedUpload?.accountName)
127
+
128
+ // Note: updateUploadStatus with just status is private, so we'll skip this test
129
+
130
+ // Test upload removal
131
+ uploadsStorageManager.removeUpload(uploadId)
132
+ val deletedUpload = uploadsStorageManager.getUploadById(uploadId)
133
+ assertNull(" Upload should be removed" , deletedUpload)
134
+ }
135
+
136
+ @Test
137
+ fun testFixedChunkUploadRemoteOperationCreation () {
138
+ // Test that FixedChunkUploadRemoteOperation can be created
139
+ val testFile = createTestFile(" chunk_test.bin" , 3 * 1024 * 1024L ) // 3MB
140
+
141
+ val operation = FixedChunkUploadRemoteOperation (
142
+ testFile.absolutePath,
143
+ " /remote/chunk_test.bin" ,
144
+ " application/octet-stream" ,
145
+ null ,
146
+ System .currentTimeMillis(),
147
+ null ,
148
+ false ,
149
+ context
150
+ )
151
+
152
+ assertNotNull(" Chunk upload operation should be created" , operation)
153
+ assertFalse(" Operation should not be cancelled initially" , operation.isCancelled())
154
+ assertEquals(" Fixed chunk size should be 1MB" , 1024 * 1024 , FixedChunkUploadRemoteOperation .FIXED_CHUNK_SIZE )
155
+ }
156
+
157
+ @Test
158
+ fun testProgressCalculationWithRealFiles () {
159
+ // Test progress calculation with real file sizes
160
+ val smallFile = createTestFile(" small_progress.txt" , 1024L )
161
+ val mediumFile = createTestFile(" medium_progress.txt" , 1024 * 512L ) // 512KB
162
+ val largeFile = createTestFile(" large_progress.txt" , 1024 * 1024 * 3L ) // 3MB
163
+
164
+ // Test progress calculations
165
+ assertEquals(" 0% for 0 transferred" , 0 , calculatePercent(0 , smallFile.length()))
166
+ assertEquals(" 50% for half transferred" , 50 , calculatePercent(smallFile.length() / 2 , smallFile.length()))
167
+ assertEquals(" 100% for fully transferred" , 100 , calculatePercent(smallFile.length(), smallFile.length()))
168
+
169
+ // Test with different file sizes
170
+ assertEquals(" 25% for quarter of medium file" , 25 , calculatePercent(mediumFile.length() / 4 , mediumFile.length()))
171
+ assertEquals(" 75% for three quarters of large file" , 75 , calculatePercent(largeFile.length() * 3 / 4 , largeFile.length()))
172
+
173
+ // Test edge cases
174
+ assertEquals(" 0% for zero total" , 0 , calculatePercent(100 , 0 ))
175
+ assertEquals(" 100% for over-transferred" , 100 , calculatePercent(150 , 100 ))
176
+ }
177
+
178
+ @Test
179
+ fun testChunkedUploadThresholds () {
180
+ // Test that files above threshold would trigger chunked upload logic
181
+ val smallFile = createTestFile(" small_threshold.txt" , 1024L ) // 1KB
182
+ val largeFile = createTestFile(" large_threshold.txt" , 3 * 1024 * 1024L ) // 3MB
183
+
184
+ assertTrue(" Small file should be below chunk threshold" ,
185
+ smallFile.length() < FixedChunkUploadRemoteOperation .FIXED_CHUNK_SIZE )
186
+ assertTrue(" Large file should be above chunk threshold" ,
187
+ largeFile.length() > FixedChunkUploadRemoteOperation .FIXED_CHUNK_SIZE )
188
+
189
+ // Calculate expected chunk count for large file
190
+ val expectedChunks = (largeFile.length() + FixedChunkUploadRemoteOperation .FIXED_CHUNK_SIZE - 1 ) /
191
+ FixedChunkUploadRemoteOperation .FIXED_CHUNK_SIZE
192
+ assertTrue(" Large file should require multiple chunks" , expectedChunks > 1 )
193
+ assertEquals(" Should calculate 3 chunks for 3MB file" , 3 , expectedChunks)
194
+ }
195
+
196
+ // Helper methods
197
+
198
+ private fun createTestFile (fileName : String , size : Long ): File {
199
+ val testFile = File (tempDir, fileName)
200
+ FileOutputStream (testFile).use { fos ->
201
+ val buffer = ByteArray (8192 )
202
+ var bytesWritten = 0L
203
+
204
+ // Fill buffer with test data
205
+ for (i in buffer.indices) {
206
+ buffer[i] = (i % 256 ).toByte()
207
+ }
208
+
209
+ while (bytesWritten < size) {
210
+ val bytesToWrite = Math .min(buffer.size.toLong(), size - bytesWritten).toInt()
211
+ fos.write(buffer, 0 , bytesToWrite)
212
+ bytesWritten + = bytesToWrite
213
+ }
214
+ }
215
+
216
+ assertTrue(" Test file should exist" , testFile.exists())
217
+ assertEquals(" Test file should have correct size" , size, testFile.length())
218
+ return testFile
219
+ }
220
+
221
+ private fun deleteRecursive (file : File ) {
222
+ if (file.isDirectory) {
223
+ file.listFiles()?.forEach { deleteRecursive(it) }
224
+ }
225
+ file.delete()
226
+ }
227
+
228
+ private fun generateTestNotificationId (localPath : String , fileSize : Long ): Int {
229
+ return try {
230
+ val file = File (localPath)
231
+ val canonicalPath = try {
232
+ file.canonicalPath
233
+ } catch (e: java.io.IOException ) {
234
+ localPath
235
+ }
236
+ val baseString = " ${canonicalPath} _$fileSize "
237
+ val hash = baseString.hashCode()
238
+ Math .abs(hash)
239
+ } catch (e: Exception ) {
240
+ Math .abs(" ${localPath} _$fileSize " .hashCode())
241
+ }
242
+ }
243
+
244
+ private fun calculatePercent (transferred : Long , total : Long ): Int {
245
+ return if (total == 0L ) 0 else (100.0 * transferred / total).toInt().coerceAtMost(100 )
246
+ }
247
+ }
0 commit comments