From 9b7bbe25647231c6ab38ceb46967e12ed33fb78b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Apr 2025 11:49:25 +0500 Subject: [PATCH] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../di/domain/NotificationsDomainModule.kt | 21 +++ .../DefaultNotificationsRepository.kt | 2 +- .../notifications/di/NotificationsModule.kt | 2 +- domain/notifications/build.gradle.kts | 10 +- .../notifications/GetApplicationIdUseCase.kt | 26 +++ .../NotificationsRepository.kt | 2 +- .../GetApplicationIdUseCaseTest.kt | 154 ++++++++++++++++++ 8 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt create mode 100644 domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt rename domain/notifications/src/main/java/com/tangem/domain/notifications/{ => repository}/NotificationsRepository.kt (94%) create mode 100644 domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8120e8c86e..ac8b4dfa53 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt new file mode 100644 index 0000000000..caad0bce3a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt @@ -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, + ) +} \ No newline at end of file diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt index 3f8c27c889..8648ace195 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt @@ -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 diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt b/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt index 9358da820e..555ac66802 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt @@ -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 diff --git a/domain/notifications/build.gradle.kts b/domain/notifications/build.gradle.kts index 485c40d9ab..ae59c08bd3 100644 --- a/domain/notifications/build.gradle.kts +++ b/domain/notifications/build.gradle.kts @@ -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) } \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt new file mode 100644 index 0000000000..026d3a89c6 --- /dev/null +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt @@ -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 = 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 + } + } +} \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/NotificationsRepository.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt similarity index 94% rename from domain/notifications/src/main/java/com/tangem/domain/notifications/NotificationsRepository.kt rename to domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt index bb42e3efab..5fecc4b7e4 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/NotificationsRepository.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.notifications +package com.tangem.domain.notifications.repository import com.tangem.domain.notifications.models.NotificationsEligibleNetwork diff --git a/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt b/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt new file mode 100644 index 0000000000..8f562e98b7 --- /dev/null +++ b/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt @@ -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 + } +} \ No newline at end of file