From 0a00468274d786db9de604b6e456b184b52f7675 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Wed, 25 Aug 2021 10:59:54 -0400 Subject: [PATCH 01/34] create grantee page --- frontend/src/App.js | 8 ++ frontend/src/components/SiteNav.js | 34 +++++--- .../pages/GranteeSearch/__tests__/index.js | 9 +++ frontend/src/pages/GranteeSearch/index.css | 14 ++++ frontend/src/pages/GranteeSearch/index.js | 78 +++++++++++++++++++ 5 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 frontend/src/pages/GranteeSearch/__tests__/index.js create mode 100644 frontend/src/pages/GranteeSearch/index.css create mode 100644 frontend/src/pages/GranteeSearch/index.js diff --git a/frontend/src/App.js b/frontend/src/App.js index 454faf301e..413d339c82 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -31,6 +31,7 @@ import RequestPermissions from './components/RequestPermissions'; import AriaLiveContext from './AriaLiveContext'; import AriaLiveRegion from './components/AriaLiveRegion'; import ApprovedActivityReport from './pages/ApprovedActivityReport'; +import GranteeSearch from './pages/GranteeSearch'; function App() { const [user, updateUser] = useState(); @@ -140,6 +141,13 @@ function App() { )} /> + ( + + )} + /> ( diff --git a/frontend/src/components/SiteNav.js b/frontend/src/components/SiteNav.js index 7f73e7aceb..6b95092e0b 100644 --- a/frontend/src/components/SiteNav.js +++ b/frontend/src/components/SiteNav.js @@ -3,7 +3,7 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { NavLink as Link, withRouter } from 'react-router-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faChartBar, faBorderAll } from '@fortawesome/free-solid-svg-icons'; +import { faChartBar, faBorderAll, faUserFriends } from '@fortawesome/free-solid-svg-icons'; import './SiteNav.css'; @@ -88,16 +88,28 @@ const SiteNav = ({ {admin ? ( -
  • - - - - - Regional Dashboard - -
  • + <> +
  • + + + + + Regional Dashboard + +
  • +
  • + + + + + Grantees + +
  • + ) : null } diff --git a/frontend/src/pages/GranteeSearch/__tests__/index.js b/frontend/src/pages/GranteeSearch/__tests__/index.js new file mode 100644 index 0000000000..26f63239aa --- /dev/null +++ b/frontend/src/pages/GranteeSearch/__tests__/index.js @@ -0,0 +1,9 @@ +// import '@testing-library/jest-dom'; +// import React from 'react'; +// import { +// fireEvent, +// render, screen, waitFor, within, +// } from '@testing-library/react'; +// import { act } from 'react-dom/test-utils'; + +// import GranteeSearch from '../index'; diff --git a/frontend/src/pages/GranteeSearch/index.css b/frontend/src/pages/GranteeSearch/index.css new file mode 100644 index 0000000000..7dbfc590c3 --- /dev/null +++ b/frontend/src/pages/GranteeSearch/index.css @@ -0,0 +1,14 @@ +.ttahub-grantee-search .smart-hub--button-select-toggle-btn { + padding: 0.75rem; +} + +.ttahub-grantee-search--search-input { + border-radius: 0.25rem 0 0 0.25rem; + height: auto; + padding: 0.5rem 0.25rem; + width: 480px; +} + +.ttahub-grantee-search--submit-button { + border-radius: 0 0.25rem 0.25rem 0; +} \ No newline at end of file diff --git a/frontend/src/pages/GranteeSearch/index.js b/frontend/src/pages/GranteeSearch/index.js new file mode 100644 index 0000000000..d104236d49 --- /dev/null +++ b/frontend/src/pages/GranteeSearch/index.js @@ -0,0 +1,78 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Helmet } from 'react-helmet'; +import { Grid } from '@trussworks/react-uswds'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSearch } from '@fortawesome/free-solid-svg-icons'; +import RegionalSelect from '../../components/RegionalSelect'; +import { getUserRegions } from '../../permissions'; +import './index.css'; + +function GranteeSearch({ user }) { + const hasCentralOffice = user && user.homeRegionId && user.homeRegionId === 14; + const regions = getUserRegions(user); + + // eslint-disable-next-line max-len + const [appliedRegion, setAppliedRegion] = useState(hasCentralOffice ? 14 : regions[0]); + const [query, setQuery] = useState(''); + + function onApplyRegion(region) { + setAppliedRegion(region.value); + } + + function onSubmit(e) { + e.preventDefault(); + } + + return ( + <> + + Grantee Records Search + +
    +

    Grantee Records

    + + {regions.length > 1 + && ( +
    + +
    + )} +
    + setQuery(e.target.value)} /> + +
    +
    +
    + + ); +} + +export default GranteeSearch; + +GranteeSearch.propTypes = { + user: PropTypes.shape({ + id: PropTypes.number, + name: PropTypes.string, + role: PropTypes.arrayOf(PropTypes.string), + homeRegionId: PropTypes.number, + permissions: PropTypes.arrayOf(PropTypes.shape({ + userId: PropTypes.number, + scopeId: PropTypes.number, + regionId: PropTypes.number, + })), + }), +}; + +GranteeSearch.defaultProps = { + user: null, +}; From ad513730b8fa239755e1fb86756f7209ee442e96 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Thu, 16 Sep 2021 10:29:20 -0400 Subject: [PATCH 02/34] building backend route for grantee search --- frontend/src/fetchers/grantee.js | 28 +++++++++++++++ frontend/src/pages/GranteeSearch/index.js | 16 +++++++-- src/routes/activityReports/handlers.js | 1 - src/routes/apiDirectory.js | 3 +- src/routes/grantee/handlers.js | 27 +++++++++++++++ src/routes/grantee/index.js | 9 +++++ src/services/grantee.js | 42 +++++++++++++++++++++-- 7 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 frontend/src/fetchers/grantee.js create mode 100644 src/routes/grantee/handlers.js create mode 100644 src/routes/grantee/index.js diff --git a/frontend/src/fetchers/grantee.js b/frontend/src/fetchers/grantee.js new file mode 100644 index 0000000000..b3d7260e5d --- /dev/null +++ b/frontend/src/fetchers/grantee.js @@ -0,0 +1,28 @@ +import join from 'url-join'; +import { + get, +} from './index'; +import { DECIMAL_BASE } from '../Constants'; + +const granteeUrl = join('/', 'api', 'grantee'); + +// eslint-disable-next-line import/prefer-default-export +export const searchGrantees = async (query, regionId = '') => { + try { + if (!query) { + throw new Error('Please provide a query string to search grantees'); + } + + const querySearch = `?s=${query}`; + const regionSearch = regionId ? `®ion=${regionId.toString(DECIMAL_BASE)}` : ''; + + const grantees = await get( + join(granteeUrl, 'search', querySearch, regionSearch), + ); + + return grantees.json(); + } catch (e) { + // todo - this should probably not throw an error, so it doesn't crash the frontend on a 404 + throw new Error(e); + } +}; diff --git a/frontend/src/pages/GranteeSearch/index.js b/frontend/src/pages/GranteeSearch/index.js index d104236d49..79b1b3fa7e 100644 --- a/frontend/src/pages/GranteeSearch/index.js +++ b/frontend/src/pages/GranteeSearch/index.js @@ -6,6 +6,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSearch } from '@fortawesome/free-solid-svg-icons'; import RegionalSelect from '../../components/RegionalSelect'; import { getUserRegions } from '../../permissions'; +import { searchGrantees } from '../../fetchers/grantee'; import './index.css'; function GranteeSearch({ user }) { @@ -15,13 +16,21 @@ function GranteeSearch({ user }) { // eslint-disable-next-line max-len const [appliedRegion, setAppliedRegion] = useState(hasCentralOffice ? 14 : regions[0]); const [query, setQuery] = useState(''); + const [granteeResults, setGranteeResults] = useState([]); function onApplyRegion(region) { setAppliedRegion(region.value); } - function onSubmit(e) { + async function onSubmit(e) { e.preventDefault(); + + if (!query) { + return; + } + + const results = await searchGrantees(query, appliedRegion); + setGranteeResults(results); } return ( @@ -43,7 +52,7 @@ function GranteeSearch({ user }) { /> )} -
    + setQuery(e.target.value)} />
    +
    + {granteeResults.map((grantee) =>

    {grantee.name}

    )} +
    ); diff --git a/src/routes/activityReports/handlers.js b/src/routes/activityReports/handlers.js index 0d7d800ae2..3a41346011 100644 --- a/src/routes/activityReports/handlers.js +++ b/src/routes/activityReports/handlers.js @@ -161,7 +161,6 @@ async function sendActivityReportCSV(reports, res) { key: 'lastSaved', header: 'Last saved', }, - ], }; } diff --git a/src/routes/apiDirectory.js b/src/routes/apiDirectory.js index 095299613f..2d1e993add 100644 --- a/src/routes/apiDirectory.js +++ b/src/routes/apiDirectory.js @@ -7,6 +7,7 @@ import filesRouter from './files'; import activityReportsRouter from './activityReports'; import usersRouter from './users'; import widgetsRouter from './widgets'; +import granteeRouter from './grantee'; import { userById } from '../services/users'; import { auditLogger } from '../logger'; import handleErrors from '../lib/apiErrorHandler'; @@ -24,8 +25,8 @@ router.use('/admin', adminRouter); router.use('/activity-reports', activityReportsRouter); router.use('/users', usersRouter); router.use('/widgets', widgetsRouter); - router.use('/files', filesRouter); +router.use('/grantee', granteeRouter); router.get('/user', async (req, res) => { const { userId } = req.session; diff --git a/src/routes/grantee/handlers.js b/src/routes/grantee/handlers.js new file mode 100644 index 0000000000..e485b25033 --- /dev/null +++ b/src/routes/grantee/handlers.js @@ -0,0 +1,27 @@ +import { DECIMAL_BASE } from '../../constants'; +import { granteesByNameAndRegion } from '../../services/grantee'; +import handleErrors from '../../lib/apiErrorHandler'; + +const namespace = 'SERVICE:GRANTEE'; + +const logContext = { + namespace, +}; + +// eslint-disable-next-line import/prefer-default-export +export async function searchGrantees(req, res) { + try { + const { s, region, sortBy } = req.query; + const regionId = region ? parseInt(region, DECIMAL_BASE) : null; + const sort = sortBy || 'name'; + + const grantees = await granteesByNameAndRegion(s, regionId, sort); + if (!grantees) { + res.sendStatus(404); + return; + } + res.json(grantees); + } catch (error) { + await handleErrors(req, res, error, logContext); + } +} diff --git a/src/routes/grantee/index.js b/src/routes/grantee/index.js new file mode 100644 index 0000000000..ca15927577 --- /dev/null +++ b/src/routes/grantee/index.js @@ -0,0 +1,9 @@ +import express from 'express'; +import { + searchGrantees, +} from './handlers'; + +const router = express.Router(); +router.get('/search', searchGrantees); + +export default router; diff --git a/src/services/grantee.js b/src/services/grantee.js index a91c0dd1a6..20d4d7ae56 100644 --- a/src/services/grantee.js +++ b/src/services/grantee.js @@ -1,6 +1,6 @@ -import { Grant, Grantee } from '../models'; +import { Op } from 'sequelize'; +import { Grant, Grantee, sequelize } from '../models'; -// eslint-disable-next-line import/prefer-default-export export async function allGrantees() { return Grantee.findAll({ include: [ @@ -12,3 +12,41 @@ export async function allGrantees() { ], }); } + +/** + * + * @param {string} query + * @param {number} regionId + * @param {string} sortBy + * + * @returns {object[]} grantee results + */ +export async function granteesByNameAndRegion(query) { + const q = sequelize.escape(query); + + console.log('[query]', q); + + return Grantee.findAll({ + where: { + // [Op.or]: [ + // { + name: { + [Op.iLike]: q, + }, + // }, + // { + // 'grants.id': { + // [Op.iLike]: q, + // }, + // }, + // ], + }, + include: [ + { + attributes: ['id', 'number', 'regionId'], + model: Grant, + as: 'grants', + }, + ], + }); +} From b8233c1fbe1466792782cb909216e787d23bbcb8 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Fri, 17 Sep 2021 10:36:49 -0400 Subject: [PATCH 03/34] create query for search --- frontend/src/pages/GranteeSearch/index.js | 2 +- src/services/grantee.js | 61 ++++++++++++++++------- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/frontend/src/pages/GranteeSearch/index.js b/frontend/src/pages/GranteeSearch/index.js index 79b1b3fa7e..59f8987640 100644 --- a/frontend/src/pages/GranteeSearch/index.js +++ b/frontend/src/pages/GranteeSearch/index.js @@ -62,7 +62,7 @@ function GranteeSearch({ user }) {
    - {granteeResults.map((grantee) =>

    {grantee.name}

    )} + {granteeResults.map((grantee) =>

    {grantee.name}

    )}
    diff --git a/src/services/grantee.js b/src/services/grantee.js index 20d4d7ae56..1749e0f375 100644 --- a/src/services/grantee.js +++ b/src/services/grantee.js @@ -1,5 +1,5 @@ import { Op } from 'sequelize'; -import { Grant, Grantee, sequelize } from '../models'; +import { Grant, Grantee } from '../models'; export async function allGrantees() { return Grantee.findAll({ @@ -21,31 +21,56 @@ export async function allGrantees() { * * @returns {object[]} grantee results */ -export async function granteesByNameAndRegion(query) { - const q = sequelize.escape(query); - - console.log('[query]', q); - - return Grantee.findAll({ +export async function granteesByNameAndRegion(query, regionId) { + const matchingGrantNumbers = await Grant.findAll({ where: { - // [Op.or]: [ - // { - name: { - [Op.iLike]: q, + number: { + [Op.iLike]: `%${query}%`, // sequelize automatically escapes this + }, + regionId, + }, + include: [ + { + model: Grantee, + as: 'grantee', }, - // }, - // { - // 'grants.id': { - // [Op.iLike]: q, - // }, - // }, - // ], + ], + }); + + let granteeWhere = { + name: { + [Op.iLike]: `%${query}%`, // sequelize automatically escapes this }, + }; + + console.log(matchingGrantNumbers); + + if (matchingGrantNumbers) { + const matchingGrantNumbersGranteeIds = matchingGrantNumbers.map((grant) => grant.grantee.id); + granteeWhere = { + [Op.or]: [ + { + name: { + [Op.iLike]: `%${query}%`, // sequelize automatically escapes this + }, + }, + { + id: matchingGrantNumbersGranteeIds, + }, + ], + }; + } + + return Grantee.findAll({ + where: granteeWhere, include: [ { attributes: ['id', 'number', 'regionId'], model: Grant, as: 'grants', + where: { + regionId, + }, }, ], }); From bc95606222567ee69672746515c9e0d3ced20827 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Fri, 17 Sep 2021 12:28:16 -0400 Subject: [PATCH 04/34] clean up function --- src/services/grantee.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/services/grantee.js b/src/services/grantee.js index 1749e0f375..8c3237cc06 100644 --- a/src/services/grantee.js +++ b/src/services/grantee.js @@ -19,13 +19,17 @@ export async function allGrantees() { * @param {number} regionId * @param {string} sortBy * - * @returns {object[]} grantee results + * @returns {Promise} grantee results */ export async function granteesByNameAndRegion(query, regionId) { + // fix the query + const q = `%${query}%`; + + // first get all grants with numbers that match the query string const matchingGrantNumbers = await Grant.findAll({ where: { number: { - [Op.iLike]: `%${query}%`, // sequelize automatically escapes this + [Op.iLike]: q, // sequelize automatically escapes this }, regionId, }, @@ -37,21 +41,24 @@ export async function granteesByNameAndRegion(query, regionId) { ], }); + // create a base where clause for the grantees matching the name and the query string let granteeWhere = { name: { - [Op.iLike]: `%${query}%`, // sequelize automatically escapes this + [Op.iLike]: q, // sequelize automatically escapes this }, }; - console.log(matchingGrantNumbers); - + // if we have any matching grant numbers if (matchingGrantNumbers) { + // we pull out the grantee ids + // and include them in the where clause, so either + // the grant number or the grant name matches the query string const matchingGrantNumbersGranteeIds = matchingGrantNumbers.map((grant) => grant.grantee.id); granteeWhere = { [Op.or]: [ { name: { - [Op.iLike]: `%${query}%`, // sequelize automatically escapes this + [Op.iLike]: q, // sequelize automatically escapes this }, }, { From 9010f82758ac23e205db951296150d5ac2011889 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Fri, 17 Sep 2021 12:28:27 -0400 Subject: [PATCH 05/34] add unit test --- src/services/grantee.test.js | 88 +++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/src/services/grantee.test.js b/src/services/grantee.test.js index f6961a1711..f985d73d4e 100644 --- a/src/services/grantee.test.js +++ b/src/services/grantee.test.js @@ -1,5 +1,5 @@ -import db, { Grantee } from '../models'; -import { allGrantees } from './grantee'; +import db, { Grantee, Grant } from '../models'; +import { allGrantees, granteesByNameAndRegion } from './grantee'; describe('Grantee DB service', () => { afterEach(() => { @@ -42,4 +42,88 @@ describe('Grantee DB service', () => { expect(foundIds).toContain(62); }); }); + + describe('granteesByNameAndRegion', () => { + const grantees = [ + { + id: 63, + name: 'Apple', + }, + { + id: 64, + name: 'Orange', + }, + { + id: 65, + name: 'Banana', + }, + ]; + + const grants = [ + { + id: 50, + granteeId: 63, + regionId: 1, + number: '12345', + }, + { + id: 51, + granteeId: 63, + regionId: 1, + number: '12346', + }, + { + id: 52, + granteeId: 64, + regionId: 1, + number: '55557', + }, + { + id: 53, + granteeId: 64, + regionId: 1, + number: '55558', + }, + { + id: 54, + granteeId: 65, + regionId: 1, + number: '12349', + }, + { + id: 55, + granteeId: 65, + regionId: 2, + number: '12350', + }, + ]; + + beforeEach(async () => { + await Promise.all(grantees.map((g) => Grantee.create(g))); + await Promise.all(grants.map((g) => Grant.create(g))); + }); + + afterEach(async () => { + await Grant.destroy({ where: { granteeId: grantees.map((g) => g.id) } }); + await Grantee.destroy({ where: { id: grantees.map((g) => g.id) } }); + }); + + it('finds based on grantee name', async () => { + const foundGrantees = await granteesByNameAndRegion('apple', 1); + expect(foundGrantees.length).toBe(1); + expect(foundGrantees.map((g) => g.id)).toContain(63); + }); + + it('finds based on grantee id', async () => { + const foundGrantees = await granteesByNameAndRegion('5555', 1); + expect(foundGrantees.length).toBe(1); + expect(foundGrantees.map((g) => g.id)).toContain(64); + }); + + it('finds based on region', async () => { + const foundGrantees = await granteesByNameAndRegion('banana', 2); + expect(foundGrantees.length).toBe(1); + expect(foundGrantees.map((g) => g.id)).toContain(65); + }); + }); }); From b67c34c9fd31ef5c1a205d0057b2e34f43576af5 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Fri, 17 Sep 2021 12:44:45 -0400 Subject: [PATCH 06/34] add size limit to query --- src/services/grantee.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/grantee.js b/src/services/grantee.js index 8c3237cc06..8c048ebe4d 100644 --- a/src/services/grantee.js +++ b/src/services/grantee.js @@ -80,5 +80,6 @@ export async function granteesByNameAndRegion(query, regionId) { }, }, ], + limit: 12, }); } From a6663ced57a7368adce9fe3f43ab63e5a76b782c Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Fri, 17 Sep 2021 13:11:14 -0400 Subject: [PATCH 07/34] fix broken ui test --- frontend/src/pages/GranteeSearch/__tests__/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/GranteeSearch/__tests__/index.js b/frontend/src/pages/GranteeSearch/__tests__/index.js index f4fb183bfc..59f3c82c58 100644 --- a/frontend/src/pages/GranteeSearch/__tests__/index.js +++ b/frontend/src/pages/GranteeSearch/__tests__/index.js @@ -39,7 +39,7 @@ describe('the grantee search page', () => { }; renderGranteeSearch(user); expect(screen.getByRole('heading', { name: /grantee records/i })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /open the regional select menu/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /open regional select menu/i })).toBeInTheDocument(); }); it('you can interact with the search box', () => { From a5fbd4b17d2d81aefc0438ccfa5082e2e6d5b967 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Fri, 17 Sep 2021 15:14:22 -0400 Subject: [PATCH 08/34] update ui test for new features --- .../pages/GranteeSearch/__tests__/index.js | 55 ++++++++++++++++--- frontend/src/pages/GranteeSearch/index.js | 2 +- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/frontend/src/pages/GranteeSearch/__tests__/index.js b/frontend/src/pages/GranteeSearch/__tests__/index.js index 59f3c82c58..f7a70638b8 100644 --- a/frontend/src/pages/GranteeSearch/__tests__/index.js +++ b/frontend/src/pages/GranteeSearch/__tests__/index.js @@ -2,25 +2,36 @@ import '@testing-library/jest-dom'; import React from 'react'; import { fireEvent, - render, screen, + render, + screen, + waitFor, } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import fetchMock from 'fetch-mock'; +import join from 'url-join'; +import { act } from 'react-dom/test-utils'; import GranteeSearch from '../index'; import { SCOPE_IDS } from '../../../Constants'; const userBluePrint = { id: 1, name: 'One', - role: 'Taco Alphabetizer', + role: ['Taco Alphabetizer'], homeRegionId: 1, permissions: [], }; +const granteeUrl = join('/', 'api', 'grantee'); + describe('the grantee search page', () => { const renderGranteeSearch = (user) => { render(); }; + afterEach(() => { + jest.clearAllMocks(); + }); + it('renders the heading and the region select', async () => { const user = { ...userBluePrint, @@ -42,18 +53,44 @@ describe('the grantee search page', () => { expect(screen.getByRole('button', { name: /open regional select menu/i })).toBeInTheDocument(); }); - it('you can interact with the search box', () => { - const user = { ...userBluePrint }; + it('the search bar works', async () => { + const user = { + ...userBluePrint, + permissions: [ + { + userId: 1, + scopeId: SCOPE_IDS.READ_WRITE_ACTIVITY_REPORTS, + regionId: 1, + }, + { + userId: 1, + scopeId: SCOPE_IDS.READ_WRITE_ACTIVITY_REPORTS, + regionId: 2, + }, + ], + }; + + const res = [{ + id: 2, name: 'to major tom', + }]; + renderGranteeSearch(user); + const query = 'ground control'; + const url = join(granteeUrl, 'search', `?s=${encodeURIComponent(query)}`, '®ion=1'); + fetchMock.get(url, res); + const searchBox = screen.getByRole('searchbox'); const button = screen.getByRole('button', { name: /search for matching grantees/i }); - userEvent.type(searchBox, 'ground control?'); - fireEvent.click(button); - - // Todo - once there is more ui, this should be expanded - // to test what actually happens when the button is clicked expect(button).toBeInTheDocument(); expect(searchBox).toBeInTheDocument(); + + userEvent.type(searchBox, query); + + await act(async () => { + fireEvent.click(button); + }); + + await waitFor(() => expect(screen.getByText(res[0].name)).toBeInTheDocument()); }); }); diff --git a/frontend/src/pages/GranteeSearch/index.js b/frontend/src/pages/GranteeSearch/index.js index 59f8987640..873c2d3547 100644 --- a/frontend/src/pages/GranteeSearch/index.js +++ b/frontend/src/pages/GranteeSearch/index.js @@ -62,7 +62,7 @@ function GranteeSearch({ user }) {
    - {granteeResults.map((grantee) =>

    {grantee.name}

    )} + {granteeResults.map((grantee) =>

    {grantee.name}

    )}
    From 6f99e79ccfcdaa41c4e0610a33ea7cb58ae98354 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Fri, 17 Sep 2021 15:50:19 -0400 Subject: [PATCH 09/34] add another ui test --- frontend/src/pages/GranteeSearch/__tests__/index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/GranteeSearch/__tests__/index.js b/frontend/src/pages/GranteeSearch/__tests__/index.js index f7a70638b8..6f6338f9b8 100644 --- a/frontend/src/pages/GranteeSearch/__tests__/index.js +++ b/frontend/src/pages/GranteeSearch/__tests__/index.js @@ -76,7 +76,7 @@ describe('the grantee search page', () => { renderGranteeSearch(user); const query = 'ground control'; - const url = join(granteeUrl, 'search', `?s=${encodeURIComponent(query)}`, '®ion=1'); + const url = join(granteeUrl, 'search', `?s=${encodeURIComponent(query)}`, '®ion=2'); fetchMock.get(url, res); const searchBox = screen.getByRole('searchbox'); @@ -85,6 +85,12 @@ describe('the grantee search page', () => { expect(button).toBeInTheDocument(); expect(searchBox).toBeInTheDocument(); + const regionalSelect = screen.getByRole('button', { name: /open regional select menu/i }); + fireEvent.click(regionalSelect); + const region2 = screen.getByRole('button', { name: /select to view data from region 2\. select apply filters button to apply selection/i }); + fireEvent.click(region2); + const applyFilters = screen.getByRole('button', { name: /apply filters for the regional select menu/i }); + fireEvent.click(applyFilters); userEvent.type(searchBox, query); await act(async () => { From 9f147b6dc7b64951b051600fb0c9debc712a0a35 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Mon, 20 Sep 2021 08:26:29 -0400 Subject: [PATCH 10/34] beefed up unit test --- .../Pages/components/__tests__/GoalPicker.js | 22 ++++++--- .../pages/GranteeSearch/__tests__/index.js | 49 ++++++++++++++++++- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js index 187a3abb76..6fd631f56d 100644 --- a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js +++ b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js @@ -65,7 +65,8 @@ describe('GoalPicker', () => { const select = await screen.findByText('Select goal(s) or type here to create a new goal'); userEvent.type(select, 'Unfettered'); - fireEvent.click(document.querySelector('#react-select-3-option-2')); + const newGoal = await screen.findByText('Create "Unfettered"'); + fireEvent.click(newGoal); expect(screen.getByText(/unfettered/i)).toBeInTheDocument(); }); @@ -80,12 +81,18 @@ describe('GoalPicker', () => { const select = await screen.findByText('Select goal(s) or type here to create a new goal'); userEvent.type(select, 'Unfettered'); - fireEvent.click(document.querySelector('#react-select-4-option-2')); - const unfetteredlabel = screen.getByText(/unfettered/i); + + const newGoal = await screen.findByText('Create "Unfettered"'); + fireEvent.click(newGoal); + let unfetteredlabel = await screen.findByText(/unfettered/i); expect(unfetteredlabel).toBeInTheDocument(); - userEvent.type(select, 'a'); - const unfett = document.querySelector('#react-select-4-option-0'); - fireEvent.click(unfett); + + selectEvent.openMenu(select); + // Ignore the "Goal: Unfettered" element that isn't in the multi-select menu + const selected = await screen.findByText(/unfettered/i, { ignore: 'p' }); + userEvent.click(selected); + + unfetteredlabel = screen.queryByText(/unfettered/i); expect(unfetteredlabel).not.toBeInTheDocument(); }); @@ -100,7 +107,8 @@ describe('GoalPicker', () => { const select = await screen.findByText('Select goal(s) or type here to create a new goal'); userEvent.type(select, 'Unfettered'); - fireEvent.click(document.querySelector('#react-select-5-option-2')); + const newGoal = await screen.findByText('Create "Unfettered"'); + fireEvent.click(newGoal); const menuButton = await screen.findByRole('button', { name: /actions for goal 1/i }); await waitFor(() => expect(menuButton).toBeVisible()); diff --git a/frontend/src/pages/GranteeSearch/__tests__/index.js b/frontend/src/pages/GranteeSearch/__tests__/index.js index 6f6338f9b8..39538aa6b5 100644 --- a/frontend/src/pages/GranteeSearch/__tests__/index.js +++ b/frontend/src/pages/GranteeSearch/__tests__/index.js @@ -53,9 +53,10 @@ describe('the grantee search page', () => { expect(screen.getByRole('button', { name: /open regional select menu/i })).toBeInTheDocument(); }); - it('the search bar works', async () => { + it('the regional select works', async () => { const user = { ...userBluePrint, + homeRegionId: 14, permissions: [ { userId: 1, @@ -71,7 +72,7 @@ describe('the grantee search page', () => { }; const res = [{ - id: 2, name: 'to major tom', + id: 2, name: 'major tom', }]; renderGranteeSearch(user); @@ -82,6 +83,10 @@ describe('the grantee search page', () => { const searchBox = screen.getByRole('searchbox'); const button = screen.getByRole('button', { name: /search for matching grantees/i }); + fireEvent.click(button); + + expect(fetchMock.called()).toBe(false); + expect(button).toBeInTheDocument(); expect(searchBox).toBeInTheDocument(); @@ -99,4 +104,44 @@ describe('the grantee search page', () => { await waitFor(() => expect(screen.getByText(res[0].name)).toBeInTheDocument()); }); + + it('the search bar works', async () => { + const user = { + ...userBluePrint, + permissions: [ + { + userId: 1, + scopeId: SCOPE_IDS.READ_WRITE_ACTIVITY_REPORTS, + regionId: 1, + }, + { + userId: 1, + scopeId: SCOPE_IDS.READ_WRITE_ACTIVITY_REPORTS, + regionId: 2, + }, + ], + }; + + const res = [{ + id: 2, name: 'major tom', + }]; + + renderGranteeSearch(user); + const query = 'your circuits dead'; + const url = join(granteeUrl, 'search', `?s=${encodeURIComponent(query)}`, '®ion=1'); + fetchMock.get(url, res); + + const searchBox = screen.getByRole('searchbox'); + const button = screen.getByRole('button', { name: /search for matching grantees/i }); + + expect(button).toBeInTheDocument(); + expect(searchBox).toBeInTheDocument(); + userEvent.type(searchBox, query); + + await act(async () => { + fireEvent.click(button); + }); + + await waitFor(() => expect(screen.getByText(res[0].name)).toBeInTheDocument()); + }); }); From a8f95ab5f0e317c002a130bd3bd4efc4e8561e89 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Mon, 20 Sep 2021 08:50:34 -0400 Subject: [PATCH 11/34] beefed up second unit test --- .../RegionalDashboard/__tests__/index.js | 33 +++++++++++++++++++ frontend/src/pages/RegionalDashboard/index.js | 4 +-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/RegionalDashboard/__tests__/index.js b/frontend/src/pages/RegionalDashboard/__tests__/index.js index 2510da375f..4c6a7fa86a 100644 --- a/frontend/src/pages/RegionalDashboard/__tests__/index.js +++ b/frontend/src/pages/RegionalDashboard/__tests__/index.js @@ -7,6 +7,7 @@ import fetchMock from 'fetch-mock'; import join from 'url-join'; import RegionalDashboard from '../index'; import formatDateRange from '../formatDateRange'; +import { SCOPE_IDS } from '../../../Constants'; describe('Regional Dashboard page', () => { const renderDashboard = (user) => render(); @@ -30,6 +31,38 @@ describe('Regional Dashboard page', () => { expect(dateRange).toBeInTheDocument(); }); + it('shows the selected region', async () => { + renderDashboard({ + ...user, + permissions: [ + { + regionId: 1, + scopeId: SCOPE_IDS.READ_ACTIVITY_REPORTS, + }, + { + regionId: 2, + scopeId: SCOPE_IDS.READ_ACTIVITY_REPORTS, + }, + { + regionId: 14, + scopeId: SCOPE_IDS.READ_ACTIVITY_REPORTS, + }, + ], + }); + + expect(screen.getByText('Regional TTA Activity Dashboard')).toBeInTheDocument(); + const button = screen.getByRole('button', { name: 'Open regional select menu' }); + fireEvent.click(button); + + const region1 = screen.getByRole('button', { name: 'Select to view data from Region 1. Select Apply filters button to apply selection' }); + fireEvent.click(region1); + + const apply = screen.getByRole('button', { name: 'Apply filters for the regional select menu' }); + fireEvent.click(apply); + + expect(screen.getByText('Region 1 TTA Activity Dashboard')).toBeInTheDocument(); + }); + it('shows the currently selected date range', async () => { renderDashboard(user); diff --git a/frontend/src/pages/RegionalDashboard/index.js b/frontend/src/pages/RegionalDashboard/index.js index 3b228b83d9..4f532530b7 100644 --- a/frontend/src/pages/RegionalDashboard/index.js +++ b/frontend/src/pages/RegionalDashboard/index.js @@ -97,7 +97,7 @@ export default function RegionalDashboard({ user }) { }, [selectedDateRangeOption]); const onApplyRegion = (region) => { - const regionId = region ? region.value : appliedRegion; + const regionId = region.value; updateAppliedRegion(regionId); }; @@ -106,7 +106,7 @@ export default function RegionalDashboard({ user }) { }; const onApplyDateRange = (range) => { - const rangeId = range ? range.value : selectedDateRangeOption; + const rangeId = range.value; updateSelectedDateRangeOption(rangeId); }; From 186036d03fae4cb470c9afab136bb5f8774a8723 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Fri, 24 Sep 2021 11:15:05 -0400 Subject: [PATCH 12/34] build out GranteeResults --- .../components/GranteeResults.js | 134 ++++++++++++++++++ frontend/src/pages/GranteeSearch/index.js | 5 +- 2 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 frontend/src/pages/GranteeSearch/components/GranteeResults.js diff --git a/frontend/src/pages/GranteeSearch/components/GranteeResults.js b/frontend/src/pages/GranteeSearch/components/GranteeResults.js new file mode 100644 index 0000000000..4a04be3361 --- /dev/null +++ b/frontend/src/pages/GranteeSearch/components/GranteeResults.js @@ -0,0 +1,134 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { Table } from '@trussworks/react-uswds'; +import Pagination from 'react-js-pagination'; +import Container from '../../../components/Container'; +import { renderTotal } from '../../Landing'; + +export default function GranteeResults( + { + region, + grantees, + loading, + activePage, + offset, + perPage, + count, + handlePageChange, + requestSort, + sortConfig, + }, +) { + const getClassNamesFor = (name) => (sortConfig.sortBy === name ? sortConfig.direction : ''); + const renderColumnHeader = (displayName, name) => { + const sortClassName = getClassNamesFor(name); + let fullAriaSort; + switch (sortClassName) { + case 'asc': + fullAriaSort = 'ascending'; + break; + case 'desc': + fullAriaSort = 'descending'; + break; + default: + fullAriaSort = 'none'; + break; + } + return ( + + + + ); + }; + + return ( + + + + + {renderTotal(offset, perPage, activePage, count)} + + + + +
    + + + + + {renderColumnHeader('Region', 'regionId')} + {renderColumnHeader('Grantee Name', 'name')} + {renderColumnHeader('Program Specialist', 'programSpecialist')} + + + + {grantees.map((grantee) => ( + + + + + + ))} + +
    + Grantees +

    with sorting and pagination

    +
    {region}{grantee.name}{grantee.programSpecialist}
    +
    +
    + ); +} + +GranteeResults.propTypes = { + region: PropTypes.number.isRequired, + grantees: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string, + id: PropTypes.number, + programSpecialist: PropTypes.string, + })).isRequired, + loading: PropTypes.bool.isRequired, + activePage: PropTypes.number.isRequired, + offset: PropTypes.number.isRequired, + perPage: PropTypes.number.isRequired, + count: PropTypes.number.isRequired, + handlePageChange: PropTypes.func.isRequired, + requestSort: PropTypes.func.isRequired, + sortConfig: PropTypes.shape({ + sortBy: PropTypes.string, + direction: PropTypes.string, + }).isRequired, +}; diff --git a/frontend/src/pages/GranteeSearch/index.js b/frontend/src/pages/GranteeSearch/index.js index 873c2d3547..58cf9f52c8 100644 --- a/frontend/src/pages/GranteeSearch/index.js +++ b/frontend/src/pages/GranteeSearch/index.js @@ -13,7 +13,6 @@ function GranteeSearch({ user }) { const hasCentralOffice = user && user.homeRegionId && user.homeRegionId === 14; const regions = getUserRegions(user); - // eslint-disable-next-line max-len const [appliedRegion, setAppliedRegion] = useState(hasCentralOffice ? 14 : regions[0]); const [query, setQuery] = useState(''); const [granteeResults, setGranteeResults] = useState([]); @@ -61,9 +60,9 @@ function GranteeSearch({ user }) { -
    +
    {granteeResults.map((grantee) =>

    {grantee.name}

    )} -
    + ); From f4cab7ba7f694a91c5c351d7c8199a7f0469a050 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Fri, 24 Sep 2021 17:08:06 -0400 Subject: [PATCH 13/34] add grantee results table --- frontend/src/Constants.js | 1 + frontend/src/fetchers/grantee.js | 25 +++---- .../components/GranteeResults.css | 6 ++ .../components/GranteeResults.js | 57 +++++++------- frontend/src/pages/GranteeSearch/index.js | 75 ++++++++++++++++--- src/constants.js | 1 + src/lib/orderGranteesBy.js | 33 ++++++++ src/routes/grantee/handlers.js | 7 +- src/services/grantee.js | 14 +++- 9 files changed, 160 insertions(+), 59 deletions(-) create mode 100644 frontend/src/pages/GranteeSearch/components/GranteeResults.css create mode 100644 src/lib/orderGranteesBy.js diff --git a/frontend/src/Constants.js b/frontend/src/Constants.js index 6604b57bf0..6171a0b824 100644 --- a/frontend/src/Constants.js +++ b/frontend/src/Constants.js @@ -84,6 +84,7 @@ export const REPORT_STATUSES = { export const REPORTS_PER_PAGE = 10; export const ALERTS_PER_PAGE = 10; +export const GRANTEES_PER_PAGE = 12; export const GOVERNMENT_HOSTNAME_EXTENSION = '.ohs.acf.hhs.gov'; export const ESCAPE_KEY_CODE = 27; diff --git a/frontend/src/fetchers/grantee.js b/frontend/src/fetchers/grantee.js index b3d7260e5d..4f5fa86dfb 100644 --- a/frontend/src/fetchers/grantee.js +++ b/frontend/src/fetchers/grantee.js @@ -7,22 +7,17 @@ import { DECIMAL_BASE } from '../Constants'; const granteeUrl = join('/', 'api', 'grantee'); // eslint-disable-next-line import/prefer-default-export -export const searchGrantees = async (query, regionId = '') => { - try { - if (!query) { - throw new Error('Please provide a query string to search grantees'); - } +export const searchGrantees = async (query, regionId = '', params = { sortBy: 'name', direction: 'asc', offset: 0 }) => { + if (!query) { + throw new Error('Please provide a query string to search grantees'); + } - const querySearch = `?s=${query}`; - const regionSearch = regionId ? `®ion=${regionId.toString(DECIMAL_BASE)}` : ''; + const querySearch = `?s=${query}`; + const regionSearch = regionId ? `®ion=${regionId.toString(DECIMAL_BASE)}` : ''; - const grantees = await get( - join(granteeUrl, 'search', querySearch, regionSearch), - ); + const grantees = await get( + join(granteeUrl, 'search', querySearch, regionSearch, `&sortBy=${params.sortBy}&direction=${params.direction}&offset=${params.offset}`), + ); - return grantees.json(); - } catch (e) { - // todo - this should probably not throw an error, so it doesn't crash the frontend on a 404 - throw new Error(e); - } + return grantees.json(); }; diff --git a/frontend/src/pages/GranteeSearch/components/GranteeResults.css b/frontend/src/pages/GranteeSearch/components/GranteeResults.css new file mode 100644 index 0000000000..5c4550672c --- /dev/null +++ b/frontend/src/pages/GranteeSearch/components/GranteeResults.css @@ -0,0 +1,6 @@ +.ttahub-grantee-results .usa-table .usa-button--unstyled { + color: black; + font-size: 1em; + font-weight: bold; + text-decoration: none; +} \ No newline at end of file diff --git a/frontend/src/pages/GranteeSearch/components/GranteeResults.js b/frontend/src/pages/GranteeSearch/components/GranteeResults.js index 4a04be3361..b1ad9b2500 100644 --- a/frontend/src/pages/GranteeSearch/components/GranteeResults.js +++ b/frontend/src/pages/GranteeSearch/components/GranteeResults.js @@ -1,10 +1,10 @@ import React from 'react'; import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; -import { Table } from '@trussworks/react-uswds'; import Pagination from 'react-js-pagination'; import Container from '../../../components/Container'; import { renderTotal } from '../../Landing'; +import './GranteeResults.css'; export default function GranteeResults( { @@ -43,8 +43,7 @@ export default function GranteeResults( onClick={() => { requestSort(name); }} - onKeyPress={() => requestSort(name)} - className={`sortable ${sortClassName}`} + className={`usa-button usa-button--unstyled sortable ${sortClassName}`} aria-label={`${displayName}. Activate to sort ${ sortClassName === 'asc' ? 'descending' : 'ascending' }`} @@ -56,7 +55,7 @@ export default function GranteeResults( }; return ( - + -
    - - - - - {renderColumnHeader('Region', 'regionId')} - {renderColumnHeader('Grantee Name', 'name')} - {renderColumnHeader('Program Specialist', 'programSpecialist')} +
    - Grantees -

    with sorting and pagination

    -
    + + + + {renderColumnHeader('Region', 'regionId')} + {renderColumnHeader('Grantee Name', 'name')} + {renderColumnHeader('Program Specialist', 'programSpecialist')} + + + + {grantees.map((grantee) => ( + + + + - - - {grantees.map((grantee) => ( - - - - - - ))} - -
    + Grantees +

    with sorting and pagination

    +
    {region}{grantee.name}{Array.from(new Set(grantee.grants.map((grant) => grant.programSpecialistName))).join('\n')}
    {region}{grantee.name}{grantee.programSpecialist}
    -
    + ))} + +
    ); } @@ -119,7 +116,7 @@ GranteeResults.propTypes = { name: PropTypes.string, id: PropTypes.number, programSpecialist: PropTypes.string, - })).isRequired, + })), loading: PropTypes.bool.isRequired, activePage: PropTypes.number.isRequired, offset: PropTypes.number.isRequired, @@ -132,3 +129,7 @@ GranteeResults.propTypes = { direction: PropTypes.string, }).isRequired, }; + +GranteeResults.defaultProps = { + grantees: [], +}; diff --git a/frontend/src/pages/GranteeSearch/index.js b/frontend/src/pages/GranteeSearch/index.js index 58cf9f52c8..b4174c3be0 100644 --- a/frontend/src/pages/GranteeSearch/index.js +++ b/frontend/src/pages/GranteeSearch/index.js @@ -5,8 +5,10 @@ import { Grid } from '@trussworks/react-uswds'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSearch } from '@fortawesome/free-solid-svg-icons'; import RegionalSelect from '../../components/RegionalSelect'; +import GranteeResults from './components/GranteeResults'; import { getUserRegions } from '../../permissions'; import { searchGrantees } from '../../fetchers/grantee'; +import { GRANTEES_PER_PAGE } from '../../Constants'; import './index.css'; function GranteeSearch({ user }) { @@ -14,22 +16,66 @@ function GranteeSearch({ user }) { const regions = getUserRegions(user); const [appliedRegion, setAppliedRegion] = useState(hasCentralOffice ? 14 : regions[0]); + const [granteeCount, setGranteeCount] = useState(0); const [query, setQuery] = useState(''); - const [granteeResults, setGranteeResults] = useState([]); + const [results, setResults] = useState([]); + const [activePage, setActivePage] = useState(1); + const [offset, setOffset] = useState(0); + const [loading, setLoading] = useState(false); + const [sortConfig, setSortConfig] = useState({ + sortBy: 'name', + direction: 'desc', + }); + + async function fetchGrantees() { + if (!query || loading) { + return; + } + + try { + setLoading(true); + const { rows, count } = await searchGrantees(query, appliedRegion, { ...sortConfig, offset }); + setResults(rows); + setGranteeCount(count); + } catch (err) { + // eslint-disable-next-line no-console + console.log(err); + setResults([]); + setGranteeCount(0); + } finally { + setLoading(false); + } + } function onApplyRegion(region) { setAppliedRegion(region.value); } - async function onSubmit(e) { - e.preventDefault(); - - if (!query) { + async function requestSort(sortBy) { + const config = sortConfig; + if (config.sortBy === sortBy) { + config.direction = config.direction === 'asc' ? 'desc' : 'asc'; + setSortConfig(config); + await fetchGrantees(); return; } - const results = await searchGrantees(query, appliedRegion); - setGranteeResults(results); + config.sortBy = sortBy; + setSortConfig(config); + await fetchGrantees(); + } + + async function handlePageChange(pageNumber) { + if (!loading) { + setActivePage(pageNumber); + setOffset((pageNumber - 1) * GRANTEES_PER_PAGE); + } + await fetchGrantees(); + } + + async function onSubmit(e) { + e.preventDefault(); + await fetchGrantees(); } return ( @@ -53,7 +99,7 @@ function GranteeSearch({ user }) { )}
    setQuery(e.target.value)} /> -
    diff --git a/frontend/src/pages/GranteeSearch/components/__tests__/GranteeResults.js b/frontend/src/pages/GranteeSearch/components/__tests__/GranteeResults.js new file mode 100644 index 0000000000..effbf06ecc --- /dev/null +++ b/frontend/src/pages/GranteeSearch/components/__tests__/GranteeResults.js @@ -0,0 +1,112 @@ +import '@testing-library/jest-dom'; +import React from 'react'; +import { + render, + fireEvent, + screen, +} from '@testing-library/react'; +import { Router } from 'react-router'; +import { createMemoryHistory } from 'history'; + +import GranteeResults from '../GranteeResults'; + +const history = createMemoryHistory(); + +const grantees = [ + { + id: 11, + name: 'Agency 2 in region 1, Inc.', + createdAt: '2021-09-21T19:16:15.842Z', + updatedAt: '2021-09-21T19:16:15.842Z', + grants: [ + { + id: 12, + number: '09HP01111', + regionId: 1, + programSpecialistName: 'Candyman', + }, + ], + }, + { + id: 10, + name: 'Agency 1.b in region 1, Inc.', + createdAt: '2021-09-21T19:16:15.842Z', + updatedAt: '2021-09-21T19:16:15.842Z', + grants: [ + { + id: 11, + number: '01HP022222', + regionId: 1, + programSpecialistName: null, + }, + ], + }, + { + id: 9, + name: 'Agency 1.a in region 1, Inc.', + createdAt: '2021-09-21T19:16:15.842Z', + updatedAt: '2021-09-21T19:16:15.842Z', + grants: [ + { + id: 10, + number: '01HP044444', + regionId: 1, + programSpecialistName: null, + }, + ], + }, +]; + +describe('Grantee Search > GranteeResults', () => { + const renderGranteeResults = (handlePageChange, requestSort, loading = false) => ( + render( + + + , + ) + ); + + afterEach(() => jest.clearAllMocks()); + + it('renders the component', async () => { + const handlePageChange = jest.fn(); + const requestSort = jest.fn(); + renderGranteeResults(handlePageChange, requestSort); + expect(screen.getByRole('button', { name: /region\. activate to sort ascending/i })).toBeInTheDocument(); + expect(screen.getByRole('link', { + name: /agency 1\.a in region 1, inc\./i, + })).toBeInTheDocument(); + expect(screen.getByText('Candyman')).toBeInTheDocument(); + }); + + it('calls the sort function', async () => { + const handlePageChange = jest.fn(); + const requestSort = jest.fn(); + renderGranteeResults(handlePageChange, requestSort); + const button = screen.getByRole('button', { name: /program specialist\. activate to sort ascending/i }); + fireEvent.click(button); + expect(requestSort).toHaveBeenCalledWith('programSpecialist'); + }); + + it('disables the buttons on loading', async () => { + const handlePageChange = jest.fn(); + const requestSort = jest.fn(); + renderGranteeResults(handlePageChange, requestSort, true); + const button = screen.getByRole('button', { name: /program specialist\. activate to sort ascending/i }); + expect(button).toBeDisabled(); + }); +}); From caa163f8c679479bd0d9eabbc2414d360fbc5d88 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Mon, 27 Sep 2021 09:27:06 -0400 Subject: [PATCH 16/34] backend unit tests --- src/lib/orderGranteesBy.test.js | 33 ++++++++++++++++++ src/services/grantee.test.js | 61 +++++++++++++++++++++++++++------ 2 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 src/lib/orderGranteesBy.test.js diff --git a/src/lib/orderGranteesBy.test.js b/src/lib/orderGranteesBy.test.js new file mode 100644 index 0000000000..7ca3f604e6 --- /dev/null +++ b/src/lib/orderGranteesBy.test.js @@ -0,0 +1,33 @@ +import orderGranteesBy from './orderGranteesBy'; + +describe('orderGranteesBy', () => { + it('returns the correct values', () => { + const one = orderGranteesBy('name', 'asc'); + expect(one).toStrictEqual([[ + 'name', + 'asc', + ]]); + + const two = orderGranteesBy('regionId', 'desc'); + expect(two).toStrictEqual([ + [ + 'grants', 'regionId', 'desc', + ], + [ + 'id', + 'desc', + ]]); + + const three = orderGranteesBy('', 'asc'); + + expect(three).toStrictEqual([ + [ + 'grants', 'programSpecialistName', 'asc', + ], + ]); + + const four = orderGranteesBy('sorcery', 'asc'); + + expect(four).toStrictEqual(''); + }); +}); diff --git a/src/services/grantee.test.js b/src/services/grantee.test.js index f985d73d4e..7317463af7 100644 --- a/src/services/grantee.test.js +++ b/src/services/grantee.test.js @@ -47,7 +47,7 @@ describe('Grantee DB service', () => { const grantees = [ { id: 63, - name: 'Apple', + name: 'Apple Juice', }, { id: 64, @@ -57,6 +57,10 @@ describe('Grantee DB service', () => { id: 65, name: 'Banana', }, + { + id: 66, + name: 'Apple Sauce', + }, ]; const grants = [ @@ -65,36 +69,49 @@ describe('Grantee DB service', () => { granteeId: 63, regionId: 1, number: '12345', + programSpecialistName: 'George', }, { id: 51, granteeId: 63, regionId: 1, number: '12346', + programSpecialistName: 'Belle', }, { id: 52, granteeId: 64, regionId: 1, number: '55557', + programSpecialistName: 'Caesar', }, { id: 53, granteeId: 64, regionId: 1, number: '55558', + programSpecialistName: 'Doris', }, { id: 54, granteeId: 65, regionId: 1, number: '12349', + programSpecialistName: 'Eugene', }, { id: 55, granteeId: 65, regionId: 2, number: '12350', + programSpecialistName: 'Farrah', + }, + { + id: 56, + granteeId: 66, + regionId: 1, + number: '12351', + programSpecialistName: 'Aaron', }, ]; @@ -109,21 +126,45 @@ describe('Grantee DB service', () => { }); it('finds based on grantee name', async () => { - const foundGrantees = await granteesByNameAndRegion('apple', 1); - expect(foundGrantees.length).toBe(1); - expect(foundGrantees.map((g) => g.id)).toContain(63); + const foundGrantees = await granteesByNameAndRegion('apple', 1, 'name', 'asc', 0); + expect(foundGrantees.rows.length).toBe(2); + expect(foundGrantees.rows.map((g) => g.id)).toContain(63); }); it('finds based on grantee id', async () => { - const foundGrantees = await granteesByNameAndRegion('5555', 1); - expect(foundGrantees.length).toBe(1); - expect(foundGrantees.map((g) => g.id)).toContain(64); + const foundGrantees = await granteesByNameAndRegion('5555', 1, 'name', 'asc', 0); + expect(foundGrantees.rows.length).toBe(1); + expect(foundGrantees.rows.map((g) => g.id)).toContain(64); }); it('finds based on region', async () => { - const foundGrantees = await granteesByNameAndRegion('banana', 2); - expect(foundGrantees.length).toBe(1); - expect(foundGrantees.map((g) => g.id)).toContain(65); + const foundGrantees = await granteesByNameAndRegion('banana', 2, 'name', 'asc', 0); + expect(foundGrantees.rows.length).toBe(1); + expect(foundGrantees.rows.map((g) => g.id)).toContain(65); + }); + + it('sorts based on name', async () => { + const foundGrantees = await granteesByNameAndRegion('apple', 1, 'name', 'asc', 0); + expect(foundGrantees.rows.length).toBe(2); + expect(foundGrantees.rows.map((g) => g.id)).toStrictEqual([63, 66]); + }); + + it('sorts based on program specialist', async () => { + const foundGrantees = await granteesByNameAndRegion('apple', 1, 'programSpecialist', 'asc', 0); + expect(foundGrantees.rows.length).toBe(2); + expect(foundGrantees.rows.map((g) => g.id)).toStrictEqual([66, 63]); + }); + + it('respects sort order', async () => { + const foundGrantees = await granteesByNameAndRegion('apple', 1, 'name', 'desc', 0); + expect(foundGrantees.rows.length).toBe(2); + expect(foundGrantees.rows.map((g) => g.id)).toStrictEqual([66, 63]); + }); + + it('respects the offset passed in', async () => { + const foundGrantees = await granteesByNameAndRegion('apple', 1, 'name', 'asc', 1); + expect(foundGrantees.rows.length).toBe(1); + expect(foundGrantees.rows.map((g) => g.id)).toStrictEqual([66]); }); }); }); From a197ed2fa84563ef48ce6bc7cc27fba0ec6cdacc Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Mon, 27 Sep 2021 09:57:49 -0400 Subject: [PATCH 17/34] update yarn vuln --- frontend/yarn-audit-known-issues | 22 +++++++++++----------- yarn-audit-known-issues | 6 +++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/yarn-audit-known-issues b/frontend/yarn-audit-known-issues index c12a9fcbf8..2ade72799d 100644 --- a/frontend/yarn-audit-known-issues +++ b/frontend/yarn-audit-known-issues @@ -1,11 +1,11 @@ -{"type":"auditAdvisory","data":{"resolution":{"id":1779,"path":"react-scripts>terser-webpack-plugin>cacache>tar","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"6.1.4","paths":["react-scripts>terser-webpack-plugin>cacache>tar"]},{"version":"2.2.2","paths":["react-js-pagination>tar"]}],"id":1779,"created":"2021-08-31T16:10:07.868Z","updated":"2021-08-31T16:11:58.986Z","deleted":null,"title":"Arbitrary File Creation/Overwrite via insufficient symlink protection due to directory cache poisoning using symbolic links","found_by":{"link":"","name":"Anonymous","email":""},"reported_by":{"link":"","name":"Anonymous","email":""},"module_name":"tar","cves":["CVE-2021-37701"],"vulnerable_versions":"<4.4.16 || >=5.0.0 <5.0.8 || >=6.0.0 <6.1.7","patched_versions":">=4.4.16 <5.0.0 || >=5.0.8 <6.0.0 || >=6.1.7","overview":"### Impact\n\nArbitrary File Creation, Arbitrary File Overwrite, Arbitrary Code Execution\n\n`node-tar` aims to guarantee that any file whose location would be modified by a symbolic link is not extracted. This is, in part, achieved by ensuring that extracted directories are not symlinks. Additionally, in order to prevent unnecessary stat calls to determine whether a given path is a directory, paths are cached when directories are created.\n\nThis logic was insufficient when extracting tar files that contained both a directory and a symlink with the same name as the directory, where the symlink and directory names in the archive entry used backslashes as a path separator on posix systems. The cache checking logic used both `\\` and `/` characters as path separators, however `\\` is a valid filename character on posix systems.\n\nBy first creating a directory, and then replacing that directory with a symlink, it was thus possible to bypass node-tar symlink checks on directories, essentially allowing an untrusted tar file to symlink into an arbitrary location and subsequently extracting arbitrary files into that location, thus allowing arbitrary file creation and overwrite.\n\nAdditionally, a similar confusion could arise on case-insensitive filesystems. If a tar archive contained a directory at `FOO`, followed by a symbolic link named `foo`, then on case-insensitive file systems, the creation of the symbolic link would remove the directory from the filesystem, but _not_ from the internal directory cache, as it would not be treated as a cache hit. A subsequent file entry within the `FOO` directory would then be placed in the target of the symbolic link, thinking that the directory had already been created. \n\nThese issues were addressed in releases 4.4.16, 5.0.8 and 6.1.7.\n\nThe v3 branch of `node-tar` has been deprecated and did not receive patches for these issues. If you are still using a v3 release we recommend you update to a more recent version of `node-tar`. If this is not possible, a workaround is available below.\n\n### Patches\n\n4.4.16 || 5.0.8 || 6.1.7\n\n### Workarounds\n\nUsers may work around this vulnerability without upgrading by creating a custom filter method which prevents the extraction of symbolic links.\n\n```js\nconst tar = require('tar')\n\ntar.x({\n file: 'archive.tgz',\n filter: (file, entry) => {\n if (entry.type === 'SymbolicLink') {\n return false\n } else {\n return true\n }\n }\n})\n```\n\nUsers are encouraged to upgrade to the latest patched versions, rather than attempt to sanitize tar input themselves.\n\n### Fix\n\nThe problem is addressed in the following ways:\n\n1. All paths are normalized to use `/` as a path separator, replacing `\\` with `/` on Windows systems, and leaving `\\` intact in the path on posix systems. This is performed in depth, at every level of the program where paths are consumed.\n2. Directory cache pruning is performed case-insensitively. This _may_ result in undue cache misses on case-sensitive file systems, but the performance impact is negligible.\n\n#### Caveat\n\nNote that this means that the `entry` objects exposed in various parts of tar's API will now always use `/` as a path separator, even on Windows systems. This is not expected to cause problems, as `/` is a valid path separator on Windows systems, but _may_ result in issues if `entry.path` is compared against a path string coming from some other API such as `fs.realpath()` or `path.resolve()`.\n\nUsers are encouraged to always normalize paths using a well-tested method such as `path.resolve()` before comparing paths to one another.","recommendation":"Upgrade to versions 4.4.16, 5.0.8, 6.1.7 or later","references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-37701)\n- [GitHub Advisory](https://github.com/advisories/GHSA-9r2w-394v-53qc)\n","access":"public","severity":"high","cwe":"CWE-22","metadata":{"module_type":"","exploitability":7,"affected_components":""},"url":"https://npmjs.com/advisories/1779"}}} -{"type":"auditAdvisory","data":{"resolution":{"id":1780,"path":"react-scripts>terser-webpack-plugin>cacache>tar","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"6.1.4","paths":["react-scripts>terser-webpack-plugin>cacache>tar"]},{"version":"2.2.2","paths":["react-js-pagination>tar"]}],"id":1780,"created":"2021-08-31T16:10:17.945Z","updated":"2021-08-31T16:12:52.860Z","deleted":null,"title":"Arbitrary File Creation/Overwrite via insufficient symlink protection due to directory cache poisoning using symbolic links","found_by":{"link":"","name":"Anonymous","email":""},"reported_by":{"link":"","name":"Anonymous","email":""},"module_name":"tar","cves":["CVE-2021-37712"],"vulnerable_versions":"<4.4.18 || >=5.0.0 <5.0.10 || >=6.0.0 <6.1.9","patched_versions":">=4.4.18 <5.0.0 || >=5.0.10 <6.0.0 || >=6.1.9","overview":"### Impact\nArbitrary File Creation, Arbitrary File Overwrite, Arbitrary Code Execution\n\nnode-tar aims to guarantee that any file whose location would be modified by a symbolic link is not extracted. This is, in part, achieved by ensuring that extracted directories are not symlinks. Additionally, in order to prevent unnecessary stat calls to determine whether a given path is a directory, paths are cached when directories are created.\n\nThis logic was insufficient when extracting tar files that contained two directories and a symlink with names containing unicode values that normalized to the same value. Additionally, on Windows systems, long path portions would resolve to the same file system entities as their 8.3 \"short path\" counterparts. A specially crafted tar archive could thus include directories with two forms of the path that resolve to the same file system entity, followed by a symbolic link with a name in the first form, lastly followed by a file using the second form. It led to bypassing node-tar symlink checks on directories, essentially allowing an untrusted tar file to symlink into an arbitrary location and subsequently extracting arbitrary files into that location, thus allowing arbitrary file creation and overwrite.\n\nThe v3 branch of `node-tar` has been deprecated and did not receive patches for these issues. If you are still using a v3 release we recommend you update to a more recent version of `node-tar`. If this is not possible, a workaround is available below.\n\n### Patches\n\n6.1.9 || 5.0.10 || 4.4.18\n\n### Workarounds\n\nUsers may work around this vulnerability without upgrading by creating a custom filter method which prevents the extraction of symbolic links.\n\n```js\nconst tar = require('tar')\n\ntar.x({\n file: 'archive.tgz',\n filter: (file, entry) => {\n if (entry.type === 'SymbolicLink') {\n return false\n } else {\n return true\n }\n }\n})\n```\n\nUsers are encouraged to upgrade to the latest patched versions, rather than attempt to sanitize tar input themselves.\n\n#### Fix\n\nThe problem is addressed in the following ways, when comparing paths in the directory cache and path reservation systems:\n\n1. The `String.normalize('NFKD')` method is used to first normalize all unicode to its maximally compatible and multi-code-point form.\n2. All slashes are normalized to `/` on Windows systems (on posix systems, `\\` is a valid filename character, and thus left intact).\n3. When a symbolic link is encountered on Windows systems, the entire directory cache is cleared. Collisions related to use of 8.3 short names to replace directories with other (non-symlink) types of entries may make archives fail to extract properly, but will not result in arbitrary file writes.\n","recommendation":"Upgrade to versions 4.4.18, 5.0.10, 6.1.9 or later","references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-37712)\n- [GitHub Advisory](https://github.com/advisories/GHSA-qq89-hq3f-393p)\n","access":"public","severity":"high","cwe":"CWE-22","metadata":{"module_type":"","exploitability":7,"affected_components":""},"url":"https://npmjs.com/advisories/1780"}}} -{"type":"auditAdvisory","data":{"resolution":{"id":1781,"path":"react-scripts>terser-webpack-plugin>cacache>tar","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"6.1.4","paths":["react-scripts>terser-webpack-plugin>cacache>tar"]},{"version":"2.2.2","paths":["react-js-pagination>tar"]}],"id":1781,"created":"2021-08-31T16:10:27.513Z","updated":"2021-08-31T16:12:58.622Z","deleted":null,"title":"Arbitrary File Creation/Overwrite on Windows via insufficient relative path sanitization","found_by":{"link":"","name":"Anonymous","email":""},"reported_by":{"link":"","name":"Anonymous","email":""},"module_name":"tar","cves":["CVE-2021-37713"],"vulnerable_versions":"<4.4.18 || >=5.0.0 <5.0.10 || >=6.0.0 <6.1.9","patched_versions":">=4.4.18 <5.0.0 || >=5.0.10 <6.0.0 || >=6.1.9","overview":"### Impact\n\nArbitrary File Creation, Arbitrary File Overwrite, Arbitrary Code Execution\n\nnode-tar aims to guarantee that any file whose location would be outside of the extraction target directory is not extracted. This is, in part, accomplished by sanitizing absolute paths of entries within the archive, skipping archive entries that contain `..` path portions, and resolving the sanitized paths against the extraction target directory.\n\nThis logic was insufficient on Windows systems when extracting tar files that contained a path that was not an absolute path, but specified a drive letter different from the extraction target, such as `C:some\\path`. If the drive letter does not match the extraction target, for example `D:\\extraction\\dir`, then the result of `path.resolve(extractionDirectory, entryPath)` would resolve against the current working directory on the `C:` drive, rather than the extraction target directory.\n\nAdditionally, a `..` portion of the path could occur immediately after the drive letter, such as `C:../foo`, and was not properly sanitized by the logic that checked for `..` within the normalized and split portions of the path.\n\nThis only affects users of `node-tar` on Windows systems.\n\n### Patches\n\n4.4.18 || 5.0.10 || 6.1.9\n\n### Workarounds\n\nThere is no reasonable way to work around this issue without performing the same path normalization procedures that node-tar now does.\n\nUsers are encouraged to upgrade to the latest patched versions of node-tar, rather than attempt to sanitize paths themselves.\n\n### Fix\n\nThe fixed versions strip path roots from all paths prior to being resolved against the extraction target folder, even if such paths are not \"absolute\".\n\nAdditionally, a path starting with a drive letter and then two dots, like `c:../`, would bypass the check for `..` path portions. This is checked properly in the patched versions.\n\nFinally, a defense in depth check is added, such that if the `entry.absolute` is outside of the extraction taret, and we are not in preservePaths:true mode, a warning is raised on that entry, and it is skipped. Currently, it is believed that this check is redundant, but it did catch some oversights in development.\n","recommendation":"Upgrade to versions 4.4.18, 5.0.10, 6.1.9 or later","references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-37713)\n- [GitHub Advisory](https://github.com/advisories/GHSA-5955-9wpr-37jh)\n","access":"public","severity":"high","cwe":"CWE-22","metadata":{"module_type":"","exploitability":7,"affected_components":""},"url":"https://npmjs.com/advisories/1781"}}} -{"type":"auditAdvisory","data":{"resolution":{"id":1747,"path":"react-scripts>react-dev-utils>browserslist","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"4.14.2","paths":["react-scripts>react-dev-utils>browserslist"]}],"id":1747,"created":"2021-05-24T19:56:39.062Z","updated":"2021-05-24T19:59:05.419Z","deleted":null,"title":"Regular Expression Denial of Service","found_by":{"link":"","name":"Anonymous","email":""},"reported_by":{"link":"","name":"Anonymous","email":""},"module_name":"browserslist","cves":["CVE-2021-23364"],"vulnerable_versions":">=4.0.0 <4.16.5","patched_versions":">=4.16.5","overview":"The package `browserslist` from 4.0.0 and before 4.16.5 are vulnerable to Regular Expression Denial of Service (ReDoS) during parsing of queries.","recommendation":"Upgrade to version 4.16.5 or later","references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-23364)\n- [GitHub Advisory](https://github.com/advisories/GHSA-w8qv-6jwh-64r5)\n","access":"public","severity":"moderate","cwe":"CWE-400","metadata":{"module_type":"","exploitability":5,"affected_components":""},"url":"https://npmjs.com/advisories/1747"}}} -{"type":"auditAdvisory","data":{"resolution":{"id":1751,"path":"react-scripts>webpack>watchpack>watchpack-chokidar2>chokidar>glob-parent","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"3.1.0","paths":["react-scripts>webpack>watchpack>watchpack-chokidar2>chokidar>glob-parent","react-scripts>webpack-dev-server>chokidar>glob-parent"]}],"id":1751,"created":"2021-06-07T21:57:10.135Z","updated":"2021-06-07T21:58:07.745Z","deleted":null,"title":"Regular expression denial of service","found_by":{"link":"","name":"Anonymous","email":""},"reported_by":{"link":"","name":"Anonymous","email":""},"module_name":"glob-parent","cves":["CVE-2020-28469"],"vulnerable_versions":"<5.1.2","patched_versions":">=5.1.2","overview":"`glob-parent` before 5.1.2 has a regular expression denial of service vulnerability. The enclosure regex used to check for strings ending in enclosure containing path separator.","recommendation":"Upgrade to version 5.1.2 or later","references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2020-28469)\n- [GitHub Advisory](https://github.com/advisories/GHSA-ww39-953v-wcq6)\n","access":"public","severity":"moderate","cwe":"CWE-400","metadata":{"module_type":"","exploitability":5,"affected_components":""},"url":"https://npmjs.com/advisories/1751"}}} -{"type":"auditAdvisory","data":{"resolution":{"id":1751,"path":"react-scripts>webpack-dev-server>chokidar>glob-parent","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"3.1.0","paths":["react-scripts>webpack>watchpack>watchpack-chokidar2>chokidar>glob-parent","react-scripts>webpack-dev-server>chokidar>glob-parent"]}],"id":1751,"created":"2021-06-07T21:57:10.135Z","updated":"2021-06-07T21:58:07.745Z","deleted":null,"title":"Regular expression denial of service","found_by":{"link":"","name":"Anonymous","email":""},"reported_by":{"link":"","name":"Anonymous","email":""},"module_name":"glob-parent","cves":["CVE-2020-28469"],"vulnerable_versions":"<5.1.2","patched_versions":">=5.1.2","overview":"`glob-parent` before 5.1.2 has a regular expression denial of service vulnerability. The enclosure regex used to check for strings ending in enclosure containing path separator.","recommendation":"Upgrade to version 5.1.2 or later","references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2020-28469)\n- [GitHub Advisory](https://github.com/advisories/GHSA-ww39-953v-wcq6)\n","access":"public","severity":"moderate","cwe":"CWE-400","metadata":{"module_type":"","exploitability":5,"affected_components":""},"url":"https://npmjs.com/advisories/1751"}}} -{"type":"auditAdvisory","data":{"resolution":{"id":1770,"path":"react-js-pagination>tar","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"2.2.2","paths":["react-js-pagination>tar"]}],"id":1770,"created":"2021-08-03T18:11:06.582Z","updated":"2021-08-03T19:07:08.152Z","deleted":null,"title":"Arbitrary File Creation/Overwrite due to insufficient absolute path sanitization","found_by":{"link":"","name":"Anonymous","email":""},"reported_by":{"link":"","name":"Anonymous","email":""},"module_name":"tar","cves":["CVE-2021-32804"],"vulnerable_versions":"<3.2.2 || >=4.0.0 <4.4.14 || >=5.0.0 <5.0.6 || >=6.0.0 <6.1.1","patched_versions":">=3.2.2 <4.0.0 || >=4.4.14 <5.0.0 || >=5.0.6 <6.0.0 || >=6.1.1","overview":"The `tar` package has a high severity vulnerability before versions 3.2.2, 4.4.14, 5.0.6, and 6.1.1.\n\n### Impact\n\nArbitrary File Creation, Arbitrary File Overwrite, Arbitrary Code Execution\n\n`node-tar` aims to prevent extraction of absolute file paths by turning absolute paths into relative paths when the `preservePaths` flag is not set to `true`. This is achieved by stripping the absolute path root from any absolute file paths contained in a tar file. For example `/home/user/.bashrc` would turn into `home/user/.bashrc`. \n\nThis logic was insufficient when file paths contained repeated path roots such as `////home/user/.bashrc`. `node-tar` would only strip a single path root from such paths. When given an absolute file path with repeating path roots, the resulting path (e.g. `///home/user/.bashrc`) would still resolve to an absolute path, thus allowing arbitrary file creation and overwrite. \n\n### Workarounds\n\nUsers may work around this vulnerability without upgrading by creating a custom `onentry` method which sanitizes the `entry.path` or a `filter` method which removes entries with absolute paths.\n\n```js\nconst path = require('path')\nconst tar = require('tar')\n\ntar.x({\n file: 'archive.tgz',\n // either add this function...\n onentry: (entry) => {\n if (path.isAbsolute(entry.path)) {\n entry.path = sanitizeAbsolutePathSomehow(entry.path)\n entry.absolute = path.resolve(entry.path)\n }\n },\n\n // or this one\n filter: (file, entry) => {\n if (path.isAbsolute(entry.path)) {\n return false\n } else {\n return true\n }\n }\n})\n```\n\nUsers are encouraged to upgrade to the latest patch versions, rather than attempt to sanitize tar input themselves.","recommendation":"Upgrade to version 3.2.2, 4.4.14, 5.0.6, 6.1.1 or later","references":"- [GitHub Advisory](https://github.com/npm/node-tar/security/advisories/GHSA-3jfq-g458-7qm9)\n- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-32804)\n- [Related but distinct advisory involving symlinks](https://www.npmjs.com/advisories/1771)","access":"public","severity":"high","cwe":"CWE-22","metadata":{"module_type":"","exploitability":8,"affected_components":""},"url":"https://npmjs.com/advisories/1770"}}} -{"type":"auditAdvisory","data":{"resolution":{"id":1771,"path":"react-js-pagination>tar","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"2.2.2","paths":["react-js-pagination>tar"]}],"id":1771,"created":"2021-08-03T18:14:17.499Z","updated":"2021-08-03T19:01:35.564Z","deleted":null,"title":"Arbitrary File Creation/Overwrite via insufficient symlink protection due to directory cache poisoning","found_by":{"link":"","name":"Anonymous","email":""},"reported_by":{"link":"","name":"Anonymous","email":""},"module_name":"tar","cves":["CVE-2021-32803"],"vulnerable_versions":"<3.2.3 || >=4.0.0 <4.4.15 || >=5.0.0 <5.0.7 || >=6.0.0 <6.1.2","patched_versions":">=3.2.3 <4.0.0 || >=4.4.15 <5.0.0 || >=5.0.7 <6.0.0 || >=6.1.2","overview":"The `tar` package has a high severity vulnerability before versions 3.2.3, 4.4.15, 5.0.7, and 6.1.2.\n\n### Impact\n\nArbitrary File Creation, Arbitrary File Overwrite, Arbitrary Code Execution\n\n`node-tar` aims to prevent extraction of absolute file paths by turning absolute paths into relative paths when the `preservePaths` flag is not set to `true`. This is achieved by stripping the absolute path root from any absolute file paths contained in a tar file. For example `/home/user/.bashrc` would turn into `home/user/.bashrc`. \n\nThis logic was insufficient when file paths contained repeated path roots such as `////home/user/.bashrc`. `node-tar` would only strip a single path root from such paths. When given an absolute file path with repeating path roots, the resulting path (e.g. `///home/user/.bashrc`) would still resolve to an absolute path, thus allowing arbitrary file creation and overwrite. \n\n### Workarounds\n\nUsers may work around this vulnerability without upgrading by creating a custom `onentry` method which sanitizes the `entry.path` or a `filter` method which removes entries with absolute paths.\n\n```js\nconst path = require('path')\nconst tar = require('tar')\n\ntar.x({\n file: 'archive.tgz',\n // either add this function...\n onentry: (entry) => {\n if (path.isAbsolute(entry.path)) {\n entry.path = sanitizeAbsolutePathSomehow(entry.path)\n entry.absolute = path.resolve(entry.path)\n }\n },\n\n // or this one\n filter: (file, entry) => {\n if (path.isAbsolute(entry.path)) {\n return false\n } else {\n return true\n }\n }\n})\n```\n\nUsers are encouraged to upgrade to the latest patch versions, rather than attempt to sanitize tar input themselves.","recommendation":"Upgrade to version 3.2.3, 4.4.15, 5.0.7, 6.1.2 or later","references":"- [GitHub Advisory](https://github.com/npm/node-tar/security/advisories/GHSA-3jfq-g458-7qm9)\n- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-32803)\n- [Related but distinct advisory involving absolute paths](https://www.npmjs.com/advisories/1770)","access":"public","severity":"high","cwe":"CWE-22","metadata":{"module_type":"","exploitability":8,"affected_components":""},"url":"https://npmjs.com/advisories/1771"}}} -{"type":"auditAdvisory","data":{"resolution":{"id":1779,"path":"react-js-pagination>tar","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"6.1.4","paths":["react-scripts>terser-webpack-plugin>cacache>tar"]},{"version":"2.2.2","paths":["react-js-pagination>tar"]}],"id":1779,"created":"2021-08-31T16:10:07.868Z","updated":"2021-08-31T16:11:58.986Z","deleted":null,"title":"Arbitrary File Creation/Overwrite via insufficient symlink protection due to directory cache poisoning using symbolic links","found_by":{"link":"","name":"Anonymous","email":""},"reported_by":{"link":"","name":"Anonymous","email":""},"module_name":"tar","cves":["CVE-2021-37701"],"vulnerable_versions":"<4.4.16 || >=5.0.0 <5.0.8 || >=6.0.0 <6.1.7","patched_versions":">=4.4.16 <5.0.0 || >=5.0.8 <6.0.0 || >=6.1.7","overview":"### Impact\n\nArbitrary File Creation, Arbitrary File Overwrite, Arbitrary Code Execution\n\n`node-tar` aims to guarantee that any file whose location would be modified by a symbolic link is not extracted. This is, in part, achieved by ensuring that extracted directories are not symlinks. Additionally, in order to prevent unnecessary stat calls to determine whether a given path is a directory, paths are cached when directories are created.\n\nThis logic was insufficient when extracting tar files that contained both a directory and a symlink with the same name as the directory, where the symlink and directory names in the archive entry used backslashes as a path separator on posix systems. The cache checking logic used both `\\` and `/` characters as path separators, however `\\` is a valid filename character on posix systems.\n\nBy first creating a directory, and then replacing that directory with a symlink, it was thus possible to bypass node-tar symlink checks on directories, essentially allowing an untrusted tar file to symlink into an arbitrary location and subsequently extracting arbitrary files into that location, thus allowing arbitrary file creation and overwrite.\n\nAdditionally, a similar confusion could arise on case-insensitive filesystems. If a tar archive contained a directory at `FOO`, followed by a symbolic link named `foo`, then on case-insensitive file systems, the creation of the symbolic link would remove the directory from the filesystem, but _not_ from the internal directory cache, as it would not be treated as a cache hit. A subsequent file entry within the `FOO` directory would then be placed in the target of the symbolic link, thinking that the directory had already been created. \n\nThese issues were addressed in releases 4.4.16, 5.0.8 and 6.1.7.\n\nThe v3 branch of `node-tar` has been deprecated and did not receive patches for these issues. If you are still using a v3 release we recommend you update to a more recent version of `node-tar`. If this is not possible, a workaround is available below.\n\n### Patches\n\n4.4.16 || 5.0.8 || 6.1.7\n\n### Workarounds\n\nUsers may work around this vulnerability without upgrading by creating a custom filter method which prevents the extraction of symbolic links.\n\n```js\nconst tar = require('tar')\n\ntar.x({\n file: 'archive.tgz',\n filter: (file, entry) => {\n if (entry.type === 'SymbolicLink') {\n return false\n } else {\n return true\n }\n }\n})\n```\n\nUsers are encouraged to upgrade to the latest patched versions, rather than attempt to sanitize tar input themselves.\n\n### Fix\n\nThe problem is addressed in the following ways:\n\n1. All paths are normalized to use `/` as a path separator, replacing `\\` with `/` on Windows systems, and leaving `\\` intact in the path on posix systems. This is performed in depth, at every level of the program where paths are consumed.\n2. Directory cache pruning is performed case-insensitively. This _may_ result in undue cache misses on case-sensitive file systems, but the performance impact is negligible.\n\n#### Caveat\n\nNote that this means that the `entry` objects exposed in various parts of tar's API will now always use `/` as a path separator, even on Windows systems. This is not expected to cause problems, as `/` is a valid path separator on Windows systems, but _may_ result in issues if `entry.path` is compared against a path string coming from some other API such as `fs.realpath()` or `path.resolve()`.\n\nUsers are encouraged to always normalize paths using a well-tested method such as `path.resolve()` before comparing paths to one another.","recommendation":"Upgrade to versions 4.4.16, 5.0.8, 6.1.7 or later","references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-37701)\n- [GitHub Advisory](https://github.com/advisories/GHSA-9r2w-394v-53qc)\n","access":"public","severity":"high","cwe":"CWE-22","metadata":{"module_type":"","exploitability":7,"affected_components":""},"url":"https://npmjs.com/advisories/1779"}}} -{"type":"auditAdvisory","data":{"resolution":{"id":1780,"path":"react-js-pagination>tar","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"6.1.4","paths":["react-scripts>terser-webpack-plugin>cacache>tar"]},{"version":"2.2.2","paths":["react-js-pagination>tar"]}],"id":1780,"created":"2021-08-31T16:10:17.945Z","updated":"2021-08-31T16:12:52.860Z","deleted":null,"title":"Arbitrary File Creation/Overwrite via insufficient symlink protection due to directory cache poisoning using symbolic links","found_by":{"link":"","name":"Anonymous","email":""},"reported_by":{"link":"","name":"Anonymous","email":""},"module_name":"tar","cves":["CVE-2021-37712"],"vulnerable_versions":"<4.4.18 || >=5.0.0 <5.0.10 || >=6.0.0 <6.1.9","patched_versions":">=4.4.18 <5.0.0 || >=5.0.10 <6.0.0 || >=6.1.9","overview":"### Impact\nArbitrary File Creation, Arbitrary File Overwrite, Arbitrary Code Execution\n\nnode-tar aims to guarantee that any file whose location would be modified by a symbolic link is not extracted. This is, in part, achieved by ensuring that extracted directories are not symlinks. Additionally, in order to prevent unnecessary stat calls to determine whether a given path is a directory, paths are cached when directories are created.\n\nThis logic was insufficient when extracting tar files that contained two directories and a symlink with names containing unicode values that normalized to the same value. Additionally, on Windows systems, long path portions would resolve to the same file system entities as their 8.3 \"short path\" counterparts. A specially crafted tar archive could thus include directories with two forms of the path that resolve to the same file system entity, followed by a symbolic link with a name in the first form, lastly followed by a file using the second form. It led to bypassing node-tar symlink checks on directories, essentially allowing an untrusted tar file to symlink into an arbitrary location and subsequently extracting arbitrary files into that location, thus allowing arbitrary file creation and overwrite.\n\nThe v3 branch of `node-tar` has been deprecated and did not receive patches for these issues. If you are still using a v3 release we recommend you update to a more recent version of `node-tar`. If this is not possible, a workaround is available below.\n\n### Patches\n\n6.1.9 || 5.0.10 || 4.4.18\n\n### Workarounds\n\nUsers may work around this vulnerability without upgrading by creating a custom filter method which prevents the extraction of symbolic links.\n\n```js\nconst tar = require('tar')\n\ntar.x({\n file: 'archive.tgz',\n filter: (file, entry) => {\n if (entry.type === 'SymbolicLink') {\n return false\n } else {\n return true\n }\n }\n})\n```\n\nUsers are encouraged to upgrade to the latest patched versions, rather than attempt to sanitize tar input themselves.\n\n#### Fix\n\nThe problem is addressed in the following ways, when comparing paths in the directory cache and path reservation systems:\n\n1. The `String.normalize('NFKD')` method is used to first normalize all unicode to its maximally compatible and multi-code-point form.\n2. All slashes are normalized to `/` on Windows systems (on posix systems, `\\` is a valid filename character, and thus left intact).\n3. When a symbolic link is encountered on Windows systems, the entire directory cache is cleared. Collisions related to use of 8.3 short names to replace directories with other (non-symlink) types of entries may make archives fail to extract properly, but will not result in arbitrary file writes.\n","recommendation":"Upgrade to versions 4.4.18, 5.0.10, 6.1.9 or later","references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-37712)\n- [GitHub Advisory](https://github.com/advisories/GHSA-qq89-hq3f-393p)\n","access":"public","severity":"high","cwe":"CWE-22","metadata":{"module_type":"","exploitability":7,"affected_components":""},"url":"https://npmjs.com/advisories/1780"}}} -{"type":"auditAdvisory","data":{"resolution":{"id":1781,"path":"react-js-pagination>tar","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"6.1.4","paths":["react-scripts>terser-webpack-plugin>cacache>tar"]},{"version":"2.2.2","paths":["react-js-pagination>tar"]}],"id":1781,"created":"2021-08-31T16:10:27.513Z","updated":"2021-08-31T16:12:58.622Z","deleted":null,"title":"Arbitrary File Creation/Overwrite on Windows via insufficient relative path sanitization","found_by":{"link":"","name":"Anonymous","email":""},"reported_by":{"link":"","name":"Anonymous","email":""},"module_name":"tar","cves":["CVE-2021-37713"],"vulnerable_versions":"<4.4.18 || >=5.0.0 <5.0.10 || >=6.0.0 <6.1.9","patched_versions":">=4.4.18 <5.0.0 || >=5.0.10 <6.0.0 || >=6.1.9","overview":"### Impact\n\nArbitrary File Creation, Arbitrary File Overwrite, Arbitrary Code Execution\n\nnode-tar aims to guarantee that any file whose location would be outside of the extraction target directory is not extracted. This is, in part, accomplished by sanitizing absolute paths of entries within the archive, skipping archive entries that contain `..` path portions, and resolving the sanitized paths against the extraction target directory.\n\nThis logic was insufficient on Windows systems when extracting tar files that contained a path that was not an absolute path, but specified a drive letter different from the extraction target, such as `C:some\\path`. If the drive letter does not match the extraction target, for example `D:\\extraction\\dir`, then the result of `path.resolve(extractionDirectory, entryPath)` would resolve against the current working directory on the `C:` drive, rather than the extraction target directory.\n\nAdditionally, a `..` portion of the path could occur immediately after the drive letter, such as `C:../foo`, and was not properly sanitized by the logic that checked for `..` within the normalized and split portions of the path.\n\nThis only affects users of `node-tar` on Windows systems.\n\n### Patches\n\n4.4.18 || 5.0.10 || 6.1.9\n\n### Workarounds\n\nThere is no reasonable way to work around this issue without performing the same path normalization procedures that node-tar now does.\n\nUsers are encouraged to upgrade to the latest patched versions of node-tar, rather than attempt to sanitize paths themselves.\n\n### Fix\n\nThe fixed versions strip path roots from all paths prior to being resolved against the extraction target folder, even if such paths are not \"absolute\".\n\nAdditionally, a path starting with a drive letter and then two dots, like `c:../`, would bypass the check for `..` path portions. This is checked properly in the patched versions.\n\nFinally, a defense in depth check is added, such that if the `entry.absolute` is outside of the extraction taret, and we are not in preservePaths:true mode, a warning is raised on that entry, and it is skipped. Currently, it is believed that this check is redundant, but it did catch some oversights in development.\n","recommendation":"Upgrade to versions 4.4.18, 5.0.10, 6.1.9 or later","references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-37713)\n- [GitHub Advisory](https://github.com/advisories/GHSA-5955-9wpr-37jh)\n","access":"public","severity":"high","cwe":"CWE-22","metadata":{"module_type":"","exploitability":7,"affected_components":""},"url":"https://npmjs.com/advisories/1781"}}} +{"type":"auditAdvisory","data":{"resolution":{"id":1779,"path":"react-scripts>terser-webpack-plugin>cacache>tar","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"6.1.4","paths":["react-scripts>terser-webpack-plugin>cacache>tar"]},{"version":"2.2.2","paths":["react-js-pagination>tar"]}],"found_by":{"link":"","name":"Anonymous","email":""},"module_name":"tar","reported_by":{"link":"","name":"Anonymous","email":""},"cves":["CVE-2021-37701"],"references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-37701)\n- [GitHub Advisory](https://github.com/advisories/GHSA-9r2w-394v-53qc)\n","updated":"2021-08-31T16:11:58.986Z","id":1779,"deleted":null,"severity":"high","created":"2021-08-31T16:10:07.868Z","metadata":{"module_type":"","exploitability":7,"affected_components":""},"vulnerable_versions":"<4.4.16 || >=5.0.0 <5.0.8 || >=6.0.0 <6.1.7","overview":"### Impact\n\nArbitrary File Creation, Arbitrary File Overwrite, Arbitrary Code Execution\n\n`node-tar` aims to guarantee that any file whose location would be modified by a symbolic link is not extracted. This is, in part, achieved by ensuring that extracted directories are not symlinks. Additionally, in order to prevent unnecessary stat calls to determine whether a given path is a directory, paths are cached when directories are created.\n\nThis logic was insufficient when extracting tar files that contained both a directory and a symlink with the same name as the directory, where the symlink and directory names in the archive entry used backslashes as a path separator on posix systems. The cache checking logic used both `\\` and `/` characters as path separators, however `\\` is a valid filename character on posix systems.\n\nBy first creating a directory, and then replacing that directory with a symlink, it was thus possible to bypass node-tar symlink checks on directories, essentially allowing an untrusted tar file to symlink into an arbitrary location and subsequently extracting arbitrary files into that location, thus allowing arbitrary file creation and overwrite.\n\nAdditionally, a similar confusion could arise on case-insensitive filesystems. If a tar archive contained a directory at `FOO`, followed by a symbolic link named `foo`, then on case-insensitive file systems, the creation of the symbolic link would remove the directory from the filesystem, but _not_ from the internal directory cache, as it would not be treated as a cache hit. A subsequent file entry within the `FOO` directory would then be placed in the target of the symbolic link, thinking that the directory had already been created. \n\nThese issues were addressed in releases 4.4.16, 5.0.8 and 6.1.7.\n\nThe v3 branch of `node-tar` has been deprecated and did not receive patches for these issues. If you are still using a v3 release we recommend you update to a more recent version of `node-tar`. If this is not possible, a workaround is available below.\n\n### Patches\n\n4.4.16 || 5.0.8 || 6.1.7\n\n### Workarounds\n\nUsers may work around this vulnerability without upgrading by creating a custom filter method which prevents the extraction of symbolic links.\n\n```js\nconst tar = require('tar')\n\ntar.x({\n file: 'archive.tgz',\n filter: (file, entry) => {\n if (entry.type === 'SymbolicLink') {\n return false\n } else {\n return true\n }\n }\n})\n```\n\nUsers are encouraged to upgrade to the latest patched versions, rather than attempt to sanitize tar input themselves.\n\n### Fix\n\nThe problem is addressed in the following ways:\n\n1. All paths are normalized to use `/` as a path separator, replacing `\\` with `/` on Windows systems, and leaving `\\` intact in the path on posix systems. This is performed in depth, at every level of the program where paths are consumed.\n2. Directory cache pruning is performed case-insensitively. This _may_ result in undue cache misses on case-sensitive file systems, but the performance impact is negligible.\n\n#### Caveat\n\nNote that this means that the `entry` objects exposed in various parts of tar's API will now always use `/` as a path separator, even on Windows systems. This is not expected to cause problems, as `/` is a valid path separator on Windows systems, but _may_ result in issues if `entry.path` is compared against a path string coming from some other API such as `fs.realpath()` or `path.resolve()`.\n\nUsers are encouraged to always normalize paths using a well-tested method such as `path.resolve()` before comparing paths to one another.","cwe":"CWE-22","patched_versions":">=4.4.16 <5.0.0 || >=5.0.8 <6.0.0 || >=6.1.7","title":"Arbitrary File Creation/Overwrite via insufficient symlink protection due to directory cache poisoning using symbolic links","recommendation":"Upgrade to versions 4.4.16, 5.0.8, 6.1.7 or later","access":"public","url":"https://npmjs.com/advisories/1779"}}} +{"type":"auditAdvisory","data":{"resolution":{"id":1780,"path":"react-scripts>terser-webpack-plugin>cacache>tar","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"6.1.4","paths":["react-scripts>terser-webpack-plugin>cacache>tar"]},{"version":"2.2.2","paths":["react-js-pagination>tar"]}],"found_by":{"link":"","name":"Anonymous","email":""},"module_name":"tar","reported_by":{"link":"","name":"Anonymous","email":""},"cves":["CVE-2021-37712"],"references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-37712)\n- [GitHub Advisory](https://github.com/advisories/GHSA-qq89-hq3f-393p)\n","updated":"2021-08-31T16:12:52.860Z","id":1780,"deleted":null,"severity":"high","created":"2021-08-31T16:10:17.945Z","metadata":{"module_type":"","exploitability":7,"affected_components":""},"vulnerable_versions":"<4.4.18 || >=5.0.0 <5.0.10 || >=6.0.0 <6.1.9","overview":"### Impact\nArbitrary File Creation, Arbitrary File Overwrite, Arbitrary Code Execution\n\nnode-tar aims to guarantee that any file whose location would be modified by a symbolic link is not extracted. This is, in part, achieved by ensuring that extracted directories are not symlinks. Additionally, in order to prevent unnecessary stat calls to determine whether a given path is a directory, paths are cached when directories are created.\n\nThis logic was insufficient when extracting tar files that contained two directories and a symlink with names containing unicode values that normalized to the same value. Additionally, on Windows systems, long path portions would resolve to the same file system entities as their 8.3 \"short path\" counterparts. A specially crafted tar archive could thus include directories with two forms of the path that resolve to the same file system entity, followed by a symbolic link with a name in the first form, lastly followed by a file using the second form. It led to bypassing node-tar symlink checks on directories, essentially allowing an untrusted tar file to symlink into an arbitrary location and subsequently extracting arbitrary files into that location, thus allowing arbitrary file creation and overwrite.\n\nThe v3 branch of `node-tar` has been deprecated and did not receive patches for these issues. If you are still using a v3 release we recommend you update to a more recent version of `node-tar`. If this is not possible, a workaround is available below.\n\n### Patches\n\n6.1.9 || 5.0.10 || 4.4.18\n\n### Workarounds\n\nUsers may work around this vulnerability without upgrading by creating a custom filter method which prevents the extraction of symbolic links.\n\n```js\nconst tar = require('tar')\n\ntar.x({\n file: 'archive.tgz',\n filter: (file, entry) => {\n if (entry.type === 'SymbolicLink') {\n return false\n } else {\n return true\n }\n }\n})\n```\n\nUsers are encouraged to upgrade to the latest patched versions, rather than attempt to sanitize tar input themselves.\n\n#### Fix\n\nThe problem is addressed in the following ways, when comparing paths in the directory cache and path reservation systems:\n\n1. The `String.normalize('NFKD')` method is used to first normalize all unicode to its maximally compatible and multi-code-point form.\n2. All slashes are normalized to `/` on Windows systems (on posix systems, `\\` is a valid filename character, and thus left intact).\n3. When a symbolic link is encountered on Windows systems, the entire directory cache is cleared. Collisions related to use of 8.3 short names to replace directories with other (non-symlink) types of entries may make archives fail to extract properly, but will not result in arbitrary file writes.\n","cwe":"CWE-22","patched_versions":">=4.4.18 <5.0.0 || >=5.0.10 <6.0.0 || >=6.1.9","title":"Arbitrary File Creation/Overwrite via insufficient symlink protection due to directory cache poisoning using symbolic links","recommendation":"Upgrade to versions 4.4.18, 5.0.10, 6.1.9 or later","access":"public","url":"https://npmjs.com/advisories/1780"}}} +{"type":"auditAdvisory","data":{"resolution":{"id":1781,"path":"react-scripts>terser-webpack-plugin>cacache>tar","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"6.1.4","paths":["react-scripts>terser-webpack-plugin>cacache>tar"]},{"version":"2.2.2","paths":["react-js-pagination>tar"]}],"found_by":{"link":"","name":"Anonymous","email":""},"module_name":"tar","reported_by":{"link":"","name":"Anonymous","email":""},"cves":["CVE-2021-37713"],"references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-37713)\n- [GitHub Advisory](https://github.com/advisories/GHSA-5955-9wpr-37jh)\n","updated":"2021-08-31T16:12:58.622Z","id":1781,"deleted":null,"severity":"high","created":"2021-08-31T16:10:27.513Z","metadata":{"module_type":"","exploitability":7,"affected_components":""},"vulnerable_versions":"<4.4.18 || >=5.0.0 <5.0.10 || >=6.0.0 <6.1.9","overview":"### Impact\n\nArbitrary File Creation, Arbitrary File Overwrite, Arbitrary Code Execution\n\nnode-tar aims to guarantee that any file whose location would be outside of the extraction target directory is not extracted. This is, in part, accomplished by sanitizing absolute paths of entries within the archive, skipping archive entries that contain `..` path portions, and resolving the sanitized paths against the extraction target directory.\n\nThis logic was insufficient on Windows systems when extracting tar files that contained a path that was not an absolute path, but specified a drive letter different from the extraction target, such as `C:some\\path`. If the drive letter does not match the extraction target, for example `D:\\extraction\\dir`, then the result of `path.resolve(extractionDirectory, entryPath)` would resolve against the current working directory on the `C:` drive, rather than the extraction target directory.\n\nAdditionally, a `..` portion of the path could occur immediately after the drive letter, such as `C:../foo`, and was not properly sanitized by the logic that checked for `..` within the normalized and split portions of the path.\n\nThis only affects users of `node-tar` on Windows systems.\n\n### Patches\n\n4.4.18 || 5.0.10 || 6.1.9\n\n### Workarounds\n\nThere is no reasonable way to work around this issue without performing the same path normalization procedures that node-tar now does.\n\nUsers are encouraged to upgrade to the latest patched versions of node-tar, rather than attempt to sanitize paths themselves.\n\n### Fix\n\nThe fixed versions strip path roots from all paths prior to being resolved against the extraction target folder, even if such paths are not \"absolute\".\n\nAdditionally, a path starting with a drive letter and then two dots, like `c:../`, would bypass the check for `..` path portions. This is checked properly in the patched versions.\n\nFinally, a defense in depth check is added, such that if the `entry.absolute` is outside of the extraction taret, and we are not in preservePaths:true mode, a warning is raised on that entry, and it is skipped. Currently, it is believed that this check is redundant, but it did catch some oversights in development.\n","cwe":"CWE-22","patched_versions":">=4.4.18 <5.0.0 || >=5.0.10 <6.0.0 || >=6.1.9","title":"Arbitrary File Creation/Overwrite on Windows via insufficient relative path sanitization","recommendation":"Upgrade to versions 4.4.18, 5.0.10, 6.1.9 or later","access":"public","url":"https://npmjs.com/advisories/1781"}}} +{"type":"auditAdvisory","data":{"resolution":{"id":1747,"path":"react-scripts>react-dev-utils>browserslist","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"4.14.2","paths":["react-scripts>react-dev-utils>browserslist"]}],"found_by":{"link":"","name":"Anonymous","email":""},"module_name":"browserslist","reported_by":{"link":"","name":"Anonymous","email":""},"cves":["CVE-2021-23364"],"references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-23364)\n- [GitHub Advisory](https://github.com/advisories/GHSA-w8qv-6jwh-64r5)\n","updated":"2021-05-24T19:59:05.419Z","id":1747,"deleted":null,"severity":"moderate","created":"2021-05-24T19:56:39.062Z","metadata":{"module_type":"","exploitability":5,"affected_components":""},"vulnerable_versions":">=4.0.0 <4.16.5","overview":"The package `browserslist` from 4.0.0 and before 4.16.5 are vulnerable to Regular Expression Denial of Service (ReDoS) during parsing of queries.","cwe":"CWE-400","patched_versions":">=4.16.5","title":"Regular Expression Denial of Service","recommendation":"Upgrade to version 4.16.5 or later","access":"public","url":"https://npmjs.com/advisories/1747"}}} +{"type":"auditAdvisory","data":{"resolution":{"id":1751,"path":"react-scripts>webpack>watchpack>watchpack-chokidar2>chokidar>glob-parent","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"3.1.0","paths":["react-scripts>webpack>watchpack>watchpack-chokidar2>chokidar>glob-parent","react-scripts>webpack-dev-server>chokidar>glob-parent"]}],"found_by":{"link":"","name":"Anonymous","email":""},"module_name":"glob-parent","reported_by":{"link":"","name":"Anonymous","email":""},"cves":["CVE-2020-28469"],"references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2020-28469)\n- [GitHub Advisory](https://github.com/advisories/GHSA-ww39-953v-wcq6)\n","updated":"2021-06-07T21:58:07.745Z","id":1751,"deleted":null,"severity":"moderate","created":"2021-06-07T21:57:10.135Z","metadata":{"module_type":"","exploitability":5,"affected_components":""},"vulnerable_versions":"<5.1.2","overview":"`glob-parent` before 5.1.2 has a regular expression denial of service vulnerability. The enclosure regex used to check for strings ending in enclosure containing path separator.","cwe":"CWE-400","patched_versions":">=5.1.2","title":"Regular expression denial of service","recommendation":"Upgrade to version 5.1.2 or later","access":"public","url":"https://npmjs.com/advisories/1751"}}} +{"type":"auditAdvisory","data":{"resolution":{"id":1751,"path":"react-scripts>webpack-dev-server>chokidar>glob-parent","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"3.1.0","paths":["react-scripts>webpack>watchpack>watchpack-chokidar2>chokidar>glob-parent","react-scripts>webpack-dev-server>chokidar>glob-parent"]}],"found_by":{"link":"","name":"Anonymous","email":""},"module_name":"glob-parent","reported_by":{"link":"","name":"Anonymous","email":""},"cves":["CVE-2020-28469"],"references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2020-28469)\n- [GitHub Advisory](https://github.com/advisories/GHSA-ww39-953v-wcq6)\n","updated":"2021-06-07T21:58:07.745Z","id":1751,"deleted":null,"severity":"moderate","created":"2021-06-07T21:57:10.135Z","metadata":{"module_type":"","exploitability":5,"affected_components":""},"vulnerable_versions":"<5.1.2","overview":"`glob-parent` before 5.1.2 has a regular expression denial of service vulnerability. The enclosure regex used to check for strings ending in enclosure containing path separator.","cwe":"CWE-400","patched_versions":">=5.1.2","title":"Regular expression denial of service","recommendation":"Upgrade to version 5.1.2 or later","access":"public","url":"https://npmjs.com/advisories/1751"}}} +{"type":"auditAdvisory","data":{"resolution":{"id":1770,"path":"react-js-pagination>tar","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"2.2.2","paths":["react-js-pagination>tar"]}],"found_by":{"link":"","name":"Anonymous","email":""},"module_name":"tar","reported_by":{"link":"","name":"Anonymous","email":""},"cves":["CVE-2021-32804"],"references":"- [GitHub Advisory](https://github.com/npm/node-tar/security/advisories/GHSA-3jfq-g458-7qm9)\n- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-32804)\n- [Related but distinct advisory involving symlinks](https://www.npmjs.com/advisories/1771)\n- [GitHub Advisory](https://github.com/advisories/GHSA-3jfq-g458-7qm9)\n","updated":"2021-09-23T08:07:33.040Z","id":1770,"deleted":null,"severity":"high","created":"2021-08-03T18:11:06.582Z","metadata":{"module_type":"","exploitability":8,"affected_components":""},"vulnerable_versions":"<3.2.2 || >=4.0.0 <4.4.14 || >=5.0.0 <5.0.6 || >=6.0.0 <6.1.1","overview":"The `tar` package has a high severity vulnerability before versions 3.2.2, 4.4.14, 5.0.6, and 6.1.1.\n\n### Impact\n\nArbitrary File Creation, Arbitrary File Overwrite, Arbitrary Code Execution\n\n`node-tar` aims to prevent extraction of absolute file paths by turning absolute paths into relative paths when the `preservePaths` flag is not set to `true`. This is achieved by stripping the absolute path root from any absolute file paths contained in a tar file. For example `/home/user/.bashrc` would turn into `home/user/.bashrc`. \n\nThis logic was insufficient when file paths contained repeated path roots such as `////home/user/.bashrc`. `node-tar` would only strip a single path root from such paths. When given an absolute file path with repeating path roots, the resulting path (e.g. `///home/user/.bashrc`) would still resolve to an absolute path, thus allowing arbitrary file creation and overwrite. \n\n### Workarounds\n\nUsers may work around this vulnerability without upgrading by creating a custom `onentry` method which sanitizes the `entry.path` or a `filter` method which removes entries with absolute paths.\n\n```js\nconst path = require('path')\nconst tar = require('tar')\n\ntar.x({\n file: 'archive.tgz',\n // either add this function...\n onentry: (entry) => {\n if (path.isAbsolute(entry.path)) {\n entry.path = sanitizeAbsolutePathSomehow(entry.path)\n entry.absolute = path.resolve(entry.path)\n }\n },\n\n // or this one\n filter: (file, entry) => {\n if (path.isAbsolute(entry.path)) {\n return false\n } else {\n return true\n }\n }\n})\n```\n\nUsers are encouraged to upgrade to the latest patch versions, rather than attempt to sanitize tar input themselves.","cwe":"CWE-22","patched_versions":">=3.2.2 <4.0.0 || >=4.4.14 <5.0.0 || >=5.0.6 <6.0.0 || >=6.1.1","title":"Arbitrary File Creation/Overwrite due to insufficient absolute path sanitization","recommendation":"Upgrade to version 3.2.2, 4.4.14, 5.0.6, 6.1.1 or later","access":"public","url":"https://npmjs.com/advisories/1770"}}} +{"type":"auditAdvisory","data":{"resolution":{"id":1771,"path":"react-js-pagination>tar","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"2.2.2","paths":["react-js-pagination>tar"]}],"found_by":{"link":"","name":"Anonymous","email":""},"module_name":"tar","reported_by":{"link":"","name":"Anonymous","email":""},"cves":["CVE-2021-32803"],"references":"- [GitHub Advisory](https://github.com/npm/node-tar/security/advisories/GHSA-3jfq-g458-7qm9)\n- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-32803)\n- [Related but distinct advisory involving absolute paths](https://www.npmjs.com/advisories/1770)\n- [GitHub Advisory](https://github.com/advisories/GHSA-3jfq-g458-7qm9)\n","updated":"2021-09-23T08:07:32.379Z","id":1771,"deleted":null,"severity":"high","created":"2021-08-03T18:14:17.499Z","metadata":{"module_type":"","exploitability":8,"affected_components":""},"vulnerable_versions":"<3.2.3 || >=4.0.0 <4.4.15 || >=5.0.0 <5.0.7 || >=6.0.0 <6.1.2","overview":"The `tar` package has a high severity vulnerability before versions 3.2.3, 4.4.15, 5.0.7, and 6.1.2.\n\n### Impact\n\nArbitrary File Creation, Arbitrary File Overwrite, Arbitrary Code Execution\n\n`node-tar` aims to prevent extraction of absolute file paths by turning absolute paths into relative paths when the `preservePaths` flag is not set to `true`. This is achieved by stripping the absolute path root from any absolute file paths contained in a tar file. For example `/home/user/.bashrc` would turn into `home/user/.bashrc`. \n\nThis logic was insufficient when file paths contained repeated path roots such as `////home/user/.bashrc`. `node-tar` would only strip a single path root from such paths. When given an absolute file path with repeating path roots, the resulting path (e.g. `///home/user/.bashrc`) would still resolve to an absolute path, thus allowing arbitrary file creation and overwrite. \n\n### Workarounds\n\nUsers may work around this vulnerability without upgrading by creating a custom `onentry` method which sanitizes the `entry.path` or a `filter` method which removes entries with absolute paths.\n\n```js\nconst path = require('path')\nconst tar = require('tar')\n\ntar.x({\n file: 'archive.tgz',\n // either add this function...\n onentry: (entry) => {\n if (path.isAbsolute(entry.path)) {\n entry.path = sanitizeAbsolutePathSomehow(entry.path)\n entry.absolute = path.resolve(entry.path)\n }\n },\n\n // or this one\n filter: (file, entry) => {\n if (path.isAbsolute(entry.path)) {\n return false\n } else {\n return true\n }\n }\n})\n```\n\nUsers are encouraged to upgrade to the latest patch versions, rather than attempt to sanitize tar input themselves.","cwe":"CWE-22","patched_versions":">=3.2.3 <4.0.0 || >=4.4.15 <5.0.0 || >=5.0.7 <6.0.0 || >=6.1.2","title":"Arbitrary File Creation/Overwrite via insufficient symlink protection due to directory cache poisoning","recommendation":"Upgrade to version 3.2.3, 4.4.15, 5.0.7, 6.1.2 or later","access":"public","url":"https://npmjs.com/advisories/1771"}}} +{"type":"auditAdvisory","data":{"resolution":{"id":1779,"path":"react-js-pagination>tar","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"6.1.4","paths":["react-scripts>terser-webpack-plugin>cacache>tar"]},{"version":"2.2.2","paths":["react-js-pagination>tar"]}],"found_by":{"link":"","name":"Anonymous","email":""},"module_name":"tar","reported_by":{"link":"","name":"Anonymous","email":""},"cves":["CVE-2021-37701"],"references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-37701)\n- [GitHub Advisory](https://github.com/advisories/GHSA-9r2w-394v-53qc)\n","updated":"2021-08-31T16:11:58.986Z","id":1779,"deleted":null,"severity":"high","created":"2021-08-31T16:10:07.868Z","metadata":{"module_type":"","exploitability":7,"affected_components":""},"vulnerable_versions":"<4.4.16 || >=5.0.0 <5.0.8 || >=6.0.0 <6.1.7","overview":"### Impact\n\nArbitrary File Creation, Arbitrary File Overwrite, Arbitrary Code Execution\n\n`node-tar` aims to guarantee that any file whose location would be modified by a symbolic link is not extracted. This is, in part, achieved by ensuring that extracted directories are not symlinks. Additionally, in order to prevent unnecessary stat calls to determine whether a given path is a directory, paths are cached when directories are created.\n\nThis logic was insufficient when extracting tar files that contained both a directory and a symlink with the same name as the directory, where the symlink and directory names in the archive entry used backslashes as a path separator on posix systems. The cache checking logic used both `\\` and `/` characters as path separators, however `\\` is a valid filename character on posix systems.\n\nBy first creating a directory, and then replacing that directory with a symlink, it was thus possible to bypass node-tar symlink checks on directories, essentially allowing an untrusted tar file to symlink into an arbitrary location and subsequently extracting arbitrary files into that location, thus allowing arbitrary file creation and overwrite.\n\nAdditionally, a similar confusion could arise on case-insensitive filesystems. If a tar archive contained a directory at `FOO`, followed by a symbolic link named `foo`, then on case-insensitive file systems, the creation of the symbolic link would remove the directory from the filesystem, but _not_ from the internal directory cache, as it would not be treated as a cache hit. A subsequent file entry within the `FOO` directory would then be placed in the target of the symbolic link, thinking that the directory had already been created. \n\nThese issues were addressed in releases 4.4.16, 5.0.8 and 6.1.7.\n\nThe v3 branch of `node-tar` has been deprecated and did not receive patches for these issues. If you are still using a v3 release we recommend you update to a more recent version of `node-tar`. If this is not possible, a workaround is available below.\n\n### Patches\n\n4.4.16 || 5.0.8 || 6.1.7\n\n### Workarounds\n\nUsers may work around this vulnerability without upgrading by creating a custom filter method which prevents the extraction of symbolic links.\n\n```js\nconst tar = require('tar')\n\ntar.x({\n file: 'archive.tgz',\n filter: (file, entry) => {\n if (entry.type === 'SymbolicLink') {\n return false\n } else {\n return true\n }\n }\n})\n```\n\nUsers are encouraged to upgrade to the latest patched versions, rather than attempt to sanitize tar input themselves.\n\n### Fix\n\nThe problem is addressed in the following ways:\n\n1. All paths are normalized to use `/` as a path separator, replacing `\\` with `/` on Windows systems, and leaving `\\` intact in the path on posix systems. This is performed in depth, at every level of the program where paths are consumed.\n2. Directory cache pruning is performed case-insensitively. This _may_ result in undue cache misses on case-sensitive file systems, but the performance impact is negligible.\n\n#### Caveat\n\nNote that this means that the `entry` objects exposed in various parts of tar's API will now always use `/` as a path separator, even on Windows systems. This is not expected to cause problems, as `/` is a valid path separator on Windows systems, but _may_ result in issues if `entry.path` is compared against a path string coming from some other API such as `fs.realpath()` or `path.resolve()`.\n\nUsers are encouraged to always normalize paths using a well-tested method such as `path.resolve()` before comparing paths to one another.","cwe":"CWE-22","patched_versions":">=4.4.16 <5.0.0 || >=5.0.8 <6.0.0 || >=6.1.7","title":"Arbitrary File Creation/Overwrite via insufficient symlink protection due to directory cache poisoning using symbolic links","recommendation":"Upgrade to versions 4.4.16, 5.0.8, 6.1.7 or later","access":"public","url":"https://npmjs.com/advisories/1779"}}} +{"type":"auditAdvisory","data":{"resolution":{"id":1780,"path":"react-js-pagination>tar","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"6.1.4","paths":["react-scripts>terser-webpack-plugin>cacache>tar"]},{"version":"2.2.2","paths":["react-js-pagination>tar"]}],"found_by":{"link":"","name":"Anonymous","email":""},"module_name":"tar","reported_by":{"link":"","name":"Anonymous","email":""},"cves":["CVE-2021-37712"],"references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-37712)\n- [GitHub Advisory](https://github.com/advisories/GHSA-qq89-hq3f-393p)\n","updated":"2021-08-31T16:12:52.860Z","id":1780,"deleted":null,"severity":"high","created":"2021-08-31T16:10:17.945Z","metadata":{"module_type":"","exploitability":7,"affected_components":""},"vulnerable_versions":"<4.4.18 || >=5.0.0 <5.0.10 || >=6.0.0 <6.1.9","overview":"### Impact\nArbitrary File Creation, Arbitrary File Overwrite, Arbitrary Code Execution\n\nnode-tar aims to guarantee that any file whose location would be modified by a symbolic link is not extracted. This is, in part, achieved by ensuring that extracted directories are not symlinks. Additionally, in order to prevent unnecessary stat calls to determine whether a given path is a directory, paths are cached when directories are created.\n\nThis logic was insufficient when extracting tar files that contained two directories and a symlink with names containing unicode values that normalized to the same value. Additionally, on Windows systems, long path portions would resolve to the same file system entities as their 8.3 \"short path\" counterparts. A specially crafted tar archive could thus include directories with two forms of the path that resolve to the same file system entity, followed by a symbolic link with a name in the first form, lastly followed by a file using the second form. It led to bypassing node-tar symlink checks on directories, essentially allowing an untrusted tar file to symlink into an arbitrary location and subsequently extracting arbitrary files into that location, thus allowing arbitrary file creation and overwrite.\n\nThe v3 branch of `node-tar` has been deprecated and did not receive patches for these issues. If you are still using a v3 release we recommend you update to a more recent version of `node-tar`. If this is not possible, a workaround is available below.\n\n### Patches\n\n6.1.9 || 5.0.10 || 4.4.18\n\n### Workarounds\n\nUsers may work around this vulnerability without upgrading by creating a custom filter method which prevents the extraction of symbolic links.\n\n```js\nconst tar = require('tar')\n\ntar.x({\n file: 'archive.tgz',\n filter: (file, entry) => {\n if (entry.type === 'SymbolicLink') {\n return false\n } else {\n return true\n }\n }\n})\n```\n\nUsers are encouraged to upgrade to the latest patched versions, rather than attempt to sanitize tar input themselves.\n\n#### Fix\n\nThe problem is addressed in the following ways, when comparing paths in the directory cache and path reservation systems:\n\n1. The `String.normalize('NFKD')` method is used to first normalize all unicode to its maximally compatible and multi-code-point form.\n2. All slashes are normalized to `/` on Windows systems (on posix systems, `\\` is a valid filename character, and thus left intact).\n3. When a symbolic link is encountered on Windows systems, the entire directory cache is cleared. Collisions related to use of 8.3 short names to replace directories with other (non-symlink) types of entries may make archives fail to extract properly, but will not result in arbitrary file writes.\n","cwe":"CWE-22","patched_versions":">=4.4.18 <5.0.0 || >=5.0.10 <6.0.0 || >=6.1.9","title":"Arbitrary File Creation/Overwrite via insufficient symlink protection due to directory cache poisoning using symbolic links","recommendation":"Upgrade to versions 4.4.18, 5.0.10, 6.1.9 or later","access":"public","url":"https://npmjs.com/advisories/1780"}}} +{"type":"auditAdvisory","data":{"resolution":{"id":1781,"path":"react-js-pagination>tar","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"6.1.4","paths":["react-scripts>terser-webpack-plugin>cacache>tar"]},{"version":"2.2.2","paths":["react-js-pagination>tar"]}],"found_by":{"link":"","name":"Anonymous","email":""},"module_name":"tar","reported_by":{"link":"","name":"Anonymous","email":""},"cves":["CVE-2021-37713"],"references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-37713)\n- [GitHub Advisory](https://github.com/advisories/GHSA-5955-9wpr-37jh)\n","updated":"2021-08-31T16:12:58.622Z","id":1781,"deleted":null,"severity":"high","created":"2021-08-31T16:10:27.513Z","metadata":{"module_type":"","exploitability":7,"affected_components":""},"vulnerable_versions":"<4.4.18 || >=5.0.0 <5.0.10 || >=6.0.0 <6.1.9","overview":"### Impact\n\nArbitrary File Creation, Arbitrary File Overwrite, Arbitrary Code Execution\n\nnode-tar aims to guarantee that any file whose location would be outside of the extraction target directory is not extracted. This is, in part, accomplished by sanitizing absolute paths of entries within the archive, skipping archive entries that contain `..` path portions, and resolving the sanitized paths against the extraction target directory.\n\nThis logic was insufficient on Windows systems when extracting tar files that contained a path that was not an absolute path, but specified a drive letter different from the extraction target, such as `C:some\\path`. If the drive letter does not match the extraction target, for example `D:\\extraction\\dir`, then the result of `path.resolve(extractionDirectory, entryPath)` would resolve against the current working directory on the `C:` drive, rather than the extraction target directory.\n\nAdditionally, a `..` portion of the path could occur immediately after the drive letter, such as `C:../foo`, and was not properly sanitized by the logic that checked for `..` within the normalized and split portions of the path.\n\nThis only affects users of `node-tar` on Windows systems.\n\n### Patches\n\n4.4.18 || 5.0.10 || 6.1.9\n\n### Workarounds\n\nThere is no reasonable way to work around this issue without performing the same path normalization procedures that node-tar now does.\n\nUsers are encouraged to upgrade to the latest patched versions of node-tar, rather than attempt to sanitize paths themselves.\n\n### Fix\n\nThe fixed versions strip path roots from all paths prior to being resolved against the extraction target folder, even if such paths are not \"absolute\".\n\nAdditionally, a path starting with a drive letter and then two dots, like `c:../`, would bypass the check for `..` path portions. This is checked properly in the patched versions.\n\nFinally, a defense in depth check is added, such that if the `entry.absolute` is outside of the extraction taret, and we are not in preservePaths:true mode, a warning is raised on that entry, and it is skipped. Currently, it is believed that this check is redundant, but it did catch some oversights in development.\n","cwe":"CWE-22","patched_versions":">=4.4.18 <5.0.0 || >=5.0.10 <6.0.0 || >=6.1.9","title":"Arbitrary File Creation/Overwrite on Windows via insufficient relative path sanitization","recommendation":"Upgrade to versions 4.4.18, 5.0.10, 6.1.9 or later","access":"public","url":"https://npmjs.com/advisories/1781"}}} diff --git a/yarn-audit-known-issues b/yarn-audit-known-issues index 704a6d7be0..f5d468031a 100644 --- a/yarn-audit-known-issues +++ b/yarn-audit-known-issues @@ -1,3 +1,3 @@ -{"type":"auditAdvisory","data":{"resolution":{"id":1751,"path":"@babel/cli>@nicolo-ribaudo/chokidar-2>glob-parent","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"3.1.0","paths":["@babel/cli>@nicolo-ribaudo/chokidar-2>glob-parent"]}],"id":1751,"created":"2021-06-07T21:57:10.135Z","updated":"2021-06-07T21:58:07.745Z","deleted":null,"title":"Regular expression denial of service","found_by":{"link":"","name":"Anonymous","email":""},"reported_by":{"link":"","name":"Anonymous","email":""},"module_name":"glob-parent","cves":["CVE-2020-28469"],"vulnerable_versions":"<5.1.2","patched_versions":">=5.1.2","overview":"`glob-parent` before 5.1.2 has a regular expression denial of service vulnerability. The enclosure regex used to check for strings ending in enclosure containing path separator.","recommendation":"Upgrade to version 5.1.2 or later","references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2020-28469)\n- [GitHub Advisory](https://github.com/advisories/GHSA-ww39-953v-wcq6)\n","access":"public","severity":"moderate","cwe":"CWE-400","metadata":{"module_type":"","exploitability":5,"affected_components":""},"url":"https://npmjs.com/advisories/1751"}}} -{"type":"auditAdvisory","data":{"resolution":{"id":1774,"path":"@axe-core/cli>selenium-webdriver>jszip","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"3.6.0","paths":["@axe-core/cli>selenium-webdriver>jszip","selenium-webdriver>jszip"]}],"id":1774,"created":"2021-08-10T16:10:19.561Z","updated":"2021-08-10T16:12:08.480Z","deleted":null,"title":"Prototype Pollution","found_by":{"link":"","name":"Anonymous","email":""},"reported_by":{"link":"","name":"Anonymous","email":""},"module_name":"jszip","cves":["CVE-2021-23413"],"vulnerable_versions":"<3.7.0","patched_versions":">=3.7.0","overview":"Affected versions of `jszip` have a prototype pollution vulnerability. Crafting a new zip file with filenames set to Object prototype values (e.g __proto__, toString, etc) results in a returned object with a modified prototype instance.","recommendation":"Upgrade to version 3.7.0 or later","references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-23413)\n- [GitHub Advisory](https://github.com/advisories/GHSA-jg8v-48h5-wgxg)\n","access":"public","severity":"moderate","cwe":"CWE-1321","metadata":{"module_type":"","exploitability":5,"affected_components":""},"url":"https://npmjs.com/advisories/1774"}}} -{"type":"auditAdvisory","data":{"resolution":{"id":1774,"path":"selenium-webdriver>jszip","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"3.6.0","paths":["@axe-core/cli>selenium-webdriver>jszip","selenium-webdriver>jszip"]}],"id":1774,"created":"2021-08-10T16:10:19.561Z","updated":"2021-08-10T16:12:08.480Z","deleted":null,"title":"Prototype Pollution","found_by":{"link":"","name":"Anonymous","email":""},"reported_by":{"link":"","name":"Anonymous","email":""},"module_name":"jszip","cves":["CVE-2021-23413"],"vulnerable_versions":"<3.7.0","patched_versions":">=3.7.0","overview":"Affected versions of `jszip` have a prototype pollution vulnerability. Crafting a new zip file with filenames set to Object prototype values (e.g __proto__, toString, etc) results in a returned object with a modified prototype instance.","recommendation":"Upgrade to version 3.7.0 or later","references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-23413)\n- [GitHub Advisory](https://github.com/advisories/GHSA-jg8v-48h5-wgxg)\n","access":"public","severity":"moderate","cwe":"CWE-1321","metadata":{"module_type":"","exploitability":5,"affected_components":""},"url":"https://npmjs.com/advisories/1774"}}} +{"type":"auditAdvisory","data":{"resolution":{"id":1751,"path":"@babel/cli>@nicolo-ribaudo/chokidar-2>glob-parent","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"3.1.0","paths":["@babel/cli>@nicolo-ribaudo/chokidar-2>glob-parent"]}],"found_by":{"link":"","name":"Anonymous","email":""},"module_name":"glob-parent","reported_by":{"link":"","name":"Anonymous","email":""},"cves":["CVE-2020-28469"],"references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2020-28469)\n- [GitHub Advisory](https://github.com/advisories/GHSA-ww39-953v-wcq6)\n","updated":"2021-06-07T21:58:07.745Z","id":1751,"deleted":null,"severity":"moderate","created":"2021-06-07T21:57:10.135Z","metadata":{"module_type":"","exploitability":5,"affected_components":""},"vulnerable_versions":"<5.1.2","overview":"`glob-parent` before 5.1.2 has a regular expression denial of service vulnerability. The enclosure regex used to check for strings ending in enclosure containing path separator.","cwe":"CWE-400","patched_versions":">=5.1.2","title":"Regular expression denial of service","recommendation":"Upgrade to version 5.1.2 or later","access":"public","url":"https://npmjs.com/advisories/1751"}}} +{"type":"auditAdvisory","data":{"resolution":{"id":1774,"path":"@axe-core/cli>selenium-webdriver>jszip","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"3.6.0","paths":["@axe-core/cli>selenium-webdriver>jszip","selenium-webdriver>jszip"]}],"found_by":{"link":"","name":"Anonymous","email":""},"module_name":"jszip","reported_by":{"link":"","name":"Anonymous","email":""},"cves":["CVE-2021-23413"],"references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-23413)\n- [GitHub Advisory](https://github.com/advisories/GHSA-jg8v-48h5-wgxg)\n","updated":"2021-08-10T16:12:08.480Z","id":1774,"deleted":null,"severity":"moderate","created":"2021-08-10T16:10:19.561Z","metadata":{"module_type":"","exploitability":5,"affected_components":""},"vulnerable_versions":"<3.7.0","overview":"Affected versions of `jszip` have a prototype pollution vulnerability. Crafting a new zip file with filenames set to Object prototype values (e.g __proto__, toString, etc) results in a returned object with a modified prototype instance.","cwe":"CWE-1321","patched_versions":">=3.7.0","title":"Prototype Pollution","recommendation":"Upgrade to version 3.7.0 or later","access":"public","url":"https://npmjs.com/advisories/1774"}}} +{"type":"auditAdvisory","data":{"resolution":{"id":1774,"path":"selenium-webdriver>jszip","dev":false,"optional":false,"bundled":false},"advisory":{"findings":[{"version":"3.6.0","paths":["@axe-core/cli>selenium-webdriver>jszip","selenium-webdriver>jszip"]}],"found_by":{"link":"","name":"Anonymous","email":""},"module_name":"jszip","reported_by":{"link":"","name":"Anonymous","email":""},"cves":["CVE-2021-23413"],"references":"- [CVE](https://nvd.nist.gov/vuln/detail/CVE-2021-23413)\n- [GitHub Advisory](https://github.com/advisories/GHSA-jg8v-48h5-wgxg)\n","updated":"2021-08-10T16:12:08.480Z","id":1774,"deleted":null,"severity":"moderate","created":"2021-08-10T16:10:19.561Z","metadata":{"module_type":"","exploitability":5,"affected_components":""},"vulnerable_versions":"<3.7.0","overview":"Affected versions of `jszip` have a prototype pollution vulnerability. Crafting a new zip file with filenames set to Object prototype values (e.g __proto__, toString, etc) results in a returned object with a modified prototype instance.","cwe":"CWE-1321","patched_versions":">=3.7.0","title":"Prototype Pollution","recommendation":"Upgrade to version 3.7.0 or later","access":"public","url":"https://npmjs.com/advisories/1774"}}} From 6709bb01a041bf25a5abb64d74f203785f230079 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Mon, 27 Sep 2021 11:31:51 -0400 Subject: [PATCH 18/34] starting to fill test coverage gaps --- .../pages/GranteeSearch/__tests__/index.js | 34 ++++++++++++++++++- frontend/src/pages/GranteeSearch/index.js | 1 + src/lib/orderGranteesBy.test.js | 2 +- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/GranteeSearch/__tests__/index.js b/frontend/src/pages/GranteeSearch/__tests__/index.js index 99c0f434e1..e548c45cfc 100644 --- a/frontend/src/pages/GranteeSearch/__tests__/index.js +++ b/frontend/src/pages/GranteeSearch/__tests__/index.js @@ -136,9 +136,41 @@ describe('the grantee search page', () => { await act(async () => { fireEvent.click(button); - sortButton.click(button); + fireEvent.click(sortButton); + }); + + fetchMock.get('/api/grantee/search?s=ground%20control®ion=1&sortBy=programSpecialist&direction=asc&offset=0', res); + + await act(async () => { + fireEvent.click(sortButton); }); await waitFor(() => expect(screen.getByText('major tom')).toBeInTheDocument()); }); + + it('handles an error', async () => { + const searchBox = screen.getByRole('searchbox'); + const button = screen.getByRole('button', { name: /search for matching grantees/i }); + + expect(button).toBeInTheDocument(); + expect(searchBox).toBeInTheDocument(); + userEvent.type(searchBox, 'ground control'); + + await act(async () => { + fireEvent.click(button); + }); + + let majorTom; + + await waitFor(() => { + majorTom = screen.getByText('major tom'); + expect(majorTom).toBeInTheDocument(); + }); + + fetchMock.get('/api/grantee/search?s=ground%20control®ion=1&sortBy=programSpecialist&direction=desc&offset=0', 500); + await act(async () => { + fireEvent.click(button); + }); + expect(majorTom).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/pages/GranteeSearch/index.js b/frontend/src/pages/GranteeSearch/index.js index b4174c3be0..665cdf4c8c 100644 --- a/frontend/src/pages/GranteeSearch/index.js +++ b/frontend/src/pages/GranteeSearch/index.js @@ -37,6 +37,7 @@ function GranteeSearch({ user }) { const { rows, count } = await searchGrantees(query, appliedRegion, { ...sortConfig, offset }); setResults(rows); setGranteeCount(count); + console.log(rows); } catch (err) { // eslint-disable-next-line no-console console.log(err); diff --git a/src/lib/orderGranteesBy.test.js b/src/lib/orderGranteesBy.test.js index 7ca3f604e6..1860acb665 100644 --- a/src/lib/orderGranteesBy.test.js +++ b/src/lib/orderGranteesBy.test.js @@ -18,7 +18,7 @@ describe('orderGranteesBy', () => { 'desc', ]]); - const three = orderGranteesBy('', 'asc'); + const three = orderGranteesBy('programSpecialist', 'asc'); expect(three).toStrictEqual([ [ From e84c3cd99a10c6317096506c77637afba17c6964 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Mon, 27 Sep 2021 12:20:38 -0400 Subject: [PATCH 19/34] more work on ui tests --- frontend/src/fetchers/grantee.js | 139 +++++++++++++++++- .../pages/GranteeSearch/__tests__/index.js | 138 ++++++++++++++++- frontend/src/pages/GranteeSearch/index.js | 7 +- 3 files changed, 269 insertions(+), 15 deletions(-) diff --git a/frontend/src/fetchers/grantee.js b/frontend/src/fetchers/grantee.js index 4f5fa86dfb..b063bfd1b9 100644 --- a/frontend/src/fetchers/grantee.js +++ b/frontend/src/fetchers/grantee.js @@ -1,7 +1,7 @@ import join from 'url-join'; -import { - get, -} from './index'; +// import { +// get, +// } from './index'; import { DECIMAL_BASE } from '../Constants'; const granteeUrl = join('/', 'api', 'grantee'); @@ -15,9 +15,134 @@ export const searchGrantees = async (query, regionId = '', params = { sortBy: 'n const querySearch = `?s=${query}`; const regionSearch = regionId ? `®ion=${regionId.toString(DECIMAL_BASE)}` : ''; - const grantees = await get( - join(granteeUrl, 'search', querySearch, regionSearch, `&sortBy=${params.sortBy}&direction=${params.direction}&offset=${params.offset}`), - ); + console.log(join(granteeUrl, 'search', querySearch, regionSearch, `&sortBy=${params.sortBy}&direction=${params.direction}&offset=${params.offset}`)); - return grantees.json(); + // const grantees = await get( + // join(granteeUrl, 'search', querySearch, regionSearch, + // `&sortBy=${params.sortBy}&direction=${params.direction}&offset=${params.offset}`), + // ); + // return grantees.json(); + + return { + rows: [ + { + id: 2, + name: 'major tom', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 3, + name: 'major bob', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 4, + name: 'major sara', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 5, + name: 'major tara', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 6, + name: 'major jim', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 7, + name: 'major xi', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 1, + name: 'major larry', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 8, + name: 'major maggie', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 10, + name: 'major brian', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 11, + name: 'major chumley', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 12, + name: 'major karen', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 13, + name: 'major superhero', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 14, + name: 'major barack', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + ], + count: 13, + }; }; diff --git a/frontend/src/pages/GranteeSearch/__tests__/index.js b/frontend/src/pages/GranteeSearch/__tests__/index.js index e548c45cfc..6ea3186cac 100644 --- a/frontend/src/pages/GranteeSearch/__tests__/index.js +++ b/frontend/src/pages/GranteeSearch/__tests__/index.js @@ -26,7 +26,7 @@ const userBluePrint = { const history = createMemoryHistory(); const res = { - count: 1, + count: 13, rows: [ { id: 2, @@ -37,6 +37,114 @@ const res = { }, ], }, + { + id: 3, + name: 'major bob', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 4, + name: 'major sara', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 5, + name: 'major tara', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 6, + name: 'major jim', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 7, + name: 'major xi', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 1, + name: 'major larry', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 8, + name: 'major maggie', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 10, + name: 'major brian', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 11, + name: 'major chumley', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 12, + name: 'major karen', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 13, + name: 'major superhero', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + { + id: 14, + name: 'major barack', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, ], }; @@ -148,7 +256,7 @@ describe('the grantee search page', () => { await waitFor(() => expect(screen.getByText('major tom')).toBeInTheDocument()); }); - it('handles an error', async () => { + it('requests the next page', async () => { const searchBox = screen.getByRole('searchbox'); const button = screen.getByRole('button', { name: /search for matching grantees/i }); @@ -160,6 +268,25 @@ describe('the grantee search page', () => { fireEvent.click(button); }); + const next = await screen.findByRole('link', { name: /go to page number 2/i }); + + await act(async () => { + fireEvent.click(next); + }); + + screen.logTestingPlaygroundURL(); + expect(true).toBe(false); + }); + + it('handles an error', async () => { + const searchBox = screen.getByRole('searchbox'); + const button = screen.getByRole('button', { name: /search for matching grantees/i }); + userEvent.type(searchBox, 'ground control'); + + await act(async () => { + fireEvent.click(button); + }); + let majorTom; await waitFor(() => { @@ -167,10 +294,13 @@ describe('the grantee search page', () => { expect(majorTom).toBeInTheDocument(); }); - fetchMock.get('/api/grantee/search?s=ground%20control®ion=1&sortBy=programSpecialist&direction=desc&offset=0', 500); + fetchMock.get('/api/grantee/search?s=ground%20controls®ion=1&sortBy=name&direction=desc&offset=0', 404); + userEvent.clear(searchBox); + userEvent.type(searchBox, 'ground controls'); + await act(async () => { fireEvent.click(button); }); - expect(majorTom).not.toBeInTheDocument(); + await waitFor(() => expect(majorTom).not.toBeInTheDocument()); }); }); diff --git a/frontend/src/pages/GranteeSearch/index.js b/frontend/src/pages/GranteeSearch/index.js index 665cdf4c8c..cec3bfb9f4 100644 --- a/frontend/src/pages/GranteeSearch/index.js +++ b/frontend/src/pages/GranteeSearch/index.js @@ -37,10 +37,7 @@ function GranteeSearch({ user }) { const { rows, count } = await searchGrantees(query, appliedRegion, { ...sortConfig, offset }); setResults(rows); setGranteeCount(count); - console.log(rows); } catch (err) { - // eslint-disable-next-line no-console - console.log(err); setResults([]); setGranteeCount(0); } finally { @@ -68,10 +65,12 @@ function GranteeSearch({ user }) { async function handlePageChange(pageNumber) { if (!loading) { + console.log(offset, pageNumber); setActivePage(pageNumber); setOffset((pageNumber - 1) * GRANTEES_PER_PAGE); + + await fetchGrantees(); } - await fetchGrantees(); } async function onSubmit(e) { From 408e718e398a03a0ea8038fbb1cb9b25e612422d Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Mon, 27 Sep 2021 13:24:16 -0400 Subject: [PATCH 20/34] return fetchers to prev state --- frontend/src/fetchers/grantee.js | 138 ++----------------------------- 1 file changed, 5 insertions(+), 133 deletions(-) diff --git a/frontend/src/fetchers/grantee.js b/frontend/src/fetchers/grantee.js index b063bfd1b9..809a1ccb01 100644 --- a/frontend/src/fetchers/grantee.js +++ b/frontend/src/fetchers/grantee.js @@ -1,7 +1,5 @@ import join from 'url-join'; -// import { -// get, -// } from './index'; +import { get } from './index'; import { DECIMAL_BASE } from '../Constants'; const granteeUrl = join('/', 'api', 'grantee'); @@ -15,134 +13,8 @@ export const searchGrantees = async (query, regionId = '', params = { sortBy: 'n const querySearch = `?s=${query}`; const regionSearch = regionId ? `®ion=${regionId.toString(DECIMAL_BASE)}` : ''; - console.log(join(granteeUrl, 'search', querySearch, regionSearch, `&sortBy=${params.sortBy}&direction=${params.direction}&offset=${params.offset}`)); - - // const grantees = await get( - // join(granteeUrl, 'search', querySearch, regionSearch, - // `&sortBy=${params.sortBy}&direction=${params.direction}&offset=${params.offset}`), - // ); - // return grantees.json(); - - return { - rows: [ - { - id: 2, - name: 'major tom', - grants: [ - { - programSpecialistName: 'someone else', - }, - ], - }, - { - id: 3, - name: 'major bob', - grants: [ - { - programSpecialistName: 'someone else', - }, - ], - }, - { - id: 4, - name: 'major sara', - grants: [ - { - programSpecialistName: 'someone else', - }, - ], - }, - { - id: 5, - name: 'major tara', - grants: [ - { - programSpecialistName: 'someone else', - }, - ], - }, - { - id: 6, - name: 'major jim', - grants: [ - { - programSpecialistName: 'someone else', - }, - ], - }, - { - id: 7, - name: 'major xi', - grants: [ - { - programSpecialistName: 'someone else', - }, - ], - }, - { - id: 1, - name: 'major larry', - grants: [ - { - programSpecialistName: 'someone else', - }, - ], - }, - { - id: 8, - name: 'major maggie', - grants: [ - { - programSpecialistName: 'someone else', - }, - ], - }, - { - id: 10, - name: 'major brian', - grants: [ - { - programSpecialistName: 'someone else', - }, - ], - }, - { - id: 11, - name: 'major chumley', - grants: [ - { - programSpecialistName: 'someone else', - }, - ], - }, - { - id: 12, - name: 'major karen', - grants: [ - { - programSpecialistName: 'someone else', - }, - ], - }, - { - id: 13, - name: 'major superhero', - grants: [ - { - programSpecialistName: 'someone else', - }, - ], - }, - { - id: 14, - name: 'major barack', - grants: [ - { - programSpecialistName: 'someone else', - }, - ], - }, - ], - count: 13, - }; + const grantees = await get( + join(granteeUrl, 'search', querySearch, regionSearch, `&sortBy=${params.sortBy}&direction=${params.direction}&offset=${params.offset}`), + ); + return grantees.json(); }; From 57e8100b471e33c52314ed5d4cc4414270082622 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Mon, 27 Sep 2021 13:52:22 -0400 Subject: [PATCH 21/34] ui test for grantee search --- .../pages/GranteeSearch/__tests__/index.js | 41 ++++----- frontend/src/pages/GranteeSearch/index.js | 84 ++++++++++++++----- 2 files changed, 82 insertions(+), 43 deletions(-) diff --git a/frontend/src/pages/GranteeSearch/__tests__/index.js b/frontend/src/pages/GranteeSearch/__tests__/index.js index 6ea3186cac..7c8e1b4fde 100644 --- a/frontend/src/pages/GranteeSearch/__tests__/index.js +++ b/frontend/src/pages/GranteeSearch/__tests__/index.js @@ -136,15 +136,6 @@ const res = { }, ], }, - { - id: 14, - name: 'major barack', - grants: [ - { - programSpecialistName: 'someone else', - }, - ], - }, ], }; @@ -198,22 +189,17 @@ describe('the grantee search page', () => { expect(button).toBeInTheDocument(); expect(searchBox).toBeInTheDocument(); - + userEvent.type(searchBox, 'ground control'); + await act(async () => fireEvent.click(button)); const regionalSelect = screen.getByRole('button', { name: /open regional select menu/i }); fireEvent.click(regionalSelect); const region2 = screen.getByRole('button', { name: /select to view data from region 2\. select apply filters button to apply selection/i }); fireEvent.click(region2); const applyFilters = screen.getByRole('button', { name: /apply filters for the regional select menu/i }); - fireEvent.click(applyFilters); - userEvent.type(searchBox, 'ground control'); - fetchMock.get('/api/grantee/search?s=ground%20control®ion=2&sortBy=name&direction=desc&offset=0', res); - - await act(async () => { - fireEvent.click(button); - }); - + await act(async () => fireEvent.click(applyFilters)); await waitFor(() => expect(screen.getByText('major tom')).toBeInTheDocument()); + expect(fetchMock.called()).toBeTruthy(); }); it('the search bar works', async () => { @@ -270,12 +256,27 @@ describe('the grantee search page', () => { const next = await screen.findByRole('link', { name: /go to page number 2/i }); + fetchMock.get('/api/grantee/search?s=ground%20control®ion=1&sortBy=name&direction=desc&offset=12', + { + count: 13, + rows: [ + { + id: 14, + name: 'major barack', + grants: [ + { + programSpecialistName: 'someone else', + }, + ], + }, + ], + }); + await act(async () => { fireEvent.click(next); }); - screen.logTestingPlaygroundURL(); - expect(true).toBe(false); + await waitFor(() => expect(screen.getByText('major barack')).toBeInTheDocument()); }); it('handles an error', async () => { diff --git a/frontend/src/pages/GranteeSearch/index.js b/frontend/src/pages/GranteeSearch/index.js index cec3bfb9f4..ddaa5d81d3 100644 --- a/frontend/src/pages/GranteeSearch/index.js +++ b/frontend/src/pages/GranteeSearch/index.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; import { Grid } from '@trussworks/react-uswds'; @@ -27,55 +27,93 @@ function GranteeSearch({ user }) { direction: 'desc', }); - async function fetchGrantees() { - if (!query || loading) { - return; - } + const inputRef = useRef(); - try { - setLoading(true); - const { rows, count } = await searchGrantees(query, appliedRegion, { ...sortConfig, offset }); - setResults(rows); - setGranteeCount(count); - } catch (err) { - setResults([]); - setGranteeCount(0); - } finally { - setLoading(false); + useEffect(() => { + async function fetchGrantees() { + /** + * get up to date query + */ + if (inputRef.current) { + setQuery(inputRef.current.value); + } + /** + * We assume the function that changed the state also changed loading. + * That's why, if the app is not in a loading state, we return + */ + + if (!query || !loading) { + return; + } + + /** + * if we have no query, the form was submitted in error (extra button press, etc) + */ + if (!query) { + setLoading(false); + return; + } + + try { + const { + rows, + count, + } = await searchGrantees(query, appliedRegion, { ...sortConfig, offset }); + setResults(rows); + setGranteeCount(count); + } catch (err) { + setResults([]); + setGranteeCount(0); + } finally { + setLoading(false); + } } - } + + fetchGrantees(); + }, [appliedRegion, loading, offset, query, sortConfig]); function onApplyRegion(region) { setAppliedRegion(region.value); + setLoading(true); } async function requestSort(sortBy) { + if (loading) { + return; + } + const config = sortConfig; if (config.sortBy === sortBy) { config.direction = config.direction === 'asc' ? 'desc' : 'asc'; setSortConfig(config); - await fetchGrantees(); return; } config.sortBy = sortBy; setSortConfig(config); - await fetchGrantees(); + setLoading(true); } async function handlePageChange(pageNumber) { if (!loading) { - console.log(offset, pageNumber); setActivePage(pageNumber); setOffset((pageNumber - 1) * GRANTEES_PER_PAGE); - - await fetchGrantees(); + setLoading(true); } } async function onSubmit(e) { e.preventDefault(); - await fetchGrantees(); + + if (loading) { + return; + } + + if (inputRef.current) { + setQuery(inputRef.current.value); + } + + setLoading(true); } return ( @@ -98,7 +136,7 @@ function GranteeSearch({ user }) { )}
    - setQuery(e.target.value)} /> +