repo created

This commit is contained in:
Fr4nz D13trich 2025-09-18 17:54:51 +02:00
commit 1ef725ef20
2483 changed files with 278273 additions and 0 deletions

View file

@ -0,0 +1,222 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.files
import android.net.Uri
import com.nextcloud.client.account.Server
import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager
import com.owncloud.android.lib.resources.status.OwnCloudVersion
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.junit.runners.Suite
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import java.net.URI
@RunWith(Suite::class)
@Suite.SuiteClasses(
DeepLinkHandlerTest.DeepLinkPattern::class,
DeepLinkHandlerTest.FileDeepLink::class
)
class DeepLinkHandlerTest {
@RunWith(Parameterized::class)
class DeepLinkPattern {
companion object {
val FILE_ID = 1234
val SERVER_BASE_URLS = listOf(
"http://hostname.net",
"https://hostname.net",
"http://hostname.net/subdir1",
"https://hostname.net/subdir1",
"http://hostname.net/subdir1/subdir2",
"https://hostname.net/subdir1/subdir2",
"http://hostname.net/subdir1/subdir2/subdir3",
"https://hostname.net/subdir1/subdir2/subdir3"
)
val INDEX_PHP_PATH = listOf(
"",
"/index.php"
)
@Parameterized.Parameters
@JvmStatic
fun urls(): Array<Array<Any>> {
val testInput = mutableListOf<Array<Any>>()
SERVER_BASE_URLS.forEach { baseUrl ->
INDEX_PHP_PATH.forEach { indexPath ->
val url = "$baseUrl$indexPath/f/$FILE_ID"
testInput.add(arrayOf(baseUrl, indexPath, "$FILE_ID", url))
}
}
return testInput.toTypedArray()
}
}
@Parameterized.Parameter(0)
lateinit var baseUrl: String
@Parameterized.Parameter(1)
lateinit var indexPath: String
@Parameterized.Parameter(2)
lateinit var fileId: String
@Parameterized.Parameter(3)
lateinit var url: String
@Test
fun matches_deep_link_patterns() {
val match = DeepLinkHandler.DEEP_LINK_PATTERN.matchEntire(url)
assertNotNull("Url [$url] does not match pattern", match)
assertEquals(baseUrl, match?.groupValues?.get(DeepLinkHandler.BASE_URL_GROUP_INDEX))
assertEquals(indexPath, match?.groupValues?.get(DeepLinkHandler.INDEX_PATH_GROUP_INDEX))
assertEquals(fileId, match?.groupValues?.get(DeepLinkHandler.FILE_ID_GROUP_INDEX))
}
@Test
fun no_trailing_path_allowed_after_file_id() {
val invalidUrl = "$url/"
val match = DeepLinkHandler.DEEP_LINK_PATTERN.matchEntire(invalidUrl)
assertNull(match)
}
}
class FileDeepLink {
companion object {
const val OTHER_SERVER_BASE_URL = "https://someotherserver.net"
const val SERVER_BASE_URL = "https://server.net"
const val FILE_ID = "1234567890"
val DEEP_LINK = Uri.parse("$SERVER_BASE_URL/index.php/f/$FILE_ID")
fun createMockUser(serverBaseUrl: String): User {
val user = mock<User>()
val uri = URI.create(serverBaseUrl)
val server = Server(uri = uri, version = OwnCloudVersion.nextcloud_19)
whenever(user.server).thenReturn(server)
return user
}
}
@Mock
lateinit var userAccountManager: UserAccountManager
lateinit var allUsers: List<User>
lateinit var handler: DeepLinkHandler
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(userAccountManager.allUsers).thenAnswer { allUsers }
allUsers = emptyList()
handler = DeepLinkHandler(userAccountManager)
}
@Test
fun no_user_can_open_file() {
// GIVEN
// no user capable of opening the file
allUsers = listOf(
createMockUser(OTHER_SERVER_BASE_URL),
createMockUser(OTHER_SERVER_BASE_URL)
)
// WHEN
// deep link is parsed
val match = handler.parseDeepLink(DEEP_LINK)
// THEN
// link is valid
// no user can open the file
assertNotNull(match)
assertEquals(0, match?.users?.size)
}
@Test
fun single_user_can_open_file() {
// GIVEN
// multiple users registered
// one user capable of opening the link
val matchingUser = createMockUser(SERVER_BASE_URL)
allUsers = listOf(
createMockUser(OTHER_SERVER_BASE_URL),
matchingUser,
createMockUser(OTHER_SERVER_BASE_URL)
)
// WHEN
// deep link is parsed
val match = handler.parseDeepLink(DEEP_LINK)
// THEN
// link can be opened by single user
assertNotNull(match)
assertSame(matchingUser, match?.users?.get(0))
}
@Test
fun multiple_users_can_open_file() {
// GIVEN
// mutltiple users registered
// multiple users capable of opening the link
val matchingUsers = setOf(
createMockUser(SERVER_BASE_URL),
createMockUser(SERVER_BASE_URL)
)
val otherUsers = setOf(
createMockUser(OTHER_SERVER_BASE_URL),
createMockUser(OTHER_SERVER_BASE_URL)
)
allUsers = listOf(matchingUsers, otherUsers).flatten()
// WHEN
// deep link is parsed
val match = handler.parseDeepLink(DEEP_LINK)
// THEN
// link can be opened by multiple matching users
assertNotNull(match)
assertEquals(matchingUsers, match?.users?.toSet())
}
@Test
fun match_contains_extracted_file_id() {
// WHEN
// valid deep file link is parsed
val match = handler.parseDeepLink(DEEP_LINK)
// THEN
// file id is returned
assertEquals(FILE_ID, match?.fileId)
}
@Test
fun no_match_for_invalid_link() {
// GIVEN
// invalid deep link
val invalidLink = Uri.parse("http://www.dodgylink.com/index.php")
// WHEN
// deep link is parsed
val match = handler.parseDeepLink(invalidLink)
// THEN
// no match
assertNull(match)
}
}
}

View file

@ -0,0 +1,46 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.files.download
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.rule.ServiceTestRule
import com.nextcloud.client.account.MockUser
import com.nextcloud.client.jobs.transfer.FileTransferService
import io.mockk.MockKAnnotations
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
class DownloaderServiceTest {
@get:Rule
val service = ServiceTestRule.withTimeout(3, TimeUnit.SECONDS)
val user = MockUser()
@Before
fun setUp() {
MockKAnnotations.init(this, relaxed = true)
}
@Test(expected = TimeoutException::class)
fun cannot_bind_to_service_without_user() {
val intent = FileTransferService.createBindIntent(getApplicationContext(), user)
intent.removeExtra(FileTransferService.EXTRA_USER)
service.bindService(intent)
}
@Test
fun bind_with_user() {
val intent = FileTransferService.createBindIntent(getApplicationContext(), user)
val binder = service.bindService(intent)
assertTrue(binder is FileTransferService.Binder)
}
}

View file

@ -0,0 +1,514 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.files.download
import com.nextcloud.client.account.User
import com.nextcloud.client.files.DownloadRequest
import com.nextcloud.client.files.Registry
import com.nextcloud.client.files.Request
import com.nextcloud.client.jobs.transfer.Transfer
import com.nextcloud.client.jobs.transfer.TransferState
import com.owncloud.android.datamodel.OCFile
import io.mockk.CapturingSlot
import io.mockk.MockKAnnotations
import io.mockk.clearAllMocks
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Suite
import java.util.UUID
@RunWith(Suite::class)
@Suite.SuiteClasses(
RegistryTest.Pending::class,
RegistryTest.Start::class,
RegistryTest.Progress::class,
RegistryTest.Complete::class,
RegistryTest.GetTransfers::class,
RegistryTest.IsRunning::class
)
class RegistryTest {
abstract class Base {
companion object {
const val MAX_TRANSFER_THREADS = 4
const val PROGRESS_FULL = 100
const val PROGRESS_HALF = 50
}
@MockK
lateinit var user: User
lateinit var file: OCFile
@MockK
lateinit var onTransferStart: (UUID, Request) -> Unit
@MockK
lateinit var onTransferChanged: (Transfer) -> Unit
internal lateinit var registry: Registry
@Before
fun setUpBase() {
MockKAnnotations.init(this, relaxed = true)
file = OCFile("/test/path")
registry = Registry(onTransferStart, onTransferChanged, MAX_TRANSFER_THREADS)
resetMocks()
}
fun resetMocks() {
clearAllMocks()
every { onTransferStart(any(), any()) } answers {}
every { onTransferChanged(any()) } answers {}
}
}
class Pending : Base() {
@Test
fun inserting_pending_transfer() {
// GIVEN
// registry has no pending transfers
assertEquals(0, registry.pending.size)
// WHEN
// new transfer requests added
val addedTransfersCount = 10
for (i in 0 until addedTransfersCount) {
val request = DownloadRequest(user, file)
registry.add(request)
}
// THEN
// transfer is added to the pending queue
assertEquals(addedTransfersCount, registry.pending.size)
}
}
class Start : Base() {
companion object {
const val ENQUEUED_REQUESTS_COUNT = 10
}
@Before
fun setUp() {
for (i in 0 until ENQUEUED_REQUESTS_COUNT) {
registry.add(DownloadRequest(user, file))
}
assertEquals(ENQUEUED_REQUESTS_COUNT, registry.pending.size)
}
@Test
fun starting_transfer() {
// WHEN
// started
registry.startNext()
// THEN
// up to max threads requests are started
// start callback is triggered
// update callback is triggered on transfer transition
// started transfers are in running state
assertEquals(
"Transfers not moved to running queue",
MAX_TRANSFER_THREADS,
registry.running.size
)
assertEquals(
"Transfers not moved from pending queue",
ENQUEUED_REQUESTS_COUNT - MAX_TRANSFER_THREADS,
registry.pending.size
)
verify(exactly = MAX_TRANSFER_THREADS) { onTransferStart(any(), any()) }
val startedTransfers = mutableListOf<Transfer>()
verify(exactly = MAX_TRANSFER_THREADS) { onTransferChanged(capture(startedTransfers)) }
assertEquals(
"Callbacks not invoked for running transfers",
MAX_TRANSFER_THREADS,
startedTransfers.size
)
startedTransfers.forEach {
assertEquals("Transfer not placed into running state", TransferState.RUNNING, it.state)
}
}
@Test
fun start_is_ignored_if_no_more_free_threads() {
// WHEN
// max number of running transfers
registry.startNext()
assertEquals(MAX_TRANSFER_THREADS, registry.running.size)
clearAllMocks()
// WHEN
// starting more transfers
registry.startNext()
// THEN
// no more transfers can be started
assertEquals(MAX_TRANSFER_THREADS, registry.running.size)
verify(exactly = 0) { onTransferStart(any(), any()) }
}
}
class Progress : Base() {
var uuid: UUID = UUID.randomUUID()
@Before
fun setUp() {
val request = DownloadRequest(user, file)
uuid = registry.add(request)
registry.startNext()
assertEquals(uuid, request.uuid)
assertEquals(1, registry.running.size)
resetMocks()
}
@Test
fun transfer_progress_is_updated() {
// GIVEN
// a transfer is running
// WHEN
// transfer progress is updated
val progressHalf = 50
registry.progress(uuid, progressHalf)
// THEN
// progress is updated
// update callback is invoked
val transfer = mutableListOf<Transfer>()
verify { onTransferChanged(capture(transfer)) }
assertEquals(1, transfer.size)
assertEquals(progressHalf, transfer.first().progress)
}
@Test
fun updates_for_non_running_transfers_are_ignored() {
// GIVEN
// transfer is not running
registry.complete(uuid, true)
assertEquals(0, registry.running.size)
resetMocks()
// WHEN
// progress for a non-running transfer is updated
registry.progress(uuid, PROGRESS_HALF)
// THEN
// progress update is ignored
verify(exactly = 0) { onTransferChanged(any()) }
}
@Test
fun updates_for_non_existing_transfers_are_ignored() {
// GIVEN
// some transfer is running
// WHEN
// progress is updated for non-existing transfer
val nonExistingTransferId = UUID.randomUUID()
registry.progress(nonExistingTransferId, PROGRESS_HALF)
// THEN
// progress uppdate is ignored
verify(exactly = 0) { onTransferChanged(any()) }
}
}
class Complete : Base() {
lateinit var uuid: UUID
@Before
fun setUp() {
uuid = registry.add(DownloadRequest(user, file))
registry.startNext()
registry.progress(uuid, PROGRESS_FULL)
resetMocks()
}
@Test
fun complete_successful_transfer_with_updated_file() {
// GIVEN
// a transfer is running
// WHEN
// transfer is completed
// file has been updated
val updatedFile = OCFile("/updated/file")
registry.complete(uuid, true, updatedFile)
// THEN
// transfer is completed successfully
// status carries updated file
val slot = CapturingSlot<Transfer>()
verify { onTransferChanged(capture(slot)) }
assertEquals(TransferState.COMPLETED, slot.captured.state)
assertSame(slot.captured.file, updatedFile)
}
@Test
fun complete_successful_transfer() {
// GIVEN
// a transfer is running
// WHEN
// transfer is completed
// file is not updated
registry.complete(uuid = uuid, success = true, file = null)
// THEN
// transfer is completed successfully
// status carries previous file
val slot = CapturingSlot<Transfer>()
verify { onTransferChanged(capture(slot)) }
assertEquals(TransferState.COMPLETED, slot.captured.state)
assertSame(slot.captured.file, file)
}
@Test
fun complete_failed_transfer() {
// GIVEN
// a transfer is running
// WHEN
// transfer is failed
registry.complete(uuid, false)
// THEN
// transfer is completed successfully
val slot = CapturingSlot<Transfer>()
verify { onTransferChanged(capture(slot)) }
assertEquals(TransferState.FAILED, slot.captured.state)
}
}
class GetTransfers : Base() {
val pendingTransferFile = OCFile("/pending")
val runningTransferFile = OCFile("/running")
val completedTransferFile = OCFile("/completed")
lateinit var pendingTransferId: UUID
lateinit var runningTransferId: UUID
lateinit var completedTransferId: UUID
@Before
fun setUp() {
completedTransferId = registry.add(DownloadRequest(user, completedTransferFile))
registry.startNext()
registry.complete(completedTransferId, true)
runningTransferId = registry.add(DownloadRequest(user, runningTransferFile))
registry.startNext()
pendingTransferId = registry.add(DownloadRequest(user, pendingTransferFile))
resetMocks()
assertEquals(1, registry.pending.size)
assertEquals(1, registry.running.size)
assertEquals(1, registry.completed.size)
}
@Test
fun get_by_path_searches_pending_queue() {
// GIVEN
// file transfer is pending
// WHEN
// transfer status is retrieved
val transfer = registry.getTransfer(pendingTransferFile)
// THEN
// transfer from pending queue is returned
assertNotNull(transfer)
assertEquals(pendingTransferId, transfer?.uuid)
}
@Test
fun get_by_id_searches_pending_queue() {
// GIVEN
// file transfer is pending
// WHEN
// transfer status is retrieved
val transfer = registry.getTransfer(pendingTransferId)
// THEN
// transfer from pending queue is returned
assertNotNull(transfer)
assertEquals(pendingTransferId, transfer?.uuid)
}
@Test
fun get_by_path_searches_running_queue() {
// GIVEN
// file transfer is running
// WHEN
// transfer status is retrieved
val transfer = registry.getTransfer(runningTransferFile)
// THEN
// transfer from pending queue is returned
assertNotNull(transfer)
assertEquals(runningTransferId, transfer?.uuid)
}
@Test
fun get_by_id_searches_running_queue() {
// GIVEN
// file transfer is running
// WHEN
// transfer status is retrieved
val transfer = registry.getTransfer(runningTransferId)
// THEN
// transfer from pending queue is returned
assertNotNull(transfer)
assertEquals(runningTransferId, transfer?.uuid)
}
@Test
fun get_by_path_searches_completed_queue() {
// GIVEN
// file transfer is pending
// WHEN
// transfer status is retrieved
val transfer = registry.getTransfer(completedTransferFile)
// THEN
// transfer from pending queue is returned
assertNotNull(transfer)
assertEquals(completedTransferId, transfer?.uuid)
}
@Test
fun get_by_id_searches_completed_queue() {
// GIVEN
// file transfer is pending
// WHEN
// transfer status is retrieved
val transfer = registry.getTransfer(completedTransferId)
// THEN
// transfer from pending queue is returned
assertNotNull(transfer)
assertEquals(completedTransferId, transfer?.uuid)
}
@Test
fun not_found_by_path() {
// GIVEN
// no transfer for a file
val nonExistingTransferFile = OCFile("/non-nexisting/transfer")
// WHEN
// transfer status is retrieved for a file
val transfer = registry.getTransfer(nonExistingTransferFile)
// THEN
// no transfer is found
assertNull(transfer)
}
@Test
fun not_found_by_id() {
// GIVEN
// no transfer for an id
val nonExistingId = UUID.randomUUID()
// WHEN
// transfer status is retrieved for a file
val transfer = registry.getTransfer(nonExistingId)
// THEN
// no transfer is found
assertNull(transfer)
}
}
class IsRunning : Base() {
@Test
fun no_requests() {
// WHEN
// all queues empty
assertEquals(0, registry.pending.size)
assertEquals(0, registry.running.size)
assertEquals(0, registry.completed.size)
// THEN
// not running
assertFalse(registry.isRunning)
}
@Test
fun request_pending() {
// WHEN
// request is enqueued
val request = DownloadRequest(user, OCFile("/path/alpha/1"))
registry.add(request)
assertEquals(1, registry.pending.size)
assertEquals(0, registry.running.size)
assertEquals(0, registry.completed.size)
// THEN
// is running
assertTrue(registry.isRunning)
}
@Test
fun request_running() {
// WHEN
// request is running
val request = DownloadRequest(user, OCFile("/path/alpha/1"))
registry.add(request)
registry.startNext()
assertEquals(0, registry.pending.size)
assertEquals(1, registry.running.size)
assertEquals(0, registry.completed.size)
// THEN
// is running
assertTrue(registry.isRunning)
}
@Test
fun request_completed() {
// WHEN
// request is running
val request = DownloadRequest(user, OCFile("/path/alpha/1"))
val id = registry.add(request)
registry.startNext()
registry.complete(id, true)
assertEquals(0, registry.pending.size)
assertEquals(0, registry.running.size)
assertEquals(1, registry.completed.size)
// THEN
// is not running
assertFalse(registry.isRunning)
}
}
}

View file

@ -0,0 +1,233 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.files.download
import android.content.ComponentName
import android.content.Context
import com.nextcloud.client.account.MockUser
import com.nextcloud.client.files.DownloadRequest
import com.nextcloud.client.jobs.transfer.FileTransferService
import com.nextcloud.client.jobs.transfer.Transfer
import com.nextcloud.client.jobs.transfer.TransferManager
import com.nextcloud.client.jobs.transfer.TransferManagerConnection
import com.nextcloud.client.jobs.transfer.TransferState
import com.owncloud.android.datamodel.OCFile
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class TransferManagerConnectionTest {
lateinit var connection: TransferManagerConnection
@MockK
lateinit var context: Context
@MockK
lateinit var firstDownloadListener: (Transfer) -> Unit
@MockK
lateinit var secondDownloadListener: (Transfer) -> Unit
@MockK
lateinit var firstStatusListener: (TransferManager.Status) -> Unit
@MockK
lateinit var secondStatusListener: (TransferManager.Status) -> Unit
@MockK
lateinit var binder: FileTransferService.Binder
val file get() = OCFile("/path")
val componentName = ComponentName("", FileTransferService::class.java.simpleName)
val user = MockUser()
@Before
fun setUp() {
MockKAnnotations.init(this, relaxed = true)
connection = TransferManagerConnection(context, user)
}
@Test
fun listeners_are_set_after_connection() {
// GIVEN
// not connected
// listener is added
connection.registerTransferListener(firstDownloadListener)
connection.registerTransferListener(secondDownloadListener)
// WHEN
// service is bound
connection.onServiceConnected(componentName, binder)
// THEN
// all listeners are passed to the service
val listeners = mutableListOf<(Transfer) -> Unit>()
verify { binder.registerTransferListener(capture(listeners)) }
assertEquals(listOf(firstDownloadListener, secondDownloadListener), listeners)
}
@Test
fun listeners_are_set_immediately_when_connected() {
// GIVEN
// service is bound
connection.onServiceConnected(componentName, binder)
// WHEN
// listeners are added
connection.registerTransferListener(firstDownloadListener)
// THEN
// listener is forwarded to service
verify { binder.registerTransferListener(firstDownloadListener) }
}
@Test
fun listeners_are_removed_when_unbinding() {
// GIVEN
// service is bound
// service has some listeners
connection.onServiceConnected(componentName, binder)
connection.registerTransferListener(firstDownloadListener)
connection.registerTransferListener(secondDownloadListener)
// WHEN
// service unbound
connection.unbind()
// THEN
// listeners removed from service
verify { binder.removeTransferListener(firstDownloadListener) }
verify { binder.removeTransferListener(secondDownloadListener) }
}
@Test
fun missed_updates_are_delivered_on_connection() {
// GIVEN
// not bound
// has listeners
// download is scheduled and is progressing
connection.registerTransferListener(firstDownloadListener)
connection.registerTransferListener(secondDownloadListener)
val request1 = DownloadRequest(user, file)
connection.enqueue(request1)
val download1 = Transfer(request1.uuid, TransferState.RUNNING, 50, request1.file, request1)
val request2 = DownloadRequest(user, file)
connection.enqueue(request2)
val download2 = Transfer(request2.uuid, TransferState.RUNNING, 50, request2.file, request1)
every { binder.getTransfer(request1.uuid) } returns download1
every { binder.getTransfer(request2.uuid) } returns download2
// WHEN
// service is bound
connection.onServiceConnected(componentName, binder)
// THEN
// listeners receive current download state for pending downloads
val firstListenerNotifications = mutableListOf<Transfer>()
verify { firstDownloadListener(capture(firstListenerNotifications)) }
assertEquals(listOf(download1, download2), firstListenerNotifications)
val secondListenerNotifications = mutableListOf<Transfer>()
verify { secondDownloadListener(capture(secondListenerNotifications)) }
assertEquals(listOf(download1, download2), secondListenerNotifications)
}
@Test
fun downloader_status_updates_are_delivered_on_connection() {
// GIVEN
// not bound
// has status listeners
val mockStatus: TransferManager.Status = mockk()
every { binder.status } returns mockStatus
connection.registerStatusListener(firstStatusListener)
connection.registerStatusListener(secondStatusListener)
// WHEN
// service is bound
connection.onServiceConnected(componentName, binder)
// THEN
// downloader status is delivered
verify { firstStatusListener(mockStatus) }
verify { secondStatusListener(mockStatus) }
}
@Test
fun downloader_status_not_requested_if_no_listeners() {
// GIVEN
// not bound
// no status listeners
// WHEN
// service is bound
connection.onServiceConnected(componentName, binder)
// THEN
// downloader status is not requested
verify(exactly = 0) { binder.status }
}
@Test
fun not_running_if_not_connected() {
// GIVEN
// downloader is running
// connection not bound
every { binder.isRunning } returns true
// THEN
// not running
assertFalse(connection.isRunning)
}
@Test
fun is_running_from_binder_if_connected() {
// GIVEN
// service bound
every { binder.isRunning } returns true
connection.onServiceConnected(componentName, binder)
// WHEN
// is runnign flag accessed
val isRunning = connection.isRunning
// THEN
// call delegated to binder
assertTrue(isRunning)
verify(exactly = 1) { binder.isRunning }
}
@Test
fun missed_updates_not_tracked_before_listeners_registered() {
// GIVEN
// not bound
// some downloads requested without listener
val request = DownloadRequest(user, file)
connection.enqueue(request)
val download = Transfer(request.uuid, TransferState.RUNNING, 50, request.file, request)
connection.registerTransferListener(firstDownloadListener)
every { binder.getTransfer(request.uuid) } returns download
// WHEN
// service is bound
connection.onServiceConnected(componentName, binder)
// THEN
// missed updates not redelivered
verify(exactly = 0) { firstDownloadListener(any()) }
}
}

View file

@ -0,0 +1,279 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.client.files.download
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.nextcloud.client.account.User
import com.nextcloud.client.core.ManualAsyncRunner
import com.nextcloud.client.core.OnProgressCallback
import com.nextcloud.client.files.DownloadRequest
import com.nextcloud.client.files.Request
import com.nextcloud.client.jobs.download.DownloadTask
import com.nextcloud.client.jobs.transfer.Transfer
import com.nextcloud.client.jobs.transfer.TransferManagerImpl
import com.nextcloud.client.jobs.transfer.TransferState
import com.nextcloud.client.jobs.upload.UploadTask
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.lib.common.OwnCloudClient
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Suite
import org.mockito.MockitoAnnotations
@RunWith(Suite::class)
@Suite.SuiteClasses(
TransferManagerTest.Enqueue::class,
TransferManagerTest.TransferStatusUpdates::class
)
class TransferManagerTest {
abstract class Base {
companion object {
const val MAX_TRANSFER_THREADS = 4
}
@MockK
lateinit var user: User
@MockK
lateinit var client: OwnCloudClient
@MockK
lateinit var mockDownloadTaskFactory: DownloadTask.Factory
@MockK
lateinit var mockUploadTaskFactory: UploadTask.Factory
/**
* All task mock functions created during test run are
* stored here.
*/
lateinit var downloadTaskMocks: MutableList<DownloadTask>
lateinit var runner: ManualAsyncRunner
lateinit var transferManager: TransferManagerImpl
/**
* Response value for all download tasks
*/
var downloadTaskResult: Boolean = true
/**
* Progress values posted by all download task mocks before
* returning result value
*/
var taskProgress = listOf<Int>()
@Before
fun setUpBase() {
MockKAnnotations.init(this, relaxed = true)
MockitoAnnotations.initMocks(this)
downloadTaskMocks = mutableListOf()
runner = ManualAsyncRunner()
transferManager = TransferManagerImpl(
runner = runner,
downloadTaskFactory = mockDownloadTaskFactory,
uploadTaskFactory = mockUploadTaskFactory,
threads = MAX_TRANSFER_THREADS
)
downloadTaskResult = true
every { mockDownloadTaskFactory.create() } answers { createMockTask() }
}
private fun createMockTask(): DownloadTask {
val task = mockk<DownloadTask>()
every { task.download(any(), any(), any()) } answers {
taskProgress.forEach {
arg<OnProgressCallback<Int>>(1).invoke(it)
}
val request = arg<Request>(0)
DownloadTask.Result(request.file, downloadTaskResult)
}
downloadTaskMocks.add(task)
return task
}
}
class Enqueue : Base() {
@Test
fun enqueued_download_is_started_immediately() {
// GIVEN
// downloader has no running downloads
// WHEN
// download is enqueued
val file = OCFile("/path")
val request = DownloadRequest(user, file)
transferManager.enqueue(request)
// THEN
// download is started immediately
val download = transferManager.getTransfer(request.uuid)
assertEquals(TransferState.RUNNING, download?.state)
}
@Test
fun enqueued_downloads_are_pending_if_running_queue_is_full() {
// GIVEN
// downloader is downloading max simultaneous files
for (i in 0 until MAX_TRANSFER_THREADS) {
val file = OCFile("/running/download/path/$i")
val request = DownloadRequest(user, file)
transferManager.enqueue(request)
val runningDownload = transferManager.getTransfer(request.uuid)
assertEquals(runningDownload?.state, TransferState.RUNNING)
}
// WHEN
// another download is enqueued
val file = OCFile("/path")
val request = DownloadRequest(user, file)
transferManager.enqueue(request)
// THEN
// download is pending
val download = transferManager.getTransfer(request.uuid)
assertEquals(TransferState.PENDING, download?.state)
}
}
class TransferStatusUpdates : Base() {
@get:Rule
val rule = InstantTaskExecutorRule()
val file = OCFile("/path")
@Test
fun download_task_completes() {
// GIVEN
// download is running
// download is being observed
val downloadUpdates = mutableListOf<Transfer>()
transferManager.registerTransferListener { downloadUpdates.add(it) }
transferManager.enqueue(DownloadRequest(user, file))
// WHEN
// download task finishes successfully
runner.runOne()
// THEN
// listener is notified about status change
assertEquals(TransferState.RUNNING, downloadUpdates[0].state)
assertEquals(TransferState.COMPLETED, downloadUpdates[1].state)
}
@Test
fun download_task_fails() {
// GIVEN
// download is running
// download is being observed
val downloadUpdates = mutableListOf<Transfer>()
transferManager.registerTransferListener { downloadUpdates.add(it) }
transferManager.enqueue(DownloadRequest(user, file))
// WHEN
// download task fails
downloadTaskResult = false
runner.runOne()
// THEN
// listener is notified about status change
assertEquals(TransferState.RUNNING, downloadUpdates[0].state)
assertEquals(TransferState.FAILED, downloadUpdates[1].state)
}
@Test
fun download_progress_is_updated() {
// GIVEN
// download is running
val downloadUpdates = mutableListOf<Transfer>()
transferManager.registerTransferListener { downloadUpdates.add(it) }
transferManager.enqueue(DownloadRequest(user, file))
// WHEN
// download progress updated 4 times before completion
taskProgress = listOf(25, 50, 75, 100)
runner.runOne()
// THEN
// listener receives 6 status updates
// transition to running
// 4 progress updates
// completion
assertEquals(6, downloadUpdates.size)
if (downloadUpdates.size >= 6) {
assertEquals(TransferState.RUNNING, downloadUpdates[0].state)
assertEquals(25, downloadUpdates[1].progress)
assertEquals(50, downloadUpdates[2].progress)
assertEquals(75, downloadUpdates[3].progress)
assertEquals(100, downloadUpdates[4].progress)
assertEquals(TransferState.COMPLETED, downloadUpdates[5].state)
}
}
@Test
fun download_task_is_created_only_for_running_downloads() {
// WHEN
// multiple downloads are enqueued
for (i in 0 until MAX_TRANSFER_THREADS * 2) {
transferManager.enqueue(DownloadRequest(user, file))
}
// THEN
// download task is created only for running downloads
assertEquals(MAX_TRANSFER_THREADS, downloadTaskMocks.size)
}
}
class RunningStatusUpdates : Base() {
@get:Rule
val rule = InstantTaskExecutorRule()
@Test
fun is_running_flag_on_enqueue() {
// WHEN
// download is enqueued
val file = OCFile("/path/to/file")
val request = DownloadRequest(user, file)
transferManager.enqueue(request)
// THEN
// is running changes
assertTrue(transferManager.isRunning)
}
@Test
fun is_running_flag_on_completion() {
// GIVEN
// a download is in progress
val file = OCFile("/path/to/file")
val request = DownloadRequest(user, file)
transferManager.enqueue(request)
assertTrue(transferManager.isRunning)
// WHEN
// download is processed
runner.runOne()
// THEN
// downloader is not running
assertFalse(transferManager.isRunning)
}
}
}