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  }