feat: harden offline sync and add diagnostics
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
package pl.firmatpp.kierowca.data.sync
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class NetworkMonitorTest {
|
||||
@Test
|
||||
fun captivePortalIsNotTreatedAsOnline() {
|
||||
assertFalse(hasValidatedInternetCapabilities(hasInternet = true, hasValidated = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validatedInternetIsOnline() {
|
||||
assertTrue(hasValidatedInternetCapabilities(hasInternet = true, hasValidated = true))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package pl.firmatpp.kierowca.diagnostics
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class DiagnosticSnapshotProviderTest {
|
||||
@Test
|
||||
fun secureIdentifierSummaryDoesNotExposeWholeToken() {
|
||||
val token = "secret-session-token-123456"
|
||||
|
||||
val summary = secureIdentifierSummary(token)
|
||||
|
||||
assertFalse(summary.contains(token))
|
||||
assertTrue(summary.contains("długość ${token.length}"))
|
||||
assertTrue(summary.contains("końcówka …3456"))
|
||||
assertTrue(summary.contains("SHA-256"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun copiedReportExplainsThatCredentialsAreRedacted() {
|
||||
val snapshot = DiagnosticSnapshot(
|
||||
generatedAtEpochMillis = 1_700_000_000_000,
|
||||
sections = listOf(
|
||||
DiagnosticSection(
|
||||
title = "Sesja",
|
||||
entries = listOf(DiagnosticEntry("Token", secureIdentifierSummary("top-secret"))),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val report = snapshot.asPlainText()
|
||||
|
||||
assertTrue(report.contains("[Sesja]"))
|
||||
assertTrue(report.contains("Token: obecny"))
|
||||
assertTrue(report.contains("nie zawiera danych pozwalających przejąć sesję"))
|
||||
assertFalse(report.contains("top-secret"))
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,23 @@ class DriverLiveSyncClientTest {
|
||||
assertEquals(2, factory.sockets.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun backgroundClosesSocketAndForegroundReconnects() = runTest {
|
||||
val factory = FakeWebSocketFactory()
|
||||
val client = liveClient(factory = factory, scope = backgroundScope)
|
||||
|
||||
client.start("driver-1", config)
|
||||
client.setForeground(false)
|
||||
|
||||
assertTrue(factory.sockets.single().closed)
|
||||
assertEquals(LiveSyncConnectionState.Stopped, client.connectionState.value)
|
||||
|
||||
client.setForeground(true)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(2, factory.sockets.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun retriesWhenPrivateChannelSubscriptionAuthFails() = runTest {
|
||||
val gateway = FakeLiveSyncGateway(authFailure = RuntimeException("offline"))
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package pl.firmatpp.kierowca.sync
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class DriverRuntimeSyncStateTest {
|
||||
@After
|
||||
fun reset() {
|
||||
DriverRuntimeSyncState.foreground = false
|
||||
DriverRuntimeSyncState.realtimeConnected = false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun websocketSuppressesDuplicateFcmSyncOnlyWhenForegroundAndConnected() {
|
||||
DriverRuntimeSyncState.foreground = true
|
||||
DriverRuntimeSyncState.realtimeConnected = true
|
||||
assertTrue(DriverRuntimeSyncState.websocketOwnsForegroundHints())
|
||||
|
||||
DriverRuntimeSyncState.realtimeConnected = false
|
||||
assertFalse(DriverRuntimeSyncState.websocketOwnsForegroundHints())
|
||||
}
|
||||
}
|
||||
@@ -261,6 +261,32 @@ class DriverUiRulesTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun healthyOnlineStateDoesNotShowOfflineBanner() {
|
||||
assertEquals(
|
||||
null,
|
||||
connectionStatusBannerMessage(
|
||||
isOnline = true,
|
||||
isStale = false,
|
||||
serverConnectionState = ServerConnectionState.Reachable,
|
||||
showRealtimeReconnecting = false,
|
||||
syncLabel = "12:30",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serverFailureAndRealtimeReconnectAreNotCalledOffline() {
|
||||
assertEquals(
|
||||
"Internet działa, ale serwer jest chwilowo niedostępny. Pokazujemy dane z 12:30.",
|
||||
connectionStatusBannerMessage(true, true, ServerConnectionState.Degraded, false, "12:30"),
|
||||
)
|
||||
assertEquals(
|
||||
"Ponowne łączenie. Dane są uzgadniane z serwerem.",
|
||||
connectionStatusBannerMessage(true, false, ServerConnectionState.Reachable, true, "12:30"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun explainsStaleCachedDataWhenOnline() {
|
||||
assertEquals(
|
||||
@@ -321,7 +347,7 @@ class DriverUiRulesTest {
|
||||
val steps = routeFlowSteps(route, emptyList())
|
||||
val action = routeLifecyclePrimaryAction(route, "2026-06-30", today)
|
||||
|
||||
assertEquals(listOf("Zdjęcia załadunku", "Waga załadunku", "W trasie", "Rozładunek"), steps.map { it.title })
|
||||
assertEquals(listOf("Zdjęcia załadunku", "Tonaż załadunku", "W trasie", "Rozładunek"), steps.map { it.title })
|
||||
assertEquals(RouteFlowStepState.Todo, steps[0].state)
|
||||
assertEquals(RouteFlowStepState.Todo, steps[1].state)
|
||||
assertEquals(RouteFlowStepState.Todo, steps[2].state)
|
||||
@@ -393,12 +419,22 @@ class DriverUiRulesTest {
|
||||
assertEquals(RouteFlowStepState.NeedsAttention, steps[3].state)
|
||||
assertEquals("Kurs wymaga obsługi. Dane wpisane w telefonie zostały zachowane.", callout?.text)
|
||||
assertTrue(callout?.isConflict ?: false)
|
||||
assertFalse(callout?.canCorrect ?: true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun permanentlyRejectedRouteActionOffersDataCorrection() {
|
||||
val callout = routeSyncCallout(
|
||||
listOf(action(RouteActionType.Start, RouteActionStatus.FailedPermanent, "Brakuje wymaganych danych.")),
|
||||
)
|
||||
|
||||
assertTrue(callout?.canCorrect ?: false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun routeStageSubmitBlockerExplainsMissingRequirements() {
|
||||
assertEquals("Podaj wagę.", routeStageSubmitBlocker("", serverPhotoCount = 1, localUploadCount = 0))
|
||||
assertEquals("Waga musi być większa od zera.", routeStageSubmitBlocker("0", serverPhotoCount = 1, localUploadCount = 0))
|
||||
assertEquals("Podaj tonaż.", routeStageSubmitBlocker("", serverPhotoCount = 1, localUploadCount = 0))
|
||||
assertEquals("Tonaż musi być większy od zera.", routeStageSubmitBlocker("0", serverPhotoCount = 1, localUploadCount = 0))
|
||||
assertEquals("Dodaj co najmniej jedno zdjęcie etapu.", routeStageSubmitBlocker("12,5", serverPhotoCount = 0, localUploadCount = 0))
|
||||
assertEquals(null, routeStageSubmitBlocker("12,5", serverPhotoCount = 0, localUploadCount = 1))
|
||||
}
|
||||
@@ -415,9 +451,9 @@ class DriverUiRulesTest {
|
||||
|
||||
@Test
|
||||
fun loadingWeightStepRequiresPositiveWeightAndPreviousLoadingPhoto() {
|
||||
assertEquals("Podaj wagę.", loadingWeightSubmitBlocker("", serverPhotoCount = 1, localUploadCount = 0))
|
||||
assertEquals("Podaj poprawną wagę.", loadingWeightSubmitBlocker("abc", serverPhotoCount = 1, localUploadCount = 0))
|
||||
assertEquals("Waga musi być większa od zera.", loadingWeightSubmitBlocker("0", serverPhotoCount = 1, localUploadCount = 0))
|
||||
assertEquals("Podaj tonaż.", loadingWeightSubmitBlocker("", serverPhotoCount = 1, localUploadCount = 0))
|
||||
assertEquals("Podaj poprawny tonaż.", loadingWeightSubmitBlocker("abc", serverPhotoCount = 1, localUploadCount = 0))
|
||||
assertEquals("Tonaż musi być większy od zera.", loadingWeightSubmitBlocker("0", serverPhotoCount = 1, localUploadCount = 0))
|
||||
assertEquals(
|
||||
"Dodaj co najmniej jedno zdjęcie załadunku.",
|
||||
loadingWeightSubmitBlocker("12,5", serverPhotoCount = 0, localUploadCount = 0),
|
||||
@@ -425,6 +461,61 @@ class DriverUiRulesTest {
|
||||
assertEquals(null, loadingWeightSubmitBlocker("12,5", serverPhotoCount = 0, localUploadCount = 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun optionalStageInputsAreVisibleButDoNotBlockConfirmation() {
|
||||
assertEquals(
|
||||
null,
|
||||
routeStageSubmitBlocker(
|
||||
weightText = "",
|
||||
serverPhotoCount = 0,
|
||||
localUploadCount = 0,
|
||||
weightRequirement = RouteStageRequirement.Optional,
|
||||
photoRequirement = RouteStageRequirement.Optional,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
"Podaj poprawny tonaż.",
|
||||
routeStageSubmitBlocker(
|
||||
weightText = "abc",
|
||||
serverPhotoCount = 0,
|
||||
localUploadCount = 0,
|
||||
weightRequirement = RouteStageRequirement.Optional,
|
||||
photoRequirement = RouteStageRequirement.Optional,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun disabledStageInputsAreHiddenAndDoNotBlockConfirmation() {
|
||||
assertEquals(RouteStageRequirement.Required, normalizeRouteStageRequirement(null))
|
||||
assertEquals(RouteStageRequirement.Required, normalizeRouteStageRequirement("unknown"))
|
||||
assertFalse(routeStageRequirementIsVisible(RouteStageRequirement.Disabled))
|
||||
assertTrue(routeStageRequirementIsVisible(RouteStageRequirement.Optional))
|
||||
assertTrue(routeStageRequirementIsRequired(RouteStageRequirement.Required))
|
||||
assertEquals(
|
||||
null,
|
||||
routeStageSubmitBlocker(
|
||||
weightText = "invalid value is ignored",
|
||||
serverPhotoCount = 0,
|
||||
localUploadCount = 0,
|
||||
weightRequirement = RouteStageRequirement.Disabled,
|
||||
photoRequirement = RouteStageRequirement.Disabled,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun disabledLoadingStepsAreRemovedFromRouteFlow() {
|
||||
val steps = routeFlowSteps(
|
||||
route(status = "ZAPLANOWANA"),
|
||||
emptyList(),
|
||||
loadingPhotoRequirement = RouteStageRequirement.Disabled,
|
||||
loadingWeightRequirement = RouteStageRequirement.Disabled,
|
||||
)
|
||||
|
||||
assertEquals(listOf("W trasie", "Rozładunek"), steps.map { it.title })
|
||||
}
|
||||
|
||||
private fun route(
|
||||
status: String = "ZAPLANOWANA",
|
||||
loadingWeight: Double? = null,
|
||||
|
||||
@@ -3,6 +3,7 @@ package pl.firmatpp.kierowca.ui
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import java.io.IOException
|
||||
import pl.firmatpp.kierowca.data.model.DriverLeaveRequestDto
|
||||
import pl.firmatpp.kierowca.data.model.DriverRouteDto
|
||||
import pl.firmatpp.kierowca.data.upload.RouteActionEntity
|
||||
@@ -26,6 +27,21 @@ class DriverUiStateTest {
|
||||
assertEquals(AppThemeMode.Material3, state.themeMode)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun networkTimeoutDoesNotChangeValidatedNetworkState() {
|
||||
val state = DriverUiState(
|
||||
isOnline = true,
|
||||
serverConnectionState = ServerConnectionState.Reachable,
|
||||
lastServerSyncAtEpochMillis = 123L,
|
||||
)
|
||||
|
||||
val failed = state.withApiError(IOException("timeout"))
|
||||
|
||||
assertTrue(failed.isOnline)
|
||||
assertTrue(failed.isStale)
|
||||
assertEquals(ServerConnectionState.Degraded, failed.serverConnectionState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshingLeaveRequestsKeepsDetailScreenOpenAndUpdatesSelectedRequest() {
|
||||
val state = DriverUiState(
|
||||
|
||||
Reference in New Issue
Block a user