Repository: commons-app/apps-android-commons (language: kotlin)

Issue:
Add unit test for fetching campaigns
**Summary:** 

Now that we have #2793 in place we can easily test campaigns. At least we should have unit tests to verify the API layer (using mock web server). Specifically we need to test `OkHttpJsonApiClient:getCampaigns` for 2 scenarios. 

- when the API returns at least 1 campaign(active or otherwise)
- when the API returns no campaigns. 


Discussion hints:
Can i  work on this @maskaravivek 
Sure take this up. @JobGetabu 
Hi @maskaravivek! I would like to work on this issue if no one else is looking into it.
@SaumiaSinghal  Yes, sure. Assigned to you. 
Hi @maskaravivek! Thanks for assigning the issue. I'm running into an error on running the tests that I wrote, I have pointed out the error on my draft PR [here](https://github.com/commons-app/apps-android-commons/pull/3862/files#r448880632). Can you please check once whenever you have time? Thanks. 
@neslihanturan Could I work on this one as part of Hacktoberfest?
Hi, if no one else is looking into this issue I'd like to try to resolve it.

Pull request: Test/2819 add campaigns api tests
**Description (required)**

Fixes #2819

What changes did you make and why?

This PR implements the requested unit tests for the `OkHttpJsonApiClient:getCampaigns` method to ensure the API layer is working correctly.

The changes include:
1.  **Model Fixes:** Updated `CampaignConfig` and `CampaignResponseDTO` by changing their fields from immutable (`val`) to mutable (`var`). This is essential to allow the Gson library to correctly deserialize the JSON data into these Kotlin data classes during the test run and at application runtime.
2.  **Test Resources:** Added two new JSON files (`campaigns_response_with_data.json` and `campaigns_response_empty.json`) to the test resources directory to provide mock responses for the API calls.
3.  **Unit Tests:** Implemented the two required test cases in `OkHttpJsonApiClientTests.kt` to cover both scenarios: a successful response with campaign data and a successful response with an empty campaign list.

**Tests performed (required)**

Tested `ProdDebug` on `Redmi note 13 pro 5g` with API level `35`.

Test command: ./gradlew :app:testBetaDebugUnitTest --tests "fr.free.nrw.commons.OkHttpJsonApiClientTests"

Fix patch:
diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignConfig.kt b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignConfig.kt
index 6bf0bc0ed6..9f94e85927 100644
--- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignConfig.kt
+++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignConfig.kt
@@ -7,8 +7,8 @@ import com.google.gson.annotations.SerializedName
  */
 class CampaignConfig {
     @SerializedName("showOnlyLiveCampaigns")
-    private val showOnlyLiveCampaigns = false
+    var showOnlyLiveCampaigns = false
 
     @SerializedName("sortBy")
-    private val sortBy: String? = null
-}
+    var sortBy: String? = null
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignResponseDTO.kt b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignResponseDTO.kt
index 767732eb78..1656109e74 100644
--- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignResponseDTO.kt
+++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignResponseDTO.kt
@@ -8,8 +8,8 @@ import fr.free.nrw.commons.campaigns.models.Campaign
  */
 class CampaignResponseDTO {
     @SerializedName("config")
-    val campaignConfig: CampaignConfig? = null
+    var campaignConfig: CampaignConfig? = null
 
     @SerializedName("campaigns")
-    val campaigns: List<Campaign>? = null
-}
+    var campaigns: List<Campaign>? = null
+}
\ No newline at end of file


Test patch:
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/OkHttpJsonApiClientTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/OkHttpJsonApiClientTests.kt
index 9e78915603..1a9d1500b5 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/OkHttpJsonApiClientTests.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/OkHttpJsonApiClientTests.kt
@@ -3,6 +3,10 @@ package fr.free.nrw.commons
 import com.google.gson.Gson
 import com.nhaarman.mockitokotlin2.any
 import com.nhaarman.mockitokotlin2.verify
+import com.nhaarman.mockitokotlin2.times
+import fr.free.nrw.commons.campaigns.CampaignResponseDTO
+import fr.free.nrw.commons.campaigns.CampaignConfig
+import fr.free.nrw.commons.campaigns.models.Campaign
 import fr.free.nrw.commons.explore.depictions.DepictsClient
 import fr.free.nrw.commons.location.LatLng
 import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
@@ -10,13 +14,22 @@ import fr.free.nrw.commons.nearby.model.NearbyQueryParams
 import okhttp3.Call
 import okhttp3.HttpUrl
 import okhttp3.OkHttpClient
+import okhttp3.Request
 import okhttp3.Response
+import okhttp3.ResponseBody
+import okhttp3.mockwebserver.MockResponse
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
 import org.mockito.Mockito
-import org.mockito.Mockito.times
+import org.mockito.Mockito.eq
 import org.mockito.MockitoAnnotations
+import java.io.BufferedReader
+import java.io.InputStreamReader
 import java.lang.Exception
 
 class OkHttpJsonApiClientTests {
@@ -45,34 +58,43 @@ class OkHttpJsonApiClientTests {
     @Mock
     lateinit var response: Response
 
+    @Mock
+    lateinit var responseBody: ResponseBody
+
+    private lateinit var mockWebServer: TestWebServer
+
     @Before
     fun setUp() {
         MockitoAnnotations.openMocks(this)
-        okHttpJsonApiClient =
-            OkHttpJsonApiClient(
-                okhttpClient,
-                depictsClient,
-                wikiMediaToolforgeUrl,
-                sparqlQueryUrl,
-                campaignsUrl,
-                gson,
-            )
+        mockWebServer = TestWebServer()
+        mockWebServer.setUp()
+        okHttpJsonApiClient = OkHttpJsonApiClient(
+            okhttpClient,
+            depictsClient,
+            wikiMediaToolforgeUrl,
+            sparqlQueryUrl,
+            mockWebServer.getUrl(), //use the mock server for the campaignsUrl
+            gson
+        )
         Mockito.`when`(okhttpClient.newCall(any())).thenReturn(call)
         Mockito.`when`(call.execute()).thenReturn(response)
+        Mockito.`when`(response.isSuccessful).thenReturn(false)
+        Mockito.`when`(response.message).thenReturn("test")
+        Mockito.`when`(response.body).thenReturn(responseBody)
+        Mockito.`when`(responseBody.string()).thenReturn("{\"error\": \"test\"}")
     }
 
     @Test
     fun testGetNearbyPlacesCustomQuery() {
-        Mockito.`when`(response.message).thenReturn("test")
         try {
             okHttpJsonApiClient.getNearbyPlaces(latLng, "test", 10.0, "test")
         } catch (e: Exception) {
-            assert(e.message.equals("test"))
+            assertEquals("test", e.message)
         }
         try {
             okHttpJsonApiClient.getNearbyPlaces(NearbyQueryParams.Rectangular(latLng, latLng), "test", true, "test")
         } catch (e: Exception) {
-            assert(e.message.equals("test"))
+            assertEquals("test", e.message)
         }
         verify(okhttpClient, times(2)).newCall(any())
         verify(call, times(2)).execute()
@@ -80,11 +102,10 @@ class OkHttpJsonApiClientTests {
 
     @Test
     fun testGetNearbyPlaces() {
-        Mockito.`when`(response.message).thenReturn("test")
         try {
             okHttpJsonApiClient.getNearbyPlaces(latLng, "test", 10.0, null)
         } catch (e: Exception) {
-            assert(e.message.equals("test"))
+            assertEquals("test", e.message)
         }
         try {
             okHttpJsonApiClient.getNearbyPlaces(
@@ -93,9 +114,8 @@ class OkHttpJsonApiClientTests {
                 true,
                 null
             )
-
         } catch (e: Exception) {
-            assert(e.message.equals("test"))
+            assertEquals("test", e.message)
         }
         try {
             okHttpJsonApiClient.getNearbyPlaces(
@@ -105,7 +125,7 @@ class OkHttpJsonApiClientTests {
                 null
             )
         } catch (e: Exception) {
-            assert(e.message.equals("test"))
+            assertEquals("test", e.message)
         }
         verify(okhttpClient, times(3)).newCall(any())
         verify(call, times(3)).execute()
@@ -113,18 +133,121 @@ class OkHttpJsonApiClientTests {
 
     @Test
     fun testGetNearbyItemCount() {
-        Mockito.`when`(response.message).thenReturn("test")
         try {
             okHttpJsonApiClient.getNearbyItemCount(NearbyQueryParams.Radial(latLng, 10f))
         } catch (e: Exception) {
-            assert(e.message.equals("test"))
+            assertEquals("test", e.message)
         }
         try {
             okHttpJsonApiClient.getNearbyItemCount(NearbyQueryParams.Rectangular(latLng, latLng))
         } catch (e: Exception) {
-            assert(e.message.equals("test"))
+            assertEquals("test", e.message)
         }
         verify(okhttpClient, times(2)).newCall(any())
         verify(call, times(2)).execute()
     }
-}
+
+    @Test
+    fun testGetCampaignsWithData() {
+        //loads the json response from resources
+        val jsonResponse = loadJsonFromResource("campaigns_response_with_data.json")
+
+        //mocks the succesfull response chain
+        Mockito.`when`(response.isSuccessful).thenReturn(true)
+        Mockito.`when`(response.message).thenReturn("OK")
+        Mockito.`when`(response.body).thenReturn(responseBody)
+        Mockito.`when`(responseBody.string()).thenReturn(jsonResponse)
+
+        val campaignResponse = CampaignResponseDTO().apply {
+            campaignConfig = CampaignConfig().apply {
+                showOnlyLiveCampaigns = false
+                sortBy = "startDate"
+            }
+            campaigns = listOf(
+                Campaign().apply {
+                    title = "Wiki Loves Monuments"
+                    isWLMCampaign = true
+                },
+                Campaign().apply {
+                    title = "Wiki Loves Nature"
+                    isWLMCampaign = false
+                }
+            )
+        }
+
+        //any() for the string argument and eq() for the class argument.
+        Mockito.`when`(
+            gson.fromJson(
+                any<String>(),
+                eq(CampaignResponseDTO::class.java)
+            )
+        ).thenReturn(campaignResponse)
+
+        //call the getCampaigns
+        val result: CampaignResponseDTO? = okHttpJsonApiClient.getCampaigns().blockingGet()
+
+        //verify the  results
+        assertNotNull(result)
+        assertNotNull(result?.campaigns)
+        assertEquals(2, result?.campaigns!!.size)
+        assertEquals("Wiki Loves Monuments", result.campaigns!![0].title)
+        assertTrue(result.campaigns!![0].isWLMCampaign)
+        assertEquals("Wiki Loves Nature", result.campaigns!![1].title)
+        assertEquals(false, result.campaigns!![1].isWLMCampaign)
+        assertNotNull(result.campaignConfig)
+        assertFalse(result.campaignConfig!!.showOnlyLiveCampaigns)
+        assertEquals("startDate", result.campaignConfig!!.sortBy)
+    }
+
+    @Test
+    fun testGetCampaignsEmpty() {
+        //loads the empty json response
+        val jsonResponse = loadJsonFromResource("campaigns_response_empty.json")
+
+        //mocks the successful response chain
+        Mockito.`when`(response.isSuccessful).thenReturn(true)
+        Mockito.`when`(response.message).thenReturn("OK")
+        Mockito.`when`(response.body).thenReturn(responseBody)
+        Mockito.`when`(responseBody.string()).thenReturn(jsonResponse)
+
+        val campaignResponse = CampaignResponseDTO().apply {
+            campaignConfig = CampaignConfig().apply {
+                showOnlyLiveCampaigns = false
+                sortBy = "startDate"
+            }
+            campaigns = emptyList()
+        }
+
+        //use any() for the string argument and eq() for the class argument.
+        Mockito.`when`(
+            gson.fromJson(
+                any<String>(),
+                eq(CampaignResponseDTO::class.java)
+            )
+        ).thenReturn(campaignResponse)
+
+        //calls getCampaigns
+        val result: CampaignResponseDTO? = okHttpJsonApiClient.getCampaigns().blockingGet()
+
+        //verify the results
+        assertNotNull(result)
+        assertNotNull(result?.campaigns)
+        assertTrue(result?.campaigns!!.isEmpty())
+        assertNotNull(result.campaignConfig)
+        assertFalse(result.campaignConfig!!.showOnlyLiveCampaigns)
+        assertEquals("startDate", result.campaignConfig!!.sortBy)
+    }
+
+    fun loadJsonFromResource(fileName: String): String {
+        val resourcePath = "raw/$fileName"
+        //uses the classloader to find the resource in the test environment
+        val inputStream = javaClass.classLoader?.getResourceAsStream(resourcePath)
+
+        if (inputStream != null) {
+            //reads the entire stream content
+            return BufferedReader(InputStreamReader(inputStream)).use { it.readText() }
+        }
+        //throws an exception with the correct expected path
+        throw IllegalArgumentException("Resource $fileName not found. Please ensure the file is located in app/src/test/resources/raw/")
+    }
+}
\ No newline at end of file
diff --git a/app/src/test/resources/raw/campaigns_response_empty.json b/app/src/test/resources/raw/campaigns_response_empty.json
new file mode 100644
index 0000000000..61903e24c3
--- /dev/null
+++ b/app/src/test/resources/raw/campaigns_response_empty.json
@@ -0,0 +1,7 @@
+{
+  "config": {
+    "showOnlyLiveCampaigns": false,
+    "sortBy": "startDate"
+  },
+  "campaigns": []
+}
\ No newline at end of file
diff --git a/app/src/test/resources/raw/campaigns_response_with_data.json b/app/src/test/resources/raw/campaigns_response_with_data.json
new file mode 100644
index 0000000000..dab818e2ae
--- /dev/null
+++ b/app/src/test/resources/raw/campaigns_response_with_data.json
@@ -0,0 +1,24 @@
+{
+  "config": {
+    "showOnlyLiveCampaigns": false,
+    "sortBy": "startDate"
+  },
+  "campaigns": [
+    {
+      "title": "Wiki Loves Monuments",
+      "description": "A campaign to photograph monuments",
+      "startDate": "2025-09-01",
+      "endDate": "2025-09-30",
+      "link": "https://commons.wikimedia.org/wiki/Campaign:Wiki_Loves_Monuments",
+      "isWLMCampaign": true
+    },
+    {
+      "title": "Wiki Loves Nature",
+      "description": "A campaign to photograph nature",
+      "startDate": "2025-06-01",
+      "endDate": "2025-06-30",
+      "link": "https://commons.wikimedia.org/wiki/Campaign:Wiki_Loves_Nature",
+      "isWLMCampaign": false
+    }
+  ]
+}
\ No newline at end of file