mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 14:32:28 +00:00
fix(simkl): refresh settings from activity changes
This commit is contained in:
parent
e020b9700a
commit
9798482fff
6 changed files with 114 additions and 7 deletions
|
|
@ -103,7 +103,7 @@ internal class SimklApiClient(
|
|||
url = buildSimklApiUrl(request.path, request.query),
|
||||
headers = simklRequestHeaders(
|
||||
accessToken = token,
|
||||
contentTypeJson = request.body.isNotEmpty(),
|
||||
contentTypeJson = request.method == SimklHttpMethod.POST,
|
||||
),
|
||||
body = request.body,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ data class SimklAuthUiState(
|
|||
internal data class SimklStoredAuthState(
|
||||
val username: String? = null,
|
||||
val accountId: Long? = null,
|
||||
val hasFetchedUserSettings: Boolean = false,
|
||||
val settingsActivityWatermark: String? = null,
|
||||
val tokenExpiresAtEpochMs: Long? = null,
|
||||
val pendingAuthorizationState: String? = null,
|
||||
val pendingAuthorizationStartedAtEpochMs: Long? = null,
|
||||
|
|
@ -41,6 +43,24 @@ internal data class SimklStoredAuthState(
|
|||
get() = !pendingAuthorizationState.isNullOrBlank()
|
||||
}
|
||||
|
||||
internal enum class SimklSettingsRefreshAction {
|
||||
NONE,
|
||||
RECORD_WATERMARK,
|
||||
FETCH,
|
||||
}
|
||||
|
||||
internal fun simklSettingsRefreshAction(
|
||||
state: SimklStoredAuthState,
|
||||
activityWatermark: String?,
|
||||
): SimklSettingsRefreshAction = when {
|
||||
activityWatermark.isNullOrBlank() -> SimklSettingsRefreshAction.NONE
|
||||
activityWatermark == state.settingsActivityWatermark -> SimklSettingsRefreshAction.NONE
|
||||
state.settingsActivityWatermark == null && state.hasFetchedUserSettings -> {
|
||||
SimklSettingsRefreshAction.RECORD_WATERMARK
|
||||
}
|
||||
else -> SimklSettingsRefreshAction.FETCH
|
||||
}
|
||||
|
||||
internal sealed interface SimklAuthCallback {
|
||||
data class AuthorizationCode(
|
||||
val code: String,
|
||||
|
|
|
|||
|
|
@ -187,7 +187,21 @@ object SimklAuthRepository : TrackingAuthProvider {
|
|||
|
||||
suspend fun refreshUserSettings(): String? {
|
||||
authorizedAccessToken() ?: return null
|
||||
return fetchAndStoreUserSettings()
|
||||
return if (fetchAndStoreUserSettings()) storedState.username else null
|
||||
}
|
||||
|
||||
internal suspend fun synchronizeUserSettings(activityWatermark: String?) {
|
||||
authorizedAccessToken() ?: return
|
||||
when (simklSettingsRefreshAction(storedState, activityWatermark)) {
|
||||
SimklSettingsRefreshAction.NONE -> Unit
|
||||
SimklSettingsRefreshAction.RECORD_WATERMARK -> {
|
||||
storedState = storedState.copy(settingsActivityWatermark = activityWatermark)
|
||||
persistMetadata()
|
||||
}
|
||||
SimklSettingsRefreshAction.FETCH -> {
|
||||
fetchAndStoreUserSettings(activityWatermark)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun completeAuthorization(callback: SimklAuthCallback.AuthorizationCode) =
|
||||
|
|
@ -261,30 +275,31 @@ object SimklAuthRepository : TrackingAuthProvider {
|
|||
SimklSyncRepository.refreshAsync(TrackingRefreshIntent.INVALIDATED)
|
||||
}
|
||||
|
||||
private suspend fun fetchAndStoreUserSettings(): String? {
|
||||
private suspend fun fetchAndStoreUserSettings(activityWatermark: String? = null): Boolean {
|
||||
val response = try {
|
||||
SimklApi.client.execute(
|
||||
SimklApiRequest(
|
||||
method = SimklHttpMethod.POST,
|
||||
path = "/users/settings",
|
||||
body = "{}",
|
||||
),
|
||||
)
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
log.w { "Failed to fetch Simkl user settings: ${error.message}" }
|
||||
return null
|
||||
return false
|
||||
}
|
||||
val settings = runCatching { json.decodeFromString<SimklUserSettingsResponse>(response.body) }
|
||||
.getOrNull() ?: return null
|
||||
.getOrNull() ?: return false
|
||||
storedState = storedState.copy(
|
||||
username = settings.user?.name,
|
||||
accountId = settings.account?.id,
|
||||
hasFetchedUserSettings = true,
|
||||
settingsActivityWatermark = activityWatermark ?: storedState.settingsActivityWatermark,
|
||||
)
|
||||
persistMetadata()
|
||||
publish(error = null)
|
||||
return storedState.username
|
||||
return true
|
||||
}
|
||||
|
||||
private fun loadFromDisk() {
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@ object SimklSyncRepository : TrackingProfileStore {
|
|||
return
|
||||
}
|
||||
|
||||
if (generation != profileGeneration || profileId != ProfileRepository.activeProfileId) return
|
||||
SimklAuthRepository.synchronizeUserSettings(result.activities?.settings?.all)
|
||||
if (generation != profileGeneration || profileId != ProfileRepository.activeProfileId) return
|
||||
SimklSyncStorage.savePayload(json.encodeToString(result))
|
||||
_state.value = SimklSyncUiState(
|
||||
|
|
|
|||
|
|
@ -69,6 +69,23 @@ class SimklApiClientTest {
|
|||
assertTrue(request.headers.getValue("User-Agent").contains('/'))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bodyless post sends json content type without inventing a payload`() = runBlocking {
|
||||
val engine = RecordingEngine(response(200))
|
||||
val harness = TestHarness(engine)
|
||||
|
||||
harness.client.execute(
|
||||
SimklApiRequest(
|
||||
method = SimklHttpMethod.POST,
|
||||
path = "/users/settings",
|
||||
),
|
||||
)
|
||||
|
||||
val request = engine.requests.single()
|
||||
assertEquals("", request.body)
|
||||
assertEquals("application/json", request.headers["Content-Type"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `single use unauthenticated posts keep metadata and never retry`() = runBlocking {
|
||||
val engine = RecordingEngine(response(503), response(200))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
package com.nuvio.app.features.simkl
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class SimklSettingsPolicyTest {
|
||||
@Test
|
||||
fun `missing activity watermark does not fetch settings`() {
|
||||
assertEquals(
|
||||
SimklSettingsRefreshAction.NONE,
|
||||
simklSettingsRefreshAction(SimklStoredAuthState(), null),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `first activity watermark is recorded after sign in settings fetch`() {
|
||||
assertEquals(
|
||||
SimklSettingsRefreshAction.RECORD_WATERMARK,
|
||||
simklSettingsRefreshAction(
|
||||
state = SimklStoredAuthState(hasFetchedUserSettings = true),
|
||||
activityWatermark = "2026-07-22T08:48:07Z",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `changed activity watermark fetches settings and matching watermark skips`() {
|
||||
val state = SimklStoredAuthState(
|
||||
hasFetchedUserSettings = true,
|
||||
settingsActivityWatermark = "2026-07-22T08:48:07Z",
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
SimklSettingsRefreshAction.NONE,
|
||||
simklSettingsRefreshAction(state, "2026-07-22T08:48:07Z"),
|
||||
)
|
||||
assertEquals(
|
||||
SimklSettingsRefreshAction.FETCH,
|
||||
simklSettingsRefreshAction(state, "2026-07-22T09:12:30Z"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `legacy auth state fetches once before recording a watermark`() {
|
||||
assertEquals(
|
||||
SimklSettingsRefreshAction.FETCH,
|
||||
simklSettingsRefreshAction(
|
||||
state = SimklStoredAuthState(username = "Nuvio User"),
|
||||
activityWatermark = "2026-07-22T08:48:07Z",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue