mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 18:39:55 +02:00
708 lines
28 KiB
Kotlin
708 lines
28 KiB
Kotlin
package com.vnidrop.app.feature
|
|
|
|
import com.vnidrop.app.DeviceInfo
|
|
import com.vnidrop.app.PlatformEnvironment
|
|
import com.vnidrop.app.core.CoreState
|
|
import com.vnidrop.app.core.PickedShareFile
|
|
import com.vnidrop.app.core.ReceiveFolder
|
|
import com.vnidrop.app.core.ReceiveFolderKind
|
|
import com.vnidrop.app.core.Share
|
|
import com.vnidrop.app.core.ShareAccessPolicy
|
|
import com.vnidrop.app.core.Transfer
|
|
import com.vnidrop.app.core.TransferDirection
|
|
import com.vnidrop.app.core.TransferStatus
|
|
import com.vnidrop.app.diagnostics.BreadcrumbBuffer
|
|
import com.vnidrop.app.diagnostics.BugReportService
|
|
import com.vnidrop.app.diagnostics.DiagnosticsTransport
|
|
import com.vnidrop.app.diagnostics.NoOpDiagnosticsTransport
|
|
import com.vnidrop.app.diagnostics.RecordingDiagnosticsTransport
|
|
import com.vnidrop.app.feature.app.AppViewModel
|
|
import com.vnidrop.app.feature.receive.ReceiveHistoryDeleteTarget
|
|
import com.vnidrop.app.feature.receive.ReceiveViewModel
|
|
import com.vnidrop.app.feature.send.SendViewModel
|
|
import com.vnidrop.app.feature.settings.SettingsSection
|
|
import com.vnidrop.app.feature.settings.SettingsViewModel
|
|
import com.vnidrop.app.notifications.NotificationPermission
|
|
import com.vnidrop.app.preferences.AppPreferences
|
|
import com.vnidrop.app.preferences.PreferencesRepository
|
|
import com.vnidrop.app.support.FakeCoreGateway
|
|
import com.vnidrop.app.support.FakeFileSystemService
|
|
import com.vnidrop.app.support.FakeFilePreviewRepository
|
|
import com.vnidrop.app.support.FakeNotificationService
|
|
import com.vnidrop.app.support.FakePreferencesRepository
|
|
import com.vnidrop.app.ui.feedback.UiMessageController
|
|
import com.vnidrop.app.ui.feedback.UiText
|
|
import com.vnidrop.app.ui.navigation.AppDestination
|
|
import com.vnidrop.app.ui.theme.ThemeMode
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
import kotlinx.coroutines.flow.first
|
|
import kotlinx.coroutines.withTimeoutOrNull
|
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
|
import kotlinx.coroutines.test.advanceUntilIdle
|
|
import kotlinx.coroutines.test.resetMain
|
|
import kotlinx.coroutines.test.runTest
|
|
import kotlinx.coroutines.test.setMain
|
|
import kotlin.test.AfterTest
|
|
import kotlin.test.Test
|
|
import kotlin.test.assertEquals
|
|
import kotlin.test.assertContentEquals
|
|
import kotlin.test.assertFalse
|
|
import kotlin.test.assertNotNull
|
|
import kotlin.test.assertTrue
|
|
import vnidrop.shared.generated.resources.Res
|
|
import vnidrop.shared.generated.resources.button_show_in_files
|
|
import vnidrop.shared.generated.resources.error_permission
|
|
import vnidrop.shared.generated.resources.receive_open_files_failed
|
|
|
|
@OptIn(ExperimentalCoroutinesApi::class)
|
|
class ViewModelsTest {
|
|
@AfterTest
|
|
fun resetDispatcher() {
|
|
Dispatchers.resetMain()
|
|
}
|
|
|
|
@Test
|
|
fun appViewModelInitializesCoreAndOwnsNavigation() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway()
|
|
val viewModel = AppViewModel(environment(), core, preferences(), UiMessageController())
|
|
advanceUntilIdle()
|
|
assertTrue(core.state.value.isInitialized)
|
|
viewModel.selectDestination(AppDestination.Settings)
|
|
assertEquals(AppDestination.Settings, viewModel.state.value.destination)
|
|
}
|
|
|
|
@Test
|
|
fun settingsEnablesNotificationsOnlyAfterPermission() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val preferences = preferences()
|
|
val notifications = FakeNotificationService(NotificationPermission.Granted)
|
|
val viewModel = settingsViewModel(preferences, notifications)
|
|
advanceUntilIdle()
|
|
viewModel.setNotificationsEnabled(true)
|
|
advanceUntilIdle()
|
|
assertTrue(preferences.mutablePreferences.value.notificationsEnabled)
|
|
viewModel.setNotificationsEnabled(false)
|
|
advanceUntilIdle()
|
|
assertFalse(preferences.mutablePreferences.value.notificationsEnabled)
|
|
assertEquals(1, notifications.cancelAllCount)
|
|
}
|
|
|
|
@Test
|
|
fun settingsUsernameDraftIsNotOverwrittenByPersistedEcho() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val preferences = preferences()
|
|
val viewModel = settingsViewModel(preferences)
|
|
advanceUntilIdle()
|
|
|
|
viewModel.setUsername("Ada ")
|
|
// Immediate local state must keep the trailing space so multi-word names can be typed.
|
|
assertEquals("Ada ", viewModel.state.value.username)
|
|
assertEquals("Receiver", preferences.mutablePreferences.value.username)
|
|
|
|
testScheduler.advanceTimeBy(400)
|
|
advanceUntilIdle()
|
|
assertEquals("Ada", preferences.mutablePreferences.value.username)
|
|
assertEquals("Ada ", viewModel.state.value.username)
|
|
|
|
preferences.setThemeMode(ThemeMode.Dark)
|
|
advanceUntilIdle()
|
|
assertEquals("Ada ", viewModel.state.value.username)
|
|
}
|
|
|
|
@Test
|
|
fun settingsKeepsNotificationsDisabledWhenPermissionIsDenied() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val preferences = preferences()
|
|
val viewModel = settingsViewModel(preferences, FakeNotificationService(NotificationPermission.Denied))
|
|
advanceUntilIdle()
|
|
viewModel.setNotificationsEnabled(true)
|
|
advanceUntilIdle()
|
|
assertFalse(preferences.mutablePreferences.value.notificationsEnabled)
|
|
}
|
|
|
|
@Test
|
|
fun settingsCompletesNotificationOptInAfterSystemSettingsGrant() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val preferences = preferences()
|
|
val notifications = FakeNotificationService(NotificationPermission.Denied)
|
|
val viewModel = settingsViewModel(preferences, notifications)
|
|
advanceUntilIdle()
|
|
|
|
viewModel.openNotificationSettings()
|
|
advanceUntilIdle()
|
|
assertEquals(1, notifications.openSettingsCount)
|
|
|
|
notifications.mutablePermission.value = NotificationPermission.Granted
|
|
viewModel.refreshNotificationPermission()
|
|
advanceUntilIdle()
|
|
|
|
assertTrue(preferences.mutablePreferences.value.notificationsEnabled)
|
|
assertEquals(NotificationPermission.Granted, viewModel.state.value.notificationPermission)
|
|
}
|
|
|
|
@Test
|
|
fun settingsReportsUnsupportedNotificationPlatforms() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val preferences = preferences()
|
|
val viewModel = settingsViewModel(preferences, FakeNotificationService(NotificationPermission.Unsupported))
|
|
advanceUntilIdle()
|
|
viewModel.setNotificationsEnabled(true)
|
|
advanceUntilIdle()
|
|
assertFalse(preferences.mutablePreferences.value.notificationsEnabled)
|
|
assertEquals(NotificationPermission.Unsupported, viewModel.state.value.notificationPermission)
|
|
}
|
|
|
|
@Test
|
|
fun settingsTogglesDiagnosticsPreference() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val preferences = preferences()
|
|
val viewModel = settingsViewModel(preferences, diagnosticsIncluded = true)
|
|
advanceUntilIdle()
|
|
assertFalse(viewModel.state.value.diagnosticsEnabled)
|
|
viewModel.setDiagnosticsEnabled(true)
|
|
advanceUntilIdle()
|
|
assertTrue(preferences.mutablePreferences.value.diagnosticsEnabled)
|
|
assertTrue(viewModel.state.value.diagnosticsEnabled)
|
|
}
|
|
|
|
@Test
|
|
fun settingsIgnoresDiagnosticsOptInWhenExcluded() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val preferences = preferences()
|
|
val viewModel = settingsViewModel(preferences)
|
|
advanceUntilIdle()
|
|
|
|
viewModel.setDiagnosticsEnabled(true)
|
|
advanceUntilIdle()
|
|
|
|
assertFalse(preferences.mutablePreferences.value.diagnosticsEnabled)
|
|
assertFalse(viewModel.state.value.diagnosticsEnabled)
|
|
}
|
|
|
|
@Test
|
|
fun settingsSubmitsBugReportAndClearsForm() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val preferences = preferences()
|
|
val viewModel = settingsViewModel(preferences)
|
|
advanceUntilIdle()
|
|
viewModel.setBugWhatHappened("Transfer stuck")
|
|
viewModel.setBugExpected("It should finish")
|
|
viewModel.submitBugReport()
|
|
advanceUntilIdle()
|
|
assertEquals("", viewModel.state.value.bugWhatHappened)
|
|
assertEquals("", viewModel.state.value.bugExpected)
|
|
}
|
|
|
|
@Test
|
|
fun settingsKeepsBugReportWhenDeliveryIsUnavailable() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val viewModel = settingsViewModel(transport = NoOpDiagnosticsTransport())
|
|
advanceUntilIdle()
|
|
viewModel.selectSection(SettingsSection.BugReport)
|
|
viewModel.setBugWhatHappened("Transfer stuck")
|
|
viewModel.setBugExpected("It should finish")
|
|
|
|
viewModel.submitBugReport()
|
|
advanceUntilIdle()
|
|
|
|
assertEquals("Transfer stuck", viewModel.state.value.bugWhatHappened)
|
|
assertEquals("It should finish", viewModel.state.value.bugExpected)
|
|
assertEquals(SettingsSection.BugReport, viewModel.state.value.selectedSection)
|
|
assertFalse(viewModel.state.value.isSubmittingBugReport)
|
|
}
|
|
|
|
@Test
|
|
fun settingsUsesDefaultReceiveFolderWhenPlatformDoesNotSupportCustomFolders() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val appDocuments = ReceiveFolder(ReceiveFolderKind.FileSystemPath, "/app/Documents", "Documents")
|
|
val externalFolder = ReceiveFolder(ReceiveFolderKind.AndroidTreeUri, "content://external", "External")
|
|
val preferences = preferences().apply {
|
|
mutablePreferences.value = mutablePreferences.value.copy(receiveFolder = externalFolder)
|
|
}
|
|
val fileSystem = FakeFileSystemService(appDocuments).apply { supportsCustomFolders = false }
|
|
val viewModel = settingsViewModel(preferences = preferences, fileSystem = fileSystem)
|
|
|
|
advanceUntilIdle()
|
|
|
|
assertFalse(viewModel.state.value.supportsCustomReceiveFolders)
|
|
assertEquals(appDocuments, viewModel.state.value.receiveFolder)
|
|
assertEquals(com.vnidrop.app.core.FolderAccessStatus.Writable, viewModel.state.value.folderAccessStatus)
|
|
viewModel.chooseReceiveFolder()
|
|
assertEquals(null, withTimeoutOrNull(1) { viewModel.effectFlow.first() })
|
|
}
|
|
|
|
@Test
|
|
fun sendViewModelOwnsSelectedFileState() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val fileSystem = FakeFileSystemService(folder)
|
|
val viewModel = SendViewModel(FakeCoreGateway(), fileSystem, preferences(), FakeFilePreviewRepository(), UiMessageController())
|
|
viewModel.openComposer()
|
|
val selected = PickedShareFile("/tmp/photo.jpg", "photo.jpg", 42UL, isTemporaryCopy = true)
|
|
viewModel.onFilesPicked(listOf(selected))
|
|
assertEquals("photo.jpg", viewModel.state.value.transferName)
|
|
assertEquals(42UL, viewModel.state.value.selectedFile?.sizeBytes)
|
|
viewModel.clearSelectedSource()
|
|
advanceUntilIdle()
|
|
assertEquals(null, viewModel.state.value.selectedFile)
|
|
assertEquals(listOf(selected), fileSystem.discardedPickedFiles)
|
|
}
|
|
|
|
@Test
|
|
fun sendComposerClosesAfterSuccessfulAtomicShareCreation() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway().apply {
|
|
mutableState.value = CoreState(isInitialized = true)
|
|
shareResult = Result.success(Share(7UL, "ticket", "photo.jpg", "hash", 1UL, 42UL))
|
|
}
|
|
val previews = FakeFilePreviewRepository()
|
|
val fileSystem = FakeFileSystemService(folder)
|
|
val viewModel = SendViewModel(core, fileSystem, preferences(), previews, UiMessageController())
|
|
advanceUntilIdle()
|
|
viewModel.openComposer()
|
|
val thumbnail = ByteArray(12).also {
|
|
it[0] = 0x89.toByte(); it[1] = 'P'.code.toByte(); it[2] = 'N'.code.toByte(); it[3] = 'G'.code.toByte()
|
|
}
|
|
val selected = PickedShareFile("/tmp/photo.jpg", "photo.jpg", 42UL, thumbnail, isTemporaryCopy = true)
|
|
viewModel.onFilesPicked(listOf(selected))
|
|
viewModel.setAccessPolicy(ShareAccessPolicy.AnyoneWithTransfer)
|
|
viewModel.createShare()
|
|
advanceUntilIdle()
|
|
|
|
assertFalse(viewModel.state.value.isComposerOpen)
|
|
assertEquals(null, viewModel.state.value.selectedFile)
|
|
assertEquals(ShareAccessPolicy.AnyoneWithTransfer, core.lastShareAccessPolicy)
|
|
assertEquals(7UL, core.state.value.transfers.first().transferId)
|
|
assertContentEquals(thumbnail, previews.previews.value.getValue(7UL))
|
|
assertEquals(listOf(selected), fileSystem.discardedPickedFiles)
|
|
}
|
|
|
|
@Test
|
|
fun sendComposerStaysOpenWhenShareCreationFails() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway().apply { mutableState.value = CoreState(isInitialized = true) }
|
|
val viewModel = SendViewModel(core, FakeFileSystemService(folder), preferences(), FakeFilePreviewRepository(), UiMessageController())
|
|
advanceUntilIdle()
|
|
viewModel.openComposer()
|
|
viewModel.onFilesPicked(listOf(com.vnidrop.app.core.PickedShareFile("/tmp/photo.jpg", "photo.jpg", 42UL)))
|
|
viewModel.createShare()
|
|
advanceUntilIdle()
|
|
|
|
assertTrue(viewModel.state.value.isComposerOpen)
|
|
assertEquals("photo.jpg", viewModel.state.value.selectedFile?.displayName)
|
|
assertFalse(viewModel.state.value.isSharing)
|
|
}
|
|
|
|
@Test
|
|
fun sendViewModelSupportsMultipleFilesAndDefaultName() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway().apply {
|
|
mutableState.value = CoreState(isInitialized = true)
|
|
shareResult = Result.success(Share(9UL, "ticket", "2 files", "hash", 2UL, 84UL))
|
|
}
|
|
val viewModel = SendViewModel(core, FakeFileSystemService(folder), preferences(), FakeFilePreviewRepository(), UiMessageController())
|
|
advanceUntilIdle()
|
|
viewModel.onFilesPicked(
|
|
listOf(
|
|
com.vnidrop.app.core.PickedShareFile("/tmp/a.jpg", "a.jpg", 40UL),
|
|
com.vnidrop.app.core.PickedShareFile("/tmp/b.jpg", "b.jpg", 44UL),
|
|
),
|
|
)
|
|
assertEquals("2 files", viewModel.state.value.transferName)
|
|
assertEquals(2, viewModel.state.value.selectedFiles.size)
|
|
viewModel.createShare()
|
|
advanceUntilIdle()
|
|
assertEquals(2, core.lastShareSourceCount)
|
|
assertTrue(viewModel.state.value.selectedFiles.isEmpty())
|
|
}
|
|
|
|
@Test
|
|
fun sendViewModelNamesFolderSelectionAfterFolderDisplayName() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val viewModel = SendViewModel(FakeCoreGateway(), FakeFileSystemService(folder), preferences(), FakeFilePreviewRepository(), UiMessageController())
|
|
advanceUntilIdle()
|
|
viewModel.onFilesPicked(
|
|
listOf(
|
|
com.vnidrop.app.core.PickedShareFile(
|
|
value = "/tmp/photos",
|
|
displayName = "photos",
|
|
isDirectory = true,
|
|
),
|
|
),
|
|
)
|
|
assertEquals("photos", viewModel.state.value.transferName)
|
|
assertTrue(viewModel.state.value.selectedFiles.single().isDirectory)
|
|
}
|
|
|
|
@Test
|
|
fun sendDeletionRemovesCoreTransferAndOwnedPreview() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway().apply {
|
|
mutableState.value = CoreState(isInitialized = true, transfers = listOf(
|
|
com.vnidrop.app.core.Transfer(
|
|
localId = "send-7", transferId = 7UL,
|
|
direction = com.vnidrop.app.core.TransferDirection.Send,
|
|
status = com.vnidrop.app.core.TransferStatus.Sharing,
|
|
peerId = null, transferName = "Photo", contentHash = "hash",
|
|
fileCount = 1UL, totalSize = 42UL, ticket = "ticket",
|
|
accessPolicy = ShareAccessPolicy.RequireApproval, createdAt = 1, updatedAt = 1,
|
|
),
|
|
))
|
|
}
|
|
val previews = FakeFilePreviewRepository()
|
|
previews.save(7UL, byteArrayOf(1, 2, 3))
|
|
val viewModel = SendViewModel(core, FakeFileSystemService(folder), preferences(), previews, UiMessageController())
|
|
advanceUntilIdle()
|
|
viewModel.openTransfer(7UL)
|
|
viewModel.requestDeleteTransfer()
|
|
viewModel.confirmDeleteTransfer()
|
|
advanceUntilIdle()
|
|
|
|
assertEquals(listOf(7UL), core.deletedTransfers)
|
|
assertFalse(7UL in previews.previews.value)
|
|
assertEquals(null, viewModel.state.value.selectedTransferId)
|
|
assertFalse(viewModel.state.value.isDeleteConfirmationOpen)
|
|
}
|
|
|
|
@Test
|
|
fun receiveViewModelBuildsStateFromPreferences() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway().apply {
|
|
mutableState.value = mutableState.value.copy(isInitialized = true)
|
|
inspectionResult = Result.success(sampleTicketInspection())
|
|
}
|
|
val viewModel = ReceiveViewModel(core, FakeFileSystemService(folder), preferences(), UiMessageController())
|
|
advanceUntilIdle()
|
|
viewModel.onInvitationResult(com.vnidrop.app.feature.receive.ReceiveMethod.InvitationFile, Result.success("ticket"))
|
|
advanceUntilIdle()
|
|
assertTrue(viewModel.state.value.canReceive(coreInitialized = true))
|
|
assertEquals("Receiver", viewModel.state.value.receiverName)
|
|
}
|
|
|
|
@Test
|
|
fun receiveViewModelDeletesOneTerminalHistoryItem() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway().apply {
|
|
mutableState.value = CoreState(isInitialized = true, transfers = listOf(receivedTransfer(21UL, TransferStatus.Done)))
|
|
}
|
|
val viewModel = ReceiveViewModel(core, FakeFileSystemService(folder), preferences(), UiMessageController())
|
|
advanceUntilIdle()
|
|
|
|
viewModel.requestDeleteHistoryItem(21UL)
|
|
viewModel.confirmHistoryDelete()
|
|
advanceUntilIdle()
|
|
|
|
assertEquals(listOf(21UL), core.deletedTransfers)
|
|
assertEquals(null, viewModel.state.value.historyDeleteTarget)
|
|
}
|
|
|
|
@Test
|
|
fun receiveViewModelClearHistoryUsesAtomicCoreOperationAndKeepsActiveReceive() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway().apply {
|
|
clearReceiveHistoryResult = Result.success(2UL)
|
|
mutableState.value = CoreState(
|
|
isInitialized = true,
|
|
transfers = listOf(
|
|
receivedTransfer(21UL, TransferStatus.Done),
|
|
receivedTransfer(22UL, TransferStatus.Failed),
|
|
receivedTransfer(23UL, TransferStatus.Receiving),
|
|
),
|
|
)
|
|
}
|
|
val viewModel = ReceiveViewModel(core, FakeFileSystemService(folder), preferences(), UiMessageController())
|
|
advanceUntilIdle()
|
|
|
|
viewModel.requestClearHistory()
|
|
assertEquals(ReceiveHistoryDeleteTarget.All, viewModel.state.value.historyDeleteTarget)
|
|
viewModel.confirmHistoryDelete()
|
|
advanceUntilIdle()
|
|
|
|
assertEquals(1, core.clearReceiveHistoryCount)
|
|
assertEquals(listOf(23UL), core.state.value.transfers.map(Transfer::transferId))
|
|
}
|
|
|
|
@Test
|
|
fun receiveViewModelKeepsDeleteConfirmationAfterFailure() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway().apply {
|
|
deleteResult = Result.failure(IllegalStateException("database busy"))
|
|
mutableState.value = CoreState(isInitialized = true, transfers = listOf(receivedTransfer(21UL, TransferStatus.Done)))
|
|
}
|
|
val viewModel = ReceiveViewModel(core, FakeFileSystemService(folder), preferences(), UiMessageController())
|
|
advanceUntilIdle()
|
|
|
|
viewModel.requestDeleteHistoryItem(21UL)
|
|
viewModel.confirmHistoryDelete()
|
|
advanceUntilIdle()
|
|
|
|
assertEquals(ReceiveHistoryDeleteTarget.Transfer(21UL), viewModel.state.value.historyDeleteTarget)
|
|
assertFalse(viewModel.state.value.isDeletingHistory)
|
|
}
|
|
|
|
@Test
|
|
fun receiveViewModelCompletesSuccessfulReceiveAndResetsAcquisition() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway().apply {
|
|
mutableState.value = mutableState.value.copy(isInitialized = true)
|
|
inspectionResult = Result.success(sampleTicketInspection())
|
|
}
|
|
val messages = UiMessageController()
|
|
val viewModel = ReceiveViewModel(core, FakeFileSystemService(folder), preferences(), messages)
|
|
advanceUntilIdle()
|
|
viewModel.onInvitationResult(com.vnidrop.app.feature.receive.ReceiveMethod.InvitationFile, Result.success("ticket-abc"))
|
|
advanceUntilIdle()
|
|
|
|
viewModel.receive()
|
|
advanceUntilIdle()
|
|
|
|
assertEquals(1, core.receiveCount)
|
|
assertEquals("ticket-abc", core.lastReceiveTicket)
|
|
assertEquals("Receiver", core.lastReceiveReceiverName)
|
|
assertFalse(viewModel.state.value.isAcquisitionOpen)
|
|
assertEquals("", viewModel.state.value.ticket)
|
|
assertFalse(viewModel.state.value.isReceiving)
|
|
val completed = messages.messages.first()
|
|
assertEquals(null, completed.actionLabel)
|
|
assertEquals(null, completed.onAction)
|
|
}
|
|
|
|
@Test
|
|
fun receiveViewModelOffersCompletedFolderInFilesWhenPlatformSupportsIt() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway().apply {
|
|
mutableState.value = mutableState.value.copy(isInitialized = true)
|
|
inspectionResult = Result.success(sampleTicketInspection())
|
|
}
|
|
val fileSystem = FakeFileSystemService(folder).apply { canRevealFolder = true }
|
|
val messages = UiMessageController()
|
|
val viewModel = ReceiveViewModel(core, fileSystem, preferences(), messages)
|
|
advanceUntilIdle()
|
|
viewModel.onInvitationResult(com.vnidrop.app.feature.receive.ReceiveMethod.InvitationFile, Result.success("ticket"))
|
|
advanceUntilIdle()
|
|
|
|
viewModel.receive()
|
|
advanceUntilIdle()
|
|
val completed = messages.messages.first()
|
|
|
|
assertEquals(UiText.Resource(Res.string.button_show_in_files), completed.actionLabel)
|
|
assertNotNull(completed.onAction).invoke()
|
|
advanceUntilIdle()
|
|
assertEquals(listOf(folder), fileSystem.revealedFolders)
|
|
}
|
|
|
|
@Test
|
|
fun receiveViewModelUsesPlatformEffectiveFolderInsteadOfPersistedExternalFolder() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val internalFolder = ReceiveFolder(ReceiveFolderKind.FileSystemPath, "/app/Documents", "Documents")
|
|
val fileSystem = FakeFileSystemService(folder).apply { effectiveFolder = internalFolder }
|
|
val viewModel = ReceiveViewModel(FakeCoreGateway(), fileSystem, preferences(), UiMessageController())
|
|
|
|
advanceUntilIdle()
|
|
|
|
assertEquals(internalFolder, viewModel.state.value.receiveFolder)
|
|
}
|
|
|
|
@Test
|
|
fun receiveViewModelReportsWhenCompletedFolderCannotOpen() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway().apply {
|
|
mutableState.value = mutableState.value.copy(isInitialized = true)
|
|
inspectionResult = Result.success(sampleTicketInspection())
|
|
}
|
|
val fileSystem = FakeFileSystemService(folder).apply {
|
|
canRevealFolder = true
|
|
revealFolderResult = Result.failure(IllegalStateException("Files unavailable"))
|
|
}
|
|
val messages = UiMessageController()
|
|
val viewModel = ReceiveViewModel(core, fileSystem, preferences(), messages)
|
|
advanceUntilIdle()
|
|
viewModel.onInvitationResult(com.vnidrop.app.feature.receive.ReceiveMethod.InvitationFile, Result.success("ticket"))
|
|
advanceUntilIdle()
|
|
viewModel.receive()
|
|
advanceUntilIdle()
|
|
|
|
val completed = messages.messages.first()
|
|
assertNotNull(completed.onAction).invoke()
|
|
advanceUntilIdle()
|
|
|
|
assertEquals(
|
|
UiText.Resource(Res.string.receive_open_files_failed),
|
|
messages.messages.first().text,
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun receiveViewModelKeepsReviewStateWhenReceiveFails() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway().apply {
|
|
mutableState.value = mutableState.value.copy(isInitialized = true)
|
|
inspectionResult = Result.success(sampleTicketInspection())
|
|
receiveResult = Result.failure(IllegalStateException("sender refused"))
|
|
}
|
|
val viewModel = ReceiveViewModel(core, FakeFileSystemService(folder), preferences(), UiMessageController())
|
|
advanceUntilIdle()
|
|
viewModel.onInvitationResult(com.vnidrop.app.feature.receive.ReceiveMethod.QrCode, Result.success("ticket-xyz"))
|
|
advanceUntilIdle()
|
|
|
|
viewModel.receive()
|
|
advanceUntilIdle()
|
|
|
|
assertEquals(1, core.receiveCount)
|
|
assertTrue(viewModel.state.value.isAcquisitionOpen)
|
|
assertEquals("ticket-xyz", viewModel.state.value.ticket)
|
|
assertFalse(viewModel.state.value.isReceiving)
|
|
assertTrue(viewModel.state.value.inspection != null)
|
|
assertEquals(
|
|
UiText.Resource(Res.string.error_permission),
|
|
viewModel.state.value.lastReceiveError,
|
|
)
|
|
}
|
|
|
|
@Test
|
|
fun receiveViewModelCancelUsesActiveTransferId() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway().apply {
|
|
mutableState.value = CoreState(
|
|
isInitialized = true,
|
|
transfers = listOf(receivedTransfer(33UL, TransferStatus.Receiving)),
|
|
)
|
|
}
|
|
val viewModel = ReceiveViewModel(core, FakeFileSystemService(folder), preferences(), UiMessageController())
|
|
advanceUntilIdle()
|
|
viewModel.cancelActiveReceive()
|
|
advanceUntilIdle()
|
|
assertEquals(listOf(33UL), core.cancelledTransfers)
|
|
}
|
|
|
|
@Test
|
|
fun receiveViewModelClearsTicketWhenInspectionFailsButKeepsAcquisitionOpen() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway().apply {
|
|
mutableState.value = mutableState.value.copy(isInitialized = true)
|
|
inspectionResult = Result.failure(IllegalArgumentException("invalid ticket"))
|
|
}
|
|
val viewModel = ReceiveViewModel(core, FakeFileSystemService(folder), preferences(), UiMessageController())
|
|
advanceUntilIdle()
|
|
|
|
viewModel.onInvitationResult(com.vnidrop.app.feature.receive.ReceiveMethod.InvitationFile, Result.success("bad-ticket"))
|
|
advanceUntilIdle()
|
|
|
|
assertTrue(viewModel.state.value.isAcquisitionOpen)
|
|
assertEquals("", viewModel.state.value.ticket)
|
|
assertEquals(null, viewModel.state.value.inspection)
|
|
assertFalse(viewModel.state.value.isInspecting)
|
|
}
|
|
|
|
@Test
|
|
fun receiveViewModelIgnoresDeleteForActiveReceive() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway().apply {
|
|
mutableState.value = CoreState(
|
|
isInitialized = true,
|
|
transfers = listOf(receivedTransfer(21UL, TransferStatus.Receiving)),
|
|
)
|
|
}
|
|
val viewModel = ReceiveViewModel(core, FakeFileSystemService(folder), preferences(), UiMessageController())
|
|
advanceUntilIdle()
|
|
|
|
viewModel.requestDeleteHistoryItem(21UL)
|
|
assertEquals(null, viewModel.state.value.historyDeleteTarget)
|
|
}
|
|
|
|
@Test
|
|
fun receiveViewModelDismissResetsIdleAcquisitionButNotWhileReceiving() = runTest {
|
|
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
|
|
val core = FakeCoreGateway().apply {
|
|
mutableState.value = mutableState.value.copy(isInitialized = true)
|
|
inspectionResult = Result.success(sampleTicketInspection())
|
|
// Keep receive suspended so dismiss can be asserted mid-transfer.
|
|
receiveResult = Result.success(Unit)
|
|
receiveSuspend = true
|
|
}
|
|
val viewModel = ReceiveViewModel(core, FakeFileSystemService(folder), preferences(), UiMessageController())
|
|
advanceUntilIdle()
|
|
|
|
viewModel.openAcquisition()
|
|
viewModel.dismissAcquisition()
|
|
assertFalse(viewModel.state.value.isAcquisitionOpen)
|
|
|
|
viewModel.onInvitationResult(com.vnidrop.app.feature.receive.ReceiveMethod.InvitationFile, Result.success("ticket"))
|
|
advanceUntilIdle()
|
|
viewModel.receive()
|
|
// Start receive but do not finish the suspended core call yet.
|
|
testScheduler.runCurrent()
|
|
assertTrue(viewModel.state.value.isReceiving)
|
|
viewModel.dismissAcquisition()
|
|
assertTrue(viewModel.state.value.isAcquisitionOpen)
|
|
assertEquals("ticket", viewModel.state.value.ticket)
|
|
|
|
core.completeSuspendedReceive()
|
|
advanceUntilIdle()
|
|
assertFalse(viewModel.state.value.isAcquisitionOpen)
|
|
}
|
|
|
|
private fun preferences() = FakePreferencesRepository(
|
|
AppPreferences(
|
|
username = "Receiver",
|
|
receiveFolder = folder,
|
|
themeMode = ThemeMode.System,
|
|
notificationsEnabled = false,
|
|
diagnosticsEnabled = false,
|
|
diagnosticsInstallId = "test-install",
|
|
),
|
|
)
|
|
|
|
private fun environment() = PlatformEnvironment("Test", "1.0", "/tmp/vnidrop")
|
|
|
|
private fun settingsViewModel(
|
|
preferences: PreferencesRepository = preferences(),
|
|
notifications: FakeNotificationService = FakeNotificationService(),
|
|
transport: DiagnosticsTransport = RecordingDiagnosticsTransport(),
|
|
fileSystem: FakeFileSystemService = FakeFileSystemService(folder),
|
|
diagnosticsIncluded: Boolean = false,
|
|
) = SettingsViewModel(
|
|
environment(),
|
|
{ DeviceInfo("Device", "Model", "OS", "Wi-Fi", "80%") },
|
|
fileSystem,
|
|
preferences,
|
|
notifications,
|
|
UiMessageController(),
|
|
BugReportService(
|
|
preferencesRepository = preferences,
|
|
transport = transport,
|
|
breadcrumbs = BreadcrumbBuffer(),
|
|
appVersion = "1.0",
|
|
platform = "Test",
|
|
logReader = { "sample log line" },
|
|
),
|
|
diagnosticsIncluded = diagnosticsIncluded,
|
|
)
|
|
|
|
private fun receivedTransfer(id: ULong, status: TransferStatus) = Transfer(
|
|
localId = "receive-$id",
|
|
transferId = id,
|
|
direction = TransferDirection.Receive,
|
|
status = status,
|
|
peerId = null,
|
|
transferName = "Received $id",
|
|
contentHash = "hash-$id",
|
|
fileCount = 1UL,
|
|
totalSize = 42UL,
|
|
ticket = null,
|
|
accessPolicy = ShareAccessPolicy.RequireApproval,
|
|
createdAt = 1L,
|
|
updatedAt = 1L,
|
|
)
|
|
|
|
private fun sampleTicketInspection() = com.vnidrop.app.core.TicketInspectionModel(
|
|
kind = "vnidrop",
|
|
metadata = com.vnidrop.app.core.TransferMetadataModel(1UL, "Photo", null, "hash", 1UL, 42UL),
|
|
)
|
|
|
|
private companion object {
|
|
val folder = ReceiveFolder(ReceiveFolderKind.FileSystemPath, "/tmp", "Downloads")
|
|
}
|
|
}
|