feat: add provider login flow #4
@@ -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),
|
||||
)
|
||||
}
|
||||
-38
@@ -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) }
|
||||
}
|
||||
+77
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user
明文传输
SessionModule位于 main 源集,却显式启用CleartextAbsdkTransport并调用AbsdkBridge.create(),因此包括 release 在内的所有变体都会绑定真实明文桥接。用户选择 ABox 并登录时,凭据可能通过未经批准的明文 ABSDK 通道传输。应将该绑定移至受限的开发变体并使用模拟实现,release 不应构造此桥接。