diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 4743b89d0..07ede2b2c 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -6,6 +6,10 @@ on: - dev - main +permissions: + contents: read + packages: write + jobs: php-lint: name: PHP Lint diff --git a/.github/workflows/test-cypress-prod-links.yml b/.github/workflows/test-cypress-prod-links.yml index 4d1e036dd..9691138f5 100644 --- a/.github/workflows/test-cypress-prod-links.yml +++ b/.github/workflows/test-cypress-prod-links.yml @@ -30,7 +30,7 @@ jobs: with: browser: chrome env: NODE_ENV=test - config-file: cypress.prod.config.js + config-file: cypress.prod.links.config.js working-directory: benefit-finder - name: create github issue diff --git a/.github/workflows/thog_scan.yml b/.github/workflows/thog_scan.yml deleted file mode 100644 index 89c0b6f0c..000000000 --- a/.github/workflows/thog_scan.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: TruffleHog Scan - -on: - # schedule: - # - cron: '0 8 * * *' # Runs daily at 8 AM UTC - workflow_dispatch: - -jobs: - scan: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: scan - uses: trufflesecurity/trufflehog@main - with: - base: "" - head: ${{ github.ref_name }} - extra_args: --only-verified - - - name: Upload TruffleHog scan results - uses: actions/upload-artifact@v3 - with: - name: trufflehog-results - path: truffleHogResults.json diff --git a/.github/workflows/thog_scan_commit.yml b/.github/workflows/thog_scan_commit.yml new file mode 100644 index 000000000..00074f653 --- /dev/null +++ b/.github/workflows/thog_scan_commit.yml @@ -0,0 +1,83 @@ +name: TruffleHog Scan + +on: + push: + branches: + - main + - develop + pull_request: + +jobs: + scan: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install GitHub CLI + run: | + sudo apt-get update + sudo apt-get install -y gh + + - name: Authenticate GitHub CLI + env: + GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }} + run: | + gh auth setup-git + + - name: Run TruffleHog scan + id: trufflehog_scan + uses: trufflesecurity/trufflehog@v3.79.0 + with: + base: "" + head: ${{ github.ref_name }} + extra_args: --only-verified --json --entropy --max-depth=50 + continue-on-error: true + + - name: Check TruffleHog Results + id: check_results + run: | + if [ -f truffleHogResults.json ]; then + echo "file_exists=true" >> $GITHUB_ENV + else + echo "file_exists=false" >> $GITHUB_ENV + fi + + - name: Upload TruffleHog scan results + if: always() && env.file_exists == 'true' + uses: actions/upload-artifact@v3 + with: + name: trufflehog-results + path: truffleHogResults.json + + - name: Convert JSON to Readable Report + if: always() && env.file_exists == 'true' + run: | + jq -r '.results[] | "File: \(.path)\nCommit: \(.commit)\nDate: \(.date)\nReason: \(.reason)\n---------------------------"' truffleHogResults.json > truffleHogReport.txt + + - name: Upload Readable Report + if: always() && env.file_exists == 'true' + uses: actions/upload-artifact@v3 + with: + name: trufflehog-readable-report + path: truffleHogReport.txt + + - name: Check for findings and create issue + if: failure() && env.file_exists == 'true' + env: + GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }} + run: | + if jq -e '.results | length > 0' truffleHogResults.json > /dev/null; then + echo "Secrets found. Creating GitHub issue." + gh issue create --title "TruffleHog Scan Results" --body "$(cat truffleHogReport.txt)" --label "bug,security" --assignee "@me" + exit 1 + else + echo "No secrets found or no results file." + fi + + - name: Fail the job if any secrets are found + if: steps.trufflehog_scan.outcome == 'failure' + run: exit 1 diff --git a/.github/workflows/thog_scan_scheduled.yml b/.github/workflows/thog_scan_scheduled.yml new file mode 100644 index 000000000..e69de29bb diff --git a/benefit-finder/cypress.prod.config.js b/benefit-finder/cypress.prod.config.js deleted file mode 100644 index aabff4890..000000000 --- a/benefit-finder/cypress.prod.config.js +++ /dev/null @@ -1,12 +0,0 @@ -const { defineConfig } = require('cypress') - -module.exports = defineConfig({ - retries: { - runMode: 2, - openMode: 0, - }, - e2e: { - baseUrl: 'https://www.usa.gov', - specPattern: 'cypress/e2e/usagov-public-site/*.cy.js' - }, -}) diff --git a/benefit-finder/cypress/e2e/storybook/dataLayer.cy.js b/benefit-finder/cypress/e2e/storybook/dataLayer.cy.js index 19091367c..31dc6e51a 100644 --- a/benefit-finder/cypress/e2e/storybook/dataLayer.cy.js +++ b/benefit-finder/cypress/e2e/storybook/dataLayer.cy.js @@ -5,7 +5,7 @@ import { pageObjects } from '../../support/pageObjects' import * as EN_LOCALE_DATA from '../../../../benefit-finder/src/shared/locales/en/en.json' import * as BENEFITS_ELIBILITY_DATA from '../../fixtures/benefits-eligibility.json' -const { intro, lifeEventSection, resultsView, benefitCount } = +const { intro, lifeEventSection, resultsView, openAllBenefitAccordions } = dataLayerUtils.dataLayerStructure const dataLayerValues = [ @@ -26,17 +26,17 @@ const dataLayerValues = [ { event: resultsView.event, bfData: { - pageView: resultsView.bfData.pageView, + pageView: resultsView.bfData.pageView[0], viewTitle: 'Your potential benefits', - viewState: resultsView.bfData.viewState[1], + eligibleBenefitCount: { number: 4, string: '4' }, + moreInfoBenefitCount: { number: 1, string: '1' }, + notEligibleBenefitCount: { number: 25, string: '25' }, }, }, { - event: benefitCount.event, + event: openAllBenefitAccordions.event, bfData: { - eligible: 4, - moreInfo: 1, - notEligible: 25, + accordionsOpen: openAllBenefitAccordions.bfData.accordionsOpen, }, }, ] @@ -120,19 +120,49 @@ describe('Calls to Google Analytics Object', function () { } delete ev[0]['gtm.uniqueEventId'] - expect(dataLayerValues[2]).to.deep.equal(ev[0]) + expect(ev[0]).to.deep.equal(dataLayerValues[2]) + }) + }) + }) + }) - // // check count event - const evCount = { - ...window.dataLayer.filter( - x => x.event === dataLayerValues[3].event - ), - } + it('clicking open all on results page has a bf_open_all_accordions event', function () { + const selectedData = BENEFITS_ELIBILITY_DATA.scenario_1_covid.en.param + const scenario = utils.encodeURIFromObject(selectedData) + cy.visit(`${utils.storybookUri}${scenario}`) - delete evCount[0]['gtm.uniqueEventId'] + cy.window().then(window => { + assert.isDefined(window.dataLayer, 'window.dataLayer is defined') - expect(dataLayerValues[3]).to.deep.equal(evCount[0]) - }) + pageObjects + .expandAll() + .click() + .then(() => { + // check last page change event + const ev = { + ...window.dataLayer.filter( + x => x?.event === dataLayerValues[3].event + ), + } + delete ev[0]['gtm.uniqueEventId'] + + expect(dataLayerValues[3]).to.deep.equal(ev[0]) + }) + + pageObjects + .expandAll() + .click() + .then(() => { + // check last page change event + const ev = { + ...window.dataLayer.filter( + x => x?.event === dataLayerValues[3].event + ), + } + // we ignore dedup here so there can be multiple fires + delete ev[1]['gtm.uniqueEventId'] + + expect(dataLayerValues[4]).to.not.deep.equal(ev[1]) }) }) }) diff --git a/benefit-finder/cypress/e2e/usagov-public-site/links.cy.js b/benefit-finder/cypress/e2e/usagov-public-site/links.cy.js index 496485c77..9fc879911 100644 --- a/benefit-finder/cypress/e2e/usagov-public-site/links.cy.js +++ b/benefit-finder/cypress/e2e/usagov-public-site/links.cy.js @@ -1,64 +1,108 @@ import * as utils from '../../support/utils' import * as BENEFITS_ELIBILITY_DATA from '../../fixtures/benefits-eligibility.json' -describe('Verify correct status code when user navigates links', () => { - // to be removed when uncaught exceptions are addressed - Cypress.on('uncaught:exception', (_err, runnable) => { - return false - }) - it('Verify success status code response for links in Death of a loved one English page', () => { - const selectedData = - BENEFITS_ELIBILITY_DATA['death-of-a-loved-one'].en.param - const scenario = utils.encodeURIFromObject(selectedData) - cy.visit(`benefit-finder/death?${scenario}`) - cy.get('main a[href]').each(link => { - cy.request(link.prop('href')) +const localePaths = { + en: [ + { key: 'death-of-a-loved-one', path: 'death' }, + { key: 'retirement', path: 'retirement' }, + { key: 'disability', path: 'disability' }, + ], + es: [ + { key: 'death-of-a-loved-one', path: 'muerte' }, + { key: 'retirement', path: 'jubilacion' }, + { key: 'disability', path: 'discapacidad' }, + ], +} + +const handlerequest = ({ testLink, link }) => { + const url = testLink || link.prop('href') + return cy + .request({ + url, + failOnStatusCode: false, + }) + .then(response => { + if (response.status === 200) { + expect(response.status).to.eq(200) + } else if (response.status === 403) { + cy.get('body').children().its('length').should('be.gt', 0) + } else if (response.status === 503) { + throw new Error(`site down - gave a 503 ${url}`) + } else if (response.status === 404) { + throw new Error(`page not found - gave a 404 ${url}`) + } else { + cy.get('body').children().its('length').should('be.gt', 0) + } }) +} + +const validateErrorCodes = test => { + // we verify site is alive and fail on 404 || 503 + cy.get('#benefit-finder a[href]').each(link => { + handlerequest({ link }) }) +} - it('Verify success status code response for links in Death of a Loved One Spanish page', () => { - const selectedData = - BENEFITS_ELIBILITY_DATA['death-of-a-loved-one'].es.param - const scenario = utils.encodeURIFromObject(selectedData) - cy.visit(`es/buscador-beneficios/muerte?${scenario}`) - cy.get('main a[href]').each(link => { - cy.request(link.prop('href')) - }) +const validateLinks = ({ selectedData, path }) => { + const scenario = utils.encodeURIFromObject(selectedData) + cy.visit(`${path}?${scenario}`) + validateErrorCodes() +} + +// to be removed when uncaught exceptions are addressed +// eslint-disable-next-line n/handle-callback-err +Cypress.on('uncaught:exception', (error, runnable) => { + return false +}) + +describe('Verify correct status code handling', () => { + // negate validation on our functional code + Cypress.on('fail', (error, runnable) => { + if (JSON.stringify(error).includes('httpstat')) { + // eslint-disable-next-line no-unused-expressions + expect(error).to.not.be.undefined + } else { + throw error + } }) - it('Verify success status code response for links in Retirement English page', () => { - const selectedData = BENEFITS_ELIBILITY_DATA.retirement.en.param - const scenario = utils.encodeURIFromObject(selectedData) - cy.visit(`benefit-finder/retirement?${scenario}`) - cy.get('main a[href]').each(link => { - cy.request(link.prop('href')) - }) + it(`handles 404 with an error`, () => { + handlerequest({ testLink: 'https://httpstat.us/404' }) }) - it('Verify success status code response for links in Retirement Spanish page', () => { - const selectedData = BENEFITS_ELIBILITY_DATA.retirement.es.param - const scenario = utils.encodeURIFromObject(selectedData) - cy.visit(`es/buscador-beneficios/jubilacion?${scenario}`) - cy.get('main a[href]').each(link => { - cy.request(link.prop('href')) - }) + it(`handles 503 with an error`, () => { + handlerequest({ testLink: 'https://httpstat.us/503' }) + }) + + it(`handles 200 successfully`, () => { + handlerequest({ testLink: 'https://httpstat.us/200' }) + }) + + it(`handles any 403 successfully`, () => { + handlerequest({ testLink: 'https://httpstat.us/403' }) + }) + + it(`handles any other request successfully`, () => { + handlerequest({ testLink: 'https://httpstat.us/201' }) }) +}) - it('Verify success status code response for links in Disability English page', () => { - const selectedData = BENEFITS_ELIBILITY_DATA.disability.en.param - const scenario = utils.encodeURIFromObject(selectedData) - cy.visit(`benefit-finder/disability?${scenario}`) - cy.get('main a[href]').each(link => { - cy.request(link.prop('href')) +describe('Verify correct status code when user navigates links in each locales', () => { + localePaths.en.forEach(location => { + it(`Verify success status code response for links in ${location.key} en page`, () => { + validateLinks({ + selectedData: BENEFITS_ELIBILITY_DATA[`${location.key}`].en.param, + path: `benefit-finder/${location.path}`, + }) }) }) - it('Verify success status code response for links in Disability English page', () => { - const selectedData = BENEFITS_ELIBILITY_DATA.disability.es.param - const scenario = utils.encodeURIFromObject(selectedData) - cy.visit(`es/buscador-beneficios/discapacidad?${scenario}`) - cy.get('main a[href]').each(link => { - cy.request(link.prop('href')) + localePaths.es.forEach(location => { + it(`Verify success status code response for links in ${location.key} es page`, () => { + validateLinks({ + selectedData: BENEFITS_ELIBILITY_DATA[`${location.key}`].en.param, + path: `es/buscador-beneficios/${location.path}`, + }) }) }) }) diff --git a/benefit-finder/package.json b/benefit-finder/package.json index 4c59003bb..6013308c5 100644 --- a/benefit-finder/package.json +++ b/benefit-finder/package.json @@ -32,7 +32,7 @@ "cy:run:component": "NODE_ENV=test npx cypress run --component --browser chrome", "cy:run:e2e": "NODE_ENV=test npx cypress run --browser chrome", "cy:run": "NODE_ENV=test npm run cy:run:component && npm run cy:run:e2e", - "cy:run:prod:e2e": "NODE_ENV=test npx cypress run --config-file cypress.prod.config.js --browser chrome", + "cy:run:prod:e2e": "NODE_ENV=test npx cypress run --config-file cypress.prod.links.config.js --browser chrome", "cy:run:prod:links:e2e": "NODE_ENV=test npx cypress run --config-file cypress.prod.links.config.js --browser chrome", "cy:run:pipeline": "NODE_ENV=test concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"http-server ./storybook-static --port 6006 --silent\" -Y", "cy:run:edge:e2e": "NODE_ENV=test npx cypress run --browser edge", diff --git a/benefit-finder/src/App/__tests__/__snapshots__/index.spec.jsx.snap b/benefit-finder/src/App/__tests__/__snapshots__/index.spec.jsx.snap index 578df2ebb..30b9f84ac 100644 --- a/benefit-finder/src/App/__tests__/__snapshots__/index.spec.jsx.snap +++ b/benefit-finder/src/App/__tests__/__snapshots__/index.spec.jsx.snap @@ -348,7 +348,7 @@ exports[`loads window query scenario 1 1`] = ` >
If you reached these results by mistake, please go back to review your answers.
If you reached these results by mistake, please go back to review your answers.
Si cree que cometió un error, por favor regrese para corregir sus respuestas.