1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.healthconnect.phr.validations; 18 19 import static android.healthconnect.cts.phr.utils.ObservationBuilder.LOINC; 20 import static android.healthconnect.cts.phr.utils.ObservationBuilder.ObservationCategory.LABORATORY; 21 import static android.healthconnect.cts.phr.utils.ObservationBuilder.ObservationCategory.SOCIAL_HISTORY; 22 import static android.healthconnect.cts.phr.utils.ObservationBuilder.ObservationCategory.VITAL_SIGNS; 23 import static android.healthconnect.cts.phr.utils.ObservationBuilder.QuantityUnits.BEATS_PER_MINUTE; 24 import static android.healthconnect.cts.phr.utils.ObservationBuilder.QuantityUnits.BREATHS_PER_MINUTE; 25 import static android.healthconnect.cts.phr.utils.ObservationBuilder.QuantityUnits.CELSIUS; 26 import static android.healthconnect.cts.phr.utils.ObservationBuilder.QuantityUnits.CENTIMETERS; 27 import static android.healthconnect.cts.phr.utils.ObservationBuilder.QuantityUnits.KILOGRAMS; 28 import static android.healthconnect.cts.phr.utils.ObservationBuilder.QuantityUnits.KILOGRAMS_PER_M2; 29 import static android.healthconnect.cts.phr.utils.ObservationBuilder.QuantityUnits.MILLIMETERS_OF_MERCURY; 30 import static android.healthconnect.cts.phr.utils.ObservationBuilder.QuantityUnits.PERCENT; 31 import static android.healthconnect.cts.phr.utils.ObservationBuilder.QuantityUnits.POUNDS; 32 import static android.healthconnect.cts.phr.utils.ObservationBuilder.SNOMED_CT; 33 import static android.healthconnect.cts.phr.utils.ObservationBuilder.makeCodeableConcept; 34 import static android.healthconnect.cts.phr.utils.PhrDataFactory.DATA_SOURCE_ID; 35 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_ALLERGY; 36 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_IMMUNIZATION; 37 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_IMMUNIZATION_FIELD_MISSING_INVALID; 38 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_IMMUNIZATION_ID_EMPTY; 39 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_IMMUNIZATION_ID_NOT_EXISTS; 40 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_IMMUNIZATION_RESOURCE_TYPE_NOT_EXISTS; 41 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_IMMUNIZATION_UNSUPPORTED_RESOURCE_TYPE; 42 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_RESOURCE_ID_IMMUNIZATION; 43 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_RESOURCE_TYPE_UNSUPPORTED; 44 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_VERSION_R4; 45 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_VERSION_R4B; 46 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_VERSION_UNSUPPORTED; 47 import static android.healthconnect.cts.phr.utils.PhrDataFactory.getUpsertMedicalResourceRequest; 48 import static android.healthconnect.cts.phr.utils.PhrDataFactory.getUpsertMedicalResourceRequestBuilder; 49 50 import static com.android.healthfitness.flags.Flags.FLAG_PERSONAL_HEALTH_RECORD; 51 import static com.android.healthfitness.flags.Flags.FLAG_PERSONAL_HEALTH_RECORD_DATABASE; 52 import static com.android.healthfitness.flags.Flags.FLAG_PHR_FHIR_STRUCTURAL_VALIDATION; 53 54 import static com.google.common.truth.Truth.assertThat; 55 56 import static org.junit.Assert.assertThrows; 57 58 import android.health.connect.UpsertMedicalResourceRequest; 59 import android.health.connect.datatypes.FhirResource; 60 import android.health.connect.datatypes.MedicalResource; 61 import android.healthconnect.cts.phr.utils.ConditionBuilder; 62 import android.healthconnect.cts.phr.utils.EncountersBuilder; 63 import android.healthconnect.cts.phr.utils.ImmunizationBuilder; 64 import android.healthconnect.cts.phr.utils.MedicationsBuilder; 65 import android.healthconnect.cts.phr.utils.ObservationBuilder; 66 import android.healthconnect.cts.phr.utils.ObservationBuilder.QuantityUnits; 67 import android.healthconnect.cts.phr.utils.PatientBuilder; 68 import android.healthconnect.cts.phr.utils.PractitionerBuilder; 69 import android.healthconnect.cts.phr.utils.ProcedureBuilder; 70 import android.platform.test.annotations.EnableFlags; 71 import android.platform.test.flag.junit.SetFlagsRule; 72 73 import com.android.server.healthconnect.storage.request.UpsertMedicalResourceInternalRequest; 74 75 import com.google.testing.junit.testparameterinjector.TestParameter; 76 import com.google.testing.junit.testparameterinjector.TestParameterInjector; 77 78 import org.json.JSONArray; 79 import org.json.JSONException; 80 import org.json.JSONObject; 81 import org.junit.Rule; 82 import org.junit.Test; 83 import org.junit.runner.RunWith; 84 85 import java.util.List; 86 87 @EnableFlags({ 88 FLAG_PERSONAL_HEALTH_RECORD, 89 FLAG_PERSONAL_HEALTH_RECORD_DATABASE, 90 FLAG_PHR_FHIR_STRUCTURAL_VALIDATION 91 }) 92 @RunWith(TestParameterInjector.class) 93 public class MedicalResourceValidatorTest { 94 @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); 95 96 private FhirResourceValidator mFhirResourceValidator = null; 97 98 @Test testValidateAndCreateInternalRequest_validAndR4_populatesInternalRequest()99 public void testValidateAndCreateInternalRequest_validAndR4_populatesInternalRequest() { 100 UpsertMedicalResourceRequest upsertRequest = 101 new UpsertMedicalResourceRequest.Builder( 102 DATA_SOURCE_ID, FHIR_VERSION_R4, FHIR_DATA_IMMUNIZATION) 103 .build(); 104 UpsertMedicalResourceInternalRequest expected = 105 new UpsertMedicalResourceInternalRequest() 106 .setDataSourceId(DATA_SOURCE_ID) 107 .setMedicalResourceType(MedicalResource.MEDICAL_RESOURCE_TYPE_VACCINES) 108 .setFhirResourceType(FhirResource.FHIR_RESOURCE_TYPE_IMMUNIZATION) 109 .setFhirResourceId(FHIR_RESOURCE_ID_IMMUNIZATION) 110 .setFhirVersion(FHIR_VERSION_R4) 111 .setData(FHIR_DATA_IMMUNIZATION); 112 113 MedicalResourceValidator validator = 114 new MedicalResourceValidator(upsertRequest, getFhirResourceValidator()); 115 UpsertMedicalResourceInternalRequest validatedRequest = 116 validator.validateAndCreateInternalRequest(); 117 118 assertThat(validatedRequest).isEqualTo(expected); 119 } 120 121 @Test testValidateAndCreateInternalRequest_validAndR4B_populatesInternalRequest()122 public void testValidateAndCreateInternalRequest_validAndR4B_populatesInternalRequest() { 123 UpsertMedicalResourceRequest upsertRequest = 124 new UpsertMedicalResourceRequest.Builder( 125 DATA_SOURCE_ID, FHIR_VERSION_R4B, FHIR_DATA_IMMUNIZATION) 126 .build(); 127 UpsertMedicalResourceInternalRequest expected = 128 new UpsertMedicalResourceInternalRequest() 129 .setDataSourceId(DATA_SOURCE_ID) 130 .setMedicalResourceType(MedicalResource.MEDICAL_RESOURCE_TYPE_VACCINES) 131 .setFhirResourceType(FhirResource.FHIR_RESOURCE_TYPE_IMMUNIZATION) 132 .setFhirResourceId(FHIR_RESOURCE_ID_IMMUNIZATION) 133 .setFhirVersion(FHIR_VERSION_R4B) 134 .setData(FHIR_DATA_IMMUNIZATION); 135 136 MedicalResourceValidator validator = 137 new MedicalResourceValidator(upsertRequest, getFhirResourceValidator()); 138 UpsertMedicalResourceInternalRequest validatedRequest = 139 validator.validateAndCreateInternalRequest(); 140 141 assertThat(validatedRequest).isEqualTo(expected); 142 } 143 144 @Test testConstructor_nullValidator_succeeds()145 public void testConstructor_nullValidator_succeeds() { 146 new MedicalResourceValidator(getUpsertMedicalResourceRequest(), null); 147 } 148 149 @Test testValidateAndCreateInternalRequest_nullValidator_succeeds()150 public void testValidateAndCreateInternalRequest_nullValidator_succeeds() { 151 UpsertMedicalResourceRequest upsertRequest = 152 new UpsertMedicalResourceRequest.Builder( 153 DATA_SOURCE_ID, FHIR_VERSION_R4, FHIR_DATA_IMMUNIZATION) 154 .build(); 155 UpsertMedicalResourceInternalRequest expected = 156 new UpsertMedicalResourceInternalRequest() 157 .setDataSourceId(DATA_SOURCE_ID) 158 .setMedicalResourceType(MedicalResource.MEDICAL_RESOURCE_TYPE_VACCINES) 159 .setFhirResourceType(FhirResource.FHIR_RESOURCE_TYPE_IMMUNIZATION) 160 .setFhirResourceId(FHIR_RESOURCE_ID_IMMUNIZATION) 161 .setFhirVersion(FHIR_VERSION_R4) 162 .setData(FHIR_DATA_IMMUNIZATION); 163 164 MedicalResourceValidator validator = new MedicalResourceValidator(upsertRequest, null); 165 UpsertMedicalResourceInternalRequest validatedRequest = 166 validator.validateAndCreateInternalRequest(); 167 168 assertThat(validatedRequest).isEqualTo(expected); 169 } 170 171 @Test testValidateAndCreateInternalRequest_nullValidator_unknownFieldSucceeds()172 public void testValidateAndCreateInternalRequest_nullValidator_unknownFieldSucceeds() { 173 String immunizationWithUnknownField = 174 new ImmunizationBuilder() 175 .setId(FHIR_RESOURCE_ID_IMMUNIZATION) 176 .set("unknown", "test") 177 .toJson(); 178 UpsertMedicalResourceRequest upsertRequest = 179 new UpsertMedicalResourceRequest.Builder( 180 DATA_SOURCE_ID, FHIR_VERSION_R4, immunizationWithUnknownField) 181 .build(); 182 UpsertMedicalResourceInternalRequest expected = 183 new UpsertMedicalResourceInternalRequest() 184 .setDataSourceId(DATA_SOURCE_ID) 185 .setMedicalResourceType(MedicalResource.MEDICAL_RESOURCE_TYPE_VACCINES) 186 .setFhirResourceType(FhirResource.FHIR_RESOURCE_TYPE_IMMUNIZATION) 187 .setFhirResourceId(FHIR_RESOURCE_ID_IMMUNIZATION) 188 .setFhirVersion(FHIR_VERSION_R4) 189 .setData(immunizationWithUnknownField); 190 191 MedicalResourceValidator validator = new MedicalResourceValidator(upsertRequest, null); 192 UpsertMedicalResourceInternalRequest validatedRequest = 193 validator.validateAndCreateInternalRequest(); 194 195 assertThat(validatedRequest).isEqualTo(expected); 196 } 197 198 @Test testValidateAndCreateInternalRequest_nonNullValidator_unknownFieldThrows()199 public void testValidateAndCreateInternalRequest_nonNullValidator_unknownFieldThrows() { 200 String immunizationWithUnknownField = 201 new ImmunizationBuilder().set("unknown", "test").toJson(); 202 203 UpsertMedicalResourceRequest request = 204 new UpsertMedicalResourceRequest.Builder( 205 DATA_SOURCE_ID, FHIR_VERSION_R4, immunizationWithUnknownField) 206 .build(); 207 MedicalResourceValidator validator = 208 new MedicalResourceValidator(request, getFhirResourceValidator()); 209 210 Throwable thrown = 211 assertThrows( 212 IllegalArgumentException.class, 213 () -> validator.validateAndCreateInternalRequest()); 214 assertThat(thrown).hasMessageThat().contains("Found unexpected field "); 215 } 216 217 @Test testValidateAndCreateInternalRequest_fhirResourceWithoutType_throws()218 public void testValidateAndCreateInternalRequest_fhirResourceWithoutType_throws() { 219 MedicalResourceValidator validator = 220 makeValidator(FHIR_DATA_IMMUNIZATION_RESOURCE_TYPE_NOT_EXISTS); 221 222 var thrown = 223 assertThrows( 224 IllegalArgumentException.class, 225 validator::validateAndCreateInternalRequest); 226 assertThat(thrown) 227 .hasMessageThat() 228 .contains( 229 "Missing resourceType field for resource with id " 230 + FHIR_RESOURCE_ID_IMMUNIZATION); 231 } 232 233 @Test testValidateAndCreateInternalRequest_fhirResourceWithoutId_throws()234 public void testValidateAndCreateInternalRequest_fhirResourceWithoutId_throws() { 235 MedicalResourceValidator validator = makeValidator(FHIR_DATA_IMMUNIZATION_ID_NOT_EXISTS); 236 237 var thrown = 238 assertThrows( 239 IllegalArgumentException.class, 240 validator::validateAndCreateInternalRequest); 241 assertThat(thrown).hasMessageThat().contains("Resource is missing id field"); 242 } 243 244 @Test testValidateAndCreateInternalRequest_nullValidatorMissingId_throws()245 public void testValidateAndCreateInternalRequest_nullValidatorMissingId_throws() { 246 UpsertMedicalResourceRequest request = 247 new UpsertMedicalResourceRequest.Builder( 248 DATA_SOURCE_ID, 249 FHIR_VERSION_R4, 250 FHIR_DATA_IMMUNIZATION_ID_NOT_EXISTS) 251 .build(); 252 MedicalResourceValidator validator = new MedicalResourceValidator(request, null); 253 254 var thrown = 255 assertThrows( 256 IllegalArgumentException.class, 257 validator::validateAndCreateInternalRequest); 258 assertThat(thrown).hasMessageThat().contains("Resource is missing id field"); 259 } 260 261 @Test testValidateAndCreateInternalRequest_invalidJson_throws()262 public void testValidateAndCreateInternalRequest_invalidJson_throws() { 263 MedicalResourceValidator validator = 264 makeValidator(FHIR_DATA_IMMUNIZATION_FIELD_MISSING_INVALID); 265 266 var thrown = 267 assertThrows( 268 IllegalArgumentException.class, 269 validator::validateAndCreateInternalRequest); 270 assertThat(thrown).hasMessageThat().contains("invalid json"); 271 } 272 273 @Test testValidateAndCreateInternalRequest_emptyId_throws()274 public void testValidateAndCreateInternalRequest_emptyId_throws() { 275 MedicalResourceValidator validator = makeValidator(FHIR_DATA_IMMUNIZATION_ID_EMPTY); 276 277 var thrown = 278 assertThrows( 279 IllegalArgumentException.class, 280 validator::validateAndCreateInternalRequest); 281 assertThat(thrown).hasMessageThat().contains("id cannot be empty"); 282 } 283 284 @Test testValidateAndCreateInternalRequest_nullId_throws()285 public void testValidateAndCreateInternalRequest_nullId_throws() { 286 String immunizationJson = new ImmunizationBuilder().set("id", JSONObject.NULL).toJson(); 287 MedicalResourceValidator validator = makeValidator(immunizationJson); 288 289 var thrown = 290 assertThrows( 291 IllegalArgumentException.class, 292 validator::validateAndCreateInternalRequest); 293 assertThat(thrown).hasMessageThat().contains("id should be a string"); 294 } 295 296 @Test testValidateAndCreateInternalRequest_nonStringId_throws()297 public void testValidateAndCreateInternalRequest_nonStringId_throws() { 298 String immunizationJson = new ImmunizationBuilder().set("id", 123).toJson(); 299 MedicalResourceValidator validator = makeValidator(immunizationJson); 300 301 var thrown = 302 assertThrows( 303 IllegalArgumentException.class, 304 validator::validateAndCreateInternalRequest); 305 assertThat(thrown).hasMessageThat().contains("id should be a string"); 306 } 307 308 @Test testValidateAndCreateInternalRequest_nullString_succeeds()309 public void testValidateAndCreateInternalRequest_nullString_succeeds() { 310 String immunizationJson = new ImmunizationBuilder().set("id", "null").toJson(); 311 MedicalResourceValidator validator = makeValidator(immunizationJson); 312 313 UpsertMedicalResourceInternalRequest internalRequest = 314 validator.validateAndCreateInternalRequest(); 315 316 assertThat(internalRequest.getFhirResourceId()).isEqualTo("null"); 317 } 318 319 @Test testValidateAndCreateInternalRequest_unsupportedFhirVersion_throws()320 public void testValidateAndCreateInternalRequest_unsupportedFhirVersion_throws() { 321 UpsertMedicalResourceRequest upsertRequest = 322 getUpsertMedicalResourceRequestBuilder() 323 .setFhirVersion(FHIR_VERSION_UNSUPPORTED) 324 .build(); 325 MedicalResourceValidator validator = 326 new MedicalResourceValidator(upsertRequest, getFhirResourceValidator()); 327 328 var thrown = 329 assertThrows( 330 IllegalArgumentException.class, 331 validator::validateAndCreateInternalRequest); 332 assertThat(thrown) 333 .hasMessageThat() 334 .contains( 335 "Unsupported FHIR version " 336 + FHIR_VERSION_UNSUPPORTED 337 + " for resource with id " 338 + FHIR_RESOURCE_ID_IMMUNIZATION); 339 } 340 341 @Test testValidateAndCreateInternalRequest_unsupportedResourceType_throws()342 public void testValidateAndCreateInternalRequest_unsupportedResourceType_throws() { 343 MedicalResourceValidator validator = 344 makeValidator(FHIR_DATA_IMMUNIZATION_UNSUPPORTED_RESOURCE_TYPE); 345 346 var thrown = 347 assertThrows( 348 IllegalArgumentException.class, 349 validator::validateAndCreateInternalRequest); 350 assertThat(thrown) 351 .hasMessageThat() 352 .contains( 353 "Unsupported FHIR resource type " 354 + FHIR_RESOURCE_TYPE_UNSUPPORTED 355 + " for resource with id " 356 + FHIR_RESOURCE_ID_IMMUNIZATION); 357 } 358 359 @Test testValidateAndCreateInternalRequest_nullResourceType_throws()360 public void testValidateAndCreateInternalRequest_nullResourceType_throws() { 361 String immunizationJson = 362 new ImmunizationBuilder() 363 .setId(FHIR_RESOURCE_ID_IMMUNIZATION) 364 .set("resourceType", JSONObject.NULL) 365 .toJson(); 366 MedicalResourceValidator validator = makeValidator(immunizationJson); 367 368 var thrown = 369 assertThrows( 370 IllegalArgumentException.class, 371 validator::validateAndCreateInternalRequest); 372 assertThat(thrown) 373 .hasMessageThat() 374 .contains( 375 "Unsupported FHIR resource type null for resource with id " 376 + FHIR_RESOURCE_ID_IMMUNIZATION); 377 } 378 379 @Test testValidateAndCreateInternalRequest_containedResourceInResource_throws()380 public void testValidateAndCreateInternalRequest_containedResourceInResource_throws() { 381 String resourceId = "id1"; 382 String medicationStatementWithContainedResource = 383 new MedicationsBuilder.MedicationStatementR4Builder() 384 .setId(resourceId) 385 .setContainedMedication(new MedicationsBuilder.MedicationBuilder()) 386 .toJson(); 387 MedicalResourceValidator validator = 388 makeValidator(medicationStatementWithContainedResource); 389 390 var thrown = 391 assertThrows( 392 IllegalArgumentException.class, 393 validator::validateAndCreateInternalRequest); 394 assertThat(thrown) 395 .hasMessageThat() 396 .contains( 397 "Contained resources are not supported. Found contained resource for" 398 + " resource with id " 399 + resourceId); 400 } 401 402 @Test testValidateAndCreateInternalRequest_containedFieldNotArray_throws()403 public void testValidateAndCreateInternalRequest_containedFieldNotArray_throws() 404 throws JSONException { 405 String resourceId = "id1"; 406 String medicationStatementWithInvalidContained = 407 new MedicationsBuilder.MedicationStatementR4Builder() 408 .setId(resourceId) 409 .set("contained", new JSONObject("{}")) 410 .toJson(); 411 MedicalResourceValidator validator = makeValidator(medicationStatementWithInvalidContained); 412 413 var thrown = 414 assertThrows( 415 IllegalArgumentException.class, 416 validator::validateAndCreateInternalRequest); 417 assertThat(thrown) 418 .hasMessageThat() 419 .contains( 420 "Contained resources are not supported. Found contained field for resource" 421 + " with id " 422 + resourceId); 423 } 424 425 @Test testCalculateMedicalResourceType_allergy()426 public void testCalculateMedicalResourceType_allergy() { 427 MedicalResourceValidator validator = makeValidator(FHIR_DATA_ALLERGY); 428 429 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 430 431 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_ALLERGIES_INTOLERANCES); 432 } 433 434 @Test testCalculateMedicalResourceType_condition()435 public void testCalculateMedicalResourceType_condition() { 436 String fhirData = new ConditionBuilder().toJson(); 437 MedicalResourceValidator validator = makeValidator(fhirData); 438 439 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 440 441 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_CONDITIONS); 442 } 443 444 @Test testCalculateMedicalResourceType_procedure()445 public void testCalculateMedicalResourceType_procedure() { 446 String fhirData = new ProcedureBuilder().toJson(); 447 MedicalResourceValidator validator = makeValidator(fhirData); 448 449 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 450 451 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_PROCEDURES); 452 } 453 454 @Test testCalculateMedicalResourceType_medication()455 public void testCalculateMedicalResourceType_medication() { 456 String fhirData = MedicationsBuilder.medication().toJson(); 457 MedicalResourceValidator validator = makeValidator(fhirData); 458 459 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 460 461 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_MEDICATIONS); 462 } 463 464 @Test testCalculateMedicalResourceType_medicationStatement()465 public void testCalculateMedicalResourceType_medicationStatement() { 466 String fhirData = MedicationsBuilder.statementR4().toJson(); 467 MedicalResourceValidator validator = makeValidator(fhirData); 468 469 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 470 471 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_MEDICATIONS); 472 } 473 474 @Test testCalculateMedicalResourceType_medicationRequest()475 public void testCalculateMedicalResourceType_medicationRequest() { 476 String fhirData = MedicationsBuilder.request().toJson(); 477 MedicalResourceValidator validator = makeValidator(fhirData); 478 479 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 480 481 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_MEDICATIONS); 482 } 483 484 @Test testCalculateMedicalResourceType_patient()485 public void testCalculateMedicalResourceType_patient() { 486 String fhirData = new PatientBuilder().toJson(); 487 MedicalResourceValidator validator = makeValidator(fhirData); 488 489 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 490 491 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_PERSONAL_DETAILS); 492 } 493 494 @Test testCalculateMedicalResourceType_practitioner()495 public void testCalculateMedicalResourceType_practitioner() { 496 String fhirData = new PractitionerBuilder().toJson(); 497 MedicalResourceValidator validator = makeValidator(fhirData); 498 499 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 500 501 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_PRACTITIONER_DETAILS); 502 } 503 504 @Test testCalculateMedicalResourceType_practitionerRole()505 public void testCalculateMedicalResourceType_practitionerRole() { 506 String fhirData = PractitionerBuilder.role().toJson(); 507 MedicalResourceValidator validator = makeValidator(fhirData); 508 509 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 510 511 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_PRACTITIONER_DETAILS); 512 } 513 514 @Test testCalculateMedicalResourceType_encounter()515 public void testCalculateMedicalResourceType_encounter() { 516 String fhirData = EncountersBuilder.encounter().toJson(); 517 MedicalResourceValidator validator = makeValidator(fhirData); 518 519 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 520 521 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_VISITS); 522 } 523 524 @Test testCalculateMedicalResourceType_location()525 public void testCalculateMedicalResourceType_location() { 526 String fhirData = EncountersBuilder.location().toJson(); 527 MedicalResourceValidator validator = makeValidator(fhirData); 528 529 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 530 531 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_VISITS); 532 } 533 534 @Test testCalculateMedicalResourceType_organization()535 public void testCalculateMedicalResourceType_organization() { 536 String fhirData = EncountersBuilder.organization().toJson(); 537 MedicalResourceValidator validator = makeValidator(fhirData); 538 539 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 540 541 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_VISITS); 542 } 543 544 // IPS artifacts: https://build.fhir.org/ig/HL7/fhir-ips/artifacts.html 545 // pregnancy outcome, status and expected delivery date. 546 enum PregnancyStatusTestValue { 547 PREGNANT("82810-3", "77386006"), // Pregnancy status, pregnant 548 NOT_PREGNANT("82810-3", "60001007"); // Pregnancy status, not pregnant 549 private final String mCode; 550 private final String mValue; 551 PregnancyStatusTestValue(String code, String value)552 PregnancyStatusTestValue(String code, String value) { 553 mCode = code; 554 mValue = value; 555 } 556 } 557 558 enum PregnancyOutcomeTestValue { 559 LIVE_BIRTH("11636-8", 1), 560 NO_LIVE_BIRTH("11636-8", 0), 561 PRETERM("11637-6", 1), 562 NO_PRETERM("11637-6", 0), 563 STILL_LIVING("11638-4", 1), 564 NO_STILL_LIVING("11638-4", 0), 565 TERM("11639-2", 1), 566 NO_TERM("11639-2", 0), 567 TOTAL("11640-0", 1), 568 NO_TOTAL("11640-0", 0), 569 ABORTIONS("11612-9", 1), 570 NO_ABORTIONS("11612-9", 0), 571 INDUCED_ABORTION("11613-7", 1), 572 NO_INDUCED_ABORTIONS("11613-7", 0), 573 SPONTANEOUS_ABORTION("11614-5", 1), 574 NO_SPONTANEOUS_ABORTIONS("11614-5", 0), 575 ECTOPIC_PREGNANCY("33065-4", 1), 576 NO_ECTOPIC_PREGNANCIES("33065-4", 0), 577 ; 578 private final String mCode; 579 private final int mCount; 580 PregnancyOutcomeTestValue(String code, int count)581 PregnancyOutcomeTestValue(String code, int count) { 582 mCode = code; 583 mCount = count; 584 } 585 } 586 587 enum SmokingTestValue { 588 FORMER_SMOKER_LOINC("72166-2", makeCodeableConcept(LOINC, "LA15920-4", "Former smoker")), 589 // https://build.fhir.org/ig/HL7/fhir-ips/ValueSet-current-smoking-status-uv-ips.html 590 DAILY( 591 "72166-2", 592 makeCodeableConcept(SNOMED_CT, "449868002", "Smokes tobacco daily (finding)")), 593 OCCASIONAL( 594 "72166-2", 595 makeCodeableConcept( 596 SNOMED_CT, "428041000124106", "Occasional tobacco smoker (finding)")), 597 EX_SMOKER("72166-2", makeCodeableConcept(SNOMED_CT, "8517006", "Ex-smoker (finding)")), 598 NEVER( 599 "72166-2", 600 makeCodeableConcept(SNOMED_CT, "266919005", "Never smoked tobacco (finding)")), 601 SMOKER("72166-2", makeCodeableConcept(SNOMED_CT, "77176002", "Smoker (finding)")), 602 UNKNOWN( 603 "72166-2", 604 makeCodeableConcept( 605 SNOMED_CT, "266927001", "Tobacco smoking consumption unknown (finding)")), 606 HEAVY( 607 "72166-2", 608 makeCodeableConcept(SNOMED_CT, "230063004", "Heavy cigarette Smoker (finding)")), 609 LIGHT( 610 "72166-2", 611 makeCodeableConcept(SNOMED_CT, "230060001", "Light cigarette Smoker (finding)")), 612 ; 613 private final String mCode; 614 private final JSONObject mValueCodeableConcept; 615 SmokingTestValue(String code, JSONObject value)616 SmokingTestValue(String code, JSONObject value) { 617 mCode = code; 618 mValueCodeableConcept = value; 619 } 620 } 621 622 enum VitalSignsTestValue { 623 // https://hl7.org/fhir/R5/observation-vitalsigns.html 624 SAT_O2("2708-6", 95, PERCENT), 625 RESPIRATORY_RATE("9279-1", 26, BREATHS_PER_MINUTE), 626 HEART_RATE("8867-4", 100, BEATS_PER_MINUTE), 627 BODY_TEMPERATURE("8867-4", 36.4, CELSIUS), 628 BODY_HEIGHT("8302-2", 152, CENTIMETERS), 629 HEAD_CIRCUMFERENCE("9843-4", 51.2, CENTIMETERS), 630 BODY_WEIGHT_LBS("29463-7", 185, POUNDS), 631 BODY_WEIGHT_KG("29463-7", 77, KILOGRAMS), 632 BODY_MASS_INDEX("39156-5", 16.2, KILOGRAMS_PER_M2), 633 SYSTOLIC_BLOOD_PRESSURE("8480-6", 26, MILLIMETERS_OF_MERCURY), 634 DIASTOLIC_BLOOD_PRESSURE("8462-4", 26, MILLIMETERS_OF_MERCURY), 635 ; 636 private final String mCode; 637 private final Number mValue; 638 private final QuantityUnits mUnits; 639 VitalSignsTestValue(String code, Number value, QuantityUnits units)640 VitalSignsTestValue(String code, Number value, QuantityUnits units) { 641 mCode = code; 642 mValue = value; 643 mUnits = units; 644 } 645 } 646 647 @Test testCalculateMedicalResourceType_pregnancyStatus_pregnancy( @estParameter PregnancyStatusTestValue testValue)648 public void testCalculateMedicalResourceType_pregnancyStatus_pregnancy( 649 @TestParameter PregnancyStatusTestValue testValue) { 650 String fhirData = 651 new ObservationBuilder() 652 .setCode(LOINC, testValue.mCode) 653 .setValueCodeableConcept(SNOMED_CT, testValue.mValue) 654 .toJson(); 655 MedicalResourceValidator validator = makeValidator(fhirData); 656 657 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 658 659 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_PREGNANCY); 660 } 661 662 @Test testCalculateMedicalResourceType_pregnancyOutcome_pregnancy( @estParameter PregnancyOutcomeTestValue testValue)663 public void testCalculateMedicalResourceType_pregnancyOutcome_pregnancy( 664 @TestParameter PregnancyOutcomeTestValue testValue) { 665 String fhirData = 666 new ObservationBuilder() 667 .setCode(LOINC, testValue.mCode) 668 .setValueQuantity(testValue.mCount, QuantityUnits.COUNT) 669 .toJson(); 670 MedicalResourceValidator validator = makeValidator(fhirData); 671 672 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 673 674 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_PREGNANCY); 675 } 676 677 @Test testCalculateMedicalResourceType_expectedDeliveryDate_pregnancy( @estParameter{"11778-8", "11779-6", "11780-4"}) String code)678 public void testCalculateMedicalResourceType_expectedDeliveryDate_pregnancy( 679 @TestParameter({"11778-8", "11779-6", "11780-4"}) String code) { 680 // https://build.fhir.org/ig/HL7/fhir-ips/ValueSet-edd-method-uv-ips.html 681 String fhirData = 682 new ObservationBuilder() 683 .setCode(LOINC, code) 684 .removeAllEffectiveMultiTypeFields() 685 .set("effectiveDateTime", "2021-04-20") 686 .removeAllValueMultiTypeFields() 687 .set("valueDateTime", "2021-08-07") 688 .toJson(); 689 MedicalResourceValidator validator = makeValidator(fhirData); 690 691 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 692 693 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_PREGNANCY); 694 } 695 696 @Test testCalculateMedicalResourceType_categorySocialHistory_socialHistory()697 public void testCalculateMedicalResourceType_categorySocialHistory_socialHistory() { 698 String fhirData = new ObservationBuilder().setCategory(SOCIAL_HISTORY).toJson(); 699 MedicalResourceValidator validator = makeValidator(fhirData); 700 701 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 702 703 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_SOCIAL_HISTORY); 704 } 705 706 @Test testCalculateMedicalResourceType_smoking_socialHistory( @estParameter SmokingTestValue value)707 public void testCalculateMedicalResourceType_smoking_socialHistory( 708 @TestParameter SmokingTestValue value) { 709 // https://build.fhir.org/ig/HL7/fhir-ips/StructureDefinition-Observation-tobaccouse-uv-ips.html 710 String fhirData = 711 new ObservationBuilder() 712 .setCode(LOINC, value.mCode) 713 .removeAllValueMultiTypeFields() 714 .set("valueCodeableConcept", value.mValueCodeableConcept) 715 .toJson(); 716 MedicalResourceValidator validator = makeValidator(fhirData); 717 718 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 719 720 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_SOCIAL_HISTORY); 721 } 722 723 @Test testCalculateMedicalResourceType_ipsAlcohol_socialHistory()724 public void testCalculateMedicalResourceType_ipsAlcohol_socialHistory() throws JSONException { 725 // https://build.fhir.org/ig/HL7/fhir-ips/StructureDefinition-Observation-alcoholuse-uv-ips.html 726 String fhirData = 727 new ObservationBuilder() 728 .setCode(LOINC, "74013-4") 729 .setValueQuantity(2, QuantityUnits.GLASSES_OF_WINE_PER_DAY) 730 .toJson(); 731 MedicalResourceValidator validator = makeValidator(fhirData); 732 733 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 734 735 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_SOCIAL_HISTORY); 736 } 737 738 @Test testCalculateMedicalResourceType_categoryVitals_vitalSigns()739 public void testCalculateMedicalResourceType_categoryVitals_vitalSigns() { 740 String fhirData = new ObservationBuilder().setCategory(VITAL_SIGNS).toJson(); 741 MedicalResourceValidator validator = makeValidator(fhirData); 742 743 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 744 745 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_VITAL_SIGNS); 746 } 747 748 @Test testCalculateMedicalResourceType_vitalSigns_vitalSigns( @estParameter VitalSignsTestValue value)749 public void testCalculateMedicalResourceType_vitalSigns_vitalSigns( 750 @TestParameter VitalSignsTestValue value) { 751 // From https://hl7.org/fhir/R5/observation-vitalsigns.html 752 String fhirData = 753 new ObservationBuilder() 754 .setCode(LOINC, value.mCode) 755 .setValueQuantity(value.mValue, value.mUnits) 756 .toJson(); 757 MedicalResourceValidator validator = makeValidator(fhirData); 758 759 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 760 761 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_VITAL_SIGNS); 762 } 763 764 @Test testCalculateMedicalResourceType_vitalSignsPanel_vitalSigns()765 public void testCalculateMedicalResourceType_vitalSignsPanel_vitalSigns() { 766 // From https://hl7.org/fhir/R5/observation-example-vitals-panel.json.html 767 String fhirData = 768 "{ \"resourceType\" : \"Observation\", \"id\" : \"vitals-panel\", \"meta\" : { " 769 + " \"profile\" : [\"http://hl7.org/fhir/StructureDefinition/vitalsigns\"] }," 770 + " \"status\" : \"final\"," 771 // category deliberately left out to check categorization by code 772 + " \"code\" : { \"coding\" : [{ \"system\" :" 773 + " \"http://loinc.org\", \"code\" : \"85353-1\", \"display\" :" 774 + " \"Vital signs, weight, height, head circumference, oxygen saturation" 775 + " and BMI panel\" }], \"text\" : \"Vital signs Panel\" }, " 776 + " \"subject\" : { \"reference\" : \"Patient/example\" }, " 777 + " \"effectiveDateTime\" : \"1999-07-02\", \"hasMember\" : [{ " 778 + " \"reference\" : \"Observation/respiratory-rate\", \"display\" :" 779 + " \"Respiratory Rate\" }, { \"reference\" :" 780 + " \"Observation/heart-rate\", \"display\" : \"Heart Rate\" }, { " 781 + " \"reference\" : \"Observation/blood-pressure\", \"display\" :" 782 + " \"Blood Pressure\" }, { \"reference\" :" 783 + " \"Observation/body-temperature\", \"display\" : \"Body Temperature\"" 784 + " }]}"; 785 MedicalResourceValidator validator = makeValidator(fhirData); 786 787 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 788 789 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_VITAL_SIGNS); 790 } 791 792 @Test testCalculateMedicalResourceType_bloodPressurePanel_vitalSigns()793 public void testCalculateMedicalResourceType_bloodPressurePanel_vitalSigns() { 794 // From https://hl7.org/fhir/R5/observation-example-bloodpressure.json.html 795 String fhirData = 796 "{ \"resourceType\" : \"Observation\", \"id\" : \"blood-pressure\", \"meta\" : {" 797 + " \"profile\" : [\"http://hl7.org/fhir/StructureDefinition/vitalsigns\"] " 798 + " }, \"identifier\" : [{ \"system\" : \"urn:ietf:rfc:3986\", \"value\"" 799 + " : \"urn:uuid:187e0c12-8dd2-67e2-99b2-bf273c878281\" }], \"basedOn\" : [{ " 800 + " \"identifier\" : { \"system\" : \"https://acme.org/identifiers\", " 801 + " \"value\" : \"1234\" } }], \"status\" : \"final\"," 802 // category deliberately left out to test categorization by code 803 + " \"code\" : { \"coding\" : [{ \"system\" :" 804 + " \"http://loinc.org\", \"code\" : \"85354-9\", \"display\" :" 805 + " \"Blood pressure panel with all children optional\" }], \"text\"" 806 + " : \"Blood pressure systolic & diastolic\" }, \"subject\" : { " 807 + " \"reference\" : \"Patient/example\" }, \"effectiveDateTime\" :" 808 + " \"2012-09-17\", \"performer\" : [{ \"reference\" :" 809 + " \"Practitioner/example\" }], \"interpretation\" : [{ \"coding\" :" 810 + " [{ \"system\" :" 811 + " \"http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation\"," 812 + " \"code\" : \"L\", \"display\" : \"low\" }], \"text\" :" 813 + " \"Below low normal\" }], \"bodySite\" : { \"coding\" : [{ " 814 + " \"system\" : \"http://snomed.info/sct\", \"code\" : \"368209003\"," 815 + " \"display\" : \"Right arm\" }] }, \"component\" : [{ " 816 + " \"code\" : { \"coding\" : [{ \"system\" :" 817 + " \"http://loinc.org\", \"code\" : \"8480-6\", \"display\"" 818 + " : \"Systolic blood pressure\" }, { \"system\" :" 819 + " \"http://snomed.info/sct\", \"code\" : \"271649006\", " 820 + " \"display\" : \"Systolic blood pressure\" }, { " 821 + " \"system\" : \"http://acme.org/devices/clinical-codes\", " 822 + " \"code\" : \"bp-s\", \"display\" : \"Systolic Blood pressure\" " 823 + " }] }, \"valueQuantity\" : { \"value\" : 107, " 824 + " \"unit\" : \"mmHg\", \"system\" : \"http://unitsofmeasure.org\", " 825 + " \"code\" : \"mm[Hg]\" }, \"interpretation\" : [{ " 826 + " \"coding\" : [{ \"system\" :" 827 + " \"http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation\"," 828 + " \"code\" : \"N\", \"display\" : \"normal\" }], " 829 + " \"text\" : \"Normal\" }] }, { \"code\" : { \"coding\" : [{" 830 + " \"system\" : \"http://loinc.org\", \"code\" : \"8462-4\"," 831 + " \"display\" : \"Diastolic blood pressure\" }] }, " 832 + " \"valueQuantity\" : { \"value\" : 60, \"unit\" : \"mmHg\", " 833 + " \"system\" : \"http://unitsofmeasure.org\", \"code\" :" 834 + " \"mm[Hg]\" }, \"interpretation\" : [{ \"coding\" : [{ " 835 + " \"system\" :" 836 + " \"http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation\"," 837 + " \"code\" : \"L\", \"display\" : \"low\" }], " 838 + " \"text\" : \"Below low normal\" }] }]}"; 839 MedicalResourceValidator validator = makeValidator(fhirData); 840 841 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 842 843 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_VITAL_SIGNS); 844 } 845 846 @Test testCalculateMedicalResourceType_categoryLaboratory_laboratoryResults()847 public void testCalculateMedicalResourceType_categoryLaboratory_laboratoryResults() { 848 String fhirData = new ObservationBuilder().setCategory(LABORATORY).toJson(); 849 MedicalResourceValidator validator = makeValidator(fhirData); 850 851 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 852 853 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_LABORATORY_RESULTS); 854 } 855 856 @Test testPregancyHigherPriorityThanSocialHistory()857 public void testPregancyHigherPriorityThanSocialHistory() { 858 PregnancyStatusTestValue status = PregnancyStatusTestValue.PREGNANT; 859 String fhirData = 860 new ObservationBuilder() 861 .setCode(LOINC, status.mCode) 862 .setValueCodeableConcept(SNOMED_CT, status.mValue) 863 .setCategory(SOCIAL_HISTORY) 864 .toJson(); 865 MedicalResourceValidator validator = makeValidator(fhirData); 866 867 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 868 869 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_PREGNANCY); 870 } 871 872 @Test testSocialHistoryHigherPriorityThanVitalSigns()873 public void testSocialHistoryHigherPriorityThanVitalSigns() throws JSONException { 874 JSONArray categories = 875 new JSONArray( 876 List.of( 877 VITAL_SIGNS.toFhirCodeableConcept(), 878 SOCIAL_HISTORY.toFhirCodeableConcept())); 879 String fhirData = new ObservationBuilder().set("category", categories).toJson(); 880 MedicalResourceValidator validator = makeValidator(fhirData); 881 882 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 883 884 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_SOCIAL_HISTORY); 885 } 886 887 @Test testVitalSignsHigherPriorityThanLabResults()888 public void testVitalSignsHigherPriorityThanLabResults() throws JSONException { 889 JSONArray categories = 890 new JSONArray( 891 List.of( 892 LABORATORY.toFhirCodeableConcept(), 893 VITAL_SIGNS.toFhirCodeableConcept())); 894 String fhirData = new ObservationBuilder().set("category", categories).toJson(); 895 MedicalResourceValidator validator = makeValidator(fhirData); 896 897 int type = validator.validateAndCreateInternalRequest().getMedicalResourceType(); 898 899 assertThat(type).isEqualTo(MedicalResource.MEDICAL_RESOURCE_TYPE_VITAL_SIGNS); 900 } 901 makeValidator(String fhirData)902 private MedicalResourceValidator makeValidator(String fhirData) { 903 UpsertMedicalResourceRequest request = 904 new UpsertMedicalResourceRequest.Builder(DATA_SOURCE_ID, FHIR_VERSION_R4, fhirData) 905 .build(); 906 return new MedicalResourceValidator(request, getFhirResourceValidator()); 907 } 908 getFhirResourceValidator()909 private FhirResourceValidator getFhirResourceValidator() { 910 if (mFhirResourceValidator == null) { 911 mFhirResourceValidator = new FhirResourceValidator(); 912 } 913 return mFhirResourceValidator; 914 } 915 } 916