From 9a12e3b3fadc7e137fc13334618dda352e9f9b46 Mon Sep 17 00:00:00 2001 From: Shayan SalehiRad Date: Sun, 24 May 2026 03:19:13 +0330 Subject: [PATCH] Polish runtime, scan, and accessibility states --- .../whitedns/client/model/WhiteDnsModels.kt | 18 ++++ .../client/proxy/WhiteDnsProxyEvents.kt | 5 ++ .../client/proxy/WhiteDnsProxyService.kt | 26 +++--- .../client/scan/WhiteDnsScanService.kt | 28 +++++- .../shop/whitedns/client/ui/WhiteDnsScreen.kt | 90 ++++++++++++------- .../whitedns/client/ui/WhiteDnsViewModel.kt | 41 ++++++++- .../whitedns/client/vpn/WhiteDnsVpnEvents.kt | 5 ++ .../whitedns/client/vpn/WhiteDnsVpnService.kt | 25 ++++-- .../client/model/WhiteDnsModelsTest.kt | 21 +++++ .../client/proxy/WhiteDnsProxyEventsTest.kt | 2 + .../client/scan/WhiteDnsScanServiceTest.kt | 25 ++++++ .../client/vpn/WhiteDnsVpnEventsTest.kt | 2 + 12 files changed, 232 insertions(+), 56 deletions(-) create mode 100644 app/src/test/java/shop/whitedns/client/scan/WhiteDnsScanServiceTest.kt diff --git a/app/src/main/java/shop/whitedns/client/model/WhiteDnsModels.kt b/app/src/main/java/shop/whitedns/client/model/WhiteDnsModels.kt index edcf968..3ac3064 100644 --- a/app/src/main/java/shop/whitedns/client/model/WhiteDnsModels.kt +++ b/app/src/main/java/shop/whitedns/client/model/WhiteDnsModels.kt @@ -526,6 +526,24 @@ data class WhiteDnsScanState( ) } + fun completeResumeWithoutRemainingResolvers(nowMillis: Long): WhiteDnsScanState { + val completedCount = totalResolvers.takeIf { it > 0 } + ?: maxOf(completedResolvers, validResolvers + rejectedResolvers) + val duration = if (startedAtMillis > 0L) { + (nowMillis - startedAtMillis).coerceAtLeast(0L) + } else { + durationMillis + } + return copy( + status = WhiteDnsScanStatus.Completed, + completedResolvers = completedCount, + updatedAtMillis = nowMillis, + durationMillis = duration, + message = "No remaining resolvers to resume; Scanner result is up to date", + workerFailures = emptyList(), + ) + } + val fraction: Float get() = if (totalResolvers > 0) { completedResolvers.coerceIn(0, totalResolvers).toFloat() / totalResolvers diff --git a/app/src/main/java/shop/whitedns/client/proxy/WhiteDnsProxyEvents.kt b/app/src/main/java/shop/whitedns/client/proxy/WhiteDnsProxyEvents.kt index 4240161..bcccd57 100644 --- a/app/src/main/java/shop/whitedns/client/proxy/WhiteDnsProxyEvents.kt +++ b/app/src/main/java/shop/whitedns/client/proxy/WhiteDnsProxyEvents.kt @@ -6,6 +6,7 @@ sealed class WhiteDnsProxyEvent { data class Log(val sessionId: String, val message: String) : WhiteDnsProxyEvent() data class Ready(val sessionId: String, val message: String) : WhiteDnsProxyEvent() data class Failed(val sessionId: String, val message: String) : WhiteDnsProxyEvent() + data class Stopped(val sessionId: String, val message: String) : WhiteDnsProxyEvent() } object WhiteDnsProxyEvents { @@ -31,6 +32,10 @@ object WhiteDnsProxyEvents { emit(WhiteDnsProxyEvent.Failed(sessionId, message)) } + fun stopped(sessionId: String, message: String) { + emit(WhiteDnsProxyEvent.Stopped(sessionId, message)) + } + private fun emit(event: WhiteDnsProxyEvent) { listeners.forEach { listener -> runCatching { listener(event) } diff --git a/app/src/main/java/shop/whitedns/client/proxy/WhiteDnsProxyService.kt b/app/src/main/java/shop/whitedns/client/proxy/WhiteDnsProxyService.kt index edcf4b5..eaa69db 100644 --- a/app/src/main/java/shop/whitedns/client/proxy/WhiteDnsProxyService.kt +++ b/app/src/main/java/shop/whitedns/client/proxy/WhiteDnsProxyService.kt @@ -71,12 +71,7 @@ class WhiteDnsProxyService : Service() { stopProxyRuntime() runtimeReady = false lastTrafficNotificationUpdateMillis = 0L - WhiteDnsRuntimeStateStore.markStopped( - context = applicationContext, - mode = WhiteDnsRuntimeStateStore.ModeProxy, - sessionId = currentSessionId, - message = "Proxy service stopped", - ) + reportStopped("Proxy service stopped") exitForeground() stopSelf() START_NOT_STICKY @@ -103,12 +98,7 @@ class WhiteDnsProxyService : Service() { stopProxyRuntime() runtimeReady = false lastTrafficNotificationUpdateMillis = 0L - WhiteDnsRuntimeStateStore.markStopped( - context = applicationContext, - mode = WhiteDnsRuntimeStateStore.ModeProxy, - sessionId = currentSessionId, - message = "Proxy service stopped", - ) + reportStopped("Proxy service stopped") exitForeground() serviceScope.cancel() super.onDestroy() @@ -479,6 +469,17 @@ class WhiteDnsProxyService : Service() { sendProxyEvent(BroadcastTypeFailed, message) } + private fun reportStopped(message: String) { + WhiteDnsRuntimeStateStore.markStopped( + context = applicationContext, + mode = WhiteDnsRuntimeStateStore.ModeProxy, + sessionId = currentSessionId, + message = message, + ) + WhiteDnsProxyEvents.stopped(currentSessionId, message) + sendProxyEvent(BroadcastTypeStopped, message) + } + private fun reportReady(message: String) { Log.i(Tag, message) WhiteDnsProxyEvents.ready(currentSessionId, message) @@ -504,6 +505,7 @@ class WhiteDnsProxyService : Service() { const val BroadcastTypeLog = "log" const val BroadcastTypeReady = "ready" const val BroadcastTypeFailed = "failed" + const val BroadcastTypeStopped = "stopped" private const val ActionStart = "shop.whitedns.client.proxy.START" private const val ActionStop = "shop.whitedns.client.proxy.STOP" private const val ExtraSessionId = "shop.whitedns.client.proxy.extra.SESSION_ID" diff --git a/app/src/main/java/shop/whitedns/client/scan/WhiteDnsScanService.kt b/app/src/main/java/shop/whitedns/client/scan/WhiteDnsScanService.kt index f73cd70..bb3d2f6 100644 --- a/app/src/main/java/shop/whitedns/client/scan/WhiteDnsScanService.kt +++ b/app/src/main/java/shop/whitedns/client/scan/WhiteDnsScanService.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.json.JSONArray @@ -42,6 +43,14 @@ import shop.whitedns.client.runtime.parseStormDnsConnectionProgressLine import shop.whitedns.client.storm.StormDnsBinaryInstaller import shop.whitedns.client.storm.StormDnsConfigRenderer +internal fun shouldPublishScanHeartbeat( + nowMillis: Long, + lastHeartbeatMillis: Long, + heartbeatIntervalMillis: Long, +): Boolean { + return heartbeatIntervalMillis > 0L && nowMillis - lastHeartbeatMillis >= heartbeatIntervalMillis +} + class WhiteDnsScanService : Service() { private var foregroundStarted = false @@ -164,6 +173,7 @@ class WhiteDnsScanService : Service() { val totalResolverCount = maxOf(requestedTotalResolvers, pendingResolverCount + initialProcessedCount) val startedAtMillis = System.currentTimeMillis() var lastAggregatePublishMillis = 0L + var lastHeartbeatPublishMillis = startedAtMillis fun aggregateState(status: String, message: String): WhiteDnsScanState { val completed = (initialProcessedCount + workerStats.sumOf { it.completed }) @@ -261,7 +271,22 @@ class WhiteDnsScanService : Service() { } } - val results = jobs.awaitAll() + val heartbeatJob = launch(Dispatchers.IO) { + while (isActive && !stopRequested) { + delay(ScanHeartbeatIntervalMillis) + val now = System.currentTimeMillis() + if (shouldPublishScanHeartbeat(now, lastHeartbeatPublishMillis, ScanHeartbeatIntervalMillis)) { + lastHeartbeatPublishMillis = now + publishAggregate(WhiteDnsScanStatus.Running, "Scanning", forcePublish = true) + } + } + } + + val results = try { + jobs.awaitAll() + } finally { + heartbeatJob.cancelAndJoin() + } val successfulWorkers = results.count { it } val finalStatus = if (successfulWorkers == workerInputs.size && workerFailures.isEmpty()) { WhiteDnsScanStatus.Completed @@ -748,6 +773,7 @@ class WhiteDnsScanService : Service() { private const val MaxWorkerFailureOutputLines = 6 private const val MaxWorkerFailureOutputChars = 180 private const val ScanUiPublishMinIntervalMillis = 750L + private const val ScanHeartbeatIntervalMillis = 10_000L private val AnsiEscapeRegex = Regex("${27.toChar()}\\[[;?0-9]*[ -/]*[@-~]") fun start( diff --git a/app/src/main/java/shop/whitedns/client/ui/WhiteDnsScreen.kt b/app/src/main/java/shop/whitedns/client/ui/WhiteDnsScreen.kt index 9b8f0ff..c38f8fc 100644 --- a/app/src/main/java/shop/whitedns/client/ui/WhiteDnsScreen.kt +++ b/app/src/main/java/shop/whitedns/client/ui/WhiteDnsScreen.kt @@ -53,6 +53,7 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn @@ -1429,24 +1430,33 @@ private fun SettingProfileGuideButton( onClick: () -> Unit, ) { val haptic = rememberHapticFeedback() + val buttonDescription = WhiteDnsL10n.cdSettingGuide Box( modifier = Modifier - .size(36.dp) + .sizeIn(minWidth = 48.dp, minHeight = 48.dp) .clip(CircleShape) - .background(WhiteDnsPalette.AccentSurface) - .border(1.5.dp, WhiteDnsPalette.Accent.copy(alpha = 0.26f), CircleShape) - .clickable { + .clickable(role = Role.Button) { haptic.performLight() onClick() - }, + } + .semantics { this.contentDescription = buttonDescription }, contentAlignment = Alignment.Center, ) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.HelpOutline, - contentDescription = WhiteDnsL10n.cdSettingGuide, - tint = WhiteDnsPalette.AccentText, - modifier = Modifier.size(20.dp), - ) + Box( + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .background(WhiteDnsPalette.AccentSurface) + .border(1.5.dp, WhiteDnsPalette.Accent.copy(alpha = 0.26f), CircleShape), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.HelpOutline, + contentDescription = null, + tint = WhiteDnsPalette.AccentText, + modifier = Modifier.size(20.dp), + ) + } } } @@ -1886,6 +1896,7 @@ private fun ScanInfoNotice( onDismiss: () -> Unit, ) { val haptic = rememberHapticFeedback() + val dismissDescription = WhiteDnsL10n.cdDismissScannerInfo Row( modifier = Modifier @@ -1934,17 +1945,18 @@ private fun ScanInfoNotice( } Box( modifier = Modifier - .size(28.dp) + .sizeIn(minWidth = 48.dp, minHeight = 48.dp) .clip(CircleShape) - .clickable { + .clickable(role = Role.Button) { haptic.performLight() onDismiss() - }, + } + .semantics { this.contentDescription = dismissDescription }, contentAlignment = Alignment.Center, ) { Icon( imageVector = Icons.Rounded.Close, - contentDescription = WhiteDnsL10n.cdDismissScannerInfo, + contentDescription = null, tint = WhiteDnsPalette.AccentText, modifier = Modifier.size(16.dp), ) @@ -2638,7 +2650,7 @@ private fun HomeSelectorCard( } Icon( imageVector = Icons.Rounded.KeyboardArrowDown, - contentDescription = stringResource(R.string.cd_dropdown_advanced_settings), + contentDescription = null, tint = if (enabled) WhiteDnsPalette.Muted else WhiteDnsPalette.Disabled, modifier = Modifier.size(18.dp), ) @@ -5845,6 +5857,7 @@ private fun ProfileIconButton( modifier: Modifier = Modifier, ) { val haptic = rememberHapticFeedback() + val buttonDescription = contentDescription val background = when { !enabled -> WhiteDnsPalette.SurfaceAlt emphasized -> WhiteDnsPalette.Accent @@ -5863,22 +5876,30 @@ private fun ProfileIconButton( Box( modifier = modifier - .size(28.dp) - .clip(RoundedCornerShape(8.dp)) - .background(background) - .border(1.5.dp, border, RoundedCornerShape(8.dp)) - .clickable(enabled = enabled) { + .sizeIn(minWidth = 48.dp, minHeight = 48.dp) + .clip(RoundedCornerShape(12.dp)) + .clickable(enabled = enabled, role = Role.Button) { haptic.performMedium() onClick() - }, + } + .semantics { this.contentDescription = buttonDescription }, contentAlignment = Alignment.Center, ) { - Icon( - imageVector = icon, - contentDescription = contentDescription, - tint = iconColor, - modifier = Modifier.size(16.dp), - ) + Box( + modifier = Modifier + .size(28.dp) + .clip(RoundedCornerShape(8.dp)) + .background(background) + .border(1.5.dp, border, RoundedCornerShape(8.dp)), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = iconColor, + modifier = Modifier.size(16.dp), + ) + } } } @@ -5914,10 +5935,11 @@ private fun CompactActionButton( Box( modifier = modifier + .heightIn(min = 44.dp) .clip(RoundedCornerShape(9.dp)) .background(background) .border(1.5.dp, border, RoundedCornerShape(9.dp)) - .clickable(enabled = enabled) { + .clickable(enabled = enabled, role = Role.Button) { haptic.performMedium() onClick() } @@ -5927,11 +5949,14 @@ private fun CompactActionButton( Text( text = label, style = MaterialTheme.typography.bodyMedium.copy( - fontSize = 8.sp, + fontSize = 10.sp, + lineHeight = 12.sp, color = textColor, fontWeight = FontWeight.Medium, - letterSpacing = 0.9.sp, + letterSpacing = 0.sp, ), + maxLines = 2, + overflow = TextOverflow.Ellipsis, ) } } @@ -9438,6 +9463,7 @@ private fun WhiteDnsTextField( onValueChange = onValueChange, modifier = Modifier .fillMaxWidth() + .semantics { contentDescription = label } .onFocusChanged { focused = it.isFocused onFocusChange(it.isFocused) @@ -9597,7 +9623,7 @@ private fun WhiteDnsDropdownField( ) Icon( imageVector = Icons.Rounded.KeyboardArrowDown, - contentDescription = stringResource(R.string.cd_dropdown_advanced_settings), + contentDescription = null, tint = when { !enabled -> WhiteDnsPalette.Disabled expanded -> WhiteDnsPalette.Accent diff --git a/app/src/main/java/shop/whitedns/client/ui/WhiteDnsViewModel.kt b/app/src/main/java/shop/whitedns/client/ui/WhiteDnsViewModel.kt index 547e998..0955dae 100644 --- a/app/src/main/java/shop/whitedns/client/ui/WhiteDnsViewModel.kt +++ b/app/src/main/java/shop/whitedns/client/ui/WhiteDnsViewModel.kt @@ -156,6 +156,11 @@ class WhiteDnsViewModel( is WhiteDnsProxyEvent.Log -> handleRuntimeLog(event.sessionId, event.message) is WhiteDnsProxyEvent.Ready -> handleRuntimeReady(event.sessionId, event.message, expectedConnectionMode = "proxy") is WhiteDnsProxyEvent.Failed -> handleProxyFailure(event.sessionId, event.message) + is WhiteDnsProxyEvent.Stopped -> handleRuntimeStopped( + event.sessionId, + event.message, + expectedConnectionMode = WhiteDnsRuntimeStateStore.ModeProxy, + ) } } private val vpnEventListener: (WhiteDnsVpnEvent) -> Unit = { event -> @@ -163,6 +168,11 @@ class WhiteDnsViewModel( is WhiteDnsVpnEvent.Log -> handleRuntimeLog(event.sessionId, event.message) is WhiteDnsVpnEvent.Ready -> handleRuntimeReady(event.sessionId, event.message, expectedConnectionMode = "vpn") is WhiteDnsVpnEvent.Failed -> handleVpnFailure(event.sessionId, event.message) + is WhiteDnsVpnEvent.Stopped -> handleRuntimeStopped( + event.sessionId, + event.message, + expectedConnectionMode = WhiteDnsRuntimeStateStore.ModeVpn, + ) } } private val proxyBroadcastReceiver = object : BroadcastReceiver() { @@ -176,6 +186,11 @@ class WhiteDnsViewModel( WhiteDnsProxyService.BroadcastTypeLog -> handleRuntimeLog(sessionId, message) WhiteDnsProxyService.BroadcastTypeReady -> handleRuntimeReady(sessionId, message, expectedConnectionMode = "proxy") WhiteDnsProxyService.BroadcastTypeFailed -> handleProxyFailure(sessionId, message) + WhiteDnsProxyService.BroadcastTypeStopped -> handleRuntimeStopped( + sessionId, + message, + expectedConnectionMode = WhiteDnsRuntimeStateStore.ModeProxy, + ) } } } @@ -190,6 +205,11 @@ class WhiteDnsViewModel( WhiteDnsVpnService.BroadcastTypeLog -> handleRuntimeLog(sessionId, message) WhiteDnsVpnService.BroadcastTypeReady -> handleRuntimeReady(sessionId, message, expectedConnectionMode = "vpn") WhiteDnsVpnService.BroadcastTypeFailed -> handleVpnFailure(sessionId, message) + WhiteDnsVpnService.BroadcastTypeStopped -> handleRuntimeStopped( + sessionId, + message, + expectedConnectionMode = WhiteDnsRuntimeStateStore.ModeVpn, + ) } } } @@ -1916,9 +1936,8 @@ class WhiteDnsViewModel( return@launch } if (remainingResolverCount == 0) { - val updatedState = previousState.copy( - updatedAtMillis = System.currentTimeMillis(), - message = "No remaining resolvers to resume", + val updatedState = previousState.completeResumeWithoutRemainingResolvers( + nowMillis = System.currentTimeMillis(), ) WhiteDnsScanStateStore.write(appContext, updatedState) uiState = uiState.copy(scanState = updatedState) @@ -2161,6 +2180,22 @@ class WhiteDnsViewModel( } } + private fun handleRuntimeStopped( + sessionId: String, + message: String, + expectedConnectionMode: String, + ) { + if (isStaleRuntimeEvent(sessionId)) { + return + } + viewModelScope.launch(Dispatchers.Main.immediate) { + if (!shouldHandleRuntimeEvent(expectedConnectionMode)) { + return@launch + } + markRuntimeDisconnected(message) + } + } + private fun shouldHandleRuntimeEvent(expectedConnectionMode: String): Boolean { return uiState.connectionStatus != ConnectionStatus.DISCONNECTED && uiState.settings.resolve().connectionMode == expectedConnectionMode diff --git a/app/src/main/java/shop/whitedns/client/vpn/WhiteDnsVpnEvents.kt b/app/src/main/java/shop/whitedns/client/vpn/WhiteDnsVpnEvents.kt index f0f0b38..36fad1c 100644 --- a/app/src/main/java/shop/whitedns/client/vpn/WhiteDnsVpnEvents.kt +++ b/app/src/main/java/shop/whitedns/client/vpn/WhiteDnsVpnEvents.kt @@ -6,6 +6,7 @@ sealed class WhiteDnsVpnEvent { data class Log(val sessionId: String, val message: String) : WhiteDnsVpnEvent() data class Ready(val sessionId: String, val message: String) : WhiteDnsVpnEvent() data class Failed(val sessionId: String, val message: String) : WhiteDnsVpnEvent() + data class Stopped(val sessionId: String, val message: String) : WhiteDnsVpnEvent() } object WhiteDnsVpnEvents { @@ -31,6 +32,10 @@ object WhiteDnsVpnEvents { emit(WhiteDnsVpnEvent.Failed(sessionId, message)) } + fun stopped(sessionId: String, message: String) { + emit(WhiteDnsVpnEvent.Stopped(sessionId, message)) + } + private fun emit(event: WhiteDnsVpnEvent) { listeners.forEach { listener -> runCatching { listener(event) } diff --git a/app/src/main/java/shop/whitedns/client/vpn/WhiteDnsVpnService.kt b/app/src/main/java/shop/whitedns/client/vpn/WhiteDnsVpnService.kt index 4befbbd..f45707a 100644 --- a/app/src/main/java/shop/whitedns/client/vpn/WhiteDnsVpnService.kt +++ b/app/src/main/java/shop/whitedns/client/vpn/WhiteDnsVpnService.kt @@ -209,7 +209,7 @@ class WhiteDnsVpnService : VpnService() { } val serverProfile = launchRequest.serverProfile - stopVpn() + stopVpn(notifyStopped = false) WhiteDnsProxyService.stop(applicationContext) waitForLocalPortToClose(resolvedSettings.listenPort) stopping = false @@ -393,7 +393,7 @@ class WhiteDnsVpnService : VpnService() { } } - private fun stopVpn() { + private fun stopVpn(notifyStopped: Boolean = true) { stopping = true runtimeReady = false lastTrafficNotificationUpdateMillis = 0L @@ -421,12 +421,15 @@ class WhiteDnsVpnService : VpnService() { }.onFailure { error -> Log.w(Tag, "Failed to stop StormDNS", error) } - WhiteDnsRuntimeStateStore.markStopped( - context = applicationContext, - mode = WhiteDnsRuntimeStateStore.ModeVpn, - sessionId = currentSessionId, - message = "VPN service stopped", - ) + if (notifyStopped) { + WhiteDnsRuntimeStateStore.markStopped( + context = applicationContext, + mode = WhiteDnsRuntimeStateStore.ModeVpn, + sessionId = currentSessionId, + message = "VPN service stopped", + ) + reportStopped("VPN service stopped") + } } private fun startTrafficKeepalive(resolvedSettings: ResolvedWhiteDnsSettings) { @@ -608,6 +611,11 @@ class WhiteDnsVpnService : VpnService() { sendVpnEvent(BroadcastTypeFailed, message) } + private fun reportStopped(message: String) { + WhiteDnsVpnEvents.stopped(currentSessionId, message) + sendVpnEvent(BroadcastTypeStopped, message) + } + private fun reportReady(message: String) { Log.i(Tag, message) WhiteDnsVpnEvents.ready(currentSessionId, message) @@ -633,6 +641,7 @@ class WhiteDnsVpnService : VpnService() { const val BroadcastTypeLog = "log" const val BroadcastTypeReady = "ready" const val BroadcastTypeFailed = "failed" + const val BroadcastTypeStopped = "stopped" private const val ActionStart = "shop.whitedns.client.vpn.START" private const val ActionStop = "shop.whitedns.client.vpn.STOP" private const val ExtraSessionId = "shop.whitedns.client.vpn.extra.SESSION_ID" diff --git a/app/src/test/java/shop/whitedns/client/model/WhiteDnsModelsTest.kt b/app/src/test/java/shop/whitedns/client/model/WhiteDnsModelsTest.kt index cb5b9c8..6d47a14 100644 --- a/app/src/test/java/shop/whitedns/client/model/WhiteDnsModelsTest.kt +++ b/app/src/test/java/shop/whitedns/client/model/WhiteDnsModelsTest.kt @@ -1153,6 +1153,27 @@ class WhiteDnsModelsTest { assertEquals(freshState, recovered) } + @Test + fun completeResumeWithoutRemainingResolversMarksScanCompleted() { + val completed = WhiteDnsScanState( + sessionId = "scan-3", + status = WhiteDnsScanStatus.Stopped, + totalResolvers = 4, + completedResolvers = 2, + validResolvers = 1, + rejectedResolvers = 1, + startedAtMillis = 1_000L, + workerFailures = listOf("old worker failure"), + ).completeResumeWithoutRemainingResolvers(nowMillis = 6_000L) + + assertEquals(WhiteDnsScanStatus.Completed, completed.status) + assertEquals(4, completed.completedResolvers) + assertEquals("No remaining resolvers to resume; Scanner result is up to date", completed.message) + assertEquals(6_000L, completed.updatedAtMillis) + assertEquals(5_000L, completed.durationMillis) + assertEquals(emptyList(), completed.workerFailures) + } + private fun decodeStormDnsProfilePayload(link: String): String { val payload = link.removePrefix("stormdns://") val paddedPayload = payload.padEnd(payload.length + ((4 - payload.length % 4) % 4), '=') diff --git a/app/src/test/java/shop/whitedns/client/proxy/WhiteDnsProxyEventsTest.kt b/app/src/test/java/shop/whitedns/client/proxy/WhiteDnsProxyEventsTest.kt index 6cb2745..4ecbee5 100644 --- a/app/src/test/java/shop/whitedns/client/proxy/WhiteDnsProxyEventsTest.kt +++ b/app/src/test/java/shop/whitedns/client/proxy/WhiteDnsProxyEventsTest.kt @@ -16,6 +16,7 @@ class WhiteDnsProxyEventsTest { WhiteDnsProxyEvents.log("session-a", "log") WhiteDnsProxyEvents.ready("session-a", "ready") WhiteDnsProxyEvents.failed("session-a", "failed") + WhiteDnsProxyEvents.stopped("session-a", "stopped") } finally { WhiteDnsProxyEvents.removeListener(listener) } @@ -25,6 +26,7 @@ class WhiteDnsProxyEventsTest { WhiteDnsProxyEvent.Log("session-a", "log"), WhiteDnsProxyEvent.Ready("session-a", "ready"), WhiteDnsProxyEvent.Failed("session-a", "failed"), + WhiteDnsProxyEvent.Stopped("session-a", "stopped"), ), receivedEvents, ) diff --git a/app/src/test/java/shop/whitedns/client/scan/WhiteDnsScanServiceTest.kt b/app/src/test/java/shop/whitedns/client/scan/WhiteDnsScanServiceTest.kt new file mode 100644 index 0000000..1ace594 --- /dev/null +++ b/app/src/test/java/shop/whitedns/client/scan/WhiteDnsScanServiceTest.kt @@ -0,0 +1,25 @@ +package shop.whitedns.client.scan + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class WhiteDnsScanServiceTest { + @Test + fun shouldPublishScanHeartbeatAfterInterval() { + assertFalse( + shouldPublishScanHeartbeat( + nowMillis = 14_999L, + lastHeartbeatMillis = 0L, + heartbeatIntervalMillis = 15_000L, + ), + ) + assertTrue( + shouldPublishScanHeartbeat( + nowMillis = 15_000L, + lastHeartbeatMillis = 0L, + heartbeatIntervalMillis = 15_000L, + ), + ) + } +} diff --git a/app/src/test/java/shop/whitedns/client/vpn/WhiteDnsVpnEventsTest.kt b/app/src/test/java/shop/whitedns/client/vpn/WhiteDnsVpnEventsTest.kt index be7636d..a73434f 100644 --- a/app/src/test/java/shop/whitedns/client/vpn/WhiteDnsVpnEventsTest.kt +++ b/app/src/test/java/shop/whitedns/client/vpn/WhiteDnsVpnEventsTest.kt @@ -16,6 +16,7 @@ class WhiteDnsVpnEventsTest { WhiteDnsVpnEvents.log("session-b", "log") WhiteDnsVpnEvents.ready("session-b", "ready") WhiteDnsVpnEvents.failed("session-b", "failed") + WhiteDnsVpnEvents.stopped("session-b", "stopped") } finally { WhiteDnsVpnEvents.removeListener(listener) } @@ -25,6 +26,7 @@ class WhiteDnsVpnEventsTest { WhiteDnsVpnEvent.Log("session-b", "log"), WhiteDnsVpnEvent.Ready("session-b", "ready"), WhiteDnsVpnEvent.Failed("session-b", "failed"), + WhiteDnsVpnEvent.Stopped("session-b", "stopped"), ), receivedEvents, )