Add actions to copy and download raw data

ET-5543
ET-5545
This commit is contained in:
Stefanija Boshkovska
2025-12-23 16:58:22 +01:00
committed by Niccolò Forlini
parent 1322343b9c
commit cd8d33f611
13 changed files with 462 additions and 67 deletions
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2025 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.mailcommon.data.file
import java.io.IOException
import android.content.ContentValues
import android.content.Context
import android.os.Environment
import android.provider.MediaStore
import arrow.core.Either
import arrow.core.raise.either
import ch.protonmail.android.mailcommon.domain.model.DataError
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class ExternalFileStorage @Inject constructor(
@ApplicationContext private val context: Context
) {
suspend fun saveDataToDownloads(
fileName: String,
mimeType: String,
data: String
): Either<DataError, Unit> = either {
withContext(Dispatchers.IO) {
val values = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, fileName)
put(MediaStore.Downloads.MIME_TYPE, mimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
val uri = context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
?: raise(DataError.Local.FailedToStoreFile)
try {
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.write(data.toByteArray())
}
} catch (_: IOException) {
raise(DataError.Local.FailedToStoreFile)
}
}
}
}
@@ -19,7 +19,9 @@
package ch.protonmail.android.maildetail.dagger
import ch.protonmail.android.maildetail.data.repository.InMemoryConversationStateRepositoryImpl
import ch.protonmail.android.maildetail.data.repository.RawMessageDataRepositoryImpl
import ch.protonmail.android.maildetail.domain.repository.InMemoryConversationStateRepository
import ch.protonmail.android.maildetail.domain.repository.RawMessageDataRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@@ -34,4 +36,7 @@ internal interface ViewModelBindings {
implementation: InMemoryConversationStateRepositoryImpl
): InMemoryConversationStateRepository
@Binds
fun bindRawMessageDataRepository(implementation: RawMessageDataRepositoryImpl): RawMessageDataRepository
}
+2
View File
@@ -46,6 +46,8 @@ android {
dependencies {
implementation(libs.bundles.module.data)
implementation(project(":mail-common:data"))
implementation(project(":mail-common:domain"))
implementation(project(":mail-detail:domain"))
implementation(project(":mail-message:domain"))
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.maildetail.data.repository
import arrow.core.Either
import ch.protonmail.android.mailcommon.data.file.ExternalFileStorage
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.maildetail.domain.repository.RawMessageDataRepository
import javax.inject.Inject
class RawMessageDataRepositoryImpl @Inject constructor(
private val externalFileStorage: ExternalFileStorage
) : RawMessageDataRepository {
override suspend fun downloadRawData(fileName: String, data: String): Either<DataError, Unit> =
externalFileStorage.saveDataToDownloads(fileName, "text/plain", data)
}
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2025 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.maildetail.data.repository
import arrow.core.right
import ch.protonmail.android.mailcommon.data.file.ExternalFileStorage
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
class RawMessageDataRepositoryImplTest {
private val externalFileStorage = mockk<ExternalFileStorage>()
private val rawMessageDataRepository = RawMessageDataRepositoryImpl(externalFileStorage)
@Test
fun `should call external file storage method when download raw data is called`() = runTest {
// Given
val fileName = "headers"
val data = "raw headers"
coEvery { externalFileStorage.saveDataToDownloads(fileName, "text/plain", data) } returns Unit.right()
// When
val actual = rawMessageDataRepository.downloadRawData(fileName, data)
// Then
coVerify { externalFileStorage.saveDataToDownloads(fileName, "text/plain", data) }
assertEquals(Unit.right(), actual)
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2025 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.maildetail.domain.repository
import arrow.core.Either
import ch.protonmail.android.mailcommon.domain.model.DataError
interface RawMessageDataRepository {
suspend fun downloadRawData(fileName: String, data: String): Either<DataError, Unit>
}
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2025 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.maildetail.domain.usecase
import ch.protonmail.android.maildetail.domain.repository.RawMessageDataRepository
import javax.inject.Inject
class DownloadRawMessageData @Inject constructor(
private val rawMessageDataRepository: RawMessageDataRepository
) {
suspend operator fun invoke(fileName: String, data: String) =
rawMessageDataRepository.downloadRawData(fileName, data)
}
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2025 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/
package ch.protonmail.android.maildetail.domain.usecase
import arrow.core.right
import ch.protonmail.android.maildetail.domain.repository.RawMessageDataRepository
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
class DownloadRawMessageDataTest {
private val rawMessageDataRepository = mockk<RawMessageDataRepository>()
private val downloadRawMessageData = DownloadRawMessageData(rawMessageDataRepository)
@Test
fun `should call repository method when use case is called`() = runTest {
// Given
val fileName = "headers"
val data = "raw headers"
coEvery { rawMessageDataRepository.downloadRawData(fileName, data) } returns Unit.right()
// When
val actual = downloadRawMessageData(fileName, data)
// Then
coVerify { rawMessageDataRepository.downloadRawData(fileName, data) }
assertEquals(Unit.right(), actual)
}
}
@@ -18,12 +18,18 @@
package ch.protonmail.android.maildetail.presentation.model
import ch.protonmail.android.mailcommon.presentation.Effect
sealed class RawMessageDataState {
abstract val type: RawMessageDataType
data class Loading(override val type: RawMessageDataType) : RawMessageDataState()
data class Error(override val type: RawMessageDataType) : RawMessageDataState()
data class Data(override val type: RawMessageDataType, val data: String) : RawMessageDataState()
data class Data(
override val type: RawMessageDataType,
val data: String,
val toast: Effect<Int>
) : RawMessageDataState()
}
enum class RawMessageDataType { Headers, HTML }
@@ -18,6 +18,9 @@
package ch.protonmail.android.maildetail.presentation.ui
import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -36,12 +39,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import ch.protonmail.android.design.compose.component.ProtonCenteredProgress
import ch.protonmail.android.design.compose.component.ProtonErrorMessage
import ch.protonmail.android.design.compose.component.appbar.ProtonTopAppBar
@@ -49,7 +55,10 @@ import ch.protonmail.android.design.compose.theme.ProtonDimens
import ch.protonmail.android.design.compose.theme.ProtonTheme
import ch.protonmail.android.design.compose.theme.bodyLargeNorm
import ch.protonmail.android.design.compose.theme.bodyMediumNorm
import ch.protonmail.android.mailcommon.presentation.ConsumableLaunchedEffect
import ch.protonmail.android.mailcommon.presentation.Effect
import ch.protonmail.android.mailcommon.presentation.NO_CONTENT_DESCRIPTION
import ch.protonmail.android.mailcommon.presentation.extension.copyTextToClipboard
import ch.protonmail.android.maildetail.presentation.R
import ch.protonmail.android.maildetail.presentation.model.RawMessageDataState
import ch.protonmail.android.maildetail.presentation.model.RawMessageDataType
@@ -57,13 +66,27 @@ import ch.protonmail.android.maildetail.presentation.viewmodel.RawMessageDataVie
@Composable
fun RawMessageDataScreen(onBackClick: () -> Unit, viewModel: RawMessageDataViewModel = hiltViewModel()) {
RawMessageDataScreen(viewModel.state.collectAsStateWithLifecycle().value, onBackClick)
RawMessageDataScreen(
viewModel.state.collectAsStateWithLifecycle().value,
onBackClick
) { type, data -> viewModel.downloadData(type, data) }
}
@Composable
fun RawMessageDataScreen(state: RawMessageDataState, onBackClick: () -> Unit) {
fun RawMessageDataScreen(
state: RawMessageDataState,
onBackClick: () -> Unit,
onDownloadData: (RawMessageDataType, String) -> Unit
) {
val context = LocalContext.current
val isDropDownExpanded = remember { mutableStateOf(false) }
if (state is RawMessageDataState.Data) {
ConsumableLaunchedEffect(state.toast) {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
}
}
Scaffold(
containerColor = ProtonTheme.colors.backgroundInvertedNorm,
topBar = {
@@ -92,68 +115,81 @@ fun RawMessageDataScreen(state: RawMessageDataState, onBackClick: () -> Unit) {
}
},
actions = {
IconButton(
onClick = { isDropDownExpanded.value = true }
) {
Icon(
tint = ProtonTheme.colors.iconNorm,
painter = painterResource(id = R.drawable.ic_proton_three_dots_vertical),
contentDescription = stringResource(
id = R.string.raw_message_data_more_button_content_description
if (state is RawMessageDataState.Data) {
IconButton(
onClick = { isDropDownExpanded.value = true }
) {
Icon(
tint = ProtonTheme.colors.iconNorm,
painter = painterResource(id = R.drawable.ic_proton_three_dots_vertical),
contentDescription = stringResource(
id = R.string.raw_message_data_more_button_content_description
)
)
)
}
}
DropdownMenu(
expanded = isDropDownExpanded.value,
onDismissRequest = { isDropDownExpanded.value = false },
containerColor = ProtonTheme.colors.backgroundInvertedNorm
) {
DropdownMenuItem(
text = {
Text(
text = stringResource(id = R.string.raw_message_data_download_action),
style = ProtonTheme.typography.bodyLargeNorm
)
},
onClick = {},
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_proton_arrow_down_line),
contentDescription = NO_CONTENT_DESCRIPTION
DropdownMenu(
expanded = isDropDownExpanded.value,
onDismissRequest = { isDropDownExpanded.value = false },
containerColor = ProtonTheme.colors.backgroundInvertedNorm
) {
DropdownMenuItem(
text = {
Text(
text = stringResource(id = R.string.raw_message_data_download_action),
style = ProtonTheme.typography.bodyLargeNorm
)
},
onClick = {
isDropDownExpanded.value = false
onDownloadData(state.type, state.data)
},
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_proton_arrow_down_line),
contentDescription = NO_CONTENT_DESCRIPTION
)
}
)
DropdownMenuItem(
text = {
Text(
text = stringResource(id = R.string.raw_message_data_copy_action),
style = ProtonTheme.typography.bodyLargeNorm
)
},
onClick = {
isDropDownExpanded.value = false
context.copyTextToClipboard(state.type.name, state.data)
},
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_proton_squares),
contentDescription = NO_CONTENT_DESCRIPTION
)
}
)
if (state.type == RawMessageDataType.Headers) {
DropdownMenuItem(
text = {
Text(
text = stringResource(id = R.string.raw_message_data_learn_more_action),
style = ProtonTheme.typography.bodyLargeNorm
)
},
onClick = {
isDropDownExpanded.value = false
openLearnMoreLink(context)
},
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_proton_info_circle),
contentDescription = NO_CONTENT_DESCRIPTION
)
}
)
}
)
DropdownMenuItem(
text = {
Text(
text = stringResource(id = R.string.raw_message_data_copy_action),
style = ProtonTheme.typography.bodyLargeNorm
)
},
onClick = {},
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_proton_squares),
contentDescription = NO_CONTENT_DESCRIPTION
)
}
)
DropdownMenuItem(
text = {
Text(
text = stringResource(id = R.string.raw_message_data_learn_more_action),
style = ProtonTheme.typography.bodyLargeNorm
)
},
onClick = {},
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_proton_info_circle),
contentDescription = NO_CONTENT_DESCRIPTION
)
}
)
}
}
}
)
@@ -188,6 +224,12 @@ fun RawMessageDataScreen(state: RawMessageDataState, onBackClick: () -> Unit) {
}
}
private fun openLearnMoreLink(context: Context) {
val uri = "https://proton.me/blog/what-are-email-headers".toUri()
val intent = Intent(Intent.ACTION_VIEW, uri)
context.startActivity(intent)
}
@Preview
@Composable
fun RawMessageDataScreenPreview() {
@@ -238,9 +280,11 @@ fun RawMessageDataScreenPreview() {
User-Agent: ProtonMail/Android-5.5.3
X-Proton-Sender-Ip: 83.212.41.102
X-Proton-Relay: yes
""".trimIndent()
""".trimIndent(),
toast = Effect.empty()
),
onBackClick = {}
onBackClick = {},
onDownloadData = { _, _ -> }
)
}
@@ -21,6 +21,9 @@ package ch.protonmail.android.maildetail.presentation.viewmodel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import ch.protonmail.android.mailcommon.presentation.Effect
import ch.protonmail.android.maildetail.domain.usecase.DownloadRawMessageData
import ch.protonmail.android.maildetail.presentation.R
import ch.protonmail.android.maildetail.presentation.model.RawMessageDataState
import ch.protonmail.android.maildetail.presentation.model.RawMessageDataType
import ch.protonmail.android.maildetail.presentation.ui.RawMessageDataScreen
@@ -34,12 +37,14 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import me.proton.core.util.kotlin.deserializeOrNull
import javax.inject.Inject
@HiltViewModel
class RawMessageDataViewModel @Inject constructor(
private val downloadRawMessageData: DownloadRawMessageData,
private val getRawMessageBody: GetRawMessageBody,
private val getRawMessageHeaders: GetRawMessageHeaders,
private val savedStateHandle: SavedStateHandle,
@@ -59,11 +64,11 @@ class RawMessageDataViewModel @Inject constructor(
val event = when (rawMessageDataType) {
RawMessageDataType.Headers -> getRawMessageHeaders(primaryUserId.first(), messageId).fold(
ifLeft = { RawMessageDataState.Error(rawMessageDataType) },
ifRight = { RawMessageDataState.Data(rawMessageDataType, it) }
ifRight = { RawMessageDataState.Data(rawMessageDataType, it, Effect.empty()) }
)
RawMessageDataType.HTML -> getRawMessageBody(primaryUserId.first(), messageId).fold(
ifLeft = { RawMessageDataState.Error(rawMessageDataType) },
ifRight = { RawMessageDataState.Data(rawMessageDataType, it) }
ifRight = { RawMessageDataState.Data(rawMessageDataType, it, Effect.empty()) }
)
}
@@ -71,6 +76,33 @@ class RawMessageDataViewModel @Inject constructor(
}
}
fun downloadData(type: RawMessageDataType, data: String) = viewModelScope.launch {
val fileName = when (type) {
RawMessageDataType.Headers -> "headers"
RawMessageDataType.HTML -> "html"
}
downloadRawMessageData(fileName, data).fold(
ifLeft = {
mutableState.update {
if (it is RawMessageDataState.Data) {
it.copy(toast = Effect.of(R.string.raw_message_data_failed_download))
} else {
it
}
}
},
ifRight = {
mutableState.update {
if (it is RawMessageDataState.Data) {
it.copy(toast = Effect.of(R.string.raw_message_data_successful_download))
} else {
it
}
}
}
)
}
private fun requireMessageId(): MessageId {
val messageIdParam = savedStateHandle.get<String>(RawMessageDataScreen.MESSAGE_ID_KEY)
?: throw IllegalStateException("No Message id given")
@@ -191,4 +191,6 @@
<string name="raw_message_data_copy_action">Copy</string>
<string name="raw_message_data_learn_more_action">Learn more</string>
<string name="raw_message_data_error">Error loading data</string>
<string name="raw_message_data_successful_download">Download completed</string>
<string name="raw_message_data_failed_download">Download failed</string>
</resources>
@@ -23,8 +23,11 @@ import app.cash.turbine.test
import arrow.core.left
import arrow.core.right
import ch.protonmail.android.mailcommon.domain.model.DataError
import ch.protonmail.android.mailcommon.presentation.Effect
import ch.protonmail.android.maildetail.domain.usecase.DownloadRawMessageData
import ch.protonmail.android.maildetail.presentation.model.RawMessageDataState
import ch.protonmail.android.maildetail.presentation.model.RawMessageDataType
import ch.protonmail.android.maildetail.presentation.R
import ch.protonmail.android.maildetail.presentation.ui.RawMessageDataScreen
import ch.protonmail.android.maildetail.presentation.viewmodel.RawMessageDataViewModelTest.TestData.MESSAGE_ID
import ch.protonmail.android.maildetail.presentation.viewmodel.RawMessageDataViewModelTest.TestData.RAW_DATA_TYPE_HEADERS
@@ -49,6 +52,7 @@ import kotlin.test.assertEquals
class RawMessageDataViewModelTest {
private val downloadRawMessageData = mockk<DownloadRawMessageData>()
private val getRawMessageBody = mockk<GetRawMessageBody>()
private val getRawMessageHeaders = mockk<GetRawMessageHeaders>()
private val savedStateHandle = mockk<SavedStateHandle> {
@@ -60,6 +64,7 @@ class RawMessageDataViewModelTest {
private val rawMessageDataViewModel by lazy {
RawMessageDataViewModel(
downloadRawMessageData = downloadRawMessageData,
getRawMessageBody = getRawMessageBody,
getRawMessageHeaders = getRawMessageHeaders,
savedStateHandle = savedStateHandle,
@@ -86,7 +91,8 @@ class RawMessageDataViewModelTest {
// Then
val expected = RawMessageDataState.Data(
type = RawMessageDataType.Headers,
data = rawHeaders
data = rawHeaders,
toast = Effect.empty()
)
assertEquals(expected, awaitItem())
}
@@ -122,7 +128,8 @@ class RawMessageDataViewModelTest {
// Then
val expected = RawMessageDataState.Data(
type = RawMessageDataType.HTML,
data = rawBody
data = rawBody,
toast = Effect.empty()
)
assertEquals(expected, awaitItem())
}
@@ -144,6 +151,52 @@ class RawMessageDataViewModelTest {
}
}
@Test
fun `should emit state with success toast when downloading data is successful`() = runTest {
// Given
val messageId = MessageId(MESSAGE_ID)
val rawBody = "raw body"
every { savedStateHandle.get<String>(RawMessageDataScreen.RAW_DATA_TYPE_KEY) } returns RAW_DATA_TYPE_HTML
coEvery { getRawMessageBody(UserIdTestData.userId, messageId) } returns rawBody.right()
coEvery { downloadRawMessageData("html", rawBody) } returns Unit.right()
rawMessageDataViewModel.state.test {
awaitItem()
// When
rawMessageDataViewModel.downloadData(RawMessageDataType.HTML, rawBody)
// Then
assertEquals(
R.string.raw_message_data_successful_download,
(awaitItem() as RawMessageDataState.Data).toast.consume()
)
}
}
@Test
fun `should emit state with failure toast when downloading data failed`() = runTest {
// Given
val messageId = MessageId(MESSAGE_ID)
val rawHeaders = "raw headers"
every { savedStateHandle.get<String>(RawMessageDataScreen.RAW_DATA_TYPE_KEY) } returns RAW_DATA_TYPE_HEADERS
coEvery { getRawMessageHeaders(UserIdTestData.userId, messageId) } returns rawHeaders.right()
coEvery { downloadRawMessageData("headers", rawHeaders) } returns DataError.Local.Unknown.left()
rawMessageDataViewModel.state.test {
awaitItem()
// When
rawMessageDataViewModel.downloadData(RawMessageDataType.Headers, rawHeaders)
// Then
assertEquals(
R.string.raw_message_data_failed_download,
(awaitItem() as RawMessageDataState.Data).toast.consume()
)
}
}
object TestData {
const val MESSAGE_ID = "message_id"