Android: S-22's unpaired server from AppBuildInfo.releaseServerUrl (D-122)
AppBuildInfo gains releaseServerUrl, which :app fills from RELEASE_SERVER_URL (https://<kvitt.publicHost>:8443, D-121). AboutRepositoryImpl shows it while no server is paired, in place of DataModule.DEFAULT_SERVER_URL, which is deleted: :data no longer names a host. The fakes' AboutState uses the fixture https://kvitt.example.org:8443; 30 S-21/S-22 goldens re-recorded (only the host changed; oxipng -o 4 --strip safe). New test: unpaired About shows the release server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
@@ -6,11 +6,12 @@ import dagger.hilt.InstallIn
|
|||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import se.hagstedt.kvitt.BuildConfig
|
import se.hagstedt.kvitt.BuildConfig
|
||||||
import se.hagstedt.kvitt.data.di.AppBuildInfo
|
import se.hagstedt.kvitt.data.di.AppBuildInfo
|
||||||
|
import se.hagstedt.kvitt.link.RELEASE_SERVER_URL
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Both variants: the app's version (§10.6) and build type for `:data`'s `DataModule`, which Hilt installs in debug and
|
* Both variants: the app's version (§10.6), build type and release server ([RELEASE_SERVER_URL], D-121, D-122) for
|
||||||
* release alike (it binds no `:core:model` interface, D-041).
|
* `:data`'s `DataModule`, which Hilt installs in debug and release alike (it binds no `:core:model` interface, D-041).
|
||||||
*/
|
*/
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
@@ -20,5 +21,6 @@ object AppBuildInfoModule {
|
|||||||
versionCode = BuildConfig.VERSION_CODE,
|
versionCode = BuildConfig.VERSION_CODE,
|
||||||
versionName = BuildConfig.VERSION_NAME,
|
versionName = BuildConfig.VERSION_NAME,
|
||||||
debug = BuildConfig.DEBUG,
|
debug = BuildConfig.DEBUG,
|
||||||
|
releaseServerUrl = RELEASE_SERVER_URL,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ object FakeScenarios {
|
|||||||
clockSkew = false,
|
clockSkew = false,
|
||||||
syncRunning = false,
|
syncRunning = false,
|
||||||
update = UpdateState.None,
|
update = UpdateState.None,
|
||||||
about = AboutState("dev", 1, "https://icehack.asuscomm.com:8443", null, null),
|
about = AboutState("dev", 1, "https://kvitt.example.org:8443", null, null),
|
||||||
online = true,
|
online = true,
|
||||||
today = "2026-10-17",
|
today = "2026-10-17",
|
||||||
now = "2026-10-17T06:00:00.000Z",
|
now = "2026-10-17T06:00:00.000Z",
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ object SettingsScenarios {
|
|||||||
devices = devices,
|
devices = devices,
|
||||||
invites = listOf(pendingDeviceInvite),
|
invites = listOf(pendingDeviceInvite),
|
||||||
lastSuccessAt = "2026-10-17T11:59:00.000Z",
|
lastSuccessAt = "2026-10-17T11:59:00.000Z",
|
||||||
about = AboutState("1.1", 11, "https://icehack.asuscomm.com:8443", "1.1.0", 3),
|
about = AboutState("1.1", 11, "https://kvitt.example.org:8443", "1.1.0", 3),
|
||||||
today = "2026-10-17",
|
today = "2026-10-17",
|
||||||
now = "2026-10-17T12:02:00.000Z",
|
now = "2026-10-17T12:02:00.000Z",
|
||||||
defaultAccountId = huset.id,
|
defaultAccountId = huset.id,
|
||||||
|
|||||||
@@ -87,8 +87,11 @@ import javax.inject.Provider
|
|||||||
import javax.inject.Qualifier
|
import javax.inject.Qualifier
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/** What `:app` provides: its version (§10.6) and build type (AC-2.8, AC-16.5). */
|
/**
|
||||||
data class AppBuildInfo(val versionCode: Int, val versionName: String, val debug: Boolean)
|
* What `:app` provides: its version (§10.6), build type (AC-2.8, AC-16.5) and release server `https://<host>:8443`
|
||||||
|
* (`RELEASE_SERVER_URL`, from the Gradle property `kvitt.publicHost`, D-121), which S-22 shows before pairing (D-122).
|
||||||
|
*/
|
||||||
|
data class AppBuildInfo(val versionCode: Int, val versionName: String, val debug: Boolean, val releaseServerUrl: String)
|
||||||
|
|
||||||
@Qualifier
|
@Qualifier
|
||||||
@Retention(AnnotationRetention.BINARY)
|
@Retention(AnnotationRetention.BINARY)
|
||||||
@@ -110,9 +113,6 @@ annotation class DataScope
|
|||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object DataModule {
|
object DataModule {
|
||||||
/** §8.5: release builds use https only (the app's network security config); the default server of AC-2.7. */
|
|
||||||
const val DEFAULT_SERVER_URL = "https://icehack.asuscomm.com:8443"
|
|
||||||
|
|
||||||
@Provides @Singleton @DataScope
|
@Provides @Singleton @DataScope
|
||||||
fun scope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
fun scope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
@@ -276,7 +276,7 @@ object DataModule {
|
|||||||
|
|
||||||
@Provides @Singleton
|
@Provides @Singleton
|
||||||
fun about(credentials: CredentialStore, session: SessionManager, api: KvittApi, gate: OnlineGate, build: AppBuildInfo): AboutRepositoryImpl =
|
fun about(credentials: CredentialStore, session: SessionManager, api: KvittApi, gate: OnlineGate, build: AppBuildInfo): AboutRepositoryImpl =
|
||||||
AboutRepositoryImpl(credentials, session.serverVersion, api, gate, build.versionName, build.versionCode, DEFAULT_SERVER_URL)
|
AboutRepositoryImpl(credentials, session.serverVersion, api, gate, build.versionName, build.versionCode, build.releaseServerUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The `:core:model` interfaces bound to `:data`. Not installed automatically: `:app` includes it in its release graph. */
|
/** The `:core:model` interfaces bound to `:data`. Not installed automatically: `:app` includes it in its release graph. */
|
||||||
|
|||||||
@@ -388,7 +388,8 @@ class AboutRepositoryImpl(
|
|||||||
private val gate: OnlineGate,
|
private val gate: OnlineGate,
|
||||||
private val versionName: String,
|
private val versionName: String,
|
||||||
private val versionCode: Int,
|
private val versionCode: Int,
|
||||||
private val defaultServerUrl: String,
|
/** Shown while no server is paired: `AppBuildInfo.releaseServerUrl`, `https://<kvitt.publicHost>:8443` (D-122). */
|
||||||
|
private val releaseServerUrl: String,
|
||||||
) : AboutRepository {
|
) : AboutRepository {
|
||||||
override fun about(): Flow<AboutState> {
|
override fun about(): Flow<AboutState> {
|
||||||
val health = flow {
|
val health = flow {
|
||||||
@@ -396,7 +397,7 @@ class AboutRepositoryImpl(
|
|||||||
emit((gate.run({ api.health() }) { it } as? OnlineResult.Ok)?.value)
|
emit((gate.run({ api.health() }) { it } as? OnlineResult.Ok)?.value)
|
||||||
}
|
}
|
||||||
return combine(credentials.record, serverVersion, health) { record, header, h ->
|
return combine(credentials.record, serverVersion, health) { record, header, h ->
|
||||||
AboutState(versionName, versionCode, record.baseUrl ?: defaultServerUrl, h?.serverVersion ?: header, h?.schemaVersion)
|
AboutState(versionName, versionCode, record.baseUrl ?: releaseServerUrl, h?.serverVersion ?: header, h?.schemaVersion)
|
||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ class RepositoriesTest {
|
|||||||
h.server.restHandler = { _, path ->
|
h.server.restHandler = { _, path ->
|
||||||
if (path == "/health") HttpStatusCode.OK to """{"status":"ok","server_version":"1.2.0","schema_version":3,"db_ok":true}""" else null
|
if (path == "/health") HttpStatusCode.OK to """{"status":"ok","server_version":"1.2.0","schema_version":3,"db_ok":true}""" else null
|
||||||
}
|
}
|
||||||
val repo = AboutRepositoryImpl(h.credentials, kotlinx.coroutines.flow.MutableStateFlow("1.1.0"), h.api, h.gate, "1.1", 11, "https://default.test")
|
val repo = AboutRepositoryImpl(h.credentials, kotlinx.coroutines.flow.MutableStateFlow("1.1.0"), h.api, h.gate, "1.1", 11, RELEASE_SERVER_URL)
|
||||||
val state = repo.about().first { it.schemaVersion != null }
|
val state = repo.about().first { it.schemaVersion != null }
|
||||||
assertEquals("1.2.0", state.serverVersion)
|
assertEquals("1.2.0", state.serverVersion)
|
||||||
assertEquals(3, state.schemaVersion)
|
assertEquals(3, state.schemaVersion)
|
||||||
@@ -387,7 +387,7 @@ class RepositoriesTest {
|
|||||||
fun about_offlineOrFailingHealth_fallsBackToTheHeaderVersion() = runSync {
|
fun about_offlineOrFailingHealth_fallsBackToTheHeaderVersion() = runSync {
|
||||||
val header = kotlinx.coroutines.flow.MutableStateFlow<String?>("1.1.0")
|
val header = kotlinx.coroutines.flow.MutableStateFlow<String?>("1.1.0")
|
||||||
h.monitor.state.value = false
|
h.monitor.state.value = false
|
||||||
val repo = AboutRepositoryImpl(h.credentials, header, h.api, h.gate, "1.1", 11, "https://default.test")
|
val repo = AboutRepositoryImpl(h.credentials, header, h.api, h.gate, "1.1", 11, RELEASE_SERVER_URL)
|
||||||
val offline = repo.about().first()
|
val offline = repo.about().first()
|
||||||
assertEquals("1.1.0", offline.serverVersion)
|
assertEquals("1.1.0", offline.serverVersion)
|
||||||
assertNull(offline.schemaVersion)
|
assertNull(offline.schemaVersion)
|
||||||
@@ -400,6 +400,14 @@ class RepositoriesTest {
|
|||||||
assertEquals("1.1.0", repo.about().first().serverVersion)
|
assertEquals("1.1.0", repo.about().first().serverVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun about_unpaired_showsTheReleaseServerOfTheBuild() = runSync {
|
||||||
|
h.credentials.clear()
|
||||||
|
h.monitor.state.value = false
|
||||||
|
val repo = AboutRepositoryImpl(h.credentials, kotlinx.coroutines.flow.MutableStateFlow<String?>(null), h.api, h.gate, "1.1", 11, RELEASE_SERVER_URL)
|
||||||
|
assertEquals(RELEASE_SERVER_URL, repo.about().first().serverUrl)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun categoryReorder_sendsOnePatchPerChangedSortOrder() = runSync {
|
fun categoryReorder_sendsOnePatchPerChangedSortOrder() = runSync {
|
||||||
val patched = mutableListOf<Pair<String, Int?>>()
|
val patched = mutableListOf<Pair<String, Int?>>()
|
||||||
@@ -519,5 +527,7 @@ class RepositoriesTest {
|
|||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val OLDER = "0192a003-0000-7000-8000-000000000002"
|
const val OLDER = "0192a003-0000-7000-8000-000000000002"
|
||||||
|
/** `AppBuildInfo.releaseServerUrl` of the tests: a fixed host, not `:app`'s `kvitt.publicHost` (D-122). */
|
||||||
|
const val RELEASE_SERVER_URL = "https://kvitt.example.org:8443"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |