Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-24 11:49:25 +05:00
parent 0b8ff3e56e
commit 9b7bbe2564
8 changed files with 212 additions and 6 deletions

View file

@ -110,6 +110,7 @@ dependencies {
implementation(projects.domain.promo.models)
implementation(projects.domain.networks)
implementation(projects.domain.quotes)
implementation(projects.domain.notifications)
implementation(projects.common)
implementation(projects.common.routing)

View file

@ -0,0 +1,21 @@
package com.tangem.tap.di.domain
import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object NotificationsDomainModule {
@Provides
@Singleton
fun providesGetApplicationIdUseCase(notificationsRepository: NotificationsRepository): GetApplicationIdUseCase =
GetApplicationIdUseCase(
notificationsRepository = notificationsRepository,
)
}

View file

@ -10,7 +10,7 @@ import com.tangem.utils.info.AppInfoProvider
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.*
import com.tangem.domain.notifications.NotificationsRepository
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.notifications.models.NotificationsEligibleNetwork
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext

View file

@ -1,7 +1,7 @@
package com.tangem.data.notifications.di
import com.tangem.data.notifications.DefaultNotificationsRepository
import com.tangem.domain.notifications.NotificationsRepository
import com.tangem.domain.notifications.repository.NotificationsRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn

View file

@ -5,8 +5,12 @@ plugins {
}
dependencies {
implementation(deps.arrow.core)
implementation(deps.kotlin.coroutines)
implementation(projects.domain.core)
implementation(projects.domain.notifications.models)
/* Tests */
testImplementation(deps.test.junit)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.truth)
testImplementation(deps.test.mockk)
}

View file

@ -0,0 +1,26 @@
package com.tangem.domain.notifications
import arrow.core.Either
import com.tangem.domain.notifications.repository.NotificationsRepository
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class GetApplicationIdUseCase(
private val notificationsRepository: NotificationsRepository,
) {
private val mutex = Mutex()
suspend operator fun invoke(): Either<Throwable, String> = Either.catch {
val localApplicationId = notificationsRepository.getApplicationId()
if (localApplicationId != null) return@catch localApplicationId
mutex.withLock {
val doubleCheckedId = notificationsRepository.getApplicationId()
if (doubleCheckedId != null) return@withLock doubleCheckedId
val newApplicationId = notificationsRepository.createApplicationId()
notificationsRepository.saveApplicationId(newApplicationId)
newApplicationId
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.domain.notifications
package com.tangem.domain.notifications.repository
import com.tangem.domain.notifications.models.NotificationsEligibleNetwork

View file

@ -0,0 +1,154 @@
package com.tangem.domain.notifications
import arrow.core.Either
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.notifications.repository.NotificationsRepository
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.mockk
import kotlinx.coroutines.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
import java.net.SocketTimeoutException
class GetApplicationIdUseCaseTest {
private val notificationsRepository: NotificationsRepository = mockk()
private val useCase = GetApplicationIdUseCase(notificationsRepository)
@Test
fun `GIVEN local application ID exists WHEN invoke THEN return local application ID`() = runTest {
// GIVEN
val expectedApplicationId = "test-app-id"
coEvery { notificationsRepository.getApplicationId() } returns expectedApplicationId
// WHEN
val result = useCase()
// THEN
assertThat(result).isInstanceOf(Either.Right::class.java)
assertThat((result as Either.Right).value).isEqualTo(expectedApplicationId)
coVerify(exactly = 1) { notificationsRepository.getApplicationId() }
coVerify(inverse = true) {
notificationsRepository.createApplicationId()
notificationsRepository.saveApplicationId(any())
}
}
@Test
fun `GIVEN local application ID does not exist WHEN invoke THEN create and save new application ID`() = runTest {
// GIVEN
val newApplicationId = "new-app-id"
coEvery { notificationsRepository.getApplicationId() } returns null
coEvery { notificationsRepository.createApplicationId() } returns newApplicationId
coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit
// WHEN
val result = useCase()
// THEN
assertThat(result).isInstanceOf(Either.Right::class.java)
assertThat((result as Either.Right).value).isEqualTo(newApplicationId)
coVerifyOrder {
notificationsRepository.getApplicationId()
notificationsRepository.getApplicationId()
notificationsRepository.createApplicationId()
notificationsRepository.saveApplicationId(newApplicationId)
}
}
@Test
fun `GIVEN repository throws exception WHEN invoke THEN return Either Left with error`() = runTest {
// GIVEN
val expectedError = SocketTimeoutException("Test error")
coEvery { notificationsRepository.getApplicationId() } throws expectedError
// WHEN
val result = useCase()
// THEN
assertThat(result).isInstanceOf(Either.Left::class.java)
assertThat((result as Either.Left).value).isEqualTo(expectedError)
coVerify(exactly = 1) { notificationsRepository.getApplicationId() }
coVerify(inverse = true) {
notificationsRepository.createApplicationId()
notificationsRepository.saveApplicationId(any())
}
}
@Test
fun `GIVEN no local application ID WHEN multiple concurrent invokes THEN create only one application ID`() =
runTest {
// GIVEN
val newApplicationId = "new-app-id"
var isIdCreated = false
coEvery { notificationsRepository.getApplicationId() } answers {
if (!isIdCreated) null else newApplicationId
}
coEvery { notificationsRepository.createApplicationId() } answers {
isIdCreated = true
newApplicationId
}
coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit
// WHEN
val results = coroutineScope {
List(PARALLEL_COUNT) {
async {
delay(100)
useCase()
}
}.awaitAll()
}
// THEN
results.forEach { result ->
assertThat(result).isInstanceOf(Either.Right::class.java)
assertThat((result as Either.Right).value).isEqualTo(newApplicationId)
}
coVerify(exactly = PARALLEL_COUNT + 1) { notificationsRepository.getApplicationId() }
coVerify(exactly = 1) { notificationsRepository.createApplicationId() }
coVerify(exactly = 1) { notificationsRepository.saveApplicationId(newApplicationId) }
}
@Test
fun `GIVEN no local application ID WHEN multiple concurrent invokes with delay THEN create only one application ID`() = runTest {
// GIVEN
val newApplicationId = "new-app-id"
var isIdCreated = false
coEvery { notificationsRepository.getApplicationId() } answers {
if (!isIdCreated) null else newApplicationId
}
coEvery { notificationsRepository.createApplicationId() } answers {
isIdCreated = true
newApplicationId
}
coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit
// WHEN
val results = coroutineScope {
List(PARALLEL_COUNT) {
async {
delay(100)
useCase()
}
}.awaitAll()
}
// THEN
results.forEach { result ->
assertThat(result).isInstanceOf(Either.Right::class.java)
assertThat((result as Either.Right).value).isEqualTo(newApplicationId)
}
coVerify(exactly = PARALLEL_COUNT + 1) { notificationsRepository.getApplicationId() }
coVerify(exactly = 1) { notificationsRepository.createApplicationId() }
coVerify(exactly = 1) { notificationsRepository.saveApplicationId(newApplicationId) }
}
companion object {
private const val PARALLEL_COUNT = 100
}
}