feat(chat): save delivered files with native picker (#803)
This commit is contained in:
1 parent
509a018434
commit
19c053b3fa
9 files changed
+363
-41
No files matched your search
@@ -0,0 +1,28 @@
|
||||
package com.m57.hermescontrol.ui.chat
|
||||
|
||||
import android.app.Activity
|
||||
import android.net.Uri
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class AttachmentSavePolicyTest {
|
||||
@Test
|
||||
fun `save result accepts a destination only for RESULT_OK`() {
|
||||
val destination = mockk<Uri>()
|
||||
|
||||
assertSame(destination, acceptedSaveDestination(Activity.RESULT_OK, destination))
|
||||
assertNull(acceptedSaveDestination(Activity.RESULT_CANCELED, destination))
|
||||
assertNull(acceptedSaveDestination(Activity.RESULT_OK, null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `picker cannot start while picker or save is pending`() {
|
||||
assertTrue(canStartAttachmentSave(null, null))
|
||||
assertFalse(canStartAttachmentSave("/tmp/picker.pdf", null))
|
||||
assertFalse(canStartAttachmentSave(null, "/tmp/saving.pdf"))
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
@@ -2882,6 +2883,129 @@ class ChatViewModelTest {
|
||||
cacheDir.deleteRecursively()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveAttachment writes gateway file to selected document`() =
|
||||
runTest {
|
||||
mockkObject(GatewayFileClient)
|
||||
val bytes = "downloaded".toByteArray()
|
||||
coEvery { GatewayFileClient.fetch("/tmp/note.txt") } returns
|
||||
GatewayFileResult.Success(GatewayFile("note.txt", "text/plain", bytes))
|
||||
val resolver = mockk<android.content.ContentResolver>()
|
||||
val output = java.io.ByteArrayOutputStream()
|
||||
val destination = mockk<android.net.Uri>()
|
||||
every { app.contentResolver } returns resolver
|
||||
every { resolver.openOutputStream(destination, "wt") } returns output
|
||||
|
||||
val vm = createViewModel()
|
||||
val attachment =
|
||||
Attachment(
|
||||
uri = "unused",
|
||||
name = "note.txt",
|
||||
mimeType = "text/plain",
|
||||
gatewayUrl = "https://gw/api/files/download?path=%2Ftmp%2Fnote.txt&token=t",
|
||||
source = AttachmentSource.GATEWAY,
|
||||
)
|
||||
|
||||
vm.saveAttachment(attachment, destination)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertArrayEquals(bytes, output.toByteArray())
|
||||
assertNull(vm.uiState.value.savingAttachmentPath)
|
||||
assertEquals("Saved note.txt", vm.uiState.value.openError)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveAttachment never deletes the selected document when download fails`() =
|
||||
runTest {
|
||||
mockkObject(GatewayFileClient)
|
||||
val resolver = mockk<android.content.ContentResolver>(relaxed = true)
|
||||
val destination = mockk<android.net.Uri>()
|
||||
every { app.contentResolver } returns resolver
|
||||
coEvery {
|
||||
GatewayFileClient.fetch("/tmp/missing.pdf")
|
||||
} returns GatewayFileResult.NotFound
|
||||
|
||||
val (viewModel, _) = createViewModelWithSession()
|
||||
viewModel.saveAttachment(
|
||||
attachment =
|
||||
Attachment(
|
||||
uri = "gateway:/tmp/missing.pdf",
|
||||
name = "missing.pdf",
|
||||
mimeType = "application/pdf",
|
||||
size = 0,
|
||||
source = AttachmentSource.GATEWAY,
|
||||
gatewayUrl = "https://host/files/download?path=%2Ftmp%2Fmissing.pdf",
|
||||
),
|
||||
destination = destination,
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(exactly = 0) { resolver.delete(any(), any(), any()) }
|
||||
assertNull(viewModel.uiState.value.savingAttachmentPath)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveAttachment never deletes the selected document when writing fails`() =
|
||||
runTest {
|
||||
mockkObject(GatewayFileClient)
|
||||
val resolver = mockk<android.content.ContentResolver>(relaxed = true)
|
||||
val destination = mockk<android.net.Uri>()
|
||||
every { app.contentResolver } returns resolver
|
||||
every { resolver.openOutputStream(destination, "wt") } throws IllegalStateException("write failed")
|
||||
coEvery { GatewayFileClient.fetch("/tmp/report.pdf") } returns
|
||||
GatewayFileResult.Success(GatewayFile("report.pdf", "application/pdf", byteArrayOf(1)))
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel.saveAttachment(
|
||||
Attachment(
|
||||
uri = "gateway:/tmp/report.pdf",
|
||||
name = "report.pdf",
|
||||
mimeType = "application/pdf",
|
||||
source = AttachmentSource.GATEWAY,
|
||||
),
|
||||
destination,
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(exactly = 0) { resolver.delete(any(), any(), any()) }
|
||||
assertNull(viewModel.uiState.value.savingAttachmentPath)
|
||||
assertTrue(viewModel.uiState.value.openError.orEmpty().startsWith("Could not save"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveAttachment ignores overlapping saves`() =
|
||||
runTest {
|
||||
mockkObject(GatewayFileClient)
|
||||
val firstResult = CompletableDeferred<GatewayFileResult>()
|
||||
val fetchedPaths = mutableListOf<String>()
|
||||
coEvery { GatewayFileClient.fetch(capture(fetchedPaths)) } coAnswers { firstResult.await() }
|
||||
val resolver = mockk<android.content.ContentResolver>(relaxed = true)
|
||||
every { app.contentResolver } returns resolver
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.saveAttachment(
|
||||
Attachment("gateway:/tmp/first.pdf", "first.pdf", "application/pdf", source = AttachmentSource.GATEWAY),
|
||||
mockk(),
|
||||
)
|
||||
runCurrent()
|
||||
viewModel.saveAttachment(
|
||||
Attachment(
|
||||
"gateway:/tmp/second.pdf",
|
||||
"second.pdf",
|
||||
"application/pdf",
|
||||
source = AttachmentSource.GATEWAY,
|
||||
),
|
||||
mockk(),
|
||||
)
|
||||
|
||||
assertEquals("/tmp/first.pdf", viewModel.uiState.value.savingAttachmentPath)
|
||||
assertEquals(listOf("/tmp/first.pdf"), fetchedPaths)
|
||||
|
||||
firstResult.complete(GatewayFileResult.NotFound)
|
||||
advanceUntilIdle()
|
||||
assertNull(viewModel.uiState.value.savingAttachmentPath)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openAttachment GATEWAY not-found surfaces openError`() =
|
||||
runTest {
|
||||
|
||||
@@ -27,6 +27,7 @@ class ImageBytesResolverTest {
|
||||
@Test
|
||||
fun `extensionForMime maps known types`() {
|
||||
assertEquals("png", ImageBytesResolver.extensionForMime("image/png"))
|
||||
assertEquals("png", ImageBytesResolver.extensionForMime(" image/png; charset=binary"))
|
||||
assertEquals("jpg", ImageBytesResolver.extensionForMime("image/jpeg"))
|
||||
assertEquals("jpg", ImageBytesResolver.extensionForMime("image/JPG"))
|
||||
assertEquals("gif", ImageBytesResolver.extensionForMime("image/gif"))
|
||||
|
||||
Reference in new issue
Block a user