Napraw synchronizację kursu w aplikacji kierowcy
This commit is contained in:
@@ -2,12 +2,27 @@ package pl.firmatpp.kierowca.data
|
||||
|
||||
import java.io.IOException
|
||||
import java.net.UnknownHostException
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Test
|
||||
|
||||
class ApiErrorMapperTest {
|
||||
@Test
|
||||
fun rethrowsWorkerCancellationInsteadOfMappingItAsApplicationFailure() {
|
||||
val cancellation = CancellationException("Job was cancelled")
|
||||
|
||||
try {
|
||||
cancellation.rethrowIfCancellation()
|
||||
fail("CancellationException should be rethrown")
|
||||
} catch (thrown: CancellationException) {
|
||||
assertSame(cancellation, thrown)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mapsNetworkFailuresToRetryableOfflineMessage() {
|
||||
val error = ApiErrorMapper.map(IOException("timeout"))
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package pl.firmatpp.kierowca.data.upload
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class OfflineOutboxManagerTest {
|
||||
@Test
|
||||
fun recognizesLegacyCancellationFailureDetails() {
|
||||
assertTrue(isWorkerCancellationFailure("Typ: błąd aplikacji\nKomunikat: Job was cancelled"))
|
||||
assertTrue(isWorkerCancellationFailure("kotlinx.coroutines.JobCancellationException"))
|
||||
assertFalse(isWorkerCancellationFailure("HTTP 422: Niepoprawne zdjęcie"))
|
||||
assertFalse(isWorkerCancellationFailure(null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun repairsInterruptedAndRetryablePhotoUploadsButNotRealPermanentFailures() {
|
||||
assertTrue(shouldRepairPhotoUpload(PhotoUploadStatus.Pending.storageValue, null))
|
||||
assertTrue(shouldRepairPhotoUpload(PhotoUploadStatus.Uploading.storageValue, null))
|
||||
assertTrue(shouldRepairPhotoUpload(PhotoUploadStatus.Verifying.storageValue, null))
|
||||
assertTrue(shouldRepairPhotoUpload(PhotoUploadStatus.FailedRetryable.storageValue, "timeout"))
|
||||
assertTrue(shouldRepairPhotoUpload(PhotoUploadStatus.FailedPermanent.storageValue, "Job was cancelled"))
|
||||
assertFalse(shouldRepairPhotoUpload(PhotoUploadStatus.FailedPermanent.storageValue, "HTTP 422"))
|
||||
assertFalse(shouldRepairPhotoUpload(PhotoUploadStatus.Cancelled.storageValue, null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun repairsInterruptedRouteActionWithItsOriginalIdempotencyKey() {
|
||||
assertTrue(shouldRepairRouteAction(RouteActionStatus.Pending, null))
|
||||
assertTrue(shouldRepairRouteAction(RouteActionStatus.WaitingForPhotos, null))
|
||||
assertTrue(shouldRepairRouteAction(RouteActionStatus.Syncing, null))
|
||||
assertTrue(shouldRepairRouteAction(RouteActionStatus.FailedRetryable, "timeout"))
|
||||
assertTrue(shouldRepairRouteAction(RouteActionStatus.FailedPermanent, "Job was cancelled"))
|
||||
assertTrue(
|
||||
shouldRepairRouteAction(
|
||||
RouteActionStatus.FailedPermanent,
|
||||
"Wymagane zdjęcie nie zostało wysłane. Popraw zdjęcie i ponów potwierdzenie etapu.",
|
||||
),
|
||||
)
|
||||
assertFalse(shouldRepairRouteAction(RouteActionStatus.FailedPermanent, "Brakuje zdjęcia"))
|
||||
assertFalse(shouldRepairRouteAction(RouteActionStatus.FailedConflict, "Kurs już rozpoczęty"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package pl.firmatpp.kierowca.data.upload
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import pl.firmatpp.kierowca.data.model.RoutePhotoDto
|
||||
|
||||
class PhotoUploadReconciliationTest {
|
||||
@Test
|
||||
fun serverReceiptIdentityClearsFalseLocalFailure() {
|
||||
val upload = upload(
|
||||
clientRequestId = "request-1",
|
||||
contentSha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
)
|
||||
|
||||
assertTrue(serverPhotoConfirmsUpload(upload, serverPhoto("request-1", upload.contentSha256.uppercase())))
|
||||
assertFalse(serverPhotoConfirmsUpload(upload, serverPhoto("request-2", upload.contentSha256)))
|
||||
assertFalse(serverPhotoConfirmsUpload(upload, serverPhoto("request-1", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun legacyServerPhotoWithoutHashStillConfirmsMatchingRequestId() {
|
||||
val upload = upload("request-1", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
|
||||
assertTrue(serverPhotoConfirmsUpload(upload, serverPhoto("request-1", null)))
|
||||
}
|
||||
|
||||
private fun upload(clientRequestId: String, contentSha256: String): PhotoUploadEntity =
|
||||
PhotoUploadEntity(
|
||||
clientRequestId = clientRequestId,
|
||||
routeId = "10",
|
||||
localPath = "/tmp/$clientRequestId.jpg",
|
||||
source = "camera",
|
||||
takenAt = null,
|
||||
latitude = null,
|
||||
longitude = null,
|
||||
locationAccuracyMeters = null,
|
||||
mimeType = "image/jpeg",
|
||||
size = 100,
|
||||
contentSha256 = contentSha256,
|
||||
status = PhotoUploadStatus.FailedPermanent.storageValue,
|
||||
lastError = "Typ: błąd aplikacji\nKomunikat: Job was cancelled",
|
||||
)
|
||||
|
||||
private fun serverPhoto(clientRequestId: String, contentSha256: String?): RoutePhotoDto =
|
||||
RoutePhotoDto(
|
||||
id = "42",
|
||||
routeId = "10",
|
||||
clientRequestId = clientRequestId,
|
||||
contentSha256 = contentSha256,
|
||||
source = "camera",
|
||||
stage = "loading",
|
||||
mimeType = "image/jpeg",
|
||||
size = 100,
|
||||
url = "https://example.test/photo/42",
|
||||
takenAt = null,
|
||||
latitude = null,
|
||||
longitude = null,
|
||||
locationAccuracyMeters = null,
|
||||
createdAt = null,
|
||||
)
|
||||
}
|
||||
@@ -16,7 +16,9 @@ class RouteDisplayMapperTest {
|
||||
contractName = "Odpady przemyslowe",
|
||||
contractCode = "TPP-ODP-24",
|
||||
relationLabel = "TPP Baza Krakow -> Instalacja Odpadowa Slask",
|
||||
status = "ZAPLANOWANA",
|
||||
status = "ZAKOŃCZONA",
|
||||
driverStatus = "ZAPLANOWANA",
|
||||
operationalStatus = "ZAKOŃCZONA",
|
||||
distanceKm = 91.4,
|
||||
notes = null,
|
||||
truck = null,
|
||||
@@ -29,6 +31,7 @@ class RouteDisplayMapperTest {
|
||||
assertEquals("Instalacja Odpadowa Slask", display.destination)
|
||||
assertEquals("TPP-ODP-24", display.contractLabel)
|
||||
assertEquals("09:15", display.time)
|
||||
assertEquals("ZAPLANOWANA", display.status)
|
||||
assertEquals("91 km", display.distanceLabel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,8 +205,9 @@ class DriverUiRulesTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun allowsRetryActionOnlyForRetryableFailedUploads() {
|
||||
fun allowsRetryForRetryableAndLegacyCancelledUploads() {
|
||||
assertTrue(canRetryPhotoUpload("FAILED_RETRYABLE"))
|
||||
assertTrue(canRetryPhotoUpload("FAILED_PERMANENT", "Typ: błąd aplikacji\nKomunikat: Job was cancelled"))
|
||||
assertFalse(canRetryPhotoUpload("PENDING"))
|
||||
assertFalse(canRetryPhotoUpload("UPLOADING"))
|
||||
assertFalse(canRetryPhotoUpload("VERIFYING"))
|
||||
@@ -215,6 +216,37 @@ class DriverUiRulesTest {
|
||||
assertFalse(canRetryPhotoUpload("CANCELLED"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun permanentUploadDoesNotSatisfyRouteStageAndCanBeDiscarded() {
|
||||
val failed = upload(status = "FAILED_PERMANENT")
|
||||
val pending = upload(status = "PENDING").copy(clientRequestId = "pending-upload")
|
||||
|
||||
assertEquals(listOf("pending-upload"), routeStageUsableUploads(listOf(failed, pending)).map { it.clientRequestId })
|
||||
assertFalse(photoUploadCanSatisfyStage("FAILED_PERMANENT"))
|
||||
assertTrue(canDiscardLocalPhotoUpload("FAILED_PERMANENT", null))
|
||||
assertFalse(canDiscardLocalPhotoUpload("CONFIRMED", "42"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serverRefreshLeavesObsoleteRouteStageScreen() {
|
||||
assertEquals(
|
||||
DriverScreen.Detail,
|
||||
routeScreenAfterServerRefresh(DriverScreen.StartRoute, route(status = "W TRAKCIE")),
|
||||
)
|
||||
assertEquals(
|
||||
DriverScreen.Detail,
|
||||
routeScreenAfterServerRefresh(DriverScreen.LoadingWeight, route(status = "ZAKOŃCZONA")),
|
||||
)
|
||||
assertEquals(
|
||||
DriverScreen.Detail,
|
||||
routeScreenAfterServerRefresh(DriverScreen.FinishRoute, route(status = "ZAKOŃCZONA")),
|
||||
)
|
||||
assertEquals(
|
||||
DriverScreen.StartRoute,
|
||||
routeScreenAfterServerRefresh(DriverScreen.StartRoute, route(status = "ZAPLANOWANA")),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun showsFailureDetailsOnlyForFailedUploadsWithErrorMessage() {
|
||||
assertTrue(canShowPhotoUploadFailureDetails("FAILED_RETRYABLE", "Typ: błąd serwera"))
|
||||
@@ -365,18 +397,62 @@ class DriverUiRulesTest {
|
||||
assertEquals(RouteLifecycleAction.Start, action?.action)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun driverFlowIgnoresOperationalStatusFromGeofence() {
|
||||
val route = route(
|
||||
status = "ZAKOŃCZONA",
|
||||
driverStatus = "ZAPLANOWANA",
|
||||
operationalStatus = "ZAKOŃCZONA",
|
||||
)
|
||||
|
||||
val steps = routeFlowSteps(route, emptyList())
|
||||
|
||||
assertTrue(canStartRouteFromDriverApp(route, "2026-06-30", today))
|
||||
assertEquals(RouteFlowStepState.Todo, steps[0].state)
|
||||
assertEquals(RouteFlowStepState.Todo, steps[1].state)
|
||||
assertEquals(RouteFlowStepState.Todo, steps[2].state)
|
||||
assertEquals(DriverScreen.StartRoute, routeScreenAfterServerRefresh(DriverScreen.StartRoute, route))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun plannedRouteWithLoadingPhotoShowsOnlyPhotoStepComplete() {
|
||||
val route = route(status = "ZAPLANOWANA", photos = listOf(photo("loading-1", stage = "loading")))
|
||||
|
||||
val steps = routeFlowSteps(route, emptyList())
|
||||
|
||||
assertEquals(RouteFlowStepState.LocalComplete, steps[0].state)
|
||||
assertEquals(RouteFlowStepState.ServerSaved, steps[0].state)
|
||||
assertEquals("zapisane na serwerze", steps[0].stateLabel)
|
||||
assertEquals(RouteFlowStepState.Todo, steps[1].state)
|
||||
assertEquals(RouteFlowStepState.Todo, steps[2].state)
|
||||
assertEquals(RouteFlowStepState.Todo, steps[3].state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun freshInstallLabelsBackendLoadingDocumentationAsServerData() {
|
||||
val route = route(
|
||||
status = "ZAPLANOWANA",
|
||||
loadingWeight = 23.76,
|
||||
photos = listOf(photo("loading-1", stage = "loading")),
|
||||
)
|
||||
|
||||
val steps = routeFlowSteps(route, emptyList())
|
||||
|
||||
assertEquals(RouteFlowStepState.ServerSaved, steps[0].state)
|
||||
assertEquals("zapisane na serwerze", steps[0].stateLabel)
|
||||
assertEquals(RouteFlowStepState.ServerSaved, steps[1].state)
|
||||
assertEquals("zapisane na serwerze", steps[1].stateLabel)
|
||||
assertEquals(RouteFlowStepState.Todo, steps[2].state)
|
||||
assertEquals(RouteFlowStepState.Todo, steps[3].state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serverWeightDoesNotMarkMissingLoadingPhotoAsComplete() {
|
||||
val steps = routeFlowSteps(route(status = "ZAPLANOWANA", loadingWeight = 23.76), emptyList())
|
||||
|
||||
assertEquals(RouteFlowStepState.Todo, steps[0].state)
|
||||
assertEquals(RouteFlowStepState.ServerSaved, steps[1].state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun activeRouteShowsTransitStateAndFinishAction() {
|
||||
val route = route(status = "W TRAKCIE", loadingWeight = 12.5)
|
||||
@@ -386,7 +462,8 @@ class DriverUiRulesTest {
|
||||
|
||||
assertEquals(RouteFlowStepState.Confirmed, steps[0].state)
|
||||
assertEquals(RouteFlowStepState.Confirmed, steps[1].state)
|
||||
assertEquals(RouteFlowStepState.LocalComplete, steps[2].state)
|
||||
assertEquals(RouteFlowStepState.InProgress, steps[2].state)
|
||||
assertEquals("w trakcie", steps[2].stateLabel)
|
||||
assertEquals(RouteFlowStepState.Todo, steps[3].state)
|
||||
assertEquals("Rozładuj", action?.label)
|
||||
assertEquals(RouteLifecycleAction.Finish, action?.action)
|
||||
@@ -527,6 +604,8 @@ class DriverUiRulesTest {
|
||||
|
||||
private fun route(
|
||||
status: String = "ZAPLANOWANA",
|
||||
driverStatus: String? = null,
|
||||
operationalStatus: String? = null,
|
||||
loadingWeight: Double? = null,
|
||||
unloadingWeight: Double? = null,
|
||||
photos: List<RoutePhotoDto> = emptyList(),
|
||||
@@ -545,6 +624,8 @@ class DriverUiRulesTest {
|
||||
contractCode = "TPP-1",
|
||||
relationLabel = "Baza -> Instalacja",
|
||||
status = status,
|
||||
driverStatus = driverStatus,
|
||||
operationalStatus = operationalStatus,
|
||||
loadingWeight = loadingWeight,
|
||||
unloadingWeight = unloadingWeight,
|
||||
distanceKm = 1.0,
|
||||
|
||||
Reference in New Issue
Block a user