Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import com.simprints.infra.eventsync.event.remote.models.fromDomainToApi
internal data class ApiEventSampleUpSyncRequestPayload(
override val startTime: ApiTimestamp,
val endTime: ApiTimestamp?,
val requestId: String,
val requestId: String?,
val sampleId: String,
val size: Long,
val errorType: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class SampleUpSyncRequestEvent(
constructor(
createdAt: Timestamp,
endedAt: Timestamp,
requestId: String,
requestId: String?,
sampleId: String,
size: Long,
errorType: String? = null,
Expand All @@ -42,7 +42,7 @@ class SampleUpSyncRequestEvent(
data class SampleUpSyncRequestPayload(
override val createdAt: Timestamp,
override val endedAt: Timestamp?,
val requestId: String,
val requestId: String?,
val sampleId: String,
val size: Long,
val errorType: String?,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
package com.simprints.infra.images.remote.firestore
package com.simprints.infra.images.remote.firebase

import com.google.firebase.storage.FirebaseStorage
import com.google.firebase.storage.StorageMetadata
import com.google.firebase.storage.StorageReference
import com.google.firebase.storage.UploadTask
import com.simprints.core.tools.time.TimeHelper
import com.simprints.infra.authstore.AuthStore
import com.simprints.infra.config.sync.ConfigManager
import com.simprints.infra.events.EventRepository
import com.simprints.infra.events.event.domain.models.samples.SampleUpSyncRequestEvent
import com.simprints.infra.events.event.domain.models.scope.EventScopeEndCause
import com.simprints.infra.events.event.domain.models.scope.EventScopeType
import com.simprints.infra.images.local.ImageLocalDataSource
import com.simprints.infra.images.metadata.ImageMetadataStore
import com.simprints.infra.images.model.SecuredImageRef
import com.simprints.infra.images.remote.SampleUploader
import com.simprints.infra.images.usecase.SamplePathConverter
import com.simprints.infra.logging.LoggingConstants.CrashReportTag.SAMPLE_UPLOAD
import com.simprints.infra.logging.LoggingConstants.CrashReportTag.SYNC
import com.simprints.infra.logging.Simber
import kotlinx.coroutines.tasks.await
import java.io.FileInputStream
import javax.inject.Inject

internal class FirestoreSampleUploader @Inject constructor(
internal class FirebaseSampleUploader @Inject constructor(
private val timeHelper: TimeHelper,
private val configManager: ConfigManager,
private val authStore: AuthStore,
private val localDataSource: ImageLocalDataSource,
private val metadataStore: ImageMetadataStore,
private val samplePathUtil: SamplePathConverter,
private val eventRepository: EventRepository,
) : SampleUploader {
override suspend fun uploadAllSamples(
projectId: String,
Expand All @@ -33,33 +43,51 @@ internal class FirestoreSampleUploader @Inject constructor(
}
var allImagesUploaded = true

Simber.i("Starting sample upload to Firestore")
Simber.i("Starting sample upload to Firebase storage")
val bucketUrl = configManager.getProject(projectId).imageBucket
val rootRef = FirebaseStorage
.getInstance(firebaseApp, bucketUrl)
.reference

val urlRequestScope = eventRepository.createEventScope(type = EventScopeType.SAMPLE_UP_SYNC)

val sampleReferences = localDataSource.listImages(projectId)
sampleReferences.forEachIndexed { index, imageRef ->
Simber.i("Reading sample file: ${imageRef.relativePath.parts.last()}", tag = SAMPLE_UPLOAD)

progressCallback?.invoke(index, sampleReferences.size)
try {
val requestStartTime = timeHelper.now()
localDataSource.decryptImage(imageRef)?.let { stream ->
val metadata = metadataStore.getMetadata(imageRef.relativePath)
val uploadSuccessful = uploadSample(rootRef, stream, imageRef, metadata)
if (uploadSuccessful) {

val task = uploadSample(rootRef, stream, imageRef, metadata)
if (task.task.isSuccessful) {
localDataSource.deleteImage(imageRef)
metadataStore.deleteMetadata(imageRef.relativePath)
} else {
allImagesUploaded = false
Simber.i("Failed to upload image without exception", tag = SAMPLE_UPLOAD)
}

eventRepository.addOrUpdateEvent(
scope = urlRequestScope,
event = SampleUpSyncRequestEvent(
createdAt = requestStartTime,
endedAt = timeHelper.now(),
requestId = null,
sampleId = samplePathUtil.extract(imageRef.relativePath)?.sampleId.orEmpty(),
size = task.bytesTransferred,
errorType = task.error?.javaClass?.simpleName,
),
)
}
} catch (t: Throwable) {
allImagesUploaded = false
Simber.e("Failed to upload images", t, tag = SYNC)
}
}
eventRepository.closeEventScope(urlRequestScope, EventScopeEndCause.WORKFLOW_ENDED)

return allImagesUploaded
}
Expand All @@ -69,13 +97,13 @@ internal class FirestoreSampleUploader @Inject constructor(
imageStream: FileInputStream,
imageRef: SecuredImageRef,
metadata: Map<String, String>,
): Boolean {
): UploadTask.TaskSnapshot {
val fileRef = imageRef.relativePath.parts
.fold(rootRef) { ref, pathPart -> ref.child(pathPart) }

Simber.i("Uploading ${fileRef.path.last()}", tag = SAMPLE_UPLOAD)

val uploadTask = if (metadata.isEmpty()) {
return if (metadata.isEmpty()) {
fileRef.putStream(imageStream).await()
} else {
val storeMetadata = StorageMetadata
Expand All @@ -84,6 +112,5 @@ internal class FirestoreSampleUploader @Inject constructor(
.build()
fileRef.putStream(imageStream, storeMetadata).await()
}
return uploadTask.task.isSuccessful
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package com.simprints.infra.images.usecase
import com.simprints.infra.config.store.ConfigRepository
import com.simprints.infra.config.store.models.experimental
import com.simprints.infra.images.remote.SampleUploader
import com.simprints.infra.images.remote.firestore.FirestoreSampleUploader
import com.simprints.infra.images.remote.firebase.FirebaseSampleUploader
import com.simprints.infra.images.remote.signedurl.SignedUrlSampleUploader
import javax.inject.Inject

internal class GetUploaderUseCase @Inject constructor(
private val configRepository: ConfigRepository,
private val firestoreUploader: FirestoreSampleUploader,
private val firestoreUploader: FirebaseSampleUploader,
private val signedUrlUploader: SignedUrlSampleUploader,
) {
suspend operator fun invoke(): SampleUploader = configRepository
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
package com.simprints.infra.images.remote.firestore
package com.simprints.infra.images.remote.firebase

import androidx.test.ext.junit.runners.*
import com.google.common.truth.Truth.*
import com.google.firebase.storage.FirebaseStorage
import com.simprints.core.tools.time.TimeHelper
import com.simprints.core.tools.time.Timestamp
import com.simprints.infra.authstore.AuthStore
import com.simprints.infra.config.store.models.GeneralConfiguration
import com.simprints.infra.config.sync.ConfigManager
import com.simprints.infra.events.EventRepository
import com.simprints.infra.events.event.domain.models.scope.EventScope
import com.simprints.infra.events.event.domain.models.scope.EventScopeType
import com.simprints.infra.images.local.ImageLocalDataSource
import com.simprints.infra.images.metadata.ImageMetadataStore
import com.simprints.infra.images.model.Path
import com.simprints.infra.images.model.SecuredImageRef
import com.simprints.infra.images.usecase.SamplePathConverter
import io.mockk.*
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.tasks.await
Expand All @@ -21,7 +28,10 @@ import java.io.FileInputStream

@Suppress("DEPRECATION")
@RunWith(AndroidJUnit4::class)
class FirestoreSampleUploaderTest {
class FirebaseSampleUploaderTest {
@MockK
private lateinit var timeHelper: TimeHelper

@MockK
private lateinit var configManager: ConfigManager

Expand All @@ -34,24 +44,39 @@ class FirestoreSampleUploaderTest {
@MockK
private lateinit var metadataStore: ImageMetadataStore

@MockK
private lateinit var samplePathUtil: SamplePathConverter

@MockK
private lateinit var eventRepository: EventRepository

@MockK
private lateinit var localDataSource: ImageLocalDataSource

private lateinit var remoteDataSource: FirestoreSampleUploader
private lateinit var remoteDataSource: FirebaseSampleUploader

@Before
fun setup() {
MockKAnnotations.init(this, relaxed = true)

every { mockSecuredImageRef.relativePath.parts } returns arrayOf("Test1")

remoteDataSource = FirestoreSampleUploader(
remoteDataSource = FirebaseSampleUploader(
timeHelper = timeHelper,
configManager = configManager,
authStore = authStore,
localDataSource = localDataSource,
metadataStore = metadataStore,
samplePathUtil = samplePathUtil,
eventRepository = eventRepository,
)

every { timeHelper.now() } returns Timestamp(0L)
every { samplePathUtil.extract(any()) } returns
SamplePathConverter.PathData("sessionID", GeneralConfiguration.Modality.FACE, "sampleId")
coEvery { eventRepository.createEventScope(any(), any()) } returns mockk<EventScope>()
coJustRun { eventRepository.closeEventScope(any<EventScope>(), any()) }

// We need to mock statics and global extensions
mockkStatic(FirebaseStorage::class)
mockkStatic("kotlinx.coroutines.tasks.TasksKt")
Expand Down Expand Up @@ -118,6 +143,30 @@ class FirestoreSampleUploaderTest {
coVerify(exactly = 3) { metadataStore.deleteMetadata(any()) }
}

@Test
fun `test upload and report all upload events`() = runTest {
setupProjectConfig()
setupStorageMock()
configureLocalImageFiles(numberOfValidFiles = 3)

assertThat(remoteDataSource.uploadAllSamples(PROJECT_ID)).isTrue()
coVerify(exactly = 1) { eventRepository.createEventScope(EventScopeType.SAMPLE_UP_SYNC, any()) }
coVerify(exactly = 3) { eventRepository.addOrUpdateEvent(any(), any()) }
coVerify(exactly = 1) { eventRepository.closeEventScope(any<EventScope>(), any()) }
}

@Test
fun `test upload failed and report all upload events`() = runTest {
setupProjectConfig()
setupStorageMock(success = false)
configureLocalImageFiles(numberOfValidFiles = 3)

assertThat(remoteDataSource.uploadAllSamples(PROJECT_ID)).isFalse()
coVerify(exactly = 1) { eventRepository.createEventScope(EventScopeType.SAMPLE_UP_SYNC, any()) }
coVerify(exactly = 3) { eventRepository.addOrUpdateEvent(any(), any()) }
coVerify(exactly = 1) { eventRepository.closeEventScope(any<EventScope>(), any()) }
}

@Test
fun `test failed decryption should not return success`() = runTest {
setupProjectConfig()
Expand Down Expand Up @@ -167,10 +216,18 @@ class FirestoreSampleUploaderTest {
every { reference.child(any()) } returns mockk {
every { path } returns "testPath"
every { putStream(any()) } returns mockk {
coEvery { await().task.isSuccessful } returns success
coEvery { await() } returns mockk {
coEvery { bytesTransferred } returns 1L
coEvery { task.isSuccessful } returns success
coEvery { error } returns null
}
}
every { putStream(any(), any()) } returns mockk {
coEvery { await().task.isSuccessful } returns success
coEvery { await() } returns mockk {
coEvery { bytesTransferred } returns 1L
coEvery { task.isSuccessful } returns success
coEvery { error } returns null
}
}
}
}
Expand Down Expand Up @@ -204,7 +261,7 @@ class FirestoreSampleUploaderTest {

private fun mockImage() = SecuredImageRef(Path(VALID_PATH))

companion object {
companion object Companion {
private const val VALID_PATH = "valid.txt"
private const val PROJECT_ID = "projectId"
}
Expand Down