From 00cfea453b4952854726cb34ac408601e6c22ff0 Mon Sep 17 00:00:00 2001 From: alex-dzeda <120701369+alex-dzeda@users.noreply.github.com> Date: Wed, 12 Jun 2024 07:50:32 -0500 Subject: [PATCH 01/16] BCDA-7980: Implement gzip compression for files stored on EFS (#955) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽซ Ticket https://jira.cms.gov/browse/BCDA-7980 ## ๐Ÿ›  Changes Instead of attempting to move files, we write a gzip-encoded file to EFS and delete the temporary files after each job key ## โ„น๏ธ Context for reviewers This is the final ticket in the gzip epic, yay! Adds a new environment variable, COMPRESSION_LEVEL, that can be set between 1-9 Files are stored as gzip-encoded content, previous PRs ensure that the /data/ endpoint are content-encoding aware, and can return un-encoded content if requested, while ensuring significant gains for the 90+% of requests that do request gzip-encoded content. ## โœ… Acceptance Validation Smoke tests + unit tests pass. I've also performed manual validation utilizing the dev server. Throughput is as expected. ## ๐Ÿ”’ 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. --- bcdaworker/worker/worker.go | 50 ++++++++++++++++++++++++++++++-- bcdaworker/worker/worker_test.go | 42 +++++++++++++++++++++++---- 2 files changed, 83 insertions(+), 9 deletions(-) diff --git a/bcdaworker/worker/worker.go b/bcdaworker/worker/worker.go index 8495a479d..e558ab2c3 100644 --- a/bcdaworker/worker/worker.go +++ b/bcdaworker/worker/worker.go @@ -2,12 +2,15 @@ package worker import ( "bufio" + "compress/gzip" "context" "database/sql" "encoding/json" goerrors "errors" "fmt" + "io" "os" + "path/filepath" "strconv" "github.com/CMSgov/bcda-app/bcda/cclf/metrics" @@ -21,6 +24,7 @@ import ( "github.com/CMSgov/bcda-app/bcdaworker/repository/postgres" "github.com/CMSgov/bcda-app/conf" "github.com/CMSgov/bcda-app/log" + "github.com/sirupsen/logrus" fhircodes "github.com/google/fhir/go/proto/google/fhir/proto/stu3/codes_go_proto" "github.com/pborman/uuid" @@ -144,7 +148,7 @@ func (w *worker) ProcessJob(ctx context.Context, job models.Job, jobArgs models. } } //move the files over - err = moveFiles(tempJobPath, stagingPath) + err = compressFiles(ctx, tempJobPath, stagingPath) if err != nil { logger.Error(err) } @@ -164,25 +168,65 @@ func (w *worker) ProcessJob(ctx context.Context, job models.Job, jobArgs models. return nil } -func moveFiles(tempDir string, stagingDir string) error { +func compressFiles(ctx context.Context, tempDir string, stagingDir string) error { + logger := log.GetCtxLogger(ctx) // Open the input file files, err := os.ReadDir(tempDir) if err != nil { err = errors.Wrap(err, "Error reading from the staging directory for files for Job") return err } + gzipLevel, err := strconv.Atoi(os.Getenv("COMPRESSION_LEVEL")) + if err != nil || gzipLevel < 1 || gzipLevel > 9 { //levels 1-9 supported by BCDA. + gzipLevel = gzip.DefaultCompression + logger.Warnf("COMPRESSION_LEVEL not set to appropriate value; using default.") + } for _, f := range files { oldPath := fmt.Sprintf("%s/%s", tempDir, f.Name()) newPath := fmt.Sprintf("%s/%s", stagingDir, f.Name()) - err := os.Rename(oldPath, newPath) //#nosec G304 + //Anonymous function to ensure defer statements run + err := func() error { + inputFile, err := os.Open(filepath.Clean(oldPath)) + if err != nil { + return err + } + defer CloseOrLogError(logger, inputFile) + + outputFile, err := os.Create(filepath.Clean(newPath)) + if err != nil { + return err + } + defer CloseOrLogError(logger, outputFile) + gzipWriter, err := gzip.NewWriterLevel(outputFile, gzipLevel) + if err != nil { + return err + } + defer gzipWriter.Close() + + // Copy the data from the input file to the gzip writer + if _, err := io.Copy(gzipWriter, inputFile); err != nil { + return err + } + return nil + }() if err != nil { return err } + } return nil } +func CloseOrLogError(logger logrus.FieldLogger, f *os.File) { + if f == nil { + return + } + if err := f.Close(); err != nil { + logger.Warnf("Error closing file: %v", err) + } +} + // writeBBDataToFile sends requests to BlueButton and writes the results to ndjson files. // A list of JobKeys are returned, containing the names of files that were created. // Filesnames can be "blank.ndjson", ".ndjson", or "-error.ndjson". diff --git a/bcdaworker/worker/worker_test.go b/bcdaworker/worker/worker_test.go index a922c03fe..181ebec85 100644 --- a/bcdaworker/worker/worker_test.go +++ b/bcdaworker/worker/worker_test.go @@ -578,12 +578,38 @@ func (s *WorkerTestSuite) TestCreateDir() { assert.NoError(s.T(), err) } -func (s *WorkerTestSuite) TestMoveFiles() { - //negative case - err := moveFiles("/", "fake_dir") +func (s *WorkerTestSuite) TestCompressFilesGzipLevel() { + //In short, none of these should produce errors when being run. + tempDir1, err := os.MkdirTemp("", "*") + if err != nil { + s.FailNow(err.Error()) + } + tempDir2, err := os.MkdirTemp("", "*") + if err != nil { + s.FailNow(err.Error()) + } + + os.Setenv("COMPRESSION_LEVEL", "potato") + err = compressFiles(s.logctx, tempDir1, tempDir2) + assert.NoError(s.T(), err) + + os.Setenv("COMPRESSION_LEVEL", "1") + err = compressFiles(s.logctx, tempDir1, tempDir2) + assert.NoError(s.T(), err) + + os.Setenv("COMPRESSION_LEVEL", "11") + err = compressFiles(s.logctx, tempDir1, tempDir2) + assert.NoError(s.T(), err) + +} + +func (s *WorkerTestSuite) TestCompressFiles() { + //negative cases. + err := compressFiles(s.logctx, "/", "fake_dir") assert.Error(s.T(), err) - err = moveFiles("/proc/fakedir", "fake_dir") + err = compressFiles(s.logctx, "/proc/fakedir", "fake_dir") assert.Error(s.T(), err) + //positive case, create two temporary directories + a file, and move a file between them. tempDir1, err := os.MkdirTemp("", "*") if err != nil { @@ -597,12 +623,16 @@ func (s *WorkerTestSuite) TestMoveFiles() { if err != nil { s.FailNow(err.Error()) } - err = moveFiles(tempDir1, tempDir2) + err = compressFiles(s.logctx, tempDir1, tempDir2) assert.NoError(s.T(), err) files, _ := os.ReadDir(tempDir2) assert.Len(s.T(), files, 1) files, _ = os.ReadDir(tempDir1) - assert.Len(s.T(), files, 0) + assert.Len(s.T(), files, 1) + + //One more negative case, when the destination is not able to be moved. + err = compressFiles(s.logctx, tempDir2, "/proc/fakedir") + assert.Error(s.T(), err) } From 67e8641e4740b41ca88910c518923867524787bf Mon Sep 17 00:00:00 2001 From: alex-dzeda <120701369+alex-dzeda@users.noreply.github.com> Date: Fri, 14 Jun 2024 13:34:36 -0500 Subject: [PATCH 02/16] Ensure temp directory is cleared on process start (#958) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽซ Ticket N/A ## ๐Ÿ›  Changes Ensure that the temporary directory is emptied upon process start. ## โ„น๏ธ Context for reviewers After changing the worker's logic to write to EBS volumes prior to writing compressed files to EFS, some monitoring showed that if the bcda worker process exits in a non-graceful manner, data could accumulate on the EBS volume and cause out of space errors. This PR ensures that when the process starts, the temporary directory is cleared. ## โœ… Acceptance Validation Added unit tests that pass. ## ๐Ÿ”’ 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. --- .vscode/settings.json | 1 + bcdaworker/main.go | 25 +++++++++++++++++++++++++ bcdaworker/main_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 bcdaworker/main_test.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 54eacff04..d9513c0d4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,6 +16,7 @@ "BB_CLIENT_KEY_FILE": "${workspaceFolder}/shared_files/decrypted/bfd-dev-test-key.pem", "FHIR_PAYLOAD_DIR": "${workspaceFolder}/bcdaworker/data", "FHIR_STAGING_DIR": "${workspaceFolder}/bcdaworker/tmpdata", + "FHIR_TEMP_DIR": "${workspaceFolder}/bcdaworker/TEMP", "FHIR_ARCHIVE_DIR": "${workspaceFolder}/bcdaworker/archive", }, "go.testEnvFile": "${workspaceFolder}/shared_files/decrypted/local.env", diff --git a/bcdaworker/main.go b/bcdaworker/main.go index 45344158b..ec896ea90 100644 --- a/bcdaworker/main.go +++ b/bcdaworker/main.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/signal" + "path/filepath" "strconv" "syscall" "time" @@ -34,6 +35,30 @@ func createWorkerDirs() { if err != nil { log.Worker.Fatal(err) } + err = clearTempDirectory(localTemp) + if err != nil { + log.Worker.Fatal(err) + } + +} +func clearTempDirectory(tempDir string) error { + err := filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if path == tempDir { + return nil + } + if info.IsDir() { + return os.RemoveAll(path) + } + return os.Remove(path) + }) + + if err != nil { + return err + } + return nil } func waitForSig() { diff --git a/bcdaworker/main_test.go b/bcdaworker/main_test.go new file mode 100644 index 000000000..ef4ed85e6 --- /dev/null +++ b/bcdaworker/main_test.go @@ -0,0 +1,40 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + + "github.com/CMSgov/bcda-app/conf" + "github.com/stretchr/testify/assert" +) + +func TestClearTempDirectory(t *testing.T) { + tempDirPrefix := conf.GetEnv("FHIR_TEMP_DIR") + tempDir, err := os.MkdirTemp(tempDirPrefix, "bananas") + defer os.RemoveAll(tempDir) + assert.NoError(t, err) + + nestedDir := filepath.Join(tempDir, "nested") + err = os.Mkdir(nestedDir, 0755) + assert.DirExists(t, nestedDir) + assert.NoError(t, err) + + tempFile := filepath.Join(tempDir, "tmp.txt") + err = os.WriteFile(tempFile, []byte("test data"), 0644) + assert.NoError(t, err) + + dir, err := os.ReadDir(tempDir) + assert.NoError(t, err) + assert.Equal(t, 2, len(dir)) //We've created a file/directory, so we expect something. + + err = clearTempDirectory(tempDir) + assert.NoError(t, err) + + dir, err = os.ReadDir(tempDir) + assert.NoError(t, err) + assert.Equal(t, 0, len(dir)) //Expect that there will be no entries after cleaning. + + err = clearTempDirectory("/fakedir") //Expect an error when there's an incorrect directory passed + assert.Error(t, err) +} From c3baf17cd1e3f8f24ebb1971403d1dd4bf905567 Mon Sep 17 00:00:00 2001 From: austincanada <162146803+austincanada@users.noreply.github.com> Date: Tue, 18 Jun 2024 13:10:00 -0400 Subject: [PATCH 03/16] Add opt-out import and cclf-import workflows (#957) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽซ Ticket https://jira.cms.gov/browse/BCDA-7899 ## ๐Ÿ›  Changes Added cclf-import workflows. Added Sean's opt-out-import workflows. Added unit testing workflow for cclf and opt-out workflow. ## โ„น๏ธ Context for reviewers Creating a merge PR in order to test run the workflows. Some things will probably have to change within the workflows themselves. ## โœ… Acceptance Validation Workflows completed: Screenshot 2024-06-18 at 12 23 13โ€ฏPM Screenshot 2024-06-18 at 12 23 38โ€ฏPM Lambdas updated: Screenshot 2024-06-18 at 12 29 49โ€ฏPM ## ๐Ÿ”’ 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. --------- Co-authored-by: Sean Fern --- .github/workflows/cclf-import-dev-deploy.yml | 38 ++++++++++++++ .github/workflows/cclf-import-prod-deploy.yml | 25 +++++++++ .github/workflows/cclf-import-test-deploy.yml | 35 +++++++++++++ .../cclf-import-test-integration.yml | 51 +++++++++++++++++++ .github/workflows/ci-workflow.yml | 6 +++ .../workflows/opt-out-import-dev-deploy.yml | 38 ++++++++++++++ .../workflows/opt-out-import-prod-deploy.yml | 24 +++++++++ .../workflows/opt-out-import-test-deploy.yml | 35 +++++++++++++ .../opt-out-import-test-integration.yml | 51 +++++++++++++++++++ 9 files changed, 303 insertions(+) create mode 100644 .github/workflows/cclf-import-dev-deploy.yml create mode 100644 .github/workflows/cclf-import-prod-deploy.yml create mode 100644 .github/workflows/cclf-import-test-deploy.yml create mode 100644 .github/workflows/cclf-import-test-integration.yml create mode 100644 .github/workflows/opt-out-import-dev-deploy.yml create mode 100644 .github/workflows/opt-out-import-prod-deploy.yml create mode 100644 .github/workflows/opt-out-import-test-deploy.yml create mode 100644 .github/workflows/opt-out-import-test-integration.yml diff --git a/.github/workflows/cclf-import-dev-deploy.yml b/.github/workflows/cclf-import-dev-deploy.yml new file mode 100644 index 000000000..3d6371f8b --- /dev/null +++ b/.github/workflows/cclf-import-dev-deploy.yml @@ -0,0 +1,38 @@ +name: cclf-import dev deploy + +on: + push: + branches: + - main + paths: + - cclf-import/** + - .github/workflows/cclf-import-dev-deploy.yml + workflow_dispatch: + +jobs: + test: + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + defaults: + run: + working-directory: bcda + environment: dev + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + - name: Build cclf-import zip file + run: | + go build -o bin/cclf-import ./lambda/cclf/main.go + zip function.zip bin/cclf-import + - uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: arn:aws:iam::${{ secrets.ACCOUNT_ID }}:role/delegatedadmin/developer/bcda-dev-github-actions + - name: Upload and reload + run: | + aws s3 cp --no-progress function.zip \ + s3://bcda-dev-cclf-import-function/function-${{ github.sha }}.zip + aws lambda update-function-code --function-name bcda-dev-cclf-import \ + --s3-bucket bcda-dev-cclf-import-function --s3-key function-${{ github.sha }}.zip diff --git a/.github/workflows/cclf-import-prod-deploy.yml b/.github/workflows/cclf-import-prod-deploy.yml new file mode 100644 index 000000000..2d3a27334 --- /dev/null +++ b/.github/workflows/cclf-import-prod-deploy.yml @@ -0,0 +1,25 @@ +name: cclf-import prod deploy + +on: + workflow_dispatch: + +jobs: + deploy: + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + environment: prod + steps: + - uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: arn:aws:iam::${{ secrets.ACCOUNT_ID }}:role/delegatedadmin/developer/bcda-prod-github-actions + - name: Promote lambda code from test to prod + run: | + aws s3 cp --no-progress \ + s3://bcda-test-cclf-import-function/function-${{ github.sha }}.zip \ + s3://bcda-prod-cclf-import-function/function-${{ github.sha }}.zip + aws lambda update-function-code --function-name bcda-prod-cclf-import \ + --s3-bucket bcda-prod-cclf-import-function --s3-key function-${{ github.sha }}.zip + diff --git a/.github/workflows/cclf-import-test-deploy.yml b/.github/workflows/cclf-import-test-deploy.yml new file mode 100644 index 000000000..df2d4be76 --- /dev/null +++ b/.github/workflows/cclf-import-test-deploy.yml @@ -0,0 +1,35 @@ +name: cclf-import test deploy + +on: + workflow_call: + workflow_dispatch: + +jobs: + test: + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + defaults: + run: + working-directory: bcda + environment: test + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + - name: Build cclf-import zip file + env: + CGO_ENABLED: 0 + run: | + go build -o bin/cclf-import ./lambda/cclf/main.go + zip function.zip bin/cclf-import + - uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: arn:aws:iam::${{ secrets.ACCOUNT_ID }}:role/delegatedadmin/developer/bcda-test-github-actions + - name: Upload and reload + run: | + aws s3 cp --no-progress function.zip \ + s3://bcda-test-cclf-import-function/function-${{ github.sha }}.zip + aws lambda update-function-code --function-name bcda-test-cclf-import \ + --s3-bucket bcda-test-cclf-import-function --s3-key function-${{ github.sha }}.zip diff --git a/.github/workflows/cclf-import-test-integration.yml b/.github/workflows/cclf-import-test-integration.yml new file mode 100644 index 000000000..537044fd8 --- /dev/null +++ b/.github/workflows/cclf-import-test-integration.yml @@ -0,0 +1,51 @@ +name: cclf-import test integration + +on: + pull_request: + paths: + - .github/workflows/cclf-import-test-integration.yml + - .github/workflows/cclf-import-test-deploy.yml + - cclf/** + workflow_dispatch: + +# Ensure we have only one integration test running at a time +concurrency: + group: cclf-import-test-integration + +jobs: + # Deploy first if triggered by pull_request + deploy: + if: ${{ github.event_name == 'pull_request' }} + uses: ./.github/workflows/cclf-import-test-deploy.yml + secrets: inherit + + trigger: + if: ${{ always() }} + needs: deploy + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + defaults: + run: + working-directory: bcda + steps: + - uses: actions/checkout@v4 + - uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: arn:aws:iam::${{ secrets.ACCOUNT_ID }}:role/delegatedadmin/developer/bcda-test-cclf-import-function + - uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + # Note that we use the BFD role with access to the bucket + role-to-assume: arn:aws:iam::${{ secrets.BFD_ACCOUNT_ID }}:role/bfd-test-eft-bcda-bucket-role + role-chaining: true + role-skip-session-tagging: true + - name: Upload test file to the BFD bucket to trigger lambda function via SNS message + run: | + aws s3 cp --no-progress ../shared_files/cclf/files/synthetic/test/small/ZC0 \ + s3://bfd-test-eft/bfdeft01/bcda/in/T.NGD.DPC.RSP.D$(date +'%y%m%d').T$(date +'%H%M%S')1.IN + + # TODO Check bucket for response file + # TODO Run another job to check database for update diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 95786bb65..58b9a5e65 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -4,7 +4,13 @@ on: push: branches: - main + paths-ignore: + - .github/workflows/opt-out-import-* + - optout/** pull_request: + paths-ignore: + - .github/workflows/opt-out-import-* + - optout/** env: COMPOSE_INTERACTIVE_NO_CLI: 1 diff --git a/.github/workflows/opt-out-import-dev-deploy.yml b/.github/workflows/opt-out-import-dev-deploy.yml new file mode 100644 index 000000000..e28ada44a --- /dev/null +++ b/.github/workflows/opt-out-import-dev-deploy.yml @@ -0,0 +1,38 @@ +name: opt-out-import dev deploy + +on: + push: + branches: + - main + paths: + - optout/** + - .github/workflows/opt-out-import-dev-deploy.yml + workflow_dispatch: + +jobs: + test: + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + defaults: + run: + working-directory: bcda + environment: dev + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + - name: Build opt-out-import zip file + run: | + go build -o bin/opt-out-import ./lambda/optout/main.go + zip function.zip bin/opt-out-import + - uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: arn:aws:iam::${{ secrets.ACCOUNT_ID }}:role/delegatedadmin/developer/bcda-dev-github-actions + - name: Upload and reload + run: | + aws s3 cp --no-progress function.zip \ + s3://bcda-dev-opt-out-import-function/function-${{ github.sha }}.zip + aws lambda update-function-code --function-name bcda-dev-opt-out-import \ + --s3-bucket bcda-dev-opt-out-import-function --s3-key function-${{ github.sha }}.zip diff --git a/.github/workflows/opt-out-import-prod-deploy.yml b/.github/workflows/opt-out-import-prod-deploy.yml new file mode 100644 index 000000000..d208cb6d0 --- /dev/null +++ b/.github/workflows/opt-out-import-prod-deploy.yml @@ -0,0 +1,24 @@ +name: opt-out-import prod deploy + +on: + workflow_dispatch: + +jobs: + deploy: + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + environment: prod + steps: + - uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: arn:aws:iam::${{ secrets.ACCOUNT_ID }}:role/delegatedadmin/developer/bcda-prod-github-actions + - name: Promote lambda code from test to prod + run: | + aws s3 cp --no-progress \ + s3://bcda-test-opt-out-import-function/function-${{ github.sha }}.zip \ + s3://bcda-prod-opt-out-import-function/function-${{ github.sha }}.zip + aws lambda update-function-code --function-name bcda-prod-opt-out-import \ + --s3-bucket bcda-prod-opt-out-import-function --s3-key function-${{ github.sha }}.zip diff --git a/.github/workflows/opt-out-import-test-deploy.yml b/.github/workflows/opt-out-import-test-deploy.yml new file mode 100644 index 000000000..aa35caec5 --- /dev/null +++ b/.github/workflows/opt-out-import-test-deploy.yml @@ -0,0 +1,35 @@ +name: opt-out-import test deploy + +on: + workflow_call: + workflow_dispatch: + +jobs: + test: + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + defaults: + run: + working-directory: bcda + environment: test + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + - name: Build opt-out-import zip file + env: + CGO_ENABLED: 0 + run: | + go build -o bin/opt-out-import ./lambda/optout/main.go + zip function.zip bin/opt-out-import + - uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: arn:aws:iam::${{ secrets.ACCOUNT_ID }}:role/delegatedadmin/developer/bcda-test-github-actions + - name: Upload and reload + run: | + aws s3 cp --no-progress function.zip \ + s3://bcda-test-opt-out-import-function/function-${{ github.sha }}.zip + aws lambda update-function-code --function-name bcda-test-opt-out-import \ + --s3-bucket bcda-test-opt-out-import-function --s3-key function-${{ github.sha }}.zip diff --git a/.github/workflows/opt-out-import-test-integration.yml b/.github/workflows/opt-out-import-test-integration.yml new file mode 100644 index 000000000..96aced9a3 --- /dev/null +++ b/.github/workflows/opt-out-import-test-integration.yml @@ -0,0 +1,51 @@ +name: opt-out-import test integration + +on: + pull_request: + paths: + - .github/workflows/opt-out-import-test-integration.yml + - .github/workflows/opt-out-import-test-deploy.yml + - optout/** + workflow_dispatch: + +# Ensure we have only one integration test running at a time +concurrency: + group: opt-out-import-test-integration + +jobs: + # Deploy first if triggered by pull_request + deploy: + if: ${{ github.event_name == 'pull_request' }} + uses: ./.github/workflows/opt-out-import-test-deploy.yml + secrets: inherit + + trigger: + if: ${{ always() }} + needs: deploy + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./optout + steps: + - uses: actions/checkout@v4 + - uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: arn:aws:iam::${{ secrets.ACCOUNT_ID }}:role/delegatedadmin/developer/bcda-test-opt-out-import-function + - uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + # Note that we use the BFD role with access to the bucket + role-to-assume: arn:aws:iam::${{ secrets.BFD_ACCOUNT_ID }}:role/bfd-test-eft-bcda-bucket-role + role-chaining: true + role-skip-session-tagging: true + - name: Upload test file to the BFD bucket to trigger lambda function via SNS message + run: | + aws s3 cp --no-progress ../shared_files/synthetic1800MedicareFiles/test/T\#EFT.ON.ACO.NGD1800.DPRF.D181120.T1000009 \ + s3://bfd-test-eft/bfdeft01/bcda/in/T.NGD.DPC.RSP.D$(date +'%y%m%d').T$(date +'%H%M%S')1.IN + + # TODO Check bucket for response file + # TODO Run another job to check database for update From b8d5a3b14fb1645e0c316d1dbb0769e9ec7a7192 Mon Sep 17 00:00:00 2001 From: gfreeman-navapbc <129095098+gfreeman-navapbc@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:09:07 -0700 Subject: [PATCH 04/16] PLT-469: Updated pull request template and added CONTRIBUTING.md (#956) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽซ Ticket https://jira.cms.gov/browse/PLT-469 ## ๐Ÿ›  Changes Added the updated pull request template and contribution guidelines from [ab2d-bcda-dpc-platform #87](https://github.com/CMSgov/ab2d-bcda-dpc-platform/pull/87) ## โ„น๏ธ Context for reviewers Jason is rolling off of the project, so we're updating the pull requests templates to reflect our new ISSO @SJWalter11. We're also taking this opportunity to introduce a new contribution guidelines file, as to not clog up the average PR with extra steps. ## โœ… Acceptance Validation The pull request template should be reflected on the creation of new PRs. --- .github/CONTRIBUTING.md | 15 +++++++++++++++ .github/pull_request_template.md | 25 +++++++++++-------------- 2 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 .github/CONTRIBUTING.md diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 000000000..c14eafa82 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,15 @@ +# Contribution expectations + +The following expectations apply to each PR: + +1. The PR and branch are named for [automatic linking](https://support.atlassian.com/jira-cloud-administration/docs/use-the-github-for-jira-app/) to the most relevant JIRA issue (for example, `JRA-123 Adds foo` for PR title and `jra-123-adds-foo` for branch name). +2. Reviewers are selected to include people from all teams impacted by the changes in the PR. +3. The PR has been assigned to the people who will respond to reviews and merge when ready (usually the person filing the review, but can change when a PR is handed off to someone else). +4. The PR is reasonably limited in scope to ensure: + - It doesn't bunch together disparate features, fixes, refactorings, etc. + - There isn't too much of a burden on reviewers. + - Any problems it causes have a small blast radius. + - Changes will be easier to roll back if necessary. +5. The PR includes any required documentation changes, including `README` updates and changelog or release notes entries. +6. All new and modified code is appropriately commented to make the what and why of its design reasonably clear, even to those unfamiliar with the project. +7. Any incomplete work introduced by the PR is detailed in `TODO` comments which include a JIRA ticket ID for any items that require urgent attention. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c2a38dd1b..21ea69ce6 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,24 +1,21 @@ ## ๐ŸŽซ Ticket -https://jira.cms.gov/browse/BCDA-xxx +https://jira.cms.gov/browse/... ## ๐Ÿ›  Changes -(What was added, updated, or removed in this PR.) + -## โ„น๏ธ Context for reviewers +## โ„น๏ธ Context -(Background context, more in-depth details of the implementation, and anything else you'd like to call out or ask reviewers.) + -## โœ… Acceptance Validation + -(How were the changes verified? Did you fully test the acceptance criteria in the ticket? Provide reproducible testing instructions and screenshots if applicable.) +## ๐Ÿงช Validation -## ๐Ÿ”’ 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. + From 98b387521ba8bea9ba561ae69f56536994a79c28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:56:57 -0400 Subject: [PATCH 05/16] Bump golang.org/x/net from 0.21.0 to 0.23.0 (#932) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.21.0 to 0.23.0.
Commits
  • c48da13 http2: fix TestServerContinuationFlood flakes
  • 762b58d http2: fix tipos in comment
  • ba87210 http2: close connections when receiving too many headers
  • ebc8168 all: fix some typos
  • 3678185 http2: make TestCanonicalHeaderCacheGrowth faster
  • 448c44f http2: remove clientTester
  • c7877ac http2: convert the remaining clientTester tests to testClientConn
  • d8870b0 http2: use synthetic time in TestIdleConnTimeout
  • d73acff http2: only set up deadline when Server.IdleTimeout is positive
  • 89f602b http2: validate client/outgoing trailers
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/net&package-manager=go_modules&previous-version=0.21.0&new-version=0.23.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/CMSgov/bcda-app/network/alerts).
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: alex-dzeda <120701369+alex-dzeda@users.noreply.github.com> Co-authored-by: Kevin Yeh --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index c512c4e61..bde5193f7 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/stretchr/testify v1.8.1 github.com/tsenart/vegeta v12.7.0+incompatible github.com/urfave/cli v1.22.9 - golang.org/x/crypto v0.20.0 + golang.org/x/crypto v0.21.0 golang.org/x/text v0.14.0 gotest.tools/gotestsum v1.6.2 ) @@ -96,10 +96,10 @@ require ( github.com/subosito/gotenv v1.3.0 // indirect github.com/tsenart/go-tsz v0.0.0-20180814235614-0bd30b3df1c3 // indirect golang.org/x/mod v0.8.0 // indirect - golang.org/x/net v0.21.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/term v0.17.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect golang.org/x/tools v0.6.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gonum.org/v1/gonum v0.11.0 // indirect diff --git a/go.sum b/go.sum index 87333d419..333c57906 100644 --- a/go.sum +++ b/go.sum @@ -962,8 +962,8 @@ golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= -golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1072,8 +1072,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1189,11 +1189,11 @@ golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From fabde15cb4f460d7398dcdfcf61205840258afcd Mon Sep 17 00:00:00 2001 From: Kevin Yeh Date: Fri, 21 Jun 2024 15:47:51 -0400 Subject: [PATCH 06/16] Fix lambda handler paths (#959) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽซ Ticket N/A ## ๐Ÿ›  Changes - Remove `/bin/` directories from function zips using `zip -j` - Rename files to `bootstrap` ## โ„น๏ธ Context Was looking at the lambda logs and saw some errors with trying to find the entrypoint. After reading the AWS documentation, I realized that it expects the zip file to have exactly one binary, which is specifically named `bootstrap` ๐Ÿ˜… This was an oversight on my part โ€” I thought if the handler was specified as `cclf-import` in the lambda config, it would look for that binary filename instead. ## ๐Ÿงช Validation Verified in logs (doesn't complete successfully, we have another network issue to address..) ![CleanShot 2024-06-21 at 13 35 58@2x](https://github.com/CMSgov/bcda-app/assets/2308368/f3bd76fe-e6ee-48cc-a2e8-ce43f3713565) --- .github/workflows/cclf-import-dev-deploy.yml | 6 ++++-- .github/workflows/cclf-import-test-deploy.yml | 4 ++-- .github/workflows/opt-out-import-dev-deploy.yml | 6 ++++-- .github/workflows/opt-out-import-test-deploy.yml | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/cclf-import-dev-deploy.yml b/.github/workflows/cclf-import-dev-deploy.yml index 3d6371f8b..560a3d775 100644 --- a/.github/workflows/cclf-import-dev-deploy.yml +++ b/.github/workflows/cclf-import-dev-deploy.yml @@ -23,9 +23,11 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 - name: Build cclf-import zip file + env: + CGO_ENABLED: 0 run: | - go build -o bin/cclf-import ./lambda/cclf/main.go - zip function.zip bin/cclf-import + go build -o bin/bootstrap ./lambda/cclf/main.go + zip -j function.zip bin/bootstrap - uses: aws-actions/configure-aws-credentials@v4 with: aws-region: ${{ vars.AWS_REGION }} diff --git a/.github/workflows/cclf-import-test-deploy.yml b/.github/workflows/cclf-import-test-deploy.yml index df2d4be76..7ca347677 100644 --- a/.github/workflows/cclf-import-test-deploy.yml +++ b/.github/workflows/cclf-import-test-deploy.yml @@ -21,8 +21,8 @@ jobs: env: CGO_ENABLED: 0 run: | - go build -o bin/cclf-import ./lambda/cclf/main.go - zip function.zip bin/cclf-import + go build -o bin/bootstrap ./lambda/cclf/main.go + zip -j function.zip bin/bootstrap - uses: aws-actions/configure-aws-credentials@v4 with: aws-region: ${{ vars.AWS_REGION }} diff --git a/.github/workflows/opt-out-import-dev-deploy.yml b/.github/workflows/opt-out-import-dev-deploy.yml index e28ada44a..dd9398f0a 100644 --- a/.github/workflows/opt-out-import-dev-deploy.yml +++ b/.github/workflows/opt-out-import-dev-deploy.yml @@ -23,9 +23,11 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 - name: Build opt-out-import zip file + env: + CGO_ENABLED: 0 run: | - go build -o bin/opt-out-import ./lambda/optout/main.go - zip function.zip bin/opt-out-import + go build -o bin/bootstrap ./lambda/optout/main.go + zip -j function.zip bin/bootstrap - uses: aws-actions/configure-aws-credentials@v4 with: aws-region: ${{ vars.AWS_REGION }} diff --git a/.github/workflows/opt-out-import-test-deploy.yml b/.github/workflows/opt-out-import-test-deploy.yml index aa35caec5..29d4f9cbf 100644 --- a/.github/workflows/opt-out-import-test-deploy.yml +++ b/.github/workflows/opt-out-import-test-deploy.yml @@ -21,8 +21,8 @@ jobs: env: CGO_ENABLED: 0 run: | - go build -o bin/opt-out-import ./lambda/optout/main.go - zip function.zip bin/opt-out-import + go build -o bin/bootstrap ./lambda/optout/main.go + zip -j function.zip bin/bootstrap - uses: aws-actions/configure-aws-credentials@v4 with: aws-region: ${{ vars.AWS_REGION }} From e758d8ff8e2d9259ebbe8045eb689faf5e4a6e06 Mon Sep 17 00:00:00 2001 From: Kevin Yeh Date: Mon, 24 Jun 2024 16:02:25 -0400 Subject: [PATCH 07/16] BCDA-8208: Fix S3 path parsing (#960) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽซ Ticket https://jira.cms.gov/browse/BCDA-8208 ## ๐Ÿ›  Changes - Fix S3 path parsing ## โ„น๏ธ Context I observed failures with the lambdas attempting to list files in S3 within the bfdeft01 prefix. This is not the correct prefix โ€” the team-specific IAM roles can only access specific subfolders (i.e. bfdeft01/bcda, bfdeft01/dpc, etc.) so the lambdas _should_ be trying to list files within those subfolders. When parsing the S3 prefix from the SQS event, it needs to split from the last separator instead of the first separator. ## ๐Ÿงช Validation Deployed to Dev and verified successful import by uploading a file (I basically did this part of the integration test manually: https://github.com/CMSgov/bcda-app/blob/fabde15cb4f460d7398dcdfcf61205840258afcd/.github/workflows/opt-out-import-test-integration.yml#L47-L48 but fixed the filename since it's using the DPC filename) **Successful lambda logs** ![](https://github.com/CMSgov/bcda-app/assets/2308368/c13a4d4c-58da-466e-9df2-38a6e9b1903a) --- bcda/aws/parsers.go | 12 ++++++++++++ bcda/aws/parsers_test.go | 6 ++++++ bcda/lambda/cclf/main.go | 11 ++--------- bcda/lambda/optout/main.go | 11 ++--------- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/bcda/aws/parsers.go b/bcda/aws/parsers.go index 2346d28b7..4042a4368 100644 --- a/bcda/aws/parsers.go +++ b/bcda/aws/parsers.go @@ -2,6 +2,8 @@ package bcdaaws import ( "encoding/json" + "fmt" + "strings" "github.com/aws/aws-lambda-go/events" "github.com/pkg/errors" @@ -34,3 +36,13 @@ func ParseSQSEvent(event events.SQSEvent) (*events.S3Event, error) { return &s3Event, nil } + +func ParseS3Directory(bucket, key string) string { + lastSeparatorIdx := strings.LastIndex(key, "/") + + if lastSeparatorIdx == -1 { + return bucket + } else { + return fmt.Sprintf("%s/%s", bucket, key[:lastSeparatorIdx]) + } +} diff --git a/bcda/aws/parsers_test.go b/bcda/aws/parsers_test.go index 1575b355a..fadb102e7 100644 --- a/bcda/aws/parsers_test.go +++ b/bcda/aws/parsers_test.go @@ -47,3 +47,9 @@ func TestParseSQSEvent(t *testing.T) { assert.NotNil(t, s3Event) assert.Equal(t, "demo-bucket", s3Event.Records[0].S3.Bucket.Name) } + +func TestParseS3Directory(t *testing.T) { + assert.Equal(t, "my-bucket", ParseS3Directory("my-bucket", "some-file")) + assert.Equal(t, "my-bucket/my-dir", ParseS3Directory("my-bucket", "my-dir/some-file")) + assert.Equal(t, "my-bucket/my-dir/nested", ParseS3Directory("my-bucket", "my-dir/nested/some-file")) +} diff --git a/bcda/lambda/cclf/main.go b/bcda/lambda/cclf/main.go index 94f2f7c9f..24272aea5 100644 --- a/bcda/lambda/cclf/main.go +++ b/bcda/lambda/cclf/main.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "os" - "strings" "time" "github.com/aws/aws-lambda-go/events" @@ -56,14 +55,8 @@ func cclfImportHandler(ctx context.Context, sqsEvent events.SQSEvent) (string, e return "", err } - parts := strings.Split(e.S3.Object.Key, "/") - - if len(parts) == 1 { - return handleCclfImport(s3AssumeRoleArn, e.S3.Bucket.Name) - } else { - directory := fmt.Sprintf("%s/%s", e.S3.Bucket.Name, parts[0]) - return handleCclfImport(s3AssumeRoleArn, directory) - } + dir := bcdaaws.ParseS3Directory(e.S3.Bucket.Name, e.S3.Object.Key) + return handleCclfImport(s3AssumeRoleArn, dir) } } diff --git a/bcda/lambda/optout/main.go b/bcda/lambda/optout/main.go index 8afdeaad2..e0cc2fe46 100644 --- a/bcda/lambda/optout/main.go +++ b/bcda/lambda/optout/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "strings" "time" "github.com/aws/aws-lambda-go/events" @@ -58,14 +57,8 @@ func optOutImportHandler(ctx context.Context, sqsEvent events.SQSEvent) (string, return "", err } - parts := strings.Split(e.S3.Object.Key, "/") - - if len(parts) == 1 { - return handleOptOutImport(s3AssumeRoleArn, e.S3.Bucket.Name) - } else { - directory := fmt.Sprintf("%s/%s", e.S3.Bucket.Name, parts[0]) - return handleOptOutImport(s3AssumeRoleArn, directory) - } + dir := bcdaaws.ParseS3Directory(e.S3.Bucket.Name, e.S3.Object.Key) + return handleOptOutImport(s3AssumeRoleArn, dir) } } From 1bd21340cfb24a29e070646223268a455e1bbc13 Mon Sep 17 00:00:00 2001 From: Kevin Yeh Date: Mon, 1 Jul 2024 12:25:01 -0400 Subject: [PATCH 08/16] BCDA-8197: Prevent duplicate job keys from queue re-processing (#962) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽซ Ticket https://jira.cms.gov/browse/BCDA-8197 ## ๐Ÿ›  Changes Core Changes: - Add job_keys.que_job_id column and (job_id, que_job_id) index - Avoid re-processing a que job if we've already created job keys for the job Additional Changes: - Create multiple job keys in an single database operation instead of multiple sequential operations - Return error if we fail to compress/move files from temp --> staging - Return error if we fail to create job keys ## โ„น๏ธ Context The core change in this PR is the addition of the job_keys.que_job_id column, which is used to avoid re-processing a queue job if something happens between the job keys being created and the queue job being fully processed and deleted. The additional changes are minor tweaks to avoid any of the potential errors that may have occurred previously: - One job key being created without the error - File retrieval errors due to job keys being created without the files being moved to staging/payload directory - Queue jobs being processed and deleted without the related job keys being created - Queue jobs being processed and deleted without the parent job being marked completed ## ๐Ÿงช Validation - [x] Existing unit tests - [x] New unit tests --- bcda/models/models.go | 7 +- bcdaworker/queueing/manager/que.go | 23 ++-- bcdaworker/queueing/manager/que_test.go | 13 +- bcdaworker/repository/mock_repository.go | 116 +++++++++++++++++- bcdaworker/repository/postgres/repository.go | 38 +++++- .../repository/postgres/repository_test.go | 17 ++- bcdaworker/repository/repository.go | 8 +- bcdaworker/worker/mock_worker.go | 53 +++++--- bcdaworker/worker/worker.go | 54 +++++--- bcdaworker/worker/worker_test.go | 90 ++++++++++---- .../bcda/000016_add_que_job_id.down.sql | 6 + .../bcda/000016_add_que_job_id.up.sql | 8 ++ db/migrations/migrations_test.go | 48 ++++++++ 13 files changed, 399 insertions(+), 82 deletions(-) create mode 100644 db/migrations/bcda/000016_add_que_job_id.down.sql create mode 100644 db/migrations/bcda/000016_add_que_job_id.up.sql diff --git a/bcda/models/models.go b/bcda/models/models.go index 93365353c..9d7899409 100644 --- a/bcda/models/models.go +++ b/bcda/models/models.go @@ -48,8 +48,11 @@ func (j *Job) StatusMessage() string { const BlankFileName string = "blank.ndjson" type JobKey struct { - ID uint - JobID uint `json:"job_id"` + ID uint + JobID uint `json:"job_id"` + // Although que_job records are temporary, we store the ID to ensure + // that workers are never duplicating job keys. + QueJobID *int64 FileName string ResourceType string } diff --git a/bcdaworker/queueing/manager/que.go b/bcdaworker/queueing/manager/que.go index fa0df274a..983921075 100644 --- a/bcdaworker/queueing/manager/que.go +++ b/bcdaworker/queueing/manager/que.go @@ -101,17 +101,17 @@ func (q *masterQueue) StopQue() { q.quePool.Shutdown() } -func (q *queue) processJob(job *que.Job) error { +func (q *queue) processJob(queJob *que.Job) error { ctx, cancel := context.WithCancel(context.Background()) defer q.updateJobQueueCountCloudwatchMetric() defer cancel() var jobArgs models.JobEnqueueArgs - err := json.Unmarshal(job.Args, &jobArgs) + err := json.Unmarshal(queJob.Args, &jobArgs) if err != nil { // ACK the job because retrying it won't help us be able to deserialize the data - q.log.Warnf("Failed to deserialize job.Args '%s' %s. Removing queuejob from que.", job.Args, err) + q.log.Warnf("Failed to deserialize job.Args '%s' %s. Removing queuejob from que.", queJob.Args, err) return nil } @@ -119,14 +119,14 @@ func (q *queue) processJob(job *que.Job) error { ctx, _ = log.SetCtxLogger(ctx, "job_id", jobArgs.ID) ctx, logger := log.SetCtxLogger(ctx, "transaction_id", jobArgs.TransactionID) - exportJob, err := q.worker.ValidateJob(ctx, jobArgs) + exportJob, err := q.worker.ValidateJob(ctx, queJob.ID, jobArgs) if goerrors.Is(err, worker.ErrParentJobCancelled) { // ACK the job because we do not need to work on queue jobs associated with a cancelled parent job - logger.Warnf("queJob %d associated with a cancelled parent Job %d. Removing queuejob from que.", job.ID, jobArgs.ID) + logger.Warnf("queJob %d associated with a cancelled parent Job %d. Removing queuejob from que.", queJob.ID, jobArgs.ID) return nil } else if goerrors.Is(err, worker.ErrParentJobFailed) { // ACK the job because we do not need to work on queue jobs associated with a failed parent job - logger.Warnf("queJob %d associated with a failed parent Job %d. Removing queuejob from que.", job.ID, jobArgs.ID) + logger.Warnf("queJob %d associated with a failed parent Job %d. Removing queuejob from que.", queJob.ID, jobArgs.ID) return nil } else if goerrors.Is(err, worker.ErrNoBasePathSet) { // Data is corrupted, we cannot work on this job. @@ -136,7 +136,7 @@ func (q *queue) processJob(job *que.Job) error { // Based on the current backoff delay (j.ErrorCount^4 + 3 seconds), this should've given // us plenty of headroom to ensure that the parent job will never be found. maxNotFoundRetries := int32(utils.GetEnvInt("BCDA_WORKER_MAX_JOB_NOT_FOUND_RETRIES", 3)) - if job.ErrorCount >= maxNotFoundRetries { + if queJob.ErrorCount >= maxNotFoundRetries { logger.Errorf("No job found for ID: %d acoID: %s. Retries exhausted. Removing job from queue.", jobArgs.ID, jobArgs.ACOID) // By returning a nil error response, we're singaling to que-go to remove this job from the jobqueue. @@ -145,6 +145,13 @@ func (q *queue) processJob(job *que.Job) error { logger.Warnf("No job found for ID: %d acoID: %s. Will retry.", jobArgs.ID, jobArgs.ACOID) return errors.Wrap(repository.ErrJobNotFound, "could not retrieve job from database") + } else if goerrors.Is(err, worker.ErrQueJobProcessed) { + logger.Warnf("Queue job (que_jobs.id) %d already processed for job.id %d. Checking completion status and removing queuejob from que.", queJob.ID, jobArgs.ID) + _, err := worker.CheckJobCompleteAndCleanup(ctx, q.repository, uint(jobArgs.ID)) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("Error checking job completion & cleanup for job id %d", jobArgs.ID)) + } + return nil } else if err != nil { err := errors.Wrap(err, "failed to validate job") logger.Error(err) @@ -154,7 +161,7 @@ func (q *queue) processJob(job *que.Job) error { // start a goroutine that will periodically check the status of the parent job go checkIfCancelled(ctx, q.repository, cancel, uint(jobArgs.ID), 15) - if err := q.worker.ProcessJob(ctx, *exportJob, jobArgs); err != nil { + if err := q.worker.ProcessJob(ctx, queJob.ID, *exportJob, jobArgs); err != nil { err := errors.Wrap(err, "failed to process job") logger.Error(err) return err diff --git a/bcdaworker/queueing/manager/que_test.go b/bcdaworker/queueing/manager/que_test.go index aa94ed04d..3db744938 100644 --- a/bcdaworker/queueing/manager/que_test.go +++ b/bcdaworker/queueing/manager/que_test.go @@ -139,6 +139,7 @@ func TestProcessJobFailedValidation(t *testing.T) { {"NoBasePath", worker.ErrNoBasePathSet, nil, `^Job \d+ does not contain valid base path`}, {"NoParentJob", worker.ErrParentJobNotFound, repository.ErrJobNotFound, `^No job found for ID: \d+ acoID.*Will retry`}, {"NoParentJobRetriesExceeded", worker.ErrParentJobNotFound, nil, `No job found for ID: \d+ acoID.*Retries exhausted`}, + {"QueJobAlreadyProcessed", worker.ErrQueJobProcessed, nil, `^Queue job \(que_jobs.id\) \d+ already processed for job.id \d+`}, {"OtherError", fmt.Errorf(constants.DefaultError), fmt.Errorf(constants.DefaultError), ""}, } hook := test.NewLocal(testUtils.GetLogger(log.Worker)) @@ -148,12 +149,13 @@ func TestProcessJobFailedValidation(t *testing.T) { worker := &worker.MockWorker{} defer worker.AssertExpectations(t) - queue := &queue{worker: worker, log: logger} + repo := repository.NewMockRepository(t) + queue := &queue{worker: worker, repository: repo, log: logger} job := models.Job{ID: uint(rand.Int31())} jobArgs := models.JobEnqueueArgs{ID: int(job.ID), ACOID: uuid.New()} - var queJob que.Job + queJob := que.Job{ID: 1} queJob.Args, err = json.Marshal(jobArgs) assert.NoError(t, err) @@ -162,7 +164,12 @@ func TestProcessJobFailedValidation(t *testing.T) { queJob.ErrorCount = rand.Int31() } - worker.On("ValidateJob", testUtils.CtxMatcher, jobArgs).Return(nil, tt.validateErr) + worker.On("ValidateJob", testUtils.CtxMatcher, int64(1), jobArgs).Return(nil, tt.validateErr) + + if tt.name == "QueJobAlreadyProcessed" { + job.Status = models.JobStatusCompleted + repo.On("GetJobByID", testUtils.CtxMatcher, job.ID).Return(&job, nil) + } err = queue.processJob(&queJob) if tt.expectedErr == nil { diff --git a/bcdaworker/repository/mock_repository.go b/bcdaworker/repository/mock_repository.go index 9d6682186..a3bbbee6e 100644 --- a/bcdaworker/repository/mock_repository.go +++ b/bcdaworker/repository/mock_repository.go @@ -1,4 +1,4 @@ -// Code generated by mockery v0.0.0-dev. DO NOT EDIT. +// Code generated by mockery v2.43.2. DO NOT EDIT. package repository @@ -20,6 +20,10 @@ type MockRepository struct { func (_m *MockRepository) CreateJobKey(ctx context.Context, jobKey models.JobKey) error { ret := _m.Called(ctx, jobKey) + if len(ret) == 0 { + panic("no return value specified for CreateJobKey") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, models.JobKey) error); ok { r0 = rf(ctx, jobKey) @@ -30,11 +34,37 @@ func (_m *MockRepository) CreateJobKey(ctx context.Context, jobKey models.JobKey return r0 } +// CreateJobKeys provides a mock function with given fields: ctx, jobKeys +func (_m *MockRepository) CreateJobKeys(ctx context.Context, jobKeys []models.JobKey) error { + ret := _m.Called(ctx, jobKeys) + + if len(ret) == 0 { + panic("no return value specified for CreateJobKeys") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []models.JobKey) error); ok { + r0 = rf(ctx, jobKeys) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // GetACOByUUID provides a mock function with given fields: ctx, _a1 func (_m *MockRepository) GetACOByUUID(ctx context.Context, _a1 uuid.UUID) (*models.ACO, error) { ret := _m.Called(ctx, _a1) + if len(ret) == 0 { + panic("no return value specified for GetACOByUUID") + } + var r0 *models.ACO + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (*models.ACO, error)); ok { + return rf(ctx, _a1) + } if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) *models.ACO); ok { r0 = rf(ctx, _a1) } else { @@ -43,7 +73,6 @@ func (_m *MockRepository) GetACOByUUID(ctx context.Context, _a1 uuid.UUID) (*mod } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { r1 = rf(ctx, _a1) } else { @@ -57,7 +86,15 @@ func (_m *MockRepository) GetACOByUUID(ctx context.Context, _a1 uuid.UUID) (*mod func (_m *MockRepository) GetCCLFBeneficiaryByID(ctx context.Context, id uint) (*models.CCLFBeneficiary, error) { ret := _m.Called(ctx, id) + if len(ret) == 0 { + panic("no return value specified for GetCCLFBeneficiaryByID") + } + var r0 *models.CCLFBeneficiary + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint) (*models.CCLFBeneficiary, error)); ok { + return rf(ctx, id) + } if rf, ok := ret.Get(0).(func(context.Context, uint) *models.CCLFBeneficiary); ok { r0 = rf(ctx, id) } else { @@ -66,7 +103,6 @@ func (_m *MockRepository) GetCCLFBeneficiaryByID(ctx context.Context, id uint) ( } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, uint) error); ok { r1 = rf(ctx, id) } else { @@ -80,7 +116,15 @@ func (_m *MockRepository) GetCCLFBeneficiaryByID(ctx context.Context, id uint) ( func (_m *MockRepository) GetJobByID(ctx context.Context, jobID uint) (*models.Job, error) { ret := _m.Called(ctx, jobID) + if len(ret) == 0 { + panic("no return value specified for GetJobByID") + } + var r0 *models.Job + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint) (*models.Job, error)); ok { + return rf(ctx, jobID) + } if rf, ok := ret.Get(0).(func(context.Context, uint) *models.Job); ok { r0 = rf(ctx, jobID) } else { @@ -89,7 +133,6 @@ func (_m *MockRepository) GetJobByID(ctx context.Context, jobID uint) (*models.J } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, uint) error); ok { r1 = rf(ctx, jobID) } else { @@ -99,18 +142,55 @@ func (_m *MockRepository) GetJobByID(ctx context.Context, jobID uint) (*models.J return r0, r1 } +// GetJobKey provides a mock function with given fields: ctx, jobID, queJobID +func (_m *MockRepository) GetJobKey(ctx context.Context, jobID uint, queJobID int64) (*models.JobKey, error) { + ret := _m.Called(ctx, jobID, queJobID) + + if len(ret) == 0 { + panic("no return value specified for GetJobKey") + } + + var r0 *models.JobKey + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint, int64) (*models.JobKey, error)); ok { + return rf(ctx, jobID, queJobID) + } + if rf, ok := ret.Get(0).(func(context.Context, uint, int64) *models.JobKey); ok { + r0 = rf(ctx, jobID, queJobID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.JobKey) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint, int64) error); ok { + r1 = rf(ctx, jobID, queJobID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetJobKeyCount provides a mock function with given fields: ctx, jobID func (_m *MockRepository) GetJobKeyCount(ctx context.Context, jobID uint) (int, error) { ret := _m.Called(ctx, jobID) + if len(ret) == 0 { + panic("no return value specified for GetJobKeyCount") + } + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint) (int, error)); ok { + return rf(ctx, jobID) + } if rf, ok := ret.Get(0).(func(context.Context, uint) int); ok { r0 = rf(ctx, jobID) } else { r0 = ret.Get(0).(int) } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, uint) error); ok { r1 = rf(ctx, jobID) } else { @@ -124,6 +204,10 @@ func (_m *MockRepository) GetJobKeyCount(ctx context.Context, jobID uint) (int, func (_m *MockRepository) IncrementCompletedJobCount(ctx context.Context, jobID uint) error { ret := _m.Called(ctx, jobID) + if len(ret) == 0 { + panic("no return value specified for IncrementCompletedJobCount") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, uint) error); ok { r0 = rf(ctx, jobID) @@ -138,6 +222,10 @@ func (_m *MockRepository) IncrementCompletedJobCount(ctx context.Context, jobID func (_m *MockRepository) UpdateJobStatus(ctx context.Context, jobID uint, new models.JobStatus) error { ret := _m.Called(ctx, jobID, new) + if len(ret) == 0 { + panic("no return value specified for UpdateJobStatus") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, uint, models.JobStatus) error); ok { r0 = rf(ctx, jobID, new) @@ -152,6 +240,10 @@ func (_m *MockRepository) UpdateJobStatus(ctx context.Context, jobID uint, new m func (_m *MockRepository) UpdateJobStatusCheckStatus(ctx context.Context, jobID uint, current models.JobStatus, new models.JobStatus) error { ret := _m.Called(ctx, jobID, current, new) + if len(ret) == 0 { + panic("no return value specified for UpdateJobStatusCheckStatus") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, uint, models.JobStatus, models.JobStatus) error); ok { r0 = rf(ctx, jobID, current, new) @@ -161,3 +253,17 @@ func (_m *MockRepository) UpdateJobStatusCheckStatus(ctx context.Context, jobID return r0 } + +// NewMockRepository creates a new instance of MockRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *MockRepository { + mock := &MockRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/bcdaworker/repository/postgres/repository.go b/bcdaworker/repository/postgres/repository.go index e2b992a95..bbf87af83 100644 --- a/bcdaworker/repository/postgres/repository.go +++ b/bcdaworker/repository/postgres/repository.go @@ -144,8 +144,21 @@ func (r *Repository) IncrementCompletedJobCount(ctx context.Context, jobID uint) func (r *Repository) CreateJobKey(ctx context.Context, jobKey models.JobKey) error { ib := sqlFlavor.NewInsertBuilder().InsertInto("job_keys") - ib.Cols("job_id", "file_name", "resource_type"). - Values(jobKey.JobID, jobKey.FileName, jobKey.ResourceType) + ib.Cols("job_id", "que_job_id", "file_name", "resource_type"). + Values(jobKey.JobID, jobKey.QueJobID, jobKey.FileName, jobKey.ResourceType) + + query, args := ib.Build() + _, err := r.ExecContext(ctx, query, args...) + return err +} + +func (r *Repository) CreateJobKeys(ctx context.Context, jobKeys []models.JobKey) error { + ib := sqlFlavor.NewInsertBuilder().InsertInto("job_keys") + ib.Cols("job_id", "que_job_id", "file_name", "resource_type") + + for _, jobKey := range jobKeys { + ib.Values(jobKey.JobID, jobKey.QueJobID, jobKey.FileName, jobKey.ResourceType) + } query, args := ib.Build() _, err := r.ExecContext(ctx, query, args...) @@ -155,7 +168,7 @@ func (r *Repository) CreateJobKey(ctx context.Context, jobKey models.JobKey) err func (r *Repository) GetJobKeyCount(ctx context.Context, jobID uint) (int, error) { sb := sqlFlavor.NewSelectBuilder().Select("COUNT(1)").From("job_keys") sb.Where(sb.Equal("job_id", jobID)) - sb.Where(sb.NotLike("file_name", "%-error.ndjson%")) //Ignore error files from completed count. + sb.Where(sb.NotLike("file_name", "%-error.ndjson%")) // Ignore error files from completed count. query, args := sb.Build() var count int @@ -165,6 +178,25 @@ func (r *Repository) GetJobKeyCount(ctx context.Context, jobID uint) (int, error return count, nil } +func (r *Repository) GetJobKey(ctx context.Context, jobID uint, queJobID int64) (*models.JobKey, error) { + sb := sqlFlavor.NewSelectBuilder().Select("id").From("job_keys") + sb.Where(sb.And(sb.Equal("job_id", jobID), sb.Equal("que_job_id", queJobID))) + + query, args := sb.Build() + row := r.QueryRowContext(ctx, query, args...) + + jk := &models.JobKey{JobID: jobID, QueJobID: &queJobID} + + if err := row.Scan(&jk.ID); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, repository.ErrJobKeyNotFound + } + return nil, err + } + + return jk, nil +} + func (r *Repository) updateJob(ctx context.Context, clauses map[string]interface{}, fieldAndValues map[string]interface{}) error { ub := sqlFlavor.NewUpdateBuilder().Update("jobs") ub.Set(ub.Assign("updated_at", sqlbuilder.Raw("NOW()"))) diff --git a/bcdaworker/repository/postgres/repository_test.go b/bcdaworker/repository/postgres/repository_test.go index 4aee8d2f0..1f8d86ac5 100644 --- a/bcdaworker/repository/postgres/repository_test.go +++ b/bcdaworker/repository/postgres/repository_test.go @@ -11,6 +11,7 @@ import ( "github.com/CMSgov/bcda-app/bcda/models" "github.com/CMSgov/bcda-app/bcda/models/postgres/postgrestest" "github.com/CMSgov/bcda-app/bcda/testUtils" + "github.com/CMSgov/bcda-app/bcdaworker/repository" "github.com/CMSgov/bcda-app/bcdaworker/repository/postgres" "github.com/pborman/uuid" "github.com/stretchr/testify/assert" @@ -152,16 +153,18 @@ func (r *RepositoryTestSuite) TestJobKeyMethods() { ctx := context.Background() jobID := uint(rand.Int31()) - jk := models.JobKey{JobID: jobID} - jk1 := models.JobKey{JobID: jobID} + queJobID := rand.Int63() + queJobID1 := rand.Int63() + + jk := models.JobKey{JobID: jobID, QueJobID: &queJobID} + jk1 := models.JobKey{JobID: jobID, QueJobID: &queJobID1} jk2 := models.JobKey{JobID: jobID} otherJobID := models.JobKey{JobID: uint(rand.Int31())} defer postgrestest.DeleteJobKeysByJobIDs(r.T(), r.db, jobID, otherJobID.JobID) assert.NoError(r.repository.CreateJobKey(ctx, jk)) - assert.NoError(r.repository.CreateJobKey(ctx, jk1)) - assert.NoError(r.repository.CreateJobKey(ctx, jk2)) + assert.NoError(r.repository.CreateJobKeys(ctx, []models.JobKey{jk1, jk2})) assert.NoError(r.repository.CreateJobKey(ctx, otherJobID)) count, err := r.repository.GetJobKeyCount(ctx, jobID) @@ -175,6 +178,12 @@ func (r *RepositoryTestSuite) TestJobKeyMethods() { count, err = r.repository.GetJobKeyCount(ctx, 0) assert.NoError(err) assert.Equal(0, count) + + _, err = r.repository.GetJobKey(ctx, jobID, queJobID) + assert.NoError(err) + + _, err = r.repository.GetJobKey(ctx, jobID, -1) + assert.EqualError(err, repository.ErrJobKeyNotFound.Error()) } func assertJobsEqual(assert *assert.Assertions, expected, actual models.Job) { diff --git a/bcdaworker/repository/repository.go b/bcdaworker/repository/repository.go index 773f4faab..92de478b1 100644 --- a/bcdaworker/repository/repository.go +++ b/bcdaworker/repository/repository.go @@ -37,11 +37,13 @@ type jobRepository interface { type jobKeyRepository interface { CreateJobKey(ctx context.Context, jobKey models.JobKey) error - + CreateJobKeys(ctx context.Context, jobKeys []models.JobKey) error GetJobKeyCount(ctx context.Context, jobID uint) (int, error) + GetJobKey(ctx context.Context, jobID uint, queJobID int64) (*models.JobKey, error) } var ( - ErrJobNotUpdated = errors.New("job was not updated, no match found") - ErrJobNotFound = errors.New("no job found for given id") + ErrJobNotUpdated = errors.New("job was not updated, no match found") + ErrJobNotFound = errors.New("no job found for given id") + ErrJobKeyNotFound = errors.New("no job key found for given IDs") ) diff --git a/bcdaworker/worker/mock_worker.go b/bcdaworker/worker/mock_worker.go index 47de1faf7..c20a3356c 100644 --- a/bcdaworker/worker/mock_worker.go +++ b/bcdaworker/worker/mock_worker.go @@ -1,4 +1,4 @@ -// Code generated by mockery v0.0.0-dev. DO NOT EDIT. +// Code generated by mockery v2.43.2. DO NOT EDIT. package worker @@ -14,13 +14,17 @@ type MockWorker struct { mock.Mock } -// ProcessJob provides a mock function with given fields: ctx, job, jobArgs -func (_m *MockWorker) ProcessJob(ctx context.Context, job models.Job, jobArgs models.JobEnqueueArgs) error { - ret := _m.Called(ctx, job, jobArgs) +// ProcessJob provides a mock function with given fields: ctx, queJobID, job, jobArgs +func (_m *MockWorker) ProcessJob(ctx context.Context, queJobID int64, job models.Job, jobArgs models.JobEnqueueArgs) error { + ret := _m.Called(ctx, queJobID, job, jobArgs) + + if len(ret) == 0 { + panic("no return value specified for ProcessJob") + } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, models.Job, models.JobEnqueueArgs) error); ok { - r0 = rf(ctx, job, jobArgs) + if rf, ok := ret.Get(0).(func(context.Context, int64, models.Job, models.JobEnqueueArgs) error); ok { + r0 = rf(ctx, queJobID, job, jobArgs) } else { r0 = ret.Error(0) } @@ -28,25 +32,46 @@ func (_m *MockWorker) ProcessJob(ctx context.Context, job models.Job, jobArgs mo return r0 } -// ValidateJob provides a mock function with given fields: ctx, jobArgs -func (_m *MockWorker) ValidateJob(ctx context.Context, jobArgs models.JobEnqueueArgs) (*models.Job, error) { - ret := _m.Called(ctx, jobArgs) +// ValidateJob provides a mock function with given fields: ctx, queJobID, jobArgs +func (_m *MockWorker) ValidateJob(ctx context.Context, queJobID int64, jobArgs models.JobEnqueueArgs) (*models.Job, error) { + ret := _m.Called(ctx, queJobID, jobArgs) + + if len(ret) == 0 { + panic("no return value specified for ValidateJob") + } var r0 *models.Job - if rf, ok := ret.Get(0).(func(context.Context, models.JobEnqueueArgs) *models.Job); ok { - r0 = rf(ctx, jobArgs) + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, models.JobEnqueueArgs) (*models.Job, error)); ok { + return rf(ctx, queJobID, jobArgs) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, models.JobEnqueueArgs) *models.Job); ok { + r0 = rf(ctx, queJobID, jobArgs) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*models.Job) } } - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, models.JobEnqueueArgs) error); ok { - r1 = rf(ctx, jobArgs) + if rf, ok := ret.Get(1).(func(context.Context, int64, models.JobEnqueueArgs) error); ok { + r1 = rf(ctx, queJobID, jobArgs) } else { r1 = ret.Error(1) } return r0, r1 } + +// NewMockWorker creates a new instance of MockWorker. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockWorker(t interface { + mock.TestingT + Cleanup(func()) +}) *MockWorker { + mock := &MockWorker{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/bcdaworker/worker/worker.go b/bcdaworker/worker/worker.go index e558ab2c3..beccacdf9 100644 --- a/bcdaworker/worker/worker.go +++ b/bcdaworker/worker/worker.go @@ -32,8 +32,8 @@ import ( ) type Worker interface { - ValidateJob(ctx context.Context, jobArgs models.JobEnqueueArgs) (*models.Job, error) - ProcessJob(ctx context.Context, job models.Job, jobArgs models.JobEnqueueArgs) error + ValidateJob(ctx context.Context, queJobID int64, jobArgs models.JobEnqueueArgs) (*models.Job, error) + ProcessJob(ctx context.Context, queJobID int64, job models.Job, jobArgs models.JobEnqueueArgs) error } type worker struct { @@ -44,7 +44,7 @@ func NewWorker(db *sql.DB) Worker { return &worker{postgres.NewRepository(db)} } -func (w *worker) ValidateJob(ctx context.Context, jobArgs models.JobEnqueueArgs) (*models.Job, error) { +func (w *worker) ValidateJob(ctx context.Context, queJobID int64, jobArgs models.JobEnqueueArgs) (*models.Job, error) { if len(jobArgs.BBBasePath) == 0 { return nil, ErrNoBasePathSet } @@ -63,10 +63,19 @@ func (w *worker) ValidateJob(ctx context.Context, jobArgs models.JobEnqueueArgs) return nil, ErrParentJobFailed } - return exportJob, nil + _, err = w.r.GetJobKey(ctx, uint(jobArgs.ID), queJobID) + if goerrors.Is(err, repository.ErrJobKeyNotFound) { + // No job key exists, which means this queue job needs to be processed. + return exportJob, nil + } else if err != nil { + return nil, errors.Wrap(err, "could not retrieve job key from database") + } else { + // If there was no error, we found a job key and can avoid re-processing the job. + return nil, ErrQueJobProcessed + } } -func (w *worker) ProcessJob(ctx context.Context, job models.Job, jobArgs models.JobEnqueueArgs) error { +func (w *worker) ProcessJob(ctx context.Context, queJobID int64, job models.Job, jobArgs models.JobEnqueueArgs) error { t := metrics.GetTimer() defer t.Close() @@ -127,7 +136,7 @@ func (w *worker) ProcessJob(ctx context.Context, job models.Job, jobArgs models. return err } - jobKeys, err := writeBBDataToFile(ctx, w.r, bb, *aco.CMSID, jobArgs, tempJobPath) + jobKeys, err := writeBBDataToFile(ctx, w.r, bb, *aco.CMSID, queJobID, jobArgs, tempJobPath) // This is only run AFTER completion of all the collection if err != nil { @@ -151,11 +160,13 @@ func (w *worker) ProcessJob(ctx context.Context, job models.Job, jobArgs models. err = compressFiles(ctx, tempJobPath, stagingPath) if err != nil { logger.Error(err) + return err } err = createJobKeys(ctx, w.r, jobKeys, job.ID) if err != nil { logger.Error(err) + return err } // Not critical since we use the job_keys count as the authoritative list of completed jobs. @@ -231,9 +242,9 @@ func CloseOrLogError(logger logrus.FieldLogger, f *os.File) { // A list of JobKeys are returned, containing the names of files that were created. // Filesnames can be "blank.ndjson", ".ndjson", or "-error.ndjson". func writeBBDataToFile(ctx context.Context, r repository.Repository, bb client.APIClient, - cmsID string, jobArgs models.JobEnqueueArgs, tmpDir string) (jobKeys []models.JobKey, err error) { + cmsID string, queJobID int64, jobArgs models.JobEnqueueArgs, tmpDir string) (jobKeys []models.JobKey, err error) { - jobKeys = append(jobKeys, models.JobKey{JobID: uint(jobArgs.ID), FileName: models.BlankFileName, ResourceType: jobArgs.ResourceType}) + jobKeys = append(jobKeys, models.JobKey{JobID: uint(jobArgs.ID), QueJobID: &queJobID, FileName: models.BlankFileName, ResourceType: jobArgs.ResourceType}) logger := log.GetCtxLogger(ctx) close := metrics.NewChild(ctx, "writeBBDataToFile") @@ -366,7 +377,7 @@ func writeBBDataToFile(ctx context.Context, r repository.Repository, bb client.A } if errorCount > 0 { - jobKeys = append(jobKeys, models.JobKey{JobID: uint(jobArgs.ID), FileName: fileUUID + "-error.ndjson", ResourceType: jobArgs.ResourceType}) + jobKeys = append(jobKeys, models.JobKey{JobID: uint(jobArgs.ID), QueJobID: &queJobID, FileName: fileUUID + "-error.ndjson", ResourceType: jobArgs.ResourceType}) } return jobKeys, nil } @@ -468,7 +479,7 @@ func fhirBundleToResourceNDJSON(ctx context.Context, w *bufio.Writer, b *fhirmod } } -func checkJobCompleteAndCleanup(ctx context.Context, r repository.Repository, jobID uint) (jobCompleted bool, err error) { +func CheckJobCompleteAndCleanup(ctx context.Context, r repository.Repository, jobID uint) (jobCompleted bool, err error) { logger := log.GetCtxLogger(ctx) j, err := r.GetJobByID(ctx, jobID) if err != nil { @@ -532,17 +543,21 @@ func checkJobCompleteAndCleanup(ctx context.Context, r repository.Repository, jo } func createJobKeys(ctx context.Context, r repository.Repository, jobKeys []models.JobKey, id uint) error { - for i := 0; i < len(jobKeys); i++ { - if err := r.CreateJobKey(ctx, jobKeys[i]); err != nil { - err = errors.Wrap(err, fmt.Sprintf("Error creating job key record for filename %s", jobKeys[i].FileName)) - return err - } - _, err := checkJobCompleteAndCleanup(ctx, r, id) - if err != nil { - err = errors.Wrap(err, fmt.Sprintf("Error checking job completion & cleanup for filename %s", jobKeys[i].FileName)) - return err + if err := r.CreateJobKeys(ctx, jobKeys); err != nil { + filenames := "" + for _, jobKey := range jobKeys { + filenames += " " + jobKey.FileName } + err = errors.Wrap(err, fmt.Sprintf("Error creating job key records for filenames%s", filenames)) + return err } + + _, err := CheckJobCompleteAndCleanup(ctx, r, id) + if err != nil { + err = errors.Wrap(err, fmt.Sprintf("Error checking job completion & cleanup for job id %d", id)) + return err + } + return nil } @@ -571,4 +586,5 @@ var ( ErrParentJobNotFound = JobError{"parent job not found"} ErrParentJobCancelled = JobError{"parent job cancelled"} ErrParentJobFailed = JobError{"parent job failed"} + ErrQueJobProcessed = JobError{"que job already processed"} ) diff --git a/bcdaworker/worker/worker_test.go b/bcdaworker/worker/worker_test.go index 181ebec85..fde61e2ad 100644 --- a/bcdaworker/worker/worker_test.go +++ b/bcdaworker/worker/worker_test.go @@ -159,7 +159,7 @@ func (s *WorkerTestSuite) TestWriteResourcesToFile() { for _, tt := range tests { ctx, jobArgs, bbc := SetupWriteResourceToFile(s, tt.resource) - jobKeys, err := writeBBDataToFile(ctx, s.r, bbc, *s.testACO.CMSID, jobArgs, s.tempDir) + jobKeys, err := writeBBDataToFile(ctx, s.r, bbc, *s.testACO.CMSID, rand.Int63(), jobArgs, s.tempDir) if tt.err == nil { assert.NoError(s.T(), err) } else { @@ -262,7 +262,7 @@ func (s *WorkerTestSuite) TestWriteEmptyResourceToFile() { jobArgs := models.JobEnqueueArgs{ID: s.jobID, ResourceType: "ExplanationOfBenefit", BeneficiaryIDs: cclfBeneficiaryIDs, TransactionTime: transactionTime, ACOID: s.testACO.UUID.String()} // Set up the mock function to return the expected values bbc.On("GetExplanationOfBenefit", jobArgs, "abcdef12000", client.ClaimsWindow{}).Return(bbc.GetBundleData("ExplanationOfBenefitEmpty", "abcdef12000")) - jobKeys, err := writeBBDataToFile(s.logctx, s.r, &bbc, *s.testACO.CMSID, jobArgs, s.tempDir) + jobKeys, err := writeBBDataToFile(s.logctx, s.r, &bbc, *s.testACO.CMSID, rand.Int63(), jobArgs, s.tempDir) assert.EqualValues(s.T(), "blank.ndjson", jobKeys[0].FileName) assert.NoError(s.T(), err) } @@ -293,7 +293,7 @@ func (s *WorkerTestSuite) TestWriteEOBDataToFileWithErrorsBelowFailureThreshold( bbc.On("GetExplanationOfBenefit", jobArgs, "abcdef10000", claimsWindowMatcher()).Return(nil, errors.New("error")) bbc.On("GetExplanationOfBenefit", jobArgs, "abcdef11000", claimsWindowMatcher()).Return(nil, errors.New("error")) bbc.On("GetExplanationOfBenefit", jobArgs, "abcdef12000", claimsWindowMatcher()).Return(bbc.GetBundleData("ExplanationOfBenefit", "abcdef12000")) - jobKeys, err := writeBBDataToFile(s.logctx, s.r, &bbc, *s.testACO.CMSID, jobArgs, s.tempDir) + jobKeys, err := writeBBDataToFile(s.logctx, s.r, &bbc, *s.testACO.CMSID, rand.Int63(), jobArgs, s.tempDir) assert.NotEqual(s.T(), "blank.ndjson", jobKeys[0].FileName) assert.Contains(s.T(), jobKeys[1].FileName, "error.ndjson") assert.Len(s.T(), jobKeys, 2) @@ -347,7 +347,7 @@ func (s *WorkerTestSuite) TestWriteEOBDataToFileWithErrorsAboveFailureThreshold( jobArgs.BeneficiaryIDs = cclfBeneficiaryIDs err = createDir(s.tempDir) assert.NoError(s.T(), err) - jobKeys, err := writeBBDataToFile(s.logctx, s.r, &bbc, *s.testACO.CMSID, jobArgs, s.tempDir) + jobKeys, err := writeBBDataToFile(s.logctx, s.r, &bbc, *s.testACO.CMSID, rand.Int63(), jobArgs, s.tempDir) assert.Len(s.T(), jobKeys, 1) assert.Contains(s.T(), err.Error(), "Number of failed requests has exceeded threshold") @@ -390,7 +390,7 @@ func (s *WorkerTestSuite) TestWriteEOBDataToFile_BlueButtonIDNotFound() { } jobArgs := models.JobEnqueueArgs{ID: s.jobID, ResourceType: "ExplanationOfBenefit", BeneficiaryIDs: cclfBeneficiaryIDs, TransactionTime: time.Now(), ACOID: s.testACO.UUID.String()} - jobKeys, err := writeBBDataToFile(s.logctx, s.r, &bbc, *s.testACO.CMSID, jobArgs, s.tempDir) + jobKeys, err := writeBBDataToFile(s.logctx, s.r, &bbc, *s.testACO.CMSID, rand.Int63(), jobArgs, s.tempDir) assert.Len(s.T(), jobKeys, 1) assert.Equal(s.T(), jobKeys[0].FileName, "blank.ndjson") assert.Contains(s.T(), err.Error(), "Number of failed requests has exceeded threshold") @@ -484,7 +484,7 @@ func (s *WorkerTestSuite) TestProcessJobEOB() { } postgrestest.CreateJobs(s.T(), s.db, &j) - complete, err := checkJobCompleteAndCleanup(s.logctx, s.r, j.ID) + complete, err := CheckJobCompleteAndCleanup(s.logctx, s.r, j.ID) assert.Nil(s.T(), err) assert.False(s.T(), complete) @@ -501,7 +501,7 @@ func (s *WorkerTestSuite) TestProcessJobEOB() { ctx, logger := log.SetCtxLogger(ctx, "transaction_id", jobArgs.TransactionID) logHook = test.NewLocal(testUtils.GetLogger(logger)) - err = s.w.ProcessJob(ctx, j, jobArgs) + err = s.w.ProcessJob(ctx, rand.Int63(), j, jobArgs) entries := logHook.AllEntries() assert.Nil(s.T(), err) @@ -509,7 +509,7 @@ func (s *WorkerTestSuite) TestProcessJobEOB() { assert.Contains(s.T(), entries[0].Data, "job_id") assert.Contains(s.T(), entries[0].Data, "transaction_id") - _, err = checkJobCompleteAndCleanup(ctx, s.r, j.ID) + _, err = CheckJobCompleteAndCleanup(ctx, s.r, j.ID) assert.Nil(s.T(), err) completedJob, err := s.r.GetJobByID(context.Background(), j.ID) fmt.Printf("%+v", completedJob) @@ -540,7 +540,7 @@ func (s *WorkerTestSuite) TestProcessJobUpdateJobCheckStatus() { r.On("GetACOByUUID", testUtils.CtxMatcher, j.ACOID).Return(s.testACO, nil) r.On("UpdateJobStatusCheckStatus", testUtils.CtxMatcher, uint(jobArgs.ID), models.JobStatusPending, models.JobStatusInProgress).Return(errors.New("failure")) w := &worker{r} - err := w.ProcessJob(ctx, j, jobArgs) + err := w.ProcessJob(ctx, rand.Int63(), j, jobArgs) assert.NotNil(s.T(), err) } @@ -566,7 +566,7 @@ func (s *WorkerTestSuite) TestProcessJobACOUUID() { defer r.AssertExpectations(s.T()) r.On("GetACOByUUID", testUtils.CtxMatcher, j.ACOID).Return(nil, repository.ErrJobNotFound) w := &worker{r} - err := w.ProcessJob(ctx, j, jobArgs) + err := w.ProcessJob(ctx, rand.Int63(), j, jobArgs) assert.NotNil(s.T(), err) } @@ -658,7 +658,7 @@ func (s *WorkerTestSuite) TestProcessJob_NoBBClient() { defer conf.SetEnv(s.T(), "BB_CLIENT_CERT_FILE", origBBCert) conf.UnsetEnv(s.T(), "BB_CLIENT_CERT_FILE") - assert.Contains(s.T(), s.w.ProcessJob(s.logctx, j, jobArgs).Error(), "could not create Blue Button client") + assert.Contains(s.T(), s.w.ProcessJob(s.logctx, rand.Int63(), j, jobArgs).Error(), "could not create Blue Button client") } func (s *WorkerTestSuite) TestJobCancelledTerminalStatus() { @@ -679,7 +679,7 @@ func (s *WorkerTestSuite) TestJobCancelledTerminalStatus() { BBBasePath: constants.TestFHIRPath, } - processJobErr := s.w.ProcessJob(ctx, j, jobArgs) + processJobErr := s.w.ProcessJob(ctx, rand.Int63(), j, jobArgs) completedJob, _ := s.r.GetJobByID(ctx, j.ID) // cancelled parent job status should not update after failed queuejob @@ -744,7 +744,7 @@ func (s *WorkerTestSuite) TestProcessJobInvalidDirectory() { BBBasePath: constants.TestFHIRPath, } - processJobErr := s.w.ProcessJob(ctx, j, jobArgs) + processJobErr := s.w.ProcessJob(ctx, rand.Int63(), j, jobArgs) // cancelled parent job status should not update after failed queuejob if tt.payloadFail || tt.stagingFail || tt.tempDirFail { @@ -812,7 +812,7 @@ func (s *WorkerTestSuite) TestCheckJobCompleteAndCleanup() { } } - completed, err := checkJobCompleteAndCleanup(s.logctx, repository, jobID) + completed, err := CheckJobCompleteAndCleanup(s.logctx, repository, jobID) assert.NoError(t, err) assert.Equal(t, tt.completed, completed) @@ -852,8 +852,18 @@ func (s *WorkerTestSuite) TestValidateJob() { Return(&models.Job{ID: uint(jobCancelled.ID), Status: models.JobStatusCancelled}, nil) r.On("GetJobByID", testUtils.CtxMatcher, uint(jobFailed.ID)). Return(&models.Job{ID: uint(jobCancelled.ID), Status: models.JobStatusFailed}, nil) + r.On("GetJobByID", testUtils.CtxMatcher, uint(validJob.ID)). Return(&models.Job{ID: uint(validJob.ID), Status: models.JobStatusPending}, nil) + r.On("GetJobKey", testUtils.CtxMatcher, uint(validJob.ID), int64(0)). + Return(nil, repository.ErrJobKeyNotFound) + + // Return existing job key, indicating que job was already processed. + r.On("GetJobKey", testUtils.CtxMatcher, uint(validJob.ID), int64(1)). + Return(&models.JobKey{ID: uint(validJob.ID)}, nil) + + r.On("GetJobKey", testUtils.CtxMatcher, uint(validJob.ID), int64(2)). + Return(nil, fmt.Errorf("some db error")) defer func() { r.AssertExpectations(s.T()) @@ -861,29 +871,37 @@ func (s *WorkerTestSuite) TestValidateJob() { r.AssertNotCalled(s.T(), "GetJobByID", testUtils.CtxMatcher, uint(noBasePath.ID)) }() - j, err := w.ValidateJob(ctx, noBasePath) + j, err := w.ValidateJob(ctx, 0, noBasePath) assert.Nil(s.T(), j) assert.Contains(s.T(), err.Error(), ErrNoBasePathSet.Error()) - j, err = w.ValidateJob(ctx, jobNotFound) + j, err = w.ValidateJob(ctx, 0, jobNotFound) assert.Nil(s.T(), j) assert.Contains(s.T(), err.Error(), ErrParentJobNotFound.Error()) - j, err = w.ValidateJob(ctx, dbErr) + j, err = w.ValidateJob(ctx, 0, dbErr) assert.Nil(s.T(), j) assert.Contains(s.T(), err.Error(), "some db error") - j, err = w.ValidateJob(ctx, jobCancelled) + j, err = w.ValidateJob(ctx, 0, jobCancelled) assert.Nil(s.T(), j) assert.Contains(s.T(), err.Error(), ErrParentJobCancelled.Error()) - j, err = w.ValidateJob(ctx, jobFailed) + j, err = w.ValidateJob(ctx, 0, jobFailed) assert.Nil(s.T(), j) assert.Contains(s.T(), err.Error(), ErrParentJobFailed.Error()) - j, err = w.ValidateJob(ctx, validJob) + j, err = w.ValidateJob(ctx, 0, validJob) assert.NoError(s.T(), err) assert.EqualValues(s.T(), validJob.ID, j.ID) + + j, err = w.ValidateJob(ctx, 1, validJob) + assert.Nil(s.T(), j) + assert.Contains(s.T(), err.Error(), ErrQueJobProcessed.Error()) + + j, err = w.ValidateJob(ctx, 2, validJob) + assert.Nil(s.T(), j) + assert.Contains(s.T(), err.Error(), "could not retrieve job key from database: some db error") } func (s *WorkerTestSuite) TestCreateJobKeys() { @@ -895,7 +913,7 @@ func (s *WorkerTestSuite) TestCreateJobKeys() { } postgrestest.CreateJobs(s.T(), s.db, &j) - complete, err := checkJobCompleteAndCleanup(s.logctx, s.r, j.ID) + complete, err := CheckJobCompleteAndCleanup(s.logctx, s.r, j.ID) assert.Nil(s.T(), err) assert.False(s.T(), complete) @@ -911,6 +929,36 @@ func (s *WorkerTestSuite) TestCreateJobKeys() { } } +func (s *WorkerTestSuite) TestCreateJobKeys_CreateJobKeysError() { + r := &repository.MockRepository{} + + keys := []models.JobKey{ + {JobID: 1, FileName: models.BlankFileName, ResourceType: "Patient"}, + {JobID: 1, FileName: uuid.New() + ".ndjson", ResourceType: "Coverage"}, + } + + r.On("CreateJobKeys", testUtils.CtxMatcher, mock.Anything).Return(fmt.Errorf("some db error")) + err := createJobKeys(s.logctx, r, keys, 1234) + assert.ErrorContains(s.T(), err, "Error creating job key records for filenames") + assert.ErrorContains(s.T(), err, keys[0].FileName) + assert.ErrorContains(s.T(), err, keys[1].FileName) + assert.ErrorContains(s.T(), err, "some db error") +} + +func (s *WorkerTestSuite) TestCreateJobKeys_JobCompleteError() { + r := &repository.MockRepository{} + + keys := []models.JobKey{ + {JobID: 1, FileName: models.BlankFileName, ResourceType: "Patient"}, + {JobID: 1, FileName: uuid.New() + ".ndjson", ResourceType: "Coverage"}, + } + r.On("CreateJobKeys", testUtils.CtxMatcher, mock.Anything).Return(nil) + r.On("GetJobByID", testUtils.CtxMatcher, uint(1)).Return(nil, fmt.Errorf("some db error")) + err := createJobKeys(s.logctx, r, keys, 1) + assert.ErrorContains(s.T(), err, "Failed retrieve job by id (Job 1)") + assert.ErrorContains(s.T(), err, "some db error") +} + func generateUniqueJobID(t *testing.T, db *sql.DB, acoID uuid.UUID) int { j := models.Job{ ACOID: acoID, diff --git a/db/migrations/bcda/000016_add_que_job_id.down.sql b/db/migrations/bcda/000016_add_que_job_id.down.sql new file mode 100644 index 000000000..284b81561 --- /dev/null +++ b/db/migrations/bcda/000016_add_que_job_id.down.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE public.job_keys DROP COLUMN IF EXISTS que_job_id; +DROP INDEX IF EXISTS idx_job_keys_job_id_que_job_id; + +COMMIT; \ No newline at end of file diff --git a/db/migrations/bcda/000016_add_que_job_id.up.sql b/db/migrations/bcda/000016_add_que_job_id.up.sql new file mode 100644 index 000000000..95c6d61a3 --- /dev/null +++ b/db/migrations/bcda/000016_add_que_job_id.up.sql @@ -0,0 +1,8 @@ +-- Add que_job_id to job_keys table + +BEGIN; + +ALTER TABLE public.job_keys ADD COLUMN que_job_id bigint DEFAULT null; +CREATE INDEX IF NOT EXISTS idx_job_keys_job_id_que_job_id ON public.job_keys USING btree (job_id, que_job_id); + +COMMIT; \ No newline at end of file diff --git a/db/migrations/migrations_test.go b/db/migrations/migrations_test.go index e9e8ff318..e27fe8196 100644 --- a/db/migrations/migrations_test.go +++ b/db/migrations/migrations_test.go @@ -327,9 +327,48 @@ func (s *MigrationTestSuite) TestBCDAMigration() { assertColumnExists(t, false, db, "acos", "public_key") }, }, + { + "Increasing cmsid size", + func(t *testing.T) { + migrator.runMigration(t, 15) + assertColumnCharacterMaxLength(t, 8, db, "acos", "cms_id") + assertColumnCharacterMaxLength(t, 8, db, "cclf_files", "aco_cms_id") + assertColumnCharacterMaxLength(t, 8, db, "suppressions", "aco_cms_id") + }, + }, + { + "Adding que_job_id to job_keys table", + func(t *testing.T) { + assertColumnExists(t, false, db, "job_keys", "que_job_id") + assertIndexExists(t, false, db, "job_keys", "idx_job_keys_job_id_que_job_id") + migrator.runMigration(t, 16) + assertColumnExists(t, true, db, "job_keys", "que_job_id") + assertColumnDefaultValue(t, db, "que_job_id", nullValue, []interface{}{"job_keys"}) + assertIndexExists(t, true, db, "job_keys", "idx_job_keys_job_id_que_job_id") + }, + }, // ********************************************************** // * down migrations tests begin here with test number - 1 * // ********************************************************** + { + "Removing que_job_id from job_keys table", + func(t *testing.T) { + assertColumnExists(t, true, db, "job_keys", "que_job_id") + assertIndexExists(t, true, db, "job_keys", "idx_job_keys_job_id_que_job_id") + migrator.runMigration(t, 15) + assertColumnExists(t, false, db, "job_keys", "que_job_id") + assertIndexExists(t, false, db, "job_keys", "idx_job_keys_job_id_que_job_id") + }, + }, + { + "Reverting cmsid size", + func(t *testing.T) { + migrator.runMigration(t, 14) + assertColumnCharacterMaxLength(t, 5, db, "acos", "cms_id") + assertColumnCharacterMaxLength(t, 5, db, "cclf_files", "aco_cms_id") + assertColumnCharacterMaxLength(t, 5, db, "suppressions", "aco_cms_id") + }, + }, { "Restore alpha_secret for acos table", func(t *testing.T) { @@ -593,6 +632,15 @@ func assertTableExists(t *testing.T, shouldExist bool, db *sql.DB, tableName str assert.Equal(t, expected, count) } +func assertColumnCharacterMaxLength(t *testing.T, expectedLength int, db *sql.DB, tableName, columnName string) { + sb := sqlFlavor.NewSelectBuilder().Select("character_maximum_length").From("information_schema.columns ") + sb.Where(sb.Equal("table_name", tableName), sb.Equal("column_name", columnName)) + query, args := sb.Build() + var maxLength int + assert.NoError(t, db.QueryRow(query, args...).Scan(&maxLength)) + assert.Equal(t, expectedLength, maxLength) +} + func assertColumnDefaultValue(t *testing.T, db *sql.DB, columnName, expectedDefault string, tables []interface{}) { sb := sqlFlavor.NewSelectBuilder() sb.Select("table_name", "column_default"). From 0d9986b1726171a5eb74c0b6cf9a83aa510142e4 Mon Sep 17 00:00:00 2001 From: austincanada <162146803+austincanada@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:30:37 -0400 Subject: [PATCH 09/16] BCDA-8148: Update Postgres versions (#965) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates Postgres versions in docker files to correctly mirror what's being used in AWS. (15.5) ## ๐ŸŽซ Ticket https://jira.cms.gov/browse/BCDA-8148 ## ๐Ÿ›  Changes Updated dockerfiles ## โ„น๏ธ Context Updating dockerfiles in order to properly reflect what's available and being used within AWS. ## ๐Ÿงช Validation Screenshot 2024-06-28 at 3 46 19โ€ฏPM --- docker-compose.test.yml | 10 +++++----- docker-compose.yml | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 789b9a418..3c38b080d 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -28,7 +28,7 @@ services: - ${HOME}/.cache/go-build:/root/.cache/go-build - ${GOPATH}/pkg/mod:/go/pkg/mod db-unit-test: - image: postgres:13 + image: postgres:15 environment: - POSTGRES_PASSWORD=toor - POSTGRES_DB=bcda_test @@ -37,7 +37,7 @@ services: volumes: - ./db/testing/docker-entrypoint-initdb.d/:/docker-entrypoint-initdb.d/ # Pass a flag so we'll log all queries executed on the test db. - command: [ "postgres", "-c", "log_statement=all" ] + command: ["postgres", "-c", "log_statement=all"] # Spin up a local S3 server for CCLF and Opt Out File import testing localstack: image: localstack/localstack:latest @@ -47,12 +47,12 @@ services: - SERVICES=s3,ssm,sts,iam - DEBUG=1 ports: - - '4566-4583:4566-4583' + - "4566-4583:4566-4583" volumes: - "./.localstack_volume:/var/lib/localstack" - - '/var/run/docker.sock:/var/run/docker.sock' + - "/var/run/docker.sock:/var/run/docker.sock" healthcheck: - test: "curl --silent --fail localstack:4566/_localstack/health | grep -E '\"s3\": \"(available|running)\"'" + test: 'curl --silent --fail localstack:4566/_localstack/health | grep -E ''"s3": "(available|running)"''' interval: 10s retries: 12 start_period: 30s diff --git a/docker-compose.yml b/docker-compose.yml index f719222ae..8cec19656 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,14 +3,14 @@ version: "3" services: queue: - image: postgres:11 + image: postgres:15 environment: - POSTGRES_DB=bcda_queue - POSTGRES_PASSWORD=toor ports: - "5433:5432" db: - image: postgres:11 + image: postgres:15 environment: - POSTGRES_DB=bcda - POSTGRES_PASSWORD=toor @@ -25,7 +25,7 @@ services: args: ENVIRONMENT: development entrypoint: "" - command: [ "../scripts/watch.sh", "api", "bcda", "start-api" ] + command: ["../scripts/watch.sh", "api", "bcda", "start-api"] env_file: - ./shared_files/decrypted/local.env environment: @@ -48,7 +48,7 @@ services: args: ENVIRONMENT: development entrypoint: "" - command: [ "../scripts/watch.sh", "worker", "bcdaworker" ] + command: ["../scripts/watch.sh", "worker", "bcdaworker"] env_file: - ./shared_files/decrypted/local.env environment: From a4b8a28e9f1dbe5acd426c35e2cffdcd0473641e Mon Sep 17 00:00:00 2001 From: Kevin Yeh Date: Tue, 2 Jul 2024 13:40:25 -0400 Subject: [PATCH 10/16] BCDA-8227: Remove usage of completed_job_count in ALR jobs (#966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽซ Ticket https://jira.cms.gov/browse/BCDA-8227 ## ๐Ÿ›  Changes - Replace completed_job_count with a count of unique job keys (based on job_key.que_job_id) - Avoid re-processing ALR job if que job was already processed ## โ„น๏ธ Context As part of #964, I discovered that the completed_job_count was still being used as part of ALR jobs. This applies the same fix from the "normal" export jobs in #962. Note that many different job keys can be produced by an ALR job. To simplify the determination of whether a que job was processed, I added a new "getUniqueJobKeyCount" method. For some historical context, the ALR endpoints are not enabled in deployed environments - see #853 for more details. To avoid making a decision on whether to remove the code entirely, I am applying these changes for now to remove completed_job_count. ## ๐Ÿงช Validation Unit Testing --- bcdaworker/queueing/manager/alr.go | 47 ++++++++++++-------- bcdaworker/queueing/manager/que_test.go | 10 ++++- bcdaworker/repository/mock_repository.go | 28 ++++++++++++ bcdaworker/repository/postgres/repository.go | 12 +++++ bcdaworker/repository/repository.go | 1 + bcdaworker/worker/alr.go | 39 +++++++++------- bcdaworker/worker/alr_test.go | 5 ++- 7 files changed, 104 insertions(+), 38 deletions(-) diff --git a/bcdaworker/queueing/manager/alr.go b/bcdaworker/queueing/manager/alr.go index f95595850..40107555f 100644 --- a/bcdaworker/queueing/manager/alr.go +++ b/bcdaworker/queueing/manager/alr.go @@ -64,7 +64,7 @@ func checkIfCancelled(ctx context.Context, r repository.Repository, // startALRJob is the Job that the worker will run from the pool. This function // has been written here (alr.go) to separate from beneficiary FHIR workflow. // This job is handled by the same worker pool that works on beneficiary. -func (q *masterQueue) startAlrJob(job *que.Job) error { +func (q *masterQueue) startAlrJob(queJob *que.Job) error { // Creating Context for possible cancellation; used by checkIfCancelled fn ctx := context.Background() @@ -73,13 +73,13 @@ func (q *masterQueue) startAlrJob(job *que.Job) error { // Unmarshall JSON that contains the job details var jobArgs models.JobAlrEnqueueArgs - err := json.Unmarshal(job.Args, &jobArgs) + err := json.Unmarshal(queJob.Args, &jobArgs) // If we cannot unmarshall this, it would be very problematic. if err != nil { // TODO: perhaps just fail the job? // Currently, we retry it q.alrLog.Warnf("Failed to unmarhall job.Args '%s' %s.", - job.Args, err) + queJob.Args, err) } // Validate the job like bcdaworker/worker/worker.go#L43 @@ -90,7 +90,7 @@ func (q *masterQueue) startAlrJob(job *que.Job) error { if errors.Is(err, repository.ErrJobNotFound) { // Parent job is not found // If parent job is not found reach maxretry, fail the job - if job.ErrorCount >= q.MaxRetry { + if queJob.ErrorCount >= q.MaxRetry { q.alrLog.Errorf("No job found for ID: %d acoID: %s. Retries exhausted. Removing job from queue.", jobArgs.ID, jobArgs.CMSID) // By returning a nil error response, we're singaling to que-go to remove this job from the jobqueue. @@ -100,20 +100,30 @@ func (q *masterQueue) startAlrJob(job *que.Job) error { return fmt.Errorf("could not retrieve job from database") } // Else that job just doesn't exist - return fmt.Errorf("failed to valiate job: %w", err) + return fmt.Errorf("failed to validate job: %w", err) } // If the job was cancelled... if alrJobs.Status == models.JobStatusCancelled { q.alrLog.Warnf("ALR big job has been cancelled, worker will not be tasked for %s", - job.Args) + queJob.Args) return nil } // If the job has been failed by a previous worker... if alrJobs.Status == models.JobStatusFailed { q.alrLog.Warnf("ALR big job has been failed, worker will not be tasked for %s", - job.Args) + queJob.Args) return nil } + // if the que job was already processed... + _, err = q.repository.GetJobKey(ctx, uint(jobArgs.ID), queJob.ID) + if err == nil { + // If there was no error, we found a job key and can avoid re-processing the job. + q.alrLog.Warnf("ALR que job (que_jobs.id) %d has already been processed, worker will not be tasked for %s", queJob.ID, queJob.Args) + return nil + } + if !errors.Is(err, repository.ErrJobKeyNotFound) { + return fmt.Errorf("Failed to search for job keys in database: %w", err) + } // End of validation // Check if the job was cancelled @@ -121,7 +131,7 @@ func (q *masterQueue) startAlrJob(job *que.Job) error { // Before moving forward, check if this job has failed before // If it has reached the maxRetry, stop the parent job - if job.ErrorCount > q.MaxRetry { + if queJob.ErrorCount > q.MaxRetry { // Fail the job - ALL OR NOTHING err = q.repository.UpdateJobStatus(ctx, jobArgs.ID, models.JobStatusFailed) if err != nil { @@ -142,16 +152,16 @@ func (q *masterQueue) startAlrJob(job *que.Job) error { // don't fail the job. if !errors.Is(err, repository.ErrJobNotUpdated) { q.alrLog.Warnf("Failed to update job status '%s' %s.", - job.Args, err) + queJob.Args, err) return err } } // Run ProcessAlrJob, which is the meat of the whole operation - err = q.alrWorker.ProcessAlrJob(ctx, jobArgs) + err = q.alrWorker.ProcessAlrJob(ctx, queJob.ID, jobArgs) if err != nil { // This means the job did not finish - q.alrLog.Warnf("Failed to complete job.Args '%s' %s", job.Args, err) + q.alrLog.Warnf("Failed to complete job.Args '%s' %s", queJob.Args, err) // Re-enqueue the job return err } @@ -177,7 +187,7 @@ func (q *masterQueue) startAlrJob(job *que.Job) error { err = q.repository.UpdateJobStatus(ctx, jobArgs.ID, models.JobStatusCompleted) if err != nil { // This means the job did not finish for various reason - q.alrLog.Warnf("Failed to update job to complete for '%s' %s", job.Args, err) + q.alrLog.Warnf("Failed to update job to complete for '%s' %s", queJob.Args, err) // Re-enqueue the job return err } @@ -201,16 +211,15 @@ func (q *masterQueue) isJobComplete(ctx context.Context, jobID uint) (bool, erro return false, nil } - // Possible source of error - //completedCount, err := q.repository.GetJobKeyCount(ctx, jobID) - //if err != nil { - //return false, fmt.Errorf("failed to get job key count: %w", err) - //} + completedCount, err := q.repository.GetUniqueJobKeyCount(ctx, jobID) + if err != nil { + return false, fmt.Errorf("failed to get job key count: %w", err) + } - if j.CompletedJobCount >= j.JobCount { + if completedCount >= j.JobCount { q.alrLog.WithFields(logrus.Fields{ "jobID": j.ID, - "jobCount": j.JobCount, "completedJobCount": j.CompletedJobCount}). + "jobCount": j.JobCount, "completedJobCount": completedCount}). Println("Excess number of jobs completed.") return true, nil } diff --git a/bcdaworker/queueing/manager/que_test.go b/bcdaworker/queueing/manager/que_test.go index 3db744938..da2cc8c5b 100644 --- a/bcdaworker/queueing/manager/que_test.go +++ b/bcdaworker/queueing/manager/que_test.go @@ -262,16 +262,24 @@ func TestStartAlrJob(t *testing.T) { // Since the worker is tested by BFD, it is not tested here // and we jump straight to the work err = master.startAlrJob(&que.Job{ + ID: rand.Int63(), Args: jobArgsJson, }) assert.NoError(t, err) + + // Check job is in progress + alrJob, err := r.GetJobByID(ctx, id) + assert.NoError(t, err) + assert.Equal(t, models.JobStatusInProgress, alrJob.Status) + err = master.startAlrJob(&que.Job{ + ID: rand.Int63(), Args: jobArgsJson2, }) assert.NoError(t, err) // Check job is complete - alrJob, err := r.GetJobByID(ctx, id) + alrJob, err = r.GetJobByID(ctx, id) assert.NoError(t, err) assert.Equal(t, models.JobStatusCompleted, alrJob.Status) } diff --git a/bcdaworker/repository/mock_repository.go b/bcdaworker/repository/mock_repository.go index a3bbbee6e..c19d18cd6 100644 --- a/bcdaworker/repository/mock_repository.go +++ b/bcdaworker/repository/mock_repository.go @@ -200,6 +200,34 @@ func (_m *MockRepository) GetJobKeyCount(ctx context.Context, jobID uint) (int, return r0, r1 } +// GetUniqueJobKeyCount provides a mock function with given fields: ctx, jobID +func (_m *MockRepository) GetUniqueJobKeyCount(ctx context.Context, jobID uint) (int, error) { + ret := _m.Called(ctx, jobID) + + if len(ret) == 0 { + panic("no return value specified for GetUniqueJobKeyCount") + } + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint) (int, error)); ok { + return rf(ctx, jobID) + } + if rf, ok := ret.Get(0).(func(context.Context, uint) int); ok { + r0 = rf(ctx, jobID) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func(context.Context, uint) error); ok { + r1 = rf(ctx, jobID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // IncrementCompletedJobCount provides a mock function with given fields: ctx, jobID func (_m *MockRepository) IncrementCompletedJobCount(ctx context.Context, jobID uint) error { ret := _m.Called(ctx, jobID) diff --git a/bcdaworker/repository/postgres/repository.go b/bcdaworker/repository/postgres/repository.go index bbf87af83..5b791a337 100644 --- a/bcdaworker/repository/postgres/repository.go +++ b/bcdaworker/repository/postgres/repository.go @@ -178,6 +178,18 @@ func (r *Repository) GetJobKeyCount(ctx context.Context, jobID uint) (int, error return count, nil } +func (r *Repository) GetUniqueJobKeyCount(ctx context.Context, jobID uint) (int, error) { + sb := sqlFlavor.NewSelectBuilder().Select("COUNT(DISTINCT que_job_id)").From("job_keys") + sb.Where(sb.Equal("job_id", jobID)) + + query, args := sb.Build() + var count int + if err := r.QueryRowContext(ctx, query, args...).Scan(&count); err != nil { + return -1, err + } + return count, nil +} + func (r *Repository) GetJobKey(ctx context.Context, jobID uint, queJobID int64) (*models.JobKey, error) { sb := sqlFlavor.NewSelectBuilder().Select("id").From("job_keys") sb.Where(sb.And(sb.Equal("job_id", jobID), sb.Equal("que_job_id", queJobID))) diff --git a/bcdaworker/repository/repository.go b/bcdaworker/repository/repository.go index 92de478b1..e43c4b57e 100644 --- a/bcdaworker/repository/repository.go +++ b/bcdaworker/repository/repository.go @@ -39,6 +39,7 @@ type jobKeyRepository interface { CreateJobKey(ctx context.Context, jobKey models.JobKey) error CreateJobKeys(ctx context.Context, jobKeys []models.JobKey) error GetJobKeyCount(ctx context.Context, jobID uint) (int, error) + GetUniqueJobKeyCount(ctx context.Context, jobID uint) (int, error) GetJobKey(ctx context.Context, jobID uint, queJobID int64) (*models.JobKey, error) } diff --git a/bcdaworker/worker/alr.go b/bcdaworker/worker/alr.go index e7f0779b9..a11c5faa3 100644 --- a/bcdaworker/worker/alr.go +++ b/bcdaworker/worker/alr.go @@ -59,7 +59,7 @@ func NewAlrWorker(db *sql.DB) AlrWorker { } func goWriterV1(ctx context.Context, a *AlrWorker, c chan *alr.AlrFhirBulk, fileMap map[string]*os.File, - result chan error, resourceTypes []string, id uint) { + result chan error, resourceTypes []string, id uint, queJobID int64) { writerPool := make([]*bufio.Writer, len(fileMap)) @@ -105,20 +105,23 @@ func goWriterV1(ctx context.Context, a *AlrWorker, c chan *alr.AlrFhirBulk, file } // update the jobs keys + var jobKeys []models.JobKey for resource, path := range fileMap { filename := filepath.Base(path.Name()) - jk := models.JobKey{JobID: id, FileName: filename, ResourceType: resource} - if err := a.Repository.CreateJobKey(ctx, jk); err != nil { - result <- fmt.Errorf(constants.JobKeyCreateErr, err) - return - } + jk := models.JobKey{JobID: id, QueJobID: &queJobID, FileName: filename, ResourceType: resource} + jobKeys = append(jobKeys, jk) + } + + if err := a.Repository.CreateJobKeys(ctx, jobKeys); err != nil { + result <- fmt.Errorf(constants.JobKeyCreateErr, err) + return } result <- nil } func goWriterV2(ctx context.Context, a *AlrWorker, c chan *alr.AlrFhirBulk, fileMap map[string]*os.File, - result chan error, resourceTypes []string, id uint) { + result chan error, resourceTypes []string, id uint, queJobID int64) { writerPool := make([]*bufio.Writer, len(fileMap)) @@ -164,13 +167,16 @@ func goWriterV2(ctx context.Context, a *AlrWorker, c chan *alr.AlrFhirBulk, file } // update the jobs keys + var jobKeys []models.JobKey for resource, path := range fileMap { filename := filepath.Base(path.Name()) - jk := models.JobKey{JobID: id, FileName: filename, ResourceType: resource} - if err := a.Repository.CreateJobKey(ctx, jk); err != nil { - result <- fmt.Errorf(constants.JobKeyCreateErr, err) - return - } + jk := models.JobKey{JobID: id, QueJobID: &queJobID, FileName: filename, ResourceType: resource} + jobKeys = append(jobKeys, jk) + } + + if err := a.Repository.CreateJobKeys(ctx, jobKeys); err != nil { + result <- fmt.Errorf(constants.JobKeyCreateErr, err) + return } result <- nil @@ -184,6 +190,7 @@ func goWriterV2(ctx context.Context, a *AlrWorker, c chan *alr.AlrFhirBulk, file // ProcessAlrJob is a function called by the Worker to serve ALR data to users func (a *AlrWorker) ProcessAlrJob( ctx context.Context, + queJobID int64, jobArgs models.JobAlrEnqueueArgs, ) error { @@ -200,10 +207,10 @@ func (a *AlrWorker) ProcessAlrJob( return err } - // If we did not have an ALR data to write, we'll write a specific file name that indicates that the + // If we did not have any ALR data to write, we'll write a specific file name that indicates that // there is no data associated with this job. if len(alrModels) == 0 { - jk := models.JobKey{JobID: id, FileName: models.BlankFileName, ResourceType: "ALR"} + jk := models.JobKey{JobID: id, QueJobID: &queJobID, FileName: models.BlankFileName, ResourceType: "ALR"} if err := a.Repository.CreateJobKey(ctx, jk); err != nil { return fmt.Errorf(constants.JobKeyCreateErr, err) } @@ -245,9 +252,9 @@ func (a *AlrWorker) ProcessAlrJob( // Reason for a go routine is to not block when writing, since disk writing is // generally slower than memory access. We are streaming to keep mem lower. if jobArgs.BBBasePath == "/v1/fhir" { - go goWriterV1(ctx, a, c, fileMap, result, resources[:], id) + go goWriterV1(ctx, a, c, fileMap, result, resources[:], id, queJobID) } else { - go goWriterV2(ctx, a, c, fileMap, result, resources[:], id) + go goWriterV2(ctx, a, c, fileMap, result, resources[:], id, queJobID) } // Marshall into JSON and send it over the channel diff --git a/bcdaworker/worker/alr_test.go b/bcdaworker/worker/alr_test.go index 9860c884d..477f20fb3 100644 --- a/bcdaworker/worker/alr_test.go +++ b/bcdaworker/worker/alr_test.go @@ -3,6 +3,7 @@ package worker import ( "context" "database/sql" + "math/rand" "os" "testing" @@ -65,10 +66,10 @@ func (s *AlrWorkerTestSuite) TestNewAlrWorker() { // Test ProcessAlrJob func (s *AlrWorkerTestSuite) TestProcessAlrJob() { ctx := context.Background() - err := s.alrWorker.ProcessAlrJob(ctx, s.jobArgs[0]) + err := s.alrWorker.ProcessAlrJob(ctx, rand.Int63(), s.jobArgs[0]) // Check Job is processed with no errors assert.NoError(s.T(), err) - err = s.alrWorker.ProcessAlrJob(ctx, s.jobArgs[1]) + err = s.alrWorker.ProcessAlrJob(ctx, rand.Int63(), s.jobArgs[1]) // Check Job is processed with no errors assert.NoError(s.T(), err) } From 27364d177b6c05115226571ade20d69ac27e7de7 Mon Sep 17 00:00:00 2001 From: Kevin Yeh Date: Fri, 5 Jul 2024 11:26:03 -0400 Subject: [PATCH 11/16] Increase export job waiting period for smoke tests (#967) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽซ Ticket N/A ## ๐Ÿ›  Changes - Update maxRetries from 10 --> 20 everywhere - Autoformat postman collection JSON files (oops sorry..) ## โ„น๏ธ Context After https://github.com/CMSgov/bcda-app/releases/tag/r226 was released, it looks like jobs are taking a _little_ bit longer to run in production. It's not clear if it's because of something in the release or tangential. This updates maxRetries everywhere (previously, a handful of maxRetries were already updated to 20 due to intermittent issues.) ## ๐Ÿงช Validation Successful smoke test on production with r226 after a series of intermittent failures. --- ...ostman_Smoke_Tests.postman_collection.json | 2984 ++- ...ostman_Smoke_Tests.postman_collection.json | 2384 ++- ...A_Tests_Sequential.postman_collection.json | 16165 ++++++++-------- 3 files changed, 10479 insertions(+), 11054 deletions(-) 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 e2a020bdc..5133eee52 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 @@ -1,1502 +1,1484 @@ { - "info": { - "_postman_id": "9cc2977d-ced3-4492-80d0-cee9169ec057", - "name": "BCDA PAC Postman Smoke Tests", - "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", - "_exporter_id": "11322751" - }, - "item": [ - { - "name": "Valid Token", - "item": [ - { - "name": "Authentication", - "item": [ - { - "name": "Get auth token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var env = pm.environment.get(\"env\");", - "pm.environment.set(\"clientId\", pm.globals.get(\"clientId\"));", - "pm.environment.set(\"clientSecret\", pm.globals.get(\"clientSecret\"));", - "pm.test(\"Status code is 200\", function() {", - " pm.response.to.have.status(200);", - "});", - "", - "var responseJSON;", - "try {", - " responseJSON = JSON.parse(responseBody);", - " tests['response is valid JSON'] = true;", - "}", - "catch (e) {", - " responseJSON = {};", - " tests['response is valid JSON'] = false;", - "}", - "", - "pm.environment.set(\"token\", responseJSON.access_token);", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": { - "password": "{{clientSecret}}", - "username": "{{clientId}}" - } - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - }, - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": "{{scheme}}://{{host}}/auth/token" - }, - "response": [] - } - ] - }, - { - "name": "Patient", - "item": [ - { - "name": "General", - "item": [ - { - "name": "Start Patient v2 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(\"smokeTestPatientJobUrl\", \"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(\"smokeTestPatientJobUrl\", 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/v2/Patient/$export" - }, - "response": [] - }, - { - "name": "Get Patient v2 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(\"smokeTestPatientDataUrl\", \"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 = [\"ExplanationOfBenefit\", \"Patient\", \"Coverage\", \"Claim\", \"ClaimResponse\"];", - " const returnedResources = respJson.output.map(r => r.type);", - "", - " for (const resource of requiredResources) {", - " pm.expect(returnedResources, resource + \" is required\").to.include(resource);", - " }", - " });", - "", - " pm.environment.set(\"smokeTestPatientDataUrl\", 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(\"smokeTestPatientJobUrl\"),", - " 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": "{{smokeTestPatientJobUrl}}" - }, - "response": [] - }, - { - "name": "Get Patient v2 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": "{{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": [] - } - ] - } - ] - }, - { - "name": "Group", - "item": [ - { - "name": "/all", - "item": [ - { - "name": "Start Group 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(\"smokeTestGroupAllJobUrl\", \"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(\"smokeTestGroupAllJobUrl\", 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/v2/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(\"smokeTestGroupAllJobUrl\"),", - " 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(\"smokeTestGroupAllDataUrl\", \"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 = [\"ExplanationOfBenefit\", \"Patient\", \"Coverage\", \"Claim\", \"ClaimResponse\"];", - " const returnedResources = respJson.output.map(r => r.type);", - "", - " for (const resource of requiredResources) {", - " pm.expect(returnedResources, resource + \" is required\").to.include(resource);", - " }", - " });", - "", - " pm.environment.set(\"smokeTestGroupAllDataUrl\", 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": "{{smokeTestGroupAllJobUrl}}" - }, - "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": "{{smokeTestGroupAllDataUrl}}" - }, - "response": [] - } - ] - }, - { - "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": [ - { - "name": "Start Group export", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "var maintenanceModeEOYTest = function () {", - " 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(\"smokeTestGroupRunoutEOBJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "};", - "", - "const maintenanceModeTest = function () {", - " 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(\"smokeTestGroupRunoutEOBJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "};", - "", - "if (maintenanceMode === \"eoy\") {", - " maintenanceModeEOYTest();", - "} else {", - " maintenanceModeTest();", - "}" - ], - "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": { - "raw": "{{scheme}}://{{host}}/api/v2/Group/runout/$export?_type=ExplanationOfBenefit", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Group", - "runout", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "ExplanationOfBenefit" - } - ] - } - }, - "response": [] - }, - { - "name": "Get Group export job status", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "const retryDelay = 5000;", - "const maxRetries = 20;", - "", - "var eobJobReq = {", - " url: pm.environment.get(\"smokeTestGroupRunoutEOBJobUrl\"),", - " 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/runout export still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.log(\"Retry limit reached for Group/runout job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"Group/runout export job complete.\");", - " } else {", - " console.error(\"Unexpected response from Group/runout export job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "var maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "var maintenanceModeEOYTest = function () {", - " 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 = [\"ExplanationOfBenefit\"];", - " const otherResources = [\"Patient\", \"Coverage\", \"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(\"smokeTestGroupRunoutEOBDataUrl\", respJson.output[0].url);", - " }", - "};", - "", - "const maintenanceModeTest = function () {", - " 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(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 = [\"ExplanationOfBenefit\"];", - " const otherResources = [\"Patient\", \"Coverage\", \"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(\"smokeTestGroupRunoutEOBDataUrl\", respJson.output[0].url);", - " }", - "};", - "", - "if (maintenanceMode === \"eoy\") {", - " maintenanceModeEOYTest();", - "} else {", - " maintenanceModeTest();", - "}" - ], - "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": "{{smokeTestGroupRunoutEOBJobUrl}}" - }, - "response": [] - }, - { - "name": "Get Group export job data", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "var maintenanceModeEOYTest = function () {", - " 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)", - " });", - "};", - "", - "const maintenanceModeTest = function () {", - " 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)", - " });", - "};", - "", - "if (maintenanceMode === \"eoy\") {", - " maintenanceModeEOYTest();", - "} else {", - " maintenanceModeTest();", - "}" - ], - "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": "{{smokeTestGroupRunoutEOBDataUrl}}" - }, - "response": [] - } - ] - } - ] - } - ] - } - ] -} \ No newline at end of file + "info": { + "_postman_id": "9cc2977d-ced3-4492-80d0-cee9169ec057", + "name": "BCDA PAC Postman Smoke Tests", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", + "_exporter_id": "11322751" + }, + "item": [ + { + "name": "Valid Token", + "item": [ + { + "name": "Authentication", + "item": [ + { + "name": "Get auth token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var env = pm.environment.get(\"env\");", + "pm.environment.set(\"clientId\", pm.globals.get(\"clientId\"));", + "pm.environment.set(\"clientSecret\", pm.globals.get(\"clientSecret\"));", + "pm.test(\"Status code is 200\", function() {", + " pm.response.to.have.status(200);", + "});", + "", + "var responseJSON;", + "try {", + " responseJSON = JSON.parse(responseBody);", + " tests['response is valid JSON'] = true;", + "}", + "catch (e) {", + " responseJSON = {};", + " tests['response is valid JSON'] = false;", + "}", + "", + "pm.environment.set(\"token\", responseJSON.access_token);", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": { + "password": "{{clientSecret}}", + "username": "{{clientId}}" + } + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + }, + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": "{{scheme}}://{{host}}/auth/token" + }, + "response": [] + } + ] + }, + { + "name": "Patient", + "item": [ + { + "name": "General", + "item": [ + { + "name": "Start Patient v2 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(\"smokeTestPatientJobUrl\", \"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(\"smokeTestPatientJobUrl\", 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/v2/Patient/$export" + }, + "response": [] + }, + { + "name": "Get Patient v2 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(\"smokeTestPatientDataUrl\", \"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 = [\"ExplanationOfBenefit\", \"Patient\", \"Coverage\", \"Claim\", \"ClaimResponse\"];", + " const returnedResources = respJson.output.map(r => r.type);", + "", + " for (const resource of requiredResources) {", + " pm.expect(returnedResources, resource + \" is required\").to.include(resource);", + " }", + " });", + "", + " pm.environment.set(\"smokeTestPatientDataUrl\", respJson.output[0].url);", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const retryDelay = 5000;", + "const maxRetries = 20;", + "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(\"smokeTestPatientJobUrl\"),", + " 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": "{{smokeTestPatientJobUrl}}" + }, + "response": [] + }, + { + "name": "Get Patient v2 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": "{{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 = 20;", + "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": [] + } + ] + } + ] + }, + { + "name": "Group", + "item": [ + { + "name": "/all", + "item": [ + { + "name": "Start Group 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(\"smokeTestGroupAllJobUrl\", \"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(\"smokeTestGroupAllJobUrl\", 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/v2/Group/all/$export" + }, + "response": [] + }, + { + "name": "Get Group export job status", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const retryDelay = 5000;", + "const maxRetries = 20;", + "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(\"smokeTestGroupAllJobUrl\"),", + " 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(\"smokeTestGroupAllDataUrl\", \"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 = [\"ExplanationOfBenefit\", \"Patient\", \"Coverage\", \"Claim\", \"ClaimResponse\"];", + " const returnedResources = respJson.output.map(r => r.type);", + "", + " for (const resource of requiredResources) {", + " pm.expect(returnedResources, resource + \" is required\").to.include(resource);", + " }", + " });", + "", + " pm.environment.set(\"smokeTestGroupAllDataUrl\", 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": "{{smokeTestGroupAllJobUrl}}" + }, + "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": "{{smokeTestGroupAllDataUrl}}" + }, + "response": [] + } + ] + }, + { + "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 = 20;", + "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": [ + { + "name": "Start Group export", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "var maintenanceModeEOYTest = function () {", + " 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(\"smokeTestGroupRunoutEOBJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "};", + "", + "const maintenanceModeTest = function () {", + " 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(\"smokeTestGroupRunoutEOBJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "};", + "", + "if (maintenanceMode === \"eoy\") {", + " maintenanceModeEOYTest();", + "} else {", + " maintenanceModeTest();", + "}" + ], + "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": { + "raw": "{{scheme}}://{{host}}/api/v2/Group/runout/$export?_type=ExplanationOfBenefit", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Group", "runout", "$export"], + "query": [ + { + "key": "_type", + "value": "ExplanationOfBenefit" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Group export job status", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var eobJobReq = {", + " url: pm.environment.get(\"smokeTestGroupRunoutEOBJobUrl\"),", + " 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/runout export still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for Group/runout job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"Group/runout export job complete.\");", + " } else {", + " console.error(\"Unexpected response from Group/runout export job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "var maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "var maintenanceModeEOYTest = function () {", + " 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 = [\"ExplanationOfBenefit\"];", + " const otherResources = [\"Patient\", \"Coverage\", \"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(\"smokeTestGroupRunoutEOBDataUrl\", respJson.output[0].url);", + " }", + "};", + "", + "const maintenanceModeTest = function () {", + " 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(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 = [\"ExplanationOfBenefit\"];", + " const otherResources = [\"Patient\", \"Coverage\", \"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(\"smokeTestGroupRunoutEOBDataUrl\", respJson.output[0].url);", + " }", + "};", + "", + "if (maintenanceMode === \"eoy\") {", + " maintenanceModeEOYTest();", + "} else {", + " maintenanceModeTest();", + "}" + ], + "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": "{{smokeTestGroupRunoutEOBJobUrl}}" + }, + "response": [] + }, + { + "name": "Get Group export job data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "var maintenanceModeEOYTest = function () {", + " 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)", + " });", + "};", + "", + "const maintenanceModeTest = function () {", + " 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)", + " });", + "};", + "", + "if (maintenanceMode === \"eoy\") {", + " maintenanceModeEOYTest();", + "} else {", + " maintenanceModeTest();", + "}" + ], + "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": "{{smokeTestGroupRunoutEOBDataUrl}}" + }, + "response": [] + } + ] + } + ] + } + ] + } + ] +} diff --git a/test/postman_test/BCDA_Postman_Smoke_Tests.postman_collection.json b/test/postman_test/BCDA_Postman_Smoke_Tests.postman_collection.json index f9301df4d..cbfebbd84 100644 --- a/test/postman_test/BCDA_Postman_Smoke_Tests.postman_collection.json +++ b/test/postman_test/BCDA_Postman_Smoke_Tests.postman_collection.json @@ -1,1223 +1,1165 @@ { - "info": { - "_postman_id": "d6199346-4aa2-46dd-9b40-e4c714c7abf9", - "name": "BCDA Postman Smoke Tests", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "20786112" - }, - "item": [ - { - "name": "Invalid Token", - "item": [ - { - "name": "Authentication", - "item": [ - { - "name": "Set dummy auth token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 400\", function() {", - " pm.response.to.have.status(400);", - "});", - "", - "pm.environment.set(\"token\", \"invalidtoken\");", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{scheme}}://{{host}}/auth/token", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "auth", - "token" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Requests", - "item": [ - { - "name": "Unauthorized", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient/$export request\");\t", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 401\", function() {", - " pm.response.to.have.status(401);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " var respJson = pm.response.json();", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Diagnostics is Invalid Token\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid Token\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Patient", - "$export" - ] - } - }, - "response": [] - } - ] - } - ] - }, - { - "name": "Valid Token", - "item": [ - { - "name": "Authentication", - "item": [ - { - "name": "Get auth token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var env = pm.environment.get(\"env\");", - "pm.environment.set(\"clientId\", pm.globals.get(\"clientId\"));", - "pm.environment.set(\"clientSecret\", pm.globals.get(\"clientSecret\"));", - "pm.test(\"Status code is 200\", function() {", - " pm.response.to.have.status(200);", - "});", - "", - "var responseJSON;", - "try {", - " responseJSON = JSON.parse(responseBody);", - " tests['response is valid JSON'] = true;", - "}", - "catch (e) {", - " responseJSON = {};", - " tests['response is valid JSON'] = false;", - "}", - "", - "pm.environment.set(\"token\", responseJSON.access_token);", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{clientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{clientId}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - }, - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{scheme}}://{{host}}/auth/token", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "auth", - "token" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Patient", - "item": [ - { - "name": "/all is NOT Explicit", - "item": [ - { - "name": "Start Patient 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(\"smokeTestPatientJobUrl\", \"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(\"smokeTestPatientJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Patient", - "$export" - ] - } - }, - "response": [] - }, - { - "name": "Get Patient 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(\"smokeTestPatientDataUrl\", \"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 = [\"ExplanationOfBenefit\", \"Patient\", \"Coverage\"];", - " const partiallyAdjudicatedResources = [\"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 partiallyAdjudicatedResources) {", - " pm.expect(returnedResources, resource + \" resource type should not be returned\").to.not.include(resource);", - " }", - " });", - "", - " pm.environment.set(\"smokeTestPatientDataUrl\", 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(\"smokeTestPatientJobUrl\"),", - " 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": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{smokeTestPatientJobUrl}}", - "host": [ - "{{smokeTestPatientJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Patient 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": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{smokeTestPatientDataUrl}}", - "host": [ - "{{smokeTestPatientDataUrl}}" - ] - } - }, - "response": [] - } - ] - } - ] - }, - { - "name": "Group", - "item": [ - { - "name": "/all is Explicit", - "item": [ - { - "name": "Start Group 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(\"smokeTestGroupAllJobUrl\", \"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(\"smokeTestGroupAllJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Group/all/$export", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "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(\"smokeTestGroupAllJobUrl\"),", - " 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(\"smokeTestGroupAllDataUrl\", \"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 = [\"ExplanationOfBenefit\", \"Patient\", \"Coverage\"];", - " const partiallyAdjudicatedResources = [\"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 partiallyAdjudicatedResources) {", - " pm.expect(returnedResources, resource + \" resource type should not be returned\").to.not.include(resource);", - " }", - " });", - "", - " pm.environment.set(\"smokeTestGroupAllDataUrl\", respJson.output[0].url);", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{smokeTestGroupAllJobUrl}}", - "host": [ - "{{smokeTestGroupAllJobUrl}}" - ] - } - }, - "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": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{smokeTestGroupAllDataUrl}}", - "host": [ - "{{smokeTestGroupAllDataUrl}}" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "/runout (EOB Resource)", - "item": [ - { - "name": "Start Group export", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "var maintenanceModeEOYTest = function () {", - " 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(\"smokeTestGroupRunoutEOBJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "};", - "", - "const maintenanceModeTest = function () {", - " 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(\"smokeTestGroupRunoutEOBJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "};", - "", - "if (maintenanceMode === \"eoy\") {", - " maintenanceModeEOYTest();", - "} else {", - " maintenanceModeTest();", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Group/runout/$export?_type=ExplanationOfBenefit", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Group", - "runout", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "ExplanationOfBenefit" - } - ] - } - }, - "response": [] - }, - { - "name": "Get Group export job status", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "const retryDelay = 5000;", - "const maxRetries = 10;", - "", - "var eobJobReq = {", - " url: pm.environment.get(\"smokeTestGroupRunoutEOBJobUrl\"),", - " 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/runout export still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.log(\"Retry limit reached for Group/runout job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"Group/runout export job complete.\");", - " } else {", - " console.error(\"Unexpected response from Group/runout export job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "var maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "var maintenanceModeEOYTest = function () {", - " 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 = [\"ExplanationOfBenefit\"];", - " const otherResources = [\"Patient\", \"Coverage\", \"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(\"smokeTestGroupRunoutEOBDataUrl\", respJson.output[0].url);", - " }", - "};", - "", - "const maintenanceModeTest = function () {", - " 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(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 = [\"ExplanationOfBenefit\"];", - " const otherResources = [\"Patient\", \"Coverage\", \"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(\"smokeTestGroupRunoutEOBDataUrl\", respJson.output[0].url);", - " }", - "};", - "", - "if (maintenanceMode === \"eoy\") {", - " maintenanceModeEOYTest();", - "} else {", - " maintenanceModeTest();", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{smokeTestGroupRunoutEOBJobUrl}}", - "host": [ - "{{smokeTestGroupRunoutEOBJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Group export job data", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "var maintenanceModeEOYTest = function () {", - " 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)", - " });", - "};", - "", - "const maintenanceModeTest = function () {", - " 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)", - " });", - "};", - "", - "if (maintenanceMode === \"eoy\") {", - " maintenanceModeEOYTest();", - "} else {", - " maintenanceModeTest();", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{smokeTestGroupRunoutEOBDataUrl}}", - "host": [ - "{{smokeTestGroupRunoutEOBDataUrl}}" - ] - } - }, - "response": [] - } - ] - } - ] - } - ] - } - ] + "info": { + "_postman_id": "d6199346-4aa2-46dd-9b40-e4c714c7abf9", + "name": "BCDA Postman Smoke Tests", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "20786112" + }, + "item": [ + { + "name": "Invalid Token", + "item": [ + { + "name": "Authentication", + "item": [ + { + "name": "Set dummy auth token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 400\", function() {", + " pm.response.to.have.status(400);", + "});", + "", + "pm.environment.set(\"token\", \"invalidtoken\");", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{scheme}}://{{host}}/auth/token", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["auth", "token"] + } + }, + "response": [] + } + ] + }, + { + "name": "Requests", + "item": [ + { + "name": "Unauthorized", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient/$export request\");\t", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 401\", function() {", + " pm.response.to.have.status(401);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " var respJson = pm.response.json();", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Diagnostics is Invalid Token\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid Token\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Patient", "$export"] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Valid Token", + "item": [ + { + "name": "Authentication", + "item": [ + { + "name": "Get auth token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var env = pm.environment.get(\"env\");", + "pm.environment.set(\"clientId\", pm.globals.get(\"clientId\"));", + "pm.environment.set(\"clientSecret\", pm.globals.get(\"clientSecret\"));", + "pm.test(\"Status code is 200\", function() {", + " pm.response.to.have.status(200);", + "});", + "", + "var responseJSON;", + "try {", + " responseJSON = JSON.parse(responseBody);", + " tests['response is valid JSON'] = true;", + "}", + "catch (e) {", + " responseJSON = {};", + " tests['response is valid JSON'] = false;", + "}", + "", + "pm.environment.set(\"token\", responseJSON.access_token);", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{clientSecret}}", + "type": "string" + }, + { + "key": "username", + "value": "{{clientId}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + }, + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{scheme}}://{{host}}/auth/token", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["auth", "token"] + } + }, + "response": [] + } + ] + }, + { + "name": "Patient", + "item": [ + { + "name": "/all is NOT Explicit", + "item": [ + { + "name": "Start Patient 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(\"smokeTestPatientJobUrl\", \"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(\"smokeTestPatientJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Patient", "$export"] + } + }, + "response": [] + }, + { + "name": "Get Patient 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(\"smokeTestPatientDataUrl\", \"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 = [\"ExplanationOfBenefit\", \"Patient\", \"Coverage\"];", + " const partiallyAdjudicatedResources = [\"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 partiallyAdjudicatedResources) {", + " pm.expect(returnedResources, resource + \" resource type should not be returned\").to.not.include(resource);", + " }", + " });", + "", + " pm.environment.set(\"smokeTestPatientDataUrl\", respJson.output[0].url);", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const retryDelay = 5000;", + "const maxRetries = 20;", + "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(\"smokeTestPatientJobUrl\"),", + " 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": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{smokeTestPatientJobUrl}}", + "host": ["{{smokeTestPatientJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Patient 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": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{smokeTestPatientDataUrl}}", + "host": ["{{smokeTestPatientDataUrl}}"] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "Group", + "item": [ + { + "name": "/all is Explicit", + "item": [ + { + "name": "Start Group 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(\"smokeTestGroupAllJobUrl\", \"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(\"smokeTestGroupAllJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Group/all/$export", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Group", "all", "$export"] + } + }, + "response": [] + }, + { + "name": "Get Group export job status", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const retryDelay = 5000;", + "const maxRetries = 20;", + "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(\"smokeTestGroupAllJobUrl\"),", + " 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(\"smokeTestGroupAllDataUrl\", \"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 = [\"ExplanationOfBenefit\", \"Patient\", \"Coverage\"];", + " const partiallyAdjudicatedResources = [\"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 partiallyAdjudicatedResources) {", + " pm.expect(returnedResources, resource + \" resource type should not be returned\").to.not.include(resource);", + " }", + " });", + "", + " pm.environment.set(\"smokeTestGroupAllDataUrl\", respJson.output[0].url);", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{smokeTestGroupAllJobUrl}}", + "host": ["{{smokeTestGroupAllJobUrl}}"] + } + }, + "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": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{smokeTestGroupAllDataUrl}}", + "host": ["{{smokeTestGroupAllDataUrl}}"] + } + }, + "response": [] + } + ] + }, + { + "name": "/runout (EOB Resource)", + "item": [ + { + "name": "Start Group export", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "var maintenanceModeEOYTest = function () {", + " 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(\"smokeTestGroupRunoutEOBJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "};", + "", + "const maintenanceModeTest = function () {", + " 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(\"smokeTestGroupRunoutEOBJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "};", + "", + "if (maintenanceMode === \"eoy\") {", + " maintenanceModeEOYTest();", + "} else {", + " maintenanceModeTest();", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Group/runout/$export?_type=ExplanationOfBenefit", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Group", "runout", "$export"], + "query": [ + { + "key": "_type", + "value": "ExplanationOfBenefit" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Group export job status", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var eobJobReq = {", + " url: pm.environment.get(\"smokeTestGroupRunoutEOBJobUrl\"),", + " 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/runout export still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for Group/runout job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"Group/runout export job complete.\");", + " } else {", + " console.error(\"Unexpected response from Group/runout export job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "var maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "var maintenanceModeEOYTest = function () {", + " 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 = [\"ExplanationOfBenefit\"];", + " const otherResources = [\"Patient\", \"Coverage\", \"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(\"smokeTestGroupRunoutEOBDataUrl\", respJson.output[0].url);", + " }", + "};", + "", + "const maintenanceModeTest = function () {", + " 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(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 = [\"ExplanationOfBenefit\"];", + " const otherResources = [\"Patient\", \"Coverage\", \"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(\"smokeTestGroupRunoutEOBDataUrl\", respJson.output[0].url);", + " }", + "};", + "", + "if (maintenanceMode === \"eoy\") {", + " maintenanceModeEOYTest();", + "} else {", + " maintenanceModeTest();", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{smokeTestGroupRunoutEOBJobUrl}}", + "host": ["{{smokeTestGroupRunoutEOBJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Group export job data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "var maintenanceModeEOYTest = function () {", + " 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)", + " });", + "};", + "", + "const maintenanceModeTest = function () {", + " 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)", + " });", + "};", + "", + "if (maintenanceMode === \"eoy\") {", + " maintenanceModeEOYTest();", + "} else {", + " maintenanceModeTest();", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [""], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{smokeTestGroupRunoutEOBDataUrl}}", + "host": ["{{smokeTestGroupRunoutEOBDataUrl}}"] + } + }, + "response": [] + } + ] + } + ] + } + ] + } + ] } diff --git a/test/postman_test/BCDA_Tests_Sequential.postman_collection.json b/test/postman_test/BCDA_Tests_Sequential.postman_collection.json index 78b85ba03..e98191462 100644 --- a/test/postman_test/BCDA_Tests_Sequential.postman_collection.json +++ b/test/postman_test/BCDA_Tests_Sequential.postman_collection.json @@ -1,8334 +1,7835 @@ { - "info": { - "_postman_id": "817f4c7f-997e-4d35-aed3-ae00ef03c471", - "name": "Beneficiary Claims Data API Tests, Sequential", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "20886263" - }, - "item": [ - { - "name": "Informational Endpoints", - "item": [ - { - "name": "Get version", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Response contains version\", function() {", - " pm.expect(pm.response.json()).to.have.property(\"version\");", - "});", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}/_version", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "_version" - ] - } - }, - "response": [] - }, - { - "name": "Get metadata", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "", - "pm.test(\"Status code is 200\", function() {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Content-Type is application/json\", function() {", - " pm.response.to.have.header(\"Content-Type\", \"application/json\");", - "});", - "", - "var respJson = pm.response.json();", - "", - "pm.test(\"Resource type is CapabilityStatement\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"CapabilityStatement\")", - "});", - "", - "const schema = {", - " \"properties\": {", - " \"resourceType\": {", - " \"type\": \"string\"", - " },", - " \"status\": {", - " \"type\": \"string\"", - " },", - " \"date\": {", - " \"type\": \"string\"", - " },", - " \"publisher\": {", - " \"type\": \"string\"", - " },", - " \"kind\": {", - " \"type\": \"string\"", - " },", - " \"instantiates\": {", - " \"type\": \"array\"", - " },", - " \"software\": {", - " \"type\": \"object\",", - " \"properties\": {", - " \"name\": {},", - " \"version\": {},", - " \"releaseDate\": {}", - " }", - " },", - " \"implementation\": {", - " \"type\": \"object\"", - " },", - " \"fhirVersion\": {", - " \"type\": \"string\"", - " },", - " \"acceptUnknown\":{", - " \"type\": \"string\"", - " },", - " \"format\": {", - " \"type\": \"array\"", - " },", - " \"rest\": {", - " \"type\": \"array\"", - " }", - " }", - "};", - "", - "pm.test(\"Schema is valid\", function() {", - " pm.expect(tv4.validate(respJson, schema)).to.be.true;", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/metadata", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "metadata" - ] - } - }, - "response": [] - }, - { - "name": "Health check", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Response contains database status\", function() {", - " pm.expect(pm.response.json()).to.have.property(\"database\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}/_health", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "_health" - ] - } - }, - "response": [] - }, - { - "name": "Swagger V1", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 200\", function() {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Content-Type is text/html; charset=utf-8\", function() {", - " pm.response.to.have.header(\"Content-Type\", \"text/html; charset=utf-8\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/swagger/", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "swagger", - "" - ] - }, - "description": "TODO (BCDA-4109) - Remove test verifying v1 specific artifacts" - }, - "response": [] - }, - { - "name": "Swagger V2", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (v2Disabled) {", - " pm.test(\"Status code is 404\", function() {", - " pm.response.to.have.status(404);", - " });", - " return;", - "}", - "", - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Content-Type is text/html; charset=utf-8\", function () {", - " pm.response.to.have.header(\"Content-Type\", \"text/html; charset=utf-8\");", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/swagger/", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "swagger", - "" - ] - }, - "description": "Make request to location that should contain v1 and v2 API requests" - }, - "response": [] - } - ] - }, - { - "name": "Authenticated Endpoints", - "item": [ - { - "name": "Without Token", - "item": [ - { - "name": "V1", - "item": [ - { - "name": "Get Attribution Status v1, no token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "", - "pm.test(\"Status code is 401\", function() {", - " pm.response.to.have.status(401);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/attribution_status", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "attribution_status" - ] - } - }, - "response": [] - }, - { - "name": "Start Patient export, no token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 401\", function() {", - " pm.response.to.have.status(401);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details code is Invalid Token\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid Token\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Patient", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Patient" - } - ] - } - }, - "response": [] - }, - { - "name": "Start EOB export, no token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");\t\t\t", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 401\", function() {", - " pm.response.to.have.status(401);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " var respJson = pm.response.json();", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details code is Invalid Token\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid Token\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=ExplanationOfBenefit", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "ExplanationOfBenefit" - } - ] - } - }, - "response": [] - }, - { - "name": "Start Coverage export, no token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient Coverage endpoint request\");\t\t\t", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 401\", function() {", - " pm.response.to.have.status(401);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details code is Invalid Token\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid Token\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Coverage", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Coverage" - } - ] - } - }, - "response": [] - }, - { - "name": "Get job status, no token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 401\", function() {", - " pm.response.to.have.status(401);", - "});", - "", - "pm.test(\"Resource type is OperationOutcome\", function() {", - " var respJson = pm.response.json();", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text", - "disabled": true - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text", - "disabled": true - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/jobs/{{jobId}}", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "jobs", - "{{jobId}}" - ] - } - }, - "response": [] - }, - { - "name": "Delete job, no token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 401\", function() {", - " pm.response.to.have.status(401);", - "});", - "", - "pm.test(\"Resource type is OperationOutcome\", function() {", - " var respJson = pm.response.json();", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "DELETE", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text", - "disabled": true - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text", - "disabled": true - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/jobs/{{jobId}}", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "jobs", - "{{jobId}}" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "V2", - "item": [ - { - "name": "Get Attribution Status v2, no token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "", - "pm.test(\"Status code is 401\", function() {", - " pm.response.to.have.status(401);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/attribution_status", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "attribution_status" - ] - } - }, - "response": [] - }, - { - "name": "Start EOB export v2, no token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");\t\t\t", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 401\", function() {", - " pm.response.to.have.status(401);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " var respJson = pm.response.json();", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details code is Invalid Token\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid Token\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=ExplanationOfBenefit", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "ExplanationOfBenefit" - } - ] - } - }, - "response": [] - }, - { - "name": "Start Patient export v2, no token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 401\", function() {", - " pm.response.to.have.status(401);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details code is Invalid Token\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid Token\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=Patient", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Patient" - } - ] - } - }, - "response": [] - }, - { - "name": "Start Coverage export v2, no token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient Coverage endpoint request\");\t\t\t", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 401\", function() {", - " pm.response.to.have.status(401);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details code is Invalid Token\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid Token\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=Coverage", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Coverage" - } - ] - } - }, - "response": [] - }, - { - "name": "Get job status v2, no token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 401\", function() {", - " pm.response.to.have.status(401);", - "});", - "", - "pm.test(\"Resource type is OperationOutcome\", function() {", - " var respJson = pm.response.json();", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text", - "disabled": true - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text", - "disabled": true - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/jobs/{{jobId}}", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "jobs", - "{{jobId}}" - ] - } - }, - "response": [] - }, - { - "name": "Delete job v2, no token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 401\", function() {", - " pm.response.to.have.status(401);", - "});", - "", - "pm.test(\"Resource type is OperationOutcome\", function() {", - " var respJson = pm.response.json();", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "DELETE", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text", - "disabled": true - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text", - "disabled": true - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/jobs/{{jobId}}", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "jobs", - "{{jobId}}" - ] - } - }, - "response": [] - }, - { - "name": "Get data, no token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 401\", function() {", - " pm.response.to.have.status(401);", - "});", - "", - "pm.test(\"Resource type is OperationOutcome\", function() {", - " var respJson = pm.response.json();", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "noauth" - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}/data/{{jobId}}/{{acoId}}.ndjson", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "data", - "{{jobId}}", - "{{acoId}}.ndjson" - ] - } - }, - "response": [] - } - ] - } - ] - }, - { - "name": "With Token", - "item": [ - { - "name": "Get Access Token", - "item": [ - { - "name": "Get auth token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "var env = pm.environment.get(\"env\");", - "pm.environment.set(\"clientId\", pm.globals.get(\"clientId\"));", - "pm.environment.set(\"clientSecret\", pm.globals.get(\"clientSecret\"));", - "pm.test(\"Status code is 200\", function() {", - " pm.response.to.have.status(200);", - "});", - "", - "var responseJSON;", - "try {", - " responseJSON = JSON.parse(responseBody);", - " tests['response is valid JSON'] = true;", - "}", - "catch (e) {", - " responseJSON = {};", - " tests['response is valid JSON'] = false;", - "}", - "", - "pm.environment.set(\"token\", responseJSON.access_token);", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "{{clientSecret}}", - "type": "string" - }, - { - "key": "username", - "value": "{{clientId}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{scheme}}://{{host}}/auth/token", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "auth", - "token" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Attribution Status Scenarios", - "item": [ - { - "name": "Get Attribution Status v1", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "", - "pm.test(\"Status code is 200\", function() {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Content-Type is application/json\", function() {", - " pm.response.to.have.header(\"Content-Type\", \"application/json\");", - "});", - "", - "var respJson = pm.response.json();", - "", - "const schema = {", - " \"properties\": {", - " \"ingestion_dates\": {", - " \"type\": \"array\",", - " \"items\": [{", - " \"type\": \"object\",", - " \"properties\": {", - " \"type\": {", - " \"type\": \"string\"", - " },", - " \"timestamp\": {", - " \"type\": \"string\"", - " }", - " }", - " }]", - " }", - " }", - "};", - "", - "pm.test(\"Schema is valid\", function() {", - " pm.expect(tv4.validate(respJson, schema)).to.be.true;", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/attribution_status", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "attribution_status" - ] - } - }, - "response": [] - }, - { - "name": "Get Attribution Status v2", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "", - "pm.test(\"Status code is 200\", function() {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Content-Type is application/json\", function() {", - " pm.response.to.have.header(\"Content-Type\", \"application/json\");", - "});", - "", - "var respJson = pm.response.json();", - "", - "const schema = {", - " \"properties\": {", - " \"ingestion_dates\": {", - " \"type\": \"array\",", - " \"items\": [{", - " \"type\": \"object\",", - " \"properties\": {", - " \"type\": {", - " \"type\": \"string\"", - " },", - " \"timestamp\": {", - " \"type\": \"string\"", - " }", - " }", - " }]", - " }", - " }", - "};", - "", - "pm.test(\"Schema is valid\", function() {", - " pm.expect(tv4.validate(respJson, schema)).to.be.true;", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/attribution_status", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "attribution_status" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Job Deletions", - "item": [ - { - "name": "Start EOB export v1 for Deletion", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");\t", - " pm.environment.set(\"eobJobUrl\", \"https://bcda.cms.gov\")\t\t", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} 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(\"eobJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=ExplanationOfBenefit", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "ExplanationOfBenefit" - } - ] - } - }, - "response": [] - }, - { - "name": "Delete job, valid token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");", - " return;", - "}", - "", - "pm.test(\"Status code is 202\", function() {", - " pm.response.to.have.status(202);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{eobJobUrl}}", - "host": [ - "{{eobJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Start EOB export v2 for Deletion", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");\t", - " pm.environment.set(\"eobv2JobUrl\", \"https://bcda.cms.gov\")\t\t", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} 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(\"eobv2JobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=ExplanationOfBenefit", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "ExplanationOfBenefit" - } - ] - } - }, - "response": [] - }, - { - "name": "Delete job v2, valid token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");", - " return;", - "}", - "", - "pm.test(\"Status code is 202\", function() {", - " pm.response.to.have.status(202);", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{eobv2JobUrl}}", - "host": [ - "{{eobv2JobUrl}}" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Job End to End (General)", - "item": [ - { - "name": "Start Patient export v1", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t\t ", - " pm.environment.set(\"patientJobUrl\", \"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(\"patientJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Patient", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Patient" - } - ] - } - }, - "response": [] - }, - { - "name": "Start Coverage export v1", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient coverage endpoint request\");\t\t\t\t ", - " pm.environment.set(\"coverageJobUrl\", \"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(\"coverageJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Coverage", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Coverage" - } - ] - } - }, - "response": [] - }, - { - "name": "Start ALR export v1", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "if (pm.globals.get(\"alrEnabled\") != \"true\") {", - " console.log(\"Skipping ALR test. ALR is not enabled.\")", - " // Set the alrJobUrl to avoid pre-test validation checks (like ENOTFOUND)", - " pm.environment.set(\"alrJobUrl\", \"https://bcda.cms.gov\")", - " return;", - "}", - "", - "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(\"alrJobUrl\", pm.response.headers.get(\"Content-Location\"));" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/alr/$export?_type=Patient,Observation", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "alr", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Patient,Observation" - } - ] - } - }, - "response": [] - }, - { - "name": "Start EOB export v1", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");\t\t\t\t ", - " pm.environment.set(\"eobJobUrl\", \"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(\"eobJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=ExplanationOfBenefit", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "ExplanationOfBenefit" - } - ] - } - }, - "response": [] - }, - { - "name": "Start Patient export v2", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t\t ", - " pm.environment.set(\"patientv2JobUrl\", \"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 {", - " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - " if (v2Disabled) {", - " pm.test(\"Status code is 404\", function() {", - " pm.response.to.have.status(404);", - " });", - " return;", - " }", - "", - " 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(\"patientv2JobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=Patient", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Patient" - } - ] - } - }, - "response": [] - }, - { - "name": "Start Coverage export v2", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient Coverage endpoint request\");\t\t\t\t ", - " pm.environment.set(\"coveragev2JobUrl\", \"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 {", - " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - " if (v2Disabled) {", - " pm.test(\"Status code is 404\", function() {", - " pm.response.to.have.status(404);", - " });", - " return;", - " }", - "", - " 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(\"coveragev2JobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=Coverage", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Coverage" - } - ] - } - }, - "response": [] - }, - { - "name": "Start EOB export v2", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");\t\t\t\t ", - " pm.environment.set(\"eobv2JobUrl\", \"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 {", - " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - " if (v2Disabled) {", - " pm.test(\"Status code is 404\", function() {", - " pm.response.to.have.status(404);", - " });", - " return;", - " }", - "", - " 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(\"eobv2JobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=ExplanationOfBenefit", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "ExplanationOfBenefit" - } - ] - } - }, - "response": [] - }, - { - "name": "Get Patient export v2 job status", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient request\");", - " pm.environment.set(\"patientv2DataUrl\", \"https://bcda.cms.gov\");", - " return;", - "} else {", - " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - " if (v2Disabled) {", - " console.log(\"Not running Patient export v2 job status, v2 endpoints have been disabled\");", - " 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\"];", - " const otherResources = [\"ExplanationOfBenefit\", \"Coverage\", \"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(\"patientv2DataUrl\", respJson.output[0].url);", - " }", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint pre-request\")", - " return;", - "}", - "", - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (v2Disabled) {", - " return;", - "}", - "", - "const retryDelay = 5000;", - "const maxRetries = 10;", - "", - "var eobJobReq = {", - " url: pm.environment.get(\"patientv2JobUrl\"),", - " 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 v2 still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.log(\"Retry limit reached for Patient v2 job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"Patient export v2 job complete.\");", - " } else {", - " console.error(\"Unexpected response from Patient export v2 job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{patientv2JobUrl}}", - "host": [ - "{{patientv2JobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Patient export v2 data", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");", - " return;", - "}", - "", - "if (v2Disabled) {", - " console.log(\"Not running Patient export v2 data, v2 endpoints have been disabled\");", - " 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" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{patientv2DataUrl}}", - "host": [ - "{{patientv2DataUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Coverage export v2 job status", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient request\");", - " pm.environment.set(\"coveragev2DataUrl\", \"https://bcda.cms.gov\");", - " return;", - "}", - "", - "if (v2Disabled) {", - " console.log(\"Not running Coverage export v2 job status, v2 endpoints have been disabled\");", - " 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 In Progress\", function() {", - " pm.expect(/^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 = [\"Coverage\"];", - " const otherResources = [\"ExplanationOfBenefit\", \"Patient\", \"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(\"coveragev2DataUrl\", respJson.output[0].url);", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint pre-request for coverage information\")", - " return;", - "}", - "", - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (v2Disabled) {", - " return;", - "}", - "", - "const retryDelay = 5000;", - "const maxRetries = 10;", - "", - "var coverageJobReq = {", - " url: pm.environment.get(\"coveragev2JobUrl\"),", - " method: \"GET\",", - " header: \"Authorization: Bearer \" + pm.environment.get(\"token\")", - "};", - "", - "function awaitExportJob(retryCount) {", - " pm.sendRequest(coverageJobReq, 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(\"Coverage export v2 still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.log(\"Retry limit reached for Coverage job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"Coverage export v2 job complete.\");", - " } else {", - " console.error(\"Unexpected response from Coverage export v2 job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{coveragev2JobUrl}}", - "host": [ - "{{coveragev2JobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Coverage export v2 data", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");", - " return;", - "}", - "", - "if (v2Disabled) {", - " console.log(\"Not running Coverage export v2 data, v2 endpoints have been disabled\");", - " 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" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{coveragev2DataUrl}}", - "host": [ - "{{coveragev2DataUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get EOB export v2 job status", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient request\");", - " pm.environment.set(\"eobv2DataUrl\", \"https://bcda.cms.gov\");", - " return;", - "}", - "", - "if (v2Disabled) {", - " console.log(\"Not running EOB export v2 job status, v2 endpoints have been disabled\");", - " 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 In Progress\", function() {", - " pm.expect(/^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 = [\"ExplanationOfBenefit\"];", - " const partiallyAdjudicatedResources = [\"Patient\", \"Coverage\", \"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 partiallyAdjudicatedResources) {", - " pm.expect(returnedResources, resource + \" resource type should not be returned\").to.not.include(resource);", - " }", - " });", - " ", - " pm.environment.set(\"eobv2DataUrl\", respJson.output[0].url);", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint pre-request for EOB information\")", - " return;", - "}", - "", - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (v2Disabled) {", - " return;", - "}", - "", - "const retryDelay = 5000;", - "const maxRetries = 10;", - "", - "var eobJobReq = {", - " url: pm.environment.get(\"eobv2JobUrl\"),", - " 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(\"ExplanationOfBenefit export v2 still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.log(\"Retry limit reached for ExplanationOfBenefit v2 job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"EOB export v2 job complete.\");", - " } else {", - " console.error(\"Unexpected response from EOB export v2 job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{eobv2JobUrl}}", - "host": [ - "{{eobv2JobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get EOB export v2 data", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");", - " return;", - "}", - "", - "if (v2Disabled) {", - " console.log(\"Not running EOB export v2 data, v2 endpoints have been disabled\");", - " 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" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{eobv2DataUrl}}", - "host": [ - "{{eobv2DataUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Patient export job status", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t\t ", - " pm.environment.set(\"patientDataUrl\", \"https://bcda.cms.gov\");", - " return;", - "} else {", - " 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 In Progress\", function() {", - " pm.expect(/^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\"];", - " const otherResources = [\"ExplanationOfBenefit\", \"Coverage\", \"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(\"patientDataUrl\", respJson.output[0].url);", - " }", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient pre-request\")", - " return;", - "}", - "", - "const retryDelay = 5000;", - "const maxRetries = 10;", - "", - "var eobJobReq = {", - " url: pm.environment.get(\"patientJobUrl\"),", - " 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": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{patientJobUrl}}", - "host": [ - "{{patientJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Patient export data", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");", - " return;", - "} else {", - " 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" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{patientDataUrl}}", - "host": [ - "{{patientDataUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Coverage export job status", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient coverage endpoint request\");", - " pm.environment.set(\"coverageDataUrl\", \"https://bcda.cms.gov\");", - " return;", - "} else {", - " 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 In Progress\", function() {", - " pm.expect(/^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 = [\"Coverage\"];", - " const otherResources = [\"ExplanationOfBenefit\", \"Patient\", \"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(\"coverageDataUrl\", respJson.output[0].url);", - " }", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient EOB,Coverage endpoint request\");", - " return;", - "}", - "", - "const retryDelay = 5000;", - "const maxRetries = 10;", - "", - "var coverageJobReq = {", - " url: pm.environment.get(\"coverageJobUrl\"),", - " method: \"GET\",", - " header: \"Authorization: Bearer \" + pm.environment.get(\"token\")", - "};", - "", - "function awaitExportJob(retryCount) {", - " pm.sendRequest(coverageJobReq, 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(\"Coverage export still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.log(\"Retry limit reached for Coverage job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"Coverage export job complete.\");", - " } else {", - " console.error(\"Unexpected response from Coverage export job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{coverageJobUrl}}", - "host": [ - "{{coverageJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Coverage export data", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient coverage endpoint request\");", - " return;", - "} else {", - " 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" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{coverageDataUrl}}", - "host": [ - "{{coverageDataUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get EOB export job status", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");", - " pm.environment.set(\"eobDataUrl\", \"https://bcda.cms.gov\");", - " return;", - "} else {", - " 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 In Progress\", function() {", - " pm.expect(/^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 = [\"ExplanationOfBenefit\"];", - " const otherResources = [\"Patient\", \"Coverage\", \"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(\"eobDataUrl\", respJson.output[0].url);", - " }", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");", - " return;", - "}", - "", - "const retryDelay = 5000;", - "const maxRetries = 10;", - "", - "var eobJobReq = {", - " url: pm.environment.get(\"eobJobUrl\"),", - " 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(\"ExplanationOfBenefit export still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.log(\"Retry limit reached for ExplanationOfBenefit job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"EOB export job complete.\");", - " } else {", - " console.error(\"Unexpected response from EOB export job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{eobJobUrl}}", - "host": [ - "{{eobJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get EOB export data", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");", - " return;", - "} else {", - " 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" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{eobDataUrl}}", - "host": [ - "{{eobDataUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get ALR export job status", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "if (pm.globals.get(\"alrEnabled\") != \"true\") {", - " console.log(\"Skipping ALR test. ALR is not enabled.\")", - " // Set the alrDataUrl to avoid pre-test validation checks (like ENOTFOUND)", - " pm.environment.set(\"alrDataUrl\", \"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 In Progress\", function() {", - " pm.expect(/^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.environment.set(\"alrDataUrl\", respJson.output[0].url);", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const retryDelay = 5000;", - "const maxRetries = 10;", - "", - "if (pm.globals.get(\"alrEnabled\") != \"true\") {", - " console.log(\"Skipping pre-request ALR script. ALR is not enabled.\")", - " return;", - "}", - "", - "var alrJobReq = {", - " url: pm.environment.get(\"alrJobUrl\"),", - " method: \"GET\",", - " header: \"Authorization: Bearer \" + pm.environment.get(\"token\")", - "};", - "", - "function awaitExportJob(retryCount) {", - " pm.sendRequest(alrJobReq, 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(\"ALR export still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.log(\"Retry limit reached for ALR job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"ALR export job complete.\");", - " } else {", - " console.error(\"Unexpected response from ALR export job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{alrJobUrl}}", - "host": [ - "{{alrJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get ALR export data", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "if (pm.globals.get(\"alrEnabled\") != \"true\") {", - " console.log(\"Skipping ALR test. ALR is not enabled.\")", - " 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": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{alrDataUrl}}", - "host": [ - "{{alrDataUrl}}" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Job End To End (Resource Type Combos)", - "item": [ - { - "name": "Kick Off Patient export with Patient, Coverage, EOB explicitly", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t\t ", - " pm.environment.set(\"patientv2withPatientCoverageEOBJobUrl\", \"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]);", - " });", - "} else {", - " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - " if (v2Disabled) {", - " pm.test(\"Status code is 404\", function() {", - " pm.response.to.have.status(404);", - " });", - " return;", - " }", - "", - " 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(\"patientv2withPatientCoverageEOBJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=Patient,Coverage,ExplanationOfBenefit", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Patient,Coverage,ExplanationOfBenefit" - } - ] - } - }, - "response": [] - }, - { - "name": "Kick Off Patient export with Patient, Coverage", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t\t ", - " pm.environment.set(\"patientv2withPatientCoverageJobUrl\", \"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]);", - " });", - "} else {", - " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - " if (v2Disabled) {", - " pm.test(\"Status code is 404\", function() {", - " pm.response.to.have.status(404);", - " });", - " return;", - " }", - "", - " 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(\"patientv2withPatientCoverageJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=Patient,Coverage", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Patient,Coverage" - } - ] - } - }, - "response": [] - }, - { - "name": "Kick Off Patient export with Patient, EOB", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t\t ", - " pm.environment.set(\"patientv2withPatientEOBJobUrl\", \"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]);", - " });", - "} else {", - " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - " if (v2Disabled) {", - " pm.test(\"Status code is 404\", function() {", - " pm.response.to.have.status(404);", - " });", - " return;", - " }", - "", - " 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(\"patientv2withPatientEOBJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=Patient,ExplanationOfBenefit", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Patient,ExplanationOfBenefit" - } - ] - } - }, - "response": [] - }, - { - "name": "Kick Off Patient export with Coverage, EOB", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient EOB, Coverage endpoint request\");\t\t\t\t ", - " pm.environment.set(\"patientv2withCoverageEOBJobUrl\", \"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]);", - " });", - "} else {", - " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - " if (v2Disabled) {", - " pm.test(\"Status code is 404\", function() {", - " pm.response.to.have.status(404);", - " });", - " return;", - " }", - "", - " 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(\"patientv2withCoverageEOBJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=Coverage,ExplanationOfBenefit", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Coverage,ExplanationOfBenefit" - } - ] - } - }, - "response": [] - }, - { - "name": "Get Job status for Patient export with Patient, Coverage, EOB explicitly", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient request\");", - " pm.environment.set(\"patientv2withPatientCoverageEOBDataUrl\", \"https://bcda.cms.gov\");", - " return;", - "}", - "", - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (v2Disabled) {", - " console.log(\"Not running Patient export with Patient, Coverage, EOB Resource Types explicitly job status, v2 endpoints have been disabled\");", - " 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 = [\"ExplanationOfBenefit\", \"Patient\", \"Coverage\"];", - " const partiallyAdjudicatedResources = [\"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 partiallyAdjudicatedResources) {", - " pm.expect(returnedResources, resource + \" resource type should not be returned\").to.not.include(resource);", - " }", - " });", - " ", - " pm.environment.set(\"patientv2withPatientCoverageEOBDataUrl\", respJson.output[0].url);", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint pre-request for EOB, Coverage information\")", - " return;", - "}", - "", - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (v2Disabled) {", - " return;", - "}", - "", - "const retryDelay = 5000;", - "const maxRetries = 10;", - "", - "var eobJobReq = {", - " url: pm.environment.get(\"patientv2withPatientCoverageEOBJobUrl\"),", - " 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 with Patient, Coverage, EOB Resource Types explicitly v2 still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.log(\"Retry limit reached for Patient export with Patient, Coverage, EOB Resource Types explicitly v2 job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"Patient export v2 job complete.\");", - " } else {", - " console.error(\"Unexpected response from Patient export with Patient, Coverage, EOB Resource Types explicitly v2 job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{patientv2withPatientCoverageEOBJobUrl}}", - "host": [ - "{{patientv2withPatientCoverageEOBJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Job status for Patient export with Patient, Coverage", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient Coverage endpoint request\");", - " pm.environment.set(\"patientv2withPatientCoverageDataUrl\", \"https://bcda.cms.gov\");", - " return;", - "}", - "", - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (v2Disabled) {", - " console.log(\"Not running Patient export with Patient, Coverage Resource Types job status, v2 endpoints have been disabled\");", - " 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\", \"Coverage\"];", - " const otherResources = [\"ExplanationOfBenefit\", \"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(\"patientv2withPatientCoverageDataUrl\", respJson.output[0].url);", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint pre-request for coverage information\")", - " return;", - "}", - "", - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (v2Disabled) {", - " return;", - "}", - "", - "const retryDelay = 5000;", - "const maxRetries = 10;", - "", - "var eobJobReq = {", - " url: pm.environment.get(\"patientv2withPatientCoverageJobUrl\"),", - " 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 with Patient, Coverage Resource Types v2 still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.log(\"Retry limit reached for Patient export with Patient, Coverage Resource Types v2 job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"Patient export v2 job complete.\");", - " } else {", - " console.error(\"Unexpected response from Patient export with Patient, Coverage Resource Types v2 job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{patientv2withPatientCoverageJobUrl}}", - "host": [ - "{{patientv2withPatientCoverageJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Job status for Patient export with Patient, EOB", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");", - " pm.environment.set(\"patientv2withPatientEOBDataUrl\", \"https://bcda.cms.gov\");", - " return;", - "}", - "", - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (v2Disabled) {", - " console.log(\"Not running Patient export with Patient, EOB Resource Types job status, v2 endpoints have been disabled\");", - " 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(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 = [\"ExplanationOfBenefit\", \"Patient\"];", - " const otherResources = [\"Coverage\", \"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(\"patientv2withPatientEOBDataUrl\", respJson.output[0].url);", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint pre-request for EOB information\")", - " return;", - "}", - "", - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (v2Disabled) {", - " return;", - "}", - "", - "const retryDelay = 5000;", - "const maxRetries = 10;", - "", - "var eobJobReq = {", - " url: pm.environment.get(\"patientv2withPatientEOBJobUrl\"),", - " 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 with Patient, EOB Resource Types v2 still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.log(\"Retry limit reached for Patient export with Patient, EOB Resource Types v2 job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"Patient export v2 job complete.\");", - " } else {", - " console.error(\"Unexpected response from Patient export with Patient, EOB Resource Types v2 job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{patientv2withPatientEOBJobUrl}}", - "host": [ - "{{patientv2withPatientEOBJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Job status for Patient export with Coverage, EOB", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient EOB, Coverage endpoint request\");", - " pm.environment.set(\"patientv2withCoverageEOBDataUrl\", \"https://bcda.cms.gov\");", - " return;", - "}", - "", - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (v2Disabled) {", - " console.log(\"Not running Patient export with Coverage, EOB Resource Types job status, v2 endpoints have been disabled\");", - " 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 = [\"Coverage\", \"ExplanationOfBenefit\"];", - " const otherResources = [\"Patient\", \"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(\"patientv2withCoverageEOBDataUrl\", respJson.output[0].url);", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint pre-request for EOB information\")", - " return;", - "}", - "", - "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - "if (v2Disabled) {", - " return;", - "}", - "", - "const retryDelay = 5000;", - "const maxRetries = 10;", - "", - "var eobJobReq = {", - " url: pm.environment.get(\"patientv2withCoverageEOBJobUrl\"),", - " 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 with Coverage, EOB Resource Types v2 still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.log(\"Retry limit reached for Patient export with Coverage, EOB Resource Types v2 job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"Patient export v2 job complete.\");", - " } else {", - " console.error(\"Unexpected response from Patient export with Coverage, EOB Resource Types v2 job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{patientv2withCoverageEOBJobUrl}}", - "host": [ - "{{patientv2withCoverageEOBJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Data for Patient export with Patient, Coverage, EOB explicitly", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient EOB, Coverage endpoint request\");", - " return;", - "} else {", - " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - " if (v2Disabled) {", - " console.log(\"Not running Patient export with Patient, Coverage, EOB Resource Types Explicitly v2 data, v2 endpoints have been disabled\");", - " 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" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{patientv2withPatientCoverageEOBDataUrl}}", - "host": [ - "{{patientv2withPatientCoverageEOBDataUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Data for Patient export with Patient, Coverage", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient Coverage endpoint request\");", - " return;", - "} else {", - " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - " if (v2Disabled) {", - " console.log(\"Not running Patient export with Patient, Coverage Resource Types v2 data, v2 endpoints have been disabled\");", - " 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" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{patientv2withPatientCoverageDataUrl}}", - "host": [ - "{{patientv2withPatientCoverageDataUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Data for Patient export with Patient, EOB", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");", - " return;", - "} else {", - " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", - "", - " if (v2Disabled) {", - " console.log(\"Not running Patient export with Patient, EOB Resource Types v2 data, v2 endpoints have been disabled\");", - " 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" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{patientv2withPatientEOBDataUrl}}", - "host": [ - "{{patientv2withPatientEOBDataUrl}}" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Job End To End (Parameterized)", - "item": [ - { - "name": "Start Patient export Valid Since", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t\t ", - " pm.environment.set(\"patientValidSinceJobUrl\", \"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]);", - " });", - "} 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(\"patientValidSinceJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient pre-request\")", - " return;", - "}", - "", - "let date = new Date();", - "date.setMinutes(date.getMinutes() - 1);", - "", - "let timestamp = date.toJSON();", - "pm.environment.set('now', timestamp);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Patient&_since={{now}}", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Patient" - }, - { - "key": "_since", - "value": "{{now}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Get Patient export Valid Since job status", - "event": [ - { - "listen": "prerequest", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient request\");", - " return;", - "}", - "", - "const retryDelay = 5000;", - "const maxRetries = 10;", - "", - "var eobJobReq = {", - " url: pm.environment.get(\"patientValidSinceJobUrl\"),", - " 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" - } - }, - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");", - " 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 In Progress\", function() {", - " pm.expect(/^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.expect(respJson.output).to.be.empty;", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{patientValidSinceJobUrl}}", - "host": [ - "{{patientValidSinceJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Start Group/all export without Since", - "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(\"groupAllNoSinceJobUrl\", \"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(\"groupAllNoSinceJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_type=Patient", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Group", - "all", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Patient" - } - ] - } - }, - "response": [] - }, - { - "name": "Get Group/all without Since export job status", - "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\");", - " pm.environment.set(\"groupAllNoSinceJobUrl\", \"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 In Progress\", function() {", - " pm.expect(/^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\"];", - " const otherResources = [\"ExplanationOfBenefit\", \"Coverage\", \"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(\"groupAllNoSinceJobUrl\", respJson.output[0].url);", - "", - "", - " pm.test(\"One file in output\", function() {", - " pm.expect(respJson.output.length).to.eql(1)", - " });", - "", - " pm.test(\"File is of type Patient\", function() {", - " pm.expect(respJson.output[0].type).to.eql(\"Patient\")", - " });", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Group/all request\");", - " return;", - "}", - "", - "const retryDelay = 5000;", - "const maxRetries = 10;", - "", - "var groupAllNoSinceJobReq = {", - " url: pm.environment.get(\"groupAllNoSinceJobUrl\"),", - " method: \"GET\",", - " header: \"Authorization: Bearer \" + pm.environment.get(\"token\")", - "};", - "", - "function awaitExportJob(retryCount) {", - " pm.sendRequest(groupAllNoSinceJobReq, 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 (without _since) export still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.log(\"Retry limit reached for Group/all (without _since) job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"Group/all (without _since) export job complete.\");", - " } else {", - " console.error(\"Unexpected response from Group/all (without _since) export job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{groupAllNoSinceJobUrl}}", - "host": [ - "{{groupAllNoSinceJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Group/all without Since export 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" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{groupAllNoSinceJobUrl}}", - "host": [ - "{{groupAllNoSinceJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Start Group/all export v2 without Since", - "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(\"groupAllv2NoSinceJobUrl\", \"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(\"groupAllv2NoSinceJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Group/all/$export?_type=Patient", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Group", - "all", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Patient" - } - ] - } - }, - "response": [] - }, - { - "name": "Get Group/all v2 without Since export job status", - "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\");", - " pm.environment.set(\"groupAllv2NoSinceJobUrl\", \"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 In Progress\", function() {", - " pm.expect(/^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\"];", - " const otherResources = [\"ExplanationOfBenefit\", \"Coverage\", \"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(\"groupAllv2NoSinceJobUrl\", respJson.output[0].url);", - "", - "", - " pm.test(\"One file in output\", function() {", - " pm.expect(respJson.output.length).to.eql(1)", - " });", - "", - " pm.test(\"File is of type Patient\", function() {", - " pm.expect(respJson.output[0].type).to.eql(\"Patient\")", - " });", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Group/all request\");", - " return;", - "}", - "", - "const retryDelay = 5000;", - "const maxRetries = 10;", - "", - "var groupAllNoSinceJobReq = {", - " url: pm.environment.get(\"groupAllv2NoSinceJobUrl\"),", - " method: \"GET\",", - " header: \"Authorization: Bearer \" + pm.environment.get(\"token\")", - "};", - "", - "function awaitExportJob(retryCount) {", - " pm.sendRequest(groupAllNoSinceJobReq, 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 (without _since) export still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.log(\"Retry limit reached for Group/all (without _since) job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"Group/all (without _since) export job complete.\");", - " } else {", - " console.error(\"Unexpected response from Group/all (without _since) export job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{groupAllv2NoSinceJobUrl}}", - "host": [ - "{{groupAllv2NoSinceJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Group/all v2 without Since export 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" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{groupAllv2NoSinceJobUrl}}", - "host": [ - "{{groupAllv2NoSinceJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Start Group/all export with Since", - "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(\"groupAllSinceJobUrl\", \"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(\"groupAllSinceJobUrl\", pm.response.headers.get(\"Content-Location\"));", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_type=Patient&_since=2020-02-13T08:00:00.000-05:00", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Group", - "all", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Patient" - }, - { - "key": "_since", - "value": "2020-02-13T08:00:00.000-05:00" - } - ] - } - }, - "response": [] - }, - { - "name": "Get Group/all with Since export job status", - "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 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 In Progress\", function() {", - " pm.expect(/^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.environment.set(\"groupAllSinceJobUrl\", respJson.output[0].url);", - "", - " pm.test(\"Two files in output\", function() {", - " pm.expect(respJson.output.length).to.eql(2)", - " });", - "", - " pm.test(\"File 1 is of type Patient\", function() {", - " pm.expect(respJson.output[0].type).to.eql(\"Patient\")", - " });", - " ", - " pm.test(\"File 2 is of type Patient\", function() {", - " pm.expect(respJson.output[1].type).to.eql(\"Patient\")", - " });", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {", - " console.log(\"EOY mode is enabled - Skipping Group/all request\");", - " return;", - "}", - "", - "const retryDelay = 5000;", - "const maxRetries = 10;", - "", - "var groupAllSinceJobReq = {", - " url: pm.environment.get(\"groupAllSinceJobUrl\"),", - " method: \"GET\",", - " header: \"Authorization: Bearer \" + pm.environment.get(\"token\")", - "};", - "", - "function awaitExportJob(retryCount) {", - " pm.sendRequest(groupAllSinceJobReq, 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 (with _since) export still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.log(\"Retry limit reached for Group/all (with _since) job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"Group/all (with _since) export job complete.\");", - " } else {", - " console.error(\"Unexpected response from Group/all (with _since) export job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{groupAllSinceJobUrl}}", - "host": [ - "{{groupAllSinceJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Group/all with Since export 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" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{groupAllSinceJobUrl}}", - "host": [ - "{{groupAllSinceJobUrl}}" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Job End To End (Runouts)", - "item": [ - { - "name": "Start Group/runout export", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "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(\"groupRunoutJobUrl\", pm.response.headers.get(\"Content-Location\"));" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Group/runout/$export?_type=ExplanationOfBenefit", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Group", - "runout", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "ExplanationOfBenefit" - } - ] - } - }, - "response": [] - }, - { - "name": "Get Group/runout export job status", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "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 In Progress\", function() {", - " pm.expect(/^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 = [\"ExplanationOfBenefit\"];", - " const otherResources = [\"Patient\", \"Coverage\", \"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(\"groupRunoutDataUrl\", respJson.output[0].url);", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const retryDelay = 5000;", - "const maxRetries = 20;", - "", - "var eobJobReq = {", - " url: pm.environment.get(\"groupRunoutJobUrl\"),", - " 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(\"ExplanationOfBenefit export still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.error(\"Retry limit reached for ExplanationOfBenefit job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"EOB export job complete.\");", - " } else {", - " console.error(\"Unexpected response from EOB export job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{groupRunoutJobUrl}}", - "host": [ - "{{groupRunoutJobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Group/runout export data", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "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" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{groupRunoutDataUrl}}", - "host": [ - "{{groupRunoutDataUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Start Group/runout v2 export", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "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(\"groupRunoutv2JobUrl\", pm.response.headers.get(\"Content-Location\"));" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/Group/runout/$export?_type=ExplanationOfBenefit", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "Group", - "runout", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "ExplanationOfBenefit" - } - ] - } - }, - "response": [] - }, - { - "name": "Get Group/runout export job status", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "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 In Progress\", function() {", - " pm.expect(/^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 = [\"ExplanationOfBenefit\"];", - " const otherResources = [\"Patient\", \"Coverage\", \"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(\"groupRunoutv2DataUrl\", respJson.output[0].url);", - "}" - ], - "type": "text/javascript" - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const retryDelay = 5000;", - "const maxRetries = 20;", - "", - "var eobJobReq = {", - " url: pm.environment.get(\"groupRunoutv2JobUrl\"),", - " 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(\"ExplanationOfBenefit export still in progress. Retrying...\");", - " setTimeout(function() {", - " awaitExportJob(++retryCount);", - " }, retryDelay);", - " } else {", - " console.error(\"Retry limit reached for ExplanationOfBenefit job status.\");", - " postman.setNextRequest(null);", - " }", - " } else if (response.code == 200) {", - " console.log(\"EOB export job complete.\");", - " } else {", - " console.error(\"Unexpected response from EOB export job: \" + response.status);", - " }", - " });", - "}", - "", - "awaitExportJob(1);" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json", - "disabled": true - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async", - "disabled": true - } - ], - "url": { - "raw": "{{groupRunoutv2JobUrl}}", - "host": [ - "{{groupRunoutv2JobUrl}}" - ] - } - }, - "response": [] - }, - { - "name": "Get Group/runout export data", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "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" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{groupRunoutv2DataUrl}}", - "host": [ - "{{groupRunoutv2DataUrl}}" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Job Completion Status", - "item": [ - { - "name": "Get jobs status Completed data", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "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)", - "});", - "", - "const schema = {", - " \"properties\": {", - " \"entry\": {", - " \"type\": \"array\",", - " \"items\": {", - " \"type\": \"object\",", - " \"properties\": {", - " \"resource\": {", - " \"type\": \"object\",", - " \"properties\": {", - " \"executionPeriod\": {", - " \"type\": \"object\",", - " \"properties\": {", - " \"end\": {},", - " \"start\": {}", - " }", - " },", - " \"identifier\": {", - " \"type\": \"array\",", - " \"items\": {", - " \"type\": \"object\",", - " \"properties\": {", - " \"system\": {},", - " \"use\": {},", - " \"valyue\": {}", - " }", - " }", - " },", - " \"input\": {", - " \"type\": \"array\",", - " \"items\": {", - " \"type\": \"object\",", - " \"properties\": {", - " \"type\": {},", - " \"valueString\": {}", - " }", - " }", - " },", - " \"intent\": {", - " \"type\": \"string\"", - " },", - " \"resourceType\": {", - " \"type\": \"string\"", - " },", - " \"status\": {", - " \"type\": \"string\"", - " }", - " }", - " }", - " }", - " }", - " },", - " \"resourceType\": {", - " \"type\": \"string\"", - " },", - " \"total\": {", - " \"type\": \"integer\"", - " },", - " \"type\": {", - " \"type\": \"string\"", - " }", - " }", - "};", - "", - "var respJson = pm.response.json();", - "", - "pm.test(\"Schema is valid\", function() {", - " pm.expect(tv4.validate(respJson, schema)).to.be.true;", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/jobs?_status=Completed", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "jobs" - ], - "query": [ - { - "key": "_status", - "value": "Completed" - } - ] - } - }, - "response": [] - }, - { - "name": "Get jobs status v2 Completed data", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "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)", - "});", - "", - "const schema = {", - " \"properties\": {", - " \"entry\": {", - " \"type\": \"array\",", - " \"items\": {", - " \"type\": \"object\",", - " \"properties\": {", - " \"resource\": {", - " \"type\": \"object\",", - " \"properties\": {", - " \"executionPeriod\": {", - " \"type\": \"object\",", - " \"properties\": {", - " \"end\": {},", - " \"start\": {}", - " }", - " },", - " \"identifier\": {", - " \"type\": \"array\",", - " \"items\": {", - " \"type\": \"object\",", - " \"properties\": {", - " \"system\": {},", - " \"use\": {},", - " \"valyue\": {}", - " }", - " }", - " },", - " \"input\": {", - " \"type\": \"array\",", - " \"items\": {", - " \"type\": \"object\",", - " \"properties\": {", - " \"type\": {},", - " \"valueString\": {}", - " }", - " }", - " },", - " \"intent\": {", - " \"type\": \"string\"", - " },", - " \"resourceType\": {", - " \"type\": \"string\"", - " },", - " \"status\": {", - " \"type\": \"string\"", - " }", - " }", - " }", - " }", - " }", - " },", - " \"resourceType\": {", - " \"type\": \"string\"", - " },", - " \"total\": {", - " \"type\": \"integer\"", - " },", - " \"type\": {", - " \"type\": \"string\"", - " }", - " }", - "};", - "", - "var respJson = pm.response.json();", - "", - "pm.test(\"Schema is valid\", function() {", - " pm.expect(tv4.validate(respJson, schema)).to.be.true;", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v2/jobs?_status=Completed", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v2", - "jobs" - ], - "query": [ - { - "key": "_status", - "value": "Completed" - } - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Negative Scenarios", - "item": [ - { - "name": "Start repeated type for /Patient EOB export", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient request\");\t", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 400\", function() {", - " pm.response.to.have.status(400);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details text is Repeated resource type ExplanationOfBenefit\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Repeated resource type ExplanationOfBenefit\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=ExplanationOfBenefit,ExplanationOfBenefit", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "ExplanationOfBenefit,ExplanationOfBenefit" - } - ] - } - }, - "response": [] - }, - { - "name": "Start Patient export Invalid Since Format", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient request\");", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 400\", function() {", - " pm.response.to.have.status(400);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details text is Request Error\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid date format supplied in _since parameter. Date must be in FHIR Instant format.\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Patient&_since=123invalid", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Patient" - }, - { - "key": "_since", - "value": "123invalid" - } - ] - } - }, - "response": [] - }, - { - "name": "Start Group export Invalid Since Format", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Group/all request\");", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 400\", function() {", - " pm.response.to.have.status(400);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details text is Request Error\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid date format supplied in _since parameter. Date must be in FHIR Instant format.\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_type=Patient&_since=123invalid", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Group", - "all", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Patient" - }, - { - "key": "_since", - "value": "123invalid" - } - ] - } - }, - "response": [] - }, - { - "name": "Start /Patient non-existing resource type export", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient request\");", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 400\", function() {", - " pm.response.to.have.status(400);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details text is Invalid Resource Type\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.include(\"invalid resource type\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Practitioner", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Practitioner" - } - ] - } - }, - "response": [] - }, - { - "name": "Start /Patient PACA resource type Claim export", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient with Claim resource type request\");", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 400\", function() {", - " pm.response.to.have.status(400);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details text is Invalid Resource Type\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.include(\"invalid resource type\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Claim", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Claim" - } - ] - } - }, - "response": [] - }, - { - "name": "Start /Patient PACA resource type ClaimResponse export", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient with ClaimResponse resource type request\");", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 400\", function() {", - " pm.response.to.have.status(400);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details text is Invalid Resource Type\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.include(\"invalid resource type\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=ClaimResponse", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "ClaimResponse" - } - ] - } - }, - "response": [] - }, - { - "name": "Start /Patient Mix BCDA & PACA resource type export", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient with Mix BCDA & PACA (Coverage & ClaimResponse resource type) request\");", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 400\", function() {", - " pm.response.to.have.status(400);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details text is Invalid Resource Type\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.include(\"invalid resource type\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Coverage,ClaimResponse", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Coverage,ClaimResponse" - } - ] - } - }, - "response": [] - }, - { - "name": "Start /Group non-existing resource type 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 request\");", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 400\", function() {", - " pm.response.to.have.status(400);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - " pm.test(\"Issue details text is Invalid Resource Type\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.include(\"invalid resource type\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_type=Practitioner", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Group", - "all", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Practitioner" - } - ] - } - }, - "response": [] - }, - { - "name": "Start /Group PACA resource type Claim 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 with Claim resource type request\");", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 400\", function() {", - " pm.response.to.have.status(400);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details text is Invalid Resource Type\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.include(\"invalid resource type\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_type=Claim", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Group", - "all", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Claim" - } - ] - } - }, - "response": [] - }, - { - "name": "Start /Group PACA resource type ClaimResponse 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 with ClaimResponse resource type request\");", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 400\", function() {", - " pm.response.to.have.status(400);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details text is Invalid Resource Type\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.include(\"invalid resource type\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_type=ClaimResponse", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Group", - "all", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "ClaimResponse" - } - ] - } - }, - "response": [] - }, - { - "name": "Start /Group Mix BCDA & PACA resource type 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 with Mix BCDA & PACA (Patient & Claim) resource type request\");", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 400\", function() {", - " pm.response.to.have.status(400);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details text is Invalid Resource Type\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.include(\"invalid resource type\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_type=Claim,Patient", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Group", - "all", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "Claim,Patient" - } - ] - } - }, - "response": [] - }, - { - "name": "Start repeated type for /Group EOB 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 request\");", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 400\", function() {", - " pm.response.to.have.status(400);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details text is Repeated resource type ExplanationOfBenefit\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Repeated resource type ExplanationOfBenefit\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_type=ExplanationOfBenefit,ExplanationOfBenefit", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Group", - "all", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "ExplanationOfBenefit,ExplanationOfBenefit" - } - ] - } - }, - "response": [] - }, - { - "name": "Start Group export, invalid group id", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Group/sub request\");", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 400\", function() {", - " pm.response.to.have.status(400);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details text is Invalid group ID\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid group ID\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/fhir+json", - "type": "text" - }, - { - "key": "Prefer", - "value": "respond-async", - "type": "text" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Group/sub/$export?_type=ExplanationOfBenefit", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Group", - "sub", - "$export" - ], - "query": [ - { - "key": "_type", - "value": "ExplanationOfBenefit" - } - ] - } - }, - "response": [] - }, - { - "name": "Start Patient export, invalid _elements param", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient request\");", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 400\", function() {", - " pm.response.to.have.status(400);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details text is Invalid group ID\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid parameter: this server does not support the _elements parameter.\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_elements=Patient", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ], - "query": [ - { - "key": "_elements", - "value": "Patient" - } - ] - } - }, - "response": [] - }, - { - "name": "Start Group export, invalid _elements param", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Group/all request\");", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " pm.test(\"Status code is 400\", function() {", - " pm.response.to.have.status(400);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details text is Invalid group ID\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid parameter: this server does not support the _elements parameter.\")", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_elements=Patient", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Group", - "all", - "$export" - ], - "query": [ - { - "key": "_elements", - "value": "Patient" - } - ] - } - }, - "response": [] - } - ] - } - ] - } - ] - }, - { - "name": "Blacklisted Scenarios", - "item": [ - { - "name": "Get Blacklisted auth token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "if (!pm.globals.get(\"blacklistedClientId\") || !pm.globals.get(\"blacklistedClientSecret\")) {", - " console.log(\"Blacklist test skipped due to creds not set.\")", - " return", - "}", - "var env = pm.environment.get(\"env\");", - "pm.environment.set(\"blacklistedClientId\", pm.globals.get(\"blacklistedClientId\"));", - "pm.environment.set(\"blacklistedClientSecret\", pm.globals.get(\"blacklistedClientSecret\"));", - "pm.test(\"Status code is 200\", function() {", - " pm.response.to.have.status(200);", - "});", - "", - "var responseJSON;", - "try {", - " responseJSON = JSON.parse(responseBody);", - " tests['response is valid JSON'] = true;", - "}", - "catch (e) {", - " responseJSON = {};", - " tests['response is valid JSON'] = false;", - "}", - "", - "pm.environment.set(\"blacklistedToken\", responseJSON.access_token);", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "username", - "value": "{{blacklistedClientId}}", - "type": "string" - }, - { - "key": "password", - "value": "{{blacklistedClientSecret}}", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "name": "Content-Type", - "type": "text", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "" - }, - "url": { - "raw": "{{scheme}}://{{host}}/auth/token", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "auth", - "token" - ] - }, - "description": "Retrieve token for blacklisted ACO" - }, - "response": [] - }, - { - "name": "Start Blacklisted Patient export", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", - "", - "if (maintenanceMode === \"eoy\") {\t", - " console.log(\"EOY mode is enabled - Skipping Patient request\");", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " if (!pm.globals.get(\"blacklistedClientId\") || !pm.globals.get(\"blacklistedClientSecret\")) {", - " console.log(\"Blacklist test skipped due to creds not set.\")", - " return", - " }", - " pm.test(\"Status code is 403 (Unauthorized)\", function() {", - " pm.response.to.have.status(403);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details text are unauthorized ACO\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.match(/ACO \\(CMS_ID: .*\\) is unauthorized/)", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{blacklistedToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Patient", - "$export" - ] - } - }, - "response": [] - }, - { - "name": "Start Blacklisted Group 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 request\");", - "", - " pm.test(\"Status code is 400, 404, or 500\", function() {", - " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", - " });", - "} else {", - " if (!pm.globals.get(\"blacklistedClientId\") || !pm.globals.get(\"blacklistedClientSecret\")) {", - " console.log(\"Blacklist test skipped due to creds not set.\")", - " return", - " }", - " pm.test(\"Status code is 403 (Unauthorized)\", function() {", - " pm.response.to.have.status(403);", - " });", - "", - " var respJson = pm.response.json();", - "", - " pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - " });", - "", - " pm.test(\"Issue details text are unauthorized ACO\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.match(/ACO \\(CMS_ID: .*\\) is unauthorized/)", - " });", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{blacklistedToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "Group", - "all", - "$export" - ] - } - }, - "response": [] - }, - { - "name": "Start Blacklisted Job retrieval", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "if (!pm.globals.get(\"blacklistedClientId\") || !pm.globals.get(\"blacklistedClientSecret\")) {", - " console.log(\"Blacklist test skipped due to creds not set.\")", - " return", - "}", - "pm.test(\"Status code is 403 (Unauthorized)\", function() {", - " pm.response.to.have.status(403);", - "});", - "", - "var respJson = pm.response.json();", - "", - "pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - "});", - "", - "pm.test(\"Issue details text are unauthorized ACO\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.match(/ACO \\(CMS_ID: .*\\) is unauthorized/)", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{blacklistedToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/api/v1/jobs/1", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "api", - "v1", - "jobs", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Start Blacklisted Data Retrieval", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "if (!pm.globals.get(\"blacklistedClientId\") || !pm.globals.get(\"blacklistedClientSecret\")) {", - " console.log(\"Blacklist test skipped due to creds not set.\")", - " return", - "}", - "pm.test(\"Status code is 403 (Unauthorized)\", function() {", - " pm.response.to.have.status(403);", - "});", - "", - "var respJson = pm.response.json();", - "", - "pm.test(\"Resource type is OperationOutcome\", function() {", - " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", - "});", - "", - "pm.test(\"Issue details text are unauthorized ACO\", function() {", - " pm.expect(respJson.issue[0].diagnostics).to.match(/ACO \\(CMS_ID: .*\\) is unauthorized/)", - "});" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{blacklistedToken}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Accept", - "type": "text", - "value": "application/fhir+json" - }, - { - "key": "Prefer", - "type": "text", - "value": "respond-async" - } - ], - "url": { - "raw": "{{scheme}}://{{host}}/data/test/test.ndjson", - "protocol": "{{scheme}}", - "host": [ - "{{host}}" - ], - "path": [ - "data", - "test", - "test.ndjson" - ] - } - }, - "response": [] - } - ] - } - ] + "info": { + "_postman_id": "817f4c7f-997e-4d35-aed3-ae00ef03c471", + "name": "Beneficiary Claims Data API Tests, Sequential", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "20886263" + }, + "item": [ + { + "name": "Informational Endpoints", + "item": [ + { + "name": "Get version", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response contains version\", function() {", + " pm.expect(pm.response.json()).to.have.property(\"version\");", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{scheme}}://{{host}}/_version", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["_version"] + } + }, + "response": [] + }, + { + "name": "Get metadata", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "", + "pm.test(\"Status code is 200\", function() {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type is application/json\", function() {", + " pm.response.to.have.header(\"Content-Type\", \"application/json\");", + "});", + "", + "var respJson = pm.response.json();", + "", + "pm.test(\"Resource type is CapabilityStatement\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"CapabilityStatement\")", + "});", + "", + "const schema = {", + " \"properties\": {", + " \"resourceType\": {", + " \"type\": \"string\"", + " },", + " \"status\": {", + " \"type\": \"string\"", + " },", + " \"date\": {", + " \"type\": \"string\"", + " },", + " \"publisher\": {", + " \"type\": \"string\"", + " },", + " \"kind\": {", + " \"type\": \"string\"", + " },", + " \"instantiates\": {", + " \"type\": \"array\"", + " },", + " \"software\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"name\": {},", + " \"version\": {},", + " \"releaseDate\": {}", + " }", + " },", + " \"implementation\": {", + " \"type\": \"object\"", + " },", + " \"fhirVersion\": {", + " \"type\": \"string\"", + " },", + " \"acceptUnknown\":{", + " \"type\": \"string\"", + " },", + " \"format\": {", + " \"type\": \"array\"", + " },", + " \"rest\": {", + " \"type\": \"array\"", + " }", + " }", + "};", + "", + "pm.test(\"Schema is valid\", function() {", + " pm.expect(tv4.validate(respJson, schema)).to.be.true;", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/metadata", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "metadata"] + } + }, + "response": [] + }, + { + "name": "Health check", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response contains database status\", function() {", + " pm.expect(pm.response.json()).to.have.property(\"database\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{scheme}}://{{host}}/_health", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["_health"] + } + }, + "response": [] + }, + { + "name": "Swagger V1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function() {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type is text/html; charset=utf-8\", function() {", + " pm.response.to.have.header(\"Content-Type\", \"text/html; charset=utf-8\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/swagger/", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "swagger", ""] + }, + "description": "TODO (BCDA-4109) - Remove test verifying v1 specific artifacts" + }, + "response": [] + }, + { + "name": "Swagger V2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (v2Disabled) {", + " pm.test(\"Status code is 404\", function() {", + " pm.response.to.have.status(404);", + " });", + " return;", + "}", + "", + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type is text/html; charset=utf-8\", function () {", + " pm.response.to.have.header(\"Content-Type\", \"text/html; charset=utf-8\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/swagger/", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "swagger", ""] + }, + "description": "Make request to location that should contain v1 and v2 API requests" + }, + "response": [] + } + ] + }, + { + "name": "Authenticated Endpoints", + "item": [ + { + "name": "Without Token", + "item": [ + { + "name": "V1", + "item": [ + { + "name": "Get Attribution Status v1, no token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "", + "pm.test(\"Status code is 401\", function() {", + " pm.response.to.have.status(401);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/attribution_status", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "attribution_status"] + } + }, + "response": [] + }, + { + "name": "Start Patient export, no token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 401\", function() {", + " pm.response.to.have.status(401);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details code is Invalid Token\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid Token\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Patient", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Patient" + } + ] + } + }, + "response": [] + }, + { + "name": "Start EOB export, no token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");\t\t\t", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 401\", function() {", + " pm.response.to.have.status(401);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " var respJson = pm.response.json();", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details code is Invalid Token\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid Token\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=ExplanationOfBenefit", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "ExplanationOfBenefit" + } + ] + } + }, + "response": [] + }, + { + "name": "Start Coverage export, no token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient Coverage endpoint request\");\t\t\t", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 401\", function() {", + " pm.response.to.have.status(401);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details code is Invalid Token\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid Token\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Coverage", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Coverage" + } + ] + } + }, + "response": [] + }, + { + "name": "Get job status, no token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 401\", function() {", + " pm.response.to.have.status(401);", + "});", + "", + "pm.test(\"Resource type is OperationOutcome\", function() {", + " var respJson = pm.response.json();", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text", + "disabled": true + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/jobs/{{jobId}}", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "jobs", "{{jobId}}"] + } + }, + "response": [] + }, + { + "name": "Delete job, no token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 401\", function() {", + " pm.response.to.have.status(401);", + "});", + "", + "pm.test(\"Resource type is OperationOutcome\", function() {", + " var respJson = pm.response.json();", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text", + "disabled": true + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/jobs/{{jobId}}", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "jobs", "{{jobId}}"] + } + }, + "response": [] + } + ] + }, + { + "name": "V2", + "item": [ + { + "name": "Get Attribution Status v2, no token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "", + "pm.test(\"Status code is 401\", function() {", + " pm.response.to.have.status(401);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/attribution_status", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "attribution_status"] + } + }, + "response": [] + }, + { + "name": "Start EOB export v2, no token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");\t\t\t", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 401\", function() {", + " pm.response.to.have.status(401);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " var respJson = pm.response.json();", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details code is Invalid Token\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid Token\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=ExplanationOfBenefit", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "ExplanationOfBenefit" + } + ] + } + }, + "response": [] + }, + { + "name": "Start Patient export v2, no token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 401\", function() {", + " pm.response.to.have.status(401);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details code is Invalid Token\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid Token\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=Patient", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Patient" + } + ] + } + }, + "response": [] + }, + { + "name": "Start Coverage export v2, no token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient Coverage endpoint request\");\t\t\t", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 401\", function() {", + " pm.response.to.have.status(401);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details code is Invalid Token\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid Token\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=Coverage", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Coverage" + } + ] + } + }, + "response": [] + }, + { + "name": "Get job status v2, no token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 401\", function() {", + " pm.response.to.have.status(401);", + "});", + "", + "pm.test(\"Resource type is OperationOutcome\", function() {", + " var respJson = pm.response.json();", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text", + "disabled": true + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/jobs/{{jobId}}", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "jobs", "{{jobId}}"] + } + }, + "response": [] + }, + { + "name": "Delete job v2, no token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 401\", function() {", + " pm.response.to.have.status(401);", + "});", + "", + "pm.test(\"Resource type is OperationOutcome\", function() {", + " var respJson = pm.response.json();", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text", + "disabled": true + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/jobs/{{jobId}}", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "jobs", "{{jobId}}"] + } + }, + "response": [] + }, + { + "name": "Get data, no token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 401\", function() {", + " pm.response.to.have.status(401);", + "});", + "", + "pm.test(\"Resource type is OperationOutcome\", function() {", + " var respJson = pm.response.json();", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{scheme}}://{{host}}/data/{{jobId}}/{{acoId}}.ndjson", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["data", "{{jobId}}", "{{acoId}}.ndjson"] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "With Token", + "item": [ + { + "name": "Get Access Token", + "item": [ + { + "name": "Get auth token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "var env = pm.environment.get(\"env\");", + "pm.environment.set(\"clientId\", pm.globals.get(\"clientId\"));", + "pm.environment.set(\"clientSecret\", pm.globals.get(\"clientSecret\"));", + "pm.test(\"Status code is 200\", function() {", + " pm.response.to.have.status(200);", + "});", + "", + "var responseJSON;", + "try {", + " responseJSON = JSON.parse(responseBody);", + " tests['response is valid JSON'] = true;", + "}", + "catch (e) {", + " responseJSON = {};", + " tests['response is valid JSON'] = false;", + "}", + "", + "pm.environment.set(\"token\", responseJSON.access_token);", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{clientSecret}}", + "type": "string" + }, + { + "key": "username", + "value": "{{clientId}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{scheme}}://{{host}}/auth/token", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["auth", "token"] + } + }, + "response": [] + } + ] + }, + { + "name": "Attribution Status Scenarios", + "item": [ + { + "name": "Get Attribution Status v1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "", + "pm.test(\"Status code is 200\", function() {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type is application/json\", function() {", + " pm.response.to.have.header(\"Content-Type\", \"application/json\");", + "});", + "", + "var respJson = pm.response.json();", + "", + "const schema = {", + " \"properties\": {", + " \"ingestion_dates\": {", + " \"type\": \"array\",", + " \"items\": [{", + " \"type\": \"object\",", + " \"properties\": {", + " \"type\": {", + " \"type\": \"string\"", + " },", + " \"timestamp\": {", + " \"type\": \"string\"", + " }", + " }", + " }]", + " }", + " }", + "};", + "", + "pm.test(\"Schema is valid\", function() {", + " pm.expect(tv4.validate(respJson, schema)).to.be.true;", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/attribution_status", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "attribution_status"] + } + }, + "response": [] + }, + { + "name": "Get Attribution Status v2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "", + "pm.test(\"Status code is 200\", function() {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Content-Type is application/json\", function() {", + " pm.response.to.have.header(\"Content-Type\", \"application/json\");", + "});", + "", + "var respJson = pm.response.json();", + "", + "const schema = {", + " \"properties\": {", + " \"ingestion_dates\": {", + " \"type\": \"array\",", + " \"items\": [{", + " \"type\": \"object\",", + " \"properties\": {", + " \"type\": {", + " \"type\": \"string\"", + " },", + " \"timestamp\": {", + " \"type\": \"string\"", + " }", + " }", + " }]", + " }", + " }", + "};", + "", + "pm.test(\"Schema is valid\", function() {", + " pm.expect(tv4.validate(respJson, schema)).to.be.true;", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/attribution_status", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "attribution_status"] + } + }, + "response": [] + } + ] + }, + { + "name": "Job Deletions", + "item": [ + { + "name": "Start EOB export v1 for Deletion", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");\t", + " pm.environment.set(\"eobJobUrl\", \"https://bcda.cms.gov\")\t\t", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} 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(\"eobJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=ExplanationOfBenefit", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "ExplanationOfBenefit" + } + ] + } + }, + "response": [] + }, + { + "name": "Delete job, valid token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");", + " return;", + "}", + "", + "pm.test(\"Status code is 202\", function() {", + " pm.response.to.have.status(202);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{eobJobUrl}}", + "host": ["{{eobJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Start EOB export v2 for Deletion", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");\t", + " pm.environment.set(\"eobv2JobUrl\", \"https://bcda.cms.gov\")\t\t", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} 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(\"eobv2JobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=ExplanationOfBenefit", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "ExplanationOfBenefit" + } + ] + } + }, + "response": [] + }, + { + "name": "Delete job v2, valid token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");", + " return;", + "}", + "", + "pm.test(\"Status code is 202\", function() {", + " pm.response.to.have.status(202);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{eobv2JobUrl}}", + "host": ["{{eobv2JobUrl}}"] + } + }, + "response": [] + } + ] + }, + { + "name": "Job End to End (General)", + "item": [ + { + "name": "Start Patient export v1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t\t ", + " pm.environment.set(\"patientJobUrl\", \"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(\"patientJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Patient", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Patient" + } + ] + } + }, + "response": [] + }, + { + "name": "Start Coverage export v1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient coverage endpoint request\");\t\t\t\t ", + " pm.environment.set(\"coverageJobUrl\", \"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(\"coverageJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Coverage", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Coverage" + } + ] + } + }, + "response": [] + }, + { + "name": "Start ALR export v1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.globals.get(\"alrEnabled\") != \"true\") {", + " console.log(\"Skipping ALR test. ALR is not enabled.\")", + " // Set the alrJobUrl to avoid pre-test validation checks (like ENOTFOUND)", + " pm.environment.set(\"alrJobUrl\", \"https://bcda.cms.gov\")", + " return;", + "}", + "", + "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(\"alrJobUrl\", pm.response.headers.get(\"Content-Location\"));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/alr/$export?_type=Patient,Observation", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "alr", "$export"], + "query": [ + { + "key": "_type", + "value": "Patient,Observation" + } + ] + } + }, + "response": [] + }, + { + "name": "Start EOB export v1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");\t\t\t\t ", + " pm.environment.set(\"eobJobUrl\", \"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(\"eobJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=ExplanationOfBenefit", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "ExplanationOfBenefit" + } + ] + } + }, + "response": [] + }, + { + "name": "Start Patient export v2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t\t ", + " pm.environment.set(\"patientv2JobUrl\", \"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 {", + " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + " if (v2Disabled) {", + " pm.test(\"Status code is 404\", function() {", + " pm.response.to.have.status(404);", + " });", + " return;", + " }", + "", + " 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(\"patientv2JobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=Patient", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Patient" + } + ] + } + }, + "response": [] + }, + { + "name": "Start Coverage export v2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient Coverage endpoint request\");\t\t\t\t ", + " pm.environment.set(\"coveragev2JobUrl\", \"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 {", + " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + " if (v2Disabled) {", + " pm.test(\"Status code is 404\", function() {", + " pm.response.to.have.status(404);", + " });", + " return;", + " }", + "", + " 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(\"coveragev2JobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=Coverage", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Coverage" + } + ] + } + }, + "response": [] + }, + { + "name": "Start EOB export v2", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");\t\t\t\t ", + " pm.environment.set(\"eobv2JobUrl\", \"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 {", + " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + " if (v2Disabled) {", + " pm.test(\"Status code is 404\", function() {", + " pm.response.to.have.status(404);", + " });", + " return;", + " }", + "", + " 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(\"eobv2JobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=ExplanationOfBenefit", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "ExplanationOfBenefit" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Patient export v2 job status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient request\");", + " pm.environment.set(\"patientv2DataUrl\", \"https://bcda.cms.gov\");", + " return;", + "} else {", + " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + " if (v2Disabled) {", + " console.log(\"Not running Patient export v2 job status, v2 endpoints have been disabled\");", + " 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\"];", + " const otherResources = [\"ExplanationOfBenefit\", \"Coverage\", \"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(\"patientv2DataUrl\", respJson.output[0].url);", + " }", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint pre-request\")", + " return;", + "}", + "", + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (v2Disabled) {", + " return;", + "}", + "", + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var eobJobReq = {", + " url: pm.environment.get(\"patientv2JobUrl\"),", + " 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 v2 still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for Patient v2 job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"Patient export v2 job complete.\");", + " } else {", + " console.error(\"Unexpected response from Patient export v2 job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{patientv2JobUrl}}", + "host": ["{{patientv2JobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Patient export v2 data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");", + " return;", + "}", + "", + "if (v2Disabled) {", + " console.log(\"Not running Patient export v2 data, v2 endpoints have been disabled\");", + " 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" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{patientv2DataUrl}}", + "host": ["{{patientv2DataUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Coverage export v2 job status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient request\");", + " pm.environment.set(\"coveragev2DataUrl\", \"https://bcda.cms.gov\");", + " return;", + "}", + "", + "if (v2Disabled) {", + " console.log(\"Not running Coverage export v2 job status, v2 endpoints have been disabled\");", + " 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 In Progress\", function() {", + " pm.expect(/^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 = [\"Coverage\"];", + " const otherResources = [\"ExplanationOfBenefit\", \"Patient\", \"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(\"coveragev2DataUrl\", respJson.output[0].url);", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint pre-request for coverage information\")", + " return;", + "}", + "", + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (v2Disabled) {", + " return;", + "}", + "", + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var coverageJobReq = {", + " url: pm.environment.get(\"coveragev2JobUrl\"),", + " method: \"GET\",", + " header: \"Authorization: Bearer \" + pm.environment.get(\"token\")", + "};", + "", + "function awaitExportJob(retryCount) {", + " pm.sendRequest(coverageJobReq, 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(\"Coverage export v2 still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for Coverage job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"Coverage export v2 job complete.\");", + " } else {", + " console.error(\"Unexpected response from Coverage export v2 job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{coveragev2JobUrl}}", + "host": ["{{coveragev2JobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Coverage export v2 data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");", + " return;", + "}", + "", + "if (v2Disabled) {", + " console.log(\"Not running Coverage export v2 data, v2 endpoints have been disabled\");", + " 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" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{coveragev2DataUrl}}", + "host": ["{{coveragev2DataUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get EOB export v2 job status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient request\");", + " pm.environment.set(\"eobv2DataUrl\", \"https://bcda.cms.gov\");", + " return;", + "}", + "", + "if (v2Disabled) {", + " console.log(\"Not running EOB export v2 job status, v2 endpoints have been disabled\");", + " 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 In Progress\", function() {", + " pm.expect(/^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 = [\"ExplanationOfBenefit\"];", + " const partiallyAdjudicatedResources = [\"Patient\", \"Coverage\", \"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 partiallyAdjudicatedResources) {", + " pm.expect(returnedResources, resource + \" resource type should not be returned\").to.not.include(resource);", + " }", + " });", + " ", + " pm.environment.set(\"eobv2DataUrl\", respJson.output[0].url);", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint pre-request for EOB information\")", + " return;", + "}", + "", + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (v2Disabled) {", + " return;", + "}", + "", + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var eobJobReq = {", + " url: pm.environment.get(\"eobv2JobUrl\"),", + " 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(\"ExplanationOfBenefit export v2 still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for ExplanationOfBenefit v2 job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"EOB export v2 job complete.\");", + " } else {", + " console.error(\"Unexpected response from EOB export v2 job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{eobv2JobUrl}}", + "host": ["{{eobv2JobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get EOB export v2 data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");", + " return;", + "}", + "", + "if (v2Disabled) {", + " console.log(\"Not running EOB export v2 data, v2 endpoints have been disabled\");", + " 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" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{eobv2DataUrl}}", + "host": ["{{eobv2DataUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Patient export job status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t\t ", + " pm.environment.set(\"patientDataUrl\", \"https://bcda.cms.gov\");", + " return;", + "} else {", + " 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 In Progress\", function() {", + " pm.expect(/^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\"];", + " const otherResources = [\"ExplanationOfBenefit\", \"Coverage\", \"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(\"patientDataUrl\", respJson.output[0].url);", + " }", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient pre-request\")", + " return;", + "}", + "", + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var eobJobReq = {", + " url: pm.environment.get(\"patientJobUrl\"),", + " 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": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{patientJobUrl}}", + "host": ["{{patientJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Patient export data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");", + " return;", + "} else {", + " 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" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{patientDataUrl}}", + "host": ["{{patientDataUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Coverage export job status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient coverage endpoint request\");", + " pm.environment.set(\"coverageDataUrl\", \"https://bcda.cms.gov\");", + " return;", + "} else {", + " 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 In Progress\", function() {", + " pm.expect(/^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 = [\"Coverage\"];", + " const otherResources = [\"ExplanationOfBenefit\", \"Patient\", \"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(\"coverageDataUrl\", respJson.output[0].url);", + " }", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient EOB,Coverage endpoint request\");", + " return;", + "}", + "", + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var coverageJobReq = {", + " url: pm.environment.get(\"coverageJobUrl\"),", + " method: \"GET\",", + " header: \"Authorization: Bearer \" + pm.environment.get(\"token\")", + "};", + "", + "function awaitExportJob(retryCount) {", + " pm.sendRequest(coverageJobReq, 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(\"Coverage export still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for Coverage job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"Coverage export job complete.\");", + " } else {", + " console.error(\"Unexpected response from Coverage export job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{coverageJobUrl}}", + "host": ["{{coverageJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Coverage export data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient coverage endpoint request\");", + " return;", + "} else {", + " 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" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{coverageDataUrl}}", + "host": ["{{coverageDataUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get EOB export job status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");", + " pm.environment.set(\"eobDataUrl\", \"https://bcda.cms.gov\");", + " return;", + "} else {", + " 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 In Progress\", function() {", + " pm.expect(/^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 = [\"ExplanationOfBenefit\"];", + " const otherResources = [\"Patient\", \"Coverage\", \"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(\"eobDataUrl\", respJson.output[0].url);", + " }", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");", + " return;", + "}", + "", + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var eobJobReq = {", + " url: pm.environment.get(\"eobJobUrl\"),", + " 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(\"ExplanationOfBenefit export still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for ExplanationOfBenefit job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"EOB export job complete.\");", + " } else {", + " console.error(\"Unexpected response from EOB export job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{eobJobUrl}}", + "host": ["{{eobJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get EOB export data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");", + " return;", + "} else {", + " 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" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{eobDataUrl}}", + "host": ["{{eobDataUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get ALR export job status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.globals.get(\"alrEnabled\") != \"true\") {", + " console.log(\"Skipping ALR test. ALR is not enabled.\")", + " // Set the alrDataUrl to avoid pre-test validation checks (like ENOTFOUND)", + " pm.environment.set(\"alrDataUrl\", \"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 In Progress\", function() {", + " pm.expect(/^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.environment.set(\"alrDataUrl\", respJson.output[0].url);", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "if (pm.globals.get(\"alrEnabled\") != \"true\") {", + " console.log(\"Skipping pre-request ALR script. ALR is not enabled.\")", + " return;", + "}", + "", + "var alrJobReq = {", + " url: pm.environment.get(\"alrJobUrl\"),", + " method: \"GET\",", + " header: \"Authorization: Bearer \" + pm.environment.get(\"token\")", + "};", + "", + "function awaitExportJob(retryCount) {", + " pm.sendRequest(alrJobReq, 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(\"ALR export still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for ALR job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"ALR export job complete.\");", + " } else {", + " console.error(\"Unexpected response from ALR export job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{alrJobUrl}}", + "host": ["{{alrJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get ALR export data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.globals.get(\"alrEnabled\") != \"true\") {", + " console.log(\"Skipping ALR test. ALR is not enabled.\")", + " 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": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{alrDataUrl}}", + "host": ["{{alrDataUrl}}"] + } + }, + "response": [] + } + ] + }, + { + "name": "Job End To End (Resource Type Combos)", + "item": [ + { + "name": "Kick Off Patient export with Patient, Coverage, EOB explicitly", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t\t ", + " pm.environment.set(\"patientv2withPatientCoverageEOBJobUrl\", \"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]);", + " });", + "} else {", + " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + " if (v2Disabled) {", + " pm.test(\"Status code is 404\", function() {", + " pm.response.to.have.status(404);", + " });", + " return;", + " }", + "", + " 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(\"patientv2withPatientCoverageEOBJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=Patient,Coverage,ExplanationOfBenefit", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Patient,Coverage,ExplanationOfBenefit" + } + ] + } + }, + "response": [] + }, + { + "name": "Kick Off Patient export with Patient, Coverage", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t\t ", + " pm.environment.set(\"patientv2withPatientCoverageJobUrl\", \"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]);", + " });", + "} else {", + " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + " if (v2Disabled) {", + " pm.test(\"Status code is 404\", function() {", + " pm.response.to.have.status(404);", + " });", + " return;", + " }", + "", + " 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(\"patientv2withPatientCoverageJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=Patient,Coverage", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Patient,Coverage" + } + ] + } + }, + "response": [] + }, + { + "name": "Kick Off Patient export with Patient, EOB", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t\t ", + " pm.environment.set(\"patientv2withPatientEOBJobUrl\", \"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]);", + " });", + "} else {", + " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + " if (v2Disabled) {", + " pm.test(\"Status code is 404\", function() {", + " pm.response.to.have.status(404);", + " });", + " return;", + " }", + "", + " 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(\"patientv2withPatientEOBJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=Patient,ExplanationOfBenefit", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Patient,ExplanationOfBenefit" + } + ] + } + }, + "response": [] + }, + { + "name": "Kick Off Patient export with Coverage, EOB", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient EOB, Coverage endpoint request\");\t\t\t\t ", + " pm.environment.set(\"patientv2withCoverageEOBJobUrl\", \"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]);", + " });", + "} else {", + " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + " if (v2Disabled) {", + " pm.test(\"Status code is 404\", function() {", + " pm.response.to.have.status(404);", + " });", + " return;", + " }", + "", + " 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(\"patientv2withCoverageEOBJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Patient/$export?_type=Coverage,ExplanationOfBenefit", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Coverage,ExplanationOfBenefit" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Job status for Patient export with Patient, Coverage, EOB explicitly", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient request\");", + " pm.environment.set(\"patientv2withPatientCoverageEOBDataUrl\", \"https://bcda.cms.gov\");", + " return;", + "}", + "", + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (v2Disabled) {", + " console.log(\"Not running Patient export with Patient, Coverage, EOB Resource Types explicitly job status, v2 endpoints have been disabled\");", + " 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 = [\"ExplanationOfBenefit\", \"Patient\", \"Coverage\"];", + " const partiallyAdjudicatedResources = [\"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 partiallyAdjudicatedResources) {", + " pm.expect(returnedResources, resource + \" resource type should not be returned\").to.not.include(resource);", + " }", + " });", + " ", + " pm.environment.set(\"patientv2withPatientCoverageEOBDataUrl\", respJson.output[0].url);", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint pre-request for EOB, Coverage information\")", + " return;", + "}", + "", + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (v2Disabled) {", + " return;", + "}", + "", + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var eobJobReq = {", + " url: pm.environment.get(\"patientv2withPatientCoverageEOBJobUrl\"),", + " 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 with Patient, Coverage, EOB Resource Types explicitly v2 still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for Patient export with Patient, Coverage, EOB Resource Types explicitly v2 job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"Patient export v2 job complete.\");", + " } else {", + " console.error(\"Unexpected response from Patient export with Patient, Coverage, EOB Resource Types explicitly v2 job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{patientv2withPatientCoverageEOBJobUrl}}", + "host": ["{{patientv2withPatientCoverageEOBJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Job status for Patient export with Patient, Coverage", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient Coverage endpoint request\");", + " pm.environment.set(\"patientv2withPatientCoverageDataUrl\", \"https://bcda.cms.gov\");", + " return;", + "}", + "", + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (v2Disabled) {", + " console.log(\"Not running Patient export with Patient, Coverage Resource Types job status, v2 endpoints have been disabled\");", + " 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\", \"Coverage\"];", + " const otherResources = [\"ExplanationOfBenefit\", \"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(\"patientv2withPatientCoverageDataUrl\", respJson.output[0].url);", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint pre-request for coverage information\")", + " return;", + "}", + "", + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (v2Disabled) {", + " return;", + "}", + "", + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var eobJobReq = {", + " url: pm.environment.get(\"patientv2withPatientCoverageJobUrl\"),", + " 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 with Patient, Coverage Resource Types v2 still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for Patient export with Patient, Coverage Resource Types v2 job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"Patient export v2 job complete.\");", + " } else {", + " console.error(\"Unexpected response from Patient export with Patient, Coverage Resource Types v2 job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{patientv2withPatientCoverageJobUrl}}", + "host": ["{{patientv2withPatientCoverageJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Job status for Patient export with Patient, EOB", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");", + " pm.environment.set(\"patientv2withPatientEOBDataUrl\", \"https://bcda.cms.gov\");", + " return;", + "}", + "", + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (v2Disabled) {", + " console.log(\"Not running Patient export with Patient, EOB Resource Types job status, v2 endpoints have been disabled\");", + " 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(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 = [\"ExplanationOfBenefit\", \"Patient\"];", + " const otherResources = [\"Coverage\", \"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(\"patientv2withPatientEOBDataUrl\", respJson.output[0].url);", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint pre-request for EOB information\")", + " return;", + "}", + "", + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (v2Disabled) {", + " return;", + "}", + "", + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var eobJobReq = {", + " url: pm.environment.get(\"patientv2withPatientEOBJobUrl\"),", + " 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 with Patient, EOB Resource Types v2 still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for Patient export with Patient, EOB Resource Types v2 job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"Patient export v2 job complete.\");", + " } else {", + " console.error(\"Unexpected response from Patient export with Patient, EOB Resource Types v2 job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{patientv2withPatientEOBJobUrl}}", + "host": ["{{patientv2withPatientEOBJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Job status for Patient export with Coverage, EOB", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient EOB, Coverage endpoint request\");", + " pm.environment.set(\"patientv2withCoverageEOBDataUrl\", \"https://bcda.cms.gov\");", + " return;", + "}", + "", + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (v2Disabled) {", + " console.log(\"Not running Patient export with Coverage, EOB Resource Types job status, v2 endpoints have been disabled\");", + " 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 = [\"Coverage\", \"ExplanationOfBenefit\"];", + " const otherResources = [\"Patient\", \"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(\"patientv2withCoverageEOBDataUrl\", respJson.output[0].url);", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint pre-request for EOB information\")", + " return;", + "}", + "", + "var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + "if (v2Disabled) {", + " return;", + "}", + "", + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var eobJobReq = {", + " url: pm.environment.get(\"patientv2withCoverageEOBJobUrl\"),", + " 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 with Coverage, EOB Resource Types v2 still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for Patient export with Coverage, EOB Resource Types v2 job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"Patient export v2 job complete.\");", + " } else {", + " console.error(\"Unexpected response from Patient export with Coverage, EOB Resource Types v2 job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{patientv2withCoverageEOBJobUrl}}", + "host": ["{{patientv2withCoverageEOBJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Data for Patient export with Patient, Coverage, EOB explicitly", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient EOB, Coverage endpoint request\");", + " return;", + "} else {", + " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + " if (v2Disabled) {", + " console.log(\"Not running Patient export with Patient, Coverage, EOB Resource Types Explicitly v2 data, v2 endpoints have been disabled\");", + " 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" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{patientv2withPatientCoverageEOBDataUrl}}", + "host": ["{{patientv2withPatientCoverageEOBDataUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Data for Patient export with Patient, Coverage", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient Coverage endpoint request\");", + " return;", + "} else {", + " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + " if (v2Disabled) {", + " console.log(\"Not running Patient export with Patient, Coverage Resource Types v2 data, v2 endpoints have been disabled\");", + " 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" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{patientv2withPatientCoverageDataUrl}}", + "host": ["{{patientv2withPatientCoverageDataUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Data for Patient export with Patient, EOB", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient EOB endpoint request\");", + " return;", + "} else {", + " var v2Disabled = pm.globals.get(\"v2Disabled\") == \"true\"", + "", + " if (v2Disabled) {", + " console.log(\"Not running Patient export with Patient, EOB Resource Types v2 data, v2 endpoints have been disabled\");", + " 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" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{patientv2withPatientEOBDataUrl}}", + "host": ["{{patientv2withPatientEOBDataUrl}}"] + } + }, + "response": [] + } + ] + }, + { + "name": "Job End To End (Parameterized)", + "item": [ + { + "name": "Start Patient export Valid Since", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");\t\t\t\t ", + " pm.environment.set(\"patientValidSinceJobUrl\", \"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]);", + " });", + "} 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(\"patientValidSinceJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient pre-request\")", + " return;", + "}", + "", + "let date = new Date();", + "date.setMinutes(date.getMinutes() - 1);", + "", + "let timestamp = date.toJSON();", + "pm.environment.set('now', timestamp);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Patient&_since={{now}}", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Patient" + }, + { + "key": "_since", + "value": "{{now}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Patient export Valid Since job status", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient request\");", + " return;", + "}", + "", + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var eobJobReq = {", + " url: pm.environment.get(\"patientValidSinceJobUrl\"),", + " 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" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Patient endpoint request\");", + " 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 In Progress\", function() {", + " pm.expect(/^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.expect(respJson.output).to.be.empty;", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{patientValidSinceJobUrl}}", + "host": ["{{patientValidSinceJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Start Group/all export without Since", + "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(\"groupAllNoSinceJobUrl\", \"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(\"groupAllNoSinceJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_type=Patient", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Group", "all", "$export"], + "query": [ + { + "key": "_type", + "value": "Patient" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Group/all without Since export job status", + "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\");", + " pm.environment.set(\"groupAllNoSinceJobUrl\", \"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 In Progress\", function() {", + " pm.expect(/^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\"];", + " const otherResources = [\"ExplanationOfBenefit\", \"Coverage\", \"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(\"groupAllNoSinceJobUrl\", respJson.output[0].url);", + "", + "", + " pm.test(\"One file in output\", function() {", + " pm.expect(respJson.output.length).to.eql(1)", + " });", + "", + " pm.test(\"File is of type Patient\", function() {", + " pm.expect(respJson.output[0].type).to.eql(\"Patient\")", + " });", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Group/all request\");", + " return;", + "}", + "", + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var groupAllNoSinceJobReq = {", + " url: pm.environment.get(\"groupAllNoSinceJobUrl\"),", + " method: \"GET\",", + " header: \"Authorization: Bearer \" + pm.environment.get(\"token\")", + "};", + "", + "function awaitExportJob(retryCount) {", + " pm.sendRequest(groupAllNoSinceJobReq, 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 (without _since) export still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for Group/all (without _since) job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"Group/all (without _since) export job complete.\");", + " } else {", + " console.error(\"Unexpected response from Group/all (without _since) export job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{groupAllNoSinceJobUrl}}", + "host": ["{{groupAllNoSinceJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Group/all without Since export 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" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{groupAllNoSinceJobUrl}}", + "host": ["{{groupAllNoSinceJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Start Group/all export v2 without Since", + "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(\"groupAllv2NoSinceJobUrl\", \"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(\"groupAllv2NoSinceJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Group/all/$export?_type=Patient", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Group", "all", "$export"], + "query": [ + { + "key": "_type", + "value": "Patient" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Group/all v2 without Since export job status", + "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\");", + " pm.environment.set(\"groupAllv2NoSinceJobUrl\", \"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 In Progress\", function() {", + " pm.expect(/^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\"];", + " const otherResources = [\"ExplanationOfBenefit\", \"Coverage\", \"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(\"groupAllv2NoSinceJobUrl\", respJson.output[0].url);", + "", + "", + " pm.test(\"One file in output\", function() {", + " pm.expect(respJson.output.length).to.eql(1)", + " });", + "", + " pm.test(\"File is of type Patient\", function() {", + " pm.expect(respJson.output[0].type).to.eql(\"Patient\")", + " });", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Group/all request\");", + " return;", + "}", + "", + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var groupAllNoSinceJobReq = {", + " url: pm.environment.get(\"groupAllv2NoSinceJobUrl\"),", + " method: \"GET\",", + " header: \"Authorization: Bearer \" + pm.environment.get(\"token\")", + "};", + "", + "function awaitExportJob(retryCount) {", + " pm.sendRequest(groupAllNoSinceJobReq, 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 (without _since) export still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for Group/all (without _since) job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"Group/all (without _since) export job complete.\");", + " } else {", + " console.error(\"Unexpected response from Group/all (without _since) export job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{groupAllv2NoSinceJobUrl}}", + "host": ["{{groupAllv2NoSinceJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Group/all v2 without Since export 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" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{groupAllv2NoSinceJobUrl}}", + "host": ["{{groupAllv2NoSinceJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Start Group/all export with Since", + "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(\"groupAllSinceJobUrl\", \"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(\"groupAllSinceJobUrl\", pm.response.headers.get(\"Content-Location\"));", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_type=Patient&_since=2020-02-13T08:00:00.000-05:00", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Group", "all", "$export"], + "query": [ + { + "key": "_type", + "value": "Patient" + }, + { + "key": "_since", + "value": "2020-02-13T08:00:00.000-05:00" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Group/all with Since export job status", + "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 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 In Progress\", function() {", + " pm.expect(/^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.environment.set(\"groupAllSinceJobUrl\", respJson.output[0].url);", + "", + " pm.test(\"Two files in output\", function() {", + " pm.expect(respJson.output.length).to.eql(2)", + " });", + "", + " pm.test(\"File 1 is of type Patient\", function() {", + " pm.expect(respJson.output[0].type).to.eql(\"Patient\")", + " });", + " ", + " pm.test(\"File 2 is of type Patient\", function() {", + " pm.expect(respJson.output[1].type).to.eql(\"Patient\")", + " });", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {", + " console.log(\"EOY mode is enabled - Skipping Group/all request\");", + " return;", + "}", + "", + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var groupAllSinceJobReq = {", + " url: pm.environment.get(\"groupAllSinceJobUrl\"),", + " method: \"GET\",", + " header: \"Authorization: Bearer \" + pm.environment.get(\"token\")", + "};", + "", + "function awaitExportJob(retryCount) {", + " pm.sendRequest(groupAllSinceJobReq, 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 (with _since) export still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.log(\"Retry limit reached for Group/all (with _since) job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"Group/all (with _since) export job complete.\");", + " } else {", + " console.error(\"Unexpected response from Group/all (with _since) export job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{groupAllSinceJobUrl}}", + "host": ["{{groupAllSinceJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Group/all with Since export 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" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{groupAllSinceJobUrl}}", + "host": ["{{groupAllSinceJobUrl}}"] + } + }, + "response": [] + } + ] + }, + { + "name": "Job End To End (Runouts)", + "item": [ + { + "name": "Start Group/runout export", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"groupRunoutJobUrl\", pm.response.headers.get(\"Content-Location\"));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Group/runout/$export?_type=ExplanationOfBenefit", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Group", "runout", "$export"], + "query": [ + { + "key": "_type", + "value": "ExplanationOfBenefit" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Group/runout export job status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 In Progress\", function() {", + " pm.expect(/^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 = [\"ExplanationOfBenefit\"];", + " const otherResources = [\"Patient\", \"Coverage\", \"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(\"groupRunoutDataUrl\", respJson.output[0].url);", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var eobJobReq = {", + " url: pm.environment.get(\"groupRunoutJobUrl\"),", + " 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(\"ExplanationOfBenefit export still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.error(\"Retry limit reached for ExplanationOfBenefit job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"EOB export job complete.\");", + " } else {", + " console.error(\"Unexpected response from EOB export job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{groupRunoutJobUrl}}", + "host": ["{{groupRunoutJobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Group/runout export data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{groupRunoutDataUrl}}", + "host": ["{{groupRunoutDataUrl}}"] + } + }, + "response": [] + }, + { + "name": "Start Group/runout v2 export", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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(\"groupRunoutv2JobUrl\", pm.response.headers.get(\"Content-Location\"));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/Group/runout/$export?_type=ExplanationOfBenefit", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "Group", "runout", "$export"], + "query": [ + { + "key": "_type", + "value": "ExplanationOfBenefit" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Group/runout export job status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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 In Progress\", function() {", + " pm.expect(/^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 = [\"ExplanationOfBenefit\"];", + " const otherResources = [\"Patient\", \"Coverage\", \"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(\"groupRunoutv2DataUrl\", respJson.output[0].url);", + "}" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const retryDelay = 5000;", + "const maxRetries = 20;", + "", + "var eobJobReq = {", + " url: pm.environment.get(\"groupRunoutv2JobUrl\"),", + " 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(\"ExplanationOfBenefit export still in progress. Retrying...\");", + " setTimeout(function() {", + " awaitExportJob(++retryCount);", + " }, retryDelay);", + " } else {", + " console.error(\"Retry limit reached for ExplanationOfBenefit job status.\");", + " postman.setNextRequest(null);", + " }", + " } else if (response.code == 200) {", + " console.log(\"EOB export job complete.\");", + " } else {", + " console.error(\"Unexpected response from EOB export job: \" + response.status);", + " }", + " });", + "}", + "", + "awaitExportJob(1);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json", + "disabled": true + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async", + "disabled": true + } + ], + "url": { + "raw": "{{groupRunoutv2JobUrl}}", + "host": ["{{groupRunoutv2JobUrl}}"] + } + }, + "response": [] + }, + { + "name": "Get Group/runout export data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{groupRunoutv2DataUrl}}", + "host": ["{{groupRunoutv2DataUrl}}"] + } + }, + "response": [] + } + ] + }, + { + "name": "Job Completion Status", + "item": [ + { + "name": "Get jobs status Completed data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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)", + "});", + "", + "const schema = {", + " \"properties\": {", + " \"entry\": {", + " \"type\": \"array\",", + " \"items\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"resource\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"executionPeriod\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"end\": {},", + " \"start\": {}", + " }", + " },", + " \"identifier\": {", + " \"type\": \"array\",", + " \"items\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"system\": {},", + " \"use\": {},", + " \"valyue\": {}", + " }", + " }", + " },", + " \"input\": {", + " \"type\": \"array\",", + " \"items\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"type\": {},", + " \"valueString\": {}", + " }", + " }", + " },", + " \"intent\": {", + " \"type\": \"string\"", + " },", + " \"resourceType\": {", + " \"type\": \"string\"", + " },", + " \"status\": {", + " \"type\": \"string\"", + " }", + " }", + " }", + " }", + " }", + " },", + " \"resourceType\": {", + " \"type\": \"string\"", + " },", + " \"total\": {", + " \"type\": \"integer\"", + " },", + " \"type\": {", + " \"type\": \"string\"", + " }", + " }", + "};", + "", + "var respJson = pm.response.json();", + "", + "pm.test(\"Schema is valid\", function() {", + " pm.expect(tv4.validate(respJson, schema)).to.be.true;", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/jobs?_status=Completed", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "jobs"], + "query": [ + { + "key": "_status", + "value": "Completed" + } + ] + } + }, + "response": [] + }, + { + "name": "Get jobs status v2 Completed data", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "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)", + "});", + "", + "const schema = {", + " \"properties\": {", + " \"entry\": {", + " \"type\": \"array\",", + " \"items\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"resource\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"executionPeriod\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"end\": {},", + " \"start\": {}", + " }", + " },", + " \"identifier\": {", + " \"type\": \"array\",", + " \"items\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"system\": {},", + " \"use\": {},", + " \"valyue\": {}", + " }", + " }", + " },", + " \"input\": {", + " \"type\": \"array\",", + " \"items\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"type\": {},", + " \"valueString\": {}", + " }", + " }", + " },", + " \"intent\": {", + " \"type\": \"string\"", + " },", + " \"resourceType\": {", + " \"type\": \"string\"", + " },", + " \"status\": {", + " \"type\": \"string\"", + " }", + " }", + " }", + " }", + " }", + " },", + " \"resourceType\": {", + " \"type\": \"string\"", + " },", + " \"total\": {", + " \"type\": \"integer\"", + " },", + " \"type\": {", + " \"type\": \"string\"", + " }", + " }", + "};", + "", + "var respJson = pm.response.json();", + "", + "pm.test(\"Schema is valid\", function() {", + " pm.expect(tv4.validate(respJson, schema)).to.be.true;", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v2/jobs?_status=Completed", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v2", "jobs"], + "query": [ + { + "key": "_status", + "value": "Completed" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Negative Scenarios", + "item": [ + { + "name": "Start repeated type for /Patient EOB export", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient request\");\t", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 400\", function() {", + " pm.response.to.have.status(400);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details text is Repeated resource type ExplanationOfBenefit\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Repeated resource type ExplanationOfBenefit\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=ExplanationOfBenefit,ExplanationOfBenefit", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "ExplanationOfBenefit,ExplanationOfBenefit" + } + ] + } + }, + "response": [] + }, + { + "name": "Start Patient export Invalid Since Format", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient request\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 400\", function() {", + " pm.response.to.have.status(400);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details text is Request Error\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid date format supplied in _since parameter. Date must be in FHIR Instant format.\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Patient&_since=123invalid", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Patient" + }, + { + "key": "_since", + "value": "123invalid" + } + ] + } + }, + "response": [] + }, + { + "name": "Start Group export Invalid Since Format", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Group/all request\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 400\", function() {", + " pm.response.to.have.status(400);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details text is Request Error\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid date format supplied in _since parameter. Date must be in FHIR Instant format.\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_type=Patient&_since=123invalid", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Group", "all", "$export"], + "query": [ + { + "key": "_type", + "value": "Patient" + }, + { + "key": "_since", + "value": "123invalid" + } + ] + } + }, + "response": [] + }, + { + "name": "Start /Patient non-existing resource type export", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient request\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 400\", function() {", + " pm.response.to.have.status(400);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details text is Invalid Resource Type\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.include(\"invalid resource type\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Practitioner", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Practitioner" + } + ] + } + }, + "response": [] + }, + { + "name": "Start /Patient PACA resource type Claim export", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient with Claim resource type request\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 400\", function() {", + " pm.response.to.have.status(400);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details text is Invalid Resource Type\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.include(\"invalid resource type\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Claim", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Claim" + } + ] + } + }, + "response": [] + }, + { + "name": "Start /Patient PACA resource type ClaimResponse export", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient with ClaimResponse resource type request\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 400\", function() {", + " pm.response.to.have.status(400);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details text is Invalid Resource Type\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.include(\"invalid resource type\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=ClaimResponse", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "ClaimResponse" + } + ] + } + }, + "response": [] + }, + { + "name": "Start /Patient Mix BCDA & PACA resource type export", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient with Mix BCDA & PACA (Coverage & ClaimResponse resource type) request\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 400\", function() {", + " pm.response.to.have.status(400);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details text is Invalid Resource Type\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.include(\"invalid resource type\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_type=Coverage,ClaimResponse", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Patient", "$export"], + "query": [ + { + "key": "_type", + "value": "Coverage,ClaimResponse" + } + ] + } + }, + "response": [] + }, + { + "name": "Start /Group non-existing resource type 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 request\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 400\", function() {", + " pm.response.to.have.status(400);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + " pm.test(\"Issue details text is Invalid Resource Type\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.include(\"invalid resource type\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_type=Practitioner", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Group", "all", "$export"], + "query": [ + { + "key": "_type", + "value": "Practitioner" + } + ] + } + }, + "response": [] + }, + { + "name": "Start /Group PACA resource type Claim 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 with Claim resource type request\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 400\", function() {", + " pm.response.to.have.status(400);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details text is Invalid Resource Type\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.include(\"invalid resource type\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_type=Claim", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Group", "all", "$export"], + "query": [ + { + "key": "_type", + "value": "Claim" + } + ] + } + }, + "response": [] + }, + { + "name": "Start /Group PACA resource type ClaimResponse 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 with ClaimResponse resource type request\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 400\", function() {", + " pm.response.to.have.status(400);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details text is Invalid Resource Type\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.include(\"invalid resource type\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_type=ClaimResponse", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Group", "all", "$export"], + "query": [ + { + "key": "_type", + "value": "ClaimResponse" + } + ] + } + }, + "response": [] + }, + { + "name": "Start /Group Mix BCDA & PACA resource type 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 with Mix BCDA & PACA (Patient & Claim) resource type request\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 400\", function() {", + " pm.response.to.have.status(400);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details text is Invalid Resource Type\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.include(\"invalid resource type\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_type=Claim,Patient", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Group", "all", "$export"], + "query": [ + { + "key": "_type", + "value": "Claim,Patient" + } + ] + } + }, + "response": [] + }, + { + "name": "Start repeated type for /Group EOB 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 request\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 400\", function() {", + " pm.response.to.have.status(400);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details text is Repeated resource type ExplanationOfBenefit\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Repeated resource type ExplanationOfBenefit\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_type=ExplanationOfBenefit,ExplanationOfBenefit", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Group", "all", "$export"], + "query": [ + { + "key": "_type", + "value": "ExplanationOfBenefit,ExplanationOfBenefit" + } + ] + } + }, + "response": [] + }, + { + "name": "Start Group export, invalid group id", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Group/sub request\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 400\", function() {", + " pm.response.to.have.status(400);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details text is Invalid group ID\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid group ID\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/fhir+json", + "type": "text" + }, + { + "key": "Prefer", + "value": "respond-async", + "type": "text" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Group/sub/$export?_type=ExplanationOfBenefit", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Group", "sub", "$export"], + "query": [ + { + "key": "_type", + "value": "ExplanationOfBenefit" + } + ] + } + }, + "response": [] + }, + { + "name": "Start Patient export, invalid _elements param", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient request\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 400\", function() {", + " pm.response.to.have.status(400);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details text is Invalid group ID\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid parameter: this server does not support the _elements parameter.\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export?_elements=Patient", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Patient", "$export"], + "query": [ + { + "key": "_elements", + "value": "Patient" + } + ] + } + }, + "response": [] + }, + { + "name": "Start Group export, invalid _elements param", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Group/all request\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " pm.test(\"Status code is 400\", function() {", + " pm.response.to.have.status(400);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details text is Invalid group ID\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.eql(\"Invalid parameter: this server does not support the _elements parameter.\")", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export?_elements=Patient", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Group", "all", "$export"], + "query": [ + { + "key": "_elements", + "value": "Patient" + } + ] + } + }, + "response": [] + } + ] + } + ] + } + ] + }, + { + "name": "Blacklisted Scenarios", + "item": [ + { + "name": "Get Blacklisted auth token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (!pm.globals.get(\"blacklistedClientId\") || !pm.globals.get(\"blacklistedClientSecret\")) {", + " console.log(\"Blacklist test skipped due to creds not set.\")", + " return", + "}", + "var env = pm.environment.get(\"env\");", + "pm.environment.set(\"blacklistedClientId\", pm.globals.get(\"blacklistedClientId\"));", + "pm.environment.set(\"blacklistedClientSecret\", pm.globals.get(\"blacklistedClientSecret\"));", + "pm.test(\"Status code is 200\", function() {", + " pm.response.to.have.status(200);", + "});", + "", + "var responseJSON;", + "try {", + " responseJSON = JSON.parse(responseBody);", + " tests['response is valid JSON'] = true;", + "}", + "catch (e) {", + " responseJSON = {};", + " tests['response is valid JSON'] = false;", + "}", + "", + "pm.environment.set(\"blacklistedToken\", responseJSON.access_token);", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "{{blacklistedClientId}}", + "type": "string" + }, + { + "key": "password", + "value": "{{blacklistedClientSecret}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{scheme}}://{{host}}/auth/token", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["auth", "token"] + }, + "description": "Retrieve token for blacklisted ACO" + }, + "response": [] + }, + { + "name": "Start Blacklisted Patient export", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const maintenanceMode = pm.globals.get(\"maintenanceMode\");", + "", + "if (maintenanceMode === \"eoy\") {\t", + " console.log(\"EOY mode is enabled - Skipping Patient request\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " if (!pm.globals.get(\"blacklistedClientId\") || !pm.globals.get(\"blacklistedClientSecret\")) {", + " console.log(\"Blacklist test skipped due to creds not set.\")", + " return", + " }", + " pm.test(\"Status code is 403 (Unauthorized)\", function() {", + " pm.response.to.have.status(403);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details text are unauthorized ACO\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.match(/ACO \\(CMS_ID: .*\\) is unauthorized/)", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{blacklistedToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Patient/$export", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Patient", "$export"] + } + }, + "response": [] + }, + { + "name": "Start Blacklisted Group 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 request\");", + "", + " pm.test(\"Status code is 400, 404, or 500\", function() {", + " pm.expect(pm.response.code).to.be.oneOf([400, 404, 500]);", + " });", + "} else {", + " if (!pm.globals.get(\"blacklistedClientId\") || !pm.globals.get(\"blacklistedClientSecret\")) {", + " console.log(\"Blacklist test skipped due to creds not set.\")", + " return", + " }", + " pm.test(\"Status code is 403 (Unauthorized)\", function() {", + " pm.response.to.have.status(403);", + " });", + "", + " var respJson = pm.response.json();", + "", + " pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + " });", + "", + " pm.test(\"Issue details text are unauthorized ACO\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.match(/ACO \\(CMS_ID: .*\\) is unauthorized/)", + " });", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{blacklistedToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/Group/all/$export", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "Group", "all", "$export"] + } + }, + "response": [] + }, + { + "name": "Start Blacklisted Job retrieval", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (!pm.globals.get(\"blacklistedClientId\") || !pm.globals.get(\"blacklistedClientSecret\")) {", + " console.log(\"Blacklist test skipped due to creds not set.\")", + " return", + "}", + "pm.test(\"Status code is 403 (Unauthorized)\", function() {", + " pm.response.to.have.status(403);", + "});", + "", + "var respJson = pm.response.json();", + "", + "pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + "});", + "", + "pm.test(\"Issue details text are unauthorized ACO\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.match(/ACO \\(CMS_ID: .*\\) is unauthorized/)", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{blacklistedToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/api/v1/jobs/1", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["api", "v1", "jobs", "1"] + } + }, + "response": [] + }, + { + "name": "Start Blacklisted Data Retrieval", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (!pm.globals.get(\"blacklistedClientId\") || !pm.globals.get(\"blacklistedClientSecret\")) {", + " console.log(\"Blacklist test skipped due to creds not set.\")", + " return", + "}", + "pm.test(\"Status code is 403 (Unauthorized)\", function() {", + " pm.response.to.have.status(403);", + "});", + "", + "var respJson = pm.response.json();", + "", + "pm.test(\"Resource type is OperationOutcome\", function() {", + " pm.expect(respJson.resourceType).to.eql(\"OperationOutcome\")", + "});", + "", + "pm.test(\"Issue details text are unauthorized ACO\", function() {", + " pm.expect(respJson.issue[0].diagnostics).to.match(/ACO \\(CMS_ID: .*\\) is unauthorized/)", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{blacklistedToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "type": "text", + "value": "application/fhir+json" + }, + { + "key": "Prefer", + "type": "text", + "value": "respond-async" + } + ], + "url": { + "raw": "{{scheme}}://{{host}}/data/test/test.ndjson", + "protocol": "{{scheme}}", + "host": ["{{host}}"], + "path": ["data", "test", "test.ndjson"] + } + }, + "response": [] + } + ] + } + ] } From 627626499553d453b705cb2b2e37ee0d8d3f7a28 Mon Sep 17 00:00:00 2001 From: Kevin Yeh Date: Fri, 5 Jul 2024 12:42:08 -0400 Subject: [PATCH 12/16] Remove stdout print statements (#970) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽซ Ticket N/A ## ๐Ÿ›  Changes Remove print statements ## โ„น๏ธ Context These were causing the error alarm to go off. We used to duplicate messages to stdout and to the formatted JSON Logger since only stdout would be printed in Jenkins logs. Since we are running in lambda now, we can remove that. ## ๐Ÿงช Validation Unit Testing --- optout/local_file_handler.go | 1 + optout/s3_file_handler.go | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/optout/local_file_handler.go b/optout/local_file_handler.go index 225399a54..9e27025b3 100644 --- a/optout/local_file_handler.go +++ b/optout/local_file_handler.go @@ -12,6 +12,7 @@ import ( ) // LocalFileHandler manages files from local directories. +// This handler should only be used for local dev/testing now. type LocalFileHandler struct { Logger logrus.FieldLogger PendingDeletionDir string diff --git a/optout/s3_file_handler.go b/optout/s3_file_handler.go index 4c0b21340..7e351a7b1 100644 --- a/optout/s3_file_handler.go +++ b/optout/s3_file_handler.go @@ -30,17 +30,14 @@ type S3FileHandler struct { // 2. stdout (Jenkins) func (handler *S3FileHandler) Infof(format string, rest ...interface{}) { - fmt.Printf(format, rest...) handler.Logger.Infof(format, rest...) } func (handler *S3FileHandler) Warningf(format string, rest ...interface{}) { - fmt.Printf(format, rest...) handler.Logger.Warningf(format, rest...) } func (handler *S3FileHandler) Errorf(format string, rest ...interface{}) { - fmt.Printf(format, rest...) handler.Logger.Errorf(format, rest...) } From 777b548fab967cb52dbe382476f1a266ae4f0b2f Mon Sep 17 00:00:00 2001 From: Kevin Yeh Date: Fri, 5 Jul 2024 12:45:18 -0400 Subject: [PATCH 13/16] BCDA-8234: Import a single CCLF file zip when triggered by BFD S3 notifications (#968) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽซ Ticket https://jira.cms.gov/browse/BCDA-8234 ## ๐Ÿ›  Changes - Update lambda entrypoint to only import the CCLF file that caused the lambda instance to start - Consolidate "valid2" test zips into a single zip to match production format (CCLF0 and CCLF8 files come in a single zip, not split across multiple zips) ## โ„น๏ธ Context To avoid race conditions, we would like to ensure that each lambda instance imports only the CCLF file that started the lambda instance via S3 notifications. This ensures that each lambda instance has exactly one file responsibility, and there are no conflicts between lambdas attempting to import the same file. Note that this technically allows us to still process entire directories at once by manually sending events in the AWS Console with just the directory name, instead of the path for a single file. ## ๐Ÿงช Validation - Updated unit tests --- bcda/cclf/s3_fileprocessor_test.go | 41 ++++++++++++++++++ bcda/lambda/cclf/main.go | 6 ++- bcda/lambda/cclf/main_test.go | 11 ++--- .../valid2/T.BCD.A0001.ZCY18.D181120.T1000000 | Bin 0 -> 1542 bytes .../valid2/T.BCD.A0002.ZCY18.D181120.T1000000 | Bin 501 -> 0 bytes .../valid2/T.BCD.A0002.ZCY18.D181121.T1000000 | Bin 747 -> 0 bytes .../valid2/T.BCD.A0002.ZCY18.D181122.T1000000 | Bin 338 -> 0 bytes 7 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 shared_files/cclf/archives/valid2/T.BCD.A0001.ZCY18.D181120.T1000000 delete mode 100644 shared_files/cclf/archives/valid2/T.BCD.A0002.ZCY18.D181120.T1000000 delete mode 100644 shared_files/cclf/archives/valid2/T.BCD.A0002.ZCY18.D181121.T1000000 delete mode 100644 shared_files/cclf/archives/valid2/T.BCD.A0002.ZCY18.D181122.T1000000 diff --git a/bcda/cclf/s3_fileprocessor_test.go b/bcda/cclf/s3_fileprocessor_test.go index 92c2b4886..c69e1987a 100644 --- a/bcda/cclf/s3_fileprocessor_test.go +++ b/bcda/cclf/s3_fileprocessor_test.go @@ -89,6 +89,47 @@ func (s *S3ProcessorTestSuite) TestLoadCclfFiles() { } } +func (s *S3ProcessorTestSuite) TestLoadCclfFiles_SingleFile() { + cmsID, key := "A0001", metadataKey{perfYear: 18, fileType: models.FileTypeDefault} + tests := []struct { + path string + filename string + numCCLFFiles int // Expected count for the cmsID, perfYear above + skipped int + failure int + numCCLF0 int // Expected count for the cmsID, perfYear above + numCCLF8 int // Expected count for the cmsID, perfYear above + }{ + {"cclf/archives/valid2/", "T.BCD.A0001.ZCY18.D181120.T1000000", 2, 0, 0, 1, 1}, + } + + for _, tt := range tests { + s.T().Run(tt.path, func(t *testing.T) { + bucketName, cleanup := testUtils.CopyToS3(s.T(), filepath.Join(s.basePath, tt.path)) + defer cleanup() + + cclfMap, skipped, failure, err := s.processor.LoadCclfFiles(filepath.Join(bucketName, tt.path, tt.filename)) + cclfFiles := cclfMap[cmsID][key] + assert.NoError(t, err) + assert.Equal(t, tt.skipped, skipped) + assert.Equal(t, tt.failure, failure) + assert.Equal(t, tt.numCCLFFiles, len(cclfFiles)) + var numCCLF0, numCCLF8 int + for _, cclfFile := range cclfFiles { + if cclfFile.cclfNum == 0 { + numCCLF0++ + } else if cclfFile.cclfNum == 8 { + numCCLF8++ + } else { + assert.Fail(t, "Unexpected CCLF num received %d", cclfFile.cclfNum) + } + } + assert.Equal(t, tt.numCCLF0, numCCLF0) + assert.Equal(t, tt.numCCLF8, numCCLF8) + }) + } +} + func (s *S3ProcessorTestSuite) TestLoadCclfFiles_InvalidPath() { cclfMap, skipped, failure, err := s.processor.LoadCclfFiles("foo") assert.ErrorContains(s.T(), err, "NoSuchBucket: The specified bucket does not exist") diff --git a/bcda/lambda/cclf/main.go b/bcda/lambda/cclf/main.go index 24272aea5..e22df4589 100644 --- a/bcda/lambda/cclf/main.go +++ b/bcda/lambda/cclf/main.go @@ -55,8 +55,10 @@ func cclfImportHandler(ctx context.Context, sqsEvent events.SQSEvent) (string, e return "", err } - dir := bcdaaws.ParseS3Directory(e.S3.Bucket.Name, e.S3.Object.Key) - return handleCclfImport(s3AssumeRoleArn, dir) + // Send the entire filepath into the CCLF Importer so we are only + // importing the one file that was sent in the trigger. + filepath := fmt.Sprintf("%s/%s", e.S3.Bucket.Name, e.S3.Object.Key) + return handleCclfImport(s3AssumeRoleArn, filepath) } } diff --git a/bcda/lambda/cclf/main_test.go b/bcda/lambda/cclf/main_test.go index 2f2137770..e6189740e 100644 --- a/bcda/lambda/cclf/main_test.go +++ b/bcda/lambda/cclf/main_test.go @@ -23,7 +23,7 @@ func TestCclfImportMainSuite(t *testing.T) { } func (s *CclfImportMainSuite) TestImportCCLFDirectory() { - targetACO := "A0002" + targetACO := "A0001" assert := assert.New(s.T()) env := uuid.NewUUID() @@ -42,14 +42,15 @@ func (s *CclfImportMainSuite) TestImportCCLFDirectory() { type test struct { path string + filename string err error expectedLogs []string } tests := []test{ - {path: "../../../shared_files/cclf/archives/valid2/", expectedLogs: []string{"Successfully imported 2 files.", "Failed to import 0 files.", "Skipped 0 files."}}, - {path: "../../../shared_files/cclf/archives/invalid_bcd/", err: errors.New("one or more files failed to import correctly"), expectedLogs: []string{}}, - {path: "../../../shared_files/cclf/archives/skip/", expectedLogs: []string{"Successfully imported 0 files.", "Failed to import 0 files.", "Skipped 0 files."}}, + {path: "../../../shared_files/cclf/archives/valid2/", filename: "cclf/archives/valid2/T.BCD.A0001.ZCY18.D181120.T1000000", expectedLogs: []string{"Successfully imported 2 files.", "Failed to import 0 files.", "Skipped 0 files."}}, + {path: "../../../shared_files/cclf/archives/invalid_bcd/", filename: "cclf/archives/invalid_bcd/P.BCD.A0009.ZCY18.D181120.T0001000", err: errors.New("one or more files failed to import correctly"), expectedLogs: []string{}}, + {path: "../../../shared_files/cclf/archives/skip/", filename: "cclf/archives/skip/T.BCD.ACOB.ZC0Y18.D181120.T0001000", expectedLogs: []string{"Successfully imported 0 files.", "Failed to import 0 files.", "Skipped 0 files."}}, } for _, tc := range tests { @@ -59,7 +60,7 @@ func (s *CclfImportMainSuite) TestImportCCLFDirectory() { path, cleanup := testUtils.CopyToS3(s.T(), tc.path) defer cleanup() - res, err := cclfImportHandler(context.Background(), testUtils.GetSQSEvent(s.T(), path, "fake_filename")) + res, err := cclfImportHandler(context.Background(), testUtils.GetSQSEvent(s.T(), path, tc.filename)) if tc.err == nil { assert.Nil(err) diff --git a/shared_files/cclf/archives/valid2/T.BCD.A0001.ZCY18.D181120.T1000000 b/shared_files/cclf/archives/valid2/T.BCD.A0001.ZCY18.D181120.T1000000 new file mode 100644 index 0000000000000000000000000000000000000000..754f0e7f415e9f879df98acb039dff40fc46011e GIT binary patch literal 1542 zcmWIWW@Zs#U|`^2@M_5OJ6-Yp0zVT2!xJ6`24w~rh7dg`XBRz30|Ns?y(njkNJ9%f z7efm}Ln8yd5JRA-fq`Xc2qy!x_vWsc1i7}f(h6<{MwV}k3=CjHmrnD|zU?5;_Wq~n zFXpD1H}zkxIAJLAVWvkN&bmYiISrH6)>PH%TK#EUDcz;*lbx{maQL~jBE89_rjG-c$f~XzLh`zd^126oy!+nkpI*NGbMEp(M$M{UGd$mSZ)lk$&fy() zz^%PiwQ|nsCwtDhdS438Iem1J{T`8N7cx3Cxx51m1sOJe3sIVyz=W)U6GrNwc>kCX<)6mPu?fuDO`pL?<-P4vVzcKA`c=xvL zNACY&4M0h2euB|^%77^^0GL`Z)0!n_S~D~Nr!^m7T1&!AYq{qQ`3@`auwJlT{(#Bj z|80j?g0~dCpWXSdw(*Ktp|(z&{f9q^!WvUp?_3tGUKDzI${LXkdxN<)=iEOXbV~I2 zmC}Y5{>7JXzsxW$uThZIv##Ep*#F%7YoAm0i3RfI*=?8am$QXG>bkMr@1J-~uXfV> z<-hKiGaAF$U?Sij_{B+xtV%-FnSfW=FOU@v`}@Gd43(@S$Np3Q185McjMJwHiq z;j2|=3;!-^f2(xKdHqU1(*-9d9XZs*e_ulIApf>pKTglOGiU6Rbq{TQZx_hKbF;!L zVCtm1Cc-jjr0%sY(-n!}iq!0IJ#Wf$Sy1mp_FbE!A61LbT%O~_o8A$9%8GTphxXy9 zlFe6*560hh$yJ9 zo+GB3xaFCX;aBn0S10CA<@oe$UBpR^aCYaLT5%5|RU@W6(Y5_oXAo50>%99#i zjh{}1B?td<)jhX4d1}VYclII*_xJ<68JXmmaTQw&iur7H}JSlK{&~39Pu4xO^}Ssn6VJ^G zuYjqO?wSb8oRPZMx=dFjf-6$9!}Yu=&t*Zq6WModj($`vK681F7jJq;^eHRW^&Z-X zqe?cnrAp7(>m9RthxF0|`A=+}dSaw?9epFM7i%Bo>PKIB_Q(v8!Kb7Otvvm=H#gvGvC>ZDBR-@@MdI^W5yNk62SOiU;qXp!;(f2i>O#&g~S3{ V3VWvkN&bmYiISrH6)>PH%TK#EUDcz;*lbx{maQL~jBE89_rjG-c$f~XzLh`zd^126oy!+nkpI*NGbMEp(M$M{UGd$mSZ)lk$&fy() zz^%PiwQ|nsCwtDhdS438Iem1J{T`8N7cx3Cxx51m1sOJe3sIVyz=W)U6GrNwc>kCX<)6mPu?fuDO`pL?<-P4vVzcKA`c=xvL zNACY&4e(}Ul4Hh|+9ZIfkbwc1bQqR2f>=bQJyuBCLrZr7-mGjOBN%}&6G-O(lQaVZ E0694_kN^Mx diff --git a/shared_files/cclf/archives/valid2/T.BCD.A0002.ZCY18.D181122.T1000000 b/shared_files/cclf/archives/valid2/T.BCD.A0002.ZCY18.D181122.T1000000 deleted file mode 100644 index 77cfe00aa7685791391e7cbd7c573403b01d984f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 338 zcmWIWW@Zs#U|`^2@M_5O^An8TQwHP(FfuSGGsrN6=s7vN=s6k~7#QkBIa@{=TIjhL zS{ND{8R&%=0!0lB4MIaW8JK-Gcg39X(vK^x;AUWC`3lqxHZ=FVA>UyI9@Y!C%O5a# z{J-t+O7NDV_p>|y)iz!+E7aC$v;XiXQCMRN>z&J@)r&$;Pgx_fVQ(D5k} zzx>zza;5-pMkYCCT%M8udXIqt=t71ijUX0LUSx%M5zTu6-mGjOBN%}&6G&eMaTowD CQ(^c3 From ec9c42688a59a401c49dba59180ea52f8655e5dd Mon Sep 17 00:00:00 2001 From: Kevin Yeh Date: Mon, 8 Jul 2024 11:35:32 -0400 Subject: [PATCH 14/16] BCDA-8215: Remove completed job count application logic (#964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽซ Ticket https://jira.cms.gov/browse/BCDA-8215 ## ๐Ÿ›  Changes - Remove all application logic referencing completed job count ## โ„น๏ธ Context This column, by it's own commented definition, is not a reliable indicator of the number of actually completed sub-jobs for an export job. It appears that its only use is in the in-progress job status header to indicate roughly how far along the job processing is. Instead of attempting to maintain this field, we should be relying on the source of truth (the number of completed job keys). To avoid confusion and to reduce code complexity, we are removing this column. Before removing the column from the database, we must stop reading it from the application. ## ๐Ÿงช Validation - Unit testing --- bcda/api/requests.go | 8 ++++- bcda/api/requests_test.go | 34 +++++++++---------- bcda/models/models.go | 26 ++++++++------ bcda/models/models_test.go | 12 +++---- bcda/models/postgres/repository.go | 11 +++--- bcda/models/postgres/repository_test.go | 8 ++--- bcda/utils/common.go | 16 +++++++++ bcda/utils/common_test.go | 10 +++++- bcdaworker/queueing/manager/alr.go | 5 +-- bcdaworker/queueing/manager/que_test.go | 3 +- bcdaworker/repository/mock_repository.go | 18 ---------- bcdaworker/repository/postgres/repository.go | 25 ++------------ .../repository/postgres/repository_test.go | 14 ++------ bcdaworker/repository/repository.go | 2 -- bcdaworker/worker/worker.go | 7 ---- bcdaworker/worker/worker_test.go | 1 - 16 files changed, 83 insertions(+), 117 deletions(-) diff --git a/bcda/api/requests.go b/bcda/api/requests.go index 425723890..0c09f786f 100644 --- a/bcda/api/requests.go +++ b/bcda/api/requests.go @@ -258,7 +258,13 @@ func (h *Handler) JobStatus(w http.ResponseWriter, r *http.Request) { logger.Error(job.Status) h.RespWriter.Exception(r.Context(), w, http.StatusInternalServerError, responseutils.JobFailed, responseutils.DetailJobFailed) case models.JobStatusPending, models.JobStatusInProgress: - w.Header().Set("X-Progress", job.StatusMessage()) + completedJobKeyCount := utils.CountUniq(jobKeys, func(jobKey *models.JobKey) int64 { + if jobKey.QueJobID == nil { + return -1 + } + return *jobKey.QueJobID + }) + w.Header().Set("X-Progress", job.StatusMessage(completedJobKeyCount)) w.WriteHeader(http.StatusAccepted) return case models.JobStatusCompleted: diff --git a/bcda/api/requests_test.go b/bcda/api/requests_test.go index a73057800..886a934c5 100644 --- a/bcda/api/requests_test.go +++ b/bcda/api/requests_test.go @@ -757,15 +757,14 @@ func (s *RequestsTestSuite) TestJobStatusErrorHandling() { mockSrv.On("GetJobAndKeys", testUtils.CtxMatcher, uint(1)).Return( &models.Job{ - ID: 1, - ACOID: uuid.NewRandom(), - RequestURL: requestUrl, - Status: tt.status, - TransactionTime: timestp, - JobCount: 100, - CompletedJobCount: 100, - CreatedAt: timestp, - UpdatedAt: timestp.Add(time.Duration(tt.timestampOffset)), + ID: 1, + ACOID: uuid.NewRandom(), + RequestURL: requestUrl, + Status: tt.status, + TransactionTime: timestp, + JobCount: 100, + CreatedAt: timestp, + UpdatedAt: timestp.Add(time.Duration(tt.timestampOffset)), }, []*models.JobKey{{ ID: 1, @@ -890,15 +889,14 @@ func (s *RequestsTestSuite) TestJobFailedStatus() { timestp := time.Now() mockSrv.On("GetJobAndKeys", testUtils.CtxMatcher, uint(1)).Return( &models.Job{ - ID: 1, - ACOID: uuid.NewRandom(), - RequestURL: tt.requestUrl, - Status: tt.status, - TransactionTime: timestp, - JobCount: 100, - CompletedJobCount: 100, - CreatedAt: timestp, - UpdatedAt: timestp, + ID: 1, + ACOID: uuid.NewRandom(), + RequestURL: tt.requestUrl, + Status: tt.status, + TransactionTime: timestp, + JobCount: 100, + CreatedAt: timestp, + UpdatedAt: timestp, }, []*models.JobKey{{ ID: 1, diff --git a/bcda/models/models.go b/bcda/models/models.go index 9d7899409..5bc079dc6 100644 --- a/bcda/models/models.go +++ b/bcda/models/models.go @@ -2,6 +2,7 @@ package models import ( "fmt" + "strings" "time" "github.com/pborman/uuid" @@ -24,20 +25,19 @@ var AllJobStatuses []JobStatus = []JobStatus{JobStatusPending, JobStatusInProgre type JobStatus string type Job struct { - ID uint - ACOID uuid.UUID `json:"aco_id"` - RequestURL string `json:"request_url"` // request_url - Status JobStatus `json:"status"` // status - TransactionTime time.Time // most recent data load transaction time from BFD - JobCount int - CompletedJobCount int - CreatedAt time.Time - UpdatedAt time.Time + ID uint + ACOID uuid.UUID `json:"aco_id"` + RequestURL string `json:"request_url"` // request_url + Status JobStatus `json:"status"` // status + TransactionTime time.Time // most recent data load transaction time from BFD + JobCount int + CreatedAt time.Time + UpdatedAt time.Time } -func (j *Job) StatusMessage() string { +func (j *Job) StatusMessage(numCompletedJobKeys int) string { if j.Status == JobStatusInProgress && j.JobCount > 0 { - pct := float64(j.CompletedJobCount) / float64(j.JobCount) * 100 + pct := float64(numCompletedJobKeys) / float64(j.JobCount) * 100 return fmt.Sprintf("%s (%d%%)", j.Status, int(pct)) } @@ -57,6 +57,10 @@ type JobKey struct { ResourceType string } +func (j *JobKey) IsError() bool { + return strings.Contains(j.FileName, "-error.ndjson") +} + // ACO represents an Accountable Care Organization. type ACO struct { ID uint diff --git a/bcda/models/models_test.go b/bcda/models/models_test.go index 4c6a251bf..234abd72e 100644 --- a/bcda/models/models_test.go +++ b/bcda/models/models_test.go @@ -20,14 +20,14 @@ func TestModelsTestSuite(t *testing.T) { } func (s *ModelsTestSuite) TestJobStatusMessage() { - j := Job{Status: constants.InProgress, JobCount: 25, CompletedJobCount: 6} - assert.Equal(s.T(), "In Progress (24%)", j.StatusMessage()) + j := Job{Status: constants.InProgress, JobCount: 25} + assert.Equal(s.T(), "In Progress (24%)", j.StatusMessage(6)) - j = Job{Status: constants.InProgress, JobCount: 0, CompletedJobCount: 0} - assert.Equal(s.T(), constants.InProgress, j.StatusMessage()) + j = Job{Status: constants.InProgress, JobCount: 0} + assert.Equal(s.T(), constants.InProgress, j.StatusMessage(0)) - j = Job{Status: JobStatusCompleted, JobCount: 25, CompletedJobCount: 25} - assert.Equal(s.T(), string(JobStatusCompleted), j.StatusMessage()) + j = Job{Status: JobStatusCompleted, JobCount: 25} + assert.Equal(s.T(), string(JobStatusCompleted), j.StatusMessage(25)) } func (s *ModelsTestSuite) TestACOBlacklist() { diff --git a/bcda/models/postgres/repository.go b/bcda/models/postgres/repository.go index 11d760a97..f47c36170 100644 --- a/bcda/models/postgres/repository.go +++ b/bcda/models/postgres/repository.go @@ -367,7 +367,7 @@ func (r *Repository) UpdateSuppressionFileImportStatus(ctx context.Context, file return nil } -var jobColumns []string = []string{"id", "aco_id", "request_url", "status", "transaction_time", "job_count", "completed_job_count", "created_at", "updated_at"} +var jobColumns []string = []string{"id", "aco_id", "request_url", "status", "transaction_time", "job_count", "created_at", "updated_at"} func (r *Repository) GetJobs(ctx context.Context, acoID uuid.UUID, statuses ...models.JobStatus) ([]*models.Job, error) { s := make([]interface{}, len(statuses)) @@ -425,7 +425,7 @@ func (r *Repository) GetJobByID(ctx context.Context, jobID uint) (*models.Job, e ) err := r.QueryRowContext(ctx, query, args...).Scan(&j.ID, &j.ACOID, &j.RequestURL, &j.Status, &transactionTime, - &j.JobCount, &j.CompletedJobCount, &createdAt, &updatedAt) + &j.JobCount, &createdAt, &updatedAt) j.TransactionTime, j.CreatedAt, j.UpdatedAt = transactionTime.Time, createdAt.Time, updatedAt.Time if err != nil { @@ -439,10 +439,10 @@ func (r *Repository) CreateJob(ctx context.Context, j models.Job) (uint, error) // User raw builder since we need to retrieve the associated ID ib := sqlFlavor.NewInsertBuilder().InsertInto("jobs") ib.Cols("aco_id", "request_url", "status", - "transaction_time", "job_count", "completed_job_count", + "transaction_time", "job_count", "created_at", "updated_at"). Values(j.ACOID, j.RequestURL, j.Status, - j.TransactionTime, j.JobCount, j.CompletedJobCount, + j.TransactionTime, j.JobCount, sqlbuilder.Raw("NOW()"), sqlbuilder.Raw("NOW()")) query, args := ib.Build() @@ -465,7 +465,6 @@ func (r *Repository) UpdateJob(ctx context.Context, j models.Job) error { ub.Assign("status", j.Status), ub.Assign("transaction_time", j.TransactionTime), ub.Assign("job_count", j.JobCount), - ub.Assign("completed_job_count", j.CompletedJobCount), ub.Assign("updated_at", sqlbuilder.Raw("NOW()")), ) ub.Where(ub.Equal("id", j.ID)) @@ -545,7 +544,7 @@ func (r *Repository) getJobs(ctx context.Context, query string, args ...interfac for rows.Next() { var j models.Job if err = rows.Scan(&j.ID, &j.ACOID, &j.RequestURL, &j.Status, &transactionTime, - &j.JobCount, &j.CompletedJobCount, &createdAt, &updatedAt); err != nil { + &j.JobCount, &createdAt, &updatedAt); err != nil { return nil, err } j.TransactionTime, j.CreatedAt, j.UpdatedAt = transactionTime.Time, createdAt.Time, updatedAt.Time diff --git a/bcda/models/postgres/repository_test.go b/bcda/models/postgres/repository_test.go index 4cf3ce4a2..fe951f6b0 100644 --- a/bcda/models/postgres/repository_test.go +++ b/bcda/models/postgres/repository_test.go @@ -683,9 +683,9 @@ func (r *RepositoryTestSuite) TestJobsMethods() { defer postgrestest.DeleteACO(r.T(), r.db, aco.UUID) - failed := models.Job{ACOID: aco.UUID, RequestURL: reqURL, Status: models.JobStatusFailed, JobCount: 10, CompletedJobCount: 20} - pending := models.Job{ACOID: aco.UUID, RequestURL: reqURL, Status: models.JobStatusPending, JobCount: 30, CompletedJobCount: 40} - completed := models.Job{ACOID: aco.UUID, RequestURL: reqURL, Status: models.JobStatusCompleted, JobCount: 40, CompletedJobCount: 60} + failed := models.Job{ACOID: aco.UUID, RequestURL: reqURL, Status: models.JobStatusFailed, JobCount: 10} + pending := models.Job{ACOID: aco.UUID, RequestURL: reqURL, Status: models.JobStatusPending, JobCount: 30} + completed := models.Job{ACOID: aco.UUID, RequestURL: reqURL, Status: models.JobStatusCompleted, JobCount: 40} failed.ID, err = r.repository.CreateJob(ctx, failed) assert.NoError(err) @@ -755,7 +755,6 @@ func (r *RepositoryTestSuite) TestJobsMethods() { // Account for time precision in postgres failed.TransactionTime = time.Now().Round(time.Millisecond) failed.JobCount = failed.JobCount + 1 - failed.CompletedJobCount = failed.CompletedJobCount + 1 failed.Status = models.JobStatusArchived assert.NoError(r.repository.UpdateJob(ctx, failed)) @@ -765,7 +764,6 @@ func (r *RepositoryTestSuite) TestJobsMethods() { assert.Equal(failed.TransactionTime.UTC(), newFailed.TransactionTime.UTC()) assert.Equal(failed.Status, newFailed.Status) assert.Equal(failed.JobCount, newFailed.JobCount) - assert.Equal(failed.CompletedJobCount, newFailed.CompletedJobCount) // Verify that we did not modify other job newCompleted, err := r.repository.GetJobByID(ctx, completed.ID) diff --git a/bcda/utils/common.go b/bcda/utils/common.go index 7e42719ef..b3058b43e 100644 --- a/bcda/utils/common.go +++ b/bcda/utils/common.go @@ -115,3 +115,19 @@ func Dedup(slice []string) []string { return newSlice } + +// Count the number of unique values in the slice based on given function +func CountUniq[S []E, E any, F comparable](arr S, f func(E) F) int { + var n int + var dupcheck = make(map[F]bool, n) + + for _, val := range arr { + comparableVal := f(val) + if !dupcheck[comparableVal] { + dupcheck[comparableVal] = true + n++ + } + } + + return n +} diff --git a/bcda/utils/common_test.go b/bcda/utils/common_test.go index 477d334b5..ea5d13cd7 100644 --- a/bcda/utils/common_test.go +++ b/bcda/utils/common_test.go @@ -1,9 +1,10 @@ package utils import ( + "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "testing" ) type CommonTestSuite struct { @@ -26,6 +27,13 @@ func (s *CommonTestSuite) TestDedup() { assert.Len(s.T(), result, 3) } +func (s *CommonTestSuite) TestCountUniq() { + firstLetter := func(s string) string { return string(s[0]) } + assert.Equal(s.T(), 0, CountUniq([]string{}, firstLetter)) + assert.Equal(s.T(), 1, CountUniq([]string{"abc", "ab"}, firstLetter)) + assert.Equal(s.T(), 2, CountUniq([]string{"abc", "bcd", "ab"}, firstLetter)) +} + func TestCommonTestSuite(t *testing.T) { suite.Run(t, new(CommonTestSuite)) } diff --git a/bcdaworker/queueing/manager/alr.go b/bcdaworker/queueing/manager/alr.go index 40107555f..8518d3eef 100644 --- a/bcdaworker/queueing/manager/alr.go +++ b/bcdaworker/queueing/manager/alr.go @@ -166,10 +166,6 @@ func (q *masterQueue) startAlrJob(queJob *que.Job) error { return err } - if err := q.repository.IncrementCompletedJobCount(ctx, jobArgs.ID); err != nil { - q.alrLog.Warnf("Failed to increment completed count %s", err.Error()) - return err - } jobComplete, err := q.isJobComplete(ctx, jobArgs.ID) if err != nil { q.alrLog.Warnf("Failed to check job completion %s", err) @@ -223,5 +219,6 @@ func (q *masterQueue) isJobComplete(ctx context.Context, jobID uint) (bool, erro Println("Excess number of jobs completed.") return true, nil } + return false, nil } diff --git a/bcdaworker/queueing/manager/que_test.go b/bcdaworker/queueing/manager/que_test.go index da2cc8c5b..335507d65 100644 --- a/bcdaworker/queueing/manager/que_test.go +++ b/bcdaworker/queueing/manager/que_test.go @@ -213,8 +213,7 @@ func TestStartAlrJob(t *testing.T) { Status: models.JobStatusPending, TransactionTime: time.Now(), // JobCount is partitioned automatically, but it is done manually here - JobCount: 2, - CompletedJobCount: 0, + JobCount: 2, } id, err := r.CreateJob(ctx, job) assert.NoError(t, err) diff --git a/bcdaworker/repository/mock_repository.go b/bcdaworker/repository/mock_repository.go index c19d18cd6..5cf0bfa95 100644 --- a/bcdaworker/repository/mock_repository.go +++ b/bcdaworker/repository/mock_repository.go @@ -228,24 +228,6 @@ func (_m *MockRepository) GetUniqueJobKeyCount(ctx context.Context, jobID uint) return r0, r1 } -// IncrementCompletedJobCount provides a mock function with given fields: ctx, jobID -func (_m *MockRepository) IncrementCompletedJobCount(ctx context.Context, jobID uint) error { - ret := _m.Called(ctx, jobID) - - if len(ret) == 0 { - panic("no return value specified for IncrementCompletedJobCount") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, uint) error); ok { - r0 = rf(ctx, jobID) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // UpdateJobStatus provides a mock function with given fields: ctx, jobID, new func (_m *MockRepository) UpdateJobStatus(ctx context.Context, jobID uint, new models.JobStatus) error { ret := _m.Called(ctx, jobID, new) diff --git a/bcdaworker/repository/postgres/repository.go b/bcdaworker/repository/postgres/repository.go index 5b791a337..0924be9fa 100644 --- a/bcdaworker/repository/postgres/repository.go +++ b/bcdaworker/repository/postgres/repository.go @@ -85,7 +85,7 @@ func (r *Repository) GetCCLFBeneficiaryByID(ctx context.Context, id uint) (*mode func (r *Repository) GetJobByID(ctx context.Context, jobID uint) (*models.Job, error) { sb := sqlFlavor.NewSelectBuilder() - sb.Select("id", "aco_id", "request_url", "status", "transaction_time", "job_count", "completed_job_count", "created_at", "updated_at") + sb.Select("id", "aco_id", "request_url", "status", "transaction_time", "job_count", "created_at", "updated_at") sb.From("jobs").Where(sb.Equal("id", jobID)) query, args := sb.Build() @@ -96,7 +96,7 @@ func (r *Repository) GetJobByID(ctx context.Context, jobID uint) (*models.Job, e ) err := r.QueryRowContext(ctx, query, args...).Scan(&j.ID, &j.ACOID, &j.RequestURL, &j.Status, &transactionTime, - &j.JobCount, &j.CompletedJobCount, &createdAt, &updatedAt) + &j.JobCount, &createdAt, &updatedAt) j.TransactionTime, j.CreatedAt, j.UpdatedAt = transactionTime.Time, createdAt.Time, updatedAt.Time if err != nil { @@ -121,27 +121,6 @@ func (r *Repository) UpdateJobStatusCheckStatus(ctx context.Context, jobID uint, map[string]interface{}{"status": new}) } -func (r *Repository) IncrementCompletedJobCount(ctx context.Context, jobID uint) error { - ub := sqlFlavor.NewUpdateBuilder().Update("jobs") - ub.Set(ub.Incr("completed_job_count"), ub.Assign("updated_at", sqlbuilder.Raw("NOW()"))). - Where(ub.Equal("id", jobID)) - - query, args := ub.Build() - res, err := r.ExecContext(ctx, query, args...) - if err != nil { - return err - } - count, err := res.RowsAffected() - if err != nil { - return err - } - if count == 0 { - return fmt.Errorf("job %d not updated, no job found", jobID) - } - - return nil -} - func (r *Repository) CreateJobKey(ctx context.Context, jobKey models.JobKey) error { ib := sqlFlavor.NewInsertBuilder().InsertInto("job_keys") ib.Cols("job_id", "que_job_id", "file_name", "resource_type"). diff --git a/bcdaworker/repository/postgres/repository_test.go b/bcdaworker/repository/postgres/repository_test.go index 1f8d86ac5..6e25d7aeb 100644 --- a/bcdaworker/repository/postgres/repository_test.go +++ b/bcdaworker/repository/postgres/repository_test.go @@ -90,8 +90,8 @@ func (r *RepositoryTestSuite) TestJobsMethods() { postgrestest.CreateACO(r.T(), r.db, aco) defer postgrestest.DeleteACO(r.T(), r.db, aco.UUID) - failed := models.Job{ACOID: aco.UUID, Status: models.JobStatusFailed, CompletedJobCount: 1, TransactionTime: now} - completed := models.Job{ACOID: aco.UUID, Status: models.JobStatusCompleted, CompletedJobCount: 2, TransactionTime: now} + failed := models.Job{ACOID: aco.UUID, Status: models.JobStatusFailed, TransactionTime: now} + completed := models.Job{ACOID: aco.UUID, Status: models.JobStatusCompleted, TransactionTime: now} postgrestest.CreateJobs(r.T(), r.db, &failed, &completed) failed1, err := r.repository.GetJobByID(ctx, failed.ID) @@ -117,12 +117,6 @@ func (r *RepositoryTestSuite) TestJobsMethods() { failed.UpdatedAt = afterUpdate.UpdatedAt assertJobsEqual(assert, failed, *afterUpdate) - assert.NoError(r.repository.IncrementCompletedJobCount(ctx, failed.ID)) - afterUpdate, err = r.repository.GetJobByID(ctx, failed.ID) - assert.NoError(err) - assert.True(afterUpdate.UpdatedAt.After(failed.UpdatedAt)) - assert.Equal(afterUpdate.CompletedJobCount, failed.CompletedJobCount+1) - // After all of these updates, the completed job should remain untouched completed1, err := r.repository.GetJobByID(ctx, completed.ID) assert.NoError(err) @@ -141,10 +135,6 @@ func (r *RepositoryTestSuite) TestJobsMethods() { err = r.repository.UpdateJobStatusCheckStatus(ctx, 0, models.JobStatusFailed, models.JobStatusArchived) assert.EqualError(err, "job was not updated, no match found") - - err = r.repository.IncrementCompletedJobCount(ctx, 0) - assert.EqualError(err, "job 0 not updated, no job found") - } // TestJobKeysMethods validates the CRUD operations associated with the job_keys table diff --git a/bcdaworker/repository/repository.go b/bcdaworker/repository/repository.go index e43c4b57e..a5d859718 100644 --- a/bcdaworker/repository/repository.go +++ b/bcdaworker/repository/repository.go @@ -31,8 +31,6 @@ type jobRepository interface { // UpdateJobStatusCheckStatus updates the particular job indicated by the jobID // iff the Job's status field matches current. UpdateJobStatusCheckStatus(ctx context.Context, jobID uint, current, new models.JobStatus) error - - IncrementCompletedJobCount(ctx context.Context, jobID uint) error } type jobKeyRepository interface { diff --git a/bcdaworker/worker/worker.go b/bcdaworker/worker/worker.go index beccacdf9..a41f918a7 100644 --- a/bcdaworker/worker/worker.go +++ b/bcdaworker/worker/worker.go @@ -169,13 +169,6 @@ func (w *worker) ProcessJob(ctx context.Context, queJobID int64, job models.Job, return err } - // Not critical since we use the job_keys count as the authoritative list of completed jobs. - // CompletedJobCount is purely information and can be off. - if err := w.r.IncrementCompletedJobCount(ctx, job.ID); err != nil { - err = errors.Wrap(err, "ProcessJob: Failed to update completed job count. Will continue.") - logger.Warn(err) - } - return nil } diff --git a/bcdaworker/worker/worker_test.go b/bcdaworker/worker/worker_test.go index fde61e2ad..8bf8ec6c0 100644 --- a/bcdaworker/worker/worker_test.go +++ b/bcdaworker/worker/worker_test.go @@ -516,7 +516,6 @@ func (s *WorkerTestSuite) TestProcessJobEOB() { assert.Nil(s.T(), err) // As this test actually connects to BB, we can't be sure it will succeed assert.Contains(s.T(), []models.JobStatus{models.JobStatusFailed, models.JobStatusCompleted}, completedJob.Status) - assert.Equal(s.T(), 1, completedJob.CompletedJobCount) } func (s *WorkerTestSuite) TestProcessJobUpdateJobCheckStatus() { From 341dacd173852da45eb3c48bdd93996690f3d97a Mon Sep 17 00:00:00 2001 From: Kevin Yeh Date: Tue, 9 Jul 2024 11:07:17 -0400 Subject: [PATCH 15/16] Fix Github Actions on self-hosted (#971) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽซ Ticket N/A ## ๐Ÿ›  Changes - Add env var to fix self-hosted Github Actions steps. ## โ„น๏ธ Context See https://github.com/actions/checkout/issues/1809 for technical context. ## ๐Ÿงช Validation Sonarqube quality check passes on this PR. --- .github/workflows/ci-workflow.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 58b9a5e65..b39f25b3e 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -65,6 +65,9 @@ jobs: name: Sonarqube Quality Gate needs: build runs-on: self-hosted + env: + # Workaround until https://jira.cms.gov/browse/PLT-338 is implemented. + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: "true" steps: - name: Download code coverage uses: actions/download-artifact@v2 From 9f8730a72531277db310ba43acff8e7294811d2d Mon Sep 17 00:00:00 2001 From: Kevin Yeh Date: Wed, 10 Jul 2024 13:33:19 -0400 Subject: [PATCH 16/16] BCDA-8215: Remove completed_job_count column (#969) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽซ Ticket https://jira.cms.gov/browse/BCDA-8215 ## ๐Ÿ›  Changes - Remove completed_job_count column ## โ„น๏ธ Context Removes column from DB after #964 is merged (see related PR for details.) Since migrations are run manually, this can be placed in the same release and run _after_ the application deployment. ## ๐Ÿงช Validation Unit tests --------- Co-authored-by: alex-dzeda <120701369+alex-dzeda@users.noreply.github.com> --- .../000017_remove_completed_job_count.down.sql | 5 +++++ .../000017_remove_completed_job_count.up.sql | 5 +++++ db/migrations/migrations_test.go | 16 ++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 db/migrations/bcda/000017_remove_completed_job_count.down.sql create mode 100644 db/migrations/bcda/000017_remove_completed_job_count.up.sql diff --git a/db/migrations/bcda/000017_remove_completed_job_count.down.sql b/db/migrations/bcda/000017_remove_completed_job_count.down.sql new file mode 100644 index 000000000..356ba6406 --- /dev/null +++ b/db/migrations/bcda/000017_remove_completed_job_count.down.sql @@ -0,0 +1,5 @@ +BEGIN; +-- Add the completed_job_count column +ALTER TABLE public.jobs ADD COLUMN completed_job_count integer DEFAULT 0; +ALTER TABLE public.jobs ALTER COLUMN completed_job_count DROP DEFAULT; +COMMIT; \ No newline at end of file diff --git a/db/migrations/bcda/000017_remove_completed_job_count.up.sql b/db/migrations/bcda/000017_remove_completed_job_count.up.sql new file mode 100644 index 000000000..b0ef2d9bf --- /dev/null +++ b/db/migrations/bcda/000017_remove_completed_job_count.up.sql @@ -0,0 +1,5 @@ +BEGIN; +-- Remove completed_job_count column from jobs table +ALTER TABLE public.jobs DROP COLUMN IF EXISTS completed_job_count; + +COMMIT; \ No newline at end of file diff --git a/db/migrations/migrations_test.go b/db/migrations/migrations_test.go index e27fe8196..b02ccfc1a 100644 --- a/db/migrations/migrations_test.go +++ b/db/migrations/migrations_test.go @@ -347,9 +347,25 @@ func (s *MigrationTestSuite) TestBCDAMigration() { assertIndexExists(t, true, db, "job_keys", "idx_job_keys_job_id_que_job_id") }, }, + { + "Removing completed_job_count from jobs table", + func(t *testing.T) { + assertColumnExists(t, true, db, "jobs", "completed_job_count") + migrator.runMigration(t, 17) + assertColumnExists(t, false, db, "jobs", "completed_job_count") + }, + }, // ********************************************************** // * down migrations tests begin here with test number - 1 * // ********************************************************** + { + "Removing completed_job_count from jobs table", + func(t *testing.T) { + assertColumnExists(t, false, db, "jobs", "completed_job_count") + migrator.runMigration(t, 16) + assertColumnExists(t, true, db, "jobs", "completed_job_count") + }, + }, { "Removing que_job_id from job_keys table", func(t *testing.T) {