feat(ui): complete ABox app experience redesign #10
@@ -0,0 +1,68 @@
|
||||
package com.flagship.abox.manager.domain
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@JvmInline
|
||||
value class ActivityEventId(val value: String)
|
||||
|
||||
enum class ActivityEventKind {
|
||||
SESSION_SIGNED_IN,
|
||||
SESSION_SIGNED_OUT,
|
||||
SOCKET_COMMAND,
|
||||
SOCKET_CONFIRMATION,
|
||||
SENSOR_REFRESH,
|
||||
INFRARED_SEND,
|
||||
INFRARED_LEARN,
|
||||
SCENE_EXECUTION,
|
||||
SCENE_ACTION,
|
||||
}
|
||||
|
||||
enum class ActivitySource { USER, AUTOMATION, SYSTEM }
|
||||
|
||||
enum class ActivityOutcome {
|
||||
SUCCEEDED,
|
||||
FAILED,
|
||||
CANCELLED,
|
||||
AWAITING_CONFIRMATION,
|
||||
}
|
||||
|
||||
enum class ActivityTargetType { PROFILE, DEVICE, SCENE, SCENE_ACTION, REMOTE, INFRARED_KEY }
|
||||
|
||||
data class ActivityTarget(
|
||||
val type: ActivityTargetType,
|
||||
val stableId: String,
|
||||
val displayName: String,
|
||||
)
|
||||
|
||||
data class ActivityEvent(
|
||||
val id: ActivityEventId,
|
||||
val profileId: ProviderProfileId,
|
||||
val occurredAtEpochMillis: Long,
|
||||
val kind: ActivityEventKind,
|
||||
val source: ActivitySource,
|
||||
val outcome: ActivityOutcome,
|
||||
val target: ActivityTarget,
|
||||
val errorCode: String? = null,
|
||||
)
|
||||
|
||||
data class ActivityEventCursor(
|
||||
val occurredAtEpochMillis: Long,
|
||||
val id: ActivityEventId,
|
||||
) : Comparable<ActivityEventCursor> {
|
||||
override fun compareTo(other: ActivityEventCursor): Int {
|
||||
val timeComparison = occurredAtEpochMillis.compareTo(other.occurredAtEpochMillis)
|
||||
return if (timeComparison != 0) timeComparison else id.value.compareTo(other.id.value)
|
||||
}
|
||||
}
|
||||
|
||||
interface ActivityRepository {
|
||||
fun observeLatest(profileId: ProviderProfileId, limit: Int): Flow<DomainResult<List<ActivityEvent>>>
|
||||
|
||||
suspend fun loadBefore(
|
||||
profileId: ProviderProfileId,
|
||||
before: ActivityEventCursor,
|
||||
limit: Int,
|
||||
): DomainResult<List<ActivityEvent>>
|
||||
|
||||
suspend fun append(event: ActivityEvent): DomainResult<Unit>
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package com.flagship.abox.manager.domain.activity
|
||||
|
||||
import com.flagship.abox.manager.domain.ActivityEvent
|
||||
import com.flagship.abox.manager.domain.ActivityEventCursor
|
||||
import com.flagship.abox.manager.domain.ActivityEventId
|
||||
import com.flagship.abox.manager.domain.ActivityEventKind
|
||||
import com.flagship.abox.manager.domain.ActivityOutcome
|
||||
import com.flagship.abox.manager.domain.ActivityRepository
|
||||
import com.flagship.abox.manager.domain.ActivitySource
|
||||
import com.flagship.abox.manager.domain.ActivityTarget
|
||||
import com.flagship.abox.manager.domain.ActivityTargetType
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class ActivityContractTest {
|
||||
@Test
|
||||
fun eventDoesNotContainCredentialsOrPayloadFields() {
|
||||
val event = ActivityEvent(
|
||||
id = ActivityEventId("event-1"),
|
||||
profileId = ProviderProfileId("profile-1"),
|
||||
occurredAtEpochMillis = 10L,
|
||||
kind = ActivityEventKind.SOCKET_COMMAND,
|
||||
source = ActivitySource.USER,
|
||||
outcome = ActivityOutcome.SUCCEEDED,
|
||||
target = ActivityTarget(ActivityTargetType.DEVICE, "device-1", "客厅插座"),
|
||||
errorCode = "20504",
|
||||
)
|
||||
|
||||
val fields = ActivityEvent::class.java.declaredFields.map { it.name }
|
||||
assertFalse(fields.any { it.contains("password", ignoreCase = true) })
|
||||
assertFalse(fields.any { it.contains("token", ignoreCase = true) })
|
||||
assertFalse(fields.any { it.contains("payload", ignoreCase = true) })
|
||||
assertTrue(event.errorCode == "20504")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cursorOrdersEqualTimestampsById() {
|
||||
val first = ActivityEventCursor(10L, ActivityEventId("a"))
|
||||
val second = ActivityEventCursor(10L, ActivityEventId("b"))
|
||||
|
||||
assertTrue(first < second)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun recordingFailureDoesNotAlterOriginalDomainResult() = runTest {
|
||||
val repository = object : ActivityRepository {
|
||||
override fun observeLatest(profileId: ProviderProfileId, limit: Int): Flow<DomainResult<List<ActivityEvent>>> =
|
||||
MutableStateFlow(DomainResult.Success(emptyList()))
|
||||
override suspend fun loadBefore(profileId: ProviderProfileId, before: ActivityEventCursor, limit: Int) =
|
||||
DomainResult.Success(emptyList<ActivityEvent>())
|
||||
override suspend fun append(event: ActivityEvent) = DomainResult.Failure(com.flagship.abox.manager.domain.DomainError.Unknown())
|
||||
}
|
||||
val original: DomainResult<String> = DomainResult.Success("command-complete")
|
||||
|
||||
val recorded = repository.append(testEvent())
|
||||
|
||||
assertTrue(recorded is DomainResult.Failure)
|
||||
assertTrue(original is DomainResult.Success)
|
||||
assertTrue((original as DomainResult.Success).value == "command-complete")
|
||||
}
|
||||
|
||||
private fun testEvent() = ActivityEvent(
|
||||
id = ActivityEventId("event-1"),
|
||||
profileId = ProviderProfileId("profile-1"),
|
||||
occurredAtEpochMillis = 10L,
|
||||
kind = ActivityEventKind.SESSION_SIGNED_IN,
|
||||
source = ActivitySource.SYSTEM,
|
||||
outcome = ActivityOutcome.SUCCEEDED,
|
||||
target = ActivityTarget(ActivityTargetType.PROFILE, "profile-1", "账户"),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user