From 6a6bd6653dc62adcad784376b8e223ea9d1043f5 Mon Sep 17 00:00:00 2001 From: laurenkrugen-navapbc <126501259+laurenkrugen-navapbc@users.noreply.github.com> Date: Thu, 13 Jul 2023 09:54:01 -0700 Subject: [PATCH] BCDA-7197: v1 API throws an error for REACH ACOs when "_type" parameter is not used (#859) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## đŸŽĢ Ticket https://jira.cms.gov/browse/BCDA-7197 ## 🛠 Changes When verifying resource type (when _type is empty), the Claim and ClaimResponse data types will only be returned if the ACO is allowed those resources and if the version of the endpoint is not V1. ## ℹī¸ Context for reviewers The v1 endpoints for `/Group` and `/Patient` will return Claim and ClaimResponse resource types for specific ACOs; this functionality should only occur in v2 endpoints `/Group` and `/Patient`. ## ✅ Acceptance Validation - added unit test for getResourceTypes - postman tests passing after deploying API and running tests ## 🔒 Security Implications - [ ] This PR adds a new software dependency or dependencies. - [ ] This PR modifies or invalidates one or more of our security controls. - [ ] This PR stores or transmits data that was not stored or transmitted before. - [ ] This PR requires additional review of its security implications for other reasons. If any security implications apply, add Jason Ashbaugh (GitHub username: StewGoin) as a reviewer and do not merge this PR without his approval. --- bcda/api/requests.go | 2 +- bcda/api/requests_test.go | 27 + ...ostman_Smoke_Tests.postman_collection.json | 533 +++++++++++++++++- 3 files changed, 558 insertions(+), 4 deletions(-) diff --git a/bcda/api/requests.go b/bcda/api/requests.go index 994e165a3..48ae7a53a 100644 --- a/bcda/api/requests.go +++ b/bcda/api/requests.go @@ -601,7 +601,7 @@ func (h *Handler) getResourceTypes(parameters middleware.RequestParameters, cmsI resourceTypes = append(resourceTypes, "Patient", "ExplanationOfBenefit", "Coverage") } - if utils.ContainsString(acoConfig.Data, constants.PartiallyAdjudicated) { + if utils.ContainsString(acoConfig.Data, constants.PartiallyAdjudicated) && h.apiVersion != "v1" { resourceTypes = append(resourceTypes, "Claim", "ClaimResponse") } } diff --git a/bcda/api/requests_test.go b/bcda/api/requests_test.go index 6326fb248..ea9e72f5c 100644 --- a/bcda/api/requests_test.go +++ b/bcda/api/requests_test.go @@ -709,6 +709,33 @@ func (s *RequestsTestSuite) TestJobFailedStatus() { } } +func (s *RequestsTestSuite) TestGetResourceTypes() { + + testCases := []struct { + aco string + apiVersion string + expectedResources []string + }{ + {"TEST123", "v1", []string{"Patient", "ExplanationOfBenefit", "Coverage"}}, + {"D1234", "v1", []string{"Patient", "ExplanationOfBenefit", "Coverage"}}, + {"A0000", "v1", []string{"Patient", "ExplanationOfBenefit", "Coverage"}}, + {"TEST123", "v2", []string{"Patient", "ExplanationOfBenefit", "Coverage", "Claim", "ClaimResponse"}}, + {"A0000", "v2", []string{"Patient", "ExplanationOfBenefit", "Coverage"}}, + } + for _, test := range testCases { + h := newHandler(s.resourceType, "/"+test.apiVersion+"/fhir", test.apiVersion, s.db) + rp := middleware.RequestParameters{ + Version: test.apiVersion, + ResourceTypes: []string{}, + Since: time.Time{}, + } + rt := h.getResourceTypes(rp, test.aco) + + assert.Equal(s.T(), rt, test.expectedResources) + } + +} + func (s *RequestsTestSuite) genGroupRequest(groupID string, rp middleware.RequestParameters) *http.Request { req := httptest.NewRequest("GET", "http://bcda.cms.gov/api/v1/Group/$export", nil) diff --git a/test/postman_test/BCDA_PAC_Postman_Smoke_Tests.postman_collection.json b/test/postman_test/BCDA_PAC_Postman_Smoke_Tests.postman_collection.json index 4c1ff8167..e2a020bdc 100644 --- a/test/postman_test/BCDA_PAC_Postman_Smoke_Tests.postman_collection.json +++ b/test/postman_test/BCDA_PAC_Postman_Smoke_Tests.postman_collection.json @@ -87,7 +87,7 @@ "name": "General", "item": [ { - "name": "Start Patient export", + "name": "Start Patient v2 export", "event": [ { "listen": "test", @@ -144,7 +144,7 @@ "response": [] }, { - "name": "Get Patient export job status", + "name": "Get Patient v2 export job status", "event": [ { "listen": "test", @@ -284,7 +284,7 @@ "response": [] }, { - "name": "Get Patient export job data", + "name": "Get Patient v2 export job data", "event": [ { "listen": "test", @@ -341,6 +341,267 @@ "url": "{{smokeTestPatientDataUrl}}" }, "response": [] + }, + { + "name": "Start Patient v1 export", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient/all endpoint request\");\t\t\t\t ", + " pm.environment.set(\"smokeTestPatientv1JobUrl\", \"https://bcda.cms.gov\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + " return;", + "} else {", + " pm.test(\"Status code is 202\", function() {", + " pm.response.to.have.status(202);", + " });", + "", + " pm.test(\"Has Content-Location header\", function() {", + " pm.response.to.have.header(\"Content-Location\");", + " });", + "", + " pm.environment.set(\"smokeTestPatientv1JobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": { + "token": "{{token}}" + } + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": "{{scheme}}://{{host}}/api/v1/Patient/$export" + }, + "response": [] + }, + { + "name": "Get Patient v1 export job status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get('maintenanceMode');", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient/all endpoint request for job status\");", + "", + " pm.environment.set(\"smokeTestPatientv1DataUrl\", \"https://bcda.cms.gov\")", + " return;", + "}", + "", + "pm.test(\"Status code is 202 or 200\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([202, 200]);", + "});", + "", + "if (pm.response.code === 202) {", + " pm.test(\"X-Progress header is Pending or In Progress\", function () {", + " pm.expect(/^(Pending|In Progress \\(\\d{1,3}%\\))$/.test(pm.response.headers.get(\"X-Progress\"))).to.be.true;", + " });", + "} else if (pm.response.code === 200) {", + " const schema = {", + " \"properties\": {", + " \"transactionTime\": {", + " \"type\": \"string\"", + " },", + " \"request\": {", + " \"type\": \"string\"", + " },", + " \"requiresAccessToken\": {", + " \"type\": \"boolean\"", + " },", + " \"output\": {", + " \"type\": \"array\"", + " },", + " \"error\": {", + " \"type\": \"array\"", + " }", + " }", + " };", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Schema is valid\", function () {", + " pm.expect(tv4.validate(respJson, schema)).to.be.true;", + " });", + "", + " pm.test(\"Contains Required Resources\", () => {", + " const requiredResources = [\"Patient\", \"ExplanationOfBenefit\", \"Coverage\"];", + " const otherResources = [ \"Claim\", \"ClaimResponse\"];", + " const returnedResources = respJson.output.map(r => r.type);", + "", + " for (const resource of requiredResources) {", + " pm.expect(returnedResources, resource + \" is required\").to.include(resource);", + " }", + "", + " for (const resource of otherResources) {", + " pm.expect(returnedResources, resource + \" resource type should not be returned\").to.not.include(resource);", + " }", + " });", + " ", + " pm.environment.set(\"smokeTestPatientv1DataUrl\", respJson.output[0].url);", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const retryDelay = 5000;", + "const maxRetries = 10;", + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient/all pre-request\")", + " return;", + "}", + "", + "var eobJobReq = {", + " url: pm.environment.get(\"smokeTestPatientv1JobUrl\"),", + " method: \"GET\",", + " header: \"Authorization: Bearer \" + pm.environment.get(\"token\")", + "};", + "", + "function awaitExportJob(retryCount) {", + " pm.sendRequest(eobJobReq, function (err, response) {", + " if (err) {", + " console.error(err);", + " } else if (response.code == 202) {", + " pm.test(\"X-Progress header is Pending or In Progress\", function() {", + " pm.expect(/^(Pending|In Progress \\(\\d{1,3}%\\))$/.test(response.headers.get(\"X-Progress\"))).to.be.true;", + " });", + " if (retryCount < maxRetries) {", + " console.log(\"Patient export still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for Patient job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"Patient export job complete.\");", + " } else {", + " console.error(\"Unexpected response from Patient export job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": { + "token": "{{token}}" + } + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": "{{smokeTestPatientv1JobUrl}}" + }, + "response": [] + }, + { + "name": "Get Patient v1 export job data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient/all endpoint request\");", + " return;", + "}", + "", + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Body contains data\", function () {", + " pm.expect(pm.response.length > 0)", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": { + "token": "{{token}}" + } + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": "{{smokeTestPatientv1DataUrl}}" + }, + "response": [] } ] } @@ -610,6 +871,272 @@ } ] }, + { + "name": "/all", + "item": [ + { + "name": "Start Group v1 export", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Group/all endpoint request\");\t\t\t\t ", + " pm.environment.set(\"smokeTestGroupAllJobUrlv1\", \"https://bcda.cms.gov\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + " return;", + "} else {", + " pm.test(\"Status code is 202\", function () {", + " pm.response.to.have.status(202);", + " });", + "", + " pm.test(\"Has Content-Location header\", function () {", + " pm.response.to.have.header(\"Content-Location\");", + " });", + "", + " pm.environment.set(\"smokeTestGroupAllJobUrlv1\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": { + "token": "{{token}}" + } + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": "{{scheme}}://{{host}}/api/v1/Group/all/$export" + }, + "response": [] + }, + { + "name": "Get Group export job status", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const retryDelay = 5000;", + "const maxRetries = 10;", + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Group/all pre-request\")", + " return;", + "}", + "", + "var eobJobReq = {", + " url: pm.environment.get(\"smokeTestGroupAllJobUrlv1\"),", + " method: \"GET\",", + " header: \"Authorization: Bearer \" + pm.environment.get(\"token\")", + "};", + "", + "function awaitExportJob(retryCount) {", + " pm.sendRequest(eobJobReq, function (err, response) {", + " if (err) {", + " console.error(err);", + " } else if (response.code == 202) {", + " pm.test(\"X-Progress header is Pending or In Progress\", function() {", + " pm.expect(/^(Pending|In Progress \\(\\d{1,3}%\\))$/.test(response.headers.get(\"X-Progress\"))).to.be.true;", + " });", + " if (retryCount < maxRetries) {", + " console.log(\"Group/all export still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for Group/all job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"Group/all export job complete.\");", + " } else {", + " console.error(\"Unexpected response from Group/all export job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Group/all endpoint request for job status\");", + " ", + " pm.environment.set(\"smokeTestGroupAllDataUrlv1\", \"https://bcda.cms.gov\")", + " return;", + "}", + "", + "pm.test(\"Status code is 202 or 200\", function () {", + " pm.expect(pm.response.code).to.be.oneOf([202, 200]);", + "});", + "", + "if (pm.response.code === 202) {", + " pm.test(\"X-Progress header is Pending or In Progress\", function () {", + " pm.expect(/^(Pending|In Progress \\(\\d{1,3}%\\))$/.test(pm.response.headers.get(\"X-Progress\"))).to.be.true;", + " });", + "} else if (pm.response.code === 200) {", + " const schema = {", + " \"properties\": {", + " \"transactionTime\": {", + " \"type\": \"string\"", + " },", + " \"request\": {", + " \"type\": \"string\"", + " },", + " \"requiresAccessToken\": {", + " \"type\": \"boolean\"", + " },", + " \"output\": {", + " \"type\": \"array\"", + " },", + " \"error\": {", + " \"type\": \"array\"", + " }", + " }", + " };", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Schema is valid\", function () {", + " pm.expect(tv4.validate(respJson, schema)).to.be.true;", + " });", + "", + " pm.test(\"Contains Required Resources\", () => {", + " const requiredResources = [\"Patient\", \"ExplanationOfBenefit\", \"Coverage\"];", + " const otherResources = [ \"Claim\", \"ClaimResponse\"];", + " const returnedResources = respJson.output.map(r => r.type);", + "", + " for (const resource of requiredResources) {", + " pm.expect(returnedResources, resource + \" is required\").to.include(resource);", + " }", + "", + " for (const resource of otherResources) {", + " pm.expect(returnedResources, resource + \" resource type should not be returned\").to.not.include(resource);", + " }", + " });", + " ", + " pm.environment.set(\"smokeTestGroupAllDataUrlv1\", respJson.output[0].url);", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": { + "token": "{{token}}" + } + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": "{{smokeTestGroupAllJobUrlv1}}" + }, + "response": [] + }, + { + "name": "Get Group export job data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Group/all endpoint request\");", + " return;", + "}", + "", + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Body contains data\", function () {", + " pm.expect(pm.response.length > 0)", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": { + "token": "{{token}}" + } + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": "{{smokeTestGroupAllDataUrlv1}}" + }, + "response": [] + } + ] + }, { "name": "/runout (EOB Resource)", "item": [