Files
stack/dev/vendor/bcda-app/bcda/api/v3/api_test.go
kert 7e53d9360a chore(bcda): vendor CMSgov/bcda-app + bcda.cms.gov v3 artefacts sparsely, flatten the v3 data dictionary into crosswalk seeds, weekly drift workflow
dev/vendor/MANIFEST.toml pins bcda-app@7decdcc (v3 handler + tests, request
validation middleware, OperationOutcome writer, v3 Postman collection, gzip
fixtures, sandbox config, LICENSE) and the bcda.cms.gov v3 data dictionary,
v2 dictionary and official v3 sample ndjson. vendor_bcda_app.py --verify |
--check | --sync (stdlib only); the bcda-vendor workflow runs --check weekly
and files an issue on drift — nothing auto-commits, a payload change upstream
is something a human reads before the flattener is re-pointed.

gen_bcda_v3_crosswalk.py writes dev/seeds/bcda_v3_data_dictionary.csv
(fhirPath, IDR sourceColumn, ccwMapping, cclfMapping per v3 field),
bcda_v3_cclf_crosswalk.csv (293 CCLF elements -> v3 field) and
bcda_v3_cclf_absent.csv (52 CCLF fields v3 does not carry); the CCLF sheet
has label/name swapped on data rows and is normalised. Tests assert the
committed seeds match the vendored workbook.

refs #1139
2026-10-25 13:08:05 -04:00

684 lines
25 KiB
Go

package v3
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
"github.com/CMSgov/bcda-app/bcda/api"
"github.com/CMSgov/bcda-app/bcda/auth"
"github.com/CMSgov/bcda-app/bcda/client"
"github.com/CMSgov/bcda-app/bcda/constants"
"github.com/CMSgov/bcda-app/bcda/database"
"github.com/CMSgov/bcda-app/bcda/fhir/r4"
"github.com/CMSgov/bcda-app/bcda/models"
"github.com/CMSgov/bcda-app/bcda/models/postgres/postgrestest"
"github.com/CMSgov/bcda-app/bcda/service"
"github.com/CMSgov/bcda-app/bcda/testUtils"
"github.com/CMSgov/bcda-app/bcda/utils"
"github.com/CMSgov/bcda-app/bcda/web/middleware"
"github.com/CMSgov/bcda-app/conf"
"github.com/CMSgov/bcda-app/log"
appMiddleware "github.com/CMSgov/bcda-app/middleware"
"github.com/go-chi/chi/v5"
pgxv5Pool "github.com/jackc/pgx/v5/pgxpool"
"github.com/pborman/uuid"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
const (
expiryHeaderFormat = "2006-01-02 15:04:05.999999999 -0700 MST"
)
var (
acoUnderTest = uuid.Parse(constants.LargeACOUUID)
)
type APITestSuite struct {
suite.Suite
db *sql.DB
pool *pgxv5Pool.Pool
apiV3 *ApiV3
}
func (s *APITestSuite) SetupSuite() {
s.db = database.Connect()
s.pool = database.ConnectPool()
s.apiV3 = NewApiV3(s.db, s.pool)
origDate := conf.GetEnv("CCLF_REF_DATE")
conf.SetEnv(s.T(), "CCLF_REF_DATE", time.Now().Format("060102 15:01:01"))
conf.SetEnv(s.T(), "BB_REQUEST_RETRY_INTERVAL_MS", "10")
origBBCert := conf.GetEnv("BB_CLIENT_CERT_FILE")
conf.SetEnv(s.T(), "BB_CLIENT_CERT_FILE", "../../../shared_files/decrypted/bfd-dev-test-cert.pem")
origBBKey := conf.GetEnv("BB_CLIENT_KEY_FILE")
conf.SetEnv(s.T(), "BB_CLIENT_KEY_FILE", "../../../shared_files/decrypted/bfd-dev-test-key.pem")
s.T().Cleanup(func() {
conf.SetEnv(s.T(), "CCLF_REF_DATE", origDate)
conf.SetEnv(s.T(), "BB_CLIENT_CERT_FILE", origBBCert)
conf.SetEnv(s.T(), "BB_CLIENT_KEY_FILE", origBBKey)
})
// Set up the logger since we're using the real client
client.SetLogger(log.BFDAPI)
}
func (s *APITestSuite) TearDownTest() {
postgrestest.DeleteJobsByACOID(s.T(), s.db, acoUnderTest)
}
func TestAPITestSuite(t *testing.T) {
suite.Run(t, new(APITestSuite))
}
func (s *APITestSuite) TestJobStatusBadInputs() {
tests := []struct {
name string
jobID string
expFHIRCode r4.IssueTypeCode
expErrCode string
}{
{"InvalidJobID", "abcd", r4.IssueTypeCodeStructure, "could not parse job id"},
{"DoesNotExist", "0", r4.IssueTypeCodeNotFound, "Job not found."},
}
for _, tt := range tests {
s.T().Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", fmt.Sprintf("%sjobs/%s", constants.V3Path, tt.jobID), nil)
rr := httptest.NewRecorder()
rctx := chi.NewRouteContext()
rctx.URLParams.Add("jobID", tt.jobID)
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
ad := s.makeContextValues(acoUnderTest)
req = req.WithContext(context.WithValue(req.Context(), auth.AuthDataContextKey, ad))
newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"cms_id": "A9999", "request_id": uuid.NewRandom().String()})
req = req.WithContext(context.WithValue(req.Context(), log.CtxLoggerKey, newLogEntry))
s.apiV3.JobStatus(rr, req)
respOO := getOperationOutcome(t, rr.Body.Bytes())
assert.Equal(t, r4.IssueSeverityError, respOO.Issue[0].Severity)
assert.Equal(t, tt.expFHIRCode, respOO.Issue[0].Code)
assert.Equal(t, tt.expErrCode, respOO.Issue[0].Diagnostics)
})
}
}
func (s *APITestSuite) TestJobStatusNotComplete() {
tests := []struct {
status models.JobStatus
expStatusCode int
}{
{models.JobStatusPending, http.StatusAccepted},
{models.JobStatusInProgress, http.StatusAccepted},
{models.JobStatusFailed, http.StatusInternalServerError},
{models.JobStatusFailedExpired, http.StatusInternalServerError},
{models.JobStatusExpired, http.StatusGone},
{models.JobStatusArchived, http.StatusGone},
{models.JobStatusCancelled, http.StatusNotFound},
{models.JobStatusCancelledExpired, http.StatusNotFound},
}
for _, tt := range tests {
s.T().Run(string(tt.status), func(t *testing.T) {
j := models.Job{
ACOID: uuid.Parse("DBBD1CE1-AE24-435C-807D-ED45953077D3"),
RequestURL: constants.V3Path + constants.PatientEOBPath,
Status: tt.status,
}
postgrestest.CreateJobs(t, s.db, &j)
defer postgrestest.DeleteJobByID(t, s.db, j.ID)
req := s.createJobStatusRequest(acoUnderTest, j.ID)
rr := httptest.NewRecorder()
s.apiV3.JobStatus(rr, req)
assert.Equal(t, tt.expStatusCode, rr.Code)
switch rr.Code {
case http.StatusAccepted:
assert.Contains(t, rr.Header().Get("X-Progress"), tt.status)
assert.Equal(t, "", rr.Header().Get("Expires"))
case http.StatusInternalServerError:
assert.Contains(t, rr.Body.String(), "Service encountered numerous errors")
case http.StatusGone:
assertExpiryEquals(t, j.UpdatedAt.Add(s.apiV3.handler.JobTimeout), rr.Header().Get("Expires"))
}
})
}
}
// TODO: V3
// Some of the JobStatus tests seem to rely on existing non-cleaned up state.
// They are failing when run as part of the whole unit-test suite however succeed when run independently.
// These tests also cause failures in api/v2 tests when run as part of the whole suite.
// https://stackoverflow.com/questions/34585957/postgresql-9-3-how-to-insert-upper-case-uuid-into-table
func (s *APITestSuite) TestJobStatusCompleted() {
j := models.Job{
ACOID: acoUnderTest,
RequestURL: constants.V3Path + constants.PatientEOBPath,
Status: models.JobStatusCompleted,
}
postgrestest.CreateJobs(s.T(), s.db, &j)
defer postgrestest.DeleteJobByID(s.T(), s.db, j.ID)
var expectedUrls []string
for i := 1; i <= 10; i++ {
fileName := fmt.Sprintf("%s.ndjson", uuid.NewRandom().String())
expectedurl := fmt.Sprintf("%s/%s/%s", constants.ExpectedTestUrl, fmt.Sprint(j.ID), fileName)
expectedUrls = append(expectedUrls, expectedurl)
postgrestest.CreateJobKeys(s.T(), s.db,
models.JobKey{JobID: j.ID, FileName: fileName, ResourceType: "ExplanationOfBenefit"})
defer postgrestest.DeleteJobKeysByJobIDs(s.T(), s.db, j.ID)
}
req := s.createJobStatusRequest(acoUnderTest, j.ID)
rr := httptest.NewRecorder()
s.apiV3.JobStatus(rr, req)
assert.Equal(s.T(), http.StatusOK, rr.Code)
assert.Equal(s.T(), constants.JsonContentType, rr.Header().Get(constants.ContentType))
str := rr.Header().Get("Expires")
fmt.Println(str)
assertExpiryEquals(s.T(), j.UpdatedAt.Add(s.apiV3.handler.JobTimeout), rr.Header().Get("Expires"))
var rb api.BulkResponseBody
err := json.Unmarshal(rr.Body.Bytes(), &rb)
if err != nil {
s.T().Error(err)
}
assert.Equal(s.T(), j.RequestURL, rb.RequestURL)
assert.Equal(s.T(), true, rb.RequiresAccessToken)
assert.Equal(s.T(), "ExplanationOfBenefit", rb.Files[0].Type)
assert.Equal(s.T(), len(expectedUrls), len(rb.Files))
// Order of these values is impossible to know so this is the only way
for _, fileItem := range rb.Files {
inOutput := false
for _, expectedUrl := range expectedUrls {
if fileItem.URL == expectedUrl {
inOutput = true
break
}
}
assert.True(s.T(), inOutput)
}
assert.Empty(s.T(), rb.Errors)
}
func (s *APITestSuite) TestJobStatusCompletedErrorAndWarningFilesExist() {
j := models.Job{
ACOID: acoUnderTest,
RequestURL: constants.V3Path + constants.PatientEOBPath,
Status: models.JobStatusCompleted,
}
postgrestest.CreateJobs(s.T(), s.db, &j)
defer postgrestest.DeleteJobByID(s.T(), s.db, j.ID)
fileName := fmt.Sprintf("%s.ndjson", uuid.NewRandom().String())
jobKey := models.JobKey{
JobID: j.ID,
FileName: fileName,
ResourceType: "ExplanationOfBenefit",
}
postgrestest.CreateJobKeys(s.T(), s.db, jobKey)
defer postgrestest.DeleteJobKeysByJobIDs(s.T(), s.db, jobKey.JobID)
f := fmt.Sprintf("%s/%s", conf.GetEnv("FHIR_PAYLOAD_DIR"), fmt.Sprint(j.ID))
if _, err := os.Stat(f); os.IsNotExist(err) {
err = os.MkdirAll(f, os.ModePerm)
if err != nil {
s.T().Error(err)
}
}
errFileName := strings.Split(jobKey.FileName, ".")[0]
errFilePath := fmt.Sprintf("%s/%s/%s-error.ndjson", conf.GetEnv("FHIR_PAYLOAD_DIR"), fmt.Sprint(j.ID), errFileName)
_, err := os.Create(errFilePath)
if err != nil {
s.T().Error(err)
}
warningFilePath := fmt.Sprintf("%s/%s/%s", conf.GetEnv("FHIR_PAYLOAD_DIR"), fmt.Sprint(j.ID), constants.WarningsAndInfoFileName)
_, err = os.Create(warningFilePath)
if err != nil {
s.T().Error(err)
}
postgrestest.CreateJobKeys(s.T(), s.db, models.JobKey{
JobID: 11,
FileName: constants.WarningsAndInfoFileName,
ResourceType: "OperationOutcome",
})
defer postgrestest.DeleteJobKeysByJobIDs(s.T(), s.db, 11)
req := s.createJobStatusRequest(acoUnderTest, j.ID)
rr := httptest.NewRecorder()
s.apiV3.JobStatus(rr, req)
assert.Equal(s.T(), http.StatusOK, rr.Code)
assert.Equal(s.T(), constants.JsonContentType, rr.Header().Get(constants.ContentType))
var rb api.BulkResponseBody
err = json.Unmarshal(rr.Body.Bytes(), &rb)
if err != nil {
s.T().Error(err)
}
dataurl := fmt.Sprintf("%s/%s/%s", constants.ExpectedTestUrl, fmt.Sprint(j.ID), fileName)
errorurl := fmt.Sprintf("%s/%s/%s-error.ndjson", constants.ExpectedTestUrl, fmt.Sprint(j.ID), errFileName)
warningURL := fmt.Sprintf("%s/%s/%s", constants.ExpectedTestUrl, fmt.Sprint(j.ID), constants.WarningsAndInfoFileName)
assert.Equal(s.T(), j.RequestURL, rb.RequestURL)
assert.Equal(s.T(), true, rb.RequiresAccessToken)
assert.Equal(s.T(), "ExplanationOfBenefit", rb.Files[0].Type)
assert.Equal(s.T(), dataurl, rb.Files[0].URL)
for _, file := range rb.Files {
assert.NotContains(s.T(), file.URL, "-error.ndjson")
}
assert.Equal(s.T(), "OperationOutcome", rb.Errors[0].Type)
assert.Equal(s.T(), warningURL, rb.Errors[0].URL)
assert.Equal(s.T(), "OperationOutcome", rb.Errors[1].Type)
assert.Equal(s.T(), errorurl, rb.Errors[1].URL)
assert.Equal(s.T(), 2, len(rb.Errors))
os.Remove(errFilePath)
}
// This job is old, but has not yet been marked as expired.
func (s *APITestSuite) TestJobStatusNotExpired() {
j := models.Job{
ACOID: acoUnderTest,
RequestURL: constants.V3Path + constants.PatientEOBPath,
Status: models.JobStatusCompleted,
}
postgrestest.CreateJobs(s.T(), s.db, &j)
defer postgrestest.DeleteJobByID(s.T(), s.db, j.ID)
j.UpdatedAt = time.Now().Add(-(s.apiV3.handler.JobTimeout + time.Second))
postgrestest.UpdateJob(s.T(), s.db, j)
req := s.createJobStatusRequest(acoUnderTest, j.ID)
rr := httptest.NewRecorder()
s.apiV3.JobStatus(rr, req)
assert.Equal(s.T(), http.StatusGone, rr.Code)
assertExpiryEquals(s.T(), j.UpdatedAt.Add(s.apiV3.handler.JobTimeout), rr.Header().Get("Expires"))
}
func (s *APITestSuite) TestJobsStatus() {
req := httptest.NewRequest("GET", fmt.Sprintf("%sjobs", constants.V3Path), nil)
ad := s.makeContextValues(acoUnderTest)
req = req.WithContext(context.WithValue(req.Context(), auth.AuthDataContextKey, ad))
newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"cms_id": "A9999", "request_id": uuid.NewRandom().String()})
req = req.WithContext(context.WithValue(req.Context(), log.CtxLoggerKey, newLogEntry))
rr := httptest.NewRecorder()
j := models.Job{
ACOID: acoUnderTest,
RequestURL: fmt.Sprintf("%sPatient/$export?_type=ExplanationOfBenefit", constants.V3Path),
Status: models.JobStatusCompleted,
}
postgrestest.CreateJobs(s.T(), s.db, &j)
defer postgrestest.DeleteJobByID(s.T(), s.db, j.ID)
s.apiV3.JobsStatus(rr, req)
assert.Equal(s.T(), http.StatusOK, rr.Code)
}
func (s *APITestSuite) TestJobsStatusNotFound() {
req := httptest.NewRequest("GET", fmt.Sprintf("%sjobs", constants.V3Path), nil)
ad := s.makeContextValues(acoUnderTest)
req = req.WithContext(context.WithValue(req.Context(), auth.AuthDataContextKey, ad))
newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"cms_id": "A9999", "request_id": uuid.NewRandom().String()})
req = req.WithContext(context.WithValue(req.Context(), log.CtxLoggerKey, newLogEntry))
rr := httptest.NewRecorder()
s.apiV3.JobsStatus(rr, req)
assert.Equal(s.T(), http.StatusNotFound, rr.Code)
}
func (s *APITestSuite) TestJobsStatusNotFoundWithStatus() {
req := httptest.NewRequest("GET", fmt.Sprintf("%sjobs?_status=Failed", constants.V3Path), nil)
ad := s.makeContextValues(acoUnderTest)
req = req.WithContext(context.WithValue(req.Context(), auth.AuthDataContextKey, ad))
newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"cms_id": "A9999", "request_id": uuid.NewRandom().String()})
req = req.WithContext(context.WithValue(req.Context(), log.CtxLoggerKey, newLogEntry))
rr := httptest.NewRecorder()
j := models.Job{
ACOID: acoUnderTest,
RequestURL: fmt.Sprintf("%sPatient/$export?_type=ExplanationOfBenefit", constants.V3Path),
Status: models.JobStatusCompleted,
}
postgrestest.CreateJobs(s.T(), s.db, &j)
defer postgrestest.DeleteJobByID(s.T(), s.db, j.ID)
s.apiV3.JobsStatus(rr, req)
assert.Equal(s.T(), http.StatusNotFound, rr.Code)
}
func (s *APITestSuite) TestJobsStatusWithStatus() {
req := httptest.NewRequest("GET", fmt.Sprintf("%sjobs?_status=Failed", constants.V3Path), nil)
ad := s.makeContextValues(acoUnderTest)
req = req.WithContext(context.WithValue(req.Context(), auth.AuthDataContextKey, ad))
newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"cms_id": "A9999", "request_id": uuid.NewRandom().String()})
req = req.WithContext(context.WithValue(req.Context(), log.CtxLoggerKey, newLogEntry))
rr := httptest.NewRecorder()
j := models.Job{
ACOID: acoUnderTest,
RequestURL: fmt.Sprintf("%sPatient/$export?_type=ExplanationOfBenefit", constants.V3Path),
Status: models.JobStatusFailed,
}
postgrestest.CreateJobs(s.T(), s.db, &j)
defer postgrestest.DeleteJobByID(s.T(), s.db, j.ID)
s.apiV3.JobsStatus(rr, req)
assert.Equal(s.T(), http.StatusOK, rr.Code)
}
func (s *APITestSuite) TestJobsStatusWithStatuses() {
req := httptest.NewRequest("GET", fmt.Sprintf("%sjobs?_status=Completed,Failed", constants.V3Path), nil)
ad := s.makeContextValues(acoUnderTest)
req = req.WithContext(context.WithValue(req.Context(), auth.AuthDataContextKey, ad))
newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"cms_id": "A9999", "request_id": uuid.NewRandom().String()})
req = req.WithContext(context.WithValue(req.Context(), log.CtxLoggerKey, newLogEntry))
rr := httptest.NewRecorder()
j := models.Job{
ACOID: acoUnderTest,
RequestURL: fmt.Sprintf("%sPatient/$export?_type=ExplanationOfBenefit", constants.V3Path),
Status: models.JobStatusFailed,
}
postgrestest.CreateJobs(s.T(), s.db, &j)
defer postgrestest.DeleteJobByID(s.T(), s.db, j.ID)
s.apiV3.JobsStatus(rr, req)
assert.Equal(s.T(), http.StatusOK, rr.Code)
}
func (s *APITestSuite) TestDeleteJobBadInputs() {
tests := []struct {
name string
jobID string
expFHIRCode r4.IssueTypeCode
expErrCode string
}{
{"InvalidJobID", "abcd", r4.IssueTypeCodeStructure, "could not parse job id"},
{"DoesNotExist", "0", r4.IssueTypeCodeNotFound, "Job not found."},
}
for _, tt := range tests {
s.T().Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("DELETE", fmt.Sprintf("%sjobs/%s", constants.V3Path, tt.jobID), nil)
rr := httptest.NewRecorder()
rctx := chi.NewRouteContext()
rctx.URLParams.Add("jobID", tt.jobID)
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
ad := s.makeContextValues(acoUnderTest)
req = req.WithContext(context.WithValue(req.Context(), auth.AuthDataContextKey, ad))
newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"cms_id": "A9999", "request_id": uuid.NewRandom().String()})
req = req.WithContext(context.WithValue(req.Context(), log.CtxLoggerKey, newLogEntry))
s.apiV3.JobStatus(rr, req)
respOO := getOperationOutcome(t, rr.Body.Bytes())
assert.Equal(t, r4.IssueSeverityError, respOO.Issue[0].Severity)
assert.Equal(t, tt.expFHIRCode, respOO.Issue[0].Code)
assert.Equal(t, tt.expErrCode, respOO.Issue[0].Diagnostics)
})
}
}
func (s *APITestSuite) TestDeleteJob() {
tests := []struct {
status models.JobStatus
expStatusCode int
}{
{models.JobStatusPending, http.StatusAccepted},
{models.JobStatusInProgress, http.StatusAccepted},
{models.JobStatusFailed, http.StatusGone},
{models.JobStatusExpired, http.StatusGone},
{models.JobStatusArchived, http.StatusGone},
{models.JobStatusCompleted, http.StatusAccepted},
{models.JobStatusCancelled, http.StatusGone},
{models.JobStatusFailedExpired, http.StatusGone},
}
for _, tt := range tests {
s.T().Run(string(tt.status), func(t *testing.T) {
j := models.Job{
ACOID: uuid.Parse("DBBD1CE1-AE24-435C-807D-ED45953077D3"),
RequestURL: fmt.Sprintf("%sPatient/$export?_type=Patient,Coverage", constants.V3Path),
Status: tt.status,
}
postgrestest.CreateJobs(t, s.db, &j)
defer postgrestest.DeleteJobByID(t, s.db, j.ID)
req := s.createJobStatusRequest(acoUnderTest, j.ID)
rr := httptest.NewRecorder()
newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"cms_id": "A9999", "request_id": uuid.NewRandom().String()})
req = req.WithContext(context.WithValue(req.Context(), log.CtxLoggerKey, newLogEntry))
s.apiV3.DeleteJob(rr, req)
assert.Equal(t, tt.expStatusCode, rr.Code)
if rr.Code == http.StatusGone {
assert.Contains(t, rr.Body.String(), "job was not cancelled because it is not Pending or In Progress")
}
})
}
}
func (s *APITestSuite) TestMetadataResponse() {
ts := httptest.NewServer(http.HandlerFunc(s.apiV3.Metadata))
defer ts.Close()
res, err := http.Get(ts.URL)
assert.NoError(s.T(), err)
assert.Equal(s.T(), "application/json", res.Header.Get(constants.ContentType))
assert.Equal(s.T(), http.StatusOK, res.StatusCode)
resp, err := io.ReadAll(res.Body)
assert.NoError(s.T(), err)
var cs r4.CapabilityStatement
assert.NoError(s.T(), json.Unmarshal(resp, &cs))
// Expecting an R4 response so we'll evaluate some fields to reflect that
assert.Equal(s.T(), "4.0.1", cs.FhirVersion)
assert.Equal(s.T(), 1, len(cs.Rest))
assert.Equal(s.T(), 3, len(cs.Rest[0].Resource))
assert.Len(s.T(), cs.Instantiates, 2)
assert.Contains(s.T(), cs.Instantiates[0], fmt.Sprintf("%s/metadata", constants.BFDV3Path))
resourceData := []struct {
rt r4.ResourceTypeCode
opName string
opDefinition string
}{
{r4.ResourceTypeCodePatient, "export", "http://hl7.org/fhir/uv/bulkdata/OperationDefinition/patient-export"},
{r4.ResourceTypeCodeGroup, "export", "http://hl7.org/fhir/uv/bulkdata/OperationDefinition/group-export"},
}
for _, rd := range resourceData {
for _, r := range cs.Rest[0].Resource {
if r.Type == rd.rt {
assert.NotNil(s.T(), r)
assert.Equal(s.T(), 1, len(r.Operation))
assert.Equal(s.T(), rd.opName, r.Operation[0].Name)
assert.Equal(s.T(), rd.opDefinition, r.Operation[0].Definition)
break
}
}
}
extensions := cs.Rest[0].Security.Extension
assert.Len(s.T(), extensions, 1)
assert.Equal(s.T(), "http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris", extensions[0].Url)
subExtensions := extensions[0].Extension
assert.Len(s.T(), subExtensions, 1)
assert.Equal(s.T(), "token", subExtensions[0].Url)
assert.Equal(s.T(), ts.URL+"/auth/token", subExtensions[0].ValueUri)
}
func (s *APITestSuite) TestResourceTypes() {
tests := []struct {
name string
resourceTypes []string
statusCode int
}{
{"Supported type - Patient", []string{"Patient"}, http.StatusAccepted},
{"Supported type - Coverage", []string{"Coverage"}, http.StatusAccepted},
{"Supported type - Patient,Coverage", []string{"Patient", "Coverage"}, http.StatusAccepted},
{"Supported type - EOB", []string{"ExplanationOfBenefit"}, http.StatusAccepted},
{"Supported type - default", nil, http.StatusAccepted},
}
resources, _ := service.GetClaimTypesMap([]string{
"Patient",
"Coverage",
"ExplanationOfBenefit",
}...)
h := api.NewHandler(resources, constants.BFDV3Path, constants.V3Version, s.db, s.pool)
mockSvc := &service.MockService{}
mockSvc.On("GetLatestCCLFFile", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&models.CCLFFile{PerformanceYear: utils.GetPY()}, nil)
mockAco := service.ACOConfig{
Data: []string{"adjudicated"},
}
mockSvc.On("GetACOConfigForID", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&mockAco, true)
mockSvc.On("GetTimeConstraints", testUtils.CtxMatcher, mock.AnythingOfType("string")).Return(service.TimeConstraints{}, nil)
mockSvc.On("GetCutoffTime", testUtils.CtxMatcher, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(time.Time{}, constants.GetExistingBenes)
mockSvc.On("IsV3NoPartialClaimsModel", mock.Anything).Return(false)
h.Svc = mockSvc
for idx, handler := range []http.HandlerFunc{h.BulkGroupRequest, h.BulkPatientRequest} {
for _, tt := range tests {
s.T().Run(fmt.Sprintf("%s-%d", tt.name, idx), func(t *testing.T) {
rr := httptest.NewRecorder()
ep := "Group"
if idx == 1 {
ep = "Patient"
}
u, err := url.Parse(fmt.Sprintf("%s%s/$export", constants.V3Path, ep))
assert.NoError(t, err)
rp := middleware.RequestParameters{
Version: constants.V3Version,
ResourceTypes: tt.resourceTypes,
}
req := httptest.NewRequest("GET", u.String(), nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("groupId", "all")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
req = req.WithContext(context.WithValue(req.Context(), appMiddleware.CtxTransactionKey, uuid.New()))
ad := s.getAuthData()
req = req.WithContext(context.WithValue(req.Context(), auth.AuthDataContextKey, ad))
req = req.WithContext(middleware.SetRequestParamsCtx(req.Context(), rp))
newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"cms_id": "A9999", "request_id": uuid.NewRandom().String()})
req = req.WithContext(context.WithValue(req.Context(), log.CtxLoggerKey, newLogEntry))
handler(rr, req)
assert.Equal(t, tt.statusCode, rr.Code)
assert.Empty(t, rr.Body.String())
if rr.Code == http.StatusAccepted {
assert.NotEmpty(t, rr.Header().Get("Content-Location"))
}
})
}
}
}
func (s *APITestSuite) TestGetAttributionStatus() {
req := httptest.NewRequest("GET", fmt.Sprintf("%sattribution_status", constants.V3Path), nil)
ad := s.makeContextValues(acoUnderTest)
req = req.WithContext(context.WithValue(req.Context(), auth.AuthDataContextKey, ad))
newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"cms_id": "A9999", "request_id": uuid.NewRandom().String()})
req = req.WithContext(context.WithValue(req.Context(), log.CtxLoggerKey, newLogEntry))
rr := httptest.NewRecorder()
s.apiV3.AttributionStatus(rr, req)
assert.Equal(s.T(), http.StatusOK, rr.Code)
var resp api.AttributionFileStatusResponse
err := json.Unmarshal(rr.Body.Bytes(), &resp)
assert.NoError(s.T(), err)
aco := postgrestest.GetACOByUUID(s.T(), s.db, acoUnderTest)
cclfFile := postgrestest.GetLatestCCLFFileByCMSIDAndType(s.T(), s.db, *aco.CMSID, models.FileTypeDefault)
assert.Equal(s.T(), "last_attribution_update", resp.IngestionDates[0].Type)
assert.Equal(s.T(), cclfFile.Timestamp.Format("2006-01-02 15:04:05"), resp.IngestionDates[0].Timestamp.Format("2006-01-02 15:04:05"))
assert.Equal(s.T(), "attribution_access_expiration", resp.ExpirationDates[0].Type)
}
func (s *APITestSuite) getAuthData() (data auth.AuthData) {
aco := postgrestest.GetACOByUUID(s.T(), s.db, acoUnderTest)
return auth.AuthData{ACOID: acoUnderTest.String(), CMSID: *aco.CMSID, TokenID: uuid.NewRandom().String()}
}
func (s *APITestSuite) makeContextValues(acoID uuid.UUID) (data auth.AuthData) {
aco := postgrestest.GetACOByUUID(s.T(), s.db, acoID)
return auth.AuthData{ACOID: aco.UUID.String(), CMSID: *aco.CMSID, TokenID: uuid.NewRandom().String()}
}
func (s *APITestSuite) createJobStatusRequest(acoID uuid.UUID, jobID uint) *http.Request {
req := httptest.NewRequest("GET", fmt.Sprintf("%sjobs/%d", constants.V3Path, jobID), nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("jobID", fmt.Sprint(jobID))
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
ad := s.makeContextValues(acoID)
newLogEntry := MakeTestStructuredLoggerEntry(logrus.Fields{"cms_id": "A9999", "request_id": uuid.NewRandom().String()})
req = req.WithContext(context.WithValue(req.Context(), log.CtxLoggerKey, newLogEntry))
return req.WithContext(context.WithValue(req.Context(), auth.AuthDataContextKey, ad))
}
// Compare expiry header against the expected time value.
// There seems to be some slight difference in precision here,
// so we'll compare up to seconds
func assertExpiryEquals(t *testing.T, expectedTime time.Time, expiry string) {
expiryTime, err := time.Parse(expiryHeaderFormat, expiry)
if err != nil {
t.Fatalf("Failed to parse %s to time.Time %s", expiry, err)
}
assert.Equal(t, time.Duration(0), expectedTime.Round(time.Second).Sub(expiryTime.Round(time.Second)))
}
func getOperationOutcome(t *testing.T, data []byte) *r4.OperationOutcome {
var outcome r4.OperationOutcome
assert.NoError(t, json.Unmarshal(data, &outcome))
return &outcome
}
func MakeTestStructuredLoggerEntry(logFields logrus.Fields) *log.StructuredLoggerEntry {
var lggr logrus.Logger
newLogEntry := &log.StructuredLoggerEntry{Logger: lggr.WithFields(logFields)}
return newLogEntry
}