Add organization events API request (#1468)

This commit is contained in:
David Perez 2024-06-19 11:35:35 -05:00 committed by Álison Fernandes
parent 0faa1be4e4
commit efbb9b3a19
10 changed files with 306 additions and 0 deletions

View file

@ -0,0 +1,13 @@
package com.x8bit.bitwarden.data.platform.datasource.network.api
import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEvent
import retrofit2.http.Body
import retrofit2.http.POST
/**
* This interface defines the API service for posting event data.
*/
interface EventApi {
@POST("/collect")
suspend fun collectOrganizationEvents(@Body events: List<OrganizationEvent>): Result<Unit>
}

View file

@ -10,6 +10,8 @@ import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.RetrofitsIm
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.ZonedDateTimeSerializer
import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigService
import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigServiceImpl
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventService
import com.x8bit.bitwarden.data.platform.datasource.network.service.EventServiceImpl
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushServiceImpl
import dagger.Module
@ -36,6 +38,14 @@ object PlatformNetworkModule {
retrofits: Retrofits,
): ConfigService = ConfigServiceImpl(retrofits.unauthenticatedApiRetrofit.create())
@Provides
@Singleton
fun providesEventService(
retrofits: Retrofits,
): EventService = EventServiceImpl(
eventApi = retrofits.authenticatedEventsRetrofit.create(),
)
@Provides
@Singleton
fun providePushService(

View file

@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.platform.datasource.network.model
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEventType
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.ZonedDateTime
/**
* Represents an individual organization event including the type and time.
*/
@Serializable
data class OrganizationEvent(
@SerialName("type") val type: OrganizationEventType,
@SerialName("cipherId") val cipherId: String?,
@SerialName("date") @Contextual val date: ZonedDateTime,
)

View file

@ -14,6 +14,13 @@ interface Retrofits {
*/
val authenticatedApiRetrofit: Retrofit
/**
* Allows access to "/events" calls that must be authenticated.
*
* The base URL can be dynamically determined via the [BaseUrlInterceptors].
*/
val authenticatedEventsRetrofit: Retrofit
/**
* Allows access to "/api" calls that do not require authentication.
*

View file

@ -36,6 +36,12 @@ class RetrofitsImpl(
)
}
override val authenticatedEventsRetrofit: Retrofit by lazy {
createAuthenticatedRetrofit(
baseUrlInterceptor = baseUrlInterceptors.eventsInterceptor,
)
}
//endregion Authenticated Retrofits
//region Unauthenticated Retrofits

View file

@ -0,0 +1,13 @@
package com.x8bit.bitwarden.data.platform.datasource.network.service
import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEvent
/**
* Provides an API for submitting events.
*/
interface EventService {
/**
* Attempts to submit all of the given organizations events.
*/
suspend fun sendOrganizationEvents(events: List<OrganizationEvent>): Result<Unit>
}

View file

@ -0,0 +1,15 @@
package com.x8bit.bitwarden.data.platform.datasource.network.service
import com.x8bit.bitwarden.data.platform.datasource.network.api.EventApi
import com.x8bit.bitwarden.data.platform.datasource.network.model.OrganizationEvent
/**
* The default implementation of the [EventService].
*/
class EventServiceImpl(
private val eventApi: EventApi,
) : EventService {
override suspend fun sendOrganizationEvents(
events: List<OrganizationEvent>,
): Result<Unit> = eventApi.collectOrganizationEvents(events = events)
}

View file

@ -0,0 +1,134 @@
package com.x8bit.bitwarden.data.platform.manager.model
import androidx.annotation.Keep
import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Representation of events used for organization tracking.
*/
@Serializable(OrganizationEventTypeSerializer::class)
enum class OrganizationEventType {
@SerialName("1000")
USER_LOGGED_IN,
@SerialName("1001")
USER_CHANGED_PASSWORD,
@SerialName("1002")
USER_UPDATED_2FA,
@SerialName("1003")
USER_DISABLED_2FA,
@SerialName("1004")
USER_RECOVERED_2FA,
@SerialName("1005")
USER_FAILED_LOGIN,
@SerialName("1006")
USER_FAILED_LOGIN_2FA,
@SerialName("1007")
USER_CLIENT_EXPORTED_VAULT,
@SerialName("1100")
CIPHER_CREATED,
@SerialName("1101")
CIPHER_UPDATED,
@SerialName("1102")
CIPHER_DELETED,
@SerialName("1103")
CIPHER_ATTACHMENT_CREATED,
@SerialName("1104")
CIPHER_ATTACHMENT_DELETED,
@SerialName("1105")
CIPHER_SHARED,
@SerialName("1106")
CIPHER_UPDATED_COLLECTIONS,
@SerialName("1107")
CIPHER_CLIENT_VIEWED,
@SerialName("1108")
CIPHER_CLIENT_TOGGLED_PASSWORD_VISIBLE,
@SerialName("1109")
CIPHER_CLIENT_TOGGLED_HIDDEN_FIELD_VISIBLE,
@SerialName("1110")
CIPHER_CLIENT_TOGGLED_CARD_CODE_VISIBLE,
@SerialName("1111")
CIPHER_CLIENT_COPIED_PASSWORD,
@SerialName("1112")
CIPHER_CLIENT_COPIED_HIDDEN_FIELD,
@SerialName("1113")
CIPHER_CLIENT_COPIED_CARD_CODE,
@SerialName("1114")
CIPHER_CLIENT_AUTO_FILLED,
@SerialName("1115")
CIPHER_SOFT_DELETED,
@SerialName("1116")
CIPHER_RESTORED,
@SerialName("1117")
CIPHER_CLIENT_TOGGLED_CARD_NUMBER_VISIBLE,
@SerialName("1300")
COLLECTION_CREATED,
@SerialName("1301")
COLLECTION_UPDATED,
@SerialName("1302")
COLLECTION_DELETED,
@SerialName("1400")
GROUP_CREATED,
@SerialName("1401")
GROUP_UPDATED,
@SerialName("1402")
GROUP_DELETED,
@SerialName("1500")
ORGANIZATION_USER_INVITED,
@SerialName("1501")
ORGANIZATION_USER_CONFIRMED,
@SerialName("1502")
ORGANIZATION_USER_UPDATED,
@SerialName("1503")
ORGANIZATION_USER_REMOVED,
@SerialName("1504")
ORGANIZATION_USER_UPDATED_GROUPS,
@SerialName("1600")
ORGANIZATION_UPDATED,
@SerialName("1601")
ORGANIZATION_PURGED_VAULT,
}
@Keep
private class OrganizationEventTypeSerializer : BaseEnumeratedIntSerializer<OrganizationEventType>(
values = OrganizationEventType.entries.toTypedArray(),
)

View file

@ -101,6 +101,36 @@ class RetrofitsTest {
assertTrue(isRefreshAuthenticatorCalled)
}
@Test
fun `authenticatedEventsRetrofit should not invoke the RefreshAuthenticator on success`() =
runBlocking {
val testApi = retrofits
.authenticatedEventsRetrofit
.createMockRetrofit()
.create<TestApi>()
server.enqueue(MockResponse().setBody("""{}"""))
testApi.test()
assertFalse(isRefreshAuthenticatorCalled)
}
@Test
fun `authenticatedEventsRetrofit should invoke the RefreshAuthenticator on 401`() =
runBlocking {
val testApi = retrofits
.authenticatedEventsRetrofit
.createMockRetrofit()
.create<TestApi>()
server.enqueue(MockResponse().setResponseCode(401).setBody("""{}"""))
testApi.test()
assertTrue(isRefreshAuthenticatorCalled)
}
@Test
fun `unauthenticatedApiRetrofit should not invoke the RefreshAuthenticator`() = runBlocking {
val testApi = retrofits
@ -133,6 +163,24 @@ class RetrofitsTest {
assertFalse(isEventsInterceptorCalled)
}
@Test
fun `authenticatedEventsRetrofit should invoke the correct interceptors`() = runBlocking {
val testApi = retrofits
.authenticatedEventsRetrofit
.createMockRetrofit()
.create<TestApi>()
server.enqueue(MockResponse().setBody("""{}"""))
testApi.test()
assertTrue(isAuthInterceptorCalled)
assertFalse(isApiInterceptorCalled)
assertTrue(isheadersInterceptorCalled)
assertFalse(isIdentityInterceptorCalled)
assertTrue(isEventsInterceptorCalled)
}
@Test
fun `unauthenticatedApiRetrofit should invoke the correct interceptors`() = runBlocking {
val testApi = retrofits

View file

@ -0,0 +1,43 @@
package com.x8bit.bitwarden.data.platform.datasource.network.service
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
import com.x8bit.bitwarden.data.platform.datasource.network.api.EventApi
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 okhttp3.mockwebserver.MockResponse
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import retrofit2.create
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
import java.time.ZonedDateTime
class EventServiceTest : BaseServiceTest() {
private val fixedClock: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
private val eventApi: EventApi = retrofit.create()
private val eventService: EventService = EventServiceImpl(
eventApi = eventApi,
)
@Test
fun `sendOrganizationEvents should return the correct response`() = runTest {
server.enqueue(MockResponse())
val result = eventService.sendOrganizationEvents(
events = listOf(
OrganizationEvent(
type = OrganizationEventType.CIPHER_CREATED,
cipherId = "cipher-id",
date = ZonedDateTime.now(fixedClock),
),
),
)
assertEquals(Unit, result.getOrThrow())
}
}