Loading app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt +17 −13 Original line number Diff line number Diff line Loading @@ -42,7 +42,7 @@ class GetAppInstallationPermissionUseCaseImpl constructor( private val applicationRepository: ApplicationRepository, private val dataStoreManager: DataStoreManager, private val contentRatingRepository: GooglePlayContentRatingsRepository, private val googlePlayContentRatingsRepository: GooglePlayContentRatingsRepository, private val getParentalControlStateUseCase: GetParentalControlStateUseCase, private val playStoreRepository: PlayStoreRepository ) : GetAppInstallationPermissionUseCase { Loading @@ -57,7 +57,7 @@ constructor( is AgeGroup -> when { isFDroidApp(app) -> validateNsfwAntiFeature(app, parentalControl) else -> validateAgeLimit(app, parentalControl) else -> validateGooglePlayContentRating(app, parentalControl) } } } Loading @@ -78,19 +78,19 @@ constructor( private fun isNsfwFDroidApp(app: AppInstall) = app.antiFeatures.any { antiFeature -> antiFeature.containsKey(KEY_ANTI_FEATURES_NSFW) } private suspend fun validateAgeLimit( private suspend fun validateGooglePlayContentRating( app: AppInstall, parentalControlState: AgeGroup ): AppInstallationPermissionState { return when { isGPlayApp(app) && hasNoContentRating(app) -> DeniedOnDataLoadError hasValidAgeLimit(app, parentalControlState) -> Allowed isGooglePlayApp(app) && hasNoContentRating(app) -> DeniedOnDataLoadError hasValidContentRating(app, parentalControlState) -> Allowed else -> Denied } } private fun isGPlayApp(app: AppInstall): Boolean { private fun isGooglePlayApp(app: AppInstall): Boolean { return !isFDroidApp(app) && app.type != Type.PWA } Loading @@ -101,17 +101,21 @@ constructor( return !verifyContentRatingExists(app, authData) } private fun hasValidAgeLimit( private fun hasValidContentRating( app: AppInstall, parentalControlState: AgeGroup, ): Boolean { return when { app.contentRating.id.isBlank() -> false else -> { val allowedContentRatingGroup = contentRatingRepository.contentRatingGroups.find { googlePlayContentRatingsRepository.contentRatingGroups.find { it.id == parentalControlState.ageGroup.name } } ?: return false return (app.contentRating.id.isNotEmpty() && allowedContentRatingGroup?.ratings?.contains(app.contentRating.id) == true) allowedContentRatingGroup.ratings.contains(app.contentRating.id) } } } private suspend fun verifyContentRatingExists(app: AppInstall, authData: AuthData): Boolean { Loading app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt +184 −1 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.login.AuthenticatorRepository import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Allowed import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Denied import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.DeniedOnDataLoadError import foundation.e.apps.data.parentalcontrol.GetAppInstallationPermissionUseCaseImpl import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingGroup import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingsRepository Loading @@ -38,6 +39,7 @@ import foundation.e.apps.domain.parentalcontrol.GetParentalControlStateUseCase import foundation.e.apps.domain.parentalcontrol.model.AgeGroupValue import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState import foundation.e.apps.util.MainCoroutineRule import kotlin.test.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before Loading @@ -46,9 +48,9 @@ import org.junit.Test import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations import kotlin.test.assertEquals class GetAppInstallationPermissionUseCaseTest { // Run tasks synchronously @Rule @JvmField val instantExecutorRule = InstantTaskExecutorRule() Loading Loading @@ -294,4 +296,185 @@ class GetAppInstallationPermissionUseCaseTest { assertEquals(Denied, installationPermissionState) } } @Test fun `deny app installation on data load error when parental control is enabled and Google Play app has no content rating`() { runTest { val appPackage = "com.unit.test" val contentRatingTitle = "" val contentRatingId = contentRatingTitle.lowercase() val googlePlayContentRatingGroup = listOf( GooglePlayContentRatingGroup( id = "THREE", ageGroup = "0-3", ratings = listOf("rated for 3+") // ratings will be parsed as lowercase in real )) val email = "test@test.com" val token = "token" val contentRating = ContentRating(title = contentRatingTitle) val contentRatingWithId = ContentRating(id = contentRatingId, title = contentRatingTitle) val appPendingInstallation: AppInstall = AppInstall(packageName = appPackage).apply { this.isFDroidApp = false this.contentRating = contentRating } val application = Application(isFDroidApp = false, contentRating = contentRating) val authData = AuthData(email, token) Mockito.`when`(getParentalControlStateUseCase.invoke()) .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) Mockito.`when`(contentRatingsRepository.contentRatingGroups) .thenReturn(googlePlayContentRatingGroup) Mockito.`when`(authenticatorRepository.gplayAuth).thenReturn(authData) Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(authData) Mockito.`when`( applicationRepository.getApplicationDetails( appPendingInstallation.id, appPendingInstallation.packageName, authData, appPendingInstallation.origin)) .thenReturn(Pair(application, ResultStatus.OK)) Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) .thenReturn(contentRatingWithId) val installationPermissionState = useCase.invoke(appPendingInstallation) assertEquals(DeniedOnDataLoadError, installationPermissionState) } } @Test fun `deny app installation on data load error when parental control is enabled and Google Play app has no content rating and app details can't be loaded`() { runTest { val appPackage = "com.unit.test" val contentRatingTitle = "" val contentRatingId = contentRatingTitle.lowercase() val googlePlayContentRatingGroup = listOf( GooglePlayContentRatingGroup( id = "THREE", ageGroup = "0-3", ratings = listOf("rated for 3+") // ratings will be parsed as lowercase in real )) val email = "test@test.com" val token = "token" val contentRating = ContentRating(title = contentRatingTitle) val contentRatingWithId = ContentRating(id = contentRatingId, title = contentRatingTitle) val appPendingInstallation: AppInstall = AppInstall(packageName = appPackage).apply { this.isFDroidApp = false this.contentRating = contentRating } val application = Application(isFDroidApp = false, contentRating = contentRating) val authData = AuthData(email, token) Mockito.`when`(getParentalControlStateUseCase.invoke()) .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) Mockito.`when`(contentRatingsRepository.contentRatingGroups) .thenReturn(googlePlayContentRatingGroup) Mockito.`when`(authenticatorRepository.gplayAuth).thenReturn(authData) Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(authData) Mockito.`when`( applicationRepository.getApplicationDetails( appPendingInstallation.id, appPendingInstallation.packageName, authData, appPendingInstallation.origin)) .thenReturn(Pair(application, ResultStatus.UNKNOWN)) Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) .thenReturn(contentRatingWithId) val installationPermissionState = useCase.invoke(appPendingInstallation) assertEquals(DeniedOnDataLoadError, installationPermissionState) } } // Note: This case is unlikely to happen @Test fun `deny app installation when parental control is enabled and parental control state can't match with Google Play content ratings`() { runTest { val appPackage = "com.unit.test" val contentRatingTitle = "Rated for 3+" val contentRatingId = contentRatingTitle.lowercase() val googlePlayContentRatingGroup = listOf( GooglePlayContentRatingGroup( id = "EIGHTEEN", ageGroup = "18+", ratings = listOf("rated for 18+") // ratings will be parsed as lowercase in real )) val email = "test@test.com" val token = "token" val contentRating = ContentRating(title = contentRatingTitle) val contentRatingWithId = ContentRating(id = contentRatingId, title = contentRatingTitle) val appPendingInstallation: AppInstall = AppInstall(packageName = appPackage).apply { this.isFDroidApp = false this.contentRating = contentRating } val application = Application(isFDroidApp = false, contentRating = contentRating) val authData = AuthData(email, token) Mockito.`when`(getParentalControlStateUseCase.invoke()) .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) Mockito.`when`(contentRatingsRepository.contentRatingGroups) .thenReturn(googlePlayContentRatingGroup) Mockito.`when`(authenticatorRepository.gplayAuth).thenReturn(authData) Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(authData) Mockito.`when`( applicationRepository.getApplicationDetails( appPendingInstallation.id, appPendingInstallation.packageName, authData, appPendingInstallation.origin)) .thenReturn(Pair(application, ResultStatus.UNKNOWN)) Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) .thenReturn(contentRatingWithId) val installationPermissionState = useCase.invoke(appPendingInstallation) assertEquals(Denied, installationPermissionState) } } } Loading
app/src/main/java/foundation/e/apps/data/parentalcontrol/GetAppInstallationPermissionUseCaseImpl.kt +17 −13 Original line number Diff line number Diff line Loading @@ -42,7 +42,7 @@ class GetAppInstallationPermissionUseCaseImpl constructor( private val applicationRepository: ApplicationRepository, private val dataStoreManager: DataStoreManager, private val contentRatingRepository: GooglePlayContentRatingsRepository, private val googlePlayContentRatingsRepository: GooglePlayContentRatingsRepository, private val getParentalControlStateUseCase: GetParentalControlStateUseCase, private val playStoreRepository: PlayStoreRepository ) : GetAppInstallationPermissionUseCase { Loading @@ -57,7 +57,7 @@ constructor( is AgeGroup -> when { isFDroidApp(app) -> validateNsfwAntiFeature(app, parentalControl) else -> validateAgeLimit(app, parentalControl) else -> validateGooglePlayContentRating(app, parentalControl) } } } Loading @@ -78,19 +78,19 @@ constructor( private fun isNsfwFDroidApp(app: AppInstall) = app.antiFeatures.any { antiFeature -> antiFeature.containsKey(KEY_ANTI_FEATURES_NSFW) } private suspend fun validateAgeLimit( private suspend fun validateGooglePlayContentRating( app: AppInstall, parentalControlState: AgeGroup ): AppInstallationPermissionState { return when { isGPlayApp(app) && hasNoContentRating(app) -> DeniedOnDataLoadError hasValidAgeLimit(app, parentalControlState) -> Allowed isGooglePlayApp(app) && hasNoContentRating(app) -> DeniedOnDataLoadError hasValidContentRating(app, parentalControlState) -> Allowed else -> Denied } } private fun isGPlayApp(app: AppInstall): Boolean { private fun isGooglePlayApp(app: AppInstall): Boolean { return !isFDroidApp(app) && app.type != Type.PWA } Loading @@ -101,17 +101,21 @@ constructor( return !verifyContentRatingExists(app, authData) } private fun hasValidAgeLimit( private fun hasValidContentRating( app: AppInstall, parentalControlState: AgeGroup, ): Boolean { return when { app.contentRating.id.isBlank() -> false else -> { val allowedContentRatingGroup = contentRatingRepository.contentRatingGroups.find { googlePlayContentRatingsRepository.contentRatingGroups.find { it.id == parentalControlState.ageGroup.name } } ?: return false return (app.contentRating.id.isNotEmpty() && allowedContentRatingGroup?.ratings?.contains(app.contentRating.id) == true) allowedContentRatingGroup.ratings.contains(app.contentRating.id) } } } private suspend fun verifyContentRatingExists(app: AppInstall, authData: AuthData): Boolean { Loading
app/src/test/java/foundation/e/apps/parentalcontrol/GetAppInstallationPermissionUseCaseTest.kt +184 −1 Original line number Diff line number Diff line Loading @@ -28,6 +28,7 @@ import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.login.AuthenticatorRepository import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Allowed import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.Denied import foundation.e.apps.data.parentalcontrol.AppInstallationPermissionState.DeniedOnDataLoadError import foundation.e.apps.data.parentalcontrol.GetAppInstallationPermissionUseCaseImpl import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingGroup import foundation.e.apps.data.parentalcontrol.gplayrating.GooglePlayContentRatingsRepository Loading @@ -38,6 +39,7 @@ import foundation.e.apps.domain.parentalcontrol.GetParentalControlStateUseCase import foundation.e.apps.domain.parentalcontrol.model.AgeGroupValue import foundation.e.apps.domain.parentalcontrol.model.ParentalControlState import foundation.e.apps.util.MainCoroutineRule import kotlin.test.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before Loading @@ -46,9 +48,9 @@ import org.junit.Test import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations import kotlin.test.assertEquals class GetAppInstallationPermissionUseCaseTest { // Run tasks synchronously @Rule @JvmField val instantExecutorRule = InstantTaskExecutorRule() Loading Loading @@ -294,4 +296,185 @@ class GetAppInstallationPermissionUseCaseTest { assertEquals(Denied, installationPermissionState) } } @Test fun `deny app installation on data load error when parental control is enabled and Google Play app has no content rating`() { runTest { val appPackage = "com.unit.test" val contentRatingTitle = "" val contentRatingId = contentRatingTitle.lowercase() val googlePlayContentRatingGroup = listOf( GooglePlayContentRatingGroup( id = "THREE", ageGroup = "0-3", ratings = listOf("rated for 3+") // ratings will be parsed as lowercase in real )) val email = "test@test.com" val token = "token" val contentRating = ContentRating(title = contentRatingTitle) val contentRatingWithId = ContentRating(id = contentRatingId, title = contentRatingTitle) val appPendingInstallation: AppInstall = AppInstall(packageName = appPackage).apply { this.isFDroidApp = false this.contentRating = contentRating } val application = Application(isFDroidApp = false, contentRating = contentRating) val authData = AuthData(email, token) Mockito.`when`(getParentalControlStateUseCase.invoke()) .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) Mockito.`when`(contentRatingsRepository.contentRatingGroups) .thenReturn(googlePlayContentRatingGroup) Mockito.`when`(authenticatorRepository.gplayAuth).thenReturn(authData) Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(authData) Mockito.`when`( applicationRepository.getApplicationDetails( appPendingInstallation.id, appPendingInstallation.packageName, authData, appPendingInstallation.origin)) .thenReturn(Pair(application, ResultStatus.OK)) Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) .thenReturn(contentRatingWithId) val installationPermissionState = useCase.invoke(appPendingInstallation) assertEquals(DeniedOnDataLoadError, installationPermissionState) } } @Test fun `deny app installation on data load error when parental control is enabled and Google Play app has no content rating and app details can't be loaded`() { runTest { val appPackage = "com.unit.test" val contentRatingTitle = "" val contentRatingId = contentRatingTitle.lowercase() val googlePlayContentRatingGroup = listOf( GooglePlayContentRatingGroup( id = "THREE", ageGroup = "0-3", ratings = listOf("rated for 3+") // ratings will be parsed as lowercase in real )) val email = "test@test.com" val token = "token" val contentRating = ContentRating(title = contentRatingTitle) val contentRatingWithId = ContentRating(id = contentRatingId, title = contentRatingTitle) val appPendingInstallation: AppInstall = AppInstall(packageName = appPackage).apply { this.isFDroidApp = false this.contentRating = contentRating } val application = Application(isFDroidApp = false, contentRating = contentRating) val authData = AuthData(email, token) Mockito.`when`(getParentalControlStateUseCase.invoke()) .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) Mockito.`when`(contentRatingsRepository.contentRatingGroups) .thenReturn(googlePlayContentRatingGroup) Mockito.`when`(authenticatorRepository.gplayAuth).thenReturn(authData) Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(authData) Mockito.`when`( applicationRepository.getApplicationDetails( appPendingInstallation.id, appPendingInstallation.packageName, authData, appPendingInstallation.origin)) .thenReturn(Pair(application, ResultStatus.UNKNOWN)) Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) .thenReturn(contentRatingWithId) val installationPermissionState = useCase.invoke(appPendingInstallation) assertEquals(DeniedOnDataLoadError, installationPermissionState) } } // Note: This case is unlikely to happen @Test fun `deny app installation when parental control is enabled and parental control state can't match with Google Play content ratings`() { runTest { val appPackage = "com.unit.test" val contentRatingTitle = "Rated for 3+" val contentRatingId = contentRatingTitle.lowercase() val googlePlayContentRatingGroup = listOf( GooglePlayContentRatingGroup( id = "EIGHTEEN", ageGroup = "18+", ratings = listOf("rated for 18+") // ratings will be parsed as lowercase in real )) val email = "test@test.com" val token = "token" val contentRating = ContentRating(title = contentRatingTitle) val contentRatingWithId = ContentRating(id = contentRatingId, title = contentRatingTitle) val appPendingInstallation: AppInstall = AppInstall(packageName = appPackage).apply { this.isFDroidApp = false this.contentRating = contentRating } val application = Application(isFDroidApp = false, contentRating = contentRating) val authData = AuthData(email, token) Mockito.`when`(getParentalControlStateUseCase.invoke()) .thenReturn(ParentalControlState.AgeGroup(AgeGroupValue.THREE)) Mockito.`when`(contentRatingsRepository.contentRatingGroups) .thenReturn(googlePlayContentRatingGroup) Mockito.`when`(authenticatorRepository.gplayAuth).thenReturn(authData) Mockito.`when`(dataStoreManager.getAuthData()).thenReturn(authData) Mockito.`when`( applicationRepository.getApplicationDetails( appPendingInstallation.id, appPendingInstallation.packageName, authData, appPendingInstallation.origin)) .thenReturn(Pair(application, ResultStatus.UNKNOWN)) Mockito.`when`(playStoreRepository.getContentRatingWithId(appPackage, contentRating)) .thenReturn(contentRatingWithId) val installationPermissionState = useCase.invoke(appPendingInstallation) assertEquals(Denied, installationPermissionState) } } }