fix(cve): make cards collapsed by default

Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
This commit is contained in:
Andreea-Lupu 2024-02-08 17:01:17 +02:00 committed by Ramkumar Chinchani
parent 0edfe0f73a
commit c268991495
4 changed files with 82 additions and 46 deletions

View File

@ -81,7 +81,7 @@ jobs:
- name: Install go - name: Install go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
go-version: 1.20.x go-version: 1.21.x
- name: Checkout zot repo - name: Checkout zot repo
uses: actions/checkout@v3 uses: actions/checkout@v3

View File

@ -450,10 +450,34 @@ const mockCVEListFiltered = {
CVEListForImage: { CVEListForImage: {
Tag: '', Tag: '',
Page: { ItemCount: 20, TotalCount: 20 }, 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')) 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 = { const mockCVEFixed = {
pageOne: { pageOne: {
ImageListWithCVEFixed: { ImageListWithCVEFixed: {
@ -510,37 +534,29 @@ afterEach(() => {
describe('Vulnerabilties page', () => { describe('Vulnerabilties page', () => {
it('renders the vulnerabilities if there are any', async () => { it('renders the vulnerabilities if there are any', async () => {
let getCall = jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); jest.spyOn(api, 'get').mockResolvedValue({ 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 } });
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
await waitFor(() => expect(screen.getAllByText('Total 5')).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 () => { it('sends filtered query if user types in the search bar', async () => {
let getCall = jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); 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 } });
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
await waitFor(() => expect(screen.getAllByText(/CVE/)).toHaveLength(20));
const cveSearchInput = screen.getByPlaceholderText(/search/i); 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'); await userEvent.type(cveSearchInput, '2022');
expect((await screen.queryAllByText(/2023/i).length) === 0); expect(cveSearchInput).toHaveValue('2022')
expect((await screen.findAllByText(/2022/i)).length === 6); 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 () => { it('should have a collapsable search bar', async () => {
let getCall = jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }) jest.spyOn(api, 'get').
for (let i=0; i<21; i++) { mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }).
getCall.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageNotFixed } }); mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredExclude } });
}
getCall.mockResolvedValue({ status: 200, data: { data: mockCVEList } });
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
const cveSearchInput = screen.getByPlaceholderText(/search/i); const cveSearchInput = screen.getByPlaceholderText(/search/i);
const expandSearch = cveSearchInput.parentElement.parentElement.parentElement.parentElement.childNodes[0]; const expandSearch = cveSearchInput.parentElement.parentElement.parentElement.parentElement.childNodes[0];
@ -550,7 +566,9 @@ describe('Vulnerabilties page', () => {
); );
const excludeInput = screen.getByPlaceholderText("Exclude"); const excludeInput = screen.getByPlaceholderText("Exclude");
userEvent.type(excludeInput, '2022'); 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 () => { it('renders no vulnerabilities if there are not any', async () => {
@ -564,8 +582,10 @@ describe('Vulnerabilties page', () => {
it('should show description for vulnerabilities', async () => { it('should show description for vulnerabilities', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }) 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(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
const expandListBtn = await screen.findAllByTestId('ViewAgendaIcon');
fireEvent.click(expandListBtn[0]);
await waitFor(() => expect(screen.getAllByText(/Description/)).toHaveLength(20)); await waitFor(() => expect(screen.getAllByText(/Description/)).toHaveLength(20));
await waitFor(() => await waitFor(() =>
expect(screen.getAllByText(/CPAN 2.28 allows Signature Verification Bypass./i)).toHaveLength(1) 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 } }); .mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageTwo } });
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); 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.getByText('1.0.16')).toBeInTheDocument());
await waitFor(() => expect(screen.getAllByText(/load more/i).length).toBeGreaterThan(0)); await waitFor(() => expect(screen.getAllByText(/Load more/).length).toBe(1));
const nrLoadButtons = screen.getAllByText(/load more/i).length const loadMoreBtn = screen.getAllByText(/Load more/)[0];
const loadMoreBtn = screen.getAllByText(/load more/i)[0];
await fireEvent.click(loadMoreBtn); 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(); expect(await screen.findByText('latest')).toBeInTheDocument();
}); });
@ -600,11 +621,7 @@ describe('Vulnerabilties page', () => {
const xlsxMock = jest.createMockFromModule('xlsx'); const xlsxMock = jest.createMockFromModule('xlsx');
xlsxMock.writeFile = jest.fn(); xlsxMock.writeFile = jest.fn();
let getCall = jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); jest.spyOn(api, 'get').mockResolvedValue({ 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 } });
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
const downloadBtn = await screen.findAllByTestId('DownloadIcon'); const downloadBtn = await screen.findAllByTestId('DownloadIcon');
@ -623,32 +640,47 @@ describe('Vulnerabilties page', () => {
expect(await screen.findByTestId('export-excel-menuItem')).not.toBeInTheDocument(); expect(await screen.findByTestId('export-excel-menuItem')).not.toBeInTheDocument();
}); });
it('should expand/collapse the list of CVEs', async () => { it("should log an error when data can't be fetched for downloading", async () => {
let getCall = jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); const xlsxMock = jest.createMockFromModule('xlsx');
for (let i=0; i<21; i++) { xlsxMock.writeFile = jest.fn();
getCall.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageNotFixed } });
} jest.spyOn(api, 'get').
getCall.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }).
mockRejectedValue({ status: 500, data: {} });
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); 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(<StateVulnerabilitiesWrapper />);
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)); await waitFor(() => expect(screen.getAllByText('Fixed in')).toHaveLength(20));
const collapseListBtn = await screen.findAllByTestId('ViewHeadlineIcon'); const collapseListBtn = await screen.findAllByTestId('ViewHeadlineIcon');
fireEvent.click(collapseListBtn[0]); fireEvent.click(collapseListBtn[0]);
expect(await screen.findByText('Fixed in')).not.toBeVisible(); 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 () => { it('should handle fixed CVE query errors', async () => {
jest jest
.spyOn(api, 'get') .spyOn(api, 'get')
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }) .mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
.mockRejectedValueOnce({ status: 500, data: {} }); .mockRejectedValue({ status: 500, data: {} });
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
const error = jest.spyOn(console, 'error').mockImplementation(() => {}); const error = jest.spyOn(console, 'error').mockImplementation(() => {});
await waitFor(() => expect(screen.getAllByText(/not fixed/i).length).toBeGreaterThan(0)); const expandListBtn = await screen.findAllByTestId('KeyboardArrowRightIcon');
await waitFor(() => expect(error).toBeCalled()); fireEvent.click(expandListBtn[1]);
await waitFor(() => expect(screen.getByText(/not fixed/i)).toBeInTheDocument());
await waitFor(() => expect(error).toBeCalledTimes(1));
}); });
}); });

View File

@ -123,9 +123,10 @@ function VulnerabilitiyCard(props) {
// pagination props // pagination props
const [pageNumber, setPageNumber] = useState(1); const [pageNumber, setPageNumber] = useState(1);
const [isEndOfList, setIsEndOfList] = useState(false); const [isEndOfList, setIsEndOfList] = useState(false);
const [loadMoreInfo, setLoadMoreInfo] = useState(false);
const getPaginatedResults = () => { const getPaginatedResults = () => {
if (isEndOfList) { if (!openCVE || (!loadMoreInfo && !isEmpty(fixedInfo)) || isEndOfList) {
return; return;
} }
setLoadingFixed(true); setLoadingFixed(true);
@ -148,11 +149,13 @@ function VulnerabilitiyCard(props) {
); );
} }
setLoadingFixed(false); setLoadingFixed(false);
setLoadMoreInfo(false);
}) })
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
setIsEndOfList(true); setIsEndOfList(true);
setLoadingFixed(false); setLoadingFixed(false);
setLoadMoreInfo(false);
}); });
}; };
@ -161,7 +164,7 @@ function VulnerabilitiyCard(props) {
return () => { return () => {
abortController.abort(); abortController.abort();
}; };
}, [pageNumber]); }, [openCVE, pageNumber, loadMoreInfo]);
useEffect(() => { useEffect(() => {
setOpenCVE(expand); setOpenCVE(expand);
@ -169,6 +172,7 @@ function VulnerabilitiyCard(props) {
const loadMore = () => { const loadMore = () => {
if (loadingFixed || isEndOfList) return; if (loadingFixed || isEndOfList) return;
setLoadMoreInfo(true);
setPageNumber((pageNumber) => pageNumber + 1); setPageNumber((pageNumber) => pageNumber + 1);
}; };

View File

@ -165,7 +165,7 @@ function VulnerabilitiesDetails(props) {
const [anchorExport, setAnchorExport] = useState(null); const [anchorExport, setAnchorExport] = useState(null);
const openExport = Boolean(anchorExport); const openExport = Boolean(anchorExport);
const [selectedViewMore, setSelectedViewMore] = useState(true); const [selectedViewMore, setSelectedViewMore] = useState(false);
const getCVERequestName = () => { const getCVERequestName = () => {
return digest !== '' ? `${name}@${digest}` : `${name}:${tag}`; return digest !== '' ? `${name}@${digest}` : `${name}:${tag}`;