Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion kmp-common-sdk
Submodule kmp-common-sdk updated 0 files
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ public class MindboxConfiguration private constructor(
* Unlike most options here this one is re-read on every initialization, so it can be
* turned on or off in a later app version.
*
* Projects that route operations through an anonymizer never collect the identifier,
* whatever this option says: collection stops as soon as [operationsDomain] is set or the
* mobile config supplies an operations domain.
*
* @param disableTrackingIds - flag which turns the collection off. Default value is false,
* meaning the identifier is collected.
*/
Expand All @@ -214,6 +218,9 @@ public class MindboxConfiguration private constructor(
* (e.g. "domain.com/api/v2") — operation endpoints are appended after it.
* A blank value is treated as not set. An invalid value is logged
* and ignored during SDK initialization.
*
* While an operations domain is set, the SDK does not collect the device tracking
* identifier (see [disableTrackingIds]).
*/
public fun operationsDomain(operationsDomain: String): Builder {
this.operationsDomain = operationsDomain.trim().takeIf { it.isNotBlank() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ internal class TrackingIdsResolverImpl(

override suspend fun resolve(context: Context): List<TrackingId> {
if (!MindboxPreferences.shouldCollectTrackingIds) return emptyList()
operationsDomain()?.let { domain ->
mindboxLogI("Tracking ids are not collected: operations go through operationsDomain $domain")
return emptyList()
}

val reading = scope().async(ioDispatcher) {
pushServiceHandlers().map { handler -> handler.tryGetTrackingId(context) }
Expand All @@ -59,6 +63,10 @@ internal class TrackingIdsResolverImpl(
mindboxLogI("Sent tracking ids: ${trackingIds.joinToString { it.type }.ifEmpty { "none" }}")
}

private fun operationsDomain(): String? =
MindboxPreferences.operationsDomainFromConfig
?: DbManager.getConfigurations()?.operationsDomain?.takeIf { it.isNotBlank() }
Comment on lines +67 to +68

private fun lastSent(): List<TrackingId> = loggingRunCatching(defaultValue = emptyList()) {
MindboxPreferences.lastSentTrackingIds
.takeIf { it.isNotBlank() }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cloud.mindbox.mobile_sdk.managers

import android.content.Context
import cloud.mindbox.mobile_sdk.models.Configuration
import cloud.mindbox.mobile_sdk.models.TrackingId
import cloud.mindbox.mobile_sdk.pushes.PushServiceHandler
import cloud.mindbox.mobile_sdk.pushes.TrackingIdResult
Expand All @@ -9,8 +10,10 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.unmockkObject
import io.mockk.verify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.After
Expand All @@ -28,27 +31,48 @@ private const val STORED_BOTH =
"""[{"type":"google","value":"38400000-8cf0-11bd-b23e-10b96e40000d"},{"type":"huawei","value":"1a2b3c4d-5e6f-7081-92a3-b4c5d6e7f809"}]"""
private const val STORED_GOOGLE =
"""[{"type":"google","value":"38400000-8cf0-11bd-b23e-10b96e40000d"}]"""
private const val ANONYMIZER_DOMAIN = "anonymizer.example.com"

@OptIn(ExperimentalCoroutinesApi::class)
class TrackingIdsResolverImplTest {

private val context = mockk<Context>()

private var shouldCollect = true
private var lastSent = ""
private var operationsDomainFromConfig: String? = null
private var configuration: Configuration? = storedConfiguration(operationsDomain = null)

@Before
fun onTestStart() {
mockkObject(MindboxPreferences)
every { MindboxPreferences.shouldCollectTrackingIds } answers { shouldCollect }
every { MindboxPreferences.lastSentTrackingIds } answers { lastSent }
every { MindboxPreferences.lastSentTrackingIds = any() } answers { lastSent = firstArg() }
every { MindboxPreferences.operationsDomainFromConfig } answers { operationsDomainFromConfig }
mockkObject(DbManager)
every { DbManager.getConfigurations() } answers { configuration }
}

@After
fun onTestEnd() {
unmockkObject(MindboxPreferences)
unmockkObject(DbManager)
}

private fun storedConfiguration(operationsDomain: String?) = Configuration(
previousInstallationId = "",
previousDeviceUUID = "",
endpointId = "test-endpoint",
domain = "api.mindbox.ru",
packageName = "com.test.app",
versionName = "1.0.0",
versionCode = "1",
subscribeCustomerIfCreated = true,
shouldCreateCustomer = true,
operationsDomain = operationsDomain,
)

private fun resolver(vararg results: TrackingIdResult) = TrackingIdsResolverImpl(
pushServiceHandlers = { results.map(::handlerReturning) },
scope = { CoroutineScope(UnconfinedTestDispatcher()) },
Expand Down Expand Up @@ -205,6 +229,91 @@ class TrackingIdsResolverImplTest {
assertEquals(emptyList<TrackingId>(), payload)
}

@Test
fun `nothing is collected when operationsDomain is set in init`() = runTest {
configuration = storedConfiguration(operationsDomain = ANONYMIZER_DOMAIN)

val payload = resolver(success(GOOGLE, GOOGLE_VALUE)).resolve(context)

assertEquals(emptyList<TrackingId>(), payload)
}

@Test
fun `nothing is collected when operationsDomain comes from the mobile config`() = runTest {
operationsDomainFromConfig = ANONYMIZER_DOMAIN

val payload = resolver(success(GOOGLE, GOOGLE_VALUE)).resolve(context)

assertEquals(emptyList<TrackingId>(), payload)
}

@Test
fun `providers are not even read when operationsDomain is set`() = runTest {
operationsDomainFromConfig = ANONYMIZER_DOMAIN
val handler = handlerReturning(success(GOOGLE, GOOGLE_VALUE))
val resolver = TrackingIdsResolverImpl(
pushServiceHandlers = { listOf(handler) },
scope = { CoroutineScope(UnconfinedTestDispatcher()) },
ioDispatcher = UnconfinedTestDispatcher(),
)

resolver.resolve(context)

verify(exactly = 0) { handler.tryGetTrackingId(any()) }
}

@Test
fun `operationsDomain does not repeat the last reported set`() = runTest {
lastSent = STORED_BOTH
configuration = storedConfiguration(operationsDomain = ANONYMIZER_DOMAIN)

val payload = resolver(TrackingIdResult.Unavailable).resolve(context)

assertEquals(emptyList<TrackingId>(), payload)
}

@Test
fun `operationsDomain added to the mobile config stops the collection`() = runTest {
val resolver = resolver(success(GOOGLE, GOOGLE_VALUE))
resolver.markSent(resolver.resolve(context))

operationsDomainFromConfig = ANONYMIZER_DOMAIN
val payload = resolver.resolve(context)

assertEquals(emptyList<TrackingId>(), payload)
assertTrue(resolver.hasChanged(payload))
}

@Test
fun `operationsDomain removed from the mobile config resumes the collection`() = runTest {
val resolver = resolver(success(GOOGLE, GOOGLE_VALUE))
operationsDomainFromConfig = ANONYMIZER_DOMAIN
resolver.markSent(resolver.resolve(context))

operationsDomainFromConfig = null
val payload = resolver.resolve(context)

assertEquals(listOf(googleId), payload)
}

@Test
fun `a blank operationsDomain in the stored configuration does not stop the collection`() = runTest {
configuration = storedConfiguration(operationsDomain = " ")

val payload = resolver(success(GOOGLE, GOOGLE_VALUE)).resolve(context)

assertEquals(listOf(googleId), payload)
}

@Test
fun `a missing stored configuration does not stop the collection`() = runTest {
configuration = null

val payload = resolver(success(GOOGLE, GOOGLE_VALUE)).resolve(context)

assertEquals(listOf(googleId), payload)
}

@Test
fun `an outage is never a change, so it triggers no operation of its own`() = runTest {
lastSent = STORED_GOOGLE
Expand Down
Loading