Split module session-storage into api and impl.

This commit is contained in:
Benoit Marty 2023-03-06 13:08:54 +01:00 committed by Benoit Marty
parent e2bd966878
commit 5ea2a4292d
15 changed files with 130 additions and 28 deletions

View file

@ -0,0 +1,59 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.sessionstorage.impl
import com.squareup.anvil.annotations.ContributesBinding
import com.squareup.sqldelight.runtime.coroutines.asFlow
import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DatabaseSessionStore @Inject constructor(
private val database: SessionDatabase,
) : SessionStore {
override fun isLoggedIn(): Flow<Boolean> {
return database.sessionDataQueries.selectFirst().asFlow().mapToOneOrNull().map { it != null }
}
override suspend fun storeData(sessionData: SessionData) {
database.sessionDataQueries.insertSessionData(sessionData.toDbModel())
}
override suspend fun getLatestSession(): SessionData? {
return database.sessionDataQueries.selectFirst()
.executeAsOneOrNull()
?.toApiModel()
}
override suspend fun getSession(sessionId: String): SessionData? {
return database.sessionDataQueries.selectByUserId(sessionId)
.executeAsOneOrNull()
?.toApiModel()
}
override suspend fun removeSession(sessionId: String) {
database.sessionDataQueries.removeSession(sessionId)
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.sessionstorage.impl
import io.element.android.libraries.sessionstorage.api.SessionData
internal fun SessionData.toDbModel(): io.element.android.libraries.matrix.session.SessionData {
return io.element.android.libraries.matrix.session.SessionData(
userId = userId,
deviceId = deviceId,
accessToken = accessToken,
refreshToken = refreshToken,
homeserverUrl = homeserverUrl,
isSoftLogout = isSoftLogout,
slidingSyncProxy = slidingSyncProxy,
)
}
internal fun io.element.android.libraries.matrix.session.SessionData.toApiModel(): SessionData {
return SessionData(
userId = userId,
deviceId = deviceId,
accessToken = accessToken,
refreshToken = refreshToken,
homeserverUrl = homeserverUrl,
isSoftLogout = isSoftLogout,
slidingSyncProxy = slidingSyncProxy,
)
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.sessionstorage.impl.di
import android.content.Context
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.sessionstorage.impl.SessionDatabase
import io.element.encrypteddb.SqlCipherDriverFactory
import io.element.encrypteddb.passphrase.RandomSecretPassphraseProvider
@Module
@ContributesTo(AppScope::class)
object SessionStorageModule {
@Provides
@SingleIn(AppScope::class)
fun provideMatrixDatabase(@ApplicationContext context: Context): SessionDatabase {
val name = "session_database"
val secretFile = context.getDatabasePath("$name.key")
val passphraseProvider = RandomSecretPassphraseProvider(context, secretFile, name)
val driver = SqlCipherDriverFactory(passphraseProvider)
.create(SessionDatabase.Schema, "$name.db", context)
return SessionDatabase(driver)
}
}

View file

@ -0,0 +1,21 @@
CREATE TABLE SessionData (
userId TEXT NOT NULL PRIMARY KEY,
deviceId TEXT NOT NULL,
accessToken TEXT NOT NULL,
refreshToken TEXT,
homeserverUrl TEXT NOT NULL,
isSoftLogout INTEGER AS Boolean NOT NULL DEFAULT 0,
slidingSyncProxy TEXT
);
selectFirst:
SELECT * FROM SessionData LIMIT 1;
selectByUserId:
SELECT * FROM SessionData WHERE userId = ?;
insertSessionData:
INSERT INTO SessionData(userId, deviceId, accessToken, refreshToken, homeserverUrl, isSoftLogout, slidingSyncProxy) VALUES ?;
removeSession:
DELETE FROM SessionData WHERE userId = ?;

View file

@ -0,0 +1,112 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.sessionstorage.impl
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
import io.element.android.libraries.matrix.session.SessionData
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class DatabaseSessionStoreTests {
private lateinit var database: SessionDatabase
private lateinit var databaseSessionStore: DatabaseSessionStore
private val aSessionData = SessionData(
userId = "userId",
deviceId = "deviceId",
accessToken = "accessToken",
refreshToken = "refreshToken",
homeserverUrl = "homeserverUrl",
isSoftLogout = false,
slidingSyncProxy = null
)
@Before
fun setup() {
// Initialise in memory SQLite driver
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
SessionDatabase.Schema.create(driver)
database = SessionDatabase(driver)
databaseSessionStore = DatabaseSessionStore(database)
}
@Test
fun `storeData persists the SessionData into the DB`() = runTest {
assertThat(database.sessionDataQueries.selectFirst().executeAsOneOrNull()).isNull()
databaseSessionStore.storeData(aSessionData.toApiModel())
assertThat(database.sessionDataQueries.selectFirst().executeAsOneOrNull()).isEqualTo(aSessionData)
}
@Test
fun `isLoggedIn emits true while there are sessions in the DB`() = runTest {
databaseSessionStore.isLoggedIn().test {
assertThat(awaitItem()).isFalse()
database.sessionDataQueries.insertSessionData(aSessionData)
assertThat(awaitItem()).isTrue()
database.sessionDataQueries.removeSession(aSessionData.userId)
assertThat(awaitItem()).isFalse()
}
}
@Test
fun `getLatestSession gets the first session in the DB`() = runTest {
database.sessionDataQueries.insertSessionData(aSessionData)
database.sessionDataQueries.insertSessionData(aSessionData.copy(userId = "otherUserId"))
val latestSession = databaseSessionStore.getLatestSession()?.toDbModel()
assertThat(latestSession).isEqualTo(aSessionData)
}
@Test
fun `getSession returns a matching session in DB if exists`() = runTest {
database.sessionDataQueries.insertSessionData(aSessionData)
database.sessionDataQueries.insertSessionData(aSessionData.copy(userId = "otherUserId"))
val foundSession = databaseSessionStore.getSession(aSessionData.userId)?.toDbModel()
assertThat(foundSession).isEqualTo(aSessionData)
}
@Test
fun `getSession returns null if a no matching session exists in DB`() = runTest {
database.sessionDataQueries.insertSessionData(aSessionData.copy(userId = "otherUserId"))
val foundSession = databaseSessionStore.getSession(aSessionData.userId)
assertThat(foundSession).isNull()
}
@Test
fun `removeSession removes the associated session in DB`() = runTest {
database.sessionDataQueries.insertSessionData(aSessionData)
databaseSessionStore.removeSession(aSessionData.userId)
assertThat(database.sessionDataQueries.selectByUserId(aSessionData.userId).executeAsOneOrNull()).isNull()
}
}