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
684 lines
25 KiB
Go
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
|
|
}
|