Add Organization events database (#1470)

This commit is contained in:
David Perez 2024-06-19 17:20:35 -05:00 committed by Álison Fernandes
parent a2f2216df2
commit 170db5077d
9 changed files with 439 additions and 0 deletions

View file

@ -0,0 +1,68 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "f40d7a933b2f353d8d5b5ca619f28e24",
"entities": [
{
"tableName": "organization_events",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `organization_event_type` TEXT NOT NULL, `cipher_id` TEXT, `date` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "organizationEventType",
"columnName": "organization_event_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "cipherId",
"columnName": "cipher_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_organization_events_user_id",
"unique": false,
"columnNames": [
"user_id"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_organization_events_user_id` ON `${TABLE_NAME}` (`user_id`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f40d7a933b2f353d8d5b5ca619f28e24')"
]
}
}

View file

@ -0,0 +1,23 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEvent
/**
* Primary access point for disk information related to event data.
*/
interface EventDiskSource {
/**
* Deletes all organization events associated with the given [userId].
*/
suspend fun deleteOrganizationEvents(userId: String)
/**
* Adds a new organization event associated with the given [userId].
*/
suspend fun addOrganizationEvent(userId: String, event: OrganizationEvent)
/**
* Retrieves all organization events associated with the given [userId].
*/
suspend fun getOrganizationEvents(userId: String): List<OrganizationEvent>
}

View file

@ -0,0 +1,54 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import com.x8bit.bitwarden.data.platform.datasource.disk.dao.OrganizationEventDao
import com.x8bit.bitwarden.data.platform.datasource.disk.entity.OrganizationEventEntity
import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEventType
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
/**
* The default implementation of [EventDiskSource].
*/
class EventDiskSourceImpl(
private val organizationEventDao: OrganizationEventDao,
private val dispatcherManager: DispatcherManager,
private val json: Json,
) : EventDiskSource {
override suspend fun deleteOrganizationEvents(userId: String) {
organizationEventDao.deleteOrganizationEvents(userId = userId)
}
override suspend fun addOrganizationEvent(userId: String, event: OrganizationEvent) {
organizationEventDao.insertOrganizationEvent(
event = OrganizationEventEntity(
userId = userId,
organizationEventType = withContext(context = dispatcherManager.default) {
json.encodeToString(value = event.type)
},
cipherId = event.cipherId,
date = event.date,
),
)
}
override suspend fun getOrganizationEvents(
userId: String,
): List<OrganizationEvent> =
organizationEventDao
.getOrganizationEvents(userId = userId)
.map {
OrganizationEvent(
type = withContext(context = dispatcherManager.default) {
json.decodeFromString<OrganizationEventType>(
string = it.organizationEventType,
)
},
cipherId = it.cipherId,
date = it.date,
)
}
}

View file

@ -0,0 +1,33 @@
package com.x8bit.bitwarden.data.platform.datasource.disk.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.x8bit.bitwarden.data.platform.datasource.disk.entity.OrganizationEventEntity
/**
* Provides methods for inserting, retrieving, and deleting events from the database using the
* [OrganizationEventEntity].
*/
@Dao
interface OrganizationEventDao {
/**
* Deletes all the stored events associated with the given [userId]. This will return the
* number of rows deleted by this query.
*/
@Query("DELETE FROM organization_events WHERE user_id = :userId")
suspend fun deleteOrganizationEvents(userId: String): Int
/**
* Retrieves all events from the database for a given [userId].
*/
@Query("SELECT * FROM organization_events WHERE user_id = :userId")
suspend fun getOrganizationEvents(userId: String): List<OrganizationEventEntity>
/**
* Inserts an event into the database.
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrganizationEvent(event: OrganizationEventEntity)
}

View file

@ -0,0 +1,26 @@
package com.x8bit.bitwarden.data.platform.datasource.disk.database
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.x8bit.bitwarden.data.platform.datasource.disk.dao.OrganizationEventDao
import com.x8bit.bitwarden.data.platform.datasource.disk.entity.OrganizationEventEntity
import com.x8bit.bitwarden.data.vault.datasource.disk.convertor.ZonedDateTimeTypeConverter
/**
* Room database for storing any persisted data for platform data.
*/
@Database(
entities = [
OrganizationEventEntity::class,
],
version = 1,
exportSchema = true,
)
@TypeConverters(ZonedDateTimeTypeConverter::class)
abstract class PlatformDatabase : RoomDatabase() {
/**
* Provides the DAO for accessing organization event data.
*/
abstract fun organizationEventDao(): OrganizationEventDao
}

View file

@ -3,21 +3,28 @@ package com.x8bit.bitwarden.data.platform.datasource.disk.di
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.room.Room
import com.x8bit.bitwarden.data.platform.datasource.di.EncryptedPreferences
import com.x8bit.bitwarden.data.platform.datasource.di.UnencryptedPreferences
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSourceImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSourceImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSourceImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSourceImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.dao.OrganizationEventDao
import com.x8bit.bitwarden.data.platform.datasource.disk.database.PlatformDatabase
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigratorImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorage
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageMigrator
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacySecureStorageMigratorImpl
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.datasource.disk.convertor.ZonedDateTimeTypeConverter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -44,6 +51,38 @@ object PlatformDiskModule {
json = json,
)
@Provides
@Singleton
fun provideEventDatabase(app: Application): PlatformDatabase =
Room
.databaseBuilder(
context = app,
klass = PlatformDatabase::class.java,
name = "platform_database",
)
.fallbackToDestructiveMigration()
.addTypeConverter(ZonedDateTimeTypeConverter())
.build()
@Provides
@Singleton
fun provideOrganizationEventDao(
database: PlatformDatabase,
): OrganizationEventDao = database.organizationEventDao()
@Provides
@Singleton
fun provideEventDiskSource(
organizationEventDao: OrganizationEventDao,
dispatcherManager: DispatcherManager,
json: Json,
): EventDiskSource =
EventDiskSourceImpl(
organizationEventDao = organizationEventDao,
dispatcherManager = dispatcherManager,
json = json,
)
@Provides
@Singleton
fun provideLegacySecureStorage(

View file

@ -0,0 +1,28 @@
package com.x8bit.bitwarden.data.platform.datasource.disk.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.time.ZonedDateTime
/**
* Entity representing an organization event in the database.
*/
@Entity(tableName = "organization_events")
data class OrganizationEventEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
val id: Int = 0,
@ColumnInfo(name = "user_id", index = true)
val userId: String,
@ColumnInfo(name = "organization_event_type")
val organizationEventType: String,
@ColumnInfo(name = "cipher_id")
val cipherId: String?,
@ColumnInfo(name = "date")
val date: ZonedDateTime,
)

View file

@ -0,0 +1,142 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.datasource.disk.dao.FakeOrganizationEventDao
import com.x8bit.bitwarden.data.platform.datasource.disk.entity.OrganizationEventEntity
import com.x8bit.bitwarden.data.platform.datasource.network.di.PlatformNetworkModule
import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEventType
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
import java.time.ZonedDateTime
class EventDiskSourceTest {
private val fixedClock: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
private val fakeOrganizationEventDao = FakeOrganizationEventDao()
private val fakeDispatcherManager = FakeDispatcherManager()
private val json = PlatformNetworkModule.providesJson()
private val eventDiskSource: EventDiskSource = EventDiskSourceImpl(
organizationEventDao = fakeOrganizationEventDao,
dispatcherManager = fakeDispatcherManager,
json = json,
)
@Test
fun `addOrganizationEvent should insert a new organization event`() = runTest {
val userId = "userId-1"
val organizationEvent = OrganizationEvent(
type = OrganizationEventType.CIPHER_DELETED,
cipherId = "cipherId-1",
date = ZonedDateTime.now(fixedClock),
)
eventDiskSource.addOrganizationEvent(
userId = userId,
event = organizationEvent,
)
assertEquals(
listOf(
OrganizationEventEntity(
id = 0,
userId = userId,
organizationEventType = "1102",
cipherId = "cipherId-1",
date = ZonedDateTime.now(fixedClock),
),
),
fakeOrganizationEventDao.storedEvents,
)
assertFalse(fakeOrganizationEventDao.isDeleteCalled)
assertTrue(fakeOrganizationEventDao.isInsertCalled)
}
@Test
fun `deleteOrganizationEvents should delete all organization events`() = runTest {
val userId = "userId-1"
fakeOrganizationEventDao.storedEvents.addAll(
listOf(
OrganizationEventEntity(
id = 1,
userId = userId,
organizationEventType = "1102",
cipherId = "cipherId-1",
date = ZonedDateTime.now(fixedClock),
),
OrganizationEventEntity(
id = 2,
userId = "userId-2",
organizationEventType = "1102",
cipherId = "cipherId-2",
date = ZonedDateTime.now(fixedClock),
),
),
)
eventDiskSource.deleteOrganizationEvents(userId = userId)
assertEquals(
listOf(
OrganizationEventEntity(
id = 2,
userId = "userId-2",
organizationEventType = "1102",
cipherId = "cipherId-2",
date = ZonedDateTime.now(fixedClock),
),
),
fakeOrganizationEventDao.storedEvents,
)
assertTrue(fakeOrganizationEventDao.isDeleteCalled)
assertFalse(fakeOrganizationEventDao.isInsertCalled)
}
@Test
fun `getOrganizationEvents should retrieve the correct organization events`() = runTest {
val userId = "userId-1"
fakeOrganizationEventDao.storedEvents.addAll(
listOf(
OrganizationEventEntity(
id = 1,
userId = userId,
organizationEventType = "1102",
cipherId = "cipherId-1",
date = ZonedDateTime.now(fixedClock),
),
OrganizationEventEntity(
id = 2,
userId = "userId-2",
organizationEventType = "1102",
cipherId = "cipherId-2",
date = ZonedDateTime.now(fixedClock),
),
),
)
val result = eventDiskSource.getOrganizationEvents(userId = userId)
assertEquals(
listOf(
OrganizationEvent(
type = OrganizationEventType.CIPHER_DELETED,
cipherId = "cipherId-1",
date = ZonedDateTime.now(fixedClock),
),
),
result,
)
assertFalse(fakeOrganizationEventDao.isDeleteCalled)
assertFalse(fakeOrganizationEventDao.isInsertCalled)
}
}

View file

@ -0,0 +1,26 @@
package com.x8bit.bitwarden.data.platform.datasource.disk.dao
import com.x8bit.bitwarden.data.platform.datasource.disk.entity.OrganizationEventEntity
class FakeOrganizationEventDao : OrganizationEventDao {
val storedEvents = mutableListOf<OrganizationEventEntity>()
var isDeleteCalled = false
var isInsertCalled = false
override suspend fun deleteOrganizationEvents(userId: String): Int {
val count = storedEvents.count { it.userId == userId }
storedEvents.removeAll { it.userId == userId }
isDeleteCalled = true
return count
}
override suspend fun getOrganizationEvents(
userId: String,
): List<OrganizationEventEntity> = storedEvents.filter { it.userId == userId }
override suspend fun insertOrganizationEvent(event: OrganizationEventEntity) {
storedEvents.add(event)
isInsertCalled = true
}
}