added test to verify that wizard resource creation works correctly.

update fasten-sources (contains fix for silently ignoring failures during wizard resource creation).
fixed file upload error in wizard.
Fixed data structure error in wizard for DocumentReference.

fixes #393
This commit is contained in:
Jason Kulatunga 2024-01-26 22:01:34 -08:00
parent 1b3e43556e
commit 870f6ffee2
No known key found for this signature in database
10 changed files with 228 additions and 16 deletions

View File

@ -8,7 +8,7 @@ import (
"net/http"
)
// SECURITY: this is a secure endpoint, and should only be called after a double confirmation
// UX: this is a secure endpoint, and should only be called after a double confirmation
func DeleteAccount(c *gin.Context) {
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)

View File

@ -0,0 +1,143 @@
package handler
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg"
mock_config "github.com/fastenhealth/fasten-onprem/backend/pkg/config/mock"
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/gin-gonic/gin"
"github.com/golang/mock/gomock"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"testing"
)
// Go through this page to understand how this file is structured.
// https://pkg.go.dev/github.com/stretchr/testify/suite#section-documentation
// Define the suite, and absorb the built-in basic suite
// functionality from testify - including a T() method which
// returns the current testing context
type ResourceRelatedHandlerTestSuite struct {
suite.Suite
MockCtrl *gomock.Controller
TestDatabase *os.File
AppConfig *mock_config.MockInterface
AppRepository database.DatabaseRepository
AppEventBus event_bus.Interface
}
// BeforeTest has a function to be executed right before the test starts and receives the suite and test names as input
func (suite *ResourceRelatedHandlerTestSuite) BeforeTest(suiteName, testName string) {
suite.MockCtrl = gomock.NewController(suite.T())
dbFile, err := ioutil.TempFile("", fmt.Sprintf("%s.*.db", testName))
if err != nil {
log.Fatal(err)
}
suite.TestDatabase = dbFile
appConfig := mock_config.NewMockInterface(suite.MockCtrl)
appConfig.EXPECT().GetString("database.location").Return(suite.TestDatabase.Name()).AnyTimes()
appConfig.EXPECT().GetString("database.type").Return("sqlite").AnyTimes()
appConfig.EXPECT().IsSet("database.encryption.key").Return(false).AnyTimes()
appConfig.EXPECT().GetString("log.level").Return("INFO").AnyTimes()
suite.AppConfig = appConfig
appRepo, err := database.NewRepository(suite.AppConfig, logrus.WithField("test", suite.T().Name()), event_bus.NewNoopEventBusServer())
suite.AppRepository = appRepo
suite.AppEventBus = event_bus.NewNoopEventBusServer()
appRepo.CreateUser(context.Background(), &models.User{
Username: "test_username",
Password: "test",
})
}
// AfterTest has a function to be executed right after the test finishes and receives the suite and test names as input
func (suite *ResourceRelatedHandlerTestSuite) AfterTest(suiteName, testName string) {
suite.MockCtrl.Finish()
os.Remove(suite.TestDatabase.Name())
}
// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestResourceRelatedHandlerTestSuite(t *testing.T) {
suite.Run(t, new(ResourceRelatedHandlerTestSuite))
}
func (suite *ResourceRelatedHandlerTestSuite) TestResourceRelatedHandlerTestSuite() {
//setup
w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Set(pkg.ContextKeyTypeLogger, logrus.WithField("test", suite.T().Name()))
ctx.Set(pkg.ContextKeyTypeDatabase, suite.AppRepository)
ctx.Set(pkg.ContextKeyTypeConfig, suite.AppConfig)
ctx.Set(pkg.ContextKeyTypeEventBusServer, suite.AppEventBus)
ctx.Set(pkg.ContextKeyTypeAuthUsername, "test_username")
//test
relatedJsonForm, relatedJsonWriter := createMultipartFormData(suite.T(), "file", "testdata/related.json") // just pass the file name
relatedReq, err := http.NewRequest("POST", "/api/v1/resource/related", &relatedJsonForm)
// set the content type, this will contain the boundary.
relatedReq.Header.Set("Content-Type", relatedJsonWriter.FormDataContentType())
ctx.Request = relatedReq
require.NoError(suite.T(), err)
CreateRelatedResources(ctx)
var responseWrapper models.ResponseWrapper
err = json.Unmarshal(w.Body.Bytes(), &responseWrapper)
require.NoError(suite.T(), err)
summary := responseWrapper.Data.(map[string]interface{})
//assert
assert.EqualValues(suite.T(), http.StatusOK, w.Code)
assert.Equal(suite.T(), summary["TotalResources"], 3)
}
// https://stackoverflow.com/a/56696333/1157633
func createMultipartFormData(t *testing.T, fieldName, fileName string) (bytes.Buffer, *multipart.Writer) {
var b bytes.Buffer
var err error
w := multipart.NewWriter(&b)
var fw io.Writer
file := mustOpen(fileName)
if fw, err = w.CreateFormFile(fieldName, file.Name()); err != nil {
t.Errorf("Error creating writer: %v", err)
}
if _, err = io.Copy(fw, file); err != nil {
t.Errorf("Error with io.Copy: %v", err)
}
w.Close()
return b, w
}
func mustOpen(f string) *os.File {
r, err := os.Open(f)
if err != nil {
pwd, _ := os.Getwd()
fmt.Println("PWD: ", pwd)
panic(err)
}
return r
}

View File

@ -0,0 +1,69 @@
{
"resourceType": "List",
"entry": [],
"encounter": {
"reference": "Encounter/4e4e6346-2350-47c0-ba73-ee0fdaf3a8a7"
},
"contained": [
{
"resourceType": "Encounter",
"id": "4e4e6346-2350-47c0-ba73-ee0fdaf3a8a7",
"serviceType": {
"id": "562",
"identifier": [
{
"system": "http://terminology.hl7.org/CodeSystem/service-type",
"code": "562",
"display": "Test Message"
}
],
"text": "Test Message"
},
"status": "finished",
"participant": [],
"period": {
"start": "2024-01-04T08:00:00.000Z",
"end": "2024-01-27T08:00:00.000Z"
},
"reasonReference": [],
"serviceProvider": {}
},
{
"id": "4e2611e4-df0d-4dae-ad32-1c90537b32af",
"resourceType": "Binary",
"contentType": "application/pdf",
"data": "JVBERi0xLjcKCjEgMCBvYmogICUgZW50cnkgcG9pbnQKPDwKICAvVHlwZSAvQ2F0YWxvZwogIC9QYWdlcyAyIDAgUgo+PgplbmRvYmoKCjIgMCBvYmoKPDwKICAvVHlwZSAvUGFnZXMKICAvTWVkaWFCb3ggWyAwIDAgMjAwIDIwMCBdCiAgL0NvdW50IDEKICAvS2lkcyBbIDMgMCBSIF0KPj4KZW5kb2JqCgozIDAgb2JqCjw8CiAgL1R5cGUgL1BhZ2UKICAvUGFyZW50IDIgMCBSCiAgL1Jlc291cmNlcyA8PAogICAgL0ZvbnQgPDwKICAgICAgL0YxIDQgMCBSIAogICAgPj4KICA+PgogIC9Db250ZW50cyA1IDAgUgo+PgplbmRvYmoKCjQgMCBvYmoKPDwKICAvVHlwZSAvRm9udAogIC9TdWJ0eXBlIC9UeXBlMQogIC9CYXNlRm9udCAvVGltZXMtUm9tYW4KPj4KZW5kb2JqCgo1IDAgb2JqICAlIHBhZ2UgY29udGVudAo8PAogIC9MZW5ndGggNDQKPj4Kc3RyZWFtCkJUCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8sIHdvcmxkISkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iagoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDEwIDAwMDAwIG4gCjAwMDAwMDAwNzkgMDAwMDAgbiAKMDAwMDAwMDE3MyAwMDAwMCBuIAowMDAwMDAwMzAxIDAwMDAwIG4gCjAwMDAwMDAzODAgMDAwMDAgbiAKdHJhaWxlcgo8PAogIC9TaXplIDYKICAvUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNDkyCiUlRU9G"
},
{
"id": "53066674-9e89-41e9-95d9-8eaf24b41ea6",
"resourceType": "DocumentReference",
"status": "current",
"category": [
{
"coding": [
{
"system": "http://loinc.org",
"code": "11488-4",
"display": "Consult Note"
}
],
"text": "Consult Note"
}
],
"content": [
{
"attachment": {
"contentType": "application/pdf",
"url": "urn:uuid:4e2611e4-df0d-4dae-ad32-1c90537b32af",
"title": "test attachment"
}
}
],
"context": {
"encounter": [{
"reference": "Encounter/4e4e6346-2350-47c0-ba73-ee0fdaf3a8a7"
}]
}
}
]
}

View File

@ -30,7 +30,7 @@
</div><!-- col -->
<div class="col-6 mg-t-10 mg-lg-t-0 mg-l-10">
<label for="customFile" class="custom-file-label">{{newAttachmentForm.get('file_name').value || 'Choose file'}}</label>
<input id="customFile" (change)="onAttachmentFileChange($event)" class="custom-file-input" formControlName="file_name" type="file">
<input id="customFile" (change)="onAttachmentFileChange($event)" class="custom-file-input" type="file">
</div><!-- col -->
</ng-container>
</div>

View File

@ -41,15 +41,16 @@ export class MedicalRecordWizardAddAttachmentComponent implements OnInit {
console.log("onAttachmentFileChange")
let fileInput = $event.target as HTMLInputElement;
if (fileInput.files && fileInput.files[0]) {
let processingFile = fileInput.files[0]
let reader = new FileReader();
reader.onloadend = () => {
// use a regex to remove data url part
const base64String = (reader.result as string).replace('data:', '').replace(/^.+,/, '');
this.newAttachmentForm.get('file_content').setValue(base64String)
};
reader.readAsDataURL(fileInput.files[0]);
this.newAttachmentForm.get('file_name').setValue(fileInput.files[0].name)
this.newAttachmentForm.get('file_size').setValue(fileInput.files[0].size)
reader.readAsDataURL(processingFile);
this.newAttachmentForm.get('file_name').setValue(processingFile.name)
this.newAttachmentForm.get('file_size').setValue(processingFile.size)
}
}

View File

@ -371,13 +371,12 @@ function resourceAttachmentToR4DocumentReference(resourceStorage: ResourceStorag
}
}
],
context: [
{
encounter: {
reference: generateReferenceUriFromResourceOrReference(encounterResource) //Encounter
},
}
]
context: {
encounter: [{
reference: generateReferenceUriFromResourceOrReference(encounterResource) //Encounter
}],
}
// date: `${new Date(resourceDocumentReference.date.year,resourceDocumentReference.date.month-1,resourceDocumentReference.date.day).toISOString()}`,
} as DocumentReference
resourceStorage['DocumentReference'][documentReferenceResource.id] = documentReferenceResource

View File

@ -19,7 +19,7 @@
<br/>
<br/>
In <strong>Sandbox mode</strong> Fasten Health cannot access real patient information. You must use Sandbox credentials when authenticating to healthcare sources.
<a href="https://github.com/fastenhealth/docs/blob/main/BETA.md#connecting-a-new-source" externalLink>These credentials are available on Github</a>
<a href="https://docs.fastenhealth.com/getting-started/sandbox.html#connecting-a-new-source" externalLink>These credentials are available in our Sandbox Guide</a>
</span>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

2
go.mod
View File

@ -15,7 +15,7 @@ require (
github.com/dave/jennifer v1.6.1
github.com/dominikbraun/graph v0.15.0
github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3
github.com/fastenhealth/fasten-sources v0.5.5
github.com/fastenhealth/fasten-sources v0.5.6
github.com/fastenhealth/gofhir-models v0.0.6
github.com/gin-gonic/gin v1.9.0
github.com/go-gormigrate/gormigrate/v2 v2.1.1

4
go.sum
View File

@ -101,8 +101,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fastenhealth/fasten-sources v0.5.5 h1:+vOIG7R/J7QxEZrrfPFfaBzqYU5eDu7x/bc/cgOjvW4=
github.com/fastenhealth/fasten-sources v0.5.5/go.mod h1:hUQATAu5KrxKbACJoVt4iEKIGnRtmiOmHz+5TLfyiCM=
github.com/fastenhealth/fasten-sources v0.5.6 h1:F4Qmw9ABLSkqkWncoSnChBRDVWLrzkJv+z4z/Ue/fdc=
github.com/fastenhealth/fasten-sources v0.5.6/go.mod h1:hUQATAu5KrxKbACJoVt4iEKIGnRtmiOmHz+5TLfyiCM=
github.com/fastenhealth/gofhir-models v0.0.6 h1:yJYYaV1eJtHiGEfA1rXLsyOm/9hIi6s2cGoZzGfW1tM=
github.com/fastenhealth/gofhir-models v0.0.6/go.mod h1:xB8ikGxu3bUq2b1JYV+CZpHqBaLXpOizFR0eFBCunis=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=