feat: add production local experience simulation
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api(project(":domain"))
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
testImplementation(libs.kotlinx.coroutines.test)
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
package com.flagship.abox.manager.simulation
|
||||||
|
|
||||||
|
import com.flagship.abox.manager.domain.DeviceCatalog
|
||||||
|
import com.flagship.abox.manager.domain.DeviceDescriptor
|
||||||
|
import com.flagship.abox.manager.domain.DeviceId
|
||||||
|
import com.flagship.abox.manager.domain.DeviceName
|
||||||
|
import com.flagship.abox.manager.domain.DeviceType
|
||||||
|
import com.flagship.abox.manager.domain.DomainResult
|
||||||
|
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
|
class SimulationDeviceCatalog : DeviceCatalog {
|
||||||
|
private val devices = ConcurrentHashMap<
|
||||||
|
ProviderProfileId,
|
||||||
|
MutableStateFlow<DomainResult<List<DeviceDescriptor>>>,
|
||||||
|
>()
|
||||||
|
|
||||||
|
override fun observeDevices(
|
||||||
|
profileId: ProviderProfileId,
|
||||||
|
): Flow<DomainResult<List<DeviceDescriptor>>> = deviceFlow(profileId)
|
||||||
|
|
||||||
|
private fun deviceFlow(
|
||||||
|
profileId: ProviderProfileId,
|
||||||
|
): MutableStateFlow<DomainResult<List<DeviceDescriptor>>> = devices.getOrPut(profileId) {
|
||||||
|
MutableStateFlow(DomainResult.Success(defaultDevices(profileId)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun defaultDevices(profileId: ProviderProfileId): List<DeviceDescriptor> = listOf(
|
||||||
|
DeviceDescriptor(
|
||||||
|
id = DeviceId("${profileId.value}:socket-1"),
|
||||||
|
profileId = profileId,
|
||||||
|
name = DeviceName("socket-1"),
|
||||||
|
type = DeviceType.SOCKET,
|
||||||
|
),
|
||||||
|
DeviceDescriptor(
|
||||||
|
id = DeviceId("${profileId.value}:th-1"),
|
||||||
|
profileId = profileId,
|
||||||
|
name = DeviceName("th-1"),
|
||||||
|
type = DeviceType.TEMPERATURE_HUMIDITY,
|
||||||
|
),
|
||||||
|
DeviceDescriptor(
|
||||||
|
id = DeviceId("${profileId.value}:ir-1"),
|
||||||
|
profileId = profileId,
|
||||||
|
name = DeviceName("ir-1"),
|
||||||
|
type = DeviceType.INFRARED,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
package com.flagship.abox.manager.simulation
|
||||||
|
|
||||||
|
import com.flagship.abox.manager.domain.DeviceName
|
||||||
|
import com.flagship.abox.manager.domain.DomainResult
|
||||||
|
import com.flagship.abox.manager.domain.InfraredCode
|
||||||
|
import com.flagship.abox.manager.domain.InfraredCodeDownloadResult
|
||||||
|
import com.flagship.abox.manager.domain.InfraredGateway
|
||||||
|
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||||
|
import com.flagship.abox.manager.domain.SocketGateway
|
||||||
|
import com.flagship.abox.manager.domain.SocketPowerState
|
||||||
|
import com.flagship.abox.manager.domain.SocketStatus
|
||||||
|
import com.flagship.abox.manager.domain.TemperatureHumidityGateway
|
||||||
|
import com.flagship.abox.manager.domain.TemperatureHumidityReading
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
class SimulationSocketGateway : SocketGateway {
|
||||||
|
private val states = ConcurrentHashMap<DeviceKey, SocketPowerState>()
|
||||||
|
|
||||||
|
override suspend fun getStatus(
|
||||||
|
profileId: ProviderProfileId,
|
||||||
|
deviceName: DeviceName,
|
||||||
|
): DomainResult<SocketStatus> {
|
||||||
|
val key = DeviceKey(profileId, deviceName)
|
||||||
|
return DomainResult.Success(SocketStatus(profileId, deviceName, states[key] ?: SocketPowerState.OFF))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setPower(
|
||||||
|
profileId: ProviderProfileId,
|
||||||
|
deviceName: DeviceName,
|
||||||
|
powerState: SocketPowerState,
|
||||||
|
): DomainResult<SocketStatus> {
|
||||||
|
val key = DeviceKey(profileId, deviceName)
|
||||||
|
states[key] = powerState
|
||||||
|
return DomainResult.Success(SocketStatus(profileId, deviceName, powerState))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimulationTemperatureHumidityGateway : TemperatureHumidityGateway {
|
||||||
|
override suspend fun getReading(
|
||||||
|
profileId: ProviderProfileId,
|
||||||
|
deviceName: DeviceName,
|
||||||
|
): DomainResult<TemperatureHumidityReading> = DomainResult.Success(
|
||||||
|
TemperatureHumidityReading(
|
||||||
|
profileId = profileId,
|
||||||
|
deviceName = deviceName,
|
||||||
|
temperature = "23.5",
|
||||||
|
humidity = "45",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimulationInfraredGateway : InfraredGateway {
|
||||||
|
override suspend fun sendKey(
|
||||||
|
profileId: ProviderProfileId,
|
||||||
|
deviceName: DeviceName,
|
||||||
|
key: String,
|
||||||
|
): DomainResult<Unit> = DomainResult.Success(Unit)
|
||||||
|
|
||||||
|
override suspend fun learnKey(
|
||||||
|
profileId: ProviderProfileId,
|
||||||
|
deviceName: DeviceName,
|
||||||
|
key: String,
|
||||||
|
): DomainResult<Unit> = DomainResult.Success(Unit)
|
||||||
|
|
||||||
|
override suspend fun downloadCodes(
|
||||||
|
profileId: ProviderProfileId,
|
||||||
|
codes: List<InfraredCode>,
|
||||||
|
): DomainResult<List<InfraredCodeDownloadResult>> =
|
||||||
|
DomainResult.Success(codes.map { InfraredCodeDownloadResult(it, successful = true) })
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class DeviceKey(
|
||||||
|
val profileId: ProviderProfileId,
|
||||||
|
val deviceName: DeviceName,
|
||||||
|
)
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package com.flagship.abox.manager.simulation
|
||||||
|
|
||||||
|
import com.flagship.abox.manager.domain.DomainResult
|
||||||
|
import com.flagship.abox.manager.domain.LocalSessionGateway
|
||||||
|
import com.flagship.abox.manager.domain.LoginCredentials
|
||||||
|
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||||
|
import com.flagship.abox.manager.domain.Session
|
||||||
|
import com.flagship.abox.manager.domain.SessionValidity
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
|
class SimulationSessionGateway : LocalSessionGateway {
|
||||||
|
private val sessions = ConcurrentHashMap<ProviderProfileId, MutableStateFlow<Session?>>()
|
||||||
|
|
||||||
|
override fun observeSession(profileId: ProviderProfileId): Flow<Session?> = flow(profileId)
|
||||||
|
|
||||||
|
override suspend fun login(
|
||||||
|
profileId: ProviderProfileId,
|
||||||
|
credentials: LoginCredentials,
|
||||||
|
): DomainResult<Session> = DomainResult.Success(
|
||||||
|
Session(profileId, credentials.username).also { flow(profileId).value = it },
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun validateSession(
|
||||||
|
profileId: ProviderProfileId,
|
||||||
|
): DomainResult<SessionValidity> = DomainResult.Success(
|
||||||
|
if (flow(profileId).value == null) SessionValidity.INVALID else SessionValidity.VALID,
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun logout(profileId: ProviderProfileId): DomainResult<Unit> {
|
||||||
|
flow(profileId).value = null
|
||||||
|
return DomainResult.Success(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun flow(profileId: ProviderProfileId): MutableStateFlow<Session?> =
|
||||||
|
sessions.getOrPut(profileId) { MutableStateFlow(null) }
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package com.flagship.abox.manager.simulation
|
||||||
|
|
||||||
|
import com.flagship.abox.manager.domain.DeviceName
|
||||||
|
import com.flagship.abox.manager.domain.DeviceType
|
||||||
|
import com.flagship.abox.manager.domain.DomainResult
|
||||||
|
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||||
|
import com.flagship.abox.manager.domain.SocketPowerState
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SimulationDeviceGatewaysTest {
|
||||||
|
@Test
|
||||||
|
fun catalogProvidesSocketTemperatureHumidityAndInfraredDevicesForEachProfile() = runTest {
|
||||||
|
val profile = ProviderProfileId("local")
|
||||||
|
val devices =
|
||||||
|
(SimulationDeviceCatalog().observeDevices(profile).first() as DomainResult.Success).value
|
||||||
|
assertEquals(
|
||||||
|
setOf(DeviceType.SOCKET, DeviceType.TEMPERATURE_HUMIDITY, DeviceType.INFRARED),
|
||||||
|
devices.map { it.type }.toSet(),
|
||||||
|
)
|
||||||
|
assertTrue(devices.all { it.profileId == profile })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun socketStateIsMutableAndIsolatedByProfile() = runTest {
|
||||||
|
val gateway = SimulationSocketGateway()
|
||||||
|
val device = DeviceName("socket-1")
|
||||||
|
val first = ProviderProfileId("first")
|
||||||
|
val second = ProviderProfileId("second")
|
||||||
|
gateway.setPower(first, device, SocketPowerState.ON)
|
||||||
|
assertEquals(
|
||||||
|
SocketPowerState.ON,
|
||||||
|
(gateway.getStatus(first, device) as DomainResult.Success).value.powerState,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
SocketPowerState.OFF,
|
||||||
|
(gateway.getStatus(second, device) as DomainResult.Success).value.powerState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sensorAndInfraredOperationsSucceedWithoutNetworkConfiguration() = runTest {
|
||||||
|
val profile = ProviderProfileId("local")
|
||||||
|
val th = SimulationTemperatureHumidityGateway().getReading(profile, DeviceName("th-1"))
|
||||||
|
assertTrue(th is DomainResult.Success)
|
||||||
|
assertEquals(
|
||||||
|
DomainResult.Success(Unit),
|
||||||
|
SimulationInfraredGateway().sendKey(profile, DeviceName("ir-1"), "power"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package com.flagship.abox.manager.simulation
|
||||||
|
|
||||||
|
import com.flagship.abox.manager.domain.DomainResult
|
||||||
|
import com.flagship.abox.manager.domain.LoginCredentials
|
||||||
|
import com.flagship.abox.manager.domain.Password
|
||||||
|
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||||
|
import com.flagship.abox.manager.domain.SessionValidity
|
||||||
|
import com.flagship.abox.manager.domain.Username
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SimulationSessionGatewayTest {
|
||||||
|
@Test
|
||||||
|
fun loginObserveValidateAndLogoutAreProfileScoped() = runTest {
|
||||||
|
val gateway = SimulationSessionGateway()
|
||||||
|
val first = ProviderProfileId("local-one")
|
||||||
|
val second = ProviderProfileId("local-two")
|
||||||
|
|
||||||
|
val result = gateway.login(first, LoginCredentials(Username("local"), Password("")))
|
||||||
|
|
||||||
|
assertEquals(Username("local"), (result as DomainResult.Success).value.username)
|
||||||
|
assertEquals(first, gateway.observeSession(first).first()?.profileId)
|
||||||
|
assertNull(gateway.observeSession(second).first())
|
||||||
|
assertEquals(DomainResult.Success(SessionValidity.VALID), gateway.validateSession(first))
|
||||||
|
gateway.logout(first)
|
||||||
|
assertEquals(DomainResult.Success(SessionValidity.INVALID), gateway.validateSession(first))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ dependencyResolutionManagement {
|
|||||||
rootProject.name = "abox-manager"
|
rootProject.name = "abox-manager"
|
||||||
|
|
||||||
include(":domain")
|
include(":domain")
|
||||||
|
include(":runtime:simulation")
|
||||||
include(":absdk-bridge")
|
include(":absdk-bridge")
|
||||||
include(":testing:fakes")
|
include(":testing:fakes")
|
||||||
include(":android:core:designsystem")
|
include(":android:core:designsystem")
|
||||||
|
|||||||
Reference in New Issue
Block a user