github.com/apache/beam/sdks/v2@v2.48.2/go/test/integration/io/fhirio/fhirio_test.go (about) 1 // Licensed to the Apache Software Foundation (ASF) under one or more 2 // contributor license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright ownership. 4 // The ASF licenses this file to You under the Apache License, Version 2.0 5 // (the "License"); you may not use this file except in compliance with 6 // the License. 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 package fhirio 17 18 import ( 19 "context" 20 "crypto/rand" 21 "encoding/json" 22 "errors" 23 "flag" 24 "fmt" 25 "math/big" 26 "net/http" 27 "os" 28 "strconv" 29 "strings" 30 "testing" 31 "time" 32 33 "github.com/apache/beam/sdks/v2/go/pkg/beam" 34 "github.com/apache/beam/sdks/v2/go/pkg/beam/io/fhirio" 35 "github.com/apache/beam/sdks/v2/go/pkg/beam/options/gcpopts" 36 _ "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/dataflow" 37 "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/passert" 38 "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/ptest" 39 "github.com/apache/beam/sdks/v2/go/test/integration" 40 "google.golang.org/api/healthcare/v1" 41 "google.golang.org/api/option" 42 ) 43 44 const ( 45 datasetPathFmt = "projects/%s/locations/%s/datasets/apache-beam-integration-testing" 46 tempStoragePath = "gs://temp-storage-for-end-to-end-tests" 47 testDataDir = "../../../../data/fhir_bundles/" 48 ) 49 50 var ( 51 storeService *healthcare.ProjectsLocationsDatasetsFhirStoresFhirService 52 storeManagementService *healthcare.ProjectsLocationsDatasetsFhirStoresService 53 ) 54 55 type fhirStoreInfo struct { 56 path string 57 resourcesPaths [][]byte 58 } 59 60 func checkFlags(t *testing.T) { 61 gcpProjectIsNotSet := gcpopts.Project == nil || *gcpopts.Project == "" 62 if gcpProjectIsNotSet { 63 t.Skip("GCP project flag is not set.") 64 } 65 gcpRegionIsNotSet := gcpopts.Region == nil || *gcpopts.Region == "" 66 if gcpRegionIsNotSet { 67 t.Skip("GCP region flag is not set.") 68 } 69 } 70 71 func setupFhirStoreWithData(t *testing.T) (fhirStoreInfo, func()) { 72 return setupFhirStore(t, true) 73 } 74 75 func setupEmptyFhirStore(t *testing.T) (string, func()) { 76 storeInfo, teardown := setupFhirStore(t, false) 77 return storeInfo.path, teardown 78 } 79 80 // Sets up a test fhir store by creating and populating data to it for testing 81 // purposes. It returns the name of the created store path, a slice of the 82 // resource paths to be used in tests, and a function to teardown what has been 83 // set up. 84 func setupFhirStore(t *testing.T, shouldPopulateStore bool) (fhirStoreInfo, func()) { 85 t.Helper() 86 if storeService == nil || storeManagementService == nil { 87 t.Fatal("Healthcare Services were not initialized") 88 } 89 90 healthcareDataset := fmt.Sprintf(datasetPathFmt, *gcpopts.Project, *gcpopts.Region) 91 createdFhirStore, err := createStore(healthcareDataset) 92 if err != nil { 93 t.Fatalf("Test store failed to be created. Reason: %v", err.Error()) 94 } 95 createdFhirStorePath := createdFhirStore.Name 96 97 var resourcePaths [][]byte 98 if shouldPopulateStore { 99 resourcePaths = populateStore(createdFhirStorePath) 100 if len(resourcePaths) == 0 { 101 t.Fatal("No data got populated to test") 102 } 103 } 104 105 return fhirStoreInfo{ 106 path: createdFhirStorePath, 107 resourcesPaths: resourcePaths, 108 }, func() { 109 _, _ = deleteStore(createdFhirStorePath) 110 } 111 } 112 113 func createStore(dataset string) (*healthcare.FhirStore, error) { 114 randInt, _ := rand.Int(rand.Reader, big.NewInt(32)) 115 testFhirStoreID := "FHIR_store_write_it_" + strconv.FormatInt(time.Now().UnixMilli(), 10) + "_" + randInt.String() 116 fhirStore := &healthcare.FhirStore{ 117 DisableReferentialIntegrity: true, 118 EnableUpdateCreate: true, 119 Version: "R4", 120 } 121 return storeManagementService.Create(dataset, fhirStore).FhirStoreId(testFhirStoreID).Do() 122 } 123 124 func deleteStore(storePath string) (*healthcare.Empty, error) { 125 return storeManagementService.Delete(storePath).Do() 126 } 127 128 // Populates fhir store with data. Note that failure to populate some data is not 129 // detrimental to the tests, so it is fine to ignore. 130 func populateStore(storePath string) [][]byte { 131 resourcePaths := make([][]byte, 0) 132 for _, bundle := range readPrettyBundles() { 133 response, err := storeService.ExecuteBundle(storePath, strings.NewReader(bundle)).Do() 134 if err != nil { 135 continue 136 } 137 138 var body struct { 139 Entry []struct { 140 Response struct { 141 Location string `json:"location"` 142 Status string `json:"status"` 143 } `json:"response"` 144 } `json:"entry"` 145 } 146 err = json.NewDecoder(response.Body).Decode(&body) 147 if err != nil { 148 continue 149 } 150 151 for _, entry := range body.Entry { 152 bundleFailedToBeCreated := !strings.Contains(entry.Response.Status, "201") 153 if bundleFailedToBeCreated { 154 continue 155 } 156 157 resourcePath, err := extractResourcePathFrom(entry.Response.Location) 158 if err != nil { 159 continue 160 } 161 resourcePaths = append(resourcePaths, resourcePath) 162 } 163 } 164 return resourcePaths 165 } 166 167 func readPrettyBundles() []string { 168 files, _ := os.ReadDir(testDataDir) 169 bundles := make([]string, len(files)) 170 for i, file := range files { 171 bundle, _ := os.ReadFile(testDataDir + file.Name()) 172 bundles[i] = string(bundle) 173 } 174 return bundles 175 } 176 177 func extractResourcePathFrom(resourceLocationURL string) ([]byte, error) { 178 // The resource location url is in the following format: 179 // https://healthcare.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/datasets/DATASET_ID/fhirStores/STORE_ID/fhir/RESOURCE_NAME/RESOURCE_ID/_history/HISTORY_ID 180 // But the API calls use this format: projects/PROJECT_ID/locations/LOCATION/datasets/DATASET_ID/fhirStores/STORE_ID/fhir/RESOURCE_NAME/RESOURCE_ID 181 startIdx := strings.Index(resourceLocationURL, "projects/") 182 endIdx := strings.Index(resourceLocationURL, "/_history") 183 if startIdx == -1 || endIdx == -1 { 184 return nil, errors.New("resource location url is invalid") 185 } 186 return []byte(resourceLocationURL[startIdx:endIdx]), nil 187 } 188 189 func readTestTask(t *testing.T, s beam.Scope, testStoreInfo fhirStoreInfo) func() { 190 t.Helper() 191 192 s = s.Scope("fhirio_test.readTestTask") 193 testResources := append(testStoreInfo.resourcesPaths, []byte(testStoreInfo.path+"/fhir/Patient/invalid")) 194 resourcePathsPCollection := beam.CreateList(s, testResources) 195 resources, failedReads := fhirio.Read(s, resourcePathsPCollection) 196 passert.Count(s, resources, "", len(testStoreInfo.resourcesPaths)) 197 passert.Count(s, failedReads, "", 1) 198 return nil 199 } 200 201 func executeBundlesTestTask(t *testing.T, s beam.Scope, _ fhirStoreInfo) func() { 202 t.Helper() 203 204 s = s.Scope("fhirio_test.executeBundlesTestTask") 205 fhirStorePath, teardownFhirStore := setupEmptyFhirStore(t) 206 bundlesPCollection := beam.CreateList(s, readPrettyBundles()) 207 successBodies, failures := fhirio.ExecuteBundles(s, fhirStorePath, bundlesPCollection) 208 passert.Count(s, successBodies, "", 2) 209 passert.Count(s, failures, "", 2) 210 passert.True(s, failures, func(errorMsg string) bool { 211 return strings.Contains(errorMsg, strconv.Itoa(http.StatusBadRequest)) 212 }) 213 return teardownFhirStore 214 } 215 216 func searchTestTask(t *testing.T, s beam.Scope, testStoreInfo fhirStoreInfo) func() { 217 t.Helper() 218 219 s = s.Scope("fhirio_test.searchTestTask") 220 searchQueries := []fhirio.SearchQuery{ 221 {}, 222 {ResourceType: "Patient"}, 223 {ResourceType: "Patient", Parameters: map[string]string{"gender": "female", "family:contains": "Smith"}}, 224 {ResourceType: "Encounter"}, 225 } 226 searchQueriesCol := beam.CreateList(s, searchQueries) 227 searchResult, deadLetter := fhirio.Search(s, testStoreInfo.path, searchQueriesCol) 228 passert.Empty(s, deadLetter) 229 passert.Count(s, searchResult, "", len(searchQueries)) 230 231 resourcesFoundCount := beam.ParDo(s, func(identifier string, resourcesFound []string) int { 232 return len(resourcesFound) 233 }, searchResult) 234 passert.Equals(s, resourcesFoundCount, 4, 2, 1, 0) 235 return nil 236 } 237 238 func deidentifyTestTask(t *testing.T, s beam.Scope, testStoreInfo fhirStoreInfo) func() { 239 t.Helper() 240 241 s = s.Scope("fhirio_test.deidentifyTestTask") 242 dstFhirStorePath, teardownDstFhirStore := setupEmptyFhirStore(t) 243 res := fhirio.Deidentify(s, testStoreInfo.path, dstFhirStorePath, &healthcare.DeidentifyConfig{}) 244 passert.Count(s, res, "", 1) 245 return teardownDstFhirStore 246 } 247 248 func importTestTask(t *testing.T, s beam.Scope, _ fhirStoreInfo) func() { 249 t.Helper() 250 251 s = s.Scope("fhirio_test.importTestTask") 252 253 fhirStorePath, teardownFhirStore := setupEmptyFhirStore(t) 254 255 patientTestResource := `{"resourceType":"Patient","id":"c1q34623-b02c-3f8b-92ea-873fc4db60da","name":[{"use":"official","family":"Smith","given":["Alice"]}],"gender":"female","birthDate":"1970-01-01"}` 256 practitionerTestResource := `{"resourceType":"Practitioner","id":"b0e04623-b02c-3f8b-92ea-943fc4db60da","name":[{"family":"Tillman293","given":["Franklin857"],"prefix":["Dr."]}],"address":[{"line":["295 VARNUM AVENUE"],"city":"LOWELL","state":"MA","postalCode":"01854","country":"US"}],"gender":"male"}` 257 testResources := beam.Create(s, patientTestResource, practitionerTestResource) 258 259 failedResources, deadLetter := fhirio.Import(s, fhirStorePath, tempStoragePath, tempStoragePath, fhirio.ContentStructureResource, testResources) 260 passert.Empty(s, failedResources) 261 passert.Empty(s, deadLetter) 262 263 return teardownFhirStore 264 } 265 266 func TestFhirIO(t *testing.T) { 267 integration.CheckFilters(t) 268 checkFlags(t) 269 270 testStoreInfo, teardownFhirStore := setupFhirStoreWithData(t) 271 defer teardownFhirStore() 272 273 p, s := beam.NewPipelineWithRoot() 274 275 type testTask func(*testing.T, beam.Scope, fhirStoreInfo) func() 276 testTasks := []testTask{ 277 readTestTask, 278 executeBundlesTestTask, 279 searchTestTask, 280 deidentifyTestTask, 281 importTestTask, 282 } 283 teardownTasks := make([]func(), len(testTasks)) 284 for i, testTaskCallable := range testTasks { 285 teardownTasks[i] = testTaskCallable(t, s, testStoreInfo) 286 } 287 288 defer func() { 289 for _, teardown := range teardownTasks { 290 if teardown != nil { 291 teardown() 292 } 293 } 294 }() 295 296 ptest.RunAndValidate(t, p) 297 } 298 299 func TestMain(m *testing.M) { 300 flag.Parse() 301 beam.Init() 302 303 healthcareService, err := healthcare.NewService(context.Background(), option.WithUserAgent(fhirio.UserAgent)) 304 if err == nil { 305 storeService = healthcare.NewProjectsLocationsDatasetsFhirStoresFhirService(healthcareService) 306 storeManagementService = healthcare.NewProjectsLocationsDatasetsFhirStoresService(healthcareService) 307 } 308 309 ptest.MainRet(m) 310 }