diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ErrorCode.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ErrorCode.java index dea0cc65ed49..23a0c14c9f10 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ErrorCode.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ErrorCode.java @@ -67,6 +67,11 @@ public enum ErrorCode { E1120("Update cannot be applied as it would make existing data values inaccessible"), E1121("Data element `{0}` value type cannot be changed as it has associated data values"), + E1122("Category option combo {0} already exists for category combo {1}"), + E1123("Category option combo {0} must be associated with a category combo"), + E1124("Category option combo {0} cannot be associated with the default category combo"), + + E1125("Category option combo {0} contains options not associated with category combo {1}"), /* Org unit merge */ E1500("At least two source orgs unit must be specified"), E1501("Target org unit must be specified"), diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/CategoryOptionComboObjectBundleHook.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/CategoryOptionComboObjectBundleHook.java new file mode 100644 index 000000000000..02fe6424da14 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/CategoryOptionComboObjectBundleHook.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2004-2024, 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.dxf2.metadata.objectbundle.hooks; + +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import org.hisp.dhis.category.CategoryCombo; +import org.hisp.dhis.category.CategoryOption; +import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.category.CategoryService; +import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundle; +import org.hisp.dhis.feedback.ErrorCode; +import org.hisp.dhis.feedback.ErrorReport; +import org.springframework.stereotype.Component; + +@Component +@AllArgsConstructor +public class CategoryOptionComboObjectBundleHook + extends AbstractObjectBundleHook { + private final CategoryService categoryService; + + static boolean haveEqualCatComboCatOptionReferenceIds( + CategoryOptionCombo one, CategoryOptionCombo other) { + if (one == null || other == null) { + return false; + } + + if (one.getCategoryCombo() == null || other.getCategoryCombo() == null) { + return false; + } + + if (one.getCategoryOptions() == null || other.getCategoryOptions() == null) { + return false; + } + + if (!one.getCategoryCombo().getUid().equals(other.getCategoryCombo().getUid())) { + return false; + } + + Set oneCategoryOptionUids = + one.getCategoryOptions().stream().map(CategoryOption::getUid).collect(Collectors.toSet()); + + Set otherCategoryOptionUids = + other.getCategoryOptions().stream().map(CategoryOption::getUid).collect(Collectors.toSet()); + + return oneCategoryOptionUids.equals(otherCategoryOptionUids); + } + + private void checkDuplicateCategoryOptionCombos( + CategoryOptionCombo categoryOptionCombo, + ObjectBundle bundle, + Consumer addReports) { + + if (bundle.isPersisted(categoryOptionCombo)) { + return; // Only check for duplicates if the object is not persisted + } + + List categoryOptionCombos = + categoryService.getAllCategoryOptionCombos().stream() + .filter( + coc -> + coc.getCategoryCombo() + .getUid() + .equals(categoryOptionCombo.getCategoryCombo().getUid())) + .toList(); + + // Check if the categoryOptionCombo is already in the list. This could be an update or re-import + // of the same object. + if (categoryOptionCombos.stream() + .anyMatch(coc -> coc.getUid().equals(categoryOptionCombo.getUid()))) { + return; + } + // Check to see if the COC already exists in the list of COCs + // If it does, then it is a duplicate + for (CategoryOptionCombo existingCategoryOptionCombo : categoryOptionCombos) { + if (haveEqualCatComboCatOptionReferenceIds(categoryOptionCombo, existingCategoryOptionCombo) + && !categoryOptionCombo.getUid().equals(existingCategoryOptionCombo.getUid())) { + addReports.accept( + new ErrorReport( + CategoryOptionCombo.class, + ErrorCode.E1122, + categoryOptionCombo.getName(), + existingCategoryOptionCombo.getName())); + } + } + } + + private void checkCategoryOptionsExistInCategoryCombo( + CategoryOptionCombo categoryOptionCombo, Consumer addReports) { + + Set categoryOptionUids = + categoryOptionCombo.getCategoryOptions().stream() + .map(CategoryOption::getUid) + .collect(Collectors.toSet()); + + CategoryCombo existingCategoryCombo = + categoryService.getCategoryCombo(categoryOptionCombo.getCategoryCombo().getUid()); + + if (existingCategoryCombo == null) { + return; + } + + Set existingCategoryOptionsInCombo = + existingCategoryCombo.getCategoryOptions().stream() + .map(CategoryOption::getUid) + .collect(Collectors.toSet()); + + categoryOptionUids.removeAll(existingCategoryOptionsInCombo); + + if (!categoryOptionUids.isEmpty()) { + addReports.accept( + new ErrorReport( + CategoryOptionCombo.class, + ErrorCode.E1125, + categoryOptionCombo.getName(), + existingCategoryCombo.getName())); + } + } + + private void checkNonStandardDefaultCatOptionCombo( + CategoryOptionCombo categoryOptionCombo, Consumer addReports) { + + CategoryCombo categoryCombo = categoryOptionCombo.getCategoryCombo(); + CategoryCombo defaultCombo = categoryService.getDefaultCategoryCombo(); + if (!categoryCombo.getUid().equals(defaultCombo.getUid())) { + return; + } + + CategoryOptionCombo defaultCatOptionCombo = categoryService.getDefaultCategoryOptionCombo(); + + if (!categoryOptionCombo.getUid().equals(defaultCatOptionCombo.getUid())) { + addReports.accept( + new ErrorReport( + CategoryOptionCombo.class, ErrorCode.E1124, categoryOptionCombo.getName())); + } + } + + @Override + public void validate( + CategoryOptionCombo categoryOptionCombo, + ObjectBundle bundle, + Consumer addReports) { + + checkNonStandardDefaultCatOptionCombo(categoryOptionCombo, addReports); + checkDuplicateCategoryOptionCombos(categoryOptionCombo, bundle, addReports); + checkCategoryOptionsExistInCategoryCombo(categoryOptionCombo, addReports); + } +} diff --git a/dhis-2/dhis-test-e2e/src/test/resources/tracker/idSchemesMetadata.json b/dhis-2/dhis-test-e2e/src/test/resources/tracker/idSchemesMetadata.json index 195b156af94c..216c33d1dca1 100644 --- a/dhis-2/dhis-test-e2e/src/test/resources/tracker/idSchemesMetadata.json +++ b/dhis-2/dhis-test-e2e/src/test/resources/tracker/idSchemesMetadata.json @@ -742,6 +742,7 @@ ], "categoryCombos": [ { + "code": "TA_CATEGORY_COMBO_ATTRIBUTE", "name": "TA Category combo attribute", "created": "2022-05-30T11:40:03.717", diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/preheat/TrackerPreheatIdentifiersTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/preheat/TrackerPreheatIdentifiersTest.java index a70af93fafa7..69891c46c990 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/preheat/TrackerPreheatIdentifiersTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/preheat/TrackerPreheatIdentifiersTest.java @@ -164,7 +164,8 @@ void testCategoryOptionIdentifiers() { @Test void testCategoryOptionComboIdentifiers() { - List> data = buildDataSet("XXXvX50cXC0", "COCA", "COCAname"); + List> data = + buildDataSet("HllvX50cXC0", "default", "default"); for (Pair pair : data) { String id = pair.getLeft(); TrackerIdSchemeParam param = pair.getRight(); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/preheat/TrackerPreheatServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/preheat/TrackerPreheatServiceTest.java index 1d083bb5f2e8..49defcb12f24 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/preheat/TrackerPreheatServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/preheat/TrackerPreheatServiceTest.java @@ -149,7 +149,7 @@ void testPreheatEvents() throws IOException { assertFalse(preheat.getAll(OrganisationUnit.class).isEmpty()); assertFalse(preheat.getAll(ProgramStage.class).isEmpty()); assertFalse(preheat.getAll(CategoryOptionCombo.class).isEmpty()); - assertNotNull(preheat.get(CategoryOptionCombo.class, "XXXvX50cXC0")); + assertNotNull(preheat.get(CategoryOptionCombo.class, "HllvX50cXC0")); assertNotNull(preheat.get(CategoryOption.class, "XXXrKDKCefk")); } } diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/validation/EventImportValidationTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/validation/EventImportValidationTest.java index 646276e5e0ec..561385cb9b4b 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/validation/EventImportValidationTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/imports/validation/EventImportValidationTest.java @@ -168,11 +168,7 @@ void testCantWriteAccessCatCombo() throws IOException { ImportReport importReport = trackerImportService.importTracker(params, trackerObjects); assertHasOnlyErrors( - importReport, - ValidationCode.E1096, - ValidationCode.E1099, - ValidationCode.E1104, - ValidationCode.E1095); + importReport, ValidationCode.E1096, ValidationCode.E1104, ValidationCode.E1095); } @Test diff --git a/dhis-2/dhis-test-integration/src/test/resources/tracker/event_metadata.json b/dhis-2/dhis-test-integration/src/test/resources/tracker/event_metadata.json index 08e0c19e4bf9..8e41a3676f9c 100644 --- a/dhis-2/dhis-test-integration/src/test/resources/tracker/event_metadata.json +++ b/dhis-2/dhis-test-integration/src/test/resources/tracker/event_metadata.json @@ -1820,24 +1820,6 @@ "id": "xYerKDKCefk" } ] - }, - { - "lastUpdated": "2019-01-28T11:01:30.775", - "code": "COCA", - "created": "2019-01-28T11:01:30.771", - "name": "COCAname", - "id": "XXXvX50cXC0", - "ignoreApproval": false, - "categoryCombo": { - "id": "bjDvmb4bfuf" - }, - "translations": [], - "attributeValues": [], - "categoryOptions": [ - { - "id": "xYerKDKCefk" - } - ] } ], "categories": [ diff --git a/dhis-2/dhis-test-integration/src/test/resources/tracker/identifier_metadata.json b/dhis-2/dhis-test-integration/src/test/resources/tracker/identifier_metadata.json index f4925a896ee3..eed1a1b69336 100644 --- a/dhis-2/dhis-test-integration/src/test/resources/tracker/identifier_metadata.json +++ b/dhis-2/dhis-test-integration/src/test/resources/tracker/identifier_metadata.json @@ -204,23 +204,6 @@ "id": "xYerKDKCefk" } ] - }, - { - "lastUpdated": "2019-01-28T11:01:30.775", - "code": "COCA", - "created": "2019-01-28T11:01:30.771", - "name": "COCAname", - "id": "XXXvX50cXC0", - "ignoreApproval": false, - "categoryCombo": { - "id": "bjDvmb4bfuf" - }, - "attributeValues": [], - "categoryOptions": [ - { - "id": "xYerKDKCefk" - } - ] } ], "categories": [ diff --git a/dhis-2/dhis-test-integration/src/test/resources/tracker/tracker_basic_metadata.json b/dhis-2/dhis-test-integration/src/test/resources/tracker/tracker_basic_metadata.json index 66db4ea628af..04b2819ab833 100644 --- a/dhis-2/dhis-test-integration/src/test/resources/tracker/tracker_basic_metadata.json +++ b/dhis-2/dhis-test-integration/src/test/resources/tracker/tracker_basic_metadata.json @@ -2690,6 +2690,25 @@ "userGroups": {} } }, + { + "id": "WAFwwid9TzE", + "code": "default2", + "name": "default2", + "shortName": "default2", + "startDate": "2019-01-01T00:00:00.000", + "attributeValues": [], + "lastUpdated": "2019-03-25T13:40:24.783", + "organisationUnits": [], + "created": "2019-03-25T13:40:24.722", + "translations": [], + "sharing": { + "public": "rwrw----", + "external": false, + "owner": null, + "users": {}, + "userGroups": {} + } + }, { "id": "LYerKDKCefk", "code": "catopt1", @@ -2835,6 +2854,30 @@ "userGroups": {} } }, + { + "id": "Vk8PHR8HBAe", + "name": "default2", + "shortName": "default2", + "code": "default2", + "dataDimensionType": "DISAGGREGATION", + "categoryOptions": [ + { + "id": "WAFwwid9TzE" + } + ], + "dataDimension": false, + "attributeValues": [], + "translations": [], + "created": "2019-03-25T13:40:24.762", + "lastUpdated": "2019-03-29T08:54:32.360", + "sharing": { + "public": "rw------", + "external": false, + "owner": null, + "users": {}, + "userGroups": {} + } + }, { "id": "LC4ZMxxHtBA", "name": "nondef1", @@ -2907,6 +2950,28 @@ "userGroups": {} } }, + { + "id": "WG3syvpSE9o", + "code": "default2", + "name": "default2", + "dataDimensionType": "DISAGGREGATION", + "categories": [ + { + "id": "Vk8PHR8HBAe" + } + ], + "created": "2019-03-25T13:40:24.771", + "lastUpdated": "2019-03-25T13:40:24.781", + "skipTotal": false, + "translations": [], + "sharing": { + "public": "rw------", + "external": false, + "owner": null, + "users": {}, + "userGroups": {} + } + }, { "id": "pFpCRzMVAbO", "code": "nondef1", @@ -3007,7 +3072,7 @@ } ], "categoryCombo": { - "id": "bjDvmb4bfuf" + "id": "WG3syvpSE9o" }, "translations": [], "startDate": "2019-01-01T00:00:00.000", diff --git a/dhis-2/dhis-test-integration/src/test/resources/tracker/validations/events-cat-write-access.json b/dhis-2/dhis-test-integration/src/test/resources/tracker/validations/events-cat-write-access.json index 254eda322691..9523595c2cde 100644 --- a/dhis-2/dhis-test-integration/src/test/resources/tracker/validations/events-cat-write-access.json +++ b/dhis-2/dhis-test-integration/src/test/resources/tracker/validations/events-cat-write-access.json @@ -57,7 +57,7 @@ "deleted": false, "attributeOptionCombo": { "idScheme": "UID", - "identifier": "KKKKX50cXC0" + "identifier": "HllvX50cXC0" }, "attributeCategoryOptions": [], "dataValues": [], diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/CategoryOptionComboControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/CategoryOptionComboControllerTest.java index ab13aa6564e4..b18677adddb2 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/CategoryOptionComboControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/CategoryOptionComboControllerTest.java @@ -27,8 +27,10 @@ */ package org.hisp.dhis.webapi.controller; +import static org.hisp.dhis.http.HttpAssertions.assertStatus; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import java.util.Set; import java.util.stream.Collectors; @@ -36,10 +38,15 @@ import org.hisp.dhis.category.CategoryOption; import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.category.CategoryService; +import org.hisp.dhis.feedback.ErrorCode; import org.hisp.dhis.http.HttpStatus; import org.hisp.dhis.jsontree.JsonArray; +import org.hisp.dhis.jsontree.JsonList; +import org.hisp.dhis.jsontree.JsonMixed; +import org.hisp.dhis.jsontree.JsonObject; import org.hisp.dhis.test.webapi.H2ControllerIntegrationTestBase; import org.hisp.dhis.test.webapi.json.domain.JsonCategoryOptionCombo; +import org.hisp.dhis.test.webapi.json.domain.JsonErrorReport; import org.hisp.dhis.test.webapi.json.domain.JsonIdentifiableObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -115,4 +122,97 @@ void catOptionCombosExcludingDefaultTest() { assertFalse( catOptionComboNames.contains("default"), "default catOptionCombo is not in payload"); } + + @Test + @DisplayName("Duplicate CategoryOptionCombos should not be allowed") + void catOptionCombosDuplicatedTest() { + + JsonObject response = + GET("/categoryOptionCombos?filter=id:eq:CocUid0001&fields=id,categoryCombo[id],categoryOptions[id]") + .content(); + JsonList catOptionCombos = + response.getList("categoryOptionCombos", JsonCategoryOptionCombo.class); + String catOptionComboAOptions = catOptionCombos.get(0).getCategoryOptions().get(0).getId(); + String catOptionComboACatComboId = catOptionCombos.get(0).getCategoryCombo().getId(); + + JsonErrorReport error = + POST( + "/categoryOptionCombos/", + """ + { "name": "A_1", + "categoryOptions" : [{"id" : "%s"}], + "categoryCombo" : {"id" : "%s"} } + """ + .formatted(catOptionComboAOptions, catOptionComboACatComboId)) + .content(HttpStatus.CONFLICT) + .find(JsonErrorReport.class, report -> report.getErrorCode() == ErrorCode.E1122); + assertNotNull(error); + assertEquals( + "Category option combo A_1 already exists for category combo CatOptCombo A", + error.getMessage()); + } + + @Test + @DisplayName("Duplicate default category option combos should not be allowed") + void catOptionCombosDuplicatedDefaultTest() { + JsonObject response = + GET("/categoryOptionCombos?filter=name:eq:default&fields=id,categoryCombo[id],categoryOptions[id]") + .content(); + JsonList catOptionCombos = + response.getList("categoryOptionCombos", JsonCategoryOptionCombo.class); + String defaultCatOptionComboOptions = + catOptionCombos.get(0).getCategoryOptions().get(0).getId(); + String defaultCatOptionComboCatComboId = catOptionCombos.get(0).getCategoryCombo().getId(); + response = + POST( + "/categoryOptionCombos/", + """ + { "name": "Not default", + "categoryOptions" : [{"id" : "%s"}], + "categoryCombo" : {"id" : "%s"} } + """ + .formatted(defaultCatOptionComboOptions, defaultCatOptionComboCatComboId)) + .content(HttpStatus.CONFLICT); + + JsonErrorReport error = + response.find(JsonErrorReport.class, report -> report.getErrorCode() == ErrorCode.E1122); + assertNotNull(error); + assertEquals( + "Category option combo Not default already exists for category combo default", + error.getMessage()); + } + + @Test + @DisplayName( + "Default category option combos with non-default category options should not be allowed") + void catOptionComboDefaultWithNonDefaultOptionsNotAllowedTest() { + JsonObject response = + GET("/categoryOptionCombos?filter=name:eq:default&fields=id,categoryCombo[id],categoryOptions[id]") + .content(); + JsonList catOptionCombos = + response.getList("categoryOptionCombos", JsonCategoryOptionCombo.class); + String defaultCategoryComboId = catOptionCombos.get(0).getCategoryCombo().getId(); + + String categoryOptionRed = + assertStatus( + HttpStatus.CREATED, POST("/categoryOptions", "{ 'name': 'Red', 'shortName': 'Red' }")); + + JsonMixed response2 = + POST( + "/categoryOptionCombos/", + """ + { "name": "Not default", + "categoryOptions" : [{"id" : "%s"}], + "categoryCombo" : {"id" : "%s"} } + """ + .formatted(categoryOptionRed, defaultCategoryComboId)) + .content(HttpStatus.CONFLICT); + + JsonErrorReport error = + response2.find(JsonErrorReport.class, report -> report.getErrorCode() == ErrorCode.E1125); + assertNotNull(error); + assertEquals( + "Category option combo Not default contains options not associated with category combo default", + error.getMessage()); + } } diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityCategoryOptionComboDuplicatedTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityCategoryOptionComboDuplicatedTest.java index 66016541b3c3..f7e4ed3a7022 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityCategoryOptionComboDuplicatedTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityCategoryOptionComboDuplicatedTest.java @@ -28,8 +28,14 @@ package org.hisp.dhis.webapi.controller.dataintegrity; import static org.hisp.dhis.http.HttpAssertions.assertStatus; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.util.HashSet; import java.util.Set; +import org.hisp.dhis.category.CategoryCombo; +import org.hisp.dhis.category.CategoryOption; +import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.http.HttpStatus; import org.hisp.dhis.jsontree.JsonList; import org.hisp.dhis.jsontree.JsonObject; @@ -48,8 +54,6 @@ class DataIntegrityCategoryOptionComboDuplicatedTest extends AbstractDataIntegri private final String check = "category_option_combos_have_duplicates"; - private String cocWithOptionsA; - private String categoryOptionRed; @Test @@ -80,44 +84,40 @@ void testCategoryOptionCombosDuplicated() { + categoryColor + "'}]} ")); - cocWithOptionsA = - assertStatus( - HttpStatus.CREATED, - POST( - "/categoryOptionCombos", - """ - { "name": "Reddish", - "categoryOptions" : [{"id" : "%s"}], - "categoryCombo" : {"id" : "%s"} } - """ - .formatted(categoryOptionRed, testCatCombo))); + HttpResponse response = GET("/categoryOptionCombos?fields=id,name&filter=name:eq:Red"); + assertStatus(HttpStatus.OK, response); + JsonObject responseContent = response.content(); - assertStatus( - HttpStatus.CREATED, - POST( - "/categoryOptionCombos", - """ - { "name": "Not Red", - "categoryOptions" : [{"id" : "%s"}]}, - "categoryCombo" : {"id" : "%s"} } - """ - .formatted(categoryOptionRed, testCatCombo))); + JsonList catOptionCombos = + responseContent.getList("categoryOptionCombos", JsonCategoryOptionCombo.class); + assertEquals(1, catOptionCombos.size()); + String redCategoryOptionComboId = catOptionCombos.get(0).getId(); + + /*We must resort to the service layer as the API will not allow us to create a duplicate*/ + CategoryCombo categoryCombo = categoryService.getCategoryCombo(testCatCombo); + CategoryOptionCombo existingCategoryOptionCombo = + categoryService.getCategoryOptionCombo(redCategoryOptionComboId); + CategoryOptionCombo categoryOptionComboDuplicate = new CategoryOptionCombo(); + categoryOptionComboDuplicate.setAutoFields(); + categoryOptionComboDuplicate.setCategoryCombo(categoryCombo); + Set newCategoryOptions = + new HashSet<>(existingCategoryOptionCombo.getCategoryOptions()); + categoryOptionComboDuplicate.setCategoryOptions(newCategoryOptions); + categoryOptionComboDuplicate.setName("Reddish"); + manager.persist(categoryOptionComboDuplicate); + dbmsManager.clearSession(); + String categoryOptionComboDuplicatedID = categoryOptionComboDuplicate.getUid(); + assertNotNull(categoryOptionComboDuplicatedID); assertNamedMetadataObjectExists("categoryOptionCombos", "default"); assertNamedMetadataObjectExists("categoryOptionCombos", "Red"); assertNamedMetadataObjectExists("categoryOptionCombos", "Reddish"); - assertNamedMetadataObjectExists("categoryOptionCombos", "Not Red"); - /*We need to get the Red category option combo to be able to check the data integrity issues*/ + /* There are three total category option combos, so we expect 33% */ + checkDataIntegritySummary(check, 1, 33, true); - JsonObject response = GET("/categoryOptionCombos?fields=id,name&filter=name:eq:Red").content(); - JsonList catOptionCombos = - response.getList("categoryOptionCombos", JsonCategoryOptionCombo.class); - String redCategoryOptionComboId = catOptionCombos.get(0).getId(); - /* There are four total category option combos, so we expect 25% */ - checkDataIntegritySummary(check, 1, 25, true); - - Set expectedCategoryOptCombos = Set.of(cocWithOptionsA, redCategoryOptionComboId); + Set expectedCategoryOptCombos = + Set.of(categoryOptionComboDuplicatedID, redCategoryOptionComboId); Set expectedMessages = Set.of("Red", "Reddish"); checkDataIntegrityDetailsIssues( check, expectedCategoryOptCombos, expectedMessages, Set.of(), "categoryOptionCombos"); @@ -135,27 +135,27 @@ void testCategoryOptionCombosNotDuplicated() { HttpStatus.CREATED, POST("/categoryOptions", "{ 'name': 'Blue', 'shortName': 'Blue' }")); - cocWithOptionsA = + String categoryColor = assertStatus( HttpStatus.CREATED, POST( - "/categoryOptionCombos", - """ - { "name": "Color", - "categoryOptions" : [{"id" : "%s"} ] } - """ - .formatted(categoryOptionRed))); + "/categories", + "{ 'name': 'Color', 'shortName': 'Color', 'dataDimensionType': 'DISAGGREGATION' ," + + "'categoryOptions' : [{'id' : '" + + categoryOptionRed + + "'}, {'id' : '" + + categoryOptionBlue + + "'} ] }")); assertStatus( HttpStatus.CREATED, POST( - "/categoryOptionCombos", - """ - { "name": "Colour", - "categoryOptions" : [{"id" : "%s"} ] } - """ - .formatted(categoryOptionBlue))); - + "/categoryCombos", + "{ 'name' : 'Color', " + + "'dataDimensionType' : 'DISAGGREGATION', 'categories' : [" + + "{'id' : '" + + categoryColor + + "'}]} ")); assertHasNoDataIntegrityIssues("categoryOptionCombos", check, true); }