From c2689914954146b704a921621c9fbacf7f33b354 Mon Sep 17 00:00:00 2001 From: Andreea-Lupu Date: Thu, 8 Feb 2024 17:01:17 +0200 Subject: [PATCH] fix(cve): make cards collapsed by default Signed-off-by: Andreea-Lupu --- .github/workflows/end-to-end-test.yml | 2 +- .../TagPage/VulnerabilitiesDetails.test.js | 116 +++++++++++------- src/components/Shared/VulnerabilityCard.jsx | 8 +- .../Tag/Tabs/VulnerabilitiesDetails.jsx | 2 +- 4 files changed, 82 insertions(+), 46 deletions(-) diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index a74408be..0fa65d6c 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -81,7 +81,7 @@ jobs: - name: Install go uses: actions/setup-go@v3 with: - go-version: 1.20.x + go-version: 1.21.x - name: Checkout zot repo uses: actions/checkout@v3 diff --git a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js index 5c980ca5..d35819ef 100644 --- a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js +++ b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js @@ -450,10 +450,34 @@ const mockCVEListFiltered = { CVEListForImage: { Tag: '', Page: { ItemCount: 20, TotalCount: 20 }, + Summary: { + Count: 5, + UnknownCount: 1, + LowCount: 1, + MediumCount: 1, + HighCount: 1, + CriticalCount: 1, + }, CVEList: mockCVEList.CVEListForImage.CVEList.filter((e) => e.Id.includes('2022')) } }; +const mockCVEListFilteredExclude = { + CVEListForImage: { + Tag: '', + Page: { ItemCount: 20, TotalCount: 20 }, + Summary: { + Count: 5, + UnknownCount: 1, + LowCount: 1, + MediumCount: 1, + HighCount: 1, + CriticalCount: 1, + }, + CVEList: mockCVEList.CVEListForImage.CVEList.filter((e) => !e.Id.includes('2022')) + } +}; + const mockCVEFixed = { pageOne: { ImageListWithCVEFixed: { @@ -510,37 +534,29 @@ afterEach(() => { describe('Vulnerabilties page', () => { it('renders the vulnerabilities if there are any', async () => { - let getCall = jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); - for (let i=0; i<21; i++) { - getCall.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageNotFixed } }); - } - getCall.mockResolvedValue({ status: 200, data: { data: mockCVEList } }); + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } }); render(); await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); await waitFor(() => expect(screen.getAllByText('Total 5')).toHaveLength(1)); - await waitFor(() => expect(screen.getAllByText(/Fixed in/)).toHaveLength(20)); + await waitFor(() => expect(screen.getAllByText(/CVE/)).toHaveLength(20)); }); it('sends filtered query if user types in the search bar', async () => { - let getCall = jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); - for (let i=0; i<21; i++) { - getCall.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageNotFixed } }); - } - getCall.mockResolvedValue({ status: 200, data: { data: mockCVEList } }); + jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); render(); + await waitFor(() => expect(screen.getAllByText(/CVE/)).toHaveLength(20)); const cveSearchInput = screen.getByPlaceholderText(/search/i); - jest.spyOn(api, 'get').mockRejectedValueOnce({ status: 200, data: { data: mockCVEListFiltered } }); + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFiltered } }); await userEvent.type(cveSearchInput, '2022'); - expect((await screen.queryAllByText(/2023/i).length) === 0); - expect((await screen.findAllByText(/2022/i)).length === 6); + expect(cveSearchInput).toHaveValue('2022') + await waitFor(() => expect(screen.queryAllByText(/2022/i)).toHaveLength(7)); + await waitFor(() => expect(screen.queryAllByText(/2021/i)).toHaveLength(1)); }); it('should have a collapsable search bar', async () => { - let getCall = jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }) - for (let i=0; i<21; i++) { - getCall.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageNotFixed } }); - } - getCall.mockResolvedValue({ status: 200, data: { data: mockCVEList } }); + jest.spyOn(api, 'get'). + mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }). + mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredExclude } }); render(); const cveSearchInput = screen.getByPlaceholderText(/search/i); const expandSearch = cveSearchInput.parentElement.parentElement.parentElement.parentElement.childNodes[0]; @@ -550,7 +566,9 @@ describe('Vulnerabilties page', () => { ); const excludeInput = screen.getByPlaceholderText("Exclude"); userEvent.type(excludeInput, '2022'); - expect((await screen.findAllByText(/2022/i)).length === 0); + expect(excludeInput).toHaveValue('2022') + await waitFor(() => expect(screen.queryAllByText(/2022/i)).toHaveLength(0)); + await waitFor(() => expect(screen.queryAllByText(/2021/i)).toHaveLength(6)); }) it('renders no vulnerabilities if there are not any', async () => { @@ -564,8 +582,10 @@ describe('Vulnerabilties page', () => { it('should show description for vulnerabilities', async () => { jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }) - .mockResolvedValue({ status: 200, data: { data: mockCVEFixed.pageNotFixed } }); + .mockResolvedValue({ status: 200, data: { data: mockCVEFixed.pageOne } }); render(); + const expandListBtn = await screen.findAllByTestId('ViewAgendaIcon'); + fireEvent.click(expandListBtn[0]); await waitFor(() => expect(screen.getAllByText(/Description/)).toHaveLength(20)); await waitFor(() => expect(screen.getAllByText(/CPAN 2.28 allows Signature Verification Bypass./i)).toHaveLength(1) @@ -587,12 +607,13 @@ describe('Vulnerabilties page', () => { .mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageTwo } }); render(); await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); + const expandListBtn = await screen.findAllByTestId('KeyboardArrowRightIcon'); + fireEvent.click(expandListBtn[1]); await waitFor(() => expect(screen.getByText('1.0.16')).toBeInTheDocument()); - await waitFor(() => expect(screen.getAllByText(/load more/i).length).toBeGreaterThan(0)); - const nrLoadButtons = screen.getAllByText(/load more/i).length - const loadMoreBtn = screen.getAllByText(/load more/i)[0]; + await waitFor(() => expect(screen.getAllByText(/Load more/).length).toBe(1)); + const loadMoreBtn = screen.getAllByText(/Load more/)[0]; await fireEvent.click(loadMoreBtn); - await waitFor(() => expect(screen.getAllByText(/load more/i).length).toBe(nrLoadButtons-1)); + await waitFor(() => expect(loadMoreBtn).not.toBeInTheDocument()); expect(await screen.findByText('latest')).toBeInTheDocument(); }); @@ -600,11 +621,7 @@ describe('Vulnerabilties page', () => { const xlsxMock = jest.createMockFromModule('xlsx'); xlsxMock.writeFile = jest.fn(); - let getCall = jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); - for (let i=0; i<21; i++) { - getCall.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageNotFixed } }); - } - getCall.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } }); render(); await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); const downloadBtn = await screen.findAllByTestId('DownloadIcon'); @@ -623,32 +640,47 @@ describe('Vulnerabilties page', () => { expect(await screen.findByTestId('export-excel-menuItem')).not.toBeInTheDocument(); }); - it('should expand/collapse the list of CVEs', async () => { - let getCall = jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); - for (let i=0; i<21; i++) { - getCall.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageNotFixed } }); - } - getCall.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); + it("should log an error when data can't be fetched for downloading", async () => { + const xlsxMock = jest.createMockFromModule('xlsx'); + xlsxMock.writeFile = jest.fn(); + + jest.spyOn(api, 'get'). + mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }). + mockRejectedValue({ status: 500, data: {} }); + const error = jest.spyOn(console, 'error').mockImplementation(() => {}); render(); await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); + const downloadBtn = await screen.findAllByTestId('DownloadIcon'); + fireEvent.click(downloadBtn[0]); + expect(await screen.findByTestId('export-csv-menuItem')).toBeInTheDocument(); + expect(await screen.findByTestId('export-excel-menuItem')).toBeInTheDocument(); + await waitFor(() => expect(error).toBeCalledTimes(1)); + }); + + it('should expand/collapse the list of CVEs', async () => { + jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); + render(); + await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEFixed.pageOne } }); + const expandListBtn = await screen.findAllByTestId('ViewAgendaIcon'); + fireEvent.click(expandListBtn[0]); await waitFor(() => expect(screen.getAllByText('Fixed in')).toHaveLength(20)); const collapseListBtn = await screen.findAllByTestId('ViewHeadlineIcon'); fireEvent.click(collapseListBtn[0]); expect(await screen.findByText('Fixed in')).not.toBeVisible(); - const expandListBtn = await screen.findAllByTestId('ViewAgendaIcon'); - fireEvent.click(expandListBtn[0]); - await waitFor(() => expect(screen.getAllByText('Fixed in')).toHaveLength(20)); }); it('should handle fixed CVE query errors', async () => { jest .spyOn(api, 'get') .mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }) - .mockRejectedValueOnce({ status: 500, data: {} }); + .mockRejectedValue({ status: 500, data: {} }); render(); await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); const error = jest.spyOn(console, 'error').mockImplementation(() => {}); - await waitFor(() => expect(screen.getAllByText(/not fixed/i).length).toBeGreaterThan(0)); - await waitFor(() => expect(error).toBeCalled()); + const expandListBtn = await screen.findAllByTestId('KeyboardArrowRightIcon'); + fireEvent.click(expandListBtn[1]); + await waitFor(() => expect(screen.getByText(/not fixed/i)).toBeInTheDocument()); + await waitFor(() => expect(error).toBeCalledTimes(1)); }); }); diff --git a/src/components/Shared/VulnerabilityCard.jsx b/src/components/Shared/VulnerabilityCard.jsx index c201c789..423eed22 100644 --- a/src/components/Shared/VulnerabilityCard.jsx +++ b/src/components/Shared/VulnerabilityCard.jsx @@ -123,9 +123,10 @@ function VulnerabilitiyCard(props) { // pagination props const [pageNumber, setPageNumber] = useState(1); const [isEndOfList, setIsEndOfList] = useState(false); + const [loadMoreInfo, setLoadMoreInfo] = useState(false); const getPaginatedResults = () => { - if (isEndOfList) { + if (!openCVE || (!loadMoreInfo && !isEmpty(fixedInfo)) || isEndOfList) { return; } setLoadingFixed(true); @@ -148,11 +149,13 @@ function VulnerabilitiyCard(props) { ); } setLoadingFixed(false); + setLoadMoreInfo(false); }) .catch((e) => { console.error(e); setIsEndOfList(true); setLoadingFixed(false); + setLoadMoreInfo(false); }); }; @@ -161,7 +164,7 @@ function VulnerabilitiyCard(props) { return () => { abortController.abort(); }; - }, [pageNumber]); + }, [openCVE, pageNumber, loadMoreInfo]); useEffect(() => { setOpenCVE(expand); @@ -169,6 +172,7 @@ function VulnerabilitiyCard(props) { const loadMore = () => { if (loadingFixed || isEndOfList) return; + setLoadMoreInfo(true); setPageNumber((pageNumber) => pageNumber + 1); }; diff --git a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx index 38c1a614..76fb17cd 100644 --- a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx +++ b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx @@ -165,7 +165,7 @@ function VulnerabilitiesDetails(props) { const [anchorExport, setAnchorExport] = useState(null); const openExport = Boolean(anchorExport); - const [selectedViewMore, setSelectedViewMore] = useState(true); + const [selectedViewMore, setSelectedViewMore] = useState(false); const getCVERequestName = () => { return digest !== '' ? `${name}@${digest}` : `${name}:${tag}`;