feat: add provider login flow #4
@@ -45,6 +45,15 @@ android {
|
|||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "17"
|
jvmTarget = "17"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
resources {
|
||||||
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
|
excludes += "/META-INF/DEPENDENCIES"
|
||||||
|
excludes += "/META-INF/LICENSE*"
|
||||||
|
excludes += "/META-INF/NOTICE*"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kapt {
|
kapt {
|
||||||
@@ -55,6 +64,8 @@ dependencies {
|
|||||||
implementation(project(":domain"))
|
implementation(project(":domain"))
|
||||||
implementation(project(":android:core:designsystem"))
|
implementation(project(":android:core:designsystem"))
|
||||||
implementation(project(":android:core:session"))
|
implementation(project(":android:core:session"))
|
||||||
|
implementation(project(":absdk-bridge"))
|
||||||
|
implementation(project(":runtime:simulation"))
|
||||||
debugImplementation(project(":testing:fakes"))
|
debugImplementation(project(":testing:fakes"))
|
||||||
|
|
||||||
implementation(platform(libs.compose.bom))
|
implementation(platform(libs.compose.bom))
|
||||||
@@ -73,6 +84,7 @@ dependencies {
|
|||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
testImplementation(libs.kotlinx.coroutines.test)
|
testImplementation(libs.kotlinx.coroutines.test)
|
||||||
|
testImplementation(project(":testing:fakes"))
|
||||||
androidTestImplementation(platform(libs.compose.bom))
|
androidTestImplementation(platform(libs.compose.bom))
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.test.runner)
|
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 android.content.Context
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.Preferences
|
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.CredentialStore
|
||||||
import com.flagship.abox.manager.domain.LocalSessionGateway
|
import com.flagship.abox.manager.domain.LocalSessionGateway
|
||||||
import com.flagship.abox.manager.domain.ProviderProfileStore
|
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.AndroidKeystoreCredentialCipher
|
||||||
import com.flagship.abox.manager.session.credential.CredentialCipher
|
import com.flagship.abox.manager.session.credential.CredentialCipher
|
||||||
import com.flagship.abox.manager.session.credential.KeystoreCredentialStore
|
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.ProviderProfileStoreImpl
|
||||||
import com.flagship.abox.manager.session.profile.sessionDataStore
|
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.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -49,7 +54,25 @@ object SessionModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@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
|
@Provides
|
||||||
@Singleton
|
@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
|
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.DomainError
|
||||||
import com.flagship.abox.manager.domain.DomainResult
|
import com.flagship.abox.manager.domain.DomainResult
|
||||||
import com.flagship.abox.manager.domain.LoginCredentials
|
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.ProviderProfileId
|
||||||
import com.flagship.abox.manager.domain.ProviderType
|
import com.flagship.abox.manager.domain.ProviderType
|
||||||
import com.flagship.abox.manager.domain.SessionCoordinator
|
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 com.flagship.abox.manager.domain.Username
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@@ -41,6 +44,37 @@ class SessionCoordinatorTest {
|
|||||||
assertNull(fixture.remote.observeSession(localProfile.id).first())
|
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
|
@Test
|
||||||
fun loginPersistsIndependentUsernameAndAutoLoginChoices() = runTest {
|
fun loginPersistsIndependentUsernameAndAutoLoginChoices() = runTest {
|
||||||
val fixture = Fixture()
|
val fixture = Fixture()
|
||||||
@@ -126,4 +160,47 @@ class SessionCoordinatorTest {
|
|||||||
private val local = FakeLocalSessionGateway()
|
private val local = FakeLocalSessionGateway()
|
||||||
val coordinator = SessionCoordinator(profiles, credentials, remote, local)
|
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 不应构造此桥接。