Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ public static ConnectJobRecord fromJson(JSONObject json) throws JSONException {
job.title = json.getString(META_NAME);
job.description = json.getString(META_DESCRIPTION);
job.organization = json.getString(META_ORGANIZATION);
job.projectEndDate = DateUtils.parseDate(json.getString(META_END_DATE));
job.projectStartDate = DateUtils.parseDate(json.getString(META_START_DATE));
job.projectEndDate = JsonExtensions.requireDate(json, META_END_DATE);
job.projectStartDate = JsonExtensions.requireDate(json, META_START_DATE);
job.maxVisits = json.getInt(META_MAX_VISITS_PER_USER);
job.maxDailyVisits = json.getInt(META_MAX_DAILY_VISITS);
job.budgetPerVisit = json.getInt(META_BUDGET_PER_VISIT);
Expand Down Expand Up @@ -260,11 +260,11 @@ public static ConnectJobRecord fromJson(JSONObject json) throws JSONException {
}

if (claim.has(META_END_DATE)) {
job.projectEndDate = DateUtils.parseDate(claim.getString(META_END_DATE));
job.projectEndDate = JsonExtensions.requireDate(claim, META_END_DATE);
}

if (claim.has(META_CLAIM_DATE)) {
job.dateClaimed = DateUtils.parseDate(claim.getString(META_CLAIM_DATE));
job.dateClaimed = JsonExtensions.requireDate(claim, META_CLAIM_DATE);
}

if (claim.has(META_PAYMENT_UNITS)) {
Expand Down
7 changes: 7 additions & 0 deletions app/src/org/commcare/utils/JsonExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

package org.commcare.utils

import org.javarosa.core.model.utils.DateUtils
import org.json.JSONException
import org.json.JSONObject
import java.util.Date

fun JSONObject.hasNonNull(key: String): Boolean = has(key) && !isNull(key)

Expand All @@ -16,3 +19,7 @@ fun JSONObject.optStringSafe(

/** Returns the value at [key] if it is present and not blank, otherwise null. */
fun JSONObject.optNonBlankStringSafe(key: String): String? = optStringSafe(key, null)?.takeIf { it.isNotBlank() }

fun JSONObject.requireDate(key: String): Date =
DateUtils.parseDate(getString(key))
?: throw JSONException("Unparseable date for $key: ${getString(key)}")
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,35 @@ class ConnectOpportunitiesParserTest {
put("learn_modules", JSONArray())
}

private fun claimedJobJson(
id: Int,
claim: JSONObject,
): JSONObject = validJobJson(id).apply { put("claim", claim) }

private fun claimJson(
endDate: Any,
dateClaimed: Any,
): JSONObject =
JSONObject().apply {
put("end_date", endDate)
put("date_claimed", dateClaimed)
}

private fun jsonArrayOf(vararg objects: JSONObject): ByteArrayInputStream {
val array = JSONArray().apply { objects.forEach { put(it) } }
return ByteArrayInputStream(array.toString().toByteArray())
}

private fun assertBadJobIsSkipped(badJob: JSONObject) {
val inputStream = jsonArrayOf(validJobJson(1), badJob)
every { ConnectJobUtils.storeJobs(any(), any(), any()) } returns 1
every { ConnectReleaseTogglesWorker.scheduleOneTimeFetch(any()) } just Runs
assertThrows(JSONException::class.java) {
parser.parse(200, inputStream, null)
}
verify(exactly = 1) { ConnectJobUtils.storeJobs(any(), match { it.size == 1 && it[0].jobId == 1 }, true) }
}

@Test
fun `parse returns empty list and does not call storeJobs when response body is empty`() {
val inputStream = ByteArrayInputStream("".toByteArray())
Expand Down Expand Up @@ -200,4 +224,56 @@ class ConnectOpportunitiesParserTest {

verify(exactly = 1) { FirebaseAnalyticsUtil.reportCccApiJobs(false, 0, 0) }
}

@Test
fun `parse skips job with null end_date and throws JSONException`() {
val nullDateJob = validJobJson(10).apply { put("end_date", JSONObject.NULL) }
val inputStream = jsonArrayOf(nullDateJob)
every { ConnectJobUtils.storeJobs(any(), any(), any()) } returns 0

assertThrows(JSONException::class.java) {
parser.parse(200, inputStream, null)
}
verify(exactly = 1) { ConnectJobUtils.storeJobs(any(), match { it.isEmpty() }, true) }
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Test
fun `parse stores valid jobs when one entry has a null end_date`() {
assertBadJobIsSkipped(validJobJson(10).apply { put("end_date", JSONObject.NULL) })
}

@Test
fun `parse stores valid job and skips job with malformed end_date`() {
assertBadJobIsSkipped(validJobJson(10).apply { put("end_date", "not-a-date") })
}

@Test
fun `parse stores valid job and skips job with null start_date`() {
assertBadJobIsSkipped(validJobJson(10).apply { put("start_date", JSONObject.NULL) })
}

@Test
fun `parse stores valid job and skips job with malformed start_date`() {
assertBadJobIsSkipped(validJobJson(10).apply { put("start_date", "not-a-date") })
}

@Test
fun `parse stores valid job and skips claimed job with null claim end_date`() {
assertBadJobIsSkipped(claimedJobJson(10, claimJson(JSONObject.NULL, "2025-01-15")))
}

@Test
fun `parse stores valid job and skips claimed job with malformed claim end_date`() {
assertBadJobIsSkipped(claimedJobJson(10, claimJson("not-a-date", "2025-01-15")))
}

@Test
fun `parse stores valid job and skips claimed job with null date_claimed`() {
assertBadJobIsSkipped(claimedJobJson(10, claimJson("2025-12-31", JSONObject.NULL)))
}

@Test
fun `parse stores valid job and skips claimed job with malformed date_claimed`() {
assertBadJobIsSkipped(claimedJobJson(10, claimJson("2025-12-31", "not-a-date")))
}
}
Loading