From b998c6419ced470b26f1b6e0d4450e34174098e6 Mon Sep 17 00:00:00 2001 From: teleivo Date: Wed, 4 Sep 2024 09:32:29 +0200 Subject: [PATCH] fix: use tracker importer in EventSavingSMSListener --- .../imports/sms/EventSavingSMSListener.java | 8 +- .../imports/sms/TrackerEventSMSListener.java | 297 +++++++++++++---- .../sms/TrackerEventSMSListenerTest.java | 311 ------------------ ...tSMSTest.java => TrackerEventSMSTest.java} | 122 ++++++- 4 files changed, 348 insertions(+), 390 deletions(-) delete mode 100644 dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/sms/TrackerEventSMSListenerTest.java rename dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/{TrackerDeleteEventSMSTest.java => TrackerEventSMSTest.java} (73%) diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/EventSavingSMSListener.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/EventSavingSMSListener.java index bc94f1abbd06..3905b1459c0e 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/EventSavingSMSListener.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/EventSavingSMSListener.java @@ -136,8 +136,8 @@ protected List saveEvent( User user, List values, SmsEventStatus eventStatus, - Date eventDate, - Date dueDate, + Date occurredDate, + Date scheduledDate, GeoPoint coordinates) { ArrayList errorUids = new ArrayList<>(); @@ -163,8 +163,8 @@ protected List saveEvent( event.setOrganisationUnit(orgUnit); event.setProgramStage(programStage); event.setEnrollment(enrollment); - event.setOccurredDate(eventDate); - event.setScheduledDate(dueDate); + event.setOccurredDate(occurredDate); + event.setScheduledDate(scheduledDate); event.setAttributeOptionCombo(aoc); event.setStoredBy(user.getUsername()); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/TrackerEventSMSListener.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/TrackerEventSMSListener.java index 368261c2359d..1fa818f700ff 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/TrackerEventSMSListener.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/sms/TrackerEventSMSListener.java @@ -27,48 +27,58 @@ */ package org.hisp.dhis.tracker.imports.sms; +import java.time.Instant; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.category.CategoryService; import org.hisp.dhis.common.IdentifiableObjectManager; +import org.hisp.dhis.common.IllegalQueryException; +import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dataelement.DataElementService; -import org.hisp.dhis.external.conf.DhisConfigurationProvider; -import org.hisp.dhis.feedback.ForbiddenException; -import org.hisp.dhis.feedback.NotFoundException; -import org.hisp.dhis.fileresource.FileResourceService; +import org.hisp.dhis.event.EventStatus; +import org.hisp.dhis.eventdatavalue.EventDataValue; import org.hisp.dhis.message.MessageSender; -import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.organisationunit.OrganisationUnitService; -import org.hisp.dhis.program.Enrollment; +import org.hisp.dhis.program.Event; import org.hisp.dhis.program.ProgramService; -import org.hisp.dhis.program.ProgramStage; -import org.hisp.dhis.program.ProgramStageService; import org.hisp.dhis.sms.incoming.IncomingSms; import org.hisp.dhis.sms.incoming.IncomingSmsService; +import org.hisp.dhis.sms.listener.CompressionSMSListener; import org.hisp.dhis.sms.listener.SMSProcessingException; +import org.hisp.dhis.smscompression.SmsConsts.SmsEventStatus; import org.hisp.dhis.smscompression.SmsConsts.SubmissionType; import org.hisp.dhis.smscompression.SmsResponse; +import org.hisp.dhis.smscompression.models.GeoPoint; import org.hisp.dhis.smscompression.models.SmsSubmission; import org.hisp.dhis.smscompression.models.TrackerEventSmsSubmission; -import org.hisp.dhis.smscompression.models.Uid; +import org.hisp.dhis.system.util.ValidationUtils; import org.hisp.dhis.trackedentity.TrackedEntityAttributeService; import org.hisp.dhis.trackedentity.TrackedEntityTypeService; -import org.hisp.dhis.tracker.export.enrollment.EnrollmentService; -import org.hisp.dhis.tracker.export.event.EventChangeLogService; -import org.hisp.dhis.tracker.export.event.EventService; +import org.hisp.dhis.tracker.imports.TrackerImportParams; +import org.hisp.dhis.tracker.imports.TrackerImportService; +import org.hisp.dhis.tracker.imports.TrackerImportStrategy; +import org.hisp.dhis.tracker.imports.domain.Event.EventBuilder; +import org.hisp.dhis.tracker.imports.domain.MetadataIdentifier; +import org.hisp.dhis.tracker.imports.domain.TrackerObjects; +import org.hisp.dhis.tracker.imports.report.ImportReport; +import org.hisp.dhis.tracker.imports.report.Status; import org.hisp.dhis.user.User; -import org.hisp.dhis.user.UserDetails; import org.hisp.dhis.user.UserService; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @Component("org.hisp.dhis.tracker.sms.TrackerEventSMSListener") @Transactional -public class TrackerEventSMSListener extends EventSavingSMSListener { - private final ProgramStageService programStageService; - - private final EnrollmentService enrollmentService; +public class TrackerEventSMSListener extends CompressionSMSListener { + private final TrackerImportService trackerImportService; public TrackerEventSMSListener( IncomingSmsService incomingSmsService, @@ -81,12 +91,7 @@ public TrackerEventSMSListener( CategoryService categoryService, DataElementService dataElementService, IdentifiableObjectManager identifiableObjectManager, - EventService eventService, - EventChangeLogService eventChangeLogService, - FileResourceService fileResourceService, - DhisConfigurationProvider config, - ProgramStageService programStageService, - EnrollmentService enrollmentService) { + TrackerImportService trackerImportService) { super( incomingSmsService, smsSender, @@ -97,13 +102,8 @@ public TrackerEventSMSListener( organisationUnitService, categoryService, dataElementService, - identifiableObjectManager, - eventService, - eventChangeLogService, - fileResourceService, - config); - this.programStageService = programStageService; - this.enrollmentService = enrollmentService; + identifiableObjectManager); + this.trackerImportService = trackerImportService; } @Override @@ -111,54 +111,219 @@ protected SmsResponse postProcess(IncomingSms sms, SmsSubmission submission, Use throws SMSProcessingException { TrackerEventSmsSubmission subm = (TrackerEventSmsSubmission) submission; - Uid ouid = subm.getOrgUnit(); - Uid stageid = subm.getProgramStage(); - Uid enrolmentid = subm.getEnrollment(); - Uid aocid = subm.getAttributeOptionCombo(); + // TODO(ivo) which fields are optional? guard against NPEs + EventBuilder event = + org.hisp.dhis.tracker.imports.domain.Event.builder() + .event(subm.getEvent().getUid()) + .enrollment(subm.getEnrollment().getUid()) + .orgUnit(MetadataIdentifier.ofUid(subm.getOrgUnit().getUid())) + .programStage(MetadataIdentifier.ofUid(subm.getProgramStage().getUid())) + .attributeOptionCombo(MetadataIdentifier.ofUid(subm.getAttributeOptionCombo().getUid())) + .storedBy(user.getUsername()) + .occurredAt(subm.getEventDate().toInstant()) + .scheduledAt(subm.getDueDate().toInstant()) + .status(map(subm.getEventStatus())) + .geometry(map(subm.getCoordinates())); + + if (subm.getEventStatus() == SmsEventStatus.COMPLETED) { + event.completedBy(user.getUsername()); + event.completedAt(Instant.now()); + } + + TrackerImportParams params = + TrackerImportParams.builder() + // TODO(ivo) don't forget to set this to CREATE_UPDATE I just want to see my test fail + // first + .importStrategy(TrackerImportStrategy.CREATE) + .userId( + user.getUid()) // SMS processing is done inside a job executed as the user that sent + // the SMS. We might want to remove the params user in favor of the currentUser set on + // the thread. + .build(); + TrackerObjects trackerObjects = TrackerObjects.builder().events(List.of(event.build())).build(); + ImportReport importReport = trackerImportService.importTracker(params, trackerObjects); + + if (Status.OK == importReport.getStatus()) { + return SmsResponse.SUCCESS; + } + // TODO(DHIS2-18003) we need to map tracker import report errors/warnings to an sms + return SmsResponse.INVALID_EVENT.set(subm.getEvent()); + + // ArrayList errorUids = new ArrayList<>(); + // List values = subm.getValues(); + // Map dataElementsAndEventDataValues = new HashMap<>(); + // if (values != null) { + // for (SmsDataValue dv : values) { + // Uid deid = dv.getDataElement(); + // String val = dv.getValue(); + // + // DataElement de = dataElementService.getDataElement(deid.getUid()); + // + // // TODO: Is this the correct way of handling errors here? + // if (de == null) { + //// log.warn( + //// String.format( + //// "Given data element [%s] could not be found. Continuing with + // submission...", + //// deid)); + // errorUids.add(deid); + // + // continue; + // } else if (val == null || StringUtils.isEmpty(val)) { + //// log.warn( + //// String.format( + //// "Value for atttribute [%s] is null or empty. Continuing with + // submission...", + //// deid)); + // continue; + // } + // + // EventDataValue eventDataValue = + // new EventDataValue(deid.getUid(), dv.getValue(), currentUserInfo); + // eventDataValue.setAutoFields(); + // dataElementsAndEventDataValues.put(de, eventDataValue); + // } + // } + // + // saveEventDataValuesAndSaveEvent(event, dataElementsAndEventDataValues); + // + // List errorUIDs = errorUids; + // + // if (!errorUIDs.isEmpty()) { + // return SmsResponse.WARN_DVERR.setList(errorUIDs); + // } else if (subm.getValues() == null || subm.getValues().isEmpty()) { + // // TODO: Should we save the event if there are no data values? + // return SmsResponse.WARN_DVEMPTY; + // } + // + // return SmsResponse.SUCCESS; + } + + private EventStatus map(SmsEventStatus eventStatus) { + return switch (eventStatus) { + case ACTIVE -> EventStatus.ACTIVE; + case COMPLETED -> EventStatus.COMPLETED; + case VISITED -> EventStatus.VISITED; + case SCHEDULE -> EventStatus.SCHEDULE; + case OVERDUE -> EventStatus.OVERDUE; + case SKIPPED -> EventStatus.SKIPPED; + }; + } + + private Geometry map(GeoPoint coordinates) { + if (coordinates == null) { + return null; + } + + GeometryFactory gf = new GeometryFactory(); + Coordinate co = new Coordinate(coordinates.getLongitude(), coordinates.getLatitude()); + + return gf.createPoint(co); + } + + private void saveEventDataValuesAndSaveEvent( + Event event, Map dataElementEventDataValueMap) { + validateEventDataValues(dataElementEventDataValueMap); + Set eventDataValues = new HashSet<>(dataElementEventDataValueMap.values()); + event.setEventDataValues(eventDataValues); - OrganisationUnit orgUnit = organisationUnitService.getOrganisationUnit(ouid.getUid()); + event.setAutoFields(); + if (!event.hasAttributeOptionCombo()) { + CategoryOptionCombo aoc = categoryService.getDefaultCategoryOptionCombo(); + event.setAttributeOptionCombo(aoc); + } + manager.save(event); - Enrollment enrollment; - try { - enrollment = - enrollmentService.getEnrollment(enrolmentid.getUid(), UserDetails.fromUser(user)); - } catch (ForbiddenException | NotFoundException e) { - throw new SMSProcessingException(SmsResponse.INVALID_ENROLL.set(enrolmentid)); + for (Map.Entry entry : dataElementEventDataValueMap.entrySet()) { + entry.getValue().setAutoFields(); + // TODO(ivo) this is done by the importer, right? + // createAndAddChangeLog(entry.getValue(), entry.getKey(), event); + handleFileDataValueSave(entry.getValue(), entry.getKey()); } + } + + private String validateEventDataValue(DataElement dataElement, EventDataValue eventDataValue) { - ProgramStage programStage = programStageService.getProgramStage(stageid.getUid()); - if (programStage == null) { - throw new SMSProcessingException(SmsResponse.INVALID_STAGE.set(stageid)); + if (StringUtils.isEmpty(eventDataValue.getStoredBy())) { + return "Stored by is null or empty"; } - CategoryOptionCombo aoc = categoryService.getCategoryOptionCombo(aocid.getUid()); - if (aoc == null) { - throw new SMSProcessingException(SmsResponse.INVALID_AOC.set(aocid)); + if (StringUtils.isEmpty(eventDataValue.getDataElement())) { + return "Data element is null or empty"; } - List errorUIDs = - saveEvent( - subm.getEvent().getUid(), - orgUnit, - programStage, - enrollment, - aoc, - user, - subm.getValues(), - subm.getEventStatus(), - subm.getEventDate(), - subm.getDueDate(), - subm.getCoordinates()); - if (!errorUIDs.isEmpty()) { - return SmsResponse.WARN_DVERR.setList(errorUIDs); - } else if (subm.getValues() == null || subm.getValues().isEmpty()) { - // TODO: Should we save the event if there are no data values? - return SmsResponse.WARN_DVEMPTY; + if (!dataElement.getUid().equals(eventDataValue.getDataElement())) { + throw new IllegalQueryException( + "DataElement " + + dataElement.getUid() + + " assigned to EventDataValues does not match with one EventDataValue: " + + eventDataValue.getDataElement()); } - return SmsResponse.SUCCESS; + String result = + ValidationUtils.valueIsValid(eventDataValue.getValue(), dataElement.getValueType()); + + return result == null ? null : "Value is not valid: " + result; + } + + private void validateEventDataValues( + Map dataElementEventDataValueMap) { + String result; + for (Map.Entry entry : dataElementEventDataValueMap.entrySet()) { + result = validateEventDataValue(entry.getKey(), entry.getValue()); + if (result != null) { + throw new IllegalQueryException(result); + } + } } + private void createAndAddChangeLog( + EventDataValue dataValue, DataElement dataElement, Event event) { + // if (!config.isEnabled(CHANGELOG_TRACKER) || dataElement == null) { + // return; + // } + // + // TrackedEntityDataValueChangeLog dataValueChangeLog = + // new TrackedEntityDataValueChangeLog( + // dataElement, + // event, + // dataValue.getValue(), + // dataValue.getStoredBy(), + // dataValue.getProvidedElsewhere(), + // ChangeLogType.CREATE); + // + // eventChangeLogService.addTrackedEntityDataValueChangeLog(dataValueChangeLog); + } + + /** Update FileResource with 'assigned' status. */ + private void handleFileDataValueSave(EventDataValue dataValue, DataElement dataElement) { + if (dataElement == null) { + return; + } + + // TODO(ivo) what is this about? + // FileResource fileResource = fetchFileResource(dataValue, dataElement); + // + // if (fileResource == null) { + // return; + // } + // + // setAssigned(fileResource); + } + + // private FileResource fetchFileResource(EventDataValue dataValue, DataElement dataElement) { + // if (!dataElement.isFileType()) { + // return null; + // } + // + // return fileResourceService.getFileResource(dataValue.getValue()); + // } + // + // private void setAssigned(FileResource fileResource) { + // fileResource.setAssigned(true); + // fileResourceService.updateFileResource(fileResource); + // } + @Override protected boolean handlesType(SubmissionType type) { return (type == SubmissionType.TRACKER_EVENT); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/sms/TrackerEventSMSListenerTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/sms/TrackerEventSMSListenerTest.java deleted file mode 100644 index 9fc351b684a4..000000000000 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/sms/TrackerEventSMSListenerTest.java +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright (c) 2004-2022, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package org.hisp.dhis.tracker.imports.sms; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.common.collect.Sets; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashSet; -import org.hisp.dhis.category.CategoryOptionCombo; -import org.hisp.dhis.category.CategoryService; -import org.hisp.dhis.common.IdentifiableObjectManager; -import org.hisp.dhis.dataelement.DataElement; -import org.hisp.dhis.dataelement.DataElementService; -import org.hisp.dhis.external.conf.DhisConfigurationProvider; -import org.hisp.dhis.feedback.ForbiddenException; -import org.hisp.dhis.feedback.NotFoundException; -import org.hisp.dhis.fileresource.FileResourceService; -import org.hisp.dhis.message.MessageSender; -import org.hisp.dhis.organisationunit.OrganisationUnit; -import org.hisp.dhis.organisationunit.OrganisationUnitService; -import org.hisp.dhis.outboundmessage.OutboundMessageResponse; -import org.hisp.dhis.program.Enrollment; -import org.hisp.dhis.program.Event; -import org.hisp.dhis.program.Program; -import org.hisp.dhis.program.ProgramService; -import org.hisp.dhis.program.ProgramStage; -import org.hisp.dhis.program.ProgramStageService; -import org.hisp.dhis.sms.incoming.IncomingSms; -import org.hisp.dhis.sms.incoming.IncomingSmsService; -import org.hisp.dhis.smscompression.SmsCompressionException; -import org.hisp.dhis.smscompression.SmsConsts.SmsEventStatus; -import org.hisp.dhis.smscompression.models.GeoPoint; -import org.hisp.dhis.smscompression.models.SmsDataValue; -import org.hisp.dhis.smscompression.models.TrackerEventSmsSubmission; -import org.hisp.dhis.trackedentity.TrackedEntityAttributeService; -import org.hisp.dhis.trackedentity.TrackedEntityTypeService; -import org.hisp.dhis.tracker.export.enrollment.EnrollmentService; -import org.hisp.dhis.tracker.export.event.EventChangeLogService; -import org.hisp.dhis.tracker.export.event.EventService; -import org.hisp.dhis.user.User; -import org.hisp.dhis.user.UserDetails; -import org.hisp.dhis.user.UserService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; - -@MockitoSettings(strictness = Strictness.LENIENT) -@ExtendWith(MockitoExtension.class) -class TrackerEventSMSListenerTest extends CompressionSMSListenerTest { - - @Mock private UserService userService; - - @Mock private IncomingSmsService incomingSmsService; - - @Mock private MessageSender smsSender; - - @Mock private DataElementService dataElementService; - - @Mock private TrackedEntityTypeService trackedEntityTypeService; - - @Mock private TrackedEntityAttributeService trackedEntityAttributeService; - - @Mock private ProgramService programService; - - @Mock private OrganisationUnitService organisationUnitService; - - @Mock private CategoryService categoryService; - - @Mock private EventService eventService; - - @Mock private IdentifiableObjectManager identifiableObjectManager; - - private User user; - - private final OutboundMessageResponse response = new OutboundMessageResponse(); - - private IncomingSms updatedIncomingSms; - - private String message = ""; - - @Mock private EnrollmentService enrollmentService; - - @Mock private ProgramStageService programStageService; - - @Mock private EventChangeLogService eventChangeLogService; - - @Mock private FileResourceService fileResourceService; - - @Mock private DhisConfigurationProvider config; - - TrackerEventSMSListener subject; - - private IncomingSms incomingSmsTrackerEvent; - - private IncomingSms incomingSmsTrackerEventWithNulls; - - private IncomingSms incomingSmsTrackerEventNoValues; - - private OrganisationUnit organisationUnit; - - private CategoryOptionCombo categoryOptionCombo; - - private DataElement dataElement; - - private ProgramStage programStage; - - private Enrollment enrollment; - - private Event event; - - @BeforeEach - public void initTest() throws SmsCompressionException, ForbiddenException, NotFoundException { - subject = - new TrackerEventSMSListener( - incomingSmsService, - smsSender, - userService, - trackedEntityTypeService, - trackedEntityAttributeService, - programService, - organisationUnitService, - categoryService, - dataElementService, - identifiableObjectManager, - eventService, - eventChangeLogService, - fileResourceService, - config, - programStageService, - enrollmentService); - - setUpInstances(); - - when(userService.getUser(anyString())).thenReturn(user); - when(smsSender.isConfigured()).thenReturn(true); - when(smsSender.sendMessage(any(), any(), anyString())) - .thenAnswer( - invocation -> { - message = (String) invocation.getArguments()[1]; - return response; - }); - - when(organisationUnitService.getOrganisationUnit(anyString())).thenReturn(organisationUnit); - when(programStageService.getProgramStage(anyString())).thenReturn(programStage); - when(enrollmentService.getEnrollment(anyString(), any(UserDetails.class))) - .thenReturn(enrollment); - when(dataElementService.getDataElement(anyString())).thenReturn(dataElement); - when(categoryService.getCategoryOptionCombo(anyString())).thenReturn(categoryOptionCombo); - - doAnswer( - invocation -> { - updatedIncomingSms = (IncomingSms) invocation.getArguments()[0]; - return updatedIncomingSms; - }) - .when(incomingSmsService) - .update(any()); - } - - @Test - void testTrackerEvent() { - subject.receive(incomingSmsTrackerEvent); - - assertNotNull(updatedIncomingSms); - assertTrue(updatedIncomingSms.isParsed()); - assertEquals(SUCCESS_MESSAGE, message); - - verify(incomingSmsService, times(1)).update(any()); - } - - @Test - void testTrackerEventRepeat() { - subject.receive(incomingSmsTrackerEvent); - subject.receive(incomingSmsTrackerEvent); - - assertNotNull(updatedIncomingSms); - assertTrue(updatedIncomingSms.isParsed()); - assertEquals(SUCCESS_MESSAGE, message); - - verify(incomingSmsService, times(2)).update(any()); - } - - @Test - void testTrackerEventWithNulls() { - subject.receive(incomingSmsTrackerEventWithNulls); - - assertNotNull(updatedIncomingSms); - assertTrue(updatedIncomingSms.isParsed()); - assertEquals(SUCCESS_MESSAGE, message); - - verify(incomingSmsService, times(1)).update(any()); - } - - @Test - void testTrackerEventNoValues() { - subject.receive(incomingSmsTrackerEventNoValues); - - assertNotNull(updatedIncomingSms); - assertTrue(updatedIncomingSms.isParsed()); - assertEquals(NOVALUES_MESSAGE, message); - - verify(incomingSmsService, times(1)).update(any()); - } - - private void setUpInstances() throws SmsCompressionException { - organisationUnit = createOrganisationUnit('O'); - Program program = createProgram('P'); - programStage = createProgramStage('S', program); - - user = makeUser("U"); - user.setPhoneNumber(ORIGINATOR); - user.setOrganisationUnits(Sets.newHashSet(organisationUnit)); - - categoryOptionCombo = createCategoryOptionCombo('C'); - dataElement = createDataElement('D'); - - program.getOrganisationUnits().add(organisationUnit); - HashSet stages = new HashSet<>(); - stages.add(programStage); - program.setProgramStages(stages); - - enrollment = new Enrollment(); - enrollment.setAutoFields(); - enrollment.setProgram(program); - - event = new Event(); - event.setAutoFields(); - - incomingSmsTrackerEvent = createSMSFromSubmission(createTrackerEventSubmission()); - incomingSmsTrackerEventWithNulls = - createSMSFromSubmission(createTrackerEventSubmissionWithNulls()); - incomingSmsTrackerEventNoValues = - createSMSFromSubmission(createTrackerEventSubmissionNoValues()); - } - - private TrackerEventSmsSubmission createTrackerEventSubmission() { - TrackerEventSmsSubmission subm = new TrackerEventSmsSubmission(); - - subm.setUserId(user.getUid()); - subm.setOrgUnit(organisationUnit.getUid()); - subm.setProgramStage(programStage.getUid()); - subm.setAttributeOptionCombo(categoryOptionCombo.getUid()); - subm.setEnrollment(enrollment.getUid()); - subm.setEvent(event.getUid()); - subm.setEventStatus(SmsEventStatus.COMPLETED); - subm.setEventDate(new Date()); - subm.setDueDate(new Date()); - subm.setCoordinates(new GeoPoint(59.9399586f, 10.7195609f)); - ArrayList values = new ArrayList<>(); - values.add(new SmsDataValue(categoryOptionCombo.getUid(), dataElement.getUid(), "10")); - - subm.setValues(values); - subm.setSubmissionId(1); - - return subm; - } - - private TrackerEventSmsSubmission createTrackerEventSubmissionWithNulls() { - TrackerEventSmsSubmission subm = createTrackerEventSubmission(); - subm.setEventDate(null); - subm.setDueDate(null); - subm.setCoordinates(null); - - return subm; - } - - private TrackerEventSmsSubmission createTrackerEventSubmissionNoValues() { - TrackerEventSmsSubmission subm = createTrackerEventSubmission(); - subm.setValues(null); - - return subm; - } -} diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerDeleteEventSMSTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerEventSMSTest.java similarity index 73% rename from dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerDeleteEventSMSTest.java rename to dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerEventSMSTest.java index f6a01656ce9d..e40403e2f7d6 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerDeleteEventSMSTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerEventSMSTest.java @@ -49,11 +49,14 @@ import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.category.CategoryService; import org.hisp.dhis.common.CodeGenerator; +import org.hisp.dhis.common.IdentifiableObject; import org.hisp.dhis.common.IdentifiableObjectManager; import org.hisp.dhis.common.UID; import org.hisp.dhis.common.ValueType; import org.hisp.dhis.dataelement.DataElement; +import org.hisp.dhis.event.EventStatus; import org.hisp.dhis.feedback.NotFoundException; +import org.hisp.dhis.organisationunit.FeatureType; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.outboundmessage.OutboundMessage; import org.hisp.dhis.program.Enrollment; @@ -68,8 +71,11 @@ import org.hisp.dhis.sms.incoming.IncomingSmsService; import org.hisp.dhis.sms.incoming.SmsMessageStatus; import org.hisp.dhis.smscompression.SmsCompressionException; +import org.hisp.dhis.smscompression.SmsConsts.SmsEventStatus; import org.hisp.dhis.smscompression.SmsResponse; import org.hisp.dhis.smscompression.models.DeleteSmsSubmission; +import org.hisp.dhis.smscompression.models.GeoPoint; +import org.hisp.dhis.smscompression.models.TrackerEventSmsSubmission; import org.hisp.dhis.test.message.FakeMessageSender; import org.hisp.dhis.test.web.HttpStatus; import org.hisp.dhis.test.webapi.PostgresControllerIntegrationTestBase; @@ -79,18 +85,28 @@ import org.hisp.dhis.tracker.export.event.EventService; import org.hisp.dhis.user.User; import org.hisp.dhis.user.sharing.UserAccess; +import org.hisp.dhis.util.DateUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; import org.springframework.beans.factory.annotation.Autowired; /** - * Tests tracker SMS to delete an event implemented via {@link - * org.hisp.dhis.tracker.imports.sms.DeleteEventSMSListener}. It also tests parts of {@link - * org.hisp.dhis.webapi.controller.sms.SmsInboundController} and other SMS classes in the SMS class - * hierarchy. + * Tests tracker SMS + * + *
    + *
  • to delete an event via a {@link DeleteSmsSubmission} implemented via {@link + * org.hisp.dhis.tracker.imports.sms.DeleteEventSMSListener} + *
  • to create an event via a {@link TrackerEventSmsSubmission} implemented via {@link + * org.hisp.dhis.tracker.imports.sms.TrackerEventSMSListener} + *
+ * + * It also tests parts of {@link org.hisp.dhis.webapi.controller.sms.SmsInboundController} and other + * SMS classes in the SMS class hierarchy. */ -class TrackerDeleteEventSMSTest extends PostgresControllerIntegrationTestBase { +class TrackerEventSMSTest extends PostgresControllerIntegrationTestBase { @Autowired private IdentifiableObjectManager manager; @Autowired private CategoryService categoryService; @@ -144,14 +160,13 @@ void setUp() { manager.save(de, false); programStage = createProgramStage('A', program); + programStage.setFeatureType(FeatureType.POINT); programStage.getSharing().setOwner(user); programStage.getSharing().addUserAccess(fullAccess(user)); ProgramStageDataElement programStageDataElement = createProgramStageDataElement(programStage, de, 1, false); programStage.setProgramStageDataElements(Sets.newHashSet(programStageDataElement)); manager.save(programStage, false); - - event = event(enrollment(trackedEntity())); } @AfterEach @@ -161,6 +176,8 @@ void afterEach() { @Test void shouldDeleteEvent() throws SmsCompressionException { + event = event(enrollment(trackedEntity())); + DeleteSmsSubmission submission = new DeleteSmsSubmission(); int submissionId = 1; submission.setSubmissionId(submissionId); @@ -205,8 +222,10 @@ void shouldDeleteEvent() throws SmsCompressionException { @Test void shouldDeleteEventViaRequestParameters() throws SmsCompressionException { + event = event(enrollment(trackedEntity())); + DeleteSmsSubmission submission = new DeleteSmsSubmission(); - int submissionId = 1; + int submissionId = 2; submission.setSubmissionId(submissionId); submission.setUserId(user.getUid()); submission.setEvent(event.getUid()); @@ -253,7 +272,7 @@ void shouldFailDeletingNonExistingEvent() throws SmsCompressionException { assertThrows(NotFoundException.class, () -> eventService.getEvent(uid)); DeleteSmsSubmission submission = new DeleteSmsSubmission(); - int submissionId = 2; + int submissionId = 3; submission.setSubmissionId(submissionId); submission.setUserId(user.getUid()); submission.setEvent(uid.getValue()); @@ -288,6 +307,87 @@ void shouldFailDeletingNonExistingEvent() throws SmsCompressionException { () -> assertContainsOnly(List.of(expectedMessage), messageSender.getAllMessages())); } + @Test + void shouldCreateEvent() throws SmsCompressionException { + Enrollment enrollment = enrollment(trackedEntity()); + // TODO(ivo) figure out data values + // TODO(ivo) make assertion readable, use submission for expected arg? assertEvent(submission, + // event) + TrackerEventSmsSubmission submission = new TrackerEventSmsSubmission(); + int submissionId = 4; + submission.setSubmissionId(submissionId); + submission.setUserId(user.getUid()); + String eventUid = CodeGenerator.generateUid(); + submission.setEvent(eventUid); + submission.setOrgUnit(orgUnit.getUid()); + submission.setProgramStage(programStage.getUid()); + submission.setEnrollment(enrollment.getUid()); + submission.setAttributeOptionCombo(coc.getUid()); + submission.setEventStatus(SmsEventStatus.COMPLETED); + Date occurredDate = DateUtils.getDate(2024, 9, 2, 10, 15); + submission.setEventDate(occurredDate); + Date scheduledDate = DateUtils.getDate(2024, 9, 3, 16, 23); + submission.setDueDate(scheduledDate); + GeoPoint coordinates = new GeoPoint(48.8575f, 2.3514f); + submission.setCoordinates(coordinates); + + String text = encodeSms(submission); + String originator = user.getPhoneNumber(); + + switchContextToUser(user); + + JsonWebMessage response = + POST( + "/sms/inbound", + format( + """ + { + "text": "%s", + "originator": "%s" + } + """, + text, originator)) + .content(HttpStatus.OK) + .as(JsonWebMessage.class); + + IncomingSms sms = getSms(response); + String expectedText = submissionId + ":" + SmsResponse.SUCCESS; + OutboundMessage expectedMessage = new OutboundMessage(null, expectedText, Set.of(originator)); + assertAll( + () -> assertEquals(SmsMessageStatus.PROCESSED, sms.getStatus()), + () -> assertTrue(sms.isParsed()), + () -> assertEquals(originator, sms.getOriginator()), + () -> assertEquals(user, sms.getCreatedBy()), + () -> { + Event event = eventService.getEvent(UID.of(eventUid)); + assertAll( + () -> assertEquals(eventUid, event.getUid()), + () -> assertEqualUids(enrollment, event.getEnrollment()), + () -> assertEqualUids(orgUnit, event.getOrganisationUnit()), + () -> assertEqualUids(programStage, event.getProgramStage()), + () -> assertEqualUids(coc, event.getAttributeOptionCombo()), + () -> assertEquals(user.getUsername(), event.getStoredBy()), + () -> assertEquals(occurredDate, event.getOccurredDate()), + () -> assertEquals(scheduledDate, event.getScheduledDate()), + () -> assertEquals(EventStatus.COMPLETED, event.getStatus()), + () -> assertEquals(user.getUsername(), event.getCompletedBy()), + () -> assertNotNull(event.getCompletedDate()), + () -> + assertEquals( + new GeometryFactory() + .createPoint( + new Coordinate( + coordinates.getLongitude(), coordinates.getLatitude())), + event.getGeometry())); + }, + () -> assertContainsOnly(List.of(expectedMessage), messageSender.getAllMessages())); + } + + @Test + void shouldUpdateEvent() throws SmsCompressionException { + // TODO(ivo) add test + } + private IncomingSms getSms(JsonWebMessage response) { assertStartsWith("Received SMS: ", response.getMessage()); @@ -354,4 +454,8 @@ private UserAccess fullAccess(User user) { a.setAccess(AccessStringHelper.FULL); return a; } + + private void assertEqualUids(IdentifiableObject expected, IdentifiableObject actual) { + assertEquals(expected.getUid(), actual.getUid()); + } }