Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
51 changes: 0 additions & 51 deletions app/res/layout/connect_job_list_item_corrupt.xml

This file was deleted.

43 changes: 6 additions & 37 deletions app/src/org/commcare/adapters/JobListConnectHomeAppsAdapter.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.commcare.adapters;


import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
Expand All @@ -16,7 +15,6 @@
import org.commcare.android.database.connect.models.ConnectJobRecord;
import org.commcare.dalvik.R;
import org.commcare.dalvik.databinding.ConnectJobListItemBinding;
import org.commcare.dalvik.databinding.ConnectJobListItemCorruptBinding;
import org.commcare.dalvik.databinding.ConnectJobListItemSectionHeaderBinding;
import org.commcare.interfaces.OnJobSelectionClick;
import org.commcare.models.connect.ConnectJobListItem;
Expand All @@ -36,8 +34,7 @@ public class JobListConnectHomeAppsAdapter extends RecyclerView.Adapter<Recycler
private final ArrayList<ConnectJobListItem> displayItems = new ArrayList<>();

private static final int VIEW_TYPE_SECTION_HEADER = 0;
private static final int VIEW_TYPE_CORRUPT_JOB = 1;
private static final int VIEW_TYPE_NON_CORRUPT_JOB = 2;
private static final int VIEW_TYPE_OPPORTUNITY = 1;

public JobListConnectHomeAppsAdapter(
Context context,
Expand All @@ -61,9 +58,6 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int
ConnectJobListItemSectionHeaderBinding
.inflate(inflater, parent, false)
);
case VIEW_TYPE_CORRUPT_JOB -> new CorruptJobViewHolder(
ConnectJobListItemCorruptBinding.inflate(inflater, parent, false)
);
default -> new NonCorruptJobViewHolder(
ConnectJobListItemBinding.inflate(inflater, parent, false)
);
Expand All @@ -85,9 +79,7 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi

// Handle the job items.
ConnectJobListItem.JobItem jobItem = (ConnectJobListItem.JobItem) displayItem;
if (holder instanceof CorruptJobViewHolder corruptJobViewHolder) {
bind(corruptJobViewHolder.binding, jobItem.getJobModel());
} else if (holder instanceof NonCorruptJobViewHolder nonCorruptJobViewHolder) {
if (holder instanceof NonCorruptJobViewHolder nonCorruptJobViewHolder) {
bind(mContext, nonCorruptJobViewHolder.binding, jobItem.getJobModel(), launcher);
}
}
Expand All @@ -106,12 +98,7 @@ public int getItemViewType(int position) {
return VIEW_TYPE_SECTION_HEADER;
}

// Handle the job items.
if (((ConnectJobListItem.JobItem) displayItem).isCorrupt()) {
return VIEW_TYPE_CORRUPT_JOB;
} else {
return VIEW_TYPE_NON_CORRUPT_JOB;
}
return VIEW_TYPE_OPPORTUNITY;
}

public static class NonCorruptJobViewHolder extends RecyclerView.ViewHolder {
Expand All @@ -123,15 +110,6 @@ public NonCorruptJobViewHolder(ConnectJobListItemBinding binding) {
}
}

public static class CorruptJobViewHolder extends RecyclerView.ViewHolder {
private final ConnectJobListItemCorruptBinding binding;

public CorruptJobViewHolder(ConnectJobListItemCorruptBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}

public static class SectionHeaderViewHolder extends RecyclerView.ViewHolder {
private final ConnectJobListItemSectionHeaderBinding binding;

Expand All @@ -140,15 +118,6 @@ public SectionHeaderViewHolder(ConnectJobListItemSectionHeaderBinding binding) {
this.binding = binding;
}
}

public void bind(
ConnectJobListItemCorruptBinding binding,
ConnectLoginJobListModel connectLoginJobListModel
) {
binding.getRoot().setTag("opp_uuid: " + connectLoginJobListModel.getUuid());
binding.tvTitle.setText(connectLoginJobListModel.getName());
}

public void bind(
Context mContext,
ConnectJobListItemBinding binding,
Expand Down Expand Up @@ -373,21 +342,21 @@ private void buildDisplayList(
if (!inProgressJobs.isEmpty()) {
displayItems.add(new ConnectJobListItem.SectionHeader(R.string.connect_in_progress));
for (ConnectLoginJobListModel jobListModel : inProgressJobs) {
displayItems.add(new ConnectJobListItem.JobItem(jobListModel, false));
displayItems.add(new ConnectJobListItem.JobItem(jobListModel));
}
}

if (!newJobs.isEmpty()) {
displayItems.add(new ConnectJobListItem.SectionHeader(R.string.connect_new_opportunities));
for (ConnectLoginJobListModel jobListModel : newJobs) {
displayItems.add(new ConnectJobListItem.JobItem(jobListModel, false));
displayItems.add(new ConnectJobListItem.JobItem(jobListModel));
}
}

if (!completedJobs.isEmpty()) {
displayItems.add(new ConnectJobListItem.SectionHeader(R.string.connect_completed));
for (ConnectLoginJobListModel jobListModel : completedJobs) {
displayItems.add(new ConnectJobListItem.JobItem(jobListModel, false));
displayItems.add(new ConnectJobListItem.JobItem(jobListModel));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,23 +194,15 @@ public ConnectJobRecord() {
dailyFinishTime = "";
}

public static ConnectJobRecord corruptJobFromJson(JSONObject json) throws JSONException {
ConnectJobRecord job = new ConnectJobRecord();
job.title = json.has(META_NAME) ? json.getString(META_NAME) : "";
job.description = json.has(META_DESCRIPTION) ? json.getString(META_DESCRIPTION) : "";
job.organization = json.has(META_ORGANIZATION) ? json.getString(META_ORGANIZATION) : "";
return job;
}

public static ConnectJobRecord fromJson(JSONObject json) throws JSONException {
ConnectJobRecord job = new ConnectJobRecord();
job.jobId = json.getInt(META_JOB_ID); // This will be eventually removed
job.jobUUID = json.getString(META_JOB_UUID);
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 +252,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
3 changes: 1 addition & 2 deletions app/src/org/commcare/models/connect/ConnectJobListItem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ sealed class ConnectJobListItem {
) : ConnectJobListItem()

data class JobItem(
val jobModel: ConnectLoginJobListModel,
val isCorrupt: Boolean,
val jobModel: ConnectLoginJobListModel
) : ConnectJobListItem()
}
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