diff --git a/src/main/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyController.java b/src/main/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyController.java index 0d6e91c..5eac08a 100644 --- a/src/main/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyController.java +++ b/src/main/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyController.java @@ -1,9 +1,9 @@ package gov.cms.madie.terminology.controller; import gov.cms.madie.models.measure.ManifestExpansion; +import gov.cms.madie.terminology.dto.Code; import gov.cms.madie.terminology.dto.QdmValueSet; import gov.cms.madie.terminology.dto.ValueSetsSearchCriteria; -import gov.cms.madie.terminology.exceptions.VsacUnauthorizedException; import gov.cms.madie.terminology.models.CodeSystem; import gov.cms.madie.terminology.models.UmlsUser; import gov.cms.madie.terminology.service.FhirTerminologyService; @@ -11,7 +11,6 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -20,7 +19,6 @@ import java.security.Principal; import java.util.List; -import java.util.Optional; @RestController @RequestMapping(path = "/terminology") @@ -34,15 +32,8 @@ public class VsacFhirTerminologyController { public ResponseEntity> getManifests(Principal principal) { final String username = principal.getName(); log.info("Retrieving List of available manifests, requested by HARP ID : [{}}]", username); - Optional umlsUser = vsacService.findByHarpId(username); - if (umlsUser.isPresent() && !StringUtils.isBlank(umlsUser.get().getApiKey())) { - return ResponseEntity.ok().body(fhirTerminologyService.getManifests(umlsUser.get())); - } - log.error( - "Unable to Retrieve List of available manifests, " - + "UMLS Authentication Key Not found for user : [{}}]", - username); - throw new VsacUnauthorizedException("Please login to UMLS before proceeding"); + UmlsUser umlsUser = vsacService.verifyUmlsAccess(username); + return ResponseEntity.ok().body(fhirTerminologyService.getManifests(umlsUser)); } @PutMapping("/value-sets/expansion/qdm") @@ -52,17 +43,10 @@ public ResponseEntity> getValueSetsExpansions( log.info( "User [{}] is attempting to fetch value sets expansions from VSAC FHIR Terminology Server.", username); - Optional umlsUser = vsacService.findByHarpId(username); - if (umlsUser.isPresent() && !StringUtils.isBlank(umlsUser.get().getApiKey())) { - List qdmValueSets = - fhirTerminologyService.getValueSetsExpansionsForQdm(searchCriteria, umlsUser.get()); - return ResponseEntity.ok().body(qdmValueSets); - } - log.error( - "Unable to Retrieve Value Sets Expansions, " - + "UMLS Authentication Key Not found for user : [{}}]", - username); - throw new VsacUnauthorizedException("Please login to UMLS before proceeding"); + UmlsUser umlsUser = vsacService.verifyUmlsAccess(username); + List qdmValueSets = + fhirTerminologyService.getValueSetsExpansionsForQdm(searchCriteria, umlsUser); + return ResponseEntity.ok().body(qdmValueSets); } @GetMapping(path = "/update-code-systems", produces = MediaType.APPLICATION_JSON_VALUE) @@ -73,18 +57,8 @@ public ResponseEntity> retrieveAndUpdateCodeSystems( @Value("${admin-api-key}") String apiKey, @RequestHeader("Authorization") String accessToken) { final String username = principal.getName(); - Optional umlsUser = vsacService.findByHarpId(username); - - if (umlsUser.isPresent() && !StringUtils.isBlank(umlsUser.get().getApiKey())) { - return ResponseEntity.ok() - .body(fhirTerminologyService.retrieveAllCodeSystems(umlsUser.get())); - } else { - log.error( - "Unable to Retrieve List of code systems, " - + "UMLS Authentication Key Not found for user : [{}}]", - username); - throw new VsacUnauthorizedException("Please login to UMLS before proceeding"); - } + UmlsUser umlsUser = vsacService.verifyUmlsAccess(username); + return ResponseEntity.ok().body(fhirTerminologyService.retrieveAllCodeSystems(umlsUser)); } @GetMapping(path = "/get-code-systems", produces = MediaType.APPLICATION_JSON_VALUE) @@ -93,4 +67,22 @@ public ResponseEntity> getAllCodeSystems(Principal principal) { log.info("Retrieving list of codeSystems for user: {}", username); return ResponseEntity.ok().body(fhirTerminologyService.getAllCodeSystems()); } + + @GetMapping(path = "/code", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCode( + @RequestParam() String code, + @RequestParam() String codeSystem, + @RequestParam() String version, + Principal principal) { + final String username = principal.getName(); + log.info( + "User {} is performing code retrieve for code: {}, system: {} and version: {}", + username, + code, + codeSystem, + version); + UmlsUser user = vsacService.verifyUmlsAccess(username); + return ResponseEntity.ok() + .body(fhirTerminologyService.retrieveCode(code, codeSystem, version, user.getApiKey())); + } } diff --git a/src/main/java/gov/cms/madie/terminology/dto/Code.java b/src/main/java/gov/cms/madie/terminology/dto/Code.java new file mode 100644 index 0000000..17b916c --- /dev/null +++ b/src/main/java/gov/cms/madie/terminology/dto/Code.java @@ -0,0 +1,15 @@ +package gov.cms.madie.terminology.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder(toBuilder = true) +public class Code { + private String name; + private String display; + private String version; + private String codeSystem; + private String codeSystemOid; + private CodeStatus status; +} diff --git a/src/main/java/gov/cms/madie/terminology/dto/CodeStatus.java b/src/main/java/gov/cms/madie/terminology/dto/CodeStatus.java new file mode 100644 index 0000000..cf7cfa6 --- /dev/null +++ b/src/main/java/gov/cms/madie/terminology/dto/CodeStatus.java @@ -0,0 +1,13 @@ +package gov.cms.madie.terminology.dto; + +public enum CodeStatus { + ACTIVE("active"), + INACTIVE("inactive"), + NA("Not available"); + + private final String value; + + CodeStatus(String value) { + this.value = value; + } +} diff --git a/src/main/java/gov/cms/madie/terminology/repositories/CodeSystemRepository.java b/src/main/java/gov/cms/madie/terminology/repositories/CodeSystemRepository.java index 71d9eea..03e3e57 100644 --- a/src/main/java/gov/cms/madie/terminology/repositories/CodeSystemRepository.java +++ b/src/main/java/gov/cms/madie/terminology/repositories/CodeSystemRepository.java @@ -6,4 +6,6 @@ public interface CodeSystemRepository extends MongoRepository { Optional findById(String id); + + Optional findByNameAndVersion(String name, String version); } diff --git a/src/main/java/gov/cms/madie/terminology/service/FhirTerminologyService.java b/src/main/java/gov/cms/madie/terminology/service/FhirTerminologyService.java index 9b1034a..689bf74 100644 --- a/src/main/java/gov/cms/madie/terminology/service/FhirTerminologyService.java +++ b/src/main/java/gov/cms/madie/terminology/service/FhirTerminologyService.java @@ -4,6 +4,8 @@ import ca.uhn.fhir.parser.IParser; import gov.cms.madie.models.mapping.CodeSystemEntry; import gov.cms.madie.models.measure.ManifestExpansion; +import gov.cms.madie.terminology.dto.Code; +import gov.cms.madie.terminology.dto.CodeStatus; import gov.cms.madie.terminology.dto.QdmValueSet; import gov.cms.madie.terminology.dto.ValueSetsSearchCriteria; import gov.cms.madie.terminology.models.CodeSystem; @@ -13,7 +15,9 @@ import gov.cms.madie.terminology.webclient.FhirTerminologyServiceWebClient; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.ValueSet; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @@ -32,6 +36,7 @@ public class FhirTerminologyService { private final FhirTerminologyServiceWebClient fhirTerminologyServiceWebClient; private final MappingService mappingService; private final CodeSystemRepository codeSystemRepository; + private final VsacService vsacService; @Cacheable("manifest-list") public List getManifests(UmlsUser umlsUser) { @@ -129,6 +134,34 @@ public List retrieveAllCodeSystems(UmlsUser umlsUser) { return allCodeSystems; } + public Code retrieveCode(String codeName, String codeSystemName, String version, String apiKey) { + if (StringUtils.isEmpty(codeName) + || StringUtils.isEmpty(codeSystemName) + || StringUtils.isEmpty(version)) { + return null; + } + CodeSystem codeSystem = + codeSystemRepository.findByNameAndVersion(codeSystemName, version).orElse(null); + if (codeSystem == null) { + return null; + } + String codeJson = fhirTerminologyServiceWebClient.getCodeResource(codeName, codeSystem, apiKey); + Parameters parameters = fhirContext.newJsonParser().parseResource(Parameters.class, codeJson); + Code code = + Code.builder() + .name(codeName) + .codeSystem(codeSystemName) + .version(version) + .display(parameters.getParameter("display").getValue().toString()) + .codeSystemOid(parameters.getParameter("Oid").getValue().toString()) + .build(); + // FHIR terminology API doesn't support code status yet. workaround is to get it from SVS. + // TODO: remove once it is supported by fhir terminology service + CodeStatus status = vsacService.getCodeStatus(code, apiKey); + code.setStatus(status); + return code; + } + private void recursiveRetrieveCodeSystems( UmlsUser umlsUser, Integer offset, Integer count, List allCodeSystems) { log.info("requesting page offset: {} count: {}", offset, count); diff --git a/src/main/java/gov/cms/madie/terminology/service/MappingService.java b/src/main/java/gov/cms/madie/terminology/service/MappingService.java index f0e9f00..d87bbb8 100644 --- a/src/main/java/gov/cms/madie/terminology/service/MappingService.java +++ b/src/main/java/gov/cms/madie/terminology/service/MappingService.java @@ -6,12 +6,14 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; import java.io.IOException; import java.net.URL; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; @Service @Slf4j @@ -35,4 +37,15 @@ public List getCodeSystemEntries() { } return Collections.emptyList(); } + + public CodeSystemEntry getCodeSystemEntry(String codeSystemName) { + List codeSystemEntries = getCodeSystemEntries(); + if (CollectionUtils.isEmpty(codeSystemEntries)) { + return null; + } + return codeSystemEntries.stream() + .filter(entry -> Objects.equals(entry.getName(), codeSystemName)) + .findFirst() + .orElse(null); + } } diff --git a/src/main/java/gov/cms/madie/terminology/service/VsacService.java b/src/main/java/gov/cms/madie/terminology/service/VsacService.java index 71764e4..56389fa 100644 --- a/src/main/java/gov/cms/madie/terminology/service/VsacService.java +++ b/src/main/java/gov/cms/madie/terminology/service/VsacService.java @@ -4,8 +4,11 @@ import gov.cms.madie.models.cql.terminology.CqlCode; import gov.cms.madie.models.cql.terminology.VsacCode; import gov.cms.madie.models.mapping.CodeSystemEntry; +import gov.cms.madie.terminology.dto.Code; +import gov.cms.madie.terminology.dto.CodeStatus; import gov.cms.madie.terminology.dto.QdmValueSet; import gov.cms.madie.terminology.dto.ValueSetsSearchCriteria; +import gov.cms.madie.terminology.exceptions.VsacUnauthorizedException; import gov.cms.madie.terminology.mapper.VsacToFhirValueSetMapper; import gov.cms.madie.terminology.models.UmlsUser; import gov.cms.madie.terminology.repositories.UmlsUserRepository; @@ -20,6 +23,7 @@ import java.time.Instant; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -156,6 +160,54 @@ public List validateCodes(List cqlCodes, UmlsUser umlsUser, St return cqlCodes; } + public CodeStatus getCodeStatus(Code code, String apiKey) { + CodeSystemEntry systemEntry = mappingService.getCodeSystemEntry(code.getCodeSystem()); + // do not call SVS API to get code status if the system is not in SVS API + if (systemEntry == null + || systemEntry.getOid().contains("NOT.IN.VSAC") + || CollectionUtils.isEmpty(systemEntry.getVersions())) { + return CodeStatus.NA; + } + + // get corresponding SVS version for given FHIR version + CodeSystemEntry.Version version = + systemEntry.getVersions().stream() + .filter(v -> Objects.equals(v.getFhir(), code.getVersion())) + .findFirst() + .orElse(null); + if (version == null || version.getVsac() == null) { + return CodeStatus.NA; + } + // prepare code path e.g. CODE:/CodeSystem/ActCode/Version/9.0.0/Code/AMB/Info + String codePath = + TerminologyServiceUtil.buildCodePath( + code.getCodeSystem(), version.getVsac(), code.getName()); + VsacCode svsCode = terminologyWebClient.getCode(codePath, apiKey); + if (svsCode.getStatus().equalsIgnoreCase("ok")) { + if ("Yes".equals(svsCode.getData().getResultSet().get(0).getActive())) { + return CodeStatus.ACTIVE; + } else { + return CodeStatus.INACTIVE; + } + } + return CodeStatus.NA; + } + + /** + * Verify if the user with given harp id has valid UMLS user + * + * @param harpId + * @return Instance of UmlsUser + */ + public UmlsUser verifyUmlsAccess(String harpId) { + UmlsUser user = findByHarpId(harpId).orElse(null); + if (user == null || StringUtils.isBlank(user.getApiKey())) { + log.error("UMLS API Key Not found for user : [{}}]", harpId); + throw new VsacUnauthorizedException("Please login to UMLS before proceeding"); + } + return user; + } + private VsacCode validateCodeAgainstVsac(String codePath, UmlsUser umlsUser) { return terminologyWebClient.getCode(codePath, umlsUser.getApiKey()); } diff --git a/src/main/java/gov/cms/madie/terminology/webclient/FhirTerminologyServiceWebClient.java b/src/main/java/gov/cms/madie/terminology/webclient/FhirTerminologyServiceWebClient.java index bb6f3b8..dba948a 100644 --- a/src/main/java/gov/cms/madie/terminology/webclient/FhirTerminologyServiceWebClient.java +++ b/src/main/java/gov/cms/madie/terminology/webclient/FhirTerminologyServiceWebClient.java @@ -1,5 +1,6 @@ package gov.cms.madie.terminology.webclient; +import gov.cms.madie.terminology.models.CodeSystem; import gov.cms.madie.terminology.util.TerminologyServiceUtil; import gov.cms.madie.models.measure.ManifestExpansion; import gov.cms.madie.terminology.dto.ValueSetsSearchCriteria; @@ -11,10 +12,12 @@ import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; import java.net.URI; import java.nio.charset.Charset; +import java.util.Map; @Component @Slf4j @@ -23,12 +26,14 @@ public class FhirTerminologyServiceWebClient { private final WebClient fhirTerminologyWebClient; private final String manifestPath; private final String codeSystemPath; + private final String codeLookupsUrl; private final String defaultProfile; public FhirTerminologyServiceWebClient( @Value("${client.fhir-terminology-service.base-url}") String fhirTerminologyServiceBaseUrl, @Value("${client.fhir-terminology-service.manifests-urn}") String manifestUrn, @Value("${client.fhir-terminology-service.code-system-urn}") String codeSystemUrn, + @Value("${client.fhir-terminology-service.code-lookups}") String codeLookupsUrl, @Value("${client.default_profile}") String defaultProfile) { fhirTerminologyWebClient = WebClient.builder() @@ -39,46 +44,19 @@ public FhirTerminologyServiceWebClient( .build(); this.manifestPath = manifestUrn; this.codeSystemPath = codeSystemUrn; + this.codeLookupsUrl = codeLookupsUrl; this.defaultProfile = defaultProfile; } public String getManifestBundle(String apiKey) { - return fhirTerminologyWebClient - .get() - .uri(manifestPath) - .headers(headers -> headers.setBasicAuth("apikey", apiKey)) - .accept(new MediaType("application", "fhir+json", Charset.defaultCharset())) - .exchangeToMono( - clientResponse -> { - if (clientResponse.statusCode().equals(HttpStatus.OK)) { - return clientResponse.bodyToMono(String.class); - } else { - log.debug("Received NON-OK response while retrieving Manifests"); - return clientResponse.createException().flatMap(Mono::error); - } - }) - .block(); + return fetchResourceFromVsac(manifestPath, apiKey, "Manifest"); } public String getCodeSystemsPage(Integer offset, Integer count, String apiKey) { // https://uat-cts.nlm.nih.gov/fhir/res/CodeSystem?_offset=0&_count=100 URI codeUri = TerminologyServiceUtil.buildRetrieveCodeSystemsUri(codeSystemPath, offset, count); log.debug("Retrieving codeSystems at {}, offset {}, count {}", codeSystemPath, offset, count); - return fhirTerminologyWebClient - .get() - .uri(codeUri.toString()) - .headers(headers -> headers.setBasicAuth("apikey", apiKey)) - .exchangeToMono( - clientResponse -> { - if (clientResponse.statusCode().equals(HttpStatus.BAD_REQUEST) - || clientResponse.statusCode().equals(HttpStatus.OK)) { - return clientResponse.bodyToMono(String.class); - } else { - log.debug("Received NON-OK response while retrieving codePath"); - return clientResponse.createException().flatMap(Mono::error); - } - }) - .block(); + return fetchResourceFromVsac(codeUri.toString(), apiKey, "CodeSystem"); } public String getValueSetResource( @@ -91,9 +69,23 @@ public String getValueSetResource( URI uri = TerminologyServiceUtil.buildValueSetResourceUri( valueSetParams, profile, includeDraft, manifestExpansion); + + return fetchResourceFromVsac(uri.toString(), apiKey, "ValueSet"); + } + + public String getCodeResource(String code, CodeSystem codeSystem, String apiKey) { + Map params = + Map.of( + "fullUrl", codeSystem.getFullUrl(), "code", code, "version", codeSystem.getVersion()); + URI uri = + UriComponentsBuilder.fromUriString(codeLookupsUrl).buildAndExpand(params).encode().toUri(); + return fetchResourceFromVsac(uri.toString(), apiKey, "Code"); + } + + private String fetchResourceFromVsac(String uri, String apiKey, String resourceType) { return fhirTerminologyWebClient .get() - .uri(uri.toString()) + .uri(uri) .headers(headers -> headers.setBasicAuth("apikey", apiKey)) .accept(new MediaType("application", "fhir+json", Charset.defaultCharset())) .exchangeToMono( @@ -101,7 +93,7 @@ public String getValueSetResource( if (clientResponse.statusCode().equals(HttpStatus.OK)) { return clientResponse.bodyToMono(String.class); } else { - log.debug("Received NON-OK response while retrieving Manifests"); + log.debug("Received NON-OK response while retrieving {}", resourceType); return clientResponse.createException().flatMap(Mono::error); } }) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5d0dd58..15c3f87 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -28,6 +28,7 @@ client: base-url: ${FHIR_TERMINOLOGY_BASE_URL:https://cts.nlm.nih.gov/fhir} manifests-urn: /Library code-system-urn: /CodeSystem + code-lookups: /CodeSystem/$lookup?system={fullUrl}&code={code}&version={version} spring: session: diff --git a/src/test/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyControllerMvcTest.java b/src/test/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyControllerMvcTest.java index b9a0c12..beed32c 100644 --- a/src/test/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyControllerMvcTest.java +++ b/src/test/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyControllerMvcTest.java @@ -1,10 +1,10 @@ package gov.cms.madie.terminology.controller; import gov.cms.madie.models.measure.ManifestExpansion; -import gov.cms.madie.models.measure.Measure; +import gov.cms.madie.terminology.dto.Code; import gov.cms.madie.terminology.dto.QdmValueSet; import gov.cms.madie.terminology.dto.ValueSetsSearchCriteria; -import gov.cms.madie.terminology.models.CodeSystem; +import gov.cms.madie.terminology.exceptions.VsacUnauthorizedException; import gov.cms.madie.terminology.models.UmlsUser; import gov.cms.madie.terminology.service.FhirTerminologyService; import gov.cms.madie.terminology.service.VsacService; @@ -21,7 +21,6 @@ import java.security.Principal; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; @@ -32,7 +31,6 @@ import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(VsacFhirTerminologyController.class) @@ -82,7 +80,7 @@ public void setup() { void testGetManifestsSuccessfullyMvc() throws Exception { Principal principal = mock(Principal.class); when(principal.getName()).thenReturn(TEST_USER); - when(vsacService.findByHarpId(anyString())).thenReturn(Optional.ofNullable(umlsUser)); + when(vsacService.verifyUmlsAccess(anyString())).thenReturn(umlsUser); when(fhirTerminologyService.getManifests(any(UmlsUser.class))).thenReturn(mockManifests); MvcResult result = mockMvc @@ -106,7 +104,9 @@ void testGetManifestsSuccessfullyMvc() throws Exception { void testUnAuthorizedUmlsUserWhileFetchingManifestsMvc() throws Exception { Principal principal = mock(Principal.class); when(principal.getName()).thenReturn(TEST_USER); - when(vsacService.findByHarpId(anyString())).thenReturn(Optional.empty()); + doThrow(new VsacUnauthorizedException("Please login to UMLS before proceeding")) + .when(vsacService) + .verifyUmlsAccess(anyString()); MvcResult result = mockMvc .perform( @@ -140,7 +140,7 @@ void testGetValueSetsExpansionsSuccessfullyMvc() throws Exception { Principal principal = mock(Principal.class); when(principal.getName()).thenReturn(TEST_USER); - when(vsacService.findByHarpId(anyString())).thenReturn(Optional.ofNullable(umlsUser)); + when(vsacService.verifyUmlsAccess(anyString())).thenReturn(umlsUser); when(fhirTerminologyService.getValueSetsExpansionsForQdm( any(ValueSetsSearchCriteria.class), any(UmlsUser.class))) .thenReturn(mockQdmValueSets); @@ -184,7 +184,9 @@ void testUnAuthorizedUmlsUserWhileGetValueSetsExpansionsMvc() throws Exception { + "}"; Principal principal = mock(Principal.class); when(principal.getName()).thenReturn(TEST_USER); - when(vsacService.findByHarpId(anyString())).thenReturn(Optional.empty()); + doThrow(new VsacUnauthorizedException("Please login to UMLS before proceeding")) + .when(vsacService) + .verifyUmlsAccess(anyString()); MvcResult result = mockMvc .perform( @@ -199,10 +201,12 @@ void testUnAuthorizedUmlsUserWhileGetValueSetsExpansionsMvc() throws Exception { } @Test - public void testRetrieveAndUpdateCodeSystemsSuccessfully() throws Exception { + public void testRetrieveAndUpdateCodeSystemsUnauthorized() throws Exception { Principal principal = mock(Principal.class); when(principal.getName()).thenReturn(TEST_USER); - when(vsacService.findByHarpId(anyString())).thenReturn(Optional.empty()); + doThrow(new VsacUnauthorizedException("Please login to UMLS before proceeding")) + .when(vsacService) + .verifyUmlsAccess(anyString()); MvcResult result = mockMvc .perform( @@ -217,10 +221,10 @@ public void testRetrieveAndUpdateCodeSystemsSuccessfully() throws Exception { } @Test - public void testRetrieveAndUpdateCodeSystemsUnauthorized() throws Exception { + public void testRetrieveAndUpdateCodeSystemsSuccessfully() throws Exception { Principal principal = mock(Principal.class); when(principal.getName()).thenReturn(TEST_USER); - when(vsacService.findByHarpId(anyString())).thenReturn(Optional.ofNullable(umlsUser)); + when(vsacService.verifyUmlsAccess(anyString())).thenReturn(umlsUser); mockMvc .perform( MockMvcRequestBuilders.get("/terminology/update-code-systems") @@ -230,4 +234,39 @@ public void testRetrieveAndUpdateCodeSystemsUnauthorized() throws Exception { .header("Authorization", TEST_TOKEN)) .andExpect(status().isOk()); } + + @Test + public void testGetCodeSuccessfully() throws Exception { + String codeName = "1963-8"; + String codeSystem = "LOINC"; + String version = "2.40"; + Code code = + Code.builder() + .name(codeName) + .codeSystem(codeSystem) + .version(version) + .display("Bicarbonate [Moles/volume] in Serum") + .codeSystemOid("2.16.840.1.113883.6.1") + .build(); + Principal principal = mock(Principal.class); + when(principal.getName()).thenReturn(TEST_USER); + when(vsacService.verifyUmlsAccess(anyString())).thenReturn(umlsUser); + when(fhirTerminologyService.retrieveCode(anyString(), anyString(), anyString(), anyString())) + .thenReturn(code); + MvcResult result = + mockMvc + .perform( + MockMvcRequestBuilders.get("/terminology/code") + .with(csrf()) + .with(user(TEST_USR)) + .param("code", codeName) + .param("codeSystem", codeSystem) + .param("version", version) + .header("Authorization", TEST_TOKEN)) + .andExpect(status().isOk()) + .andReturn(); + assertThat(result.getResponse().getStatus(), is(equalTo(200))); + assertThat(result.getResponse().getContentAsString(), containsString(code.getName())); + assertThat(result.getResponse().getContentAsString(), containsString(code.getDisplay())); + } } diff --git a/src/test/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyControllerTest.java b/src/test/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyControllerTest.java index 8ee4567..b3b9b4c 100644 --- a/src/test/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyControllerTest.java +++ b/src/test/java/gov/cms/madie/terminology/controller/VsacFhirTerminologyControllerTest.java @@ -1,6 +1,7 @@ package gov.cms.madie.terminology.controller; import gov.cms.madie.models.measure.ManifestExpansion; +import gov.cms.madie.terminology.dto.Code; import gov.cms.madie.terminology.dto.QdmValueSet; import gov.cms.madie.terminology.dto.ValueSetsSearchCriteria; import gov.cms.madie.terminology.exceptions.VsacUnauthorizedException; @@ -76,7 +77,7 @@ public void setUp() { void testGetManifestsSuccessfully() { Principal principal = mock(Principal.class); when(principal.getName()).thenReturn(TEST_USER); - when(vsacService.findByHarpId(anyString())).thenReturn(Optional.ofNullable(umlsUser)); + when(vsacService.verifyUmlsAccess(anyString())).thenReturn(umlsUser); when(fhirTerminologyService.getManifests(any(UmlsUser.class))).thenReturn(mockManifests); ResponseEntity> response = vsacFhirTerminologyController.getManifests(principal); @@ -88,8 +89,9 @@ void testGetManifestsSuccessfully() { void testUnAuthorizedUmlsUserWhileFetchingManifests() { Principal principal = mock(Principal.class); when(principal.getName()).thenReturn(TEST_USER); - when(vsacService.findByHarpId(anyString())) - .thenReturn(Optional.ofNullable(UmlsUser.builder().build())); + doThrow(new VsacUnauthorizedException("Please login to UMLS before proceeding")) + .when(vsacService) + .verifyUmlsAccess(anyString()); assertThrows( VsacUnauthorizedException.class, () -> vsacFhirTerminologyController.getManifests(principal)); @@ -100,7 +102,7 @@ void testGetValueSetsExpansionsSuccessfully() { Principal principal = mock(Principal.class); when(principal.getName()).thenReturn(TEST_USER); ValueSetsSearchCriteria valueSetsSearchCriteria = ValueSetsSearchCriteria.builder().build(); - when(vsacService.findByHarpId(anyString())).thenReturn(Optional.ofNullable(umlsUser)); + when(vsacService.verifyUmlsAccess(anyString())).thenReturn(umlsUser); when(fhirTerminologyService.getValueSetsExpansionsForQdm( any(ValueSetsSearchCriteria.class), any(UmlsUser.class))) .thenReturn(mockQdmValueSets); @@ -126,7 +128,7 @@ void retrieveAndUpdateCodeSystemsSuccessfully() { .build()); Principal principal = mock(Principal.class); when(principal.getName()).thenReturn(TEST_USER); - when(vsacService.findByHarpId(anyString())).thenReturn(Optional.ofNullable(umlsUser)); + when(vsacService.verifyUmlsAccess(anyString())).thenReturn(umlsUser); when(fhirTerminologyService.retrieveAllCodeSystems(any())).thenReturn(mockCodeSystemsPage); ResponseEntity> response = @@ -140,8 +142,9 @@ void retrieveAndUpdateCodeSystemsSuccessfully() { void testUnAuthorizedUmlsUserWhileFetchingValueSetsExpansions() { Principal principal = mock(Principal.class); when(principal.getName()).thenReturn(TEST_USER); - when(vsacService.findByHarpId(anyString())) - .thenReturn(Optional.ofNullable(UmlsUser.builder().build())); + doThrow(new VsacUnauthorizedException("Please login to UMLS before proceeding")) + .when(vsacService) + .verifyUmlsAccess(anyString()); assertThrows( VsacUnauthorizedException.class, () -> vsacFhirTerminologyController.getManifests(principal)); @@ -151,8 +154,9 @@ void testUnAuthorizedUmlsUserWhileFetchingValueSetsExpansions() { void testUnAuthorizedUmlsUserWhileretrievingAndUpdatingCodeSystems() { Principal principal = mock(Principal.class); when(principal.getName()).thenReturn(TEST_USER); - when(vsacService.findByHarpId(anyString())) - .thenReturn(Optional.ofNullable(UmlsUser.builder().build())); + doThrow(new VsacUnauthorizedException("Please login to UMLS before proceeding")) + .when(vsacService) + .verifyUmlsAccess(anyString()); assertThrows( VsacUnauthorizedException.class, () -> @@ -183,4 +187,47 @@ void testGetAllCodeSystemsSuccessfully() { assertEquals(response.getStatusCode(), HttpStatus.OK); assertEquals(response.getBody(), mockCodeSystemsPage); } + + @Test + void testGetCode() { + String codeName = "1963-8"; + String codeSystem = "LOINC"; + String version = "2.40"; + Principal principal = mock(Principal.class); + Code code = + Code.builder() + .name(codeName) + .codeSystem(codeSystem) + .version(version) + .display("Bicarbonate [Moles/volume] in Serum") + .codeSystemOid("2.16.840.1.113883.6.1") + .build(); + when(principal.getName()).thenReturn(TEST_USER); + when(vsacService.verifyUmlsAccess(anyString())).thenReturn(umlsUser); + when(fhirTerminologyService.retrieveCode(anyString(), anyString(), anyString(), anyString())) + .thenReturn(code); + + ResponseEntity response = + vsacFhirTerminologyController.getCode(codeName, codeSystem, version, principal); + assertEquals(response.getStatusCode(), HttpStatus.OK); + assertEquals(response.getBody(), code); + } + + @Test + void testGetCodeIfNoUmlsUserFound() { + String codeName = "1963-8"; + String codeSystem = "LOINC"; + String version = "2.40"; + Principal principal = mock(Principal.class); + when(principal.getName()).thenReturn(TEST_USER); + doThrow(new VsacUnauthorizedException("Please login to UMLS before proceeding")) + .when(vsacService) + .verifyUmlsAccess(anyString()); + + Exception ex = + assertThrows( + VsacUnauthorizedException.class, + () -> vsacFhirTerminologyController.getCode(codeName, codeSystem, version, principal)); + assertEquals(ex.getMessage(), "Please login to UMLS before proceeding"); + } } diff --git a/src/test/java/gov/cms/madie/terminology/service/FhirTerminologyServiceTest.java b/src/test/java/gov/cms/madie/terminology/service/FhirTerminologyServiceTest.java index a50aa93..eb8224c 100644 --- a/src/test/java/gov/cms/madie/terminology/service/FhirTerminologyServiceTest.java +++ b/src/test/java/gov/cms/madie/terminology/service/FhirTerminologyServiceTest.java @@ -4,6 +4,8 @@ import com.okta.commons.lang.Collections; import gov.cms.madie.models.mapping.CodeSystemEntry; import gov.cms.madie.models.measure.ManifestExpansion; +import gov.cms.madie.terminology.dto.Code; +import gov.cms.madie.terminology.dto.CodeStatus; import gov.cms.madie.terminology.dto.QdmValueSet; import gov.cms.madie.terminology.dto.ValueSetsSearchCriteria; import gov.cms.madie.terminology.helpers.TestHelpers; @@ -21,6 +23,11 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.times; @@ -42,8 +49,10 @@ class FhirTerminologyServiceTest { @Mock FhirTerminologyServiceWebClient fhirTerminologyServiceWebClient; @Mock FhirContext fhirContext; @Mock MappingService mappingService; - @InjectMocks FhirTerminologyService fhirTerminologyService; @Mock CodeSystemRepository codeSystemRepository; + @Mock VsacService vsacService; + @InjectMocks FhirTerminologyService fhirTerminologyService; + List codeSystemEntries; private UmlsUser umlsUser; private static final String TEST_HARP_ID = "te$tHarpId"; @@ -337,4 +346,82 @@ void testGetAllCodeSystems() { assertEquals("t1", result.get(0).getTitle()); assertEquals("t2", result.get(1).getTitle()); } + + @Test + void testRetrieveCodeWhenCodeIsNull() { + String codeSystem = "LOINC"; + String version = "2.40"; + assertThat( + fhirTerminologyService.retrieveCode(null, codeSystem, version, TEST_API_KEY), + is(equalTo(null))); + } + + @Test + void testRetrieveCodeWhenCodeSystemIsNull() { + String codeName = "1963-8"; + String version = "2.40"; + assertThat( + fhirTerminologyService.retrieveCode(codeName, null, version, TEST_API_KEY), + is(equalTo(null))); + } + + @Test + void testRetrieveCodeWhenCodeSystemVersionIsNull() { + String codeName = "1963-8"; + String codeSystem = "LOINC"; + assertThat( + fhirTerminologyService.retrieveCode(codeName, codeSystem, null, TEST_API_KEY), + is(equalTo(null))); + } + + @Test + void testRetrieveCodeWhenCodeSystemNotFound() { + String codeName = "1963-8"; + String codeSystem = "LOINC"; + String version = "2.40"; + when(codeSystemRepository.findByNameAndVersion(codeSystem, version)) + .thenReturn(Optional.empty()); + assertThat( + fhirTerminologyService.retrieveCode(codeName, codeSystem, version, TEST_API_KEY), + is(equalTo(null))); + } + + @Test + void testRetrieveCodeSuccessfully() { + String codeName = "1963-8"; + String codeSystemName = "LOINC"; + String version = "2.40"; + String codeJson = + "{\n" + + " \"resourceType\": \"Parameters\",\n" + + " \"parameter\": [ {\n" + + " \"name\": \"name\",\n" + + " \"valueString\": \"LOINC\"\n" + + " }, {\n" + + " \"name\": \"version\",\n" + + " \"valueString\": \"2.40\"\n" + + " }, {\n" + + " \"name\": \"display\",\n" + + " \"valueString\": \"Bicarbonate [Moles/volume] in Serum\"\n" + + " }, {\n" + + " \"name\": \"Oid\",\n" + + " \"valueString\": \"2.16.840.1.113883.6.1\"\n" + + " } ]\n" + + "}"; + + var codeSystem = gov.cms.madie.terminology.models.CodeSystem.builder().build(); + when(codeSystemRepository.findByNameAndVersion(anyString(), anyString())) + .thenReturn(Optional.of(codeSystem)); + when(fhirTerminologyServiceWebClient.getCodeResource(codeName, codeSystem, TEST_API_KEY)) + .thenReturn(codeJson); + when(fhirContext.newJsonParser()).thenReturn(FhirContext.forR4().newJsonParser()); + when(vsacService.getCodeStatus(any(Code.class), anyString())).thenReturn(CodeStatus.ACTIVE); + Code code = + fhirTerminologyService.retrieveCode(codeName, codeSystemName, version, TEST_API_KEY); + assertThat(code.getName(), is(equalTo(codeName))); + assertThat(code.getDisplay(), is(equalTo("Bicarbonate [Moles/volume] in Serum"))); + assertThat(code.getCodeSystem(), is(equalTo(codeSystemName))); + assertThat(code.getVersion(), is(equalTo(version))); + assertThat(code.getStatus(), is(equalTo(CodeStatus.ACTIVE))); + } } diff --git a/src/test/java/gov/cms/madie/terminology/service/VsacServiceTest.java b/src/test/java/gov/cms/madie/terminology/service/VsacServiceTest.java index 664ca06..56f5a00 100644 --- a/src/test/java/gov/cms/madie/terminology/service/VsacServiceTest.java +++ b/src/test/java/gov/cms/madie/terminology/service/VsacServiceTest.java @@ -9,6 +9,8 @@ import gov.cms.madie.models.cql.terminology.VsacCode.VsacError; import gov.cms.madie.models.mapping.CodeSystemEntry; +import gov.cms.madie.terminology.dto.Code; +import gov.cms.madie.terminology.dto.CodeStatus; import gov.cms.madie.terminology.dto.QdmValueSet; import gov.cms.madie.terminology.dto.ValueSetsSearchCriteria; import gov.cms.madie.terminology.exceptions.VsacUnauthorizedException; @@ -39,6 +41,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -423,4 +426,168 @@ public void testGetEmptyQdmValueSets() { assertThat(valueSets.get(0).getDisplayName(), is(equalTo("Office Visit"))); assertThat(valueSets.get(0).getConcepts().size(), is(equalTo(0))); } + + @Test + void testVerifyUmlsAccessUmlsUserNotFound() { + when(umlsUserRepository.findByHarpId(anyString())).thenReturn(Optional.empty()); + Exception exception = + assertThrows( + VsacUnauthorizedException.class, () -> vsacService.verifyUmlsAccess(TEST_API_KEY)); + assertThat(exception.getMessage(), is(equalTo("Please login to UMLS before proceeding"))); + } + + @Test + void testVerifyUmlsAccessUmlsUserApiKeyIsMissing() { + UmlsUser umlsUserCopy = umlsUser.toBuilder().apiKey(null).build(); + when(umlsUserRepository.findByHarpId(anyString())).thenReturn(Optional.of(umlsUserCopy)); + Exception exception = + assertThrows( + VsacUnauthorizedException.class, () -> vsacService.verifyUmlsAccess(TEST_API_KEY)); + assertThat(exception.getMessage(), is(equalTo("Please login to UMLS before proceeding"))); + } + + @Test + void testVerifyUmlsAccess() { + when(umlsUserRepository.findByHarpId(anyString())).thenReturn(Optional.of(umlsUser)); + UmlsUser user = vsacService.verifyUmlsAccess(TEST_API_KEY); + assertThat(user.getHarpId(), is(equalTo(TEST_HARP_ID))); + assertThat(user.getApiKey(), is(equalTo(TEST_API_KEY))); + } + + @Test + void testGetCodeStatusIfCodeSystemMappingAbsent() { + when(mappingService.getCodeSystemEntry(anyString())).thenReturn(null); + assertThat( + vsacService.getCodeStatus(Code.builder().codeSystem("test").build(), TEST_API_KEY), + is(equalTo(CodeStatus.NA))); + } + + @Test + void testGetCodeStatusIfCodeSystemNotInSvs() { + var cse = CodeSystemEntry.builder().oid("NOT.IN.VSAC1").build(); + when(mappingService.getCodeSystemEntry(anyString())).thenReturn(cse); + assertThat( + vsacService.getCodeStatus(Code.builder().codeSystem("test").build(), TEST_API_KEY), + is(equalTo(CodeStatus.NA))); + } + + @Test + void testGetCodeStatusIfCodeSystemVersionEmpty() { + var cse = CodeSystemEntry.builder().oid("1.1.1.1").versions(List.of()).build(); + when(mappingService.getCodeSystemEntry(anyString())).thenReturn(cse); + assertThat( + vsacService.getCodeStatus(Code.builder().codeSystem("test").build(), TEST_API_KEY), + is(equalTo(CodeStatus.NA))); + } + + @Test + void testGetCodeStatusIfCodeSystemVersionForVsacIsNull() { + CodeSystemEntry.Version version = new CodeSystemEntry.Version(); + version.setVsac(null); + version.setFhir("https://fhir-version"); + var cse = CodeSystemEntry.builder().oid("1.1.1.1").versions(List.of(version)).build(); + when(mappingService.getCodeSystemEntry(anyString())).thenReturn(cse); + assertThat( + vsacService.getCodeStatus(Code.builder().codeSystem("test").build(), TEST_API_KEY), + is(equalTo(CodeStatus.NA))); + } + + @Test + void testGetCodeStatusActive() { + CodeSystemEntry.Version version = new CodeSystemEntry.Version(); + version.setVsac("2023-09"); + version.setFhir("abc.info/20230901"); + var codeSystemEntry = + CodeSystemEntry.builder() + .name("ABC") + .oid("urn:oid:1.2.3.4.96") + .url("abc.info") + .versions(Collections.toList(version)) + .build(); + Code code = + Code.builder() + .name("1222766008") + .codeSystem("ABC") + .version("abc.info/20230901") + .display("American Joint Committee on Cancer stage IIA") + .codeSystemOid("1.2.3.4.96") + .build(); + var codeResultSet = new VsacCode.VsacDataResultSet(); + codeResultSet.setActive("Yes"); + var codeData = new VsacCode.VsacData(); + codeData.setResultSet(List.of(codeResultSet)); + VsacCode vsacCode = new VsacCode(); + vsacCode.setStatus("ok"); + vsacCode.setData(codeData); + when(mappingService.getCodeSystemEntry(anyString())).thenReturn(codeSystemEntry); + when(terminologyServiceWebClient.getCode(anyString(), anyString())).thenReturn(vsacCode); + CodeStatus status = vsacService.getCodeStatus(code, TEST_API_KEY); + assertThat(status, is(equalTo(CodeStatus.ACTIVE))); + } + + @Test + void testGetCodeStatusInactive() { + CodeSystemEntry.Version version = new CodeSystemEntry.Version(); + version.setVsac("2023-09"); + version.setFhir("abc.info/20230901"); + var codeSystemEntry = + CodeSystemEntry.builder() + .name("ABC") + .oid("urn:oid:1.2.3.4.96") + .url("abc.info") + .versions(Collections.toList(version)) + .build(); + Code code = + Code.builder() + .name("1222766008") + .codeSystem("ABC") + .version("abc.info/20230901") + .display("American Joint Committee on Cancer stage IIA") + .codeSystemOid("1.2.3.4.96") + .build(); + var codeResultSet = new VsacCode.VsacDataResultSet(); + codeResultSet.setActive("No"); + var codeData = new VsacCode.VsacData(); + codeData.setResultSet(List.of(codeResultSet)); + VsacCode vsacCode = new VsacCode(); + vsacCode.setStatus("ok"); + vsacCode.setData(codeData); + when(mappingService.getCodeSystemEntry(anyString())).thenReturn(codeSystemEntry); + when(terminologyServiceWebClient.getCode(anyString(), anyString())).thenReturn(vsacCode); + CodeStatus status = vsacService.getCodeStatus(code, TEST_API_KEY); + assertThat(status, is(equalTo(CodeStatus.INACTIVE))); + } + + @Test + void testGetCodeStatusIfCOdeNotFoundInSvs() { + CodeSystemEntry.Version version = new CodeSystemEntry.Version(); + version.setVsac("2023-09"); + version.setFhir("abc.info/20230901"); + var codeSystemEntry = + CodeSystemEntry.builder() + .name("ABC") + .oid("urn:oid:1.2.3.4.96") + .url("abc.info") + .versions(Collections.toList(version)) + .build(); + Code code = + Code.builder() + .name("1222766008") + .codeSystem("ABC") + .version("abc.info/20230901") + .display("American Joint Committee on Cancer stage IIA") + .codeSystemOid("1.2.3.4.96") + .build(); + var codeResultSet = new VsacCode.VsacDataResultSet(); + codeResultSet.setActive("No"); + var codeData = new VsacCode.VsacData(); + codeData.setResultSet(List.of(codeResultSet)); + VsacCode vsacCode = new VsacCode(); + vsacCode.setStatus("non-ok"); + vsacCode.setData(codeData); + when(mappingService.getCodeSystemEntry(anyString())).thenReturn(codeSystemEntry); + when(terminologyServiceWebClient.getCode(anyString(), anyString())).thenReturn(vsacCode); + CodeStatus status = vsacService.getCodeStatus(code, TEST_API_KEY); + assertThat(status, is(equalTo(CodeStatus.NA))); + } } diff --git a/src/test/java/gov/cms/madie/terminology/webclient/FhirTerminologyServiceWebClientTest.java b/src/test/java/gov/cms/madie/terminology/webclient/FhirTerminologyServiceWebClientTest.java index 05efcb2..8fb177d 100644 --- a/src/test/java/gov/cms/madie/terminology/webclient/FhirTerminologyServiceWebClientTest.java +++ b/src/test/java/gov/cms/madie/terminology/webclient/FhirTerminologyServiceWebClientTest.java @@ -2,6 +2,7 @@ import gov.cms.madie.models.measure.ManifestExpansion; import gov.cms.madie.terminology.dto.ValueSetsSearchCriteria; +import gov.cms.madie.terminology.models.CodeSystem; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -23,6 +24,7 @@ class FhirTerminologyServiceWebClientTest { private static final String MOCK_MANIFEST_URN = "/manifestUrn"; private static final String MOCK_CODE_SYSTEM_URN = "/codeSystemUrn"; private static final String MOCK_API_KEY = "test-api-key"; + private static final String MOCK_CODE_LOOKUP = "/CodeSystem/$lookup"; private static final String DEFAULT_PROFILE = "Most Recent Code System Versions in VSAC"; public static MockWebServer mockBackEnd; @@ -42,7 +44,7 @@ void initialize() { String baseUrl = String.format("http://localhost:%s", mockBackEnd.getPort()); fhirTerminologyServiceWebClient = new FhirTerminologyServiceWebClient( - baseUrl, MOCK_MANIFEST_URN, MOCK_CODE_SYSTEM_URN, DEFAULT_PROFILE); + baseUrl, MOCK_MANIFEST_URN, MOCK_CODE_SYSTEM_URN, MOCK_CODE_LOOKUP, DEFAULT_PROFILE); } @AfterAll @@ -174,4 +176,23 @@ void getCodeSystemsPage_ReturnsException() throws InterruptedException { RecordedRequest recordedRequest = mockBackEnd.takeRequest(); assertEquals("/codeSystemUrn?_offset=0&_count=50", recordedRequest.getPath()); } + + @Test + void testGetCodeResource() throws InterruptedException { + String codeName = "1963-8"; + CodeSystem codeSystem = + CodeSystem.builder().fullUrl("http://loinc.org").name("LOINC").version("2.40").build(); + mockBackEnd.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody(MOCK_RESPONSE_STRING) + .addHeader("Content-Type", "application/fhir+json")); + + String codeJson = + fhirTerminologyServiceWebClient.getCodeResource(codeName, codeSystem, MOCK_API_KEY); + assertNotNull(codeJson); + assertEquals(MOCK_RESPONSE_STRING, codeJson); + RecordedRequest recordedRequest = mockBackEnd.takeRequest(); + assertEquals("/CodeSystem/$lookup", recordedRequest.getPath()); + } }