feat: route provider sessions to ABSDK and simulation

This commit is contained in:
2026-08-24 19:10:46 +08:00
parent d560a78b63
commit 45f768208f
6 changed files with 114 additions and 94 deletions
+12
View File
@@ -45,6 +45,15 @@ android {
kotlinOptions {
jvmTarget = "17"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
excludes += "/META-INF/DEPENDENCIES"
excludes += "/META-INF/LICENSE*"
excludes += "/META-INF/NOTICE*"
}
}
}
kapt {
@@ -55,6 +64,8 @@ dependencies {
implementation(project(":domain"))
implementation(project(":android:core:designsystem"))
implementation(project(":android:core:session"))
implementation(project(":absdk-bridge"))
implementation(project(":runtime:simulation"))
debugImplementation(project(":testing:fakes"))
implementation(platform(libs.compose.bom))
@@ -73,6 +84,7 @@ dependencies {
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(project(":testing:fakes"))
androidTestImplementation(platform(libs.compose.bom))
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.test.runner)
@@ -1,27 +0,0 @@
package com.flagship.abox.manager.di
import com.flagship.abox.manager.domain.SessionGateway
import com.flagship.abox.manager.testing.fakes.FakeSessionGateway
import com.flagship.abox.manager.ui.provider.Availability
import com.flagship.abox.manager.ui.provider.ProviderAvailability
import com.flagship.abox.manager.ui.provider.UnavailableReason
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DebugSessionModule {
@Provides
@Singleton
fun provideRemoteSessionGateway(fake: FakeSessionGateway): SessionGateway = fake
@Provides
@Singleton
fun provideProviderAvailability(): ProviderAvailability = ProviderAvailability(
abox = Availability.Simulation,
compatible = Availability.Disabled(UnavailableReason.PROTOCOL_PENDING),
)
}
@@ -3,6 +3,8 @@ package com.flagship.abox.manager.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import com.flagship.abox.manager.absdk.bridge.AbsdkBridge
import com.flagship.abox.manager.absdk.bridge.CleartextAbsdkTransport
import com.flagship.abox.manager.domain.CredentialStore
import com.flagship.abox.manager.domain.LocalSessionGateway
import com.flagship.abox.manager.domain.ProviderProfileStore
@@ -11,9 +13,12 @@ import com.flagship.abox.manager.domain.SessionGateway
import com.flagship.abox.manager.session.credential.AndroidKeystoreCredentialCipher
import com.flagship.abox.manager.session.credential.CredentialCipher
import com.flagship.abox.manager.session.credential.KeystoreCredentialStore
import com.flagship.abox.manager.session.gateway.LocalSessionGatewayImpl
import com.flagship.abox.manager.session.profile.ProviderProfileStoreImpl
import com.flagship.abox.manager.session.profile.sessionDataStore
import com.flagship.abox.manager.simulation.SimulationSessionGateway
import com.flagship.abox.manager.ui.provider.Availability
import com.flagship.abox.manager.ui.provider.ProviderAvailability
import com.flagship.abox.manager.ui.provider.UnavailableReason
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -49,7 +54,25 @@ object SessionModule {
@Provides
@Singleton
fun provideLocalSessionGateway(): LocalSessionGateway = LocalSessionGatewayImpl()
@OptIn(CleartextAbsdkTransport::class)
fun provideAbsdkBridge(): AbsdkBridge = AbsdkBridge.create()
@Provides
@Singleton
fun provideRemoteSessionGateway(
bridge: AbsdkBridge,
): SessionGateway = bridge
@Provides
@Singleton
fun provideLocalSessionGateway(): LocalSessionGateway = SimulationSessionGateway()
@Provides
@Singleton
fun provideProviderAvailability(): ProviderAvailability = ProviderAvailability(
abox = Availability.Enabled,
compatible = Availability.Disabled(UnavailableReason.PROTOCOL_PENDING),
)
@Provides
@Singleton
@@ -1,27 +0,0 @@
package com.flagship.abox.manager.di
import com.flagship.abox.manager.domain.SessionGateway
import com.flagship.abox.manager.session.gateway.UnavailableRemoteSessionGateway
import com.flagship.abox.manager.ui.provider.Availability
import com.flagship.abox.manager.ui.provider.ProviderAvailability
import com.flagship.abox.manager.ui.provider.UnavailableReason
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object ReleaseSessionModule {
@Provides
@Singleton
fun provideRemoteSessionGateway(): SessionGateway = UnavailableRemoteSessionGateway()
@Provides
@Singleton
fun provideProviderAvailability(): ProviderAvailability = ProviderAvailability(
abox = Availability.Disabled(UnavailableReason.TRANSPORT_SECURITY_PENDING),
compatible = Availability.Disabled(UnavailableReason.PROTOCOL_PENDING),
)
}
@@ -1,38 +0,0 @@
package com.flagship.abox.manager.session.gateway
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 LocalSessionGatewayImpl : 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) }
}
@@ -1,5 +1,6 @@
package com.flagship.abox.manager.testing.fakes
import com.flagship.abox.manager.domain.CredentialStore
import com.flagship.abox.manager.domain.DomainError
import com.flagship.abox.manager.domain.DomainResult
import com.flagship.abox.manager.domain.LoginCredentials
@@ -9,6 +10,8 @@ import com.flagship.abox.manager.domain.ProviderProfile
import com.flagship.abox.manager.domain.ProviderProfileId
import com.flagship.abox.manager.domain.ProviderType
import com.flagship.abox.manager.domain.SessionCoordinator
import com.flagship.abox.manager.domain.SessionGateway
import com.flagship.abox.manager.domain.Session
import com.flagship.abox.manager.domain.Username
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
@@ -41,6 +44,37 @@ class SessionCoordinatorTest {
assertNull(fixture.remote.observeSession(localProfile.id).first())
}
@Test
fun localSelectionCreatesSessionWithoutAboxOrCredentialAccess() = runTest {
val remote = CountingSessionGateway()
val credentialsStore = CountingCredentialStore()
val coordinator = SessionCoordinator(
FakeProviderProfileStore(),
credentialsStore,
remote,
FakeLocalSessionGateway(),
)
val result = coordinator.selectProvider(localProfile)
assertTrue(result is DomainResult.Success)
assertEquals(0, remote.loginCalls)
assertEquals(0, credentialsStore.loadCalls + credentialsStore.saveCalls)
assertEquals(localProfile.id, coordinator.observeActiveSession().first()?.profileId)
}
@Test
fun aboxAuthenticationFailureDoesNotCreateSession() = runTest {
val fixture = Fixture()
fixture.coordinator.selectProvider(remoteProfile)
fixture.remote.failLogin(remoteProfile.id, DomainError.Authentication())
val result = fixture.coordinator.login(remoteProfile.id, credentials, false, false)
assertTrue(result is DomainResult.Failure)
assertNull(fixture.coordinator.observeActiveSession().first())
}
@Test
fun loginPersistsIndependentUsernameAndAutoLoginChoices() = runTest {
val fixture = Fixture()
@@ -126,4 +160,47 @@ class SessionCoordinatorTest {
private val local = FakeLocalSessionGateway()
val coordinator = SessionCoordinator(profiles, credentials, remote, local)
}
private class CountingSessionGateway(
private val delegate: FakeSessionGateway = FakeSessionGateway(),
) : SessionGateway by delegate {
var loginCalls = 0
private set
override suspend fun login(
profileId: ProviderProfileId,
credentials: LoginCredentials,
): DomainResult<Session> {
loginCalls++
return delegate.login(profileId, credentials)
}
fun failLogin(profileId: ProviderProfileId, error: DomainError) {
delegate.failLogin(profileId, error)
}
}
private class CountingCredentialStore(
private val delegate: FakeCredentialStore = FakeCredentialStore(),
) : CredentialStore by delegate {
var loadCalls = 0
private set
var saveCalls = 0
private set
override suspend fun loadCredentials(
profileId: ProviderProfileId,
): DomainResult<LoginCredentials?> {
loadCalls++
return delegate.loadCredentials(profileId)
}
override suspend fun saveCredentials(
profileId: ProviderProfileId,
credentials: LoginCredentials,
): DomainResult<Unit> {
saveCalls++
return delegate.saveCredentials(profileId, credentials)
}
}
}