diff --git a/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/DefaultSupportRepositoryTest.java b/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/DefaultSupportRepositoryTest.java new file mode 100644 index 00000000..9f9067ed --- /dev/null +++ b/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/DefaultSupportRepositoryTest.java @@ -0,0 +1,278 @@ +package com.d4rk.androidtutorials.java.data.repository; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +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.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +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.ProductDetailsResponseListener; +import com.android.billingclient.api.QueryProductDetailsResult; +import com.d4rk.androidtutorials.java.ads.AdUtils; +import com.google.android.gms.ads.AdRequest; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +public class DefaultSupportRepositoryTest { + + private Context context; + private DefaultSupportRepository repository; + + @Before + public void setUp() { + context = mock(Context.class); + when(context.getApplicationContext()).thenReturn(context); + repository = new DefaultSupportRepository(context); + } + + @Test + public void initBillingClient_buildsClientAndRunsCallbackWhenReady() throws ReflectiveOperationException { + BillingClient.Builder builder = mock(BillingClient.Builder.class); + BillingClient billingClient = mock(BillingClient.class); + PendingPurchasesParams.Builder pendingBuilder = mock(PendingPurchasesParams.Builder.class); + PendingPurchasesParams pendingParams = mock(PendingPurchasesParams.class); + + when(builder.setListener(any())).thenReturn(builder); + when(builder.enablePendingPurchases(any())).thenReturn(builder); + when(builder.enableAutoServiceReconnection()).thenReturn(builder); + when(builder.build()).thenReturn(billingClient); + when(pendingBuilder.enableOneTimeProducts()).thenReturn(pendingBuilder); + when(pendingBuilder.build()).thenReturn(pendingParams); + + try (MockedStatic billingClientStatic = mockStatic(BillingClient.class); + MockedStatic pendingStatic = mockStatic(PendingPurchasesParams.class)) { + billingClientStatic.when(() -> BillingClient.newBuilder(context)).thenReturn(builder); + pendingStatic.when(PendingPurchasesParams::newBuilder).thenReturn(pendingBuilder); + + AtomicReference listenerRef = new AtomicReference<>(); + doAnswer(invocation -> { + BillingClientStateListener listener = invocation.getArgument(0); + listenerRef.set(listener); + return null; + }).when(billingClient).startConnection(any()); + + Runnable onConnected = mock(Runnable.class); + repository.initBillingClient(onConnected); + + billingClientStatic.verify(() -> BillingClient.newBuilder(context)); + pendingStatic.verify(PendingPurchasesParams::newBuilder); + verify(builder).setListener(any()); + verify(builder).enablePendingPurchases(pendingParams); + verify(builder).enableAutoServiceReconnection(); + verify(builder).build(); + verify(billingClient).startConnection(any()); + + BillingResult okResult = mock(BillingResult.class); + when(okResult.getResponseCode()).thenReturn(BillingClient.BillingResponseCode.OK); + listenerRef.get().onBillingSetupFinished(okResult); + verify(onConnected).run(); + + BillingResult errorResult = mock(BillingResult.class); + when(errorResult.getResponseCode()).thenReturn(BillingClient.BillingResponseCode.ERROR); + listenerRef.get().onBillingSetupFinished(errorResult); + verify(onConnected, times(1)).run(); + + setBillingClient(repository, billingClient); + } + } + + @Test + public void initBillingClient_handlesNullCallbackGracefully() { + BillingClient.Builder builder = mock(BillingClient.Builder.class); + BillingClient billingClient = mock(BillingClient.class); + PendingPurchasesParams.Builder pendingBuilder = mock(PendingPurchasesParams.Builder.class); + PendingPurchasesParams pendingParams = mock(PendingPurchasesParams.class); + + when(builder.setListener(any())).thenReturn(builder); + when(builder.enablePendingPurchases(any())).thenReturn(builder); + when(builder.enableAutoServiceReconnection()).thenReturn(builder); + when(builder.build()).thenReturn(billingClient); + when(pendingBuilder.enableOneTimeProducts()).thenReturn(pendingBuilder); + when(pendingBuilder.build()).thenReturn(pendingParams); + + try (MockedStatic billingClientStatic = mockStatic(BillingClient.class); + MockedStatic pendingStatic = mockStatic(PendingPurchasesParams.class)) { + billingClientStatic.when(() -> BillingClient.newBuilder(context)).thenReturn(builder); + pendingStatic.when(PendingPurchasesParams::newBuilder).thenReturn(pendingBuilder); + + AtomicReference listenerRef = new AtomicReference<>(); + doAnswer(invocation -> { + listenerRef.set(invocation.getArgument(0)); + return null; + }).when(billingClient).startConnection(any()); + + repository.initBillingClient(null); + BillingResult okResult = mock(BillingResult.class); + when(okResult.getResponseCode()).thenReturn(BillingClient.BillingResponseCode.OK); + listenerRef.get().onBillingSetupFinished(okResult); + } + } + + @Test + public void queryProductDetails_returnsEarlyWhenClientNull() { + SupportRepository.OnProductDetailsListener listener = mock(SupportRepository.OnProductDetailsListener.class); + repository.queryProductDetails(List.of("gold"), listener); + verifyNoInteractions(listener); + } + + @Test + public void queryProductDetails_returnsEarlyWhenClientNotReady() throws ReflectiveOperationException { + BillingClient billingClient = mock(BillingClient.class); + when(billingClient.isReady()).thenReturn(false); + setBillingClient(repository, billingClient); + + SupportRepository.OnProductDetailsListener listener = mock(SupportRepository.OnProductDetailsListener.class); + repository.queryProductDetails(List.of("gold"), listener); + verify(billingClient, never()).queryProductDetailsAsync(any(), any()); + verifyNoInteractions(listener); + } + + @Test + public void queryProductDetails_populatesCacheAndNotifiesListener() throws ReflectiveOperationException { + BillingClient billingClient = mock(BillingClient.class); + when(billingClient.isReady()).thenReturn(true); + setBillingClient(repository, billingClient); + + AtomicReference responseListenerRef = new AtomicReference<>(); + doAnswer(invocation -> { + responseListenerRef.set(invocation.getArgument(1)); + return null; + }).when(billingClient).queryProductDetailsAsync(any(), any()); + + SupportRepository.OnProductDetailsListener listener = mock(SupportRepository.OnProductDetailsListener.class); + repository.queryProductDetails(List.of("gold"), listener); + + ProductDetailsResponseListener responseListener = responseListenerRef.get(); + BillingResult okResult = mock(BillingResult.class); + when(okResult.getResponseCode()).thenReturn(BillingClient.BillingResponseCode.OK); + ProductDetails productDetails = mock(ProductDetails.class); + when(productDetails.getProductId()).thenReturn("gold"); + QueryProductDetailsResult queryResult = mock(QueryProductDetailsResult.class); + when(queryResult.getProductDetailsList()).thenReturn(List.of(productDetails)); + + responseListener.onProductDetailsResponse(okResult, queryResult); + verify(listener).onProductDetailsRetrieved(List.of(productDetails)); + + SupportRepository.BillingFlowLauncher launcher = repository.initiatePurchase("gold"); + assertNotNull(launcher); + } + + @Test + public void queryProductDetails_doesNothingWhenResultEmpty() throws ReflectiveOperationException { + BillingClient billingClient = mock(BillingClient.class); + when(billingClient.isReady()).thenReturn(true); + setBillingClient(repository, billingClient); + + AtomicReference responseListenerRef = new AtomicReference<>(); + doAnswer(invocation -> { + responseListenerRef.set(invocation.getArgument(1)); + return null; + }).when(billingClient).queryProductDetailsAsync(any(), any()); + + SupportRepository.OnProductDetailsListener listener = mock(SupportRepository.OnProductDetailsListener.class); + repository.queryProductDetails(List.of("gold"), listener); + + ProductDetailsResponseListener responseListener = responseListenerRef.get(); + BillingResult okResult = mock(BillingResult.class); + when(okResult.getResponseCode()).thenReturn(BillingClient.BillingResponseCode.OK); + QueryProductDetailsResult queryResult = mock(QueryProductDetailsResult.class); + when(queryResult.getProductDetailsList()).thenReturn(Collections.emptyList()); + responseListener.onProductDetailsResponse(okResult, queryResult); + + verifyNoInteractions(listener); + assertNull(repository.initiatePurchase("gold")); + } + + @Test + public void initiatePurchase_returnsNullWhenNoDetails() throws ReflectiveOperationException { + assertNull(repository.initiatePurchase("gold")); + + BillingClient billingClient = mock(BillingClient.class); + setBillingClient(repository, billingClient); + assertNull(repository.initiatePurchase("gold")); + } + + @Test + public void initiatePurchase_launchesBillingFlowWithStoredDetails() throws ReflectiveOperationException { + BillingClient billingClient = mock(BillingClient.class); + when(billingClient.isReady()).thenReturn(true); + setBillingClient(repository, billingClient); + + AtomicReference responseListenerRef = new AtomicReference<>(); + doAnswer(invocation -> { + responseListenerRef.set(invocation.getArgument(1)); + return null; + }).when(billingClient).queryProductDetailsAsync(any(), any()); + + ProductDetails productDetails = mock(ProductDetails.class); + when(productDetails.getProductId()).thenReturn("gold"); + ProductDetails.OneTimePurchaseOfferDetails offerDetails = mock(ProductDetails.OneTimePurchaseOfferDetails.class); + when(offerDetails.getOfferToken()).thenReturn("offer"); + when(productDetails.getOneTimePurchaseOfferDetails()).thenReturn(offerDetails); + + SupportRepository.OnProductDetailsListener listener = mock(SupportRepository.OnProductDetailsListener.class); + repository.queryProductDetails(List.of("gold"), listener); + + ProductDetailsResponseListener responseListener = responseListenerRef.get(); + BillingResult okResult = mock(BillingResult.class); + when(okResult.getResponseCode()).thenReturn(BillingClient.BillingResponseCode.OK); + QueryProductDetailsResult queryResult = mock(QueryProductDetailsResult.class); + when(queryResult.getProductDetailsList()).thenReturn(List.of(productDetails)); + responseListener.onProductDetailsResponse(okResult, queryResult); + + SupportRepository.BillingFlowLauncher launcher = repository.initiatePurchase("gold"); + assertNotNull(launcher); + + when(billingClient.launchBillingFlow(any(Activity.class), any())).thenReturn(mock(BillingResult.class)); + Activity activity = mock(Activity.class); + launcher.launch(activity); + + ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(BillingFlowParams.class); + verify(billingClient).launchBillingFlow(eq(activity), paramsCaptor.capture()); + List paramsList = paramsCaptor.getValue().getProductDetailsParamsList(); + assertEquals(1, paramsList.size()); + assertSame(productDetails, paramsList.get(0).getProductDetails()); + } + + @Test + public void initMobileAds_initializesSdkAndReturnsRequest() { + try (MockedStatic adUtils = mockStatic(AdUtils.class)) { + AdRequest request = repository.initMobileAds(); + adUtils.verify(() -> AdUtils.initialize(context)); + assertNotNull(request); + } + } + + private static void setBillingClient(DefaultSupportRepository repository, BillingClient billingClient) + throws ReflectiveOperationException { + Field field = DefaultSupportRepository.class.getDeclaredField("billingClient"); + field.setAccessible(true); + field.set(repository, billingClient); + } +} diff --git a/app/src/test/java/com/d4rk/androidtutorials/java/data/source/DefaultHomeRemoteDataSourceTest.java b/app/src/test/java/com/d4rk/androidtutorials/java/data/source/DefaultHomeRemoteDataSourceTest.java new file mode 100644 index 00000000..5f8b0eda --- /dev/null +++ b/app/src/test/java/com/d4rk/androidtutorials/java/data/source/DefaultHomeRemoteDataSourceTest.java @@ -0,0 +1,148 @@ +package com.d4rk.androidtutorials.java.data.source; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.Handler; + +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.d4rk.androidtutorials.java.data.model.PromotedApp; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; + +public class DefaultHomeRemoteDataSourceTest { + + private RequestQueue requestQueue; + private MockedStatic executorsMock; + private MockedConstruction handlerConstruction; + private MockedConstruction requestConstruction; + private Response.Listener successListener; + private Response.ErrorListener errorListener; + private int capturedMethod; + private String capturedUrl; + private DefaultHomeRemoteDataSource dataSource; + + @Before + public void setUp() { + requestQueue = mock(RequestQueue.class); + capturedMethod = -1; + capturedUrl = null; + successListener = null; + errorListener = null; + + Executor immediate = Runnable::run; + executorsMock = mockStatic(Executors.class); + executorsMock.when(Executors::newSingleThreadExecutor).thenReturn(immediate); + + handlerConstruction = mockConstruction(Handler.class, (mock, context) -> + when(mock.post(any(Runnable.class))).thenAnswer(invocation -> { + Runnable runnable = invocation.getArgument(0); + runnable.run(); + return true; + }) + ); + + requestConstruction = mockConstruction(JsonObjectRequest.class, (mock, context) -> { + capturedMethod = (int) context.arguments().get(0); + capturedUrl = (String) context.arguments().get(1); + successListener = (Response.Listener) context.arguments().get(3); + errorListener = (Response.ErrorListener) context.arguments().get(4); + }); + + dataSource = new DefaultHomeRemoteDataSource(requestQueue, "https://example.com/api"); + } + + @After + public void tearDown() { + requestConstruction.close(); + handlerConstruction.close(); + executorsMock.close(); + } + + @Test + public void fetchPromotedApps_addsRequestAndDeliversParsedList() throws JSONException { + AtomicReference> resultRef = new AtomicReference<>(); + dataSource.fetchPromotedApps(resultRef::set); + + assertEquals(Request.Method.GET, capturedMethod); + assertEquals("https://example.com/api", capturedUrl); + verify(requestQueue).add(requestConstruction.constructed().get(0)); + assertNotNull("Success listener should be captured", successListener); + + successListener.onResponse(buildResponse()); + + List result = resultRef.get(); + assertNotNull(result); + assertEquals(1, result.size()); + PromotedApp app = result.get(0); + assertEquals("Cool App", app.name()); + assertEquals("com.example.cool", app.packageName()); + assertEquals("https://example.com/icon.png", app.iconUrl()); + } + + @Test + public void fetchPromotedApps_returnsEmptyListWhenParseFails() { + AtomicReference> resultRef = new AtomicReference<>(); + dataSource.fetchPromotedApps(resultRef::set); + + assertNotNull(successListener); + successListener.onResponse(new JSONObject()); + + List result = resultRef.get(); + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + public void fetchPromotedApps_returnsEmptyListOnVolleyError() { + AtomicReference> resultRef = new AtomicReference<>(); + dataSource.fetchPromotedApps(resultRef::set); + + assertNotNull(errorListener); + errorListener.onErrorResponse(new VolleyError("boom")); + + List result = resultRef.get(); + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + private static JSONObject buildResponse() throws JSONException { + JSONObject response = new JSONObject(); + JSONObject data = new JSONObject(); + JSONArray apps = new JSONArray(); + apps.put(new JSONObject() + .put("name", "Cool App") + .put("packageName", "com.example.cool") + .put("iconLogo", "https://example.com/icon.png")); + apps.put(new JSONObject() + .put("name", "Ignore App") + .put("packageName", "com.d4rk.androidtutorials.other") + .put("iconLogo", "https://example.com/ignore.png")); + data.put("apps", apps); + response.put("data", data); + return response; + } +}