feat: define activity event contracts
This commit is contained in:
@@ -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