diff --git a/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/AboutRepositoryTest.java b/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/AboutRepositoryTest.java new file mode 100644 index 00000000..22fc256a --- /dev/null +++ b/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/AboutRepositoryTest.java @@ -0,0 +1,47 @@ +package com.d4rk.androidtutorials.java.data.repository; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; + +import com.d4rk.androidtutorials.java.BuildConfig; +import com.d4rk.androidtutorials.java.R; +import com.d4rk.androidtutorials.java.ui.screens.about.repository.AboutRepository; + +import org.junit.Test; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Locale; + +public class AboutRepositoryTest { + + @Test + public void getVersionString_formatsFromContextTemplate() { + Context context = org.mockito.Mockito.mock(Context.class); + when(context.getApplicationContext()).thenReturn(context); + when(context.getString(R.string.app_version)).thenReturn("v%s (%d)"); + + AboutRepository repository = new AboutRepository(context); + + String expected = String.format("v%s (%d)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE); + assertEquals(expected, repository.getVersionString()); + + verify(context).getString(R.string.app_version); + } + + @Test + public void getCurrentYear_returnsFormattedYear() { + Context context = org.mockito.Mockito.mock(Context.class); + when(context.getApplicationContext()).thenReturn(context); + + AboutRepository repository = new AboutRepository(context); + + String expected = new SimpleDateFormat("yyyy", Locale.getDefault()) + .format(Calendar.getInstance().getTime()); + + assertEquals(expected, repository.getCurrentYear()); + } +} diff --git a/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/HelpRepositoryTest.java b/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/HelpRepositoryTest.java new file mode 100644 index 00000000..31d19f62 --- /dev/null +++ b/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/HelpRepositoryTest.java @@ -0,0 +1,97 @@ +package com.d4rk.androidtutorials.java.data.repository; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; + +import com.d4rk.androidtutorials.java.ui.screens.help.repository.HelpRepository; +import com.google.android.play.core.review.ReviewInfo; +import com.google.android.play.core.review.ReviewManager; +import com.google.android.play.core.review.ReviewManagerFactory; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.android.gms.tasks.Task; + +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +public class HelpRepositoryTest { + + @SuppressWarnings("unchecked") + @Test + public void requestReviewFlow_notifiesSuccessListener() { + Context context = mock(Context.class); + ReviewManager reviewManager = mock(ReviewManager.class); + Task task = mock(Task.class); + ReviewInfo reviewInfo = mock(ReviewInfo.class); + com.d4rk.androidtutorials.java.data.repository.HelpRepository.OnReviewInfoListener listener = + mock(com.d4rk.androidtutorials.java.data.repository.HelpRepository.OnReviewInfoListener.class); + + try (MockedStatic reviewManagerFactoryMockedStatic = Mockito.mockStatic(ReviewManagerFactory.class)) { + reviewManagerFactoryMockedStatic.when(() -> ReviewManagerFactory.create(context)).thenReturn(reviewManager); + when(reviewManager.requestReviewFlow()).thenReturn(task); + when(task.addOnSuccessListener(any(OnSuccessListener.class))).thenAnswer(invocation -> { + OnSuccessListener onSuccessListener = invocation.getArgument(0); + onSuccessListener.onSuccess(reviewInfo); + return task; + }); + when(task.addOnFailureListener(any(OnFailureListener.class))).thenReturn(task); + + HelpRepository repository = new HelpRepository(context); + + repository.requestReviewFlow(listener); + + verify(listener).onSuccess(reviewInfo); + } + } + + @SuppressWarnings("unchecked") + @Test + public void requestReviewFlow_notifiesFailureListener() { + Context context = mock(Context.class); + ReviewManager reviewManager = mock(ReviewManager.class); + Task task = mock(Task.class); + Exception error = new Exception("failure"); + com.d4rk.androidtutorials.java.data.repository.HelpRepository.OnReviewInfoListener listener = + mock(com.d4rk.androidtutorials.java.data.repository.HelpRepository.OnReviewInfoListener.class); + + try (MockedStatic reviewManagerFactoryMockedStatic = Mockito.mockStatic(ReviewManagerFactory.class)) { + reviewManagerFactoryMockedStatic.when(() -> ReviewManagerFactory.create(context)).thenReturn(reviewManager); + when(reviewManager.requestReviewFlow()).thenReturn(task); + when(task.addOnSuccessListener(any(OnSuccessListener.class))).thenReturn(task); + when(task.addOnFailureListener(any(OnFailureListener.class))).thenAnswer(invocation -> { + OnFailureListener onFailureListener = invocation.getArgument(0); + onFailureListener.onFailure(error); + return task; + }); + + HelpRepository repository = new HelpRepository(context); + + repository.requestReviewFlow(listener); + + verify(listener).onFailure(error); + } + } + + @Test + public void launchReviewFlow_forwardsToManager() { + Context context = mock(Context.class); + Activity activity = mock(Activity.class); + ReviewManager reviewManager = mock(ReviewManager.class); + ReviewInfo reviewInfo = mock(ReviewInfo.class); + + try (MockedStatic reviewManagerFactoryMockedStatic = Mockito.mockStatic(ReviewManagerFactory.class)) { + reviewManagerFactoryMockedStatic.when(() -> ReviewManagerFactory.create(context)).thenReturn(reviewManager); + HelpRepository repository = new HelpRepository(context); + + repository.launchReviewFlow(activity, reviewInfo); + + verify(reviewManager).launchReviewFlow(activity, reviewInfo); + } + } +} diff --git a/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/LessonRepositoryTest.java b/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/LessonRepositoryTest.java new file mode 100644 index 00000000..22e2d126 --- /dev/null +++ b/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/LessonRepositoryTest.java @@ -0,0 +1,45 @@ +package com.d4rk.androidtutorials.java.data.repository; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import com.d4rk.androidtutorials.java.R; + +import org.junit.Test; + +public class LessonRepositoryTest { + + private final com.d4rk.androidtutorials.java.ui.screens.android.repository.LessonRepository repository = + new com.d4rk.androidtutorials.java.ui.screens.android.repository.LessonRepository(); + + @Test + public void getLesson_knownLessonReturnsRecord() { + LessonRepository.Lesson expected = new LessonRepository.Lesson( + R.string.alert_dialog, + R.raw.text_alertdialog_java, + R.raw.text_center_button_xml + ); + + LessonRepository.Lesson result = repository.getLesson("AlertDialog"); + + assertEquals(expected, result); + } + + @Test + public void getLesson_emptyNameThrows() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> repository.getLesson("") + ); + assertEquals("Unknown lesson: ", exception.getMessage()); + } + + @Test + public void getLesson_unknownLessonThrows() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> repository.getLesson("Unknown") + ); + assertEquals("Unknown lesson: Unknown", exception.getMessage()); + } +} diff --git a/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/SettingsRepositoryTest.java b/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/SettingsRepositoryTest.java new file mode 100644 index 00000000..eded2c52 --- /dev/null +++ b/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/SettingsRepositoryTest.java @@ -0,0 +1,325 @@ +package com.d4rk.androidtutorials.java.data.repository; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; + +import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.os.LocaleListCompat; +import androidx.preference.PreferenceManager; + +import com.d4rk.androidtutorials.java.R; +import com.d4rk.androidtutorials.java.ui.screens.settings.repository.SettingsRepository; +import com.google.firebase.analytics.FirebaseAnalytics; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.Map; + +public class SettingsRepositoryTest { + + private static final String THEME_KEY = "theme_key"; + private static final String DEFAULT_THEME = "system"; + private static final String[] DARK_MODE_VALUES = new String[]{"system", "light", "dark", "battery"}; + private static final String LANGUAGE_KEY = "language_key"; + private static final String DEFAULT_LANGUAGE = "en"; + private static final String CONSENT_ANALYTICS = "consent_analytics"; + private static final String CONSENT_AD_STORAGE = "consent_ad_storage"; + private static final String CONSENT_AD_USER_DATA = "consent_ad_user_data"; + private static final String CONSENT_AD_PERSONALIZATION = "consent_ad_personalization"; + + private SettingsRepository createRepository(Context context, + SharedPreferences sharedPreferences, + Resources resources, + MockedStatic preferenceManagerMockedStatic) { + when(context.getApplicationContext()).thenReturn(context); + when(context.getResources()).thenReturn(resources); + preferenceManagerMockedStatic.when(() -> PreferenceManager.getDefaultSharedPreferences(context)) + .thenReturn(sharedPreferences); + return new SettingsRepository(context); + } + + private void stubCommonStrings(Context context, Resources resources) { + when(context.getString(R.string.key_theme)).thenReturn(THEME_KEY); + when(context.getString(R.string.default_value_theme)).thenReturn(DEFAULT_THEME); + when(resources.getStringArray(R.array.preference_theme_values)).thenReturn(DARK_MODE_VALUES); + when(context.getString(R.string.key_language)).thenReturn(LANGUAGE_KEY); + when(context.getString(R.string.default_value_language)).thenReturn(DEFAULT_LANGUAGE); + when(context.getString(R.string.key_consent_analytics)).thenReturn(CONSENT_ANALYTICS); + when(context.getString(R.string.key_consent_ad_storage)).thenReturn(CONSENT_AD_STORAGE); + when(context.getString(R.string.key_consent_ad_user_data)).thenReturn(CONSENT_AD_USER_DATA); + when(context.getString(R.string.key_consent_ad_personalization)).thenReturn(CONSENT_AD_PERSONALIZATION); + } + + @Test + public void applyTheme_updatesNightModeWhenPreferenceDiffers() { + Context context = mock(Context.class); + SharedPreferences sharedPreferences = mock(SharedPreferences.class); + Resources resources = mock(Resources.class); + stubCommonStrings(context, resources); + when(sharedPreferences.getString(THEME_KEY, DEFAULT_THEME)).thenReturn(null); + + try (MockedStatic preferenceManagerMockedStatic = Mockito.mockStatic(PreferenceManager.class); + MockedStatic appCompatDelegateMockedStatic = Mockito.mockStatic(AppCompatDelegate.class)) { + SettingsRepository repository = createRepository(context, sharedPreferences, resources, preferenceManagerMockedStatic); + + appCompatDelegateMockedStatic.when(AppCompatDelegate::getDefaultNightMode) + .thenReturn(AppCompatDelegate.MODE_NIGHT_YES); + + boolean changed = repository.applyTheme(); + + assertTrue(changed); + appCompatDelegateMockedStatic.verify(() -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)); + } + } + + @Test + public void applyTheme_returnsFalseWhenModeSame() { + Context context = mock(Context.class); + SharedPreferences sharedPreferences = mock(SharedPreferences.class); + Resources resources = mock(Resources.class); + stubCommonStrings(context, resources); + when(sharedPreferences.getString(THEME_KEY, DEFAULT_THEME)).thenReturn(DARK_MODE_VALUES[1]); + + try (MockedStatic preferenceManagerMockedStatic = Mockito.mockStatic(PreferenceManager.class); + MockedStatic appCompatDelegateMockedStatic = Mockito.mockStatic(AppCompatDelegate.class)) { + SettingsRepository repository = createRepository(context, sharedPreferences, resources, preferenceManagerMockedStatic); + + appCompatDelegateMockedStatic.when(AppCompatDelegate::getDefaultNightMode) + .thenReturn(AppCompatDelegate.MODE_NIGHT_NO); + + boolean changed = repository.applyTheme(); + + assertFalse(changed); + appCompatDelegateMockedStatic.verify(() -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM), never()); + appCompatDelegateMockedStatic.verify(() -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO), never()); + appCompatDelegateMockedStatic.verify(() -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES), never()); + appCompatDelegateMockedStatic.verify(() -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY), never()); + } + } + + @Test + public void handlePreferenceChange_nullKeyDoesNothing() { + Context context = mock(Context.class); + SharedPreferences sharedPreferences = mock(SharedPreferences.class); + Resources resources = mock(Resources.class); + stubCommonStrings(context, resources); + + try (MockedStatic preferenceManagerMockedStatic = Mockito.mockStatic(PreferenceManager.class)) { + SettingsRepository repository = createRepository(context, sharedPreferences, resources, preferenceManagerMockedStatic); + SettingsRepository spyRepository = Mockito.spy(repository); + doNothing().when(spyRepository).applyTheme(); + doNothing().when(spyRepository).applyLanguage(); + doNothing().when(spyRepository).applyConsent(); + + spyRepository.handlePreferenceChange(null); + + verify(spyRepository, never()).applyTheme(); + verify(spyRepository, never()).applyLanguage(); + verify(spyRepository, never()).applyConsent(); + } + } + + @Test + public void handlePreferenceChange_themeKeyCallsApplyTheme() { + Context context = mock(Context.class); + SharedPreferences sharedPreferences = mock(SharedPreferences.class); + Resources resources = mock(Resources.class); + stubCommonStrings(context, resources); + + try (MockedStatic preferenceManagerMockedStatic = Mockito.mockStatic(PreferenceManager.class)) { + SettingsRepository repository = createRepository(context, sharedPreferences, resources, preferenceManagerMockedStatic); + SettingsRepository spyRepository = Mockito.spy(repository); + doNothing().when(spyRepository).applyTheme(); + doNothing().when(spyRepository).applyLanguage(); + doNothing().when(spyRepository).applyConsent(); + + spyRepository.handlePreferenceChange(THEME_KEY); + + verify(spyRepository).applyTheme(); + verify(spyRepository, never()).applyLanguage(); + verify(spyRepository, never()).applyConsent(); + } + } + + @Test + public void handlePreferenceChange_languageKeyCallsApplyLanguage() { + Context context = mock(Context.class); + SharedPreferences sharedPreferences = mock(SharedPreferences.class); + Resources resources = mock(Resources.class); + stubCommonStrings(context, resources); + + try (MockedStatic preferenceManagerMockedStatic = Mockito.mockStatic(PreferenceManager.class)) { + SettingsRepository repository = createRepository(context, sharedPreferences, resources, preferenceManagerMockedStatic); + SettingsRepository spyRepository = Mockito.spy(repository); + doNothing().when(spyRepository).applyTheme(); + doNothing().when(spyRepository).applyLanguage(); + doNothing().when(spyRepository).applyConsent(); + + spyRepository.handlePreferenceChange(LANGUAGE_KEY); + + verify(spyRepository).applyLanguage(); + verify(spyRepository, never()).applyTheme(); + verify(spyRepository, never()).applyConsent(); + } + } + + @Test + public void handlePreferenceChange_consentKeyCallsApplyConsent() { + Context context = mock(Context.class); + SharedPreferences sharedPreferences = mock(SharedPreferences.class); + Resources resources = mock(Resources.class); + stubCommonStrings(context, resources); + + try (MockedStatic preferenceManagerMockedStatic = Mockito.mockStatic(PreferenceManager.class)) { + SettingsRepository repository = createRepository(context, sharedPreferences, resources, preferenceManagerMockedStatic); + SettingsRepository spyRepository = Mockito.spy(repository); + doNothing().when(spyRepository).applyTheme(); + doNothing().when(spyRepository).applyLanguage(); + doNothing().when(spyRepository).applyConsent(); + + spyRepository.handlePreferenceChange(CONSENT_AD_USER_DATA); + + verify(spyRepository).applyConsent(); + verify(spyRepository, never()).applyTheme(); + verify(spyRepository, never()).applyLanguage(); + } + } + + @Test + public void applyLanguage_setsApplicationLocalesFromPreference() { + Context context = mock(Context.class); + SharedPreferences sharedPreferences = mock(SharedPreferences.class); + Resources resources = mock(Resources.class); + stubCommonStrings(context, resources); + when(sharedPreferences.getString(LANGUAGE_KEY, DEFAULT_LANGUAGE)).thenReturn("es"); + + try (MockedStatic preferenceManagerMockedStatic = Mockito.mockStatic(PreferenceManager.class); + MockedStatic appCompatDelegateMockedStatic = Mockito.mockStatic(AppCompatDelegate.class)) { + SettingsRepository repository = createRepository(context, sharedPreferences, resources, preferenceManagerMockedStatic); + + repository.applyLanguage(); + + LocaleListCompat expectedLocales = LocaleListCompat.forLanguageTags("es"); + appCompatDelegateMockedStatic.verify(() -> AppCompatDelegate.setApplicationLocales(expectedLocales)); + } + } + + @Test + public void applyConsent_readsPreferencesAndUpdatesFirebase() { + Context context = mock(Context.class); + SharedPreferences sharedPreferences = mock(SharedPreferences.class); + Resources resources = mock(Resources.class); + stubCommonStrings(context, resources); + when(sharedPreferences.getBoolean(CONSENT_ANALYTICS, true)).thenReturn(true); + when(sharedPreferences.getBoolean(CONSENT_AD_STORAGE, true)).thenReturn(false); + when(sharedPreferences.getBoolean(CONSENT_AD_USER_DATA, true)).thenReturn(true); + when(sharedPreferences.getBoolean(CONSENT_AD_PERSONALIZATION, true)).thenReturn(false); + FirebaseAnalytics firebaseAnalytics = mock(FirebaseAnalytics.class); + + try (MockedStatic preferenceManagerMockedStatic = Mockito.mockStatic(PreferenceManager.class); + MockedStatic firebaseAnalyticsMockedStatic = Mockito.mockStatic(FirebaseAnalytics.class)) { + firebaseAnalyticsMockedStatic.when(() -> FirebaseAnalytics.getInstance(context)) + .thenReturn(firebaseAnalytics); + SettingsRepository repository = createRepository(context, sharedPreferences, resources, preferenceManagerMockedStatic); + + repository.applyConsent(); + + ArgumentCaptor> captor = + ArgumentCaptor.forClass(Map.class); + verify(firebaseAnalytics).setConsent(captor.capture()); + + Map value = captor.getValue(); + assertEquals(FirebaseAnalytics.ConsentStatus.GRANTED, + value.get(FirebaseAnalytics.ConsentType.ANALYTICS_STORAGE)); + assertEquals(FirebaseAnalytics.ConsentStatus.DENIED, + value.get(FirebaseAnalytics.ConsentType.AD_STORAGE)); + assertEquals(FirebaseAnalytics.ConsentStatus.GRANTED, + value.get(FirebaseAnalytics.ConsentType.AD_USER_DATA)); + assertEquals(FirebaseAnalytics.ConsentStatus.DENIED, + value.get(FirebaseAnalytics.ConsentType.AD_PERSONALIZATION)); + } + } + + @Test + public void registerPreferenceChangeListener_forwardsCall() { + Context context = mock(Context.class); + SharedPreferences sharedPreferences = mock(SharedPreferences.class); + Resources resources = mock(Resources.class); + stubCommonStrings(context, resources); + SharedPreferences.OnSharedPreferenceChangeListener listener = mock(SharedPreferences.OnSharedPreferenceChangeListener.class); + + try (MockedStatic preferenceManagerMockedStatic = Mockito.mockStatic(PreferenceManager.class)) { + SettingsRepository repository = createRepository(context, sharedPreferences, resources, preferenceManagerMockedStatic); + + repository.registerPreferenceChangeListener(listener); + + verify(sharedPreferences).registerOnSharedPreferenceChangeListener(listener); + } + } + + @Test + public void unregisterPreferenceChangeListener_forwardsCall() { + Context context = mock(Context.class); + SharedPreferences sharedPreferences = mock(SharedPreferences.class); + Resources resources = mock(Resources.class); + stubCommonStrings(context, resources); + SharedPreferences.OnSharedPreferenceChangeListener listener = mock(SharedPreferences.OnSharedPreferenceChangeListener.class); + + try (MockedStatic preferenceManagerMockedStatic = Mockito.mockStatic(PreferenceManager.class)) { + SettingsRepository repository = createRepository(context, sharedPreferences, resources, preferenceManagerMockedStatic); + + repository.unregisterPreferenceChangeListener(listener); + + verify(sharedPreferences).unregisterOnSharedPreferenceChangeListener(listener); + } + } + + @Test + public void getDarkMode_returnsStoredPreference() { + Context context = mock(Context.class); + SharedPreferences sharedPreferences = mock(SharedPreferences.class); + Resources resources = mock(Resources.class); + stubCommonStrings(context, resources); + when(sharedPreferences.getString(THEME_KEY, DEFAULT_THEME)).thenReturn("dark"); + + try (MockedStatic preferenceManagerMockedStatic = Mockito.mockStatic(PreferenceManager.class)) { + SettingsRepository repository = createRepository(context, sharedPreferences, resources, preferenceManagerMockedStatic); + + assertEquals("dark", repository.getDarkMode()); + } + } + + @Test + public void setConsentAccepted_updatesSharedPreferences() { + Context context = mock(Context.class); + SharedPreferences sharedPreferences = mock(SharedPreferences.class); + SharedPreferences.Editor editor = mock(SharedPreferences.Editor.class); + Resources resources = mock(Resources.class); + stubCommonStrings(context, resources); + when(sharedPreferences.edit()).thenReturn(editor); + when(editor.putBoolean(CONSENT_ANALYTICS, true)).thenReturn(editor); + + try (MockedStatic preferenceManagerMockedStatic = Mockito.mockStatic(PreferenceManager.class)) { + SettingsRepository repository = createRepository(context, sharedPreferences, resources, preferenceManagerMockedStatic); + + repository.setConsentAccepted(true); + + verify(sharedPreferences).edit(); + verify(editor).putBoolean(CONSENT_ANALYTICS, true); + verify(editor).apply(); + } + } +} diff --git a/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/StartupRepositoryTest.java b/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/StartupRepositoryTest.java new file mode 100644 index 00000000..52e8db19 --- /dev/null +++ b/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/StartupRepositoryTest.java @@ -0,0 +1,150 @@ +package com.d4rk.androidtutorials.java.data.repository; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; + +import com.d4rk.androidtutorials.java.ui.screens.startup.repository.StartupRepository; +import com.google.android.ump.ConsentForm; +import com.google.android.ump.ConsentInformation; +import com.google.android.ump.ConsentRequestParameters; +import com.google.android.ump.FormError; +import com.google.android.ump.UserMessagingPlatform; + +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +public class StartupRepositoryTest { + + @Test + public void requestConsentInfoUpdate_runsOnSuccessCallback() { + Context context = mock(Context.class); + Activity activity = mock(Activity.class); + ConsentRequestParameters parameters = mock(ConsentRequestParameters.class); + ConsentInformation consentInformation = mock(ConsentInformation.class); + Runnable onSuccess = mock(Runnable.class); + StartupRepository.OnFormError onFormError = mock(StartupRepository.OnFormError.class); + + try (MockedStatic umpMockedStatic = Mockito.mockStatic(UserMessagingPlatform.class)) { + umpMockedStatic.when(() -> UserMessagingPlatform.getConsentInformation(context)).thenReturn(consentInformation); + StartupRepository repository = new StartupRepository(context); + + doAnswer(invocation -> { + ConsentInformation.OnConsentInfoUpdateSuccessListener successListener = invocation.getArgument(2); + successListener.onConsentInfoUpdateSuccess(); + return null; + }).when(consentInformation).requestConsentInfoUpdate(eq(activity), eq(parameters), any(), any()); + + repository.requestConsentInfoUpdate(activity, parameters, onSuccess, onFormError); + + verify(consentInformation).requestConsentInfoUpdate(eq(activity), eq(parameters), any(), any()); + verify(onSuccess).run(); + verify(onFormError, never()).onFormError(any()); + } + } + + @Test + public void requestConsentInfoUpdate_failureInvokesOnError() { + Context context = mock(Context.class); + Activity activity = mock(Activity.class); + ConsentRequestParameters parameters = mock(ConsentRequestParameters.class); + ConsentInformation consentInformation = mock(ConsentInformation.class); + StartupRepository.OnFormError onFormError = mock(StartupRepository.OnFormError.class); + FormError formError = mock(FormError.class); + + try (MockedStatic umpMockedStatic = Mockito.mockStatic(UserMessagingPlatform.class)) { + umpMockedStatic.when(() -> UserMessagingPlatform.getConsentInformation(context)).thenReturn(consentInformation); + StartupRepository repository = new StartupRepository(context); + + doAnswer(invocation -> { + ConsentInformation.OnConsentInfoUpdateFailureListener failureListener = invocation.getArgument(3); + failureListener.onConsentInfoUpdateFailure(formError); + return null; + }).when(consentInformation).requestConsentInfoUpdate(eq(activity), eq(parameters), any(), any()); + + repository.requestConsentInfoUpdate(activity, parameters, () -> {}, onFormError); + + verify(onFormError).onFormError(formError); + } + } + + @Test + public void requestConsentInfoUpdate_failureWithNullErrorCallbackDoesNotCrash() { + Context context = mock(Context.class); + Activity activity = mock(Activity.class); + ConsentRequestParameters parameters = mock(ConsentRequestParameters.class); + ConsentInformation consentInformation = mock(ConsentInformation.class); + FormError formError = mock(FormError.class); + + try (MockedStatic umpMockedStatic = Mockito.mockStatic(UserMessagingPlatform.class)) { + umpMockedStatic.when(() -> UserMessagingPlatform.getConsentInformation(context)).thenReturn(consentInformation); + StartupRepository repository = new StartupRepository(context); + + doAnswer(invocation -> { + ConsentInformation.OnConsentInfoUpdateFailureListener failureListener = invocation.getArgument(3); + failureListener.onConsentInfoUpdateFailure(formError); + return null; + }).when(consentInformation).requestConsentInfoUpdate(eq(activity), eq(parameters), any(), any()); + + repository.requestConsentInfoUpdate(activity, parameters, () -> {}, null); + + verify(consentInformation).requestConsentInfoUpdate(eq(activity), eq(parameters), any(), any()); + } + } + + @Test + public void loadConsentForm_whenRequiredShowsForm() { + Context context = mock(Context.class); + Activity activity = mock(Activity.class); + ConsentInformation consentInformation = mock(ConsentInformation.class); + ConsentForm consentForm = mock(ConsentForm.class); + + try (MockedStatic umpMockedStatic = Mockito.mockStatic(UserMessagingPlatform.class)) { + umpMockedStatic.when(() -> UserMessagingPlatform.getConsentInformation(context)).thenReturn(consentInformation); + StartupRepository repository = new StartupRepository(context); + when(consentInformation.getConsentStatus()).thenReturn(ConsentInformation.ConsentStatus.REQUIRED); + + umpMockedStatic.when(() -> UserMessagingPlatform.loadConsentForm(eq(activity), any(), any())).thenAnswer(invocation -> { + UserMessagingPlatform.OnConsentFormLoadSuccessListener successListener = invocation.getArgument(1); + successListener.onConsentFormLoadSuccess(consentForm); + return null; + }); + + repository.loadConsentForm(activity, null); + + verify(consentForm).show(eq(activity), any()); + } + } + + @Test + public void loadConsentForm_failureInvokesOnError() { + Context context = mock(Context.class); + Activity activity = mock(Activity.class); + ConsentInformation consentInformation = mock(ConsentInformation.class); + StartupRepository.OnFormError onFormError = mock(StartupRepository.OnFormError.class); + FormError formError = mock(FormError.class); + + try (MockedStatic umpMockedStatic = Mockito.mockStatic(UserMessagingPlatform.class)) { + umpMockedStatic.when(() -> UserMessagingPlatform.getConsentInformation(context)).thenReturn(consentInformation); + StartupRepository repository = new StartupRepository(context); + + umpMockedStatic.when(() -> UserMessagingPlatform.loadConsentForm(eq(activity), any(), any())).thenAnswer(invocation -> { + UserMessagingPlatform.OnConsentFormLoadFailureListener failureListener = invocation.getArgument(2); + failureListener.onConsentFormLoadFailure(formError); + return null; + }); + + repository.loadConsentForm(activity, onFormError); + + verify(onFormError).onFormError(formError); + } + } +} diff --git a/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/SupportRepositoryTest.java b/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/SupportRepositoryTest.java new file mode 100644 index 00000000..1aab0b52 --- /dev/null +++ b/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/SupportRepositoryTest.java @@ -0,0 +1,255 @@ +package com.d4rk.androidtutorials.java.data.repository; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; + +import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.BillingClientStateListener; +import com.android.billingclient.api.BillingFlowParams; +import com.android.billingclient.api.BillingResult; +import com.android.billingclient.api.PendingPurchasesParams; +import com.android.billingclient.api.ProductDetails; +import com.android.billingclient.api.QueryProductDetailsParams; +import com.d4rk.androidtutorials.java.ads.AdUtils; +import com.google.android.gms.ads.AdRequest; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.lang.reflect.Method; +import java.util.List; + +public class SupportRepositoryTest { + + private DefaultSupportRepository createRepository(Context context, + BillingClient billingClient, + BillingClient.Builder builder, + PendingPurchasesParams.Builder pendingBuilder, + PendingPurchasesParams pendingParams, + MockedStatic billingClientMockedStatic, + MockedStatic pendingPurchasesMockedStatic) { + when(context.getApplicationContext()).thenReturn(context); + billingClientMockedStatic.when(() -> BillingClient.newBuilder(context)).thenReturn(builder); + when(builder.setListener(any())).thenReturn(builder); + when(builder.enablePendingPurchases(any(PendingPurchasesParams.class))).thenReturn(builder); + when(builder.enableAutoServiceReconnection()).thenReturn(builder); + when(builder.build()).thenReturn(billingClient); + pendingPurchasesMockedStatic.when(PendingPurchasesParams::newBuilder).thenReturn(pendingBuilder); + when(pendingBuilder.enableOneTimeProducts()).thenReturn(pendingBuilder); + when(pendingBuilder.build()).thenReturn(pendingParams); + doNothing().when(billingClient).startConnection(any()); + return new DefaultSupportRepository(context); + } + + @Test + public void initBillingClient_invokesCallbackWhenConnected() { + Context context = mock(Context.class); + BillingClient billingClient = mock(BillingClient.class); + BillingClient.Builder builder = mock(BillingClient.Builder.class); + PendingPurchasesParams.Builder pendingBuilder = mock(PendingPurchasesParams.Builder.class); + PendingPurchasesParams pendingParams = mock(PendingPurchasesParams.class); + Runnable onConnected = mock(Runnable.class); + BillingResult billingResult = mock(BillingResult.class); + when(billingResult.getResponseCode()).thenReturn(BillingClient.BillingResponseCode.OK); + + try (MockedStatic billingClientMockedStatic = Mockito.mockStatic(BillingClient.class); + MockedStatic pendingPurchasesMockedStatic = Mockito.mockStatic(PendingPurchasesParams.class)) { + DefaultSupportRepository repository = createRepository(context, billingClient, builder, pendingBuilder, pendingParams, + billingClientMockedStatic, pendingPurchasesMockedStatic); + + doAnswer(invocation -> { + BillingClientStateListener listener = invocation.getArgument(0); + listener.onBillingSetupFinished(billingResult); + return null; + }).when(billingClient).startConnection(any()); + + repository.initBillingClient(onConnected); + + verify(billingClient).startConnection(any()); + verify(onConnected).run(); + } + } + + @Test + public void queryProductDetails_notifiesListenerAndCachesProducts() throws Exception { + Context context = mock(Context.class); + BillingClient billingClient = mock(BillingClient.class); + BillingClient.Builder builder = mock(BillingClient.Builder.class); + PendingPurchasesParams.Builder pendingBuilder = mock(PendingPurchasesParams.Builder.class); + PendingPurchasesParams pendingParams = mock(PendingPurchasesParams.class); + BillingResult billingResult = mock(BillingResult.class); + when(billingResult.getResponseCode()).thenReturn(BillingClient.BillingResponseCode.OK); + ProductDetails productDetails = mock(ProductDetails.class); + when(productDetails.getProductId()).thenReturn("supporter"); + ProductDetails.OneTimePurchaseOfferDetails offerDetails = mock(ProductDetails.OneTimePurchaseOfferDetails.class); + when(offerDetails.getOfferToken()).thenReturn("token"); + when(productDetails.getOneTimePurchaseOfferDetails()).thenReturn(offerDetails); + List productDetailsList = List.of(productDetails); + SupportRepository.OnProductDetailsListener listener = mock(SupportRepository.OnProductDetailsListener.class); + + try (MockedStatic billingClientMockedStatic = Mockito.mockStatic(BillingClient.class); + MockedStatic pendingPurchasesMockedStatic = Mockito.mockStatic(PendingPurchasesParams.class)) { + DefaultSupportRepository repository = createRepository(context, billingClient, builder, pendingBuilder, pendingParams, + billingClientMockedStatic, pendingPurchasesMockedStatic); + repository.initBillingClient(null); + when(billingClient.isReady()).thenReturn(true); + + doAnswer(invocation -> { + Object callback = invocation.getArgument(1); + Class listenerType = callback.getClass().getInterfaces()[0]; + Method method = listenerType.getDeclaredMethods()[0]; + method.setAccessible(true); + Class resultType = method.getParameterTypes()[1]; + Object secondParameter; + if (List.class.isAssignableFrom(resultType)) { + secondParameter = productDetailsList; + } else { + secondParameter = Mockito.mock(resultType, invocationOnMock -> { + if ("getProductDetailsList".equals(invocationOnMock.getMethod().getName())) { + return productDetailsList; + } + return Mockito.RETURNS_DEFAULTS.answer(invocationOnMock); + }); + } + method.invoke(callback, billingResult, secondParameter); + return null; + }).when(billingClient).queryProductDetailsAsync(any(QueryProductDetailsParams.class), any()); + + repository.queryProductDetails(List.of("supporter"), listener); + + verify(listener).onProductDetailsRetrieved(productDetailsList); + SupportRepository.BillingFlowLauncher launcher = repository.initiatePurchase("supporter"); + assertNotNull(launcher); + } + } + + @Test + public void queryProductDetails_whenClientNotReadyDoesNothing() { + Context context = mock(Context.class); + BillingClient billingClient = mock(BillingClient.class); + BillingClient.Builder builder = mock(BillingClient.Builder.class); + PendingPurchasesParams.Builder pendingBuilder = mock(PendingPurchasesParams.Builder.class); + PendingPurchasesParams pendingParams = mock(PendingPurchasesParams.class); + SupportRepository.OnProductDetailsListener listener = mock(SupportRepository.OnProductDetailsListener.class); + + try (MockedStatic billingClientMockedStatic = Mockito.mockStatic(BillingClient.class); + MockedStatic pendingPurchasesMockedStatic = Mockito.mockStatic(PendingPurchasesParams.class)) { + DefaultSupportRepository repository = createRepository(context, billingClient, builder, pendingBuilder, pendingParams, + billingClientMockedStatic, pendingPurchasesMockedStatic); + repository.initBillingClient(null); + when(billingClient.isReady()).thenReturn(false); + + repository.queryProductDetails(List.of("supporter"), listener); + + verify(billingClient, never()).queryProductDetailsAsync(any(QueryProductDetailsParams.class), any()); + verify(listener, never()).onProductDetailsRetrieved(anyList()); + } + } + + @Test + public void initiatePurchase_launchesBillingFlowWithCachedProduct() { + Context context = mock(Context.class); + BillingClient billingClient = mock(BillingClient.class); + BillingClient.Builder builder = mock(BillingClient.Builder.class); + PendingPurchasesParams.Builder pendingBuilder = mock(PendingPurchasesParams.Builder.class); + PendingPurchasesParams pendingParams = mock(PendingPurchasesParams.class); + BillingResult billingResult = mock(BillingResult.class); + when(billingResult.getResponseCode()).thenReturn(BillingClient.BillingResponseCode.OK); + ProductDetails productDetails = mock(ProductDetails.class); + when(productDetails.getProductId()).thenReturn("supporter"); + ProductDetails.OneTimePurchaseOfferDetails offerDetails = mock(ProductDetails.OneTimePurchaseOfferDetails.class); + when(offerDetails.getOfferToken()).thenReturn("token"); + when(productDetails.getOneTimePurchaseOfferDetails()).thenReturn(offerDetails); + List productDetailsList = List.of(productDetails); + + try (MockedStatic billingClientMockedStatic = Mockito.mockStatic(BillingClient.class); + MockedStatic pendingPurchasesMockedStatic = Mockito.mockStatic(PendingPurchasesParams.class)) { + DefaultSupportRepository repository = createRepository(context, billingClient, builder, pendingBuilder, pendingParams, + billingClientMockedStatic, pendingPurchasesMockedStatic); + repository.initBillingClient(null); + when(billingClient.isReady()).thenReturn(true); + + doAnswer(invocation -> { + Object callback = invocation.getArgument(1); + Class listenerType = callback.getClass().getInterfaces()[0]; + Method method = listenerType.getDeclaredMethods()[0]; + method.setAccessible(true); + Class resultType = method.getParameterTypes()[1]; + Object secondParameter; + if (List.class.isAssignableFrom(resultType)) { + secondParameter = productDetailsList; + } else { + secondParameter = Mockito.mock(resultType, invocationOnMock -> { + if ("getProductDetailsList".equals(invocationOnMock.getMethod().getName())) { + return productDetailsList; + } + return Mockito.RETURNS_DEFAULTS.answer(invocationOnMock); + }); + } + method.invoke(callback, billingResult, secondParameter); + return null; + }).when(billingClient).queryProductDetailsAsync(any(QueryProductDetailsParams.class), any()); + + repository.queryProductDetails(List.of("supporter"), null); + + SupportRepository.BillingFlowLauncher launcher = repository.initiatePurchase("supporter"); + assertNotNull(launcher); + + Activity activity = mock(Activity.class); + BillingResult launchResult = mock(BillingResult.class); + when(billingClient.launchBillingFlow(eq(activity), any(BillingFlowParams.class))).thenReturn(launchResult); + + launcher.launch(activity); + + ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(BillingFlowParams.class); + verify(billingClient).launchBillingFlow(eq(activity), paramsCaptor.capture()); + assertNotNull(paramsCaptor.getValue()); + } + } + + @Test + public void initiatePurchase_returnsNullWhenProductMissing() { + Context context = mock(Context.class); + BillingClient billingClient = mock(BillingClient.class); + BillingClient.Builder builder = mock(BillingClient.Builder.class); + PendingPurchasesParams.Builder pendingBuilder = mock(PendingPurchasesParams.Builder.class); + PendingPurchasesParams pendingParams = mock(PendingPurchasesParams.class); + + try (MockedStatic billingClientMockedStatic = Mockito.mockStatic(BillingClient.class); + MockedStatic pendingPurchasesMockedStatic = Mockito.mockStatic(PendingPurchasesParams.class)) { + DefaultSupportRepository repository = createRepository(context, billingClient, builder, pendingBuilder, pendingParams, + billingClientMockedStatic, pendingPurchasesMockedStatic); + + assertNull(repository.initiatePurchase("unknown")); + } + } + + @Test + public void initMobileAds_initializesAndReturnsRequest() { + Context context = mock(Context.class); + when(context.getApplicationContext()).thenReturn(context); + + try (MockedStatic adUtilsMockedStatic = Mockito.mockStatic(AdUtils.class)) { + DefaultSupportRepository repository = new DefaultSupportRepository(context); + + AdRequest adRequest = repository.initMobileAds(); + + adUtilsMockedStatic.verify(() -> AdUtils.initialize(context)); + assertNotNull(adRequest); + } + } +}