diff --git a/.babelrc b/.babelrc index 26ac1c7b41..68ba92054d 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,6 @@ { "presets": [ - ["./config/babel/node", { + ["topcoder-react-utils/config/babel/node-ssr", { "baseAssetsOutputPath": "/community-app-assets" }] ] diff --git a/.circleci/config.yml b/.circleci/config.yml index df5535e2e4..40e916d303 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -111,11 +111,11 @@ jobs: # Jest unit-tests). test: docker: - - image: cimg/node:24.18.0@sha256:4a638ad75f8601fec1f193e71df584639e6accb1771cea23f3d9d3857bca480c + - image: circleci/node:10.24.1 steps: - checkout - restore_cache: - key: test-node24-modules-{{ checksum "package-lock.json" }} + key: test-node-modules-{{ checksum "package-lock.json" }} - run: name: Config Git command: git config --global url."https://git@".insteadOf git:// @@ -124,14 +124,15 @@ jobs: command: npm ci no_output_timeout: 20m - save_cache: - key: test-node24-modules-{{ checksum "package-lock.json" }} + key: test-node-modules-{{ checksum "package-lock.json" }} paths: - node_modules - - run: npm run lint && npm run jest:ci + - run: npm test Performance-Testing: docker: - - image: cimg/openjdk:17.0.19@sha256:09490c4b3e6e85f8b382c7ce0ef70aa8e940ee3a3567a11d2aede0ed094dc525 + # specify the version you desire here + - image: circleci/openjdk:8-jdk # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images @@ -150,16 +151,16 @@ jobs: # Download and cache dependencies - restore_cache: keys: - - performance-jdk17-maven3-{{ checksum "pom.xml" }} + - v1-dependencies-{{ checksum "pom.xml" }} # fallback to using the latest cache if no exact match is found - - performance-jdk17-maven3- + - v1-dependencies- - run: mvn dependency:go-offline - save_cache: paths: - ~/.m2 - key: performance-jdk17-maven3-{{ checksum "pom.xml" }} + key: v1-dependencies-{{ checksum "pom.xml" }} - run: mvn verify diff --git a/.dockerignore b/.dockerignore index ac650ed4c7..1dba739ceb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,40 +1,3 @@ -# Version-control and local editor state -.git -.github -.circleci -.idea -.vscode -*.swp -*.swo - -# Local dependencies, generated output, and test artifacts -node_modules -automated-smoke-test -build -.build-info -target -coverage -__coverage__ -.nyc_output -*.log -npm-debug.log* - -# Local environment and deployment material must never enter the build context -.env -.env.* -!.env.example -*.key -*.pem -awsenvconf -buildvar_env -deployvar_env - -# Files that are not needed to install, test, or build the application -docs -README.md -CHANGELOG.md -CONTRIBUTING.md -LICENSE -CODEOWNERS -pom.xml -*.patch +__coverage__/ +.git/ +node_modules/ \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index 5408f9f519..42dd1b1464 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,3 @@ __coverage__ -automated-smoke-test/temp build -node_modules +node_modules \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index c9fde00e2a..c323322f56 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,69 +1,12 @@ { - "extends": "airbnb", - "parser": "@babel/eslint-parser", - "parserOptions": { - "requireConfigFile": false, - "sourceType": "module", - "babelOptions": { - "babelrc": false, - "configFile": false, - "presets": ["@babel/preset-react"], - "plugins": [ - "@babel/plugin-proposal-export-default-from", - "@babel/plugin-transform-class-properties" - ] - } - }, + "extends": "./node_modules/topcoder-react-utils/config/eslint/default.json", "rules": { - "arrow-parens": ["error", "as-needed", { "requireForBlockBody": true }], - "class-methods-use-this": "off", - "default-param-last": "off", - "function-call-argument-newline": "off", - "function-paren-newline": ["error", "consistent"], - "import/no-import-module-exports": "off", - "jsx-a11y/anchor-is-valid": "off", - "jsx-a11y/control-has-associated-label": "off", - "jsx-a11y/href-no-hash": "off", - "max-classes-per-file": "off", + "jsx-a11y/anchor-is-valid": false, "import/no-cycle": [2, { "maxDepth": 1 }], - "no-multiple-empty-lines": ["error", { "max": 2, "maxBOF": 2, "maxEOF": 0 }], - "no-promise-executor-return": "off", - "no-redeclare": ["error", { "builtinGlobals": false }], - "prefer-regex-literals": "off", - "react/forbid-prop-types": "off", - "react/function-component-definition": "off", - "react/jsx-curly-brace-presence": "off", - "react/jsx-curly-newline": "off", - "react/jsx-fragments": "off", - "react/jsx-no-useless-fragment": "off", - "react/jsx-one-expression-per-line": "off", - "react/jsx-props-no-spreading": "off", - "react/no-deprecated": "off", - "react/no-invalid-html-attribute": "off", - "react/no-unstable-nested-components": "off", - "react/no-unused-class-component-methods": "off", - "react/no-unknown-property": ["error", { "ignore": ["styleName"] }], - "react/sort-comp": "off" + "react/forbid-prop-types": false, + "react/no-unknown-property": ["error", { "ignore": ["styleName"] }] }, "env": { - "browser": true, - "es6": true, - "node": true - }, - "settings": { - "import/resolver": { - "node": { - "extensions": [".js", ".jsx"], - "moduleDirectory": ["node_modules", "src/shared", "src"] - } - } - }, - "overrides": [ - { - "files": ["config/**/*.js", "webpack.config.js"], - "rules": { - "import/no-extraneous-dependencies": ["error", { "devDependencies": true }] - } - } - ] + "browser": true + } } diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index f2eeb3db8d..795006101e 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -1,10 +1,6 @@ name: Commitlint on: [pull_request] -permissions: - contents: read - pull-requests: read - jobs: commit-lint: runs-on: ubuntu-latest @@ -16,4 +12,4 @@ jobs: fetch-depth: 0 - uses: wagoid/commitlint-github-action@v1.4.0 with: - configFile: './.commitlintrc.yml' + configFile: './.commitlintrc.yml' \ No newline at end of file diff --git a/.github/workflows/trivy.yaml b/.github/workflows/trivy.yaml index 97d227d3e2..9cbcf52099 100644 --- a/.github/workflows/trivy.yaml +++ b/.github/workflows/trivy.yaml @@ -24,9 +24,8 @@ jobs: ignore-unfixed: true format: "sarif" output: "trivy-results.sarif" - severity: "CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN" - limit-severities-for-sarif: true - scanners: vuln,secret,misconfig + severity: "CRITICAL,HIGH,UNKNOWN" + scanners: vuln,secret,misconfig,license github-pat: ${{ secrets.GITHUB_TOKEN }} - name: Upload Trivy scan results to GitHub Security tab diff --git a/.npmrc b/.npmrc deleted file mode 100644 index a48ecb008f..0000000000 --- a/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -legacy-peer-deps=true -strict-allow-scripts=true diff --git a/.nvmrc b/.nvmrc index 5bcf9c6e6a..c8b7cbff70 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v24.18.0 +v10.24.1 diff --git a/.stylelintrc b/.stylelintrc index ad21f237e9..d2541eb3f4 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -1,42 +1,11 @@ { "extends": "stylelint-config-standard", "rules": { - "alpha-value-notation": null, - "annotation-no-unknown": null, - "at-rule-descriptor-value-no-unknown": null, - "at-rule-no-vendor-prefix": null, "at-rule-no-unknown": [true, { "ignoreAtRules": ["content", "extend", "for", "include", "mixin"] }], - "color-function-alias-notation": null, - "color-function-notation": null, - "declaration-block-no-duplicate-properties": [true, { - "ignore": ["consecutive-duplicates-with-different-values"] - }], - "declaration-block-no-redundant-longhand-properties": null, - "declaration-property-value-keyword-no-deprecated": null, - "declaration-property-value-no-unknown": null, - "font-family-name-quotes": null, - "function-url-quotes": null, - "import-notation": null, - "keyframes-name-pattern": null, - "media-feature-range-notation": null, - "media-query-no-invalid": null, - "nesting-selector-no-missing-scoping-root": null, - "no-descending-specificity": null, - "no-invalid-position-at-import-rule": null, - "number-max-precision": null, - "property-no-deprecated": null, - "property-no-vendor-prefix": null, - "selector-attribute-quotes": null, - "selector-class-pattern": null, - "selector-no-vendor-prefix": null, - "selector-not-notation": null, "selector-pseudo-class-no-unknown": [true, { "ignorePseudoClasses": ["global"] - }], - "shorthand-property-no-redundant-values": null, - "value-keyword-case": null, - "value-no-vendor-prefix": null + }] } } diff --git a/Dockerfile b/Dockerfile index c2e2659ea7..de3ab09f79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,122 +1,173 @@ -# syntax=docker/dockerfile:1.7 +# Builds production version of Community App inside Docker container, +# and runs it against the specified Topcoder backend (development or +# production) when container is executed. -# Pin the complete multi-platform image digest so builds cannot silently pick up -# a different base image. Renovate/Dependabot can update the tag and digest -# together when a patched Node image is published. -ARG NODE_IMAGE=node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436 -ARG NODE_BUILD_IMAGE=node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436 - -FROM ${NODE_BUILD_IMAGE} AS development-dependencies +FROM node:10.24.1 +LABEL app="Community App" version="1.0" +RUN useradd -m -s /bin/bash appuser WORKDIR /opt/app - -# Native build tools stay isolated in the disposable builder stages. The -# Alpine runtime stage below never receives them. -RUN apk add --no-cache git python3 make g++ \ - && git config --global url."https://github.com/".insteadOf "git://github.com/" - -COPY package.json package-lock.json .npmrc ./ -COPY vendor ./vendor - -RUN npm ci - -FROM development-dependencies AS test - -ENV CI=true - COPY . . -RUN npm test - -FROM test AS build +RUN chown -R appuser:appuser /opt/app +USER appuser -ARG CDN_URL -ARG NODE_CONFIG_ENV=production - -ENV BABEL_ENV=production \ - CDN_URL=${CDN_URL} \ - NODE_CONFIG_ENV=${NODE_CONFIG_ENV} \ - NODE_ENV=production - -# The browser bundle is built as before. Server/shared sources are then -# precompiled so the runtime does not need Babel or the Webpack toolchain. -RUN npm run build \ - && ./node_modules/.bin/babel src \ - --out-dir /opt/runtime-src \ - --copy-files \ - --extensions ".js,.jsx" \ - && rm -rf \ - /opt/runtime-src/client \ - /opt/runtime-src/styles \ - /opt/runtime-src/test \ - && find /opt/runtime-src -type f \ - ! -name "*.js" \ - ! -name "*.json" \ - -delete \ - && install --directory /opt/runtime-src/assets/images \ - && install --mode=0644 \ - src/assets/images/favicon.ico \ - /opt/runtime-src/assets/images/favicon.ico - -FROM development-dependencies AS production-dependencies - -ENV NODE_ENV=production - -RUN npm prune --omit=dev --ignore-scripts \ - && npm cache clean --force - -FROM ${NODE_IMAGE} AS runtime - -LABEL org.opencontainers.image.title="Topcoder Community App" \ - org.opencontainers.image.description="Topcoder Community App web server" +################################################################################ +# Receiving of build arguments. +ARG AUTH0_CLIENT_ID ARG CDN_URL -ARG NODE_CONFIG_ENV=production - -ENV BABEL_ENV=production \ - CDN_URL=${CDN_URL} \ - NODE_CONFIG_ENV=${NODE_CONFIG_ENV} \ - NODE_ENV=production \ - PORT=3000 - -WORKDIR /opt/app - -# The application starts Node directly, so package-manager executables and -# their dependency trees are unnecessary attack surface in production. -RUN rm -rf \ - /opt/yarn-* \ - /usr/local/lib/node_modules/corepack \ - /usr/local/lib/node_modules/npm \ - && rm -f \ - /usr/local/bin/corepack \ - /usr/local/bin/npm \ - /usr/local/bin/npx \ - /usr/local/bin/yarn \ - /usr/local/bin/yarnpkg - -COPY --from=production-dependencies --chown=node:node /opt/app/vendor ./vendor -COPY --from=production-dependencies --chown=node:node /opt/app/node_modules ./node_modules -COPY --from=build --chown=node:node /opt/app/build ./build -COPY --from=build --chown=node:node /opt/app/.build-info ./.build-info -COPY --from=build --chown=node:node /opt/runtime-src ./src -COPY --from=build --chown=node:node \ - /opt/app/config/custom-environment-variables.js \ - /opt/app/config/default.js \ - /opt/app/config/development.js \ - /opt/app/config/production.js \ - /opt/app/config/qa.js \ - ./config/ -COPY --from=build --chown=node:node /opt/app/config/contentful ./config/contentful -COPY --chown=node:node package.json ./package.json -COPY --chown=node:node bin/runtime.js ./bin/runtime.js - -USER node +ARG COGNITIVE_NEWSLETTER_SIGNUP_APIKEY +ARG COGNITIVE_NEWSLETTER_SIGNUP_URL +ARG CONTENTFUL_CDN_API_KEY +ARG CONTENTFUL_PREVIEW_API_KEY +ARG CONTENTFUL_SPACE_ID + +# Credentials for access to Zurich space in Contentful CMS +ARG CONTENTFUL_ZURICH_SPACE_ID +ARG CONTENTFUL_ZURICH_CDN_API_KEY +ARG CONTENTFUL_ZURICH_PREVIEW_API_KEY + +# Credentials for access to TopGear space in Contentful CMS +ARG CONTENTFUL_TOPGEAR_SPACE_ID +ARG CONTENTFUL_TOPGEAR_CDN_API_KEY +ARG CONTENTFUL_TOPGEAR_PREVIEW_API_KEY + +# Credentials for access to Comcast space in Contentful CMS +ARG CONTENTFUL_COMCAST_SPACE_ID +ARG CONTENTFUL_COMCAST_CDN_API_KEY +ARG CONTENTFUL_COMCAST_PREVIEW_API_KEY + +#Credentials for Contentfu EDU space + +ARG CONTENTFUL_MANAGEMENT_TOKEN +ARG CONTENTFUL_EDU_SPACE_ID +ARG CONTENTFUL_EDU_CDN_API_KEY +ARG CONTENTFUL_EDU_PREVIEW_API_KEY + +ARG FILESTACK_API_KEY +ARG FILESTACK_SUBMISSION_CONTAINER +ARG RECRUITCRM_API_KEY + +# Credentials for Mailchimp service +ARG MAILCHIMP_API_KEY +ARG MAILCHIMP_BASE_URL + +ARG NODE_CONFIG_ENV +ARG OPEN_EXCHANGE_RATES_KEY +ARG SEGMENT_IO_API_KEY +ARG CHAMELEON_VERIFICATION_SECRET +ARG SERVER_API_KEY + +# TC M2M credentials for Community App server +ARG TC_M2M_CLIENT_ID +ARG TC_M2M_CLIENT_SECRET +ARG TC_M2M_AUDIENCE +ARG TC_M2M_GRANT_TYPE + +ARG TC_M2M_AUTH0_PROXY_SERVER_URL +ARG TC_M2M_AUTH0_URL +ARG AUTH_SECRET +ARG VALID_ISSUERS + +ARG COMMUNITY_APP_URL +ARG GSHEETS_API_KEY + +# Gig work referrals +ARG SENDGRID_API_KEY +ARG GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY + +# Optimizely +ARG OPTIMIZELY_SDK_KEY + +# Gamification +ARG GAMIFICATION_ORG_ID + +# Universal Nav +ARG UNIVERSAL_NAV_URL + +# Topgear submissions allowed domains +ARG TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS + +################################################################################ +# Setting of environment variables in the Docker image. + +ENV AUTH0_CLIENT_ID=$AUTH0_CLIENT_ID +ENV CDN_URL=$CDN_URL +ENV COGNITIVE_NEWSLETTER_SIGNUP_APIKEY=$COGNITIVE_NEWSLETTER_SIGNUP_APIKEY +ENV COGNITIVE_NEWSLETTER_SIGNUP_URL=$COGNITIVE_NEWSLETTER_SIGNUP_URL +ENV CONTENTFUL_CDN_API_KEY=$CONTENTFUL_CDN_API_KEY +ENV CONTENTFUL_PREVIEW_API_KEY=$CONTENTFUL_PREVIEW_API_KEY +ENV CONTENTFUL_SPACE_ID=$CONTENTFUL_SPACE_ID + +# Credentials for access to Zurich space in Contentful CMS +ENV CONTENTFUL_ZURICH_SPACE_ID=$CONTENTFUL_ZURICH_SPACE_ID +ENV CONTENTFUL_ZURICH_CDN_API_KEY=$CONTENTFUL_ZURICH_CDN_API_KEY +ENV CONTENTFUL_ZURICH_PREVIEW_API_KEY=$CONTENTFUL_ZURICH_PREVIEW_API_KEY + +# Credentials for access to TopGear space in Contentful CMS +ENV CONTENTFUL_TOPGEAR_SPACE_ID=$CONTENTFUL_TOPGEAR_SPACE_ID +ENV CONTENTFUL_TOPGEAR_CDN_API_KEY=$CONTENTFUL_TOPGEAR_CDN_API_KEY +ENV CONTENTFUL_TOPGEAR_PREVIEW_API_KEY=$CONTENTFUL_TOPGEAR_PREVIEW_API_KEY + +# Credentials for access to Comcast space in Contentful CMS +ENV CONTENTFUL_COMCAST_SPACE_ID=$CONTENTFUL_COMCAST_SPACE_ID +ENV CONTENTFUL_COMCAST_CDN_API_KEY=$CONTENTFUL_COMCAST_CDN_API_KEY +ENV CONTENTFUL_COMCAST_PREVIEW_API_KEY=$CONTENTFUL_COMCAST_PREVIEW_API_KEY + +ENV FILESTACK_API_KEY=$FILESTACK_API_KEY +ENV FILESTACK_SUBMISSION_CONTAINER=$FILESTACK_SUBMISSION_CONTAINER + +# Credentials for Mailchimp service +ENV MAILCHIMP_API_KEY=$MAILCHIMP_API_KEY +ENV MAILCHIMP_BASE_URL=$MAILCHIMP_BASE_URL + +ENV NODE_CONFIG_ENV=$NODE_CONFIG_ENV +ENV OPEN_EXCHANGE_RATES_KEY=$OPEN_EXCHANGE_RATES_KEY +ENV SEGMENT_IO_API_KEY=$SEGMENT_IO_API_KEY +ENV CHAMELEON_VERIFICATION_SECRET=$CHAMELEON_VERIFICATION_SECRET +ENV SERVER_API_KEY=$SERVER_API_KEY + +# TC M2M credentials for Community App server +ENV TC_M2M_CLIENT_ID=$TC_M2M_CLIENT_ID +ENV TC_M2M_CLIENT_SECRET=$TC_M2M_CLIENT_SECRET +ENV TC_M2M_AUDIENCE=$TC_M2M_AUDIENCE +ENV TC_M2M_GRANT_TYPE=$TC_M2M_GRANT_TYPE + +ENV TC_M2M_AUTH0_PROXY_SERVER_URL=$TC_M2M_AUTH0_PROXY_SERVER_URL +ENV TC_M2M_AUTH0_URL=$TC_M2M_AUTH0_URL +ENV AUTH_SECRET=$AUTH_SECRET +ENV VALID_ISSUERS=$VALID_ISSUERS + +ENV CONTENTFUL_MANAGEMENT_TOKEN=$CONTENTFUL_MANAGEMENT_TOKEN +ENV CONTENTFUL_EDU_SPACE_ID=$CONTENTFUL_EDU_SPACE_ID +ENV CONTENTFUL_EDU_CDN_API_KEY=$CONTENTFUL_EDU_CDN_API_KEY +ENV CONTENTFUL_EDU_PREVIEW_API_KEY=$CONTENTFUL_EDU_PREVIEW_API_KEY +ENV RECRUITCRM_API_KEY=$RECRUITCRM_API_KEY +ENV COMMUNITY_APP_URL=$COMMUNITY_APP_URL +ENV SENDGRID_API_KEY=$SENDGRID_API_KEY +ENV GSHEETS_API_KEY=$GSHEETS_API_KEY +ENV GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY=$GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY + +# Optimizely +ENV OPTIMIZELY_SDK_KEY=$OPTIMIZELY_SDK_KEY + +ENV GAMIFICATION_ORG_ID=$GAMIFICATION_ORG_ID + +# Universal nav +ENV UNIVERSAL_NAV_URL=$UNIVERSAL_NAV_URL + +# Topgear submissions allowed domains +ENV TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS=$TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS + +################################################################################ +# Testing and build of the application inside the container. + +RUN npm config set unsafe-perm true +RUN git config --global url."https://git@".insteadOf git:// +RUN npm ci +RUN npm test +RUN npm run build EXPOSE 3000 - -HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ - CMD ["node", "-e", "const http=require('http');const req=http.get({host:'127.0.0.1',port:process.env.PORT||3000,path:'/api/cdn/public/ping',timeout:3000},res=>{res.resume();process.exit(res.statusCode===200?0:1);});req.on('timeout',()=>{req.destroy();process.exit(1);});req.on('error',()=>process.exit(1));"] - -STOPSIGNAL SIGTERM - -CMD ["node", "--max-old-space-size=8192", "bin/runtime.js"] +CMD ["npm", "start"] diff --git a/__tests__/.eslintrc b/__tests__/.eslintrc index f2d3a19d77..c4c8a8486f 100644 --- a/__tests__/.eslintrc +++ b/__tests__/.eslintrc @@ -1,12 +1,3 @@ { - "env": { - "jest": true - }, - "plugins": [ - "jest" - ], - "rules": { - "global-require": 0, - "import/no-dynamic-require": 0 - } -} + "extends": "../node_modules/topcoder-react-utils/config/eslint/jest.json" +} \ No newline at end of file diff --git a/__tests__/config/security.js b/__tests__/config/security.js deleted file mode 100644 index 1636b629e0..0000000000 --- a/__tests__/config/security.js +++ /dev/null @@ -1,138 +0,0 @@ -/* eslint-env jest */ - -const fs = require('fs'); -const nodePath = require('path'); - -const backupDefaults = require('../../config/backup-default'); -const customEnvironmentVariables = require('../../config/custom-environment-variables'); -const defaults = require('../../config/default'); -const development = require('../../config/development'); -const production = require('../../config/production'); -const qa = require('../../config/qa'); -const testConfig = require('../../config/test'); - -/** - * Gets a nested configuration value from a dot-separated path. - * - * @param {Object} object configuration object - * @param {String} path dot-separated configuration path - * @returns {*} resolved value - */ -function get(object, path) { - return path.split('.').reduce((value, key) => value && value[key], object); -} - -const DEFAULT_CREDENTIAL_PATHS = [ - 'LOG_ENTRIES_TOKEN', - 'NEWSLETTER_SIGNUP.COGNITIVE.APIKEY', - 'SEGMENT_IO_API_KEY', - 'SERVER_API_KEY', - 'FILESTACK.API_KEY', - 'SECRET.CONTENTFUL.MANAGEMENT_TOKEN', - 'SECRET.CONTENTFUL.default.master.CDN_API_KEY', - 'SECRET.CONTENTFUL.default.master.PREVIEW_API_KEY', - 'SECRET.CONTENTFUL.EDU.master.CDN_API_KEY', - 'SECRET.CONTENTFUL.EDU.master.PREVIEW_API_KEY', - 'SECRET.CONTENTFUL.zurich.master.CDN_API_KEY', - 'SECRET.CONTENTFUL.zurich.master.PREVIEW_API_KEY', - 'SECRET.CONTENTFUL.topgear.master.CDN_API_KEY', - 'SECRET.CONTENTFUL.topgear.master.PREVIEW_API_KEY', - 'SECRET.CONTENTFUL.comcast.master.CDN_API_KEY', - 'SECRET.CONTENTFUL.comcast.master.PREVIEW_API_KEY', - 'SECRET.MAILCHIMP.default.API_KEY', - 'SECRET.OPEN_EXCHANGE_RATES_KEY', - 'SECRET.TC_M2M.CLIENT_ID', - 'SECRET.TC_M2M.CLIENT_SECRET', - 'SECRET.RECRUITCRM_API_KEY', - 'SECRET.SENDGRID_API_KEY', - 'SECRET.JWT_AUTH.SECRET', - 'SECRET.JWT_AUTH.AUTH_SECRET', - 'SECRET.CHAMELEON_VERIFICATION_SECRET', - 'GSHEETS_API_KEY', - 'GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY', -]; - -const ENVIRONMENT_CREDENTIAL_PATHS = [ - ['development', development, 'SEGMENT_IO_API_KEY'], - ['development', development, 'SERVER_API_KEY'], - ['production', production, 'LOG_ENTRIES_TOKEN'], - ['production', production, 'SERVER_API_KEY'], - ['qa', qa, 'SEGMENT_IO_API_KEY'], - ['qa', qa, 'SERVER_API_KEY'], - ['test', testConfig, 'SERVER_API_KEY'], -]; - -describe('credential configuration', () => { - [ - ['default', defaults], - ['backup-default', backupDefaults], - ].forEach(([name, config]) => { - test(`${name} configuration has no credential fallback values`, () => { - DEFAULT_CREDENTIAL_PATHS.forEach((path) => { - expect(get(config, path)).toBe(''); - }); - }); - }); - - ENVIRONMENT_CREDENTIAL_PATHS.forEach(([name, config, path]) => { - test(`${name} configuration leaves ${path} empty`, () => { - expect(get(config, path)).toBe(''); - }); - }); - - test.each(DEFAULT_CREDENTIAL_PATHS)( - '%s is mapped to an environment variable', - (path) => { - expect(get(customEnvironmentVariables, path)).toMatch(/^[A-Z0-9_]+$/); - }, - ); - - test('Docker builds receive only non-secret build arguments', () => { - const source = fs.readFileSync( - nodePath.resolve(__dirname, '../../build.sh'), - 'utf8', - ); - const buildArguments = [...source.matchAll(/--build-arg\s+["']?([A-Z0-9_]+)/g)] - .map(match => match[1]); - - expect(buildArguments).toEqual(['CDN_URL', 'NODE_CONFIG_ENV']); - }); - - test('JMeter loads M2M credentials from runtime properties', () => { - const source = fs.readFileSync( - nodePath.resolve(__dirname, '../../src/test/jmeter/Community-25UV.jmx'), - 'utf8', - ); - - // eslint-disable-next-line no-template-curly-in-string - expect(source).toContain('${__P(TC_M2M_CLIENT_ID,)}'); - // eslint-disable-next-line no-template-curly-in-string - expect(source).toContain('${__P(TC_M2M_CLIENT_SECRET,)}'); - expect(source).not.toMatch( - /name="client_(?:id|secret)"[\s\S]{0,250}Argument\.value">(?!(?:\$\{__P\(|<\/))/, - ); - }); - - test('Segment analytics uses the configured key without a static literal', () => { - const source = fs.readFileSync( - nodePath.resolve(__dirname, '../../src/server/index.js'), - 'utf8', - ); - - expect(source).toMatch( - /analytics\.load\(\$\{serializeJs\(config\.SEGMENT_IO_API_KEY\)\}\);/, - ); - expect(source).not.toMatch(/analytics\.load\(['"][^'"]+['"]\)/); - }); - - test('API-key authorization fails closed when configuration is empty', () => { - const source = fs.readFileSync( - nodePath.resolve(__dirname, '../../src/server/index.js'), - 'utf8', - ); - - expect(source).toMatch( - /if \(!config\.SERVER_API_KEY\s*\|\|\s*req\.headers\.authorization !==/, - ); - }); -}); diff --git a/__tests__/config/webpack.js b/__tests__/config/webpack.js deleted file mode 100644 index 262e850ff6..0000000000 --- a/__tests__/config/webpack.js +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-env jest */ - -const crypto = require('crypto'); -const fs = require('fs'); -const forge = require('node-forge'); - -jest.unmock('webpack'); - -const createWebpackConfig = require('../../config/webpack/create'); - -describe('webpack build information', () => { - afterEach(() => { - jest.restoreAllMocks(); - }); - - test('generates a raw 32-byte key accepted by the renderer cipher', () => { - const bytes = [ - 0, 10, 13, 34, 92, 255, - ...Array.from({ length: 26 }, (_, index) => index + 128), - ]; - jest.spyOn(crypto, 'randomBytes').mockReturnValue(Buffer.from(bytes)); - const writeFile = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); - - const webpackConfig = createWebpackConfig('production'); - - const buildInfo = JSON.parse(writeFile.mock.calls[0][1]); - const definePlugin = webpackConfig.plugins.find(plugin => ( - plugin.definitions && plugin.definitions.BUILD_INFO - )); - const clientBuildInfo = JSON.parse(definePlugin.definitions.BUILD_INFO); - expect(buildInfo.key).toHaveLength(32); - expect([...buildInfo.key].map(character => character.charCodeAt(0))).toEqual(bytes); - expect(clientBuildInfo.key).toBe(buildInfo.key); - expect(() => forge.cipher.createCipher('AES-CBC', buildInfo.key)).not.toThrow(); - expect(() => forge.cipher.createDecipher('AES-CBC', clientBuildInfo.key)).not.toThrow(); - }); -}); diff --git a/__tests__/server/avatar.js b/__tests__/server/avatar.js deleted file mode 100644 index fd77a73541..0000000000 --- a/__tests__/server/avatar.js +++ /dev/null @@ -1,101 +0,0 @@ -import fetch from 'isomorphic-fetch'; -import sharp from 'sharp'; - -import getAvatar, { normalizeAvatarUrl } from 'server/services/avatar'; - -jest.mock('isomorphic-fetch', () => jest.fn()); -jest.mock('sharp', () => jest.fn()); - -function mockResponse({ - body = Buffer.from('image'), - contentType = 'image/png', - location, - status = 200, -} = {}) { - const headers = { - 'content-length': String(body.length), - 'content-type': contentType, - location, - }; - return { - buffer: jest.fn(() => Promise.resolve(body)), - headers: { - get: jest.fn(name => headers[name.toLowerCase()] || null), - }, - ok: status >= 200 && status < 300, - status, - }; -} - -describe('avatar service security boundaries', () => { - let resize; - let toBuffer; - - beforeEach(() => { - jest.clearAllMocks(); - toBuffer = jest.fn(() => Promise.resolve(Buffer.from('resized'))); - resize = jest.fn(() => ({ toBuffer })); - sharp.mockReturnValue({ resize }); - }); - - test('rejects loopback and attacker-controlled destinations before fetching', async () => { - await expect(getAvatar('http://127.0.0.1/latest/meta-data', 32)) - .rejects.toThrow('Avatar URL is not trusted'); - await expect(getAvatar('https://member-media.topcoder.com.attacker.test/a.png', 32)) - .rejects.toThrow('Avatar URL is not trusted'); - - expect(fetch).not.toHaveBeenCalled(); - }); - - test('normalizes only legacy relative paths against the configured site', () => { - const normalized = normalizeAvatarUrl('/i/m/avatar.png'); - - expect(normalized.pathname).toBe('/i/m/avatar.png'); - expect(normalized.protocol).toBe('https:'); - }); - - test('fetches and resizes a bounded raster image from a trusted media host', async () => { - fetch.mockResolvedValue(mockResponse()); - - await expect(getAvatar( - 'https://topcoder-prod-media.s3.amazonaws.com/member/profile/avatar.png', - 64, - )).resolves.toEqual(Buffer.from('resized')); - - expect(fetch).toHaveBeenCalledWith( - 'https://topcoder-prod-media.s3.amazonaws.com/member/profile/avatar.png', - expect.objectContaining({ redirect: 'manual' }), - ); - expect(sharp).toHaveBeenCalledWith( - Buffer.from('image'), - { limitInputPixels: 40000000 }, - ); - expect(resize).toHaveBeenCalledWith(64, 64, { fit: 'inside' }); - }); - - test('rejects a redirect that leaves the trusted media origins', async () => { - fetch.mockResolvedValue(mockResponse({ - location: 'http://169.254.169.254/latest/meta-data', - status: 302, - })); - - await expect(getAvatar( - 'https://member-media.topcoder.com/avatar.png', - 32, - )).rejects.toThrow('Avatar URL is not trusted'); - expect(fetch).toHaveBeenCalledTimes(1); - }); - - test('rejects unsupported content types and excessive resize requests', async () => { - fetch.mockResolvedValue(mockResponse({ contentType: 'text/html' })); - - await expect(getAvatar( - 'https://member-media.topcoder.com/avatar.png', - 32, - )).rejects.toThrow('Avatar response is not a supported image'); - await expect(getAvatar( - 'https://member-media.topcoder.com/avatar.png', - 2048, - )).rejects.toThrow('Invalid avatar size'); - }); -}); diff --git a/__tests__/server/recruitCRM.js b/__tests__/server/recruitCRM.js deleted file mode 100644 index 5b2b01aaa1..0000000000 --- a/__tests__/server/recruitCRM.js +++ /dev/null @@ -1,96 +0,0 @@ -import fetch from 'isomorphic-fetch'; - -import RecruitCRMService, { - normalizeRecruitCrmIdentifier, - parseApplicationForm, -} from 'server/services/recruitCRM'; - -jest.mock('isomorphic-fetch', () => jest.fn()); -jest.mock('topcoder-react-lib', () => ({ - logger: { - error: jest.fn(), - }, - services: { - api: {}, - }, -})); -jest.mock('server/services/sendGrid', () => ({ - sendEmailDirect: jest.fn(), -})); - -function validApplication(overrides = {}) { - return { - city: 'Hobart', - contact_number: '+61 400 000 000', - custom_fields: [ - { field_id: 1, value: 'https://topcoder.com/members/member' }, - { field_id: 2, value: 'member' }, - { field_id: 14, value: 'Job information' }, - ], - email: 'member@example.com', - first_name: 'Test', - last_name: 'Member', - locality: 'Australia', - salary_expectation: '', - skill: 'JavaScript', - ...overrides, - }; -} - -describe('RecruitCRM input security boundaries', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('accepts bounded opaque identifiers and rejects URL path injection', () => { - expect(normalizeRecruitCrmIdentifier('job_slug-123')).toBe('job_slug-123'); - expect(normalizeRecruitCrmIdentifier('../../admin')).toBeNull(); - expect(normalizeRecruitCrmIdentifier('job/assign?admin=true')).toBeNull(); - expect(normalizeRecruitCrmIdentifier('short')).toBeNull(); - }); - - test('parses a valid bounded application form', () => { - const parsed = parseApplicationForm(JSON.stringify(validApplication())); - - expect(parsed.email).toBe('member@example.com'); - expect(parsed.custom_fields).toHaveLength(3); - }); - - test('rejects malformed and unbounded custom field arrays', () => { - expect(() => parseApplicationForm('{invalid-json')) - .toThrow('Invalid application form'); - expect(() => parseApplicationForm(JSON.stringify(validApplication({ - custom_fields: Array.from( - { length: 33 }, - (value, fieldId) => ({ field_id: fieldId + 1, value: '' }), - ), - })))).toThrow('Invalid application form'); - }); - - test('rejects an invalid job identifier before any upstream request', async () => { - const service = new RecruitCRMService(); - const req = { - body: { form: JSON.stringify(validApplication()) }, - params: { id: '../../../metadata' }, - }; - const res = { - json: jest.fn(), - status: jest.fn(), - }; - res.status.mockReturnValue(res); - - await service.applyForJob(req, res, jest.fn()); - - expect(res.status).toHaveBeenCalledWith(400); - expect(res.json).toHaveBeenCalledWith({ error: 'Invalid job ID format.' }); - expect(fetch).not.toHaveBeenCalled(); - }); - - test('does not return an upstream exception or stack trace to the cache', async () => { - fetch.mockRejectedValue(new Error('private upstream stack details')); - const service = new RecruitCRMService(); - - await expect(service.getAll({ job_status: 1 })) - .resolves.toEqual({ error: true }); - }); -}); diff --git a/__tests__/server/routes/authentication.js b/__tests__/server/routes/authentication.js deleted file mode 100644 index d45b688508..0000000000 --- a/__tests__/server/routes/authentication.js +++ /dev/null @@ -1,63 +0,0 @@ -import { createJwtAuthenticator } from 'server/routes/authentication'; - -function mockResponse() { - const res = { - json: jest.fn(), - status: jest.fn(), - }; - res.status.mockReturnValue(res); - return res; -} - -describe('JWT route configuration', () => { - test('fails closed when the canonical secret is absent', () => { - const factory = jest.fn(); - const handler = createJwtAuthenticator({ - AUTH_SECRET: 'legacy-fallback-must-not-be-used', - SECRET: '', - VALID_ISSUERS: '["https://api.topcoder.com"]', - }, factory); - const next = jest.fn(); - const res = mockResponse(); - - handler({}, res, next); - - expect(factory).not.toHaveBeenCalled(); - expect(next).not.toHaveBeenCalled(); - expect(res.status).toHaveBeenCalledWith(503); - expect(res.json).toHaveBeenCalledWith({ - error: 'Authentication is unavailable.', - }); - }); - - test('fails closed when issuer configuration is malformed', () => { - const factory = jest.fn(); - const handler = createJwtAuthenticator({ - SECRET: 'configured-secret', - VALID_ISSUERS: 'not-json', - }, factory); - const next = jest.fn(); - const res = mockResponse(); - - handler({}, res, next); - - expect(factory).not.toHaveBeenCalled(); - expect(next).not.toHaveBeenCalled(); - expect(res.status).toHaveBeenCalledWith(503); - }); - - test('maps the canonical secret to the tc-core authenticator contract', () => { - const expectedMiddleware = jest.fn(); - const factory = jest.fn(() => expectedMiddleware); - const handler = createJwtAuthenticator({ - SECRET: 'configured-secret', - VALID_ISSUERS: ['https://api.topcoder.com'], - }, factory); - - expect(handler).toBe(expectedMiddleware); - expect(factory).toHaveBeenCalledWith({ - AUTH_SECRET: 'configured-secret', - VALID_ISSUERS: '["https://api.topcoder.com"]', - }); - }); -}); diff --git a/__tests__/server/routes/security.js b/__tests__/server/routes/security.js deleted file mode 100644 index 9a8dfa1df6..0000000000 --- a/__tests__/server/routes/security.js +++ /dev/null @@ -1,98 +0,0 @@ -import config from 'config'; -import express from 'express'; -import request from 'supertest'; - -import { - configuredJwtAuthenticator, - protectedCorsOptions, -} from 'server/routes/authentication'; -import contentfulRoutes, { - articleVoteLimiter, -} from 'server/routes/contentful'; -import recruitRoutes, { - sensitiveRouteLimiter, -} from 'server/routes/recruitCRM'; - -jest.mock('tc-core-library-js', () => ({ - middleware: { - jwtAuthenticator: jest.fn(() => (req, res, next) => next()), - }, -})); - -jest.mock('server/services/contentful', () => ({ - ALLOWED_DOMAINS: [], - ASSETS_DOMAIN: 'assets.example.test', - IMAGES_DOMAIN: 'images.example.test', - articleVote: jest.fn(), - getService: jest.fn(), - getSpaceId: jest.fn(), -})); - -jest.mock('server/services/recruitCRM', () => jest.fn()); - -/** - * Gets middleware attached to a specific Express router method. - * @param {Function} router Express router. - * @param {String} path Route path. - * @param {String} method Lowercase HTTP method. - * @return {Function[]} Attached middleware functions. - */ -function getRouteMiddleware(router, path, method) { - const layer = router.stack.find(item => item.route - && item.route.path === path - && item.route.methods[method]); - return layer ? layer.route.stack.map(item => item.handle) : []; -} - -describe('authenticated route protections', () => { - test.each([ - ['/jobs/cache/flush', 'get'], - ['/jobs/:id/apply', 'post'], - ['/profile', 'get'], - ['/profile', 'post'], - ])('rate limits RecruitCRM %s %s before handling it', (path, method) => { - const routeMiddleware = getRouteMiddleware(recruitRoutes, path, method); - expect(routeMiddleware).toContain(sensitiveRouteLimiter); - expect(routeMiddleware).toContain(configuredJwtAuthenticator); - expect(routeMiddleware.indexOf(sensitiveRouteLimiter)) - .toBeLessThan(routeMiddleware.indexOf(configuredJwtAuthenticator)); - }); - - test('rate limits Contentful article voting', () => { - const routeMiddleware = getRouteMiddleware( - contentfulRoutes, - '/:spaceName/:environment/votes', - 'post', - ); - expect(routeMiddleware).toContain(articleVoteLimiter); - expect(routeMiddleware).toContain(configuredJwtAuthenticator); - expect(routeMiddleware.indexOf(articleVoteLimiter)) - .toBeLessThan(routeMiddleware.indexOf(configuredJwtAuthenticator)); - }); - - test('uses exact configured origins instead of reflecting any request origin', () => { - expect(protectedCorsOptions.origin).toContain(new URL(config.URL.BASE).origin); - expect(protectedCorsOptions.origin).not.toContain('*'); - expect(protectedCorsOptions.origin).not.toContain(true); - }); - - test('does not reflect an untrusted origin on the job application preflight', async () => { - const app = express(); - app.use(recruitRoutes); - const trustedOrigin = new URL(config.URL.BASE).origin; - - const trustedResponse = await request(app) - .options('/jobs/job_slug-123/apply') - .set('Origin', trustedOrigin) - .set('Access-Control-Request-Method', 'POST'); - const untrustedResponse = await request(app) - .options('/jobs/job_slug-123/apply') - .set('Origin', 'https://attacker.example') - .set('Access-Control-Request-Method', 'POST'); - - expect(trustedResponse.headers['access-control-allow-origin']) - .toBe(trustedOrigin); - expect(untrustedResponse.headers['access-control-allow-origin']) - .toBeUndefined(); - }); -}); diff --git a/__tests__/shared/components/ChallengeTile/fixtures/design.json b/__tests__/shared/components/ChallengeTile/__mocks__/design.json similarity index 100% rename from __tests__/shared/components/ChallengeTile/fixtures/design.json rename to __tests__/shared/components/ChallengeTile/__mocks__/design.json diff --git a/__tests__/shared/components/ChallengeTile/fixtures/develop.json b/__tests__/shared/components/ChallengeTile/__mocks__/develop.json similarity index 100% rename from __tests__/shared/components/ChallengeTile/fixtures/develop.json rename to __tests__/shared/components/ChallengeTile/__mocks__/develop.json diff --git a/__tests__/shared/components/ChallengeTile/fixtures/marathon.json b/__tests__/shared/components/ChallengeTile/__mocks__/marathon.json similarity index 100% rename from __tests__/shared/components/ChallengeTile/fixtures/marathon.json rename to __tests__/shared/components/ChallengeTile/__mocks__/marathon.json diff --git a/__tests__/shared/components/ChallengeTile/__snapshots__/index.jsx.snap b/__tests__/shared/components/ChallengeTile/__snapshots__/index.jsx.snap index 4beb7d5141..04faa6f2d0 100644 --- a/__tests__/shared/components/ChallengeTile/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/ChallengeTile/__snapshots__/index.jsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders design 1`] = `
({ - getService: jest.fn(), -})); - -describe('Contentful SearchBar URL construction', () => { - const input = '&role=admin'; - - test.each([ - ['Author', { author: input }], - ['Title', { title: input }], - ['All', { phrase: input }], - ['Tags', { tags: [input] }], - ])('encodes %s input as query data rather than DOM markup', (filter, expected) => { - const searchUrl = buildSearchUrl(filter, input); - const query = searchUrl.slice(searchUrl.indexOf('?') + 1); - - expect(qs.parse(query)).toEqual(expected); - expect(searchUrl).not.toContain(' { - const searchUrl = buildSearchUrl('Tags', 'JavaScript & Node.js'); - const query = searchUrl.slice(searchUrl.indexOf('?') + 1); - - expect(qs.parse(query)).toEqual({ tags: ['JavaScript & Node.js'] }); - }); -}); diff --git a/__tests__/shared/components/Contentful/Shape/__snapshots__/Shape.jsx.snap b/__tests__/shared/components/Contentful/Shape/__snapshots__/Shape.jsx.snap index f476d6b902..32a134509d 100644 --- a/__tests__/shared/components/Contentful/Shape/__snapshots__/Shape.jsx.snap +++ b/__tests__/shared/components/Contentful/Shape/__snapshots__/Shape.jsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Matches shallow shapshot 1`] = `
`; diff --git a/__tests__/shared/components/Contentful/TracksTree/ChildListRow/__snapshots__/ChildListRow.jsx.snap b/__tests__/shared/components/Contentful/TracksTree/ChildListRow/__snapshots__/ChildListRow.jsx.snap index a0c3db1942..be163816b1 100644 --- a/__tests__/shared/components/Contentful/TracksTree/ChildListRow/__snapshots__/ChildListRow.jsx.snap +++ b/__tests__/shared/components/Contentful/TracksTree/ChildListRow/__snapshots__/ChildListRow.jsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Matches shallow shapshot 1`] = `
diff --git a/__tests__/shared/components/GUIKit/TextInput/__snapshots__/index.jsx.snap b/__tests__/shared/components/GUIKit/TextInput/__snapshots__/index.jsx.snap index a4fda570a9..739f1a551e 100644 --- a/__tests__/shared/components/GUIKit/TextInput/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/GUIKit/TextInput/__snapshots__/index.jsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Default render 1`] = `
@@ -102,7 +102,7 @@ exports[`Matches shallow shapshot 1`] = ` > @@ -156,7 +156,7 @@ exports[`Matches shallow shapshot 1`] = ` > @@ -210,7 +210,7 @@ exports[`Matches shallow shapshot 1`] = ` > diff --git a/__tests__/shared/components/Leaderboard/__snapshots__/Podium.jsx.snap b/__tests__/shared/components/Leaderboard/__snapshots__/Podium.jsx.snap index 22be825a52..c5a8a3f5e5 100644 --- a/__tests__/shared/components/Leaderboard/__snapshots__/Podium.jsx.snap +++ b/__tests__/shared/components/Leaderboard/__snapshots__/Podium.jsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Matches shallow shapshot 1`] = `
@@ -68,7 +68,7 @@ exports[`Matches shallow shapshot 2`] = ` > diff --git a/__tests__/shared/components/Loader/__snapshots__/Loader.jsx.snap b/__tests__/shared/components/Loader/__snapshots__/Loader.jsx.snap index df549582c8..38508a917b 100644 --- a/__tests__/shared/components/Loader/__snapshots__/Loader.jsx.snap +++ b/__tests__/shared/components/Loader/__snapshots__/Loader.jsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Matches shallow shapshot 1`] = `

Your submission has been received and may undergo AI-assisted review during Submission phase. Results will be available for inspection in the review app and final evaluation occurs during Review phase. diff --git a/__tests__/shared/components/SubmissionManagement/__snapshots__/ScreeningStatus.jsx.snap b/__tests__/shared/components/SubmissionManagement/__snapshots__/ScreeningStatus.jsx.snap index 0954185768..e0c4ffcd17 100644 --- a/__tests__/shared/components/SubmissionManagement/__snapshots__/ScreeningStatus.jsx.snap +++ b/__tests__/shared/components/SubmissionManagement/__snapshots__/ScreeningStatus.jsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Snapshot match 1`] = `