diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9d5d9a5..10adf49a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: branches: - main push: - branches: [ master ] + branches: [ main ] jobs: run-unit-tests: @@ -16,16 +16,18 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up JDK 11 - uses: actions/setup-java@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: 11 + java-version: '17' - name: Run Unit tests and generate report - run: mvn -B clean test jacoco:report --file pom.xml + run: mvn -B clean test jacoco:report --file pom.xml --file README.md --file LICENSE -X -e + env: + PROXY_TO: "https://my.test-server.org/fhir" - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4.0.1 with: - token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/README.md b/README.md index 59809712..5b204180 100755 --- a/README.md +++ b/README.md @@ -227,6 +227,14 @@ environment variables If not set, defaults to https://smartregister.org/related-entity-location-tag-id +**Monitoring** + +[Spring actuator](https://docs.spring.io/spring-boot/reference/actuator/enabling.html) +dependency has been added to provide monitoring for the application and its +components using HTTP endpoints or with JMX. By default, the health endpoint is +exposed over HTTP and JMX. To expose other endpoints e.g. prometheus, one has to +update the application configuration. + ### Run project As documented on the Info Gateway modules @@ -396,6 +404,67 @@ Example: [GET] /LocationHierarchy?_id=&administrativeLevelMin=2&administrativeLevelMax=4&_count=&_page=&_sort= ``` +##### Inventory Filters + +The `LocationHierarchy` endpoint supports filtering by inventory availability, +allowing users to specify whether they want to retrieve only locations that have +associated inventories. This filter can be particularly useful for narrowing +down the results to locations that are actively involved in inventory +management. + +The following search parameter is available: + +- `filterInventory`: A boolean parameter that specifies whether the response + should be filtered by locations with inventories. + - `filterInventory=true`: Only locations with inventories will be included in + the response. + - `filterInventory=false` (or not set): Locations with or without inventories + will be returned. This effectively disables inventory-based filtering. The + response will include all locations, regardless of their inventory status. + Both locations with and without inventories will be returned. + +Example: + +``` +[GET] /LocationHierarchy?_id=&filterInventory=true&_count=&_page=&_sort= +``` + +##### LastUpdated Filters + +The `LocationHierarchy` endpoint supports filtering by the lastUpdated timestamp +of locations. This filter allows users to retrieve locations based on the last +modification date, making it useful for tracking recent updates or syncing data +changes over time. + +Behavior based on the lastUpdated parameter: + +- `_lastUpdated` Not Defined: The endpoint will include all locations in the + response, regardless of when they were last modified. +- `_lastUpdated` Defined: The response will include only those locations that + were updated on or after the specified timestamp. + +Note: This filter only works when in list mode i.e `mode=list` is set as one of +the parameters + +Example: + +``` +[GET] /LocationHierarchy?_id=&mode=list&_lastUpdated=2024-09-22T15%3A13%3A53.014%2B00%3A00&_count=&_page=&_sort= +``` + +##### LocationHierarchy Summary Count + +The LocationHierarchy endpoint supports the `_summary=count` parameter. This +allows users to retrieve the total number of matching resources without +returning the resource data. This filter only works when in list mode i.e +`mode=list` is set as one of the parameters. + +Example: + +``` +GET /LocationHierarchy?_id=&mode=list&_summary=count +``` + #### Important Note: Developers, please update your client applications accordingly to accommodate diff --git a/exec/pom.xml b/exec/pom.xml index 2b38ba03..8da4af50 100755 --- a/exec/pom.xml +++ b/exec/pom.xml @@ -4,7 +4,7 @@ org.smartregister opensrp-gateway-plugin - 2.0.8-alpha + 2.2.2-alpha exec @@ -12,7 +12,7 @@ ${project.parent.basedir} - 2.7.5 + 3.3.4 true @@ -46,13 +46,13 @@ com.google.fhir.gateway server - 0.3.1 + ${fhir.gateway.version} com.google.fhir.gateway plugins - 0.3.1 + ${fhir.gateway.version} @@ -70,7 +70,7 @@ org.smartregister plugins - 2.0.8-alpha + 2.2.2-alpha @@ -98,7 +98,7 @@ org.smartregister.fhir.gateway.MainApp - - javax.servlet - javax.servlet-api - 4.0.1 - provided + jakarta.servlet + jakarta.servlet-api + ${jakarta-servlet.version} ca.uhn.hapi.fhir @@ -51,12 +51,12 @@ io.sentry sentry-logback - 7.6.0 + ${sentry.version} io.sentry - sentry-spring-boot-starter - 7.6.0 + sentry-spring-boot-starter-jakarta + ${sentry.version} diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/CacheHelper.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/CacheHelper.java index 8fdcc3ac..3803a043 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/CacheHelper.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/CacheHelper.java @@ -21,6 +21,8 @@ public enum CacheHelper { Cache stringCache; + Cache> listStringCache; + CacheHelper() { cache = Caffeine.newBuilder() @@ -42,6 +44,11 @@ public enum CacheHelper { .expireAfterWrite(getCacheExpiryDurationInSeconds(), TimeUnit.SECONDS) .maximumSize(DEFAULT_CACHE_SIZE) .build(); + listStringCache = + Caffeine.newBuilder() + .expireAfterWrite(getCacheExpiryDurationInSeconds(), TimeUnit.SECONDS) + .maximumSize(DEFAULT_CACHE_SIZE) + .build(); } private int getCacheExpiryDurationInSeconds() { diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java index 6d075942..8133f9a2 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java @@ -6,6 +6,7 @@ public class Constants { "https://smartregister.org/care-team-tag-id"; public static final String CODE_URL_VALUE_SEPARATOR = "|"; public static final String EMPTY_STRING = ""; + public static final String FORWARD_SLASH = "/"; public static final String LOCATION_TAG_URL_ENV = "LOCATION_TAG_URL"; public static final String DEFAULT_LOCATION_TAG_URL = "https://smartregister.org/location-tag-id"; @@ -23,6 +24,10 @@ public class Constants { public static final String IDENTIFIER = "_id"; public static final String MIN_ADMIN_LEVEL = "administrativeLevelMin"; public static final String MAX_ADMIN_LEVEL = "administrativeLevelMax"; + public static final String FILTER_INVENTORY = "filterInventory"; + public static final String LAST_UPDATED = "_lastUpdated"; + public static final String SUMMARY = "_summary"; + public static final String COUNT = "count"; public static final int DEFAULT_MAX_ADMIN_LEVEL = 10; public static final int DEFAULT_MIN_ADMIN_LEVEL = 0; public static final String PAGINATION_PAGE_SIZE = "_count"; @@ -38,6 +43,7 @@ public class Constants { public static final String ROLE_WEB_CLIENT = "WEB_CLIENT"; public static final String MODE = "mode"; public static final String LIST = "list"; + public static final String SUBJECT = "subject"; public static final String CORS_ALLOW_HEADERS_KEY = "Access-Control-Allow-Headers"; public static final String CORS_ALLOW_HEADERS_VALUE = "authorization, cache-control"; public static final String CORS_ALLOW_METHODS_KEY = "Access-Control-Allow-Methods"; diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java index a97787f7..6d6383ff 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java @@ -3,22 +3,21 @@ import static org.smartregister.utils.Constants.LOCATION_RESOURCE; import static org.smartregister.utils.Constants.LOCATION_RESOURCE_NOT_FOUND; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import javax.annotation.Nullable; -import javax.servlet.http.HttpServletRequest; - import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.r4.model.Binary; import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.CodeableConcept; -import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Composition; +import org.hl7.fhir.r4.model.ListResource; import org.hl7.fhir.r4.model.Location; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StringType; @@ -38,6 +37,8 @@ import ca.uhn.fhir.rest.gclient.TokenClientParam; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import jakarta.annotation.Nullable; +import jakarta.servlet.http.HttpServletRequest; public class LocationHierarchyEndpointHelper { @@ -57,42 +58,45 @@ private IGenericClient getFhirClientForR4() { public LocationHierarchy getLocationHierarchy( String locationId, List preFetchAdminLevels, - List postFetchAdminLevels) { - LocationHierarchy locationHierarchy; - - if (CacheHelper.INSTANCE.skipCache()) { - locationHierarchy = - getLocationHierarchyCore(locationId, preFetchAdminLevels, postFetchAdminLevels); - } else { - locationHierarchy = - (LocationHierarchy) - CacheHelper.INSTANCE.resourceCache.get( - locationId, - key -> - getLocationHierarchyCore( - locationId, - preFetchAdminLevels, - postFetchAdminLevels)); - } - return locationHierarchy; + List postFetchAdminLevels, + Boolean filterInventory, + String lastUpdated) { + // TODO: implement correct caching + return getLocationHierarchyCore( + locationId, + preFetchAdminLevels, + postFetchAdminLevels, + filterInventory, + lastUpdated); } public List getLocationHierarchies( List locationIds, List preFetchAdminLevels, - List postFetchAdminLevels) { + List postFetchAdminLevels, + Boolean filterInventory, + String lastUpdated) { + + locationIds = locationIds != null ? locationIds : Collections.emptyList(); + return locationIds.parallelStream() .map( locationId -> getLocationHierarchy( - locationId, preFetchAdminLevels, postFetchAdminLevels)) + locationId, + preFetchAdminLevels, + postFetchAdminLevels, + filterInventory, + lastUpdated)) .collect(Collectors.toList()); } public LocationHierarchy getLocationHierarchyCore( String locationId, List preFetchAdminLevels, - List postFetchAdminLevels) { + List postFetchAdminLevels, + Boolean filterInventory, + String lastUpdated) { Location location = getLocationById(locationId); LocationHierarchyTree locationHierarchyTree = new LocationHierarchyTree(); @@ -100,10 +104,13 @@ public LocationHierarchy getLocationHierarchyCore( if (location != null) { logger.info("Building Location Hierarchy of Location Id : {}", locationId); - locationHierarchyTree.buildTreeFromList( - filterLocationsByAdminLevels( - getDescendants(locationId, location, preFetchAdminLevels), - postFetchAdminLevels)); + List descendants = getDescendants(locationId, location, preFetchAdminLevels); + + descendants = + postFetchFilters( + descendants, postFetchAdminLevels, filterInventory, lastUpdated); + + locationHierarchyTree.buildTreeFromList(descendants); StringType locationIdString = new StringType().setId(locationId).getIdElement(); locationHierarchy.setLocationId(locationIdString); locationHierarchy.setId(LOCATION_RESOURCE + locationId); @@ -120,7 +127,9 @@ public List getLocationHierarchyLocations( String locationId, Location parentLocation, List preFetchAdminLevels, - List postFetchAdminLevels) { + List postFetchAdminLevels, + Boolean filterInventory, + String lastUpdated) { List descendants; if (CacheHelper.INSTANCE.skipCache()) { @@ -131,7 +140,9 @@ public List getLocationHierarchyLocations( locationId, key -> getDescendants(locationId, parentLocation, preFetchAdminLevels)); } - return filterLocationsByAdminLevels(descendants, postFetchAdminLevels); + descendants = + postFetchFilters(descendants, postFetchAdminLevels, filterInventory, lastUpdated); + return descendants; } public List getDescendants( @@ -158,15 +169,18 @@ public List getDescendants( } Bundle childLocationBundle = - query.usingStyle(SearchStyleEnum.POST).returnBundle(Bundle.class).execute(); + query.usingStyle(SearchStyleEnum.POST) + .count( + SyncAccessDecision.SyncAccessDecisionConstants + .REL_LOCATION_CHUNK_SIZE) + .returnBundle(Bundle.class) + .execute(); List allLocations = Collections.synchronizedList(new ArrayList<>()); if (parentLocation != null) { allLocations.add(parentLocation); } - if (childLocationBundle != null) { - childLocationBundle.getEntry().parallelStream() .forEach( childLocation -> { @@ -179,6 +193,24 @@ public List getDescendants( null, adminLevels)); }); + + while (childLocationBundle.getLink(Bundle.LINK_NEXT) != null) { + childLocationBundle = + getFhirClientForR4().loadPage().next(childLocationBundle).execute(); + + childLocationBundle.getEntry().parallelStream() + .forEach( + childLocation -> { + Location childLocationEntity = + (Location) childLocation.getResource(); + allLocations.add(childLocationEntity); + allLocations.addAll( + getDescendants( + childLocationEntity.getIdElement().getIdPart(), + null, + adminLevels)); + }); + } } return allLocations; @@ -199,18 +231,25 @@ public List getDescendants( public Bundle handleIdentifierRequest(HttpServletRequest request, String identifier) { String administrativeLevelMin = request.getParameter(Constants.MIN_ADMIN_LEVEL); String administrativeLevelMax = request.getParameter(Constants.MAX_ADMIN_LEVEL); + String mode = request.getParameter(Constants.MODE); + Boolean filterInventory = Boolean.valueOf(request.getParameter(Constants.FILTER_INVENTORY)); + String lastUpdated = ""; List preFetchAdminLevels = generateAdminLevels( String.valueOf(Constants.DEFAULT_MIN_ADMIN_LEVEL), administrativeLevelMax); List postFetchAdminLevels = generateAdminLevels(administrativeLevelMin, administrativeLevelMax); - String mode = request.getParameter(Constants.MODE); if (Constants.LIST.equals(mode)) { List locationIds = Collections.singletonList(identifier); return getPaginatedLocations(request, locationIds); } else { LocationHierarchy locationHierarchy = - getLocationHierarchy(identifier, preFetchAdminLevels, postFetchAdminLevels); + getLocationHierarchy( + identifier, + preFetchAdminLevels, + postFetchAdminLevels, + filterInventory, + lastUpdated); return Utils.createBundle(Collections.singletonList(locationHierarchy)); } } @@ -223,6 +262,7 @@ public Bundle handleNonIdentifierRequest( String syncLocationsParam = request.getParameter(Constants.SYNC_LOCATIONS_SEARCH_PARAM); String administrativeLevelMin = request.getParameter(Constants.MIN_ADMIN_LEVEL); String administrativeLevelMax = request.getParameter(Constants.MAX_ADMIN_LEVEL); + Boolean filterInventory = Boolean.valueOf(request.getParameter(Constants.FILTER_INVENTORY)); List preFetchAdminLevels = generateAdminLevels( String.valueOf(Constants.DEFAULT_MIN_ADMIN_LEVEL), administrativeLevelMax); @@ -233,6 +273,7 @@ public Bundle handleNonIdentifierRequest( List userRoles = JwtUtils.getUserRolesFromJWT(verifiedJwt); String applicationId = JwtUtils.getApplicationIdFromJWT(verifiedJwt); String syncStrategy = getSyncStrategyByAppId(applicationId); + String lastUpdated = ""; if (Constants.LIST.equals(mode)) { if (Constants.SyncStrategy.RELATED_ENTITY_LOCATION.equalsIgnoreCase(syncStrategy) @@ -241,15 +282,9 @@ public Bundle handleNonIdentifierRequest( return getPaginatedLocations(request, selectedSyncLocations); } else { - List locations = - practitionerDetailsEndpointHelper - .getPractitionerDetailsByKeycloakId(practitionerId) - .getFhirPractitionerDetails() - .getLocations(); - List locationIds = new ArrayList<>(); - for (Location location : locations) { - locationIds.add(location.getIdElement().getIdPart()); - } + List locationIds = + practitionerDetailsEndpointHelper.getPractitionerLocationIdsByByKeycloakId( + practitionerId); return getPaginatedLocations(request, locationIds); } @@ -259,22 +294,35 @@ public Bundle handleNonIdentifierRequest( && !selectedSyncLocations.isEmpty()) { List locationHierarchies = getLocationHierarchies( - selectedSyncLocations, preFetchAdminLevels, postFetchAdminLevels); + selectedSyncLocations, + preFetchAdminLevels, + postFetchAdminLevels, + filterInventory, + lastUpdated); List resourceList = - locationHierarchies.stream() - .map(locationHierarchy -> (Resource) locationHierarchy) - .collect(Collectors.toList()); + locationHierarchies != null + ? locationHierarchies.stream() + .map(locationHierarchy -> (Resource) locationHierarchy) + .collect(Collectors.toList()) + : Collections.emptyList(); return Utils.createBundle(resourceList); } else { + List locationIds = + practitionerDetailsEndpointHelper.getPractitionerLocationIdsByByKeycloakId( + practitionerId); List locationHierarchies = - practitionerDetailsEndpointHelper - .getPractitionerDetailsByKeycloakId(practitionerId) - .getFhirPractitionerDetails() - .getLocationHierarchyList(); + getLocationHierarchies( + locationIds, + preFetchAdminLevels, + postFetchAdminLevels, + filterInventory, + lastUpdated); List resourceList = - locationHierarchies.stream() - .map(locationHierarchy -> (Resource) locationHierarchy) - .collect(Collectors.toList()); + locationHierarchies != null + ? locationHierarchies.stream() + .map(locationHierarchy -> (Resource) locationHierarchy) + .collect(Collectors.toList()) + : Collections.emptyList(); return Utils.createBundle(resourceList); } } @@ -330,6 +378,9 @@ public Bundle getPaginatedLocations(HttpServletRequest request, List loc String pageNumber = request.getParameter(Constants.PAGINATION_PAGE_NUMBER); String administrativeLevelMin = request.getParameter(Constants.MIN_ADMIN_LEVEL); String administrativeLevelMax = request.getParameter(Constants.MAX_ADMIN_LEVEL); + Boolean filterInventory = Boolean.valueOf(request.getParameter(Constants.FILTER_INVENTORY)); + String lastUpdated = request.getParameter(Constants.LAST_UPDATED); + String summary = request.getParameter(Constants.SUMMARY); List preFetchAdminLevels = generateAdminLevels( String.valueOf(Constants.DEFAULT_MIN_ADMIN_LEVEL), administrativeLevelMax); @@ -348,19 +399,31 @@ public Bundle getPaginatedLocations(HttpServletRequest request, List loc int start = Math.max(0, (page - 1)) * count; - List resourceLocations = new ArrayList<>(); - for (String identifier : locationIds) { - Location parentLocation = getLocationById(identifier); - List locations = - getLocationHierarchyLocations( - identifier, parentLocation, preFetchAdminLevels, postFetchAdminLevels); - resourceLocations.addAll(locations); - } + List resourceLocations = + locationIds.parallelStream() + .flatMap( + identifier -> + getLocationHierarchyLocations( + identifier, + getLocationById(identifier), + preFetchAdminLevels, + postFetchAdminLevels, + filterInventory, + lastUpdated) + .stream()) + .collect(Collectors.toList()); int totalEntries = resourceLocations.size(); int end = Math.min(start + count, resourceLocations.size()); List paginatedResourceLocations = resourceLocations.subList(start, end); Bundle resultBundle; + if (Constants.COUNT.equals(summary)) { + resultBundle = + Utils.createEmptyBundle( + request.getRequestURL() + "?" + request.getQueryString()); + resultBundle.setTotal(totalEntries); + return resultBundle; + } if (resourceLocations.isEmpty()) { resultBundle = @@ -415,24 +478,57 @@ public List generateAdminLevels( return adminLevels; } - public List filterLocationsByAdminLevels( - List locations, List postFetchAdminLevels) { - if (postFetchAdminLevels == null) { - return locations; - } - List allLocations = new ArrayList<>(); - for (Location location : locations) { - for (CodeableConcept codeableConcept : location.getType()) { - List codings = codeableConcept.getCoding(); - for (Coding coding : codings) { - if (coding.getSystem().equals(Constants.DEFAULT_ADMIN_LEVEL_TYPE_URL)) { - if (postFetchAdminLevels.contains(coding.getCode())) { - allLocations.add(location); - } - } - } - } - } - return allLocations; + public List postFetchFilters( + List locations, + List postFetchAdminLevels, + boolean filterByInventory, + String lastUpdated) { + return locations.stream() + .filter( + location -> + postFetchAdminLevels == null + || postFetchAdminLevels.isEmpty() + || adminLevelFilter(location, postFetchAdminLevels)) + .filter( + location -> + lastUpdated == null + || lastUpdated.isBlank() + || lastUpdatedFilter(location, lastUpdated)) + .filter(location -> !filterByInventory || inventoryFilter(location)) + .collect(Collectors.toList()); + } + + public boolean adminLevelFilter(Location location, List postFetchAdminLevels) { + return location.getType().stream() + .flatMap(codeableConcept -> codeableConcept.getCoding().stream()) + .anyMatch( + coding -> + Constants.DEFAULT_ADMIN_LEVEL_TYPE_URL.equals(coding.getSystem()) + && postFetchAdminLevels.contains(coding.getCode())); + } + + public boolean lastUpdatedFilter(Location location, String lastUpdated) { + Date locationlastUpdated = location.getMeta().getLastUpdated(); + OffsetDateTime locationLastUpdated = + locationlastUpdated.toInstant().atOffset(ZoneOffset.UTC); + return locationLastUpdated.isAfter(OffsetDateTime.parse(lastUpdated)) + || locationLastUpdated.isEqual(OffsetDateTime.parse(lastUpdated)); + } + + public boolean inventoryFilter(Location location) { + String locationId = location.getIdElement().getIdPart(); + String locationReference = + Constants.SyncStrategy.LOCATION + Constants.FORWARD_SLASH + locationId; + + Bundle listBundle = + getFhirClientForR4() + .search() + .forResource(ListResource.class) + .where(new ReferenceClientParam(Constants.SUBJECT).hasId(locationReference)) + .usingStyle(SearchStyleEnum.POST) + .returnBundle(Bundle.class) + .execute(); + + return listBundle != null && !listBundle.getEntry().isEmpty(); } } diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java index 870452be..039a4566 100755 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java @@ -16,7 +16,6 @@ import org.hl7.fhir.r4.model.CareTeam; import org.hl7.fhir.r4.model.Composition; import org.hl7.fhir.r4.model.Organization; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.smartregister.fhir.gateway.plugins.interfaces.ResourceFinder; @@ -40,6 +39,7 @@ import ca.uhn.fhir.rest.api.SearchStyleEnum; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import jakarta.annotation.Nonnull; public class PermissionAccessChecker implements AccessChecker { private static final Logger logger = LoggerFactory.getLogger(PermissionAccessChecker.class); @@ -291,7 +291,7 @@ private List getLocationUuids(String[] syncLocations) { return locationUuids; } - @NotNull + @Nonnull private Map> collateSyncStrategyIds( String syncStrategy, PractitionerDetails practitionerDetails, diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java index 1ef9e969..520f3abe 100755 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java @@ -23,18 +23,20 @@ import org.hl7.fhir.r4.model.OrganizationAffiliation; import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.PractitionerRole; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.smartregister.model.location.LocationHierarchy; import org.smartregister.model.location.ParentChildrenMap; import org.smartregister.model.practitioner.FhirPractitionerDetails; import org.smartregister.model.practitioner.PractitionerDetails; -import org.springframework.lang.Nullable; + +import com.google.common.annotations.VisibleForTesting; import ca.uhn.fhir.rest.api.SearchStyleEnum; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.gclient.ReferenceClientParam; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; public class PractitionerDetailsEndpointHelper { private static final Logger logger = @@ -88,7 +90,8 @@ public Bundle getSupervisorPractitionerDetailsByKeycloakId(String keycloakUuid) return bundle; } - private Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner practitioner) { + @VisibleForTesting + protected Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner practitioner) { Bundle responseBundle = new Bundle(); List attributedPractitioners = new ArrayList<>(); PractitionerDetails practitionerDetails = @@ -157,8 +160,10 @@ private Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner pract return responseBundle; } - @NotNull + @Nonnull public static Set getAttributedLocations(List locationHierarchies) { + locationHierarchies = + locationHierarchies != null ? locationHierarchies : Collections.emptyList(); List parentChildrenList = locationHierarchies.stream() .flatMap( @@ -189,7 +194,8 @@ public static Set getAttributedLocations(List locatio .collect(Collectors.toSet()); } - private List getOrganizationIdsByLocationIds(Set attributedLocationsList) { + @VisibleForTesting + protected List getOrganizationIdsByLocationIds(Set attributedLocationsList) { if (attributedLocationsList == null || attributedLocationsList.isEmpty()) { return new ArrayList<>(); } @@ -247,6 +253,70 @@ public PractitionerDetails getPractitionerDetailsByPractitioner(Practitioner pra return practitionerDetails; } + public List getPractitionerLocationIdsByByKeycloakId(String keycloakUUID) { + logger.info("Searching for Practitioner with user id: " + keycloakUUID); + Practitioner practitioner = getPractitionerByIdentifier(keycloakUUID); + List locationIds = new ArrayList<>(); + + if (practitioner != null) { + String practitionerId = getPractitionerIdentifier(practitioner); + if (CacheHelper.INSTANCE.skipCache()) { + locationIds = getPractitionerLocationIdsByByKeycloakIdCore(practitionerId); + } else { + locationIds = + CacheHelper.INSTANCE.listStringCache.get( + keycloakUUID, + key -> + getPractitionerLocationIdsByByKeycloakIdCore( + practitionerId)); + } + + } else { + logger.error("Practitioner with KC identifier : " + keycloakUUID + " not found"); + } + return locationIds; + } + + @VisibleForTesting + protected List getPractitionerLocationIdsByByKeycloakIdCore(String practitionerId) { + + logger.info("Searching for CareTeams with Practitioner Id: " + practitionerId); + Bundle careTeams = getCareTeams(practitionerId); + List careTeamsList = mapBundleToCareTeams(careTeams); + + logger.info( + "Searching for Organizations tied to CareTeams list of size : " + + careTeamsList.size()); + Set careTeamManagingOrganizationIds = + getManagingOrganizationsOfCareTeamIds(careTeamsList); + + List practitionerRoleList = + getPractitionerRolesByPractitionerId(practitionerId); + logger.info("Practitioner Roles fetched: " + practitionerRoleList.size()); + + Set practitionerOrganizationIds = + getOrganizationIdsByPractitionerRoles(practitionerRoleList); + + Set organizationIds = + Stream.concat( + careTeamManagingOrganizationIds.stream(), + practitionerOrganizationIds.stream()) + .collect(Collectors.toSet()); + + logger.info("Searching for locations by organizations: " + organizationIds.size()); + + Bundle organizationAffiliationsBundle = + getOrganizationAffiliationsByOrganizationIdsBundle(organizationIds); + + List organizationAffiliations = + mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); + + List locationIds = + getLocationIdsByOrganizationAffiliations(organizationAffiliations); + + return locationIds; + } + public PractitionerDetails getPractitionerDetailsByPractitionerCore( String practitionerId, Practitioner practitioner) { @@ -323,10 +393,11 @@ public PractitionerDetails getPractitionerDetailsByPractitionerCore( List locationIds = getLocationIdsByOrganizationAffiliations(organizationAffiliations); - logger.info("Searching for location hierarchy list by locations identifiers"); - List locationHierarchyList = getLocationsHierarchy(locationIds); - - fhirPractitionerDetails.setLocationHierarchyList(locationHierarchyList); + // logger.info("Searching for location hierarchy list by locations identifiers"); + // List locationHierarchyList = + // getLocationsHierarchy(locationIds); + // + // fhirPractitionerDetails.setLocationHierarchyList(locationHierarchyList); logger.info("Searching for locations by ids : " + locationIds); List locationsList = getLocationsByIds(locationIds); @@ -363,12 +434,14 @@ public static Predicate distinctByKey(Function uniqueKeyExt return t -> seen.add(uniqueKeyExtractor.apply(t)); } - private List getPractitionerRolesByPractitionerId(String practitionerId) { + @VisibleForTesting + protected List getPractitionerRolesByPractitionerId(String practitionerId) { Bundle practitionerRoles = getPractitionerRoles(practitionerId); return mapBundleToPractitionerRolesWithOrganization(practitionerRoles); } - private Set getOrganizationIdsByPractitionerRoles( + @VisibleForTesting + protected Set getOrganizationIdsByPractitionerRoles( List practitionerRoles) { return practitionerRoles.stream() .filter(PractitionerRole::hasOrganization) @@ -376,7 +449,8 @@ private Set getOrganizationIdsByPractitionerRoles( .collect(Collectors.toSet()); } - private Practitioner getPractitionerByIdentifier(String identifier) { + @VisibleForTesting + protected Practitioner getPractitionerByIdentifier(String identifier) { Bundle resultBundle = getFhirClientForR4() .search() @@ -391,7 +465,8 @@ private Practitioner getPractitionerByIdentifier(String identifier) { : null; } - private List getCareTeamsByOrganizationIds(List organizationIds) { + @VisibleForTesting + protected List getCareTeamsByOrganizationIds(List organizationIds) { if (organizationIds.isEmpty()) return new ArrayList<>(); Bundle bundle = @@ -421,7 +496,8 @@ private List getCareTeamsByOrganizationIds(List organizationId .collect(Collectors.toList()); } - private Bundle getCareTeams(String practitionerId) { + @VisibleForTesting + protected Bundle getCareTeams(String practitionerId) { return getFhirClientForR4() .search() .forResource(CareTeam.class) @@ -450,7 +526,8 @@ private static String getReferenceIDPart(String reference) { reference.lastIndexOf(org.smartregister.utils.Constants.FORWARD_SLASH) + 1); } - private Bundle getOrganizationsById(Set organizationIds) { + @VisibleForTesting + protected Bundle getOrganizationsById(Set organizationIds) { return organizationIds.isEmpty() ? EMPTY_BUNDLE : getFhirClientForR4() @@ -464,7 +541,8 @@ private Bundle getOrganizationsById(Set organizationIds) { .execute(); } - private @Nullable List getLocationsByIds(List locationIds) { + @VisibleForTesting + protected @Nullable List getLocationsByIds(List locationIds) { if (locationIds == null || locationIds.isEmpty()) { return new ArrayList<>(); } @@ -485,7 +563,8 @@ private Bundle getOrganizationsById(Set organizationIds) { .collect(Collectors.toList()); } - private List getOrganizationAffiliationsByOrganizationIds( + @VisibleForTesting + protected List getOrganizationAffiliationsByOrganizationIds( Set organizationIds) { if (organizationIds == null || organizationIds.isEmpty()) { return new ArrayList<>(); @@ -495,7 +574,9 @@ private List getOrganizationAffiliationsByOrganizationI return mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); } - private Bundle getOrganizationAffiliationsByOrganizationIdsBundle(Set organizationIds) { + @VisibleForTesting + protected Bundle getOrganizationAffiliationsByOrganizationIdsBundle( + Set organizationIds) { return organizationIds.isEmpty() ? EMPTY_BUNDLE : getFhirClientForR4() @@ -509,7 +590,8 @@ private Bundle getOrganizationAffiliationsByOrganizationIdsBundle(Set or .execute(); } - private List getLocationIdsByOrganizationAffiliations( + @VisibleForTesting + protected List getLocationIdsByOrganizationAffiliations( List organizationAffiliations) { return organizationAffiliations.stream() @@ -523,7 +605,8 @@ private List getLocationIdsByOrganizationAffiliations( .collect(Collectors.toList()); } - private Set getManagingOrganizationsOfCareTeamIds(List careTeamsList) { + @VisibleForTesting + protected Set getManagingOrganizationsOfCareTeamIds(List careTeamsList) { return careTeamsList.stream() .filter(CareTeam::hasManagingOrganization) .flatMap(it -> it.getManagingOrganization().stream()) @@ -531,7 +614,8 @@ private Set getManagingOrganizationsOfCareTeamIds(List careTea .collect(Collectors.toSet()); } - private List mapBundleToCareTeams(Bundle careTeams) { + @VisibleForTesting + protected List mapBundleToCareTeams(Bundle careTeams) { return careTeams != null ? careTeams.getEntry().stream() .map(bundleEntryComponent -> (CareTeam) bundleEntryComponent.getResource()) @@ -556,7 +640,8 @@ private List mapBundleToGroups(Bundle groupsBundle) { : Collections.emptyList(); } - private List mapBundleToOrganizationAffiliation( + @VisibleForTesting + protected List mapBundleToOrganizationAffiliation( Bundle organizationAffiliationBundle) { return organizationAffiliationBundle != null ? organizationAffiliationBundle.getEntry().stream() @@ -573,7 +658,8 @@ public static List getLocationsHierarchy(List locatio .map( locationsIdentifier -> new LocationHierarchyEndpointHelper(r4FHIRClient) - .getLocationHierarchy(locationsIdentifier, null, null)) + .getLocationHierarchy( + locationsIdentifier, null, null, false, "")) .filter( locationHierarchy -> !org.smartregister.utils.Constants.LOCATION_RESOURCE_NOT_FOUND diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java index ee00dc6b..65c0d9b6 100755 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java @@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory; import org.smartregister.fhir.gateway.plugins.interfaces.ResourceFinder; +import com.google.common.annotations.VisibleForTesting; import com.google.fhir.gateway.ExceptionUtil; import com.google.fhir.gateway.interfaces.RequestDetailsReader; @@ -30,7 +31,8 @@ private ResourceFinderImp(FhirContext fhirContext) { this.fhirContext = fhirContext; } - private IBaseResource createResourceFromRequest(RequestDetailsReader request) { + @VisibleForTesting + protected IBaseResource createResourceFromRequest(RequestDetailsReader request) { byte[] requestContentBytes = request.loadRequestContents(); Charset charset = request.getCharset(); if (charset == null) { diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtils.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtils.java index 5afb3a44..5d07a384 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtils.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtils.java @@ -2,9 +2,6 @@ import static org.smartregister.fhir.gateway.plugins.Constants.AUTHORIZATION; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,6 +9,8 @@ import com.google.fhir.gateway.TokenVerifier; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; public class RestUtils { diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java index cb78dabd..d67eb89c 100755 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java @@ -13,8 +13,6 @@ import java.util.Map; import java.util.stream.Collectors; -import javax.annotation.Nullable; - import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpResponse; @@ -25,7 +23,6 @@ import org.hl7.fhir.r4.model.ListResource; import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Resource; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,6 +39,8 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import lombok.Getter; public class SyncAccessDecision implements AccessDecision { @@ -152,7 +151,7 @@ public RequestMutation getRequestMutation(RequestDetailsReader requestDetailsRea requestMutation = RequestMutation.builder() - .queryParams( + .additionalQueryParams( Map.of( Constants.TAG_SEARCH_PARAM, List.of( @@ -359,7 +358,7 @@ private boolean includeAttributedPractitioners(String requestPath) { && SyncAccessDecisionConstants.ENDPOINT_PRACTITIONER_DETAILS.equals(requestPath); } - @NotNull + @Nonnull private static OperationOutcome createOperationOutcome(String exception) { OperationOutcome operationOutcome = new OperationOutcome(); OperationOutcome.OperationOutcomeIssueComponent operationOutcomeIssueComponent = @@ -371,7 +370,7 @@ private static OperationOutcome createOperationOutcome(String exception) { return operationOutcome; } - @NotNull + @Nonnull private static Bundle processListEntriesGatewayModeByListResource( ListResource responseListResource, int start, int count) { Bundle requestBundle = new Bundle(); @@ -418,7 +417,7 @@ private Bundle processListEntriesGatewayModeByBundle( return requestBundle.setEntry(bundleEntryComponentList); } - @NotNull + @Nonnull static Bundle.BundleEntryComponent createBundleEntryComponent( Bundle.HTTPVerb method, String requestPath, @Nullable String condition) { diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/endpoint/BaseEndpoint.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/endpoint/BaseEndpoint.java index 80c01144..a4fe70eb 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/endpoint/BaseEndpoint.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/endpoint/BaseEndpoint.java @@ -6,10 +6,6 @@ import java.io.PrintWriter; import java.nio.charset.StandardCharsets; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.smartregister.fhir.gateway.plugins.RestUtils; @@ -18,14 +14,25 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; public abstract class BaseEndpoint extends HttpServlet { private static final Logger logger = LoggerFactory.getLogger(BaseEndpoint.class); - protected final TokenVerifier tokenVerifier = TokenVerifier.createFromEnvVars(); - protected final FhirContext fhirR4Context = FhirContext.forR4(); + protected static TokenVerifier tokenVerifier; + protected final FhirContext fhirR4Context = FhirContext.forR4Cached(); protected final IParser fhirR4JsonParser = fhirR4Context.newJsonParser().setPrettyPrint(true); + static { + try { + tokenVerifier = TokenVerifier.createFromEnvVars(); + } catch (Exception exception) { + logger.error(exception.getMessage()); + } + } + @Override protected void doOptions(HttpServletRequest request, HttpServletResponse response) { RestUtils.addCorsHeaders(response); diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/endpoint/LocationHierarchyEndpoint.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/endpoint/LocationHierarchyEndpoint.java index b0c79f9f..bac1b75a 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/endpoint/LocationHierarchyEndpoint.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/endpoint/LocationHierarchyEndpoint.java @@ -4,10 +4,6 @@ import java.io.IOException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import org.apache.http.HttpStatus; import org.hl7.fhir.r4.model.Bundle; import org.smartregister.fhir.gateway.plugins.Constants; @@ -18,6 +14,9 @@ import com.auth0.jwt.interfaces.DecodedJWT; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; @WebServlet("/LocationHierarchy") public class LocationHierarchyEndpoint extends BaseEndpoint { diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/endpoint/PractitionerDetailEndpoint.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/endpoint/PractitionerDetailEndpoint.java index 3f174177..35412a5a 100755 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/endpoint/PractitionerDetailEndpoint.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/endpoint/PractitionerDetailEndpoint.java @@ -5,10 +5,6 @@ import java.io.IOException; import java.util.Collections; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import org.apache.http.HttpStatus; import org.smartregister.fhir.gateway.plugins.Constants; import org.smartregister.fhir.gateway.plugins.PractitionerDetailsEndpointHelper; @@ -17,6 +13,9 @@ import org.smartregister.model.practitioner.PractitionerDetails; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; @WebServlet("/PractitionerDetail") public class PractitionerDetailEndpoint extends BaseEndpoint { diff --git a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelperTest.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelperTest.java index 92d9e44a..6521a724 100644 --- a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelperTest.java +++ b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelperTest.java @@ -6,20 +6,23 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import java.time.OffsetDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; -import javax.servlet.http.HttpServletRequest; - import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.ListResource; import org.hl7.fhir.r4.model.Location; +import org.hl7.fhir.r4.model.Meta; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -27,6 +30,7 @@ import org.mockito.Mockito; import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs; import org.smartregister.model.location.LocationHierarchy; +import org.smartregister.model.location.LocationHierarchyTree; import com.auth0.jwt.interfaces.DecodedJWT; @@ -36,6 +40,7 @@ import ca.uhn.fhir.rest.gclient.IQuery; import ca.uhn.fhir.rest.gclient.IUntypedQuery; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import jakarta.servlet.http.HttpServletRequest; public class LocationHierarchyEndpointHelperTest { @@ -54,7 +59,8 @@ public void testGetLocationHierarchyNotFound() { .when(client) .fetchResourceFromUrl(any(), any()); LocationHierarchy locationHierarchy = - locationHierarchyEndpointHelper.getLocationHierarchy("non-existent", null, null); + locationHierarchyEndpointHelper.getLocationHierarchy( + "non-existent", null, null, false, ""); assertEquals( org.smartregister.utils.Constants.LOCATION_RESOURCE_NOT_FOUND, locationHierarchy.getId()); @@ -69,7 +75,8 @@ public void testGetLocationHierarchyFound() { .when(client) .fetchResourceFromUrl(Location.class, "Location/12345"); LocationHierarchy locationHierarchy = - locationHierarchyEndpointHelper.getLocationHierarchy("12345", null, null); + locationHierarchyEndpointHelper.getLocationHierarchy( + "12345", null, null, false, ""); assertEquals("Location Resource : 12345", locationHierarchy.getId()); } @@ -93,19 +100,31 @@ public void testGetPaginatedLocationsPaginatesLocations() { LocationHierarchyEndpointHelper mockLocationHierarchyEndpointHelper = mock(LocationHierarchyEndpointHelper.class); - List locations = createLocationList(5, false); + List locations = createTestLocationList(5, false, false); List adminLevels = new ArrayList<>(); List locationIds = Collections.singletonList("12345"); + + Mockito.doReturn(false) + .when(mockLocationHierarchyEndpointHelper) + .adminLevelFilter(Mockito.any(Location.class), Mockito.any()); + Mockito.doReturn(false) + .when(mockLocationHierarchyEndpointHelper) + .lastUpdatedFilter(Mockito.any(Location.class), Mockito.any()); + Mockito.doReturn(false) + .when(mockLocationHierarchyEndpointHelper) + .inventoryFilter(Mockito.any(Location.class)); Mockito.doCallRealMethod() .when(mockLocationHierarchyEndpointHelper) .getPaginatedLocations(request, locationIds); Mockito.doCallRealMethod() .when(mockLocationHierarchyEndpointHelper) - .filterLocationsByAdminLevels(locations, adminLevels); + .postFetchFilters(locations, adminLevels, false, ""); + Mockito.doReturn(locations) .when(mockLocationHierarchyEndpointHelper) - .getLocationHierarchyLocations("12345", null, adminLevels, adminLevels); + .getLocationHierarchyLocations( + "12345", null, adminLevels, adminLevels, false, null); Bundle resultBundle = mockLocationHierarchyEndpointHelper.getPaginatedLocations(request, locationIds); @@ -160,7 +179,7 @@ public void testHandleNonIdentifierRequestListModePaginatesLocations() { MockedStatic mockJwtUtils = Mockito.mockStatic(JwtUtils.class); List adminLevels = new ArrayList<>(); - List locations = createLocationList(4, false); + List locations = createTestLocationList(4, false, false); List locationIds = List.of("1", "2", "3", "4"); List userRoles = Collections.singletonList(Constants.ROLE_ALL_LOCATIONS); @@ -175,13 +194,27 @@ public void testHandleNonIdentifierRequestListModePaginatesLocations() { .when(mockLocationHierarchyEndpointHelper) .handleNonIdentifierRequest( request, mockPractitionerDetailsEndpointHelper, mockDecodedJWT); + Mockito.doReturn(false) + .when(mockLocationHierarchyEndpointHelper) + .adminLevelFilter(Mockito.any(), Mockito.any()); + Mockito.doReturn(false) + .when(mockLocationHierarchyEndpointHelper) + .lastUpdatedFilter(Mockito.any(Location.class), Mockito.any()); + Mockito.doReturn(false) + .when(mockLocationHierarchyEndpointHelper) + .inventoryFilter(Mockito.any(Location.class)); Mockito.doCallRealMethod() .when(mockLocationHierarchyEndpointHelper) - .filterLocationsByAdminLevels(locations, adminLevels); + .postFetchFilters(locations, adminLevels, false, ""); Mockito.doReturn(locations) .when(mockLocationHierarchyEndpointHelper) .getLocationHierarchyLocations( - Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.anyString(), + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.any(), + Mockito.any()); Mockito.doReturn(Constants.SyncStrategy.RELATED_ENTITY_LOCATION) .when(mockLocationHierarchyEndpointHelper) .getSyncStrategyByAppId(Mockito.any()); @@ -198,6 +231,7 @@ public void testHandleNonIdentifierRequestListModePaginatesLocations() { Assert.assertTrue(resultBundle.hasLink()); Assert.assertTrue(resultBundle.hasTotal()); Assert.assertEquals(16, resultBundle.getEntry().size()); + mockJwtUtils.close(); } @Test @@ -271,8 +305,10 @@ public void testGetDescendantsWithAdminLevelFiltersReturnsLocationsWithinAdminLe Mockito.doReturn(queryMock).when(queryMock).where(any(ICriterion.class)); Mockito.doReturn(queryMock).when(queryMock).and(any(ICriterion.class)); Mockito.doReturn(queryMock).when(queryMock).usingStyle(SearchStyleEnum.POST); + Mockito.doReturn(queryMock) + .when(queryMock) + .count(SyncAccessDecision.SyncAccessDecisionConstants.REL_LOCATION_CHUNK_SIZE); Mockito.doReturn(queryMock).when(queryMock).returnBundle(Bundle.class); - Mockito.doReturn(firstBundleMock, secondBundleMock).when(queryMock).execute(); List descendants = @@ -289,12 +325,11 @@ public void testGetDescendantsWithAdminLevelFiltersReturnsLocationsWithinAdminLe @Test public void testFilterLocationsByAdminLevelsBasic() { - List locations = createLocationList(5, true); + List locations = createTestLocationList(5, true, false); List adminLevels = List.of("1", "3"); List filteredLocations = - locationHierarchyEndpointHelper.filterLocationsByAdminLevels( - locations, adminLevels); + locationHierarchyEndpointHelper.postFetchFilters(locations, adminLevels, false, ""); Assert.assertEquals(2, filteredLocations.size()); Assert.assertEquals("1", filteredLocations.get(0).getId()); @@ -303,10 +338,10 @@ public void testFilterLocationsByAdminLevelsBasic() { @Test public void testFilterLocationsByAdminLevelsWithNullAdminLevelsDoesNotFilter() { - List locations = createLocationList(5, true); + List locations = createTestLocationList(5, true, false); List filteredLocations = - locationHierarchyEndpointHelper.filterLocationsByAdminLevels(locations, null); + locationHierarchyEndpointHelper.postFetchFilters(locations, null, false, ""); Assert.assertEquals(5, filteredLocations.size()); Assert.assertEquals("0", filteredLocations.get(0).getId()); @@ -316,6 +351,259 @@ public void testFilterLocationsByAdminLevelsWithNullAdminLevelsDoesNotFilter() { Assert.assertEquals("4", filteredLocations.get(4).getId()); } + @Test + public void testFilterLocationsByInventoryWithInventory() { + IUntypedQuery untypedQueryMock = mock(IUntypedQuery.class); + IQuery queryMock = mock(IQuery.class); + + Bundle bundleWithInventory = new Bundle(); + List entriesWithInventory = new ArrayList<>(); + + ListResource resource1 = new ListResource(); + resource1.setId("1"); + entriesWithInventory.add(new Bundle.BundleEntryComponent().setResource(resource1)); + bundleWithInventory.setEntry(entriesWithInventory); + + Mockito.doReturn(untypedQueryMock).when(client).search(); + Mockito.doReturn(queryMock).when(untypedQueryMock).forResource(ListResource.class); + Mockito.doReturn(queryMock).when(queryMock).where(any(ICriterion.class)); + Mockito.doReturn(queryMock).when(queryMock).usingStyle(SearchStyleEnum.POST); + Mockito.doReturn(queryMock).when(queryMock).returnBundle(Bundle.class); + Mockito.doReturn(bundleWithInventory).when(queryMock).execute(); + + List locations = createTestLocationList(5, true, false); + List filteredLocations = + locationHierarchyEndpointHelper.postFetchFilters(locations, null, true, ""); + + Assert.assertNotNull(filteredLocations); + Assert.assertEquals(5, filteredLocations.size()); + } + + @Test + public void testFilterLocationsByInventoryNoInventory() { + IUntypedQuery untypedQueryMock = mock(IUntypedQuery.class); + IQuery queryMock = mock(IQuery.class); + + Bundle bundleWithInventory = new Bundle(); + + Mockito.doReturn(untypedQueryMock).when(client).search(); + Mockito.doReturn(queryMock).when(untypedQueryMock).forResource(ListResource.class); + Mockito.doReturn(queryMock).when(queryMock).where(any(ICriterion.class)); + Mockito.doReturn(queryMock).when(queryMock).usingStyle(SearchStyleEnum.POST); + Mockito.doReturn(queryMock).when(queryMock).returnBundle(Bundle.class); + Mockito.doReturn(bundleWithInventory).when(queryMock).execute(); + + List locations = createTestLocationList(5, true, false); + List filteredLocations = + locationHierarchyEndpointHelper.postFetchFilters(locations, null, true, ""); + + Assert.assertNotNull(filteredLocations); + Assert.assertEquals(0, filteredLocations.size()); + } + + @Test + public void testGetPaginatedLocationsSummaryReturnsSummary() { + HttpServletRequest request = mock(HttpServletRequest.class); + Mockito.doReturn("12345").when(request).getParameter(Constants.IDENTIFIER); + Mockito.doReturn("2").when(request).getParameter(Constants.PAGINATION_PAGE_SIZE); + Mockito.doReturn("2").when(request).getParameter(Constants.PAGINATION_PAGE_NUMBER); + Mockito.doReturn(Constants.COUNT).when(request).getParameter(Constants.SUMMARY); + Mockito.doReturn(new StringBuffer("http://test:8080/LocationHierarchy")) + .when(request) + .getRequestURL(); + + Map parameters = new HashMap<>(); + parameters.put(Constants.IDENTIFIER, new String[] {"12345"}); + parameters.put(Constants.PAGINATION_PAGE_SIZE, new String[] {"2"}); + parameters.put(Constants.PAGINATION_PAGE_NUMBER, new String[] {"2"}); + parameters.put(Constants.SUMMARY, new String[] {Constants.COUNT}); + + Mockito.doReturn(parameters).when(request).getParameterMap(); + + LocationHierarchyEndpointHelper mockLocationHierarchyEndpointHelper = + mock(LocationHierarchyEndpointHelper.class); + List locations = createTestLocationList(5, false, false); + List adminLevels = new ArrayList<>(); + + List locationIds = Collections.singletonList("12345"); + + Mockito.doReturn(false) + .when(mockLocationHierarchyEndpointHelper) + .adminLevelFilter(Mockito.any(Location.class), Mockito.any()); + Mockito.doReturn(false) + .when(mockLocationHierarchyEndpointHelper) + .lastUpdatedFilter(Mockito.any(Location.class), Mockito.any()); + Mockito.doReturn(false) + .when(mockLocationHierarchyEndpointHelper) + .inventoryFilter(Mockito.any(Location.class)); + Mockito.doCallRealMethod() + .when(mockLocationHierarchyEndpointHelper) + .getPaginatedLocations(request, locationIds); + Mockito.doCallRealMethod() + .when(mockLocationHierarchyEndpointHelper) + .postFetchFilters(locations, adminLevels, false, ""); + + Mockito.doReturn(locations) + .when(mockLocationHierarchyEndpointHelper) + .getLocationHierarchyLocations( + "12345", null, adminLevels, adminLevels, false, null); + + Bundle resultBundle = + mockLocationHierarchyEndpointHelper.getPaginatedLocations(request, locationIds); + + Assert.assertFalse(resultBundle.hasEntry()); + Assert.assertTrue(resultBundle.hasType()); + Assert.assertTrue(resultBundle.hasTotal()); + Assert.assertTrue(resultBundle.hasTotal()); + Assert.assertEquals(0, resultBundle.getEntry().size()); + Assert.assertEquals(5, resultBundle.getTotal()); + } + + @Test + public void + testHandleNonIdentifierRequestNonListModeWithSelectedLocationsReturnsLocationHierarchies() { + HttpServletRequest request = mock(HttpServletRequest.class); + Mockito.doReturn("1,2,3,4") + .when(request) + .getParameter(Constants.SYNC_LOCATIONS_SEARCH_PARAM); + Mockito.doReturn(new StringBuffer("http://test:8080/LocationHierarchy")) + .when(request) + .getRequestURL(); + Map parameters = new HashMap<>(); + parameters.put(Constants.SYNC_LOCATIONS_SEARCH_PARAM, new String[] {"1,2,3,4"}); + LocationHierarchyEndpointHelper mockLocationHierarchyEndpointHelper = + mock(LocationHierarchyEndpointHelper.class); + PractitionerDetailsEndpointHelper mockPractitionerDetailsEndpointHelper = + mock(PractitionerDetailsEndpointHelper.class); + DecodedJWT mockDecodedJWT = mock(DecodedJWT.class); + MockedStatic mockJwtUtils = Mockito.mockStatic(JwtUtils.class); + + List locations = createTestLocationList(4, false, false); + LocationHierarchy locationHierarchy = createLocationHierarchy(locations); + List locationHierarchies = new ArrayList<>(); + locationHierarchies.add(locationHierarchy); + + List userRoles = Collections.singletonList(Constants.ROLE_ALL_LOCATIONS); + + Mockito.doReturn(parameters).when(request).getParameterMap(); + Mockito.doCallRealMethod() + .when(mockLocationHierarchyEndpointHelper) + .extractSyncLocations("1,2,3,4"); + Mockito.doCallRealMethod() + .when(mockLocationHierarchyEndpointHelper) + .handleNonIdentifierRequest( + request, mockPractitionerDetailsEndpointHelper, mockDecodedJWT); + Mockito.doReturn(locationHierarchies) + .when(mockLocationHierarchyEndpointHelper) + .getLocationHierarchies( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.doReturn(Constants.SyncStrategy.RELATED_ENTITY_LOCATION) + .when(mockLocationHierarchyEndpointHelper) + .getSyncStrategyByAppId(Mockito.any()); + mockJwtUtils + .when(() -> JwtUtils.getUserRolesFromJWT(any(DecodedJWT.class))) + .thenReturn(userRoles); + Bundle resultBundle = + mockLocationHierarchyEndpointHelper.handleNonIdentifierRequest( + request, mockPractitionerDetailsEndpointHelper, mockDecodedJWT); + Assert.assertEquals(1, resultBundle.getTotal()); + Assert.assertEquals(1, resultBundle.getEntry().size()); + mockJwtUtils.close(); + } + + @Test + public void + testHandleNonIdentifierRequestNonListModeWithoutSelectedLocationsReturnsLocationHierarchies() { + HttpServletRequest request = mock(HttpServletRequest.class); + Mockito.doReturn("1,2,3,4") + .when(request) + .getParameter(Constants.SYNC_LOCATIONS_SEARCH_PARAM); + Mockito.doReturn(new StringBuffer("http://test:8080/LocationHierarchy")) + .when(request) + .getRequestURL(); + Map parameters = new HashMap<>(); + parameters.put(Constants.SYNC_LOCATIONS_SEARCH_PARAM, new String[] {"1,2,3,4"}); + LocationHierarchyEndpointHelper mockLocationHierarchyEndpointHelper = + mock(LocationHierarchyEndpointHelper.class); + PractitionerDetailsEndpointHelper mockPractitionerDetailsEndpointHelper = + mock(PractitionerDetailsEndpointHelper.class); + DecodedJWT mockDecodedJWT = mock(DecodedJWT.class); + MockedStatic mockJwtUtils = Mockito.mockStatic(JwtUtils.class); + List locations = createTestLocationList(4, false, false); + LocationHierarchy locationHierarchy = createLocationHierarchy(locations); + List locationHierarchies = new ArrayList<>(); + locationHierarchies.add(locationHierarchy); + List userRoles = Collections.singletonList(Constants.ROLE_ALL_LOCATIONS); + Mockito.doReturn(parameters).when(request).getParameterMap(); + Mockito.doReturn(Collections.emptyList()) + .when(mockLocationHierarchyEndpointHelper) + .extractSyncLocations(Mockito.any()); + Mockito.doCallRealMethod() + .when(mockLocationHierarchyEndpointHelper) + .handleNonIdentifierRequest( + request, mockPractitionerDetailsEndpointHelper, mockDecodedJWT); + Mockito.doReturn(locationHierarchies) + .when(mockLocationHierarchyEndpointHelper) + .getLocationHierarchies( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.doReturn(Arrays.asList("1", "2", "3", "4")) + .when(mockPractitionerDetailsEndpointHelper) + .getPractitionerLocationIdsByByKeycloakId(Mockito.any()); + + Mockito.doReturn(Constants.SyncStrategy.RELATED_ENTITY_LOCATION) + .when(mockLocationHierarchyEndpointHelper) + .getSyncStrategyByAppId(Mockito.any()); + + mockJwtUtils + .when(() -> JwtUtils.getUserRolesFromJWT(any(DecodedJWT.class))) + .thenReturn(userRoles); + + Bundle resultBundle = + mockLocationHierarchyEndpointHelper.handleNonIdentifierRequest( + request, mockPractitionerDetailsEndpointHelper, mockDecodedJWT); + + Assert.assertEquals(1, resultBundle.getTotal()); + Assert.assertEquals(1, resultBundle.getEntry().size()); + mockJwtUtils.close(); + } + + @Test + public void testFilterLocationsByLastUpdatedBasic() { + List locations = createTestLocationList(5, true, true); + String lastUpdated = OffsetDateTime.now().minusDays(2).toString(); + List filteredLocations = + locationHierarchyEndpointHelper.postFetchFilters( + locations, null, false, lastUpdated); + Assert.assertEquals(filteredLocations.size(), 5); + filteredLocations.forEach( + location -> + Assert.assertTrue( + location.getMeta() + .getLastUpdated() + .toInstant() + .isAfter(OffsetDateTime.parse(lastUpdated).toInstant()))); + } + + @Test + public void testFilterLocationsWithNoLastUpdatedFilter() { + List locations = createTestLocationList(5, true, true); + List filteredLocations = + locationHierarchyEndpointHelper.postFetchFilters(locations, null, false, ""); + Assert.assertEquals(locations.size(), filteredLocations.size()); + } + + @Test + public void testFilterLocationsByLastUpdatedAllFilteredOut() { + List locations = createTestLocationList(5, true, true); + String lastUpdated = OffsetDateTime.now().plusDays(1).toString(); + + List filteredLocations = + locationHierarchyEndpointHelper.postFetchFilters( + locations, null, false, lastUpdated); + + Assert.assertEquals(0, filteredLocations.size()); + } + private Bundle getLocationBundle() { Bundle bundleLocation = new Bundle(); bundleLocation.setId("Location/1234"); @@ -332,7 +620,8 @@ private Bundle getLocationBundle() { return bundleLocation; } - private List createLocationList(int numLocations, boolean setAdminLevel) { + public static List createTestLocationList( + int numLocations, boolean setAdminLevel, boolean setLastUpdated) { List locations = new ArrayList<>(); for (int i = 0; i < numLocations; i++) { Location location = new Location(); @@ -347,7 +636,20 @@ private List createLocationList(int numLocations, boolean setAdminLeve type.addCoding(coding); location.addType(type); } + if (setLastUpdated) { + Meta meta = new Meta(); + meta.setLastUpdated(Date.from(OffsetDateTime.now().toInstant())); + location.setMeta(meta); + } } return locations; } + + public static LocationHierarchy createLocationHierarchy(List locations) { + LocationHierarchy locationHierarchy = new LocationHierarchy(); + LocationHierarchyTree locationHierarchyTree = new LocationHierarchyTree(); + locationHierarchyTree.buildTreeFromList(locations); + locationHierarchy.setLocationHierarchyTree(locationHierarchyTree); + return locationHierarchy; + } } diff --git a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelperTest.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelperTest.java index 4da93052..6e43937c 100644 --- a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelperTest.java +++ b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelperTest.java @@ -4,20 +4,32 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.smartregister.fhir.gateway.plugins.PractitionerDetailsEndpointHelper.EMPTY_BUNDLE; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CareTeam; import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Location; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.OrganizationAffiliation; import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.PractitionerRole; +import org.hl7.fhir.r4.model.Reference; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs; import org.smartregister.model.location.LocationHierarchy; +import org.smartregister.model.practitioner.FhirPractitionerDetails; import org.smartregister.model.practitioner.PractitionerDetails; import ca.uhn.fhir.context.FhirContext; @@ -39,7 +51,6 @@ public void setUp() { @Test public void testGetPractitonerDetailsByKeycloakIdNotFound() { - Bundle bundlePractitioner = new Bundle(); Object whenObj = client.search() @@ -119,10 +130,468 @@ public void testGetAttributedLocationsWithNoParentChildrenReturnsLocationId() { Assert.assertEquals("12345", attributedLocationIds.iterator().next()); } + @Test + public void testGetSupervisorPractitionerDetailsByKeycloakIdWithInvalidIDReturnsEmptyBundle() { + Bundle bundlePractitioner = new Bundle(); + Object whenObj = + client.search() + .forResource(eq(Practitioner.class)) + .where(any(ICriterion.class)) + .usingStyle(SearchStyleEnum.POST) + .returnBundle(any()) + .execute(); + + when(whenObj).thenReturn(bundlePractitioner); + Bundle supervisorBundle = + practitionerDetailsEndpointHelper.getSupervisorPractitionerDetailsByKeycloakId( + "222"); + assertEquals(0, supervisorBundle.getEntry().size()); + } + + @Test + public void testGetSupervisorPractitionerDetailsByKeycloakIdWithValidIDReturnsBundle() { + Bundle bundlePractitioner = getPractitionerBundle(); + Practitioner practitioner = getPractitioner(); + PractitionerDetailsEndpointHelper mockPractitionerDetailsEndpointHelper = + mock(PractitionerDetailsEndpointHelper.class); + Mockito.doReturn(practitioner) + .when(mockPractitionerDetailsEndpointHelper) + .getPractitionerByIdentifier("keycloak-uuid-1234-1234"); + Mockito.doReturn(bundlePractitioner) + .when(mockPractitionerDetailsEndpointHelper) + .getAttributedPractitionerDetailsByPractitioner(practitioner); + Mockito.doCallRealMethod() + .when(mockPractitionerDetailsEndpointHelper) + .getSupervisorPractitionerDetailsByKeycloakId("keycloak-uuid-1234-1234"); + Bundle resultBundle = + mockPractitionerDetailsEndpointHelper.getSupervisorPractitionerDetailsByKeycloakId( + "keycloak-uuid-1234-1234"); + Assert.assertNotNull(resultBundle); + assertEquals(1, resultBundle.getEntry().size()); + assertEquals("Practitioner/1234", resultBundle.getEntry().get(0).getResource().getId()); + } + + @Test + public void + testGetAttributedPractitionerDetailsByPractitionerWithPractitionerReturnsAttributedPractitioner() { + Practitioner practitioner = getPractitioner(); + CareTeam careTeam = getCareTeam(); + LocationHierarchy locationHierarchy = new LocationHierarchy(); + List careTeams = new ArrayList<>(); + careTeams.add(careTeam); + List locationHierarchies = new ArrayList<>(); + locationHierarchies.add(locationHierarchy); + String id = "1234"; + Set ids = new HashSet<>(); + ids.add(id); + List stringIds = new ArrayList<>(); + stringIds.add(id); + List organizationAffiliations = new ArrayList<>(); + organizationAffiliations.add(getOrganizationAffiliation()); + PractitionerDetails practitionerDetails = getPractitionerDetails(); + PractitionerDetailsEndpointHelper mockPractitionerDetailsEndpointHelper = + mock(PractitionerDetailsEndpointHelper.class); + + Mockito.doReturn(practitionerDetails) + .when(mockPractitionerDetailsEndpointHelper) + .getPractitionerDetailsByPractitioner(practitioner); + Mockito.doReturn(ids) + .when(mockPractitionerDetailsEndpointHelper) + .getManagingOrganizationsOfCareTeamIds(careTeams); + Mockito.doReturn(organizationAffiliations) + .when(mockPractitionerDetailsEndpointHelper) + .getOrganizationAffiliationsByOrganizationIds(ids); + Mockito.doReturn(stringIds) + .when(mockPractitionerDetailsEndpointHelper) + .getLocationIdsByOrganizationAffiliations(organizationAffiliations); + + MockedStatic + mockStaticPractitionerDetailsEndpointHelper = + Mockito.mockStatic(PractitionerDetailsEndpointHelper.class); + mockStaticPractitionerDetailsEndpointHelper + .when(() -> PractitionerDetailsEndpointHelper.getLocationsHierarchy(stringIds)) + .thenReturn(locationHierarchies); + mockStaticPractitionerDetailsEndpointHelper + .when( + () -> + PractitionerDetailsEndpointHelper.getAttributedLocations( + locationHierarchies)) + .thenReturn(ids); + + Mockito.doReturn(stringIds) + .when(mockPractitionerDetailsEndpointHelper) + .getOrganizationIdsByLocationIds(ids); + + Mockito.doReturn(careTeams) + .when(mockPractitionerDetailsEndpointHelper) + .getCareTeamsByOrganizationIds(Mockito.any()); + Mockito.doCallRealMethod() + .when(mockPractitionerDetailsEndpointHelper) + .getAttributedPractitionerDetailsByPractitioner(practitioner); + + Bundle resultBundle = + mockPractitionerDetailsEndpointHelper + .getAttributedPractitionerDetailsByPractitioner(practitioner); + Assert.assertNotNull(resultBundle); + Assert.assertEquals(1, resultBundle.getTotal()); + Assert.assertEquals(1, resultBundle.getEntry().size()); + mockStaticPractitionerDetailsEndpointHelper.close(); + } + + @Test + public void testGetOrganizationIdsByLocationIdsWithEmptyLocationsReturnsEmptyArray() { + Set emptyLocationIds = new HashSet<>(); + List organizationIds = + practitionerDetailsEndpointHelper.getOrganizationIdsByLocationIds(emptyLocationIds); + Assert.assertTrue(organizationIds.isEmpty()); + } + + @Test + public void testGetOrganizationIdsByLocationIdsWithLocationIdsReturnsOrganizationIds() { + Bundle mockOrganizationAffiliationBundle = new Bundle(); + OrganizationAffiliation orgAffiliation1 = new OrganizationAffiliation(); + orgAffiliation1.setOrganization(new Reference("Organization/1234")); + + OrganizationAffiliation orgAffiliation2 = new OrganizationAffiliation(); + orgAffiliation2.setOrganization(new Reference("Organization/5678")); + + Bundle.BundleEntryComponent entry1 = new Bundle.BundleEntryComponent(); + entry1.setResource(orgAffiliation1); + Bundle.BundleEntryComponent entry2 = new Bundle.BundleEntryComponent(); + entry2.setResource(orgAffiliation2); + + mockOrganizationAffiliationBundle.addEntry(entry1); + mockOrganizationAffiliationBundle.addEntry(entry2); + + Object whenOrganizationAffiliationSearch = + client.search() + .forResource(eq(OrganizationAffiliation.class)) + .where(any(ICriterion.class)) + .usingStyle(SearchStyleEnum.POST) + .returnBundle(Bundle.class) + .execute(); + + when(whenOrganizationAffiliationSearch).thenReturn(mockOrganizationAffiliationBundle); + Set locationIds = new HashSet<>(Arrays.asList("Location1", "Location2")); + + List organizationIds = + practitionerDetailsEndpointHelper.getOrganizationIdsByLocationIds(locationIds); + + Assert.assertNotNull(organizationIds); + Assert.assertEquals(2, organizationIds.size()); + Assert.assertTrue(organizationIds.contains("1234")); + Assert.assertTrue(organizationIds.contains("5678")); + } + + @Test + public void testGetCareTeamsByOrganizationIdsWithOrganizationIdsReturnsCorrectCareTeams() { + List organizationIds = Arrays.asList("1", "2", "3"); + CareTeam careTeam1 = new CareTeam(); + careTeam1.setId("CareTeam/1"); + careTeam1.setManagingOrganization(Arrays.asList(new Reference("Organization/1"))); + CareTeam careTeam2 = new CareTeam(); + careTeam2.setId("CareTeam/2"); + careTeam2.setManagingOrganization(Arrays.asList(new Reference("Organization/2"))); + CareTeam careTeam3 = new CareTeam(); + careTeam3.setId("CareTeam/3"); + careTeam3.setManagingOrganization(Arrays.asList(new Reference("Organization/3"))); + Bundle bundle = new Bundle(); + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(careTeam1)); + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(careTeam2)); + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(careTeam3)); + + Object whenCareTeamSearch = + client.search() + .forResource(CareTeam.class) + .where(any(ICriterion.class)) + .usingStyle(SearchStyleEnum.POST) + .returnBundle(Bundle.class) + .execute(); + + when(whenCareTeamSearch).thenReturn(bundle); + List result = + practitionerDetailsEndpointHelper.getCareTeamsByOrganizationIds(organizationIds); + Assert.assertEquals(3, result.size()); + Assert.assertTrue(result.stream().anyMatch(ct -> ct.getId().equals("CareTeam/1"))); + Assert.assertTrue(result.stream().anyMatch(ct -> ct.getId().equals("CareTeam/2"))); + Assert.assertTrue(result.stream().anyMatch(ct -> ct.getId().equals("CareTeam/3"))); + } + + @Test + public void testGetOrganizationsByIdWithEmptyOrganizationIdsReturnsEmptyBundle() { + Set organizationIds = Collections.emptySet(); + Bundle result = practitionerDetailsEndpointHelper.getOrganizationsById(organizationIds); + Assert.assertSame(EMPTY_BUNDLE, result); + } + + @Test + public void testGetOrganizationsByIdWithValidOrganizationIdsReturnsOrganizations() { + Set organizationIds = new HashSet<>(Arrays.asList("1", "2")); + + Organization org1 = new Organization(); + org1.setId("Organization/1"); + + Organization org2 = new Organization(); + org2.setId("Organization/2"); + + Bundle bundle = new Bundle(); + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(org1)); + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(org2)); + + Object whenSearch = + client.search() + .forResource(Organization.class) + .where(any(ICriterion.class)) + .usingStyle(SearchStyleEnum.POST) + .returnBundle(Bundle.class) + .execute(); + + when(whenSearch).thenReturn(bundle); + Bundle result = practitionerDetailsEndpointHelper.getOrganizationsById(organizationIds); + Assert.assertNotNull(result); + Assert.assertEquals(2, result.getEntry().size()); + Assert.assertEquals("Organization/1", result.getEntry().get(0).getResource().getId()); + Assert.assertEquals("Organization/2", result.getEntry().get(1).getResource().getId()); + } + + @Test + public void testGetLocationsByIdsWithNullLocationIdsReturnsEmptyResult() { + List locationIds = null; + List result = practitionerDetailsEndpointHelper.getLocationsByIds(locationIds); + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testGetLocationsByIdsWithEmptyLocationIdsReturnsEmptyResult() { + List locationIds = new ArrayList<>(); + List result = practitionerDetailsEndpointHelper.getLocationsByIds(locationIds); + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testGetLocationsByIdsWithValidLocationIdsReturnsLocations() { + List locationIds = Arrays.asList("1", "2"); + Location location1 = new Location(); + location1.setId("Location/1"); + Location location2 = new Location(); + location2.setId("Location/2"); + Bundle bundle = new Bundle(); + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(location1)); + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(location2)); + + Object whenSearch = + client.search() + .forResource(Location.class) + .where(any(ICriterion.class)) + .usingStyle(SearchStyleEnum.POST) + .returnBundle(Bundle.class) + .execute(); + + when(whenSearch).thenReturn(bundle); + List result = practitionerDetailsEndpointHelper.getLocationsByIds(locationIds); + Assert.assertNotNull(result); + Assert.assertEquals(2, result.size()); + Assert.assertEquals("Location/1", result.get(0).getId()); + Assert.assertEquals("Location/2", result.get(1).getId()); + } + + @Test + public void + testGetOrganizationAffiliationsByOrganizationIdsWithNullOrganizationIdsReturnsEmptyResult() { + Set organizationIds = null; + List result = + practitionerDetailsEndpointHelper.getOrganizationAffiliationsByOrganizationIds( + organizationIds); + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void + testGetOrganizationAffiliationsByOrganizationIdsWithEmptyOrganizationIdsReturnsEmptyResult() { + Set organizationIds = Collections.emptySet(); + List result = + practitionerDetailsEndpointHelper.getOrganizationAffiliationsByOrganizationIds( + organizationIds); + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void + testGetOrganizationAffiliationsByOrganizationIdsWithValidOrganizationIdsReturnsOrganizationAffiliations() { + Set organizationIds = new HashSet<>(Arrays.asList("1", "2")); + OrganizationAffiliation affiliation1 = new OrganizationAffiliation(); + affiliation1.setId("OrganizationAffiliation/1"); + OrganizationAffiliation affiliation2 = new OrganizationAffiliation(); + affiliation2.setId("OrganizationAffiliation/2"); + + Bundle bundle = new Bundle(); + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(affiliation1)); + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(affiliation2)); + + Object whenSearch = + client.search() + .forResource(OrganizationAffiliation.class) + .where(any(ICriterion.class)) + .usingStyle(SearchStyleEnum.POST) + .returnBundle(Bundle.class) + .execute(); + + when(whenSearch).thenReturn(bundle); + List result = + practitionerDetailsEndpointHelper.getOrganizationAffiliationsByOrganizationIds( + organizationIds); + Assert.assertNotNull(result); + Assert.assertEquals(2, result.size()); + Assert.assertEquals("OrganizationAffiliation/1", result.get(0).getId()); + Assert.assertEquals("OrganizationAffiliation/2", result.get(1).getId()); + } + + @Test + public void + testGetOrganizationAffiliationsByOrganizationIdsBundleWithEmptyOrganizationIdsReturnsEmptyBundle() { + Set organizationIds = Collections.emptySet(); + Bundle result = + practitionerDetailsEndpointHelper + .getOrganizationAffiliationsByOrganizationIdsBundle(organizationIds); + Assert.assertSame(EMPTY_BUNDLE, result); + } + + @Test + public void + testGetOrganizationAffiliationsByOrganizationIdsBundleWithValidOrganizationIdsReturnsOrganizationAffiliations() { + Set organizationIds = new HashSet<>(Arrays.asList("1", "2")); + OrganizationAffiliation affiliation1 = new OrganizationAffiliation(); + affiliation1.setId("OrganizationAffiliation/1"); + + Bundle bundle = new Bundle(); + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(affiliation1)); + Object whenSearch = + client.search() + .forResource(OrganizationAffiliation.class) + .where(any(ICriterion.class)) + .usingStyle(SearchStyleEnum.POST) + .returnBundle(Bundle.class) + .execute(); + + when(whenSearch).thenReturn(bundle); + Bundle result = + practitionerDetailsEndpointHelper + .getOrganizationAffiliationsByOrganizationIdsBundle(organizationIds); + Assert.assertNotNull(result); + Assert.assertEquals(1, result.getEntry().size()); + Assert.assertEquals( + "OrganizationAffiliation/1", result.getEntry().get(0).getResource().getId()); + } + + @Test + public void + testGetLocationIdsByOrganizationAffiliationsWithEmptyAffiliationsReturnsEmptyResult() { + List affiliations = Collections.emptyList(); + List result = + practitionerDetailsEndpointHelper.getLocationIdsByOrganizationAffiliations( + affiliations); + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void + testGetLocationIdsByOrganizationAffiliationsWithValidAffiliationsReturnsLocationIds() { + OrganizationAffiliation affiliation1 = new OrganizationAffiliation(); + affiliation1.addLocation(new Reference("Location/1")); + OrganizationAffiliation affiliation2 = new OrganizationAffiliation(); + affiliation2.addLocation(new Reference("Location/2")); + List affiliations = Arrays.asList(affiliation1, affiliation2); + List result = + practitionerDetailsEndpointHelper.getLocationIdsByOrganizationAffiliations( + affiliations); + + Assert.assertNotNull(result); + Assert.assertEquals(2, result.size()); + Assert.assertEquals("1", result.get(0)); + Assert.assertEquals("2", result.get(1)); + } + + @Test + public void testGetPractitionerLocationIdsByByKeycloakIdCoreReturnsLocationIds() { + String practitionerId = "keycloak-uuid-1234-1234"; + Bundle careTeamBundle = getPractitionerBundle(); + List careTeamList = new ArrayList<>(); + careTeamList.add(getCareTeam()); + Set careTeamManagingOrganizationIds = new HashSet<>(); + careTeamManagingOrganizationIds.add("Organization/1234"); + + List practitionerRoleList = getPractitionerRoleList(); + Set practitionerOrganizationIds = new HashSet<>(); + practitionerOrganizationIds.add("Organization/5678"); + + Set combinedOrganizationIds = new HashSet<>(); + combinedOrganizationIds.addAll(careTeamManagingOrganizationIds); + combinedOrganizationIds.addAll(practitionerOrganizationIds); + + Bundle organizationAffiliationsBundle = getOrganizationAffiliationsBundle(); + List organizationAffiliations = new ArrayList<>(); + + organizationAffiliations.add(getOrganizationAffiliation()); + + List locationIds = new ArrayList<>(); + locationIds.add("Location/1234"); + + PractitionerDetailsEndpointHelper mockPractitionerDetailsEndpointHelper = + mock(PractitionerDetailsEndpointHelper.class); + + Mockito.doReturn(careTeamBundle) + .when(mockPractitionerDetailsEndpointHelper) + .getCareTeams(practitionerId); + Mockito.doReturn(careTeamList) + .when(mockPractitionerDetailsEndpointHelper) + .mapBundleToCareTeams(careTeamBundle); + Mockito.doReturn(careTeamManagingOrganizationIds) + .when(mockPractitionerDetailsEndpointHelper) + .getManagingOrganizationsOfCareTeamIds(careTeamList); + Mockito.doReturn(practitionerRoleList) + .when(mockPractitionerDetailsEndpointHelper) + .getPractitionerRolesByPractitionerId(practitionerId); + Mockito.doReturn(practitionerOrganizationIds) + .when(mockPractitionerDetailsEndpointHelper) + .getOrganizationIdsByPractitionerRoles(practitionerRoleList); + Mockito.doReturn(organizationAffiliationsBundle) + .when(mockPractitionerDetailsEndpointHelper) + .getOrganizationAffiliationsByOrganizationIdsBundle(combinedOrganizationIds); + Mockito.doReturn(organizationAffiliations) + .when(mockPractitionerDetailsEndpointHelper) + .mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); + Mockito.doReturn(locationIds) + .when(mockPractitionerDetailsEndpointHelper) + .getLocationIdsByOrganizationAffiliations(organizationAffiliations); + + Mockito.doCallRealMethod() + .when(mockPractitionerDetailsEndpointHelper) + .getPractitionerLocationIdsByByKeycloakIdCore(practitionerId); + List resultLocationIds = + mockPractitionerDetailsEndpointHelper.getPractitionerLocationIdsByByKeycloakIdCore( + practitionerId); + + Assert.assertNotNull(resultLocationIds); + Assert.assertEquals(1, resultLocationIds.size()); + Assert.assertEquals("Location/1234", resultLocationIds.get(0)); + } + private Bundle getPractitionerBundle() { Bundle bundlePractitioner = new Bundle(); bundlePractitioner.setId("Practitioner/1234"); Bundle.BundleEntryComponent bundleEntryComponent = new Bundle.BundleEntryComponent(); + Practitioner practitioner = getPractitioner(); + bundleEntryComponent.setResource(practitioner); + bundlePractitioner.addEntry(bundleEntryComponent); + return bundlePractitioner; + } + + private Practitioner getPractitioner() { Practitioner practitioner = new Practitioner(); practitioner.setId("Practitioner/1234"); Identifier identifier = new Identifier(); @@ -131,8 +600,69 @@ private Bundle getPractitionerBundle() { List identifiers = new ArrayList(); identifiers.add(identifier); practitioner.setIdentifier(identifiers); - bundleEntryComponent.setResource(practitioner); - bundlePractitioner.addEntry(bundleEntryComponent); - return bundlePractitioner; + return practitioner; + } + + private PractitionerDetails getPractitionerDetails() { + PractitionerDetails practitionerDetails = new PractitionerDetails(); + practitionerDetails.setId("PractitionerDetails/1234"); + FhirPractitionerDetails fhirPractitionerDetails = getFhirPractitionerDetails(); + practitionerDetails.setFhirPractitionerDetails(fhirPractitionerDetails); + Identifier identifier = new Identifier(); + identifier.setSystem("Secondary"); + identifier.setValue("keycloak-uuid-1234-1234"); + List identifiers = new ArrayList<>(); + identifiers.add(identifier); + practitionerDetails.setIdentifier(identifiers); + return practitionerDetails; + } + + private FhirPractitionerDetails getFhirPractitionerDetails() { + FhirPractitionerDetails fhirPractitionerDetails = new FhirPractitionerDetails(); + fhirPractitionerDetails.setId("FhirPractitionerDetails/1234"); + CareTeam careTeam = getCareTeam(); + List careTeams = Collections.singletonList(careTeam); + fhirPractitionerDetails.setCareTeams(careTeams); + return fhirPractitionerDetails; + } + + private OrganizationAffiliation getOrganizationAffiliation() { + OrganizationAffiliation organizationAffiliation = new OrganizationAffiliation(); + organizationAffiliation.setId("OrganizationAffiliation/1234"); + return organizationAffiliation; + } + + private CareTeam getCareTeam() { + CareTeam careTeam = new CareTeam(); + careTeam.setId("CareTeam/1234"); + CareTeam.CareTeamParticipantComponent participant = + new CareTeam.CareTeamParticipantComponent(); + participant.setMember(new Reference("Practitioner/1234")); + careTeam.addParticipant(participant); + return careTeam; + } + + private Bundle getOrganizationAffiliationsBundle() { + Bundle bundle = new Bundle(); + bundle.setId("OrganizationAffiliationsBundle/1234"); + Bundle.BundleEntryComponent bundleEntryComponent = new Bundle.BundleEntryComponent(); + OrganizationAffiliation organizationAffiliation = getOrganizationAffiliation(); + bundleEntryComponent.setResource(organizationAffiliation); + bundle.addEntry(bundleEntryComponent); + return bundle; + } + + private List getPractitionerRoleList() { + PractitionerRole practitionerRole = new PractitionerRole(); + practitionerRole.setId("PractitionerRole/1234"); + Reference organizationRef = new Reference(); + organizationRef.setReference("Organization/1234"); + practitionerRole.setOrganization(organizationRef); + Reference practitionerRef = new Reference(); + practitionerRef.setReference("Practitioner/1234"); + practitionerRole.setPractitioner(practitionerRef); + List practitionerRoles = new ArrayList<>(); + practitionerRoles.add(practitionerRole); + return practitionerRoles; } } diff --git a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImpTest.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImpTest.java new file mode 100644 index 00000000..2f50ec7f --- /dev/null +++ b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImpTest.java @@ -0,0 +1,155 @@ +package org.smartregister.fhir.gateway.plugins; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Resource; +import org.junit.Before; +import org.junit.Test; + +import com.google.fhir.gateway.interfaces.RequestDetailsReader; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.api.RequestTypeEnum; + +public class ResourceFinderImpTest { + + private FhirContext fhirContextMock; + private IParser jsonParserMock; + private RequestDetailsReader requestDetailsReaderMock; + private ResourceFinderImp resourceFinder; + + @Before + public void setUp() throws Exception { + Field instanceField = ResourceFinderImp.class.getDeclaredField("instance"); + instanceField.setAccessible(true); + instanceField.set(null, null); + + fhirContextMock = mock(FhirContext.class); + jsonParserMock = mock(IParser.class); + requestDetailsReaderMock = mock(RequestDetailsReader.class); + resourceFinder = ResourceFinderImp.getInstance(fhirContextMock); + when(fhirContextMock.newJsonParser()).thenReturn(jsonParserMock); + } + + @Test + public void testCreateResourceFromRequestWithValidJson() { + String jsonString = "{\"resourceType\":\"Bundle\"}"; + byte[] requestContentBytes = jsonString.getBytes(StandardCharsets.UTF_8); + + when(requestDetailsReaderMock.loadRequestContents()).thenReturn(requestContentBytes); + when(requestDetailsReaderMock.getCharset()).thenReturn(StandardCharsets.UTF_8); + Bundle expectedBundle = new Bundle(); + when(jsonParserMock.parseResource(jsonString)).thenReturn(expectedBundle); + + IBaseResource resource = resourceFinder.createResourceFromRequest(requestDetailsReaderMock); + + assertNotNull(resource); + assertTrue(resource instanceof Bundle); + assertEquals(expectedBundle, resource); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateResourceFromRequestWithInvalidJson() { + String invalidJsonString = "{invalid json}"; + byte[] requestContentBytes = invalidJsonString.getBytes(StandardCharsets.UTF_8); + when(requestDetailsReaderMock.loadRequestContents()).thenReturn(requestContentBytes); + when(requestDetailsReaderMock.getCharset()).thenReturn(StandardCharsets.UTF_8); + when(jsonParserMock.parseResource(invalidJsonString)) + .thenThrow(new IllegalArgumentException("Invalid JSON")); + resourceFinder.createResourceFromRequest(requestDetailsReaderMock); + } + + @Test + public void testFindResourcesInBundleWithValidTransactionBundle() { + String bundleJson = + "{\"resourceType\": \"Bundle\", \"type\": \"transaction\", \"entry\": []}"; + when(requestDetailsReaderMock.loadRequestContents()) + .thenReturn(bundleJson.getBytes(StandardCharsets.UTF_8)); + when(requestDetailsReaderMock.getCharset()).thenReturn(StandardCharsets.UTF_8); + + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.TRANSACTION); + + when(jsonParserMock.parseResource(bundleJson)).thenReturn(bundle); + List result = + resourceFinder.findResourcesInBundle(requestDetailsReaderMock); + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test(expected = RuntimeException.class) + public void testFindResourcesInBundleWithNonTransactionBundle() { + String bundleJson = "{\"resourceType\": \"Bundle\", \"type\": \"document\", \"entry\": []}"; + when(requestDetailsReaderMock.loadRequestContents()) + .thenReturn(bundleJson.getBytes(StandardCharsets.UTF_8)); + when(requestDetailsReaderMock.getCharset()).thenReturn(StandardCharsets.UTF_8); + + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.DOCUMENT); + when(jsonParserMock.parseResource(bundleJson)).thenReturn(bundle); + resourceFinder.findResourcesInBundle(requestDetailsReaderMock); + } + + @Test(expected = RuntimeException.class) + public void testFindResourcesInBundleWithNonBundleResource() { + String nonBundleJson = "{\"resourceType\": \"Patient\", \"id\": \"123\"}"; + when(requestDetailsReaderMock.loadRequestContents()) + .thenReturn(nonBundleJson.getBytes(StandardCharsets.UTF_8)); + when(requestDetailsReaderMock.getCharset()).thenReturn(StandardCharsets.UTF_8); + + Resource nonBundleResource = mock(Resource.class); + when(jsonParserMock.parseResource(nonBundleJson)).thenReturn(nonBundleResource); + resourceFinder.findResourcesInBundle(requestDetailsReaderMock); + } + + @Test(expected = RuntimeException.class) + public void testFindResourcesInBundleWithBundleMissingResourceInEntry() { + String bundleJson = + "{\"resourceType\": \"Bundle\", \"type\": \"transaction\", \"entry\":" + + " [{\"request\": {\"method\": \"POST\"}}]}"; + when(requestDetailsReaderMock.loadRequestContents()) + .thenReturn(bundleJson.getBytes(StandardCharsets.UTF_8)); + when(requestDetailsReaderMock.getCharset()).thenReturn(StandardCharsets.UTF_8); + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.TRANSACTION); + + Bundle.BundleEntryComponent entry = new Bundle.BundleEntryComponent(); + entry.getRequest().setMethod(Bundle.HTTPVerb.POST); + bundle.addEntry(entry); + when(jsonParserMock.parseResource(bundleJson)).thenReturn(bundle); + resourceFinder.findResourcesInBundle(requestDetailsReaderMock); + } + + @Test + public void testFindResourcesInBundleWithValidEntries() { + String bundleJson = + "{\"resourceType\": \"Bundle\", \"type\": \"transaction\", \"entry\":" + + " [{\"request\": {\"method\": \"POST\"}, \"resource\": {\"resourceType\":" + + " \"Patient\", \"id\": \"123\"}}]}"; + when(requestDetailsReaderMock.loadRequestContents()) + .thenReturn(bundleJson.getBytes(StandardCharsets.UTF_8)); + when(requestDetailsReaderMock.getCharset()).thenReturn(StandardCharsets.UTF_8); + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.TRANSACTION); + Bundle.BundleEntryComponent entry = new Bundle.BundleEntryComponent(); + entry.getRequest().setMethod(Bundle.HTTPVerb.POST); + Resource resource = mock(Resource.class); + entry.setResource(resource); + bundle.addEntry(entry); + when(jsonParserMock.parseResource(bundleJson)).thenReturn(bundle); + List result = + resourceFinder.findResourcesInBundle(requestDetailsReaderMock); + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals(RequestTypeEnum.POST, result.get(0).getRequestType()); + assertEquals(resource, result.get(0).getResource()); + } +} diff --git a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/RestUtilTest.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/RestUtilTest.java index b88f97dd..fef62f98 100644 --- a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/RestUtilTest.java +++ b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/RestUtilTest.java @@ -5,11 +5,34 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import javax.servlet.http.HttpServletResponse; - import org.junit.Test; +import org.mockito.Mockito; + +import com.google.fhir.gateway.TokenVerifier; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; public class RestUtilTest { + + @Test(expected = RuntimeException.class) + public void testCheckAuthenticationThrowsExceptionWhenNoAuthHeader() { + HttpServletRequest requestMock = mock(HttpServletRequest.class); + TokenVerifier tokenVerifierMock = mock(TokenVerifier.class); + Mockito.when(requestMock.getHeader(Constants.AUTHORIZATION)).thenReturn(null); + RestUtils.checkAuthentication(requestMock, tokenVerifierMock); + } + + @Test + public void testCheckAuthenticationCallsTokenVerifierWhenAuthHeaderExists() { + HttpServletRequest requestMock = mock(HttpServletRequest.class); + TokenVerifier tokenVerifierMock = mock(TokenVerifier.class); + String authHeader = "Bearer someToken"; + Mockito.when(requestMock.getHeader(Constants.AUTHORIZATION)).thenReturn(authHeader); + RestUtils.checkAuthentication(requestMock, tokenVerifierMock); + verify(tokenVerifierMock).decodeAndVerifyBearerToken(authHeader); + } + @Test public void testAddCorsHeadersSetsCorsHeaders() { HttpServletResponse responseMock = mock(HttpServletResponse.class); diff --git a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java index 244c337d..c3c7ef60 100755 --- a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java +++ b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java @@ -83,9 +83,10 @@ public void preProcessShouldAddLocationIdFiltersWhenUserIsAssignedToLocationsOnl Assert.assertFalse(requestDetails.getCompleteUrl().contains(locationId)); Assert.assertFalse(requestDetails.getRequestPath().contains(locationId)); } + assert mutatedRequest != null; Assert.assertTrue( mutatedRequest - .getQueryParams() + .getAdditionalQueryParams() .get(Constants.TAG_SEARCH_PARAM) .get(0) .contains( @@ -95,7 +96,8 @@ public void preProcessShouldAddLocationIdFiltersWhenUserIsAssignedToLocationsOnl + Constants.DEFAULT_LOCATION_TAG_URL + Constants.CODE_URL_VALUE_SEPARATOR))); - for (String param : mutatedRequest.getQueryParams().get(Constants.TAG_SEARCH_PARAM)) { + for (String param : + mutatedRequest.getAdditionalQueryParams().get(Constants.TAG_SEARCH_PARAM)) { Assert.assertFalse(param.contains(Constants.DEFAULT_CARE_TEAM_TAG_URL)); Assert.assertFalse(param.contains(Constants.DEFAULT_ORGANISATION_TAG_URL)); } @@ -180,7 +182,7 @@ public void preProcessWhenNotOneClientRoleIsAddedShouldThrowError() throws IOExc } Assert.assertTrue( mutatedRequest - .getQueryParams() + .getAdditionalQueryParams() .get(Constants.TAG_SEARCH_PARAM) .get(0) .contains( @@ -190,7 +192,8 @@ public void preProcessWhenNotOneClientRoleIsAddedShouldThrowError() throws IOExc + Constants.DEFAULT_RELATED_ENTITY_TAG_URL + Constants.CODE_URL_VALUE_SEPARATOR))); - for (String param : mutatedRequest.getQueryParams().get(Constants.TAG_SEARCH_PARAM)) { + for (String param : + mutatedRequest.getAdditionalQueryParams().get(Constants.TAG_SEARCH_PARAM)) { Assert.assertFalse(param.contains(Constants.DEFAULT_CARE_TEAM_TAG_URL)); Assert.assertFalse(param.contains(Constants.DEFAULT_ORGANISATION_TAG_URL)); } @@ -258,12 +261,15 @@ public void preProcessWhenNotOneClientRoleIsAddedShouldThrowError() throws IOExc Assert.assertEquals( expected.substring(0, expected.length() - 1), - mutatedRequest.getQueryParams().get(Constants.TAG_SEARCH_PARAM).get(0)); + mutatedRequest + .getAdditionalQueryParams() + .get(Constants.TAG_SEARCH_PARAM) + .get(0)); Collections.reverse(relatedEntityLocationIds); Assert.assertFalse( mutatedRequest - .getQueryParams() + .getAdditionalQueryParams() .get(Constants.TAG_SEARCH_PARAM) .get(0) .contains( @@ -371,7 +377,7 @@ public void preProcessShouldAddCareTeamIdFiltersWhenUserIsAssignedToCareTeamsOnl Assert.assertTrue( mutatedRequest - .getQueryParams() + .getAdditionalQueryParams() .get(Constants.TAG_SEARCH_PARAM) .get(0) .contains( @@ -381,7 +387,8 @@ public void preProcessShouldAddCareTeamIdFiltersWhenUserIsAssignedToCareTeamsOnl + Constants.DEFAULT_CARE_TEAM_TAG_URL + Constants.CODE_URL_VALUE_SEPARATOR))); - for (String param : mutatedRequest.getQueryParams().get(Constants.TAG_SEARCH_PARAM)) { + for (String param : + mutatedRequest.getAdditionalQueryParams().get(Constants.TAG_SEARCH_PARAM)) { Assert.assertFalse(param.contains(Constants.DEFAULT_LOCATION_TAG_URL)); Assert.assertFalse(param.contains(Constants.DEFAULT_ORGANISATION_TAG_URL)); } @@ -410,7 +417,7 @@ public void preProcessShouldAddOrganisationIdFiltersWhenUserIsAssignedToOrganisa Assert.assertFalse(requestDetails.getRequestPath().contains(locationId)); Assert.assertTrue( mutatedRequest - .getQueryParams() + .getAdditionalQueryParams() .get(Constants.TAG_SEARCH_PARAM) .contains( Constants.DEFAULT_ORGANISATION_TAG_URL @@ -418,7 +425,8 @@ public void preProcessShouldAddOrganisationIdFiltersWhenUserIsAssignedToOrganisa + locationId)); } - for (String param : mutatedRequest.getQueryParams().get(Constants.TAG_SEARCH_PARAM)) { + for (String param : + mutatedRequest.getAdditionalQueryParams().get(Constants.TAG_SEARCH_PARAM)) { Assert.assertFalse(param.contains(Constants.DEFAULT_LOCATION_TAG_URL)); Assert.assertFalse(param.contains(Constants.DEFAULT_CARE_TEAM_TAG_URL)); } @@ -445,11 +453,11 @@ public void preProcessShouldAddFiltersWhenResourceNotInSyncFilterIgnoredResource for (String locationId : organisationIds) { Assert.assertFalse(requestDetails.getCompleteUrl().contains(locationId)); Assert.assertFalse(requestDetails.getRequestPath().contains(locationId)); - Assert.assertEquals(1, mutatedRequest.getQueryParams().size()); + Assert.assertEquals(1, mutatedRequest.getAdditionalQueryParams().size()); } Assert.assertTrue( mutatedRequest - .getQueryParams() + .getAdditionalQueryParams() .get(Constants.TAG_SEARCH_PARAM) .get(0) .contains( @@ -557,7 +565,7 @@ public void preProcessShouldSkipAddingFiltersWhenResourceInSyncFilterIgnoredReso testInstance.getRequestMutation(new TestRequestDetailsToReader(requestDetails)); List searchParamArrays = - mutatedRequest.getQueryParams().get(Constants.TAG_SEARCH_PARAM); + mutatedRequest.getAdditionalQueryParams().get(Constants.TAG_SEARCH_PARAM); Assert.assertNotNull(searchParamArrays); Assert.assertTrue( @@ -1013,7 +1021,7 @@ public void preProcessWhenRequestIsAnOperationRequestShouldAddFilters() { } Assert.assertTrue( mutatedRequest - .getQueryParams() + .getAdditionalQueryParams() .get(Constants.TAG_SEARCH_PARAM) .get(0) .contains( @@ -1023,7 +1031,8 @@ public void preProcessWhenRequestIsAnOperationRequestShouldAddFilters() { + Constants.DEFAULT_LOCATION_TAG_URL + Constants.CODE_URL_VALUE_SEPARATOR))); - for (String param : mutatedRequest.getQueryParams().get(Constants.TAG_SEARCH_PARAM)) { + for (String param : + mutatedRequest.getAdditionalQueryParams().get(Constants.TAG_SEARCH_PARAM)) { Assert.assertFalse(param.contains(Constants.DEFAULT_CARE_TEAM_TAG_URL)); Assert.assertFalse(param.contains(Constants.DEFAULT_ORGANISATION_TAG_URL)); } diff --git a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestBundleResources.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestBundleResources.java new file mode 100644 index 00000000..f6bc00d0 --- /dev/null +++ b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestBundleResources.java @@ -0,0 +1,33 @@ +package org.smartregister.fhir.gateway.plugins; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.Test; + +import ca.uhn.fhir.rest.api.RequestTypeEnum; + +public class TestBundleResources { + + @Test + public void testConstructorAndGetters() { + RequestTypeEnum requestType = RequestTypeEnum.POST; + IBaseResource resource = mock(IBaseResource.class); + BundleResources bundleResources = new BundleResources(requestType, resource); + + assertEquals(requestType, bundleResources.getRequestType()); + assertEquals(resource, bundleResources.getResource()); + } + + @Test + public void testSetters() { + BundleResources bundleResources = new BundleResources(null, null); + RequestTypeEnum requestType = RequestTypeEnum.GET; + IBaseResource resource = mock(IBaseResource.class); + bundleResources.setRequestType(requestType); + bundleResources.setResource(resource); + assertEquals(requestType, bundleResources.getRequestType()); + assertEquals(resource, bundleResources.getResource()); + } +} diff --git a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/UtilsTest.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/UtilsTest.java new file mode 100644 index 00000000..16f23680 --- /dev/null +++ b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/UtilsTest.java @@ -0,0 +1,182 @@ +package org.smartregister.fhir.gateway.plugins; + +import java.util.Arrays; +import java.util.Base64; + +import org.hl7.fhir.r4.model.Base64BinaryType; +import org.hl7.fhir.r4.model.Binary; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Composition; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Reference; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.client.api.IGenericClient; + +public class UtilsTest { + + private FhirContext fhirContextMock; + + @Before + public void setUp() { + fhirContextMock = Mockito.mock(FhirContext.class); + IGenericClient clientMock = Mockito.mock(IGenericClient.class); + + Mockito.when(fhirContextMock.newRestfulGenericClient(Mockito.anyString())) + .thenReturn(clientMock); + } + + @Test + public void testCreateEmptyBundle() { + String requestURL = "http://example.com/fhir/Bundle"; + Bundle result = Utils.createEmptyBundle(requestURL); + Assert.assertNotNull(result); + Assert.assertNotNull(result.getId()); + Assert.assertEquals(0, result.getTotal()); + Assert.assertEquals(Bundle.BundleType.SEARCHSET, result.getType()); + Assert.assertEquals(1, result.getLink().size()); + Assert.assertEquals(Bundle.LINK_SELF, result.getLink().get(0).getRelation()); + Assert.assertEquals(requestURL, result.getLink().get(0).getUrl()); + } + + @Test + public void testGetBinaryResourceReferenceWithNullComposition() { + String result = Utils.getBinaryResourceReference(null); + + Assert.assertEquals("", result); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testGetBinaryResourceReferenceWithEmptySection() { + Composition composition = new Composition(); + String result = Utils.getBinaryResourceReference(composition); + Assert.assertEquals("", result); + } + + @Test(expected = ArrayIndexOutOfBoundsException.class) + public void testGetBinaryResourceReferenceWithNoMatchingSection() { + Composition composition = new Composition(); + Composition.SectionComponent sectionComponent = new Composition.SectionComponent(); + sectionComponent.setFocus( + new Reference().setIdentifier(new Identifier().setValue("otherValue"))); + composition.setSection(Arrays.asList(sectionComponent)); + Utils.getBinaryResourceReference(composition); + } + + @Test + public void testGetBinaryResourceReferenceWithMatchingSection() { + Composition composition = new Composition(); + Composition.SectionComponent sectionComponent = new Composition.SectionComponent(); + Identifier identifier = new Identifier(); + identifier.setValue(Constants.AppConfigJsonKey.APPLICATION); + Reference reference = new Reference(); + reference.setIdentifier(identifier); + reference.setReference("Binary/1234"); + sectionComponent.setFocus(reference); + composition.setSection(Arrays.asList(sectionComponent)); + String result = Utils.getBinaryResourceReference(composition); + Assert.assertEquals("Binary/1234", result); + } + + @Test + public void testGetBinaryResourceReferenceWithNullFocus() { + Composition composition = new Composition(); + Composition.SectionComponent sectionComponent = new Composition.SectionComponent(); + sectionComponent.setFocus(null); + sectionComponent.setFocus( + new Reference() + .setIdentifier( + new Identifier().setValue(Constants.AppConfigJsonKey.APPLICATION))); + composition.setSection(Arrays.asList(sectionComponent)); + String result = Utils.getBinaryResourceReference(composition); + Assert.assertNull(result); + } + + @Test(expected = ArrayIndexOutOfBoundsException.class) + public void testGetBinaryResourceReferenceWithNoIdentifier() { + Composition composition = new Composition(); + Composition.SectionComponent sectionComponent = new Composition.SectionComponent(); + Reference reference = new Reference(); + reference.setReference("Binary/5678"); + sectionComponent.setFocus(reference); + sectionComponent.setFocus(new Reference()); + composition.setSection(Arrays.asList(sectionComponent)); + Utils.getBinaryResourceReference(composition); + } + + @Test + public void testGetBinaryResourceReferenceWithNoFocusReference() { + Composition composition = new Composition(); + Composition.SectionComponent sectionComponent = new Composition.SectionComponent(); + sectionComponent.setFocus( + new Reference() + .setIdentifier( + new Identifier().setValue(Constants.AppConfigJsonKey.APPLICATION))); + composition.setSection(Arrays.asList(sectionComponent)); + String result = Utils.getBinaryResourceReference(composition); + Assert.assertNull(result); + } + + @Test + public void testFindSyncStrategyWithNullBinary() { + String result = Utils.findSyncStrategy(null); + Assert.assertEquals(org.smartregister.utils.Constants.EMPTY_STRING, result); + } + + @Test + public void testFindSyncStrategyWithEmptySyncStrategyArray() { + Binary binary = new Binary(); + JsonObject jsonObject = new JsonObject(); + jsonObject.add(Constants.AppConfigJsonKey.SYNC_STRATEGY, new JsonArray()); + String json = jsonObject.toString(); + String encodedJson = Base64.getEncoder().encodeToString(json.getBytes()); + binary.setDataElement(new Base64BinaryType(encodedJson)); + String result = Utils.findSyncStrategy(binary); + Assert.assertEquals(org.smartregister.utils.Constants.EMPTY_STRING, result); + } + + @Test + public void testFindSyncStrategyWithValidSyncStrategy() { + Binary binary = new Binary(); + JsonObject jsonObject = new JsonObject(); + JsonArray syncStrategyArray = new JsonArray(); + syncStrategyArray.add("PUSH"); + jsonObject.add(Constants.AppConfigJsonKey.SYNC_STRATEGY, syncStrategyArray); + String json = jsonObject.toString(); + String encodedJson = Base64.getEncoder().encodeToString(json.getBytes()); + binary.setDataElement(new Base64BinaryType(encodedJson)); + + String result = Utils.findSyncStrategy(binary); + Assert.assertEquals("PUSH", result); + } + + @Test + public void testFindSyncStrategyWithMultipleSyncStrategies() { + Binary binary = new Binary(); + JsonObject jsonObject = new JsonObject(); + JsonArray syncStrategyArray = new JsonArray(); + syncStrategyArray.add("PUSH"); + syncStrategyArray.add("PULL"); + jsonObject.add(Constants.AppConfigJsonKey.SYNC_STRATEGY, syncStrategyArray); + + String json = jsonObject.toString(); + String encodedJson = Base64.getEncoder().encodeToString(json.getBytes()); + binary.setDataElement(new Base64BinaryType(encodedJson)); + + String result = Utils.findSyncStrategy(binary); + Assert.assertEquals("PUSH", result); + } + + @Test + public void testReadApplicationConfigBinaryResourceWithEmptyResourceId() { + Binary result = Utils.readApplicationConfigBinaryResource("", fhirContextMock); + Assert.assertNull(result); + } +} diff --git a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/endpoint/BaseEndpointTest.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/endpoint/BaseEndpointTest.java new file mode 100644 index 00000000..7d8aa4c3 --- /dev/null +++ b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/endpoint/BaseEndpointTest.java @@ -0,0 +1,21 @@ +package org.smartregister.fhir.gateway.plugins.endpoint; + +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Test; + +public class BaseEndpointTest { + + @Test + public void testBaseEndpointInstanceCreation() throws IOException { + + TestBaseEndpoint baseEndpoint = new TestBaseEndpoint(); + Assert.assertNotNull(baseEndpoint); + } + + static class TestBaseEndpoint extends BaseEndpoint { + + protected TestBaseEndpoint() throws IOException {} + } +} diff --git a/pom.xml b/pom.xml index d8e2ddd3..d66aab6e 100755 --- a/pom.xml +++ b/pom.xml @@ -6,12 +6,12 @@ implementations do not have to do this; they can redeclare those deps. --> com.google.fhir.gateway fhir-gateway - 0.3.2 + 0.4.0 org.smartregister opensrp-gateway-plugin - 2.0.8-alpha + 2.2.2-alpha pom @@ -20,19 +20,41 @@ - 2.30.0 + 2.43.0 + 0.4.0 + 3.3.4 + 1.13.7-SNAPSHOT - ca.uhn.hapi.fhir - hapi-fhir-client - ${hapifhir_version} + org.springframework.boot + spring-boot-starter-actuator + ${spring-boot.version} + + + io.micrometer + micrometer-registry-prometheus + ${micrometer.version} + + + spring-io-snapshot + https://repo.spring.io/snapshot + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + com.mycila license-maven-plugin @@ -110,7 +132,7 @@ - 1.15.0 + 1.17.0