diff --git a/src/__tests__/TagPage/TagDetails.test.js b/src/__tests__/TagPage/TagDetails.test.js index 7ef570d7..754bdf7a 100644 --- a/src/__tests__/TagPage/TagDetails.test.js +++ b/src/__tests__/TagPage/TagDetails.test.js @@ -4,11 +4,16 @@ import userEvent from '@testing-library/user-event'; import { api } from 'api'; import TagDetails from 'components/Tag/TagDetails'; import MockThemeProvier from '__mocks__/MockThemeProvider'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; const TagDetailsThemeWrapper = () => { return ( - + + + } /> + + ); }; @@ -191,6 +196,48 @@ const mockImageHigh = { } }; +const mockDependenciesList = { + data: { + BaseImageList: { + Page: { ItemCount: 4, TotalCount: 4 }, + Results: [ + { + RepoName: 'project-stacker/c3/static-ubuntu-amd64', + Tag: 'tag1', + Vulnerabilities: { + MaxSeverity: 'HIGH', + Count: 5 + } + }, + { + RepoName: 'tag2', + Tag: 'tag2', + Vulnerabilities: { + MaxSeverity: 'CRITICAL', + Count: 2 + } + }, + { + RepoName: 'tag3', + Tag: 'tag3', + Vulnerabilities: { + MaxSeverity: 'LOW', + Count: 7 + } + }, + { + RepoName: 'tag4', + Tag: 'tag4', + Vulnerabilities: { + MaxSeverity: 'HIGH', + Count: 5 + } + } + ] + } + } +}; + // mock clipboard copy fn const mockCopyToClipboard = jest.fn(); Object.assign(navigator, { @@ -232,7 +279,7 @@ describe('Tags details', () => { it('should show tabs and allow nagivation between them', async () => { jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImage } }); render(); - jest.spyOn(api, 'get').mockResolvedValue({ status: 500, data: { data: { errors: ['test error'] } } }); + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockDependenciesList }); const dependenciesTab = await screen.findByTestId('dependencies-tab'); fireEvent.click(dependenciesTab); expect(await screen.findByTestId('depends-on-container')).toBeInTheDocument(); diff --git a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js index c503aa18..668a89bb 100644 --- a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js +++ b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js @@ -433,17 +433,35 @@ const mockCVEList = { }; const mockCVEFixed = { - ImageListWithCVEFixed: [ - { - Tag: '1.0.16' - }, - { - Tag: '0.4.33' - }, - { - Tag: '1.0.17' + pageOne: { + ImageListWithCVEFixed: { + Page: { TotalCount: 5, ItemCount: 3 }, + Results: [ + { + Tag: '1.0.16' + }, + { + Tag: '0.4.33' + }, + { + Tag: '1.0.17' + } + ] } - ] + }, + pageTwo: { + ImageListWithCVEFixed: { + Page: { TotalCount: 5, ItemCount: 2 }, + Results: [ + { + Tag: 'slim' + }, + { + Tag: 'latest' + } + ] + } + } }; beforeEach(() => { @@ -484,7 +502,7 @@ describe('Vulnerabilties page', () => { render(); await waitFor(() => expect(screen.getAllByText(/description/i)).toHaveLength(20)); const openText = screen.getAllByText(/description/i); - fireEvent.click(openText[0]); + await fireEvent.click(openText[0]); await waitFor(() => expect(screen.getAllByText(/CPAN 2.28 allows Signature Verification Bypass./i)).toHaveLength(1) ); @@ -504,13 +522,30 @@ describe('Vulnerabilties page', () => { it('should find out which version fixes the CVEs', async () => { jest .spyOn(api, 'get') - .mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }) - - .mockResolvedValue({ status: 200, data: { data: mockCVEFixed } }); + .mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageOne } }) + .mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageTwo } }); render(); await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); - fireEvent.click(screen.getAllByText(/fixed in/i)[0]); + await fireEvent.click(screen.getAllByText(/fixed in/i)[0]); await waitFor(() => expect(screen.getByText('1.0.16')).toBeInTheDocument()); + const loadMoreBtn = screen.getByText(/load more/i); + expect(loadMoreBtn).toBeInTheDocument(); + await fireEvent.click(loadMoreBtn); + await waitFor(() => expect(loadMoreBtn).not.toBeInTheDocument()); + await expect(await screen.findByText('latest')).toBeInTheDocument(); + }); + + it('should handle fixed CVE query errors', async () => { + jest + .spyOn(api, 'get') + .mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }) + .mockRejectedValue({ status: 500, data: {} }); + render(); + await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); + const error = jest.spyOn(console, 'error').mockImplementation(() => {}); + await fireEvent.click(screen.getAllByText(/fixed in/i)[0]); + await waitFor(() => expect(screen.getByText(/not fixed/i)).toBeInTheDocument()); + await waitFor(() => expect(error).toBeCalledTimes(1)); }); }); diff --git a/src/api.js b/src/api.js index 18bc8346..22894688 100644 --- a/src/api.js +++ b/src/api.js @@ -86,8 +86,10 @@ const endpoints = { }}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`, layersDetailsForImage: (name) => `/v2/_zot/ext/search?query={Image(image: "${name}"){History {Layer {Size Digest Score} HistoryDescription {Created CreatedBy Author Comment EmptyLayer} }}}`, - imageListWithCVEFixed: (cveId, repoName) => - `/v2/_zot/ext/search?query={ImageListWithCVEFixed(id:"${cveId}", image:"${repoName}") {Tag}}`, + imageListWithCVEFixed: (cveId, repoName, { pageNumber = 1, pageSize = 3 }) => + `/v2/_zot/ext/search?query={ImageListWithCVEFixed(id:"${cveId}", image:"${repoName}", requestedPage: {limit:${pageSize} offset:${ + (pageNumber - 1) * pageSize + }}) {Page {TotalCount ItemCount} Results {Tag}}}`, dependsOnForImage: (name, { pageNumber = 1, pageSize = 15 } = {}) => `/v2/_zot/ext/search?query={BaseImageList(image: "${name}", requestedPage: {limit:${pageSize} offset:${ (pageNumber - 1) * pageSize diff --git a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx index 83204016..e00b08dc 100644 --- a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx +++ b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx @@ -14,7 +14,7 @@ import Loading from '../../Shared/Loading'; import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material'; import { VulnerabilityChipCheck } from 'utilities/vulnerabilityAndSignatureCheck'; import { mapCVEInfo } from 'utilities/objectModels'; -import { EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants'; +import { CVE_FIXEDIN_PAGE_SIZE, EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants'; const useStyles = makeStyles(() => ({ card: { @@ -91,25 +91,50 @@ function VulnerabilitiyCard(props) { const [openFixed, setOpenFixed] = useState(false); const [loadingFixed, setLoadingFixed] = useState(true); const [fixedInfo, setFixedInfo] = useState([]); + const abortController = useMemo(() => new AbortController(), []); - useEffect(() => { - if (!openFixed || !isEmpty(fixedInfo)) { + // pagination props + const [pageNumber, setPageNumber] = useState(1); + const [isEndOfList, setIsEndOfList] = useState(false); + + const getPaginatedResults = () => { + if (!openFixed || isEndOfList) { return; } setLoadingFixed(true); api - .get(`${host()}${endpoints.imageListWithCVEFixed(cve.id, name)}`) + .get( + `${host()}${endpoints.imageListWithCVEFixed(cve.id, name, { pageNumber, pageSize: CVE_FIXEDIN_PAGE_SIZE })}`, + abortController.signal + ) .then((response) => { if (response.data && response.data.data) { - const fixedTagsList = response.data.data.ImageListWithCVEFixed?.map((e) => e.Tag); - setFixedInfo(fixedTagsList); + const fixedTagsList = response.data.data.ImageListWithCVEFixed?.Results?.map((e) => e.Tag); + setFixedInfo((previousState) => [...previousState, ...fixedTagsList]); + setIsEndOfList( + [...fixedInfo, ...fixedTagsList].length >= response.data.data.ImageListWithCVEFixed?.Page?.TotalCount + ); } setLoadingFixed(false); }) .catch((e) => { console.error(e); + setIsEndOfList(true); + setLoadingFixed(false); }); - }, [openFixed]); + }; + + useEffect(() => { + getPaginatedResults(); + return () => { + abortController.abort(); + }; + }, [openFixed, pageNumber]); + + const loadMore = () => { + if (loadingFixed || isEndOfList) return; + setPageNumber((pageNumber) => pageNumber + 1); + }; const renderFixedVer = () => { if (!isEmpty(fixedInfo)) { @@ -152,8 +177,28 @@ function VulnerabilitiyCard(props) { - {' '} - {loadingFixed ? 'Loading...' : renderFixedVer()}{' '} + {loadingFixed ? ( + 'Loading...' + ) : ( + <> + {renderFixedVer()} + {!isEndOfList && ( + + Load more + + )} + + )} diff --git a/src/utilities/paginationConstants.js b/src/utilities/paginationConstants.js index 9ed5f8f9..18a77d07 100644 --- a/src/utilities/paginationConstants.js +++ b/src/utilities/paginationConstants.js @@ -1,5 +1,6 @@ const HEADER_SEARCH_PAGE_SIZE = 9; const EXPLORE_PAGE_SIZE = 10; const HOME_PAGE_SIZE = 10; +const CVE_FIXEDIN_PAGE_SIZE = 5; -export { HEADER_SEARCH_PAGE_SIZE, EXPLORE_PAGE_SIZE, HOME_PAGE_SIZE }; +export { HEADER_SEARCH_PAGE_SIZE, EXPLORE_PAGE_SIZE, HOME_PAGE_SIZE, CVE_FIXEDIN_PAGE_SIZE };