feat: manage saved infrared keys
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
package com.flagship.abox.manager.domain.infrared
|
||||||
|
|
||||||
|
import com.flagship.abox.manager.domain.DomainError
|
||||||
|
import com.flagship.abox.manager.domain.DomainResult
|
||||||
|
import com.flagship.abox.manager.domain.InfraredGateway
|
||||||
|
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||||
|
|
||||||
|
class SendInfraredKey(
|
||||||
|
private val repository: InfraredKeyRepository,
|
||||||
|
private val gateway: InfraredGateway,
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(ref: InfraredKeyRef): DomainResult<Unit> {
|
||||||
|
val key = when (val result = repository.getKey(ref)) {
|
||||||
|
is DomainResult.Success -> result.value
|
||||||
|
?: return DomainResult.Failure(
|
||||||
|
DomainError.NoData(detail = "Infrared key not found"),
|
||||||
|
)
|
||||||
|
|
||||||
|
is DomainResult.Failure -> return result
|
||||||
|
}
|
||||||
|
if (key.status != InfraredKeyStatus.READY) {
|
||||||
|
return DomainResult.Failure(
|
||||||
|
DomainError.InvalidRequest(detail = "Infrared key is not ready"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return gateway.sendKey(
|
||||||
|
profileId = key.profileId,
|
||||||
|
deviceName = key.deviceName,
|
||||||
|
key = key.sdkKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RenameInfraredKey(
|
||||||
|
private val repository: InfraredKeyRepository,
|
||||||
|
private val clock: EpochMillisClock,
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(
|
||||||
|
ref: InfraredKeyRef,
|
||||||
|
displayName: String,
|
||||||
|
): DomainResult<Unit> {
|
||||||
|
val trimmed = displayName.trim()
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
return DomainResult.Failure(
|
||||||
|
DomainError.InvalidRequest(detail = "Infrared key display name is required"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return repository.rename(ref, trimmed, clock.now())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeleteInfraredKey(
|
||||||
|
private val repository: InfraredKeyRepository,
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(ref: InfraredKeyRef): DomainResult<Unit> =
|
||||||
|
repository.deleteIfNotLearning(ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecoverInterruptedInfraredLearning(
|
||||||
|
private val repository: InfraredKeyRepository,
|
||||||
|
private val clock: EpochMillisClock,
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(profileId: ProviderProfileId): DomainResult<Int> =
|
||||||
|
repository.failInterruptedLearning(profileId, clock.now())
|
||||||
|
}
|
||||||
+39
-1
@@ -25,19 +25,43 @@ data class InfraredStatusTransitionCall(
|
|||||||
val updatedAtEpochMillis: Long,
|
val updatedAtEpochMillis: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class InfraredSendCall(
|
||||||
|
val profileId: ProviderProfileId,
|
||||||
|
val deviceName: DeviceName,
|
||||||
|
val key: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class InfraredRenameCall(
|
||||||
|
val ref: InfraredKeyRef,
|
||||||
|
val displayName: String,
|
||||||
|
val updatedAtEpochMillis: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class InfraredRecoveryCall(
|
||||||
|
val profileId: ProviderProfileId,
|
||||||
|
val updatedAtEpochMillis: Long,
|
||||||
|
)
|
||||||
|
|
||||||
class InMemoryInfraredKeyRepository : InfraredKeyRepository {
|
class InMemoryInfraredKeyRepository : InfraredKeyRepository {
|
||||||
private val keys = linkedMapOf<InfraredKeyRef, SavedInfraredKey>()
|
private val keys = linkedMapOf<InfraredKeyRef, SavedInfraredKey>()
|
||||||
private val state = MutableStateFlow<List<SavedInfraredKey>>(emptyList())
|
private val state = MutableStateFlow<List<SavedInfraredKey>>(emptyList())
|
||||||
|
|
||||||
var getError: DomainError? = null
|
var getError: DomainError? = null
|
||||||
var insertError: DomainError? = null
|
var insertError: DomainError? = null
|
||||||
|
var renameError: DomainError? = null
|
||||||
|
var deleteError: DomainError? = null
|
||||||
|
var recoveryError: DomainError? = null
|
||||||
val transitionErrors = mutableMapOf<InfraredKeyStatus, DomainError>()
|
val transitionErrors = mutableMapOf<InfraredKeyStatus, DomainError>()
|
||||||
|
|
||||||
var onInsert: ((SavedInfraredKey) -> Unit)? = null
|
var onInsert: ((SavedInfraredKey) -> Unit)? = null
|
||||||
var onTransition: ((InfraredStatusTransitionCall) -> Unit)? = null
|
var onTransition: ((InfraredStatusTransitionCall) -> Unit)? = null
|
||||||
|
|
||||||
val insertedKeys = mutableListOf<SavedInfraredKey>()
|
val insertedKeys = mutableListOf<SavedInfraredKey>()
|
||||||
|
val getCalls = mutableListOf<InfraredKeyRef>()
|
||||||
val transitionCalls = mutableListOf<InfraredStatusTransitionCall>()
|
val transitionCalls = mutableListOf<InfraredStatusTransitionCall>()
|
||||||
|
val renameCalls = mutableListOf<InfraredRenameCall>()
|
||||||
|
val deleteCalls = mutableListOf<InfraredKeyRef>()
|
||||||
|
val recoveryCalls = mutableListOf<InfraredRecoveryCall>()
|
||||||
|
|
||||||
override fun observeKeys(
|
override fun observeKeys(
|
||||||
profileId: ProviderProfileId,
|
profileId: ProviderProfileId,
|
||||||
@@ -49,6 +73,7 @@ class InMemoryInfraredKeyRepository : InfraredKeyRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getKey(ref: InfraredKeyRef): DomainResult<SavedInfraredKey?> {
|
override suspend fun getKey(ref: InfraredKeyRef): DomainResult<SavedInfraredKey?> {
|
||||||
|
getCalls += ref
|
||||||
getError?.let { return DomainResult.Failure(it) }
|
getError?.let { return DomainResult.Failure(it) }
|
||||||
return DomainResult.Success(keys[ref])
|
return DomainResult.Success(keys[ref])
|
||||||
}
|
}
|
||||||
@@ -101,6 +126,8 @@ class InMemoryInfraredKeyRepository : InfraredKeyRepository {
|
|||||||
displayName: String,
|
displayName: String,
|
||||||
updatedAtEpochMillis: Long,
|
updatedAtEpochMillis: Long,
|
||||||
): DomainResult<Unit> {
|
): DomainResult<Unit> {
|
||||||
|
renameCalls += InfraredRenameCall(ref, displayName, updatedAtEpochMillis)
|
||||||
|
renameError?.let { return DomainResult.Failure(it) }
|
||||||
val existing = keys[ref]
|
val existing = keys[ref]
|
||||||
?: return DomainResult.Failure(DomainError.InvalidRequest(detail = "Infrared key not found"))
|
?: return DomainResult.Failure(DomainError.InvalidRequest(detail = "Infrared key not found"))
|
||||||
keys[ref] = existing.copy(
|
keys[ref] = existing.copy(
|
||||||
@@ -112,6 +139,8 @@ class InMemoryInfraredKeyRepository : InfraredKeyRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteIfNotLearning(ref: InfraredKeyRef): DomainResult<Unit> {
|
override suspend fun deleteIfNotLearning(ref: InfraredKeyRef): DomainResult<Unit> {
|
||||||
|
deleteCalls += ref
|
||||||
|
deleteError?.let { return DomainResult.Failure(it) }
|
||||||
val existing = keys[ref]
|
val existing = keys[ref]
|
||||||
?: return DomainResult.Failure(DomainError.InvalidRequest(detail = "Infrared key not found"))
|
?: return DomainResult.Failure(DomainError.InvalidRequest(detail = "Infrared key not found"))
|
||||||
if (existing.status == InfraredKeyStatus.LEARNING) {
|
if (existing.status == InfraredKeyStatus.LEARNING) {
|
||||||
@@ -128,6 +157,8 @@ class InMemoryInfraredKeyRepository : InfraredKeyRepository {
|
|||||||
profileId: ProviderProfileId,
|
profileId: ProviderProfileId,
|
||||||
updatedAtEpochMillis: Long,
|
updatedAtEpochMillis: Long,
|
||||||
): DomainResult<Int> {
|
): DomainResult<Int> {
|
||||||
|
recoveryCalls += InfraredRecoveryCall(profileId, updatedAtEpochMillis)
|
||||||
|
recoveryError?.let { return DomainResult.Failure(it) }
|
||||||
var changed = 0
|
var changed = 0
|
||||||
keys.replaceAll { _, key ->
|
keys.replaceAll { _, key ->
|
||||||
if (key.profileId == profileId && key.status == InfraredKeyStatus.LEARNING) {
|
if (key.profileId == profileId && key.status == InfraredKeyStatus.LEARNING) {
|
||||||
@@ -152,6 +183,8 @@ class InMemoryInfraredKeyRepository : InfraredKeyRepository {
|
|||||||
|
|
||||||
fun onlyKey(): SavedInfraredKey = keys.values.single()
|
fun onlyKey(): SavedInfraredKey = keys.values.single()
|
||||||
|
|
||||||
|
fun allKeys(): List<SavedInfraredKey> = keys.values.toList()
|
||||||
|
|
||||||
private fun publish() {
|
private fun publish() {
|
||||||
state.value = keys.values.toList()
|
state.value = keys.values.toList()
|
||||||
}
|
}
|
||||||
@@ -159,8 +192,10 @@ class InMemoryInfraredKeyRepository : InfraredKeyRepository {
|
|||||||
|
|
||||||
class RecordingInfraredGateway : InfraredGateway {
|
class RecordingInfraredGateway : InfraredGateway {
|
||||||
var learnResult: DomainResult<Unit> = DomainResult.Success(Unit)
|
var learnResult: DomainResult<Unit> = DomainResult.Success(Unit)
|
||||||
|
var sendResult: DomainResult<Unit> = DomainResult.Success(Unit)
|
||||||
var onLearn: ((ProviderProfileId, DeviceName, String) -> DomainResult<Unit>)? = null
|
var onLearn: ((ProviderProfileId, DeviceName, String) -> DomainResult<Unit>)? = null
|
||||||
val learnedKeys = mutableListOf<InfraredLearningCall>()
|
val learnedKeys = mutableListOf<InfraredLearningCall>()
|
||||||
|
val sentKeys = mutableListOf<InfraredSendCall>()
|
||||||
|
|
||||||
override suspend fun learnKey(
|
override suspend fun learnKey(
|
||||||
profileId: ProviderProfileId,
|
profileId: ProviderProfileId,
|
||||||
@@ -175,7 +210,10 @@ class RecordingInfraredGateway : InfraredGateway {
|
|||||||
profileId: ProviderProfileId,
|
profileId: ProviderProfileId,
|
||||||
deviceName: DeviceName,
|
deviceName: DeviceName,
|
||||||
key: String,
|
key: String,
|
||||||
): DomainResult<Unit> = DomainResult.Success(Unit)
|
): DomainResult<Unit> {
|
||||||
|
sentKeys += InfraredSendCall(profileId, deviceName, key)
|
||||||
|
return sendResult
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun downloadCodes(
|
override suspend fun downloadCodes(
|
||||||
profileId: ProviderProfileId,
|
profileId: ProviderProfileId,
|
||||||
|
|||||||
+30
@@ -183,6 +183,36 @@ class LearnInfraredKeyTest {
|
|||||||
assertEquals(InfraredKeyStatus.FAILED, repository.onlyKey().status)
|
assertEquals(InfraredKeyStatus.FAILED, repository.onlyKey().status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun retryExpectedStatusFailurePreventsHardwareAfterConcurrentStatusChange() = runTest {
|
||||||
|
val original = failedKey()
|
||||||
|
repository.seed(original)
|
||||||
|
repository.onTransition = { call ->
|
||||||
|
if (call.targetStatus == InfraredKeyStatus.LEARNING) {
|
||||||
|
repository.seed(original.copy(status = InfraredKeyStatus.LEARNING))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = retry(original.ref())
|
||||||
|
|
||||||
|
assertTrue((result as DomainResult.Failure).error is DomainError.InvalidRequest)
|
||||||
|
assertTrue(gateway.learnedKeys.isEmpty())
|
||||||
|
assertEquals(InfraredKeyStatus.LEARNING, repository.onlyKey().status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun retryClearsPriorErrorWhileGatewayCallbackIsRunning() = runTest {
|
||||||
|
val original = failedKey()
|
||||||
|
repository.seed(original)
|
||||||
|
gateway.onLearn = { _, _, _ ->
|
||||||
|
assertEquals(InfraredKeyStatus.LEARNING, repository.onlyKey().status)
|
||||||
|
assertEquals(null, repository.onlyKey().lastErrorCode)
|
||||||
|
DomainResult.Success(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(DomainResult.Success(Unit), retry(original.ref()))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun retryGatewayFailureMarksFailedWithExternalCodeAndReturnsOriginalError() = runTest {
|
fun retryGatewayFailureMarksFailedWithExternalCodeAndReturnsOriginalError() = runTest {
|
||||||
val original = failedKey()
|
val original = failedKey()
|
||||||
|
|||||||
+232
@@ -0,0 +1,232 @@
|
|||||||
|
package com.flagship.abox.manager.domain.infrared
|
||||||
|
|
||||||
|
import com.flagship.abox.manager.domain.DeviceName
|
||||||
|
import com.flagship.abox.manager.domain.DomainError
|
||||||
|
import com.flagship.abox.manager.domain.DomainResult
|
||||||
|
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ManageInfraredKeyTest {
|
||||||
|
private val profile = ProviderProfileId("abox")
|
||||||
|
private val otherProfile = ProviderProfileId("other")
|
||||||
|
private val repository = InMemoryInfraredKeyRepository()
|
||||||
|
private val gateway = RecordingInfraredGateway()
|
||||||
|
private val clock = SteppingManagementClock(500L)
|
||||||
|
private val send = SendInfraredKey(repository, gateway)
|
||||||
|
private val rename = RenameInfraredKey(repository, clock)
|
||||||
|
private val delete = DeleteInfraredKey(repository)
|
||||||
|
private val recover = RecoverInterruptedInfraredLearning(repository, clock)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sendsReadyKeyUsingStoredDeviceAndSdkKeyWithoutRepositoryWrites() = runTest {
|
||||||
|
val key = readyKey(deviceName = DeviceName("ir-living"), sdkKey = "app_ir_power")
|
||||||
|
repository.seed(key)
|
||||||
|
|
||||||
|
assertEquals(DomainResult.Success(Unit), send(key.ref()))
|
||||||
|
assertEquals(
|
||||||
|
InfraredSendCall(profile, DeviceName("ir-living"), "app_ir_power"),
|
||||||
|
gateway.sentKeys.single(),
|
||||||
|
)
|
||||||
|
assertEquals(listOf(key.ref()), repository.getCalls)
|
||||||
|
assertTrue(repository.transitionCalls.isEmpty())
|
||||||
|
assertTrue(repository.renameCalls.isEmpty())
|
||||||
|
assertTrue(repository.deleteCalls.isEmpty())
|
||||||
|
assertTrue(repository.recoveryCalls.isEmpty())
|
||||||
|
assertEquals(key, repository.onlyKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sendFailureDoesNotDemoteReadyKey() = runTest {
|
||||||
|
val key = readyKey()
|
||||||
|
repository.seed(key)
|
||||||
|
val gatewayError = DomainError.OperationFailed("20205")
|
||||||
|
gateway.sendResult = DomainResult.Failure(gatewayError)
|
||||||
|
|
||||||
|
assertEquals(gatewayError, (send(key.ref()) as DomainResult.Failure).error)
|
||||||
|
assertEquals(InfraredKeyStatus.READY, repository.onlyKey().status)
|
||||||
|
assertTrue(repository.transitionCalls.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun missingSendReturnsNoDataWithoutHardwareCall() = runTest {
|
||||||
|
val result = send(InfraredKeyRef(profile, InfraredKeyId("missing")))
|
||||||
|
|
||||||
|
assertTrue((result as DomainResult.Failure).error is DomainError.NoData)
|
||||||
|
assertEquals(
|
||||||
|
listOf(InfraredKeyRef(profile, InfraredKeyId("missing"))),
|
||||||
|
repository.getCalls,
|
||||||
|
)
|
||||||
|
assertTrue(gateway.sentKeys.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nonReadySendReturnsInvalidRequestWithoutHardwareCall() = runTest {
|
||||||
|
repository.seed(failedKey())
|
||||||
|
|
||||||
|
val result = send(repository.onlyKey().ref())
|
||||||
|
|
||||||
|
assertTrue((result as DomainResult.Failure).error is DomainError.InvalidRequest)
|
||||||
|
assertTrue(gateway.sentKeys.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sendReturnsRepositoryFailureWithoutHardwareCall() = runTest {
|
||||||
|
val repositoryError = DomainError.Unknown(detail = "database unavailable")
|
||||||
|
repository.getError = repositoryError
|
||||||
|
|
||||||
|
val result = send(InfraredKeyRef(profile, InfraredKeyId("key1")))
|
||||||
|
|
||||||
|
assertEquals(repositoryError, (result as DomainResult.Failure).error)
|
||||||
|
assertTrue(gateway.sentKeys.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun renameTrimsDisplayNameAndPreservesStableIdentity() = runTest {
|
||||||
|
val before = readyKey(displayName = "Power", sdkKey = "app_ir_power")
|
||||||
|
repository.seed(before)
|
||||||
|
|
||||||
|
assertEquals(DomainResult.Success(Unit), rename(before.ref(), " 空调开关 "))
|
||||||
|
|
||||||
|
val after = repository.onlyKey()
|
||||||
|
assertEquals("空调开关", after.displayName)
|
||||||
|
assertEquals(before.sdkKey, after.sdkKey)
|
||||||
|
assertEquals(before.id, after.id)
|
||||||
|
assertEquals(before.createdAtEpochMillis, after.createdAtEpochMillis)
|
||||||
|
assertEquals(500L, after.updatedAtEpochMillis)
|
||||||
|
assertEquals(
|
||||||
|
InfraredRenameCall(before.ref(), "空调开关", 500L),
|
||||||
|
repository.renameCalls.single(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun renameRejectsBlankDisplayNameWithoutRepositoryCall() = runTest {
|
||||||
|
val key = readyKey()
|
||||||
|
repository.seed(key)
|
||||||
|
|
||||||
|
val result = rename(key.ref(), " \t ")
|
||||||
|
|
||||||
|
assertTrue((result as DomainResult.Failure).error is DomainError.InvalidRequest)
|
||||||
|
assertTrue(repository.renameCalls.isEmpty())
|
||||||
|
assertEquals(key, repository.onlyKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun renameReturnsRepositoryFailure() = runTest {
|
||||||
|
val key = readyKey()
|
||||||
|
repository.seed(key)
|
||||||
|
val repositoryError = DomainError.Unknown(detail = "write failed")
|
||||||
|
repository.renameError = repositoryError
|
||||||
|
|
||||||
|
val result = rename(key.ref(), "New name")
|
||||||
|
|
||||||
|
assertEquals(repositoryError, (result as DomainResult.Failure).error)
|
||||||
|
assertEquals("Power", repository.onlyKey().displayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deleteDelegatesAtomicLearningGuardToRepository() = runTest {
|
||||||
|
val key = learningKey()
|
||||||
|
repository.seed(key)
|
||||||
|
|
||||||
|
val result = delete(key.ref())
|
||||||
|
|
||||||
|
assertTrue((result as DomainResult.Failure).error is DomainError.InvalidRequest)
|
||||||
|
assertEquals(listOf(key.ref()), repository.deleteCalls)
|
||||||
|
assertTrue(repository.getCalls.isEmpty())
|
||||||
|
assertTrue(repository.allKeys().isNotEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deleteRemovesReadyKey() = runTest {
|
||||||
|
val key = readyKey()
|
||||||
|
repository.seed(key)
|
||||||
|
|
||||||
|
assertEquals(DomainResult.Success(Unit), delete(key.ref()))
|
||||||
|
assertEquals(listOf(key.ref()), repository.deleteCalls)
|
||||||
|
assertTrue(repository.getCalls.isEmpty())
|
||||||
|
assertTrue(repository.allKeys().isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun recoveryDelegatesOnceAndMarksOnlyProfileLearningKeysFailed() = runTest {
|
||||||
|
val learningOne = learningKey(id = InfraredKeyId("one"), lastErrorCode = "old")
|
||||||
|
val learningTwo = learningKey(id = InfraredKeyId("two"))
|
||||||
|
val ready = readyKey(id = InfraredKeyId("ready"))
|
||||||
|
val other = learningKey(id = InfraredKeyId("other"), profileId = otherProfile)
|
||||||
|
repository.seed(learningOne)
|
||||||
|
repository.seed(learningTwo)
|
||||||
|
repository.seed(ready)
|
||||||
|
repository.seed(other)
|
||||||
|
|
||||||
|
assertEquals(DomainResult.Success(2), recover(profile))
|
||||||
|
assertEquals(
|
||||||
|
listOf(InfraredRecoveryCall(profile, 500L)),
|
||||||
|
repository.recoveryCalls,
|
||||||
|
)
|
||||||
|
assertTrue(repository.getCalls.isEmpty())
|
||||||
|
assertTrue(repository.transitionCalls.isEmpty())
|
||||||
|
val recovered = repository.allKeys().filter { it.profileId == profile && it.id.value in setOf("one", "two") }
|
||||||
|
assertTrue(recovered.all { it.status == InfraredKeyStatus.FAILED })
|
||||||
|
assertTrue(recovered.all { it.lastErrorCode == null })
|
||||||
|
assertTrue(recovered.all { it.updatedAtEpochMillis == 500L })
|
||||||
|
assertEquals(InfraredKeyStatus.READY, repository.allKeys().single { it.id.value == "ready" }.status)
|
||||||
|
assertEquals(InfraredKeyStatus.LEARNING, repository.allKeys().single { it.profileId == otherProfile }.status)
|
||||||
|
assertTrue(gateway.learnedKeys.isEmpty())
|
||||||
|
assertTrue(gateway.sentKeys.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun recoveryReturnsRepositoryFailureWithoutHardwareCall() = runTest {
|
||||||
|
val repositoryError = DomainError.Unknown(detail = "write failed")
|
||||||
|
repository.recoveryError = repositoryError
|
||||||
|
|
||||||
|
val result = recover(profile)
|
||||||
|
|
||||||
|
assertEquals(repositoryError, (result as DomainResult.Failure).error)
|
||||||
|
assertEquals(listOf(InfraredRecoveryCall(profile, 500L)), repository.recoveryCalls)
|
||||||
|
assertTrue(gateway.learnedKeys.isEmpty())
|
||||||
|
assertTrue(gateway.sentKeys.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readyKey(
|
||||||
|
id: InfraredKeyId = InfraredKeyId("key1"),
|
||||||
|
profileId: ProviderProfileId = profile,
|
||||||
|
deviceName: DeviceName = DeviceName("ir-1"),
|
||||||
|
displayName: String = "Power",
|
||||||
|
sdkKey: String = "app_ir_key1",
|
||||||
|
) = SavedInfraredKey(
|
||||||
|
id = id,
|
||||||
|
profileId = profileId,
|
||||||
|
deviceName = deviceName,
|
||||||
|
displayName = displayName,
|
||||||
|
sdkKey = sdkKey,
|
||||||
|
status = InfraredKeyStatus.READY,
|
||||||
|
lastErrorCode = null,
|
||||||
|
createdAtEpochMillis = 1L,
|
||||||
|
updatedAtEpochMillis = 2L,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun failedKey() = readyKey().copy(
|
||||||
|
status = InfraredKeyStatus.FAILED,
|
||||||
|
lastErrorCode = "old-error",
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun learningKey(
|
||||||
|
id: InfraredKeyId = InfraredKeyId("key1"),
|
||||||
|
profileId: ProviderProfileId = profile,
|
||||||
|
lastErrorCode: String? = null,
|
||||||
|
) = readyKey(id = id, profileId = profileId).copy(
|
||||||
|
status = InfraredKeyStatus.LEARNING,
|
||||||
|
lastErrorCode = lastErrorCode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SteppingManagementClock(start: Long) : EpochMillisClock {
|
||||||
|
private var next = start
|
||||||
|
|
||||||
|
override fun now(): Long = next++
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user