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
@@ -0,0 +1,87 @@
package org.dhis2.mobile.sync.data

import org.dhis2.mobile.sync.model.ProgramType
import org.hisp.dhis.android.core.D2
import org.hisp.dhis.android.core.common.State
import org.hisp.dhis.android.core.program.ProgramType as CoreProgramType

internal class SyncProgramRepositoryImpl(
private val d2: D2,
) : SyncProgramRepository {
override suspend fun getProgramType(programUid: String): ProgramType =
when (
d2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you think something in this method could throw a crash? should he handle the error?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think so

.programModule()
.programs()
.uid(programUid)
.suspendGet()
?.programType
) {
CoreProgramType.WITH_REGISTRATION -> ProgramType.Tracker
CoreProgramType.WITHOUT_REGISTRATION -> ProgramType.Event
null -> ProgramType.None
}

override suspend fun uploadTrackerProgram(programUid: String) {
d2
.trackedEntityModule()
.trackedEntityInstances()
.byProgramUids(listOf(programUid))
.blockingUpload()
}

override suspend fun downloadTrackerProgram(programUid: String) {
d2
.trackedEntityModule()
.trackedEntityInstanceDownloader()
.byProgramUid(programUid)
.blockingDownload()
}

override suspend fun uploadEventProgram(programUid: String) {
d2
.eventModule()
.events()
.byProgramUid()
.eq(programUid)
.blockingUpload()
}

override suspend fun downloadEventProgram(programUid: String) {
d2
.eventModule()
.eventDownloader()
.byProgramUid(programUid)
.blockingDownload()
}

override suspend fun downloadFileResources(programUid: String) {
d2
.fileResourceModule()
.fileResourceDownloader()
.byProgramUid()
.eq(programUid)
.blockingDownload()
}

override suspend fun allTeisAreSynced(programUid: String) =
d2
.trackedEntityModule()
.trackedEntityInstances()
.byProgramUids(listOf(programUid))
.byAggregatedSyncState()
.notIn(State.SYNCED, State.RELATIONSHIP)
.blockingGet()
.isEmpty()

override suspend fun allEventsAreSynced(programUid: String) =
d2
.eventModule()
.events()
.byProgramUid()
.eq(programUid)
.byAggregatedSyncState()
.notIn(State.SYNCED)
.blockingGet()
.isEmpty()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.dhis2.mobile.sync.data

import org.dhis2.mobile.sync.model.ProgramType

typealias IsSynced = Boolean

internal interface SyncProgramRepository {
suspend fun getProgramType(programUid: String): ProgramType

suspend fun uploadTrackerProgram(programUid: String)

suspend fun downloadTrackerProgram(programUid: String)

suspend fun uploadEventProgram(programUid: String)

suspend fun downloadEventProgram(programUid: String)

suspend fun downloadFileResources(programUid: String)

suspend fun allTeisAreSynced(programUid: String): Boolean

suspend fun allEventsAreSynced(programUid: String): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.dhis2.mobile.sync.domain

import org.dhis2.mobile.commons.domain.UseCase
import org.dhis2.mobile.sync.data.SyncProgramRepository
import org.dhis2.mobile.sync.model.ProgramType

internal typealias ProgramUid = String

internal class SyncProgram(
private val syncProgramRepository: SyncProgramRepository,
private val syncStatusController: SyncStatusController,
) : UseCase<ProgramUid, Unit> {
override suspend fun invoke(input: ProgramUid): Result<Unit> =
try {
val isSynced =
when (syncProgramRepository.getProgramType(input)) {
ProgramType.Event -> syncEvents(input)
ProgramType.Tracker -> syncTracker(input)
ProgramType.None -> return Result.failure(Exception("Unknown program"))
}

if (isSynced) {
syncStatusController.updateSingleProgramToSuccess(input)
Result.success(Unit)
} else {
Result.failure(Exception("Status is not synced"))
}
} catch (e: Exception) {
Result.failure(e)
}

private suspend fun syncEvents(programUid: ProgramUid): Boolean =
with(syncProgramRepository) {
uploadEventProgram(programUid)
val isSynced = allEventsAreSynced(programUid)
if (isSynced) {
downloadEventProgram(programUid)
downloadFileResources(programUid)
}
isSynced
}

private suspend fun syncTracker(programUid: ProgramUid): Boolean =
with(syncProgramRepository) {
uploadTrackerProgram(programUid)
val isSynced = allTeisAreSynced(programUid)
if (isSynced) {
downloadTrackerProgram(programUid)
downloadFileResources(programUid)
}
isSynced
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.dhis2.mobile.sync.model

sealed interface ProgramType {
data object Event : ProgramType

data object Tracker : ProgramType

data object None : ProgramType
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.dhis2.mobile.sync.domain

import kotlinx.coroutines.runBlocking
import org.dhis2.mobile.sync.data.SyncProgramRepository
import org.dhis2.mobile.sync.model.ProgramType
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import kotlin.test.assertTrue

class SyncProgramTest {
private val repository: SyncProgramRepository = mock()
private val syncStatusController = SyncStatusController()
private val useCase = SyncProgram(repository, syncStatusController)

@Test
fun `should sync event program successfully when all events are synced`() =
runBlocking {
val programUid = "eventProgramUid"
whenever(repository.getProgramType(programUid)).thenReturn(ProgramType.Event)
whenever(repository.allEventsAreSynced(programUid)).thenReturn(true)

val result = useCase(programUid)

assertTrue(result.isSuccess)
verify(repository).uploadEventProgram(programUid)
verify(repository).downloadEventProgram(programUid)
verify(repository).downloadFileResources(programUid)
}

@Test
fun `should not download event program when events are not synced`() =
runBlocking {
val programUid = "eventProgramUid"
whenever(repository.getProgramType(programUid)).thenReturn(ProgramType.Event)
whenever(repository.allEventsAreSynced(programUid)).thenReturn(false)

val result = useCase(programUid)

assertTrue(result.isFailure)
verify(repository).uploadEventProgram(programUid)
verify(repository, never()).downloadEventProgram(programUid)
verify(repository, never()).downloadFileResources(programUid)
}

@Test
fun `should sync tracker program successfully when all teis are synced`() =
runBlocking {
val programUid = "trackerProgramUid"
whenever(repository.getProgramType(programUid)).thenReturn(ProgramType.Tracker)
whenever(repository.allTeisAreSynced(programUid)).thenReturn(true)

val result = useCase(programUid)

assertTrue(result.isSuccess)
verify(repository).uploadTrackerProgram(programUid)
verify(repository).downloadTrackerProgram(programUid)
verify(repository).downloadFileResources(programUid)
}

@Test
fun `should not download tracker program when teis are not synced`() =
runBlocking {
val programUid = "trackerProgramUid"
whenever(repository.getProgramType(programUid)).thenReturn(ProgramType.Tracker)
whenever(repository.allTeisAreSynced(programUid)).thenReturn(false)

val result = useCase(programUid)

assertTrue(result.isFailure)
verify(repository).uploadTrackerProgram(programUid)
verify(repository, never()).downloadTrackerProgram(programUid)
verify(repository, never()).downloadFileResources(programUid)
}

@Test
fun `should return failure for unknown program type`() =
runBlocking {
val programUid = "unknownProgramUid"
whenever(repository.getProgramType(programUid)).thenReturn(ProgramType.None)

val result = useCase(programUid)

assertTrue(result.isFailure)
}

@Test
fun `should return failure when getProgramType throws`() =
runBlocking {
val programUid = "programUid"
whenever(repository.getProgramType(programUid)).thenThrow(RuntimeException("type error"))

val result = useCase(programUid)

assertTrue(result.isFailure)
}
}