diff --git a/gsa/src/web/entity/__tests__/box.js b/gsa/src/web/entity/__tests__/box.js index d2a1a1fe2d..962ee97ca1 100644 --- a/gsa/src/web/entity/__tests__/box.js +++ b/gsa/src/web/entity/__tests__/box.js @@ -19,20 +19,27 @@ import React from 'react'; import Date, {setLocale} from 'gmp/models/date'; -import {render} from 'web/utils/testing'; +import {setTimezone} from 'web/store/usersettings/actions'; +import {rendererWith} from 'web/utils/testing'; import EntityBox from '../box'; setLocale('en'); -const date = Date('2019-01-01T12:00:00Z'); +const date1 = Date('2019-01-01T12:00:00Z'); const date2 = Date('2019-02-02T12:00:00Z'); describe('EntityBox component tests', () => { test('should render', () => { + const {render, store} = rendererWith({ + store: true, + }); + + store.dispatch(setTimezone('CET')); + const {element} = render( { expect(element).toHaveTextContent('tool'); expect(element).toHaveTextContent('foo'); expect(element).toHaveTextContent('child'); - expect(element).toHaveTextContent('Active untilTue, Jan 1, 2019'); - expect(element).toHaveTextContent('ModifiedSat, Feb 2, 2019'); + expect(element).toHaveTextContent( + 'Active untilTue, Jan 1, 2019 1:00 PM CET', + ); + expect(element).toHaveTextContent('ModifiedSat, Feb 2, 2019 1:00 PM CET'); expect(element).toHaveStyleRule('width', '400px'); }); }); diff --git a/gsa/src/web/entity/__tests__/note.js b/gsa/src/web/entity/__tests__/note.js index 71b1bd599d..7d73bdebbf 100644 --- a/gsa/src/web/entity/__tests__/note.js +++ b/gsa/src/web/entity/__tests__/note.js @@ -21,6 +21,7 @@ import Capabilities from 'gmp/capabilities/capabilities'; import {setLocale} from 'gmp/models/date'; import Note from 'gmp/models/note'; +import {setTimezone} from 'web/store/usersettings/actions'; import {rendererWith} from 'web/utils/testing'; import NoteBox from '../note'; @@ -44,11 +45,14 @@ const note = Note.fromElement({ describe('NoteBox component tests', () => { test('should render with DetailsLink', () => { - const {render} = rendererWith({ + const {render, store} = rendererWith({ capabilities: caps, router: true, + store: true, }); + store.dispatch(setTimezone('CET')); + const {element, getByTestId} = render( , ); @@ -59,18 +63,22 @@ describe('NoteBox component tests', () => { expect(link).toBeDefined(); expect(header).toHaveTextContent('Note'); expect(element).toHaveTextContent('details.svg'); - expect(element).toHaveTextContent('ModifiedSat, Feb 2, 2019'); - expect(element).toHaveTextContent('Active untilTue, Jan 1, 2019'); + expect(element).toHaveTextContent('ModifiedSat, Feb 2, 2019 1:00 PM CET'); + expect(element).toHaveTextContent( + 'Active untilTue, Jan 1, 2019 1:00 PM CET', + ); expect(element).toHaveTextContent('foo'); }); test('should render without DetailsLink', () => { - const {render} = rendererWith({ + const {render, store} = rendererWith({ capabilities: caps, router: true, store: true, }); + store.dispatch(setTimezone('CET')); + const {element} = render(); const link = element.querySelector('a'); @@ -78,8 +86,10 @@ describe('NoteBox component tests', () => { expect(link).toEqual(null); expect(element).toHaveTextContent('foo'); expect(element).not.toHaveTextContent('details.svg'); - expect(element).toHaveTextContent('ModifiedSat, Feb 2, 2019'); - expect(element).toHaveTextContent('Active untilTue, Jan 1, 2019'); + expect(element).toHaveTextContent('ModifiedSat, Feb 2, 2019 1:00 PM CET'); + expect(element).toHaveTextContent( + 'Active untilTue, Jan 1, 2019 1:00 PM CET', + ); }); }); diff --git a/gsa/src/web/entity/__tests__/override.js b/gsa/src/web/entity/__tests__/override.js index ba1a73ca78..5baf02c83d 100644 --- a/gsa/src/web/entity/__tests__/override.js +++ b/gsa/src/web/entity/__tests__/override.js @@ -21,6 +21,7 @@ import Capabilities from 'gmp/capabilities/capabilities'; import {setLocale} from 'gmp/models/date'; import Override from 'gmp/models/override'; +import {setTimezone} from 'web/store/usersettings/actions'; import {rendererWith} from 'web/utils/testing'; import OverrideBox from '../override'; @@ -40,11 +41,14 @@ const override = Override.fromElement({ describe('OverrideBox component tests', () => { test('should render with DetailsLink', () => { - const {render} = rendererWith({ + const {render, store} = rendererWith({ capabilities: caps, router: true, + store: true, }); + store.dispatch(setTimezone('CET')); + const {element, getByTestId} = render( , ); @@ -58,17 +62,21 @@ describe('OverrideBox component tests', () => { expect(link).toBeDefined(); expect(link.getAttribute('href')).toEqual('/override/123'); expect(element).toHaveTextContent('details.svg'); - expect(element).toHaveTextContent('ModifiedSat, Feb 2, 2019'); - expect(element).toHaveTextContent('Active untilTue, Jan 1, 2019'); + expect(element).toHaveTextContent('ModifiedSat, Feb 2, 2019 1:00 PM CET'); + expect(element).toHaveTextContent( + 'Active untilTue, Jan 1, 2019 1:00 PM CET', + ); expect(element).toHaveTextContent('foo'); }); test('should render without DetailsLink', () => { - const {render} = rendererWith({ + const {render, store} = rendererWith({ capabilities: caps, router: true, + store: true, }); + store.dispatch(setTimezone('CET')); const {element} = render( , ); @@ -78,8 +86,10 @@ describe('OverrideBox component tests', () => { expect(link).toEqual(null); expect(element).toHaveTextContent('foo'); expect(element).not.toHaveTextContent('details.svg'); - expect(element).toHaveTextContent('ModifiedSat, Feb 2, 2019'); - expect(element).toHaveTextContent('Active untilTue, Jan 1, 2019'); + expect(element).toHaveTextContent('ModifiedSat, Feb 2, 2019 1:00 PM CET'); + expect(element).toHaveTextContent( + 'Active untilTue, Jan 1, 2019 1:00 PM CET', + ); }); }); diff --git a/gsa/src/web/entity/box.js b/gsa/src/web/entity/box.js index 9dd8792d23..ba80041503 100644 --- a/gsa/src/web/entity/box.js +++ b/gsa/src/web/entity/box.js @@ -21,14 +21,10 @@ import React from 'react'; import styled from 'styled-components'; import _ from 'gmp/locale'; -import {longDate} from 'gmp/locale/date'; +import DateTime from 'web/components/date/datetime'; import {isDefined} from 'gmp/utils/identity'; -import PropTypes from 'web/utils/proptypes'; - -import Theme from 'web/utils/theme'; - import Layout from 'web/components/layout/layout'; import InfoTable from 'web/components/table/infotable'; @@ -36,6 +32,10 @@ import TableBody from 'web/components/table/body'; import TableData from 'web/components/table/data'; import TableRow from 'web/components/table/row'; +import PropTypes from 'web/utils/proptypes'; + +import Theme from 'web/utils/theme'; + const Pre = styled.pre` white-space: pre-wrap; word-wrap: break-word; @@ -73,12 +73,16 @@ const EntityBox = ({ {isDefined(end) && ( {_('Active until')} - {longDate(end)} + + + )} {_('Modified')} - {longDate(modified)} + + + diff --git a/gsa/src/web/pages/credentials/__tests__/detailspage.js b/gsa/src/web/pages/credentials/__tests__/detailspage.js new file mode 100644 index 0000000000..e6a59df0b1 --- /dev/null +++ b/gsa/src/web/pages/credentials/__tests__/detailspage.js @@ -0,0 +1,574 @@ +/* Copyright (C) 2021 Greenbone Networks GmbH + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React from 'react'; + +import Capabilities from 'gmp/capabilities/capabilities'; +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import {setLocale} from 'gmp/locale/lang'; + +import Credential from 'gmp/models/credential'; +import Filter from 'gmp/models/filter'; + +import {isDefined} from 'gmp/utils/identity'; + +import {entityLoadingActions} from 'web/store/entities/credentials'; +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; + +import {rendererWith, screen, fireEvent} from 'web/utils/testing'; + +import Detailspage, {ToolBarIcons} from '../detailspage'; + +setLocale('en'); + +let getCredential; +let getEntities; +let currentSettings; +let renewSession; + +beforeEach(() => { + getCredential = jest.fn().mockResolvedValue({ + data: credential, + }); + getEntities = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + currentSettings = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + renewSession = jest.fn().mockResolvedValue({ + foo: 'bar', + }); +}); + +if (!isDefined(window.URL)) { + window.URL = {}; +} +window.URL.createObjectURL = jest.fn(); + +const caps = new Capabilities(['everything']); + +const reloadInterval = -1; +const manualUrl = 'test/'; + +const credential = Credential.fromElement({ + _id: '6575', + allow_insecure: 1, + creation_time: '2020-12-16T15:23:59Z', + comment: 'blah', + formats: {format: 'pem'}, + full_type: 'client certificate', + in_use: 0, + login: '', + modification_time: '2021-03-02T10:28:15Z', + name: 'credential 1', + owner: {name: 'admin'}, + permissions: {permission: {name: 'Everything'}}, + type: 'cc', + writable: 1, + certificate_info: { + activation_time: '2018-10-10T11:41:23.022Z', + expiration_time: '2019-10-10T11:41:23.022Z', + md5_fingerprint: 'asdf', + issuer: 'dn', + }, +}); + +const noPermCredential = Credential.fromElement({ + _id: '6575', + allow_insecure: 1, + creation_time: '2020-12-16T15:23:59Z', + comment: 'blah', + formats: {format: 'pem'}, + full_type: 'client certificate', + in_use: 0, + login: '', + modification_time: '2021-03-02T10:28:15Z', + name: 'credential 1', + owner: {name: 'admin'}, + permissions: {permission: {name: 'get_credentials'}}, + type: 'cc', + writable: 1, + certificate_info: { + activation_time: '2018-10-10T11:41:23.022Z', + expiration_time: '2019-10-10T11:41:23.022Z', + md5_fingerprint: 'asdf', + issuer: 'dn', + }, +}); + +const credentialInUse = Credential.fromElement({ + _id: '6575', + allow_insecure: 1, + creation_time: '2020-12-16T15:23:59Z', + comment: 'blah', + formats: {format: 'pem'}, + full_type: 'client certificate', + in_use: 1, + login: '', + modification_time: '2021-03-02T10:28:15Z', + name: 'credential 1', + owner: {name: 'admin'}, + permissions: {permission: {name: 'Everything'}}, + type: 'cc', + writable: 1, + certificate_info: { + activation_time: '2018-10-10T11:41:23.022Z', + expiration_time: '2019-10-10T11:41:23.022Z', + md5_fingerprint: 'asdf', + issuer: 'dn', + }, +}); + +describe('Credential Detailspage tests', () => { + test('should render full Detailspage', () => { + const gmp = { + credential: { + get: getCredential, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('6575', credential)); + + const {baseElement, element} = render(); + + const links = baseElement.querySelectorAll('a'); + + expect(screen.getAllByTitle('Help: Credentials')[0]).toBeInTheDocument(); + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/scanning.html#managing-credentials', + ); + + expect(screen.getAllByTitle('Credential List')[0]).toBeInTheDocument(); + expect(links[1]).toHaveAttribute('href', '/credentials'); + + expect(element).toHaveTextContent('ID:6575'); + expect(element).toHaveTextContent('Created:Wed, Dec 16, 2020 4:23 PM CET'); + expect(element).toHaveTextContent('Modified:Tue, Mar 2, 2021 11:28 AM CET'); + expect(element).toHaveTextContent('Owner:admin'); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[10]).toHaveTextContent('User Tags'); + expect(spans[12]).toHaveTextContent('Permissions'); + + expect(element).toHaveTextContent('Comment'); + expect(element).toHaveTextContent('blah'); + + expect(element).toHaveTextContent('Type'); + expect(element).toHaveTextContent('Client Certificate(cc)'); + + expect(element).toHaveTextContent('Allow Insecure Use'); + expect(element).toHaveTextContent('Yes'); + + expect(element).toHaveTextContent('Certificate'); + + expect(element).toHaveTextContent('Activation'); + expect(element).toHaveTextContent('Wed, Oct 10, 2018 1:41 PM CEST'); + + expect(element).toHaveTextContent('Expiration'); + expect(element).toHaveTextContent('Thu, Oct 10, 2019 1:41 PM CEST'); + + expect(element).toHaveTextContent('MD5 Fingerprint'); + expect(element).toHaveTextContent('asdf'); + + expect(element).toHaveTextContent('Issued By'); + expect(element).toHaveTextContent('dn'); + }); + + test('should render user tags tab', () => { + const gmp = { + credential: { + get: getCredential, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('6575', credential)); + + const {baseElement} = render(); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[10]).toHaveTextContent('User Tags'); + + fireEvent.click(spans[10]); + + expect(baseElement).toHaveTextContent('No user tags available'); + }); + + test('should render permissions tab', () => { + const gmp = { + credential: { + get: getCredential, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('6575', credential)); + + const {baseElement} = render(); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[12]).toHaveTextContent('Permissions'); + + fireEvent.click(spans[12]); + + expect(baseElement).toHaveTextContent('No permissions available'); + }); + + test('should call commands', () => { + const clone = jest.fn().mockResolvedValue({ + data: {id: 'foo'}, + }); + + const deleteFunc = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportFunc = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + credential: { + get: getCredential, + clone, + delete: deleteFunc, + export: exportFunc, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('6575', credential)); + + render(); + + const cloneIcon = screen.getAllByTitle('Clone Credential'); + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + + expect(clone).toHaveBeenCalledWith(credential); + + const exportIcon = screen.getAllByTitle('Export Credential as XML'); + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + expect(exportFunc).toHaveBeenCalledWith(credential); + + const deleteIcon = screen.getAllByTitle('Move Credential to trashcan'); + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + expect(deleteFunc).toHaveBeenCalledWith({id: credential.id}); + }); +}); + +describe('Credential ToolBarIcons tests', () => { + test('should render', () => { + const handleCredentialCloneClick = jest.fn(); + const handleCredentialDeleteClick = jest.fn(); + const handleCredentialDownloadClick = jest.fn(); + const handleCredentialEditClick = jest.fn(); + const handleCredentialCreateClick = jest.fn(); + const handleCredentialInstallerDownloadClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + const {element} = render( + , + ); + + const links = element.querySelectorAll('a'); + + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/scanning.html#managing-credentials', + ); + expect(screen.getAllByTitle('Help: Credentials')[0]).toBeInTheDocument(); + + expect(links[1]).toHaveAttribute('href', '/credentials'); + expect(screen.getAllByTitle('Credential List')[0]).toBeInTheDocument(); + }); + + test('should call click handlers', () => { + const handleCredentialCloneClick = jest.fn(); + const handleCredentialDeleteClick = jest.fn(); + const handleCredentialDownloadClick = jest.fn(); + const handleCredentialEditClick = jest.fn(); + const handleCredentialCreateClick = jest.fn(); + const handleCredentialInstallerDownloadClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + + const cloneIcon = screen.getAllByTitle('Clone Credential'); + const editIcon = screen.getAllByTitle('Edit Credential'); + const deleteIcon = screen.getAllByTitle('Move Credential to trashcan'); + const exportIcon = screen.getAllByTitle('Export Credential as XML'); + const exportCredentialInstallerIcon = screen.getAllByTitle( + 'Download Certificate (.pem)', + ); + + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + expect(handleCredentialCloneClick).toHaveBeenCalledWith(credential); + + expect(editIcon[0]).toBeInTheDocument(); + fireEvent.click(editIcon[0]); + expect(handleCredentialEditClick).toHaveBeenCalledWith(credential); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + expect(handleCredentialDeleteClick).toHaveBeenCalledWith(credential); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + expect(handleCredentialDownloadClick).toHaveBeenCalledWith(credential); + + expect(exportCredentialInstallerIcon[0]).toBeInTheDocument(); + fireEvent.click(exportCredentialInstallerIcon[0]); + expect(handleCredentialInstallerDownloadClick).toHaveBeenCalledWith( + credential, + 'pem', + ); + }); + + test('should not call click handlers without permission', () => { + const handleCredentialCloneClick = jest.fn(); + const handleCredentialDeleteClick = jest.fn(); + const handleCredentialDownloadClick = jest.fn(); + const handleCredentialEditClick = jest.fn(); + const handleCredentialCreateClick = jest.fn(); + const handleCredentialInstallerDownloadClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + + const cloneIcon = screen.getAllByTitle('Clone Credential'); + const editIcon = screen.getAllByTitle( + 'Permission to edit Credential denied', + ); + const deleteIcon = screen.getAllByTitle( + 'Permission to move Credential to trashcan denied', + ); + const exportIcon = screen.getAllByTitle('Export Credential as XML'); + + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + + expect(handleCredentialCloneClick).toHaveBeenCalledWith(noPermCredential); + + expect(editIcon[0]).toBeInTheDocument(); + fireEvent.click(editIcon[0]); + + expect(handleCredentialEditClick).not.toHaveBeenCalled(); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + expect(handleCredentialDeleteClick).not.toHaveBeenCalled(); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + expect(handleCredentialDownloadClick).toHaveBeenCalledWith( + noPermCredential, + ); + }); + + test('should (not) call click handlers for credential in use', () => { + const handleCredentialCloneClick = jest.fn(); + const handleCredentialDeleteClick = jest.fn(); + const handleCredentialDownloadClick = jest.fn(); + const handleCredentialEditClick = jest.fn(); + const handleCredentialCreateClick = jest.fn(); + const handleCredentialInstallerDownloadClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + const cloneIcon = screen.getAllByTitle('Clone Credential'); + const editIcon = screen.getAllByTitle('Edit Credential'); + const deleteIcon = screen.getAllByTitle('Credential is still in use'); + const exportIcon = screen.getAllByTitle('Export Credential as XML'); + + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + + expect(handleCredentialCloneClick).toHaveBeenCalledWith(credentialInUse); + + expect(editIcon[0]).toBeInTheDocument(); + fireEvent.click(editIcon[0]); + + expect(handleCredentialEditClick).toHaveBeenCalled(); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + expect(handleCredentialDeleteClick).not.toHaveBeenCalled(); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + expect(handleCredentialDownloadClick).toHaveBeenCalledWith(credentialInUse); + }); +}); diff --git a/gsa/src/web/pages/credentials/__tests__/listpage.js b/gsa/src/web/pages/credentials/__tests__/listpage.js new file mode 100644 index 0000000000..1827f309ff --- /dev/null +++ b/gsa/src/web/pages/credentials/__tests__/listpage.js @@ -0,0 +1,480 @@ +/* Copyright (C) 2021 Greenbone Networks GmbH + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import React from 'react'; + +import Capabilities from 'gmp/capabilities/capabilities'; +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import {setLocale} from 'gmp/locale/lang'; + +import Credential from 'gmp/models/credential'; +import Filter from 'gmp/models/filter'; + +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; +import {defaultFilterLoadingActions} from 'web/store/usersettings/defaultfilters/actions'; +import {loadingActions} from 'web/store/usersettings/defaults/actions'; + +import {rendererWith, fireEvent, screen, wait} from 'web/utils/testing'; + +import CredentialPage, {ToolBarIcons} from '../listpage'; + +setLocale('en'); + +window.URL.createObjectURL = jest.fn(); + +const credential = Credential.fromElement({ + _id: '6575', + allow_insecure: 1, + creation_time: '2020-12-16T15:23:59Z', + comment: 'blah', + formats: {format: 'pem'}, + full_type: 'client certificate', + in_use: 0, + login: '', + modification_time: '2021-03-02T10:28:15Z', + name: 'credential 1', + owner: {name: 'admin'}, + permissions: {permission: {name: 'Everything'}}, + type: 'cc', + writable: 1, + certificate_info: { + activation_time: '2018-10-10T11:41:23.022Z', + expiration_time: '2019-10-10T11:41:23.022Z', + md5_fingerprint: 'asdf', + issuer: 'dn', + }, +}); + +const caps = new Capabilities(['everything']); +const wrongCaps = new Capabilities(['get_credentials']); + +const reloadInterval = -1; +const manualUrl = 'test/'; + +let currentSettings; +let getSetting; +let getFilters; +let getCredentials; +let renewSession; + +beforeEach(() => { + currentSettings = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + getSetting = jest.fn().mockResolvedValue({ + filter: null, + }); + getFilters = jest.fn().mockReturnValue( + Promise.resolve({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }), + ); + + getCredentials = jest.fn().mockResolvedValue({ + data: [credential], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + renewSession = jest.fn().mockResolvedValue({ + foo: 'bar', + }); +}); + +describe('CredentialPage tests', () => { + test('should render full CredentialPage', async () => { + const gmp = { + credentials: { + get: getCredentials, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {currentSettings, getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('credential', defaultSettingfilter), + ); + + const {baseElement} = render(); + + await wait(); + + const inputs = baseElement.querySelectorAll('input'); + const selects = screen.getAllByTestId('select-selected-value'); + + // Toolbar Icons + expect(screen.getAllByTitle('Help: Credentials')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('New Credential')[0]).toBeInTheDocument(); + + // Powerfilter + expect(inputs[0]).toHaveAttribute('name', 'userFilterString'); + expect(screen.getAllByTitle('Update Filter')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Remove Filter')[0]).toBeInTheDocument(); + expect( + screen.getAllByTitle('Reset to Default Filter')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Help: Powerfilter')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Edit Filter')[0]).toBeInTheDocument(); + expect(selects[0]).toHaveAttribute('title', 'Loaded filter'); + expect(selects[0]).toHaveTextContent('--'); + + // Table + const header = baseElement.querySelectorAll('th'); + + expect(header[0]).toHaveTextContent('Name'); + expect(header[1]).toHaveTextContent('Type'); + expect(header[2]).toHaveTextContent('Allow insecure use'); + expect(header[3]).toHaveTextContent('Login'); + expect(header[4]).toHaveTextContent('Actions'); + + const row = baseElement.querySelectorAll('tr'); + + expect(row[1]).toHaveTextContent('credential 1'); + expect(row[1]).toHaveTextContent('(blah)'); + expect(row[1]).toHaveTextContent('Client Certificate(cc)'); + expect(row[1]).toHaveTextContent('Yes'); + + expect( + screen.getAllByTitle('Move Credential to trashcan')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Edit Credential')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Clone Credential')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Export Credential')[0]).toBeInTheDocument(); + expect( + screen.getAllByTitle('Download Certificate (.pem)')[0], + ).toBeInTheDocument(); + }); + + test('should allow to bulk action on page contents', async () => { + const deleteByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + credentials: { + get: getCredentials, + deleteByFilter, + exportByFilter, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('credential', defaultSettingfilter), + ); + + render(); + + await wait(); + + // export page contents + const exportIcon = screen.getAllByTitle('Export page contents'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByFilter).toHaveBeenCalled(); + + // move page contents to trashcan + const deleteIcon = screen.getAllByTitle('Move page contents to trashcan'); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteByFilter).toHaveBeenCalled(); + }); + + test('should allow to bulk action on selected credentials', async () => { + // mock cache issues will cause these tests to randomly fail. Will fix later. + const deleteByIds = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByIds = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + credentials: { + get: getCredentials, + delete: deleteByIds, + export: exportByIds, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('credential', defaultSettingfilter), + ); + + const {element} = render(); + + await wait(); + + const selectFields = screen.getAllByTestId('select-open-button'); + fireEvent.click(selectFields[1]); + + await wait(); + + const selectItems = screen.getAllByTestId('select-item'); + fireEvent.click(selectItems[1]); + + await wait(); + + const selected = screen.getAllByTestId('select-selected-value'); + expect(selected[1]).toHaveTextContent('Apply to selection'); + + const inputs = element.querySelectorAll('input'); + + // select an credential + fireEvent.click(inputs[1]); + await wait(); + + // export selected credential + const exportIcon = screen.getAllByTitle('Export selection'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByIds).toHaveBeenCalled(); + + // move selected credential to trashcan + const deleteIcon = screen.getAllByTitle('Move selection to trashcan'); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteByIds).toHaveBeenCalled(); + }); + + test('should allow to bulk action on filtered credentials', async () => { + // mock cache issues will cause these tests to randomly fail. Will fix later. + const deleteByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + credentials: { + get: getCredentials, + deleteByFilter, + exportByFilter, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('credential', defaultSettingfilter), + ); + + render(); + + await wait(); + + const selectFields = screen.getAllByTestId('select-open-button'); + fireEvent.click(selectFields[1]); + + await wait(); + + const selectItems = screen.getAllByTestId('select-item'); + fireEvent.click(selectItems[2]); + + await wait(); + + const selected = screen.getAllByTestId('select-selected-value'); + expect(selected[1]).toHaveTextContent('Apply to all filtered'); + + // export all filtered credentials + const exportIcon = screen.getAllByTitle('Export all filtered'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByFilter).toHaveBeenCalled(); + + // move all filtered credentials to trashcan + const deleteIcon = screen.getAllByTitle('Move all filtered to trashcan'); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteByFilter).toHaveBeenCalled(); + }); +}); + +describe('CredentialPage ToolBarIcons test', () => { + test('should render', () => { + const handleCredentialCreateClick = jest.fn(); + + const gmp = { + settings: {manualUrl}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + const {element} = render( + , + ); + + const links = element.querySelectorAll('a'); + + expect(screen.getAllByTitle('Help: Credentials')[0]).toBeInTheDocument(); + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/scanning.html#managing-credentials', + ); + }); + + test('should call click handlers', () => { + const handleCredentialCreateClick = jest.fn(); + + const gmp = { + settings: {manualUrl}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + + const newIcon = screen.getAllByTitle('New Credential'); + + expect(newIcon[0]).toBeInTheDocument(); + + fireEvent.click(newIcon[0]); + expect(handleCredentialCreateClick).toHaveBeenCalled(); + }); + + test('should not show icons if user does not have the right permissions', () => { + const handleCredentialCreateClick = jest.fn(); + + const gmp = { + settings: {manualUrl}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: wrongCaps, + router: true, + }); + + const {queryAllByTestId} = render( + , + ); + + const icons = queryAllByTestId('svg-icon'); // this test is probably approppriate to keep in the old format + expect(icons.length).toBe(1); + expect(icons[0]).toHaveAttribute('title', 'Help: Credentials'); + }); +}); diff --git a/gsa/src/web/pages/credentials/detailspage.js b/gsa/src/web/pages/credentials/detailspage.js index 4d31a35347..0309d7df6f 100644 --- a/gsa/src/web/pages/credentials/detailspage.js +++ b/gsa/src/web/pages/credentials/detailspage.js @@ -79,7 +79,7 @@ import CredentialDetails from './details'; import CredentialComponent from './component'; import CredentialDownloadIcon from './downloadicon'; -const ToolBarIcons = ({ +export const ToolBarIcons = ({ entity, onCredentialCloneClick, onCredentialCreateClick, diff --git a/gsa/src/web/pages/credentials/listpage.js b/gsa/src/web/pages/credentials/listpage.js index e2a9b3ac22..36e60733d9 100644 --- a/gsa/src/web/pages/credentials/listpage.js +++ b/gsa/src/web/pages/credentials/listpage.js @@ -44,7 +44,7 @@ import { import CredentialComponent from './component'; import CredentialsTable, {SORT_FIELDS} from './table'; -const ToolBarIcons = withCapabilities( +export const ToolBarIcons = withCapabilities( ({capabilities, onCredentialCreateClick}) => ( . + */ +import React from 'react'; + +import Capabilities from 'gmp/capabilities/capabilities'; +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import {setLocale} from 'gmp/locale/lang'; + +import Filter from 'gmp/models/filter'; +import Note from 'gmp/models/note'; + +import {isDefined} from 'gmp/utils/identity'; + +import {entityLoadingActions} from 'web/store/entities/notes'; +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; + +import {rendererWith, fireEvent, screen, wait} from 'web/utils/testing'; + +import Detailspage, {ToolBarIcons} from '../detailspage'; + +setLocale('en'); + +if (!isDefined(window.URL)) { + window.URL = {}; +} +window.URL.createObjectURL = jest.fn(); + +const caps = new Capabilities(['everything']); + +const reloadInterval = -1; +const manualUrl = 'test/'; + +const note = Note.fromElement({ + _id: '6d00d22f-551b-4fbe-8215-d8615eff73ea', + active: 1, + creation_time: '2020-12-23T14:14:11Z', + hosts: '127.0.0.1', + in_use: 0, + modification_time: '2021-01-04T11:54:12Z', + nvt: { + _oid: '123', + name: 'foo nvt', + type: 'nvt', + }, + owner: {name: 'admin'}, + permissions: {permission: {name: 'Everything'}}, + port: '666', + task: { + name: 'task x', + _id: '42', + }, + text: 'note text', + writable: 1, +}); + +const noteInUse = Note.fromElement({ + _id: '6d00d22f-551b-4fbe-8215-d8615eff73ea', + active: 1, + creation_time: '2020-12-23T14:14:11Z', + hosts: '127.0.0.1', + in_use: 1, + modification_time: '2021-01-04T11:54:12Z', + nvt: { + _oid: '123', + name: 'foo nvt', + type: 'nvt', + }, + owner: {name: 'admin'}, + permissions: {permission: {name: 'Everything'}}, + port: '666', + text: 'note text', + writable: 1, +}); + +const noPermNote = Note.fromElement({ + _id: '6d00d22f-551b-4fbe-8215-d8615eff73ea', + active: 1, + creation_time: '2020-12-23T14:14:11Z', + hosts: '127.0.0.1', + in_use: 0, + modification_time: '2021-01-04T11:54:12Z', + nvt: { + _oid: '123', + name: 'foo nvt', + type: 'nvt', + }, + owner: {name: 'admin'}, + permissions: {permission: {name: 'get_notes'}}, + port: '666', + task: { + name: 'task x', + _id: '42', + }, + text: 'note text', + writable: 1, +}); + +const getNote = jest.fn().mockResolvedValue({ + data: note, +}); + +const getEntities = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const currentSettings = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +const renewSession = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +describe('Note detailspage tests', () => { + test('should render full detailspage', () => { + const gmp = { + note: { + get: getNote, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch( + entityLoadingActions.success( + '6d00d22f-551b-4fbe-8215-d8615eff73ea', + note, + ), + ); + + const {baseElement, element} = render( + , + ); + + expect(element).toHaveTextContent('note text'); + + const links = baseElement.querySelectorAll('a'); + + expect(screen.getAllByTitle('Help: Notes')[0]).toBeInTheDocument(); + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/reports.html#managing-notes', + ); + + expect(screen.getAllByTitle('Note List')[0]).toBeInTheDocument(); + expect(links[1]).toHaveAttribute('href', '/notes'); + + expect(element).toHaveTextContent( + 'ID:6d00d22f-551b-4fbe-8215-d8615eff73ea', + ); + expect(element).toHaveTextContent('Created:Wed, Dec 23, 2020 3:14 PM CET'); + expect(element).toHaveTextContent('Modified:Mon, Jan 4, 2021 12:54 PM CET'); + expect(element).toHaveTextContent('Owner:admin'); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[9]).toHaveTextContent('User Tags'); + expect(spans[11]).toHaveTextContent('Permissions'); + + expect(element).toHaveTextContent('NVT Name'); + expect(element).toHaveTextContent('foo nvt'); + + expect(element).toHaveTextContent('NVT OID'); + expect(element).toHaveTextContent('123'); + + expect(element).toHaveTextContent('Active'); + expect(element).toHaveTextContent('Yes'); + + expect(element).toHaveTextContent('Application'); + + expect(element).toHaveTextContent('Hosts'); + expect(element).toHaveTextContent('127.0.0.1'); + + expect(element).toHaveTextContent('Port'); + expect(element).toHaveTextContent('666'); + + expect(element).toHaveTextContent('Severity'); + expect(element).toHaveTextContent('Any'); + + expect(element).toHaveTextContent('Task'); + expect(element).toHaveTextContent('task x'); + + expect(element).toHaveTextContent('Result'); + expect(element).toHaveTextContent('Any'); + + expect(element).toHaveTextContent('Appearance'); + + expect(element).toHaveTextContent('note text'); + }); + + test('should render user tags tab', () => { + const gmp = { + note: { + get: getNote, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch( + entityLoadingActions.success( + '6d00d22f-551b-4fbe-8215-d8615eff73ea', + note, + ), + ); + + const {baseElement} = render( + , + ); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[9]).toHaveTextContent('User Tags'); + fireEvent.click(spans[9]); + + expect(baseElement).toHaveTextContent('No user tags available'); + }); + + test('should render permissions tab', () => { + const gmp = { + note: { + get: getNote, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch( + entityLoadingActions.success( + '6d00d22f-551b-4fbe-8215-d8615eff73ea', + note, + ), + ); + + const {baseElement} = render( + , + ); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[11]).toHaveTextContent('Permissions'); + fireEvent.click(spans[11]); + + expect(baseElement).toHaveTextContent('No permissions available'); + }); + + test('should call commands', async () => { + const clone = jest.fn().mockResolvedValue({ + data: {id: 'foo'}, + }); + + const deleteFunc = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportFunc = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + note: { + get: getNote, + clone, + delete: deleteFunc, + export: exportFunc, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch( + entityLoadingActions.success( + '6d00d22f-551b-4fbe-8215-d8615eff73ea', + note, + ), + ); + + render(); + + await wait(); + + const cloneIcon = screen.getAllByTitle('Clone Note'); + expect(cloneIcon[0]).toBeInTheDocument(); + + fireEvent.click(cloneIcon[0]); + + await wait(); + + expect(clone).toHaveBeenCalledWith(note); + + const exportIcon = screen.getAllByTitle('Export Note as XML'); + expect(exportIcon[0]).toBeInTheDocument(); + + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportFunc).toHaveBeenCalledWith(note); + + const deleteIcon = screen.getAllByTitle('Move Note to trashcan'); + expect(deleteIcon[0]).toBeInTheDocument(); + + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteFunc).toHaveBeenCalledWith({id: note.id}); + }); +}); + +describe('Note ToolBarIcons tests', () => { + test('should render', () => { + const handleNoteCloneClick = jest.fn(); + const handleNoteDeleteClick = jest.fn(); + const handleNoteDownloadClick = jest.fn(); + const handleNoteEditClick = jest.fn(); + const handleNoteCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + const {element} = render( + , + ); + + const links = element.querySelectorAll('a'); + + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/reports.html#managing-notes', + ); + expect(screen.getAllByTitle('Help: Notes')[0]).toBeInTheDocument(); + + expect(links[1]).toHaveAttribute('href', '/notes'); + expect(screen.getAllByTitle('Note List')[0]).toBeInTheDocument(); + }); + + test('should call click handlers', () => { + const handleNoteCloneClick = jest.fn(); + const handleNoteDeleteClick = jest.fn(); + const handleNoteDownloadClick = jest.fn(); + const handleNoteEditClick = jest.fn(); + const handleNoteCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + + const cloneIcon = screen.getAllByTitle('Clone Note'); + const editIcon = screen.getAllByTitle('Edit Note'); + const deleteIcon = screen.getAllByTitle('Move Note to trashcan'); + const exportIcon = screen.getAllByTitle('Export Note as XML'); + + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + expect(handleNoteCloneClick).toHaveBeenCalledWith(note); + + expect(editIcon[0]).toBeInTheDocument(); + fireEvent.click(editIcon[0]); + expect(handleNoteEditClick).toHaveBeenCalledWith(note); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + expect(handleNoteDeleteClick).toHaveBeenCalledWith(note); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + expect(handleNoteDownloadClick).toHaveBeenCalledWith(note); + }); + + test('should not call click handlers without permission', () => { + const handleNoteCloneClick = jest.fn(); + const handleNoteDeleteClick = jest.fn(); + const handleNoteDownloadClick = jest.fn(); + const handleNoteEditClick = jest.fn(); + const handleNoteCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + + const cloneIcon = screen.getAllByTitle('Clone Note'); + const editIcon = screen.getAllByTitle('Permission to edit Note denied'); + const deleteIcon = screen.getAllByTitle( + 'Permission to move Note to trashcan denied', + ); + const exportIcon = screen.getAllByTitle('Export Note as XML'); + + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + + expect(handleNoteCloneClick).toHaveBeenCalledWith(noPermNote); + + expect(editIcon[0]).toBeInTheDocument(); + fireEvent.click(editIcon[0]); + + expect(handleNoteEditClick).not.toHaveBeenCalled(); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + expect(handleNoteDeleteClick).not.toHaveBeenCalled(); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + expect(handleNoteDownloadClick).toHaveBeenCalledWith(noPermNote); + }); + + test('should call correct click handlers for note in use', () => { + const handleNoteCloneClick = jest.fn(); + const handleNoteDeleteClick = jest.fn(); + const handleNoteDownloadClick = jest.fn(); + const handleNoteEditClick = jest.fn(); + const handleNoteCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + const cloneIcon = screen.getAllByTitle('Clone Note'); + const editIcon = screen.getAllByTitle('Edit Note'); + const deleteIcon = screen.getAllByTitle('Note is still in use'); + const exportIcon = screen.getAllByTitle('Export Note as XML'); + + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + + expect(handleNoteCloneClick).toHaveBeenCalledWith(noteInUse); + + expect(editIcon[0]).toBeInTheDocument(); + fireEvent.click(editIcon[0]); + + expect(handleNoteEditClick).toHaveBeenCalled(); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + expect(handleNoteDeleteClick).not.toHaveBeenCalled(); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + expect(handleNoteDownloadClick).toHaveBeenCalledWith(noteInUse); + }); +}); diff --git a/gsa/src/web/pages/notes/__tests__/listpage.js b/gsa/src/web/pages/notes/__tests__/listpage.js new file mode 100644 index 0000000000..5975ec4fe3 --- /dev/null +++ b/gsa/src/web/pages/notes/__tests__/listpage.js @@ -0,0 +1,564 @@ +/* Copyright (C) 2021 Greenbone Networks GmbH + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import React from 'react'; + +import Capabilities from 'gmp/capabilities/capabilities'; +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import {setLocale} from 'gmp/locale/lang'; + +import Filter from 'gmp/models/filter'; +import Note from 'gmp/models/note'; + +import {entitiesLoadingActions} from 'web/store/entities/notes'; + +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; +import {defaultFilterLoadingActions} from 'web/store/usersettings/defaultfilters/actions'; +import {loadingActions} from 'web/store/usersettings/defaults/actions'; + +import {rendererWith, fireEvent, screen, wait} from 'web/utils/testing'; + +import NotesPage, {ToolBarIcons} from '../listpage'; + +setLocale('en'); + +window.URL.createObjectURL = jest.fn(); + +const note = Note.fromElement({ + _id: '6d00d22f-551b-4fbe-8215-d8615eff73ea', + active: 1, + creation_time: '2020-12-23T14:14:11Z', + hosts: '127.0.0.1', + in_use: 0, + modification_time: '2021-01-04T11:54:12Z', + nvt: { + _oid: '123', + name: 'foo nvt', + type: 'nvt', + }, + owner: {name: 'admin'}, + permissions: {permission: {name: 'Everything'}}, + port: '666', + text: 'note text', + writable: 1, +}); + +const caps = new Capabilities(['everything']); +const wrongCaps = new Capabilities(['get_config']); + +const reloadInterval = -1; +const manualUrl = 'test/'; + +const currentSettings = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +const getSetting = jest.fn().mockResolvedValue({ + filter: null, +}); + +const getDashboardSetting = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const getAggregates = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const getFilters = jest.fn().mockReturnValue( + Promise.resolve({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }), +); + +const getNotes = jest.fn().mockResolvedValue({ + data: [note], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const renewSession = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +describe('NotesPage tests', () => { + test('should render full NotesPage', async () => { + const gmp = { + dashboard: { + getSetting: getDashboardSetting, + }, + notes: { + get: getNotes, + getActiveDaysAggregates: getAggregates, + getCreatedAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {currentSettings, getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('note', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([note], filter, loadedFilter, counts), + ); + + const {baseElement} = render(); + + await wait(); + + const display = screen.getAllByTestId('grid-item'); + const inputs = baseElement.querySelectorAll('input'); + const selects = screen.getAllByTestId('select-selected-value'); + + // Toolbar Icons + expect(screen.getAllByTitle('Help: Notes')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('New Note')[0]).toBeInTheDocument(); + + // Powerfilter + expect(inputs[0]).toHaveAttribute('name', 'userFilterString'); + expect(screen.getAllByTitle('Update Filter')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Remove Filter')[0]).toBeInTheDocument(); + expect( + screen.getAllByTitle('Reset to Default Filter')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Help: Powerfilter')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Edit Filter')[0]).toBeInTheDocument(); + expect(selects[0]).toHaveAttribute('title', 'Loaded filter'); + expect(selects[0]).toHaveTextContent('--'); + + // Dashboard + expect( + screen.getAllByTitle('Add new Dashboard Display')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Reset to Defaults')[0]).toBeInTheDocument(); + expect(display[0]).toHaveTextContent('Notes by Active Days (Total: 0)'); + expect(display[1]).toHaveTextContent('Notes by Creation Time'); + expect(display[2]).toHaveTextContent('Notes Text Word Cloud'); + + // Table + const header = baseElement.querySelectorAll('th'); + + expect(header[0]).toHaveTextContent('Text'); + expect(header[1]).toHaveTextContent('NVT'); + expect(header[2]).toHaveTextContent('Host'); + expect(header[3]).toHaveTextContent('Location'); + expect(header[4]).toHaveTextContent('Active'); + expect(header[5]).toHaveTextContent('Actions'); + + const row = baseElement.querySelectorAll('tr'); + + expect(row[1]).toHaveTextContent('note text'); + expect(row[1]).toHaveTextContent('foo nvt'); + expect(row[1]).toHaveTextContent('127.0.0.1'); + expect(row[1]).toHaveTextContent('666'); + expect(row[1]).toHaveTextContent('yes'); + + expect( + screen.getAllByTitle('Move Note to trashcan')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Edit Note')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Clone Note')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Export Note')[0]).toBeInTheDocument(); + }); + + test('should allow to bulk action on page contents', async () => { + const deleteByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + dashboard: { + getSetting: getDashboardSetting, + }, + notes: { + get: getNotes, + deleteByFilter, + exportByFilter, + getActiveDaysAggregates: getAggregates, + getCreatedAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('note', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([note], filter, loadedFilter, counts), + ); + + render(); + + await wait(); + + // export page contents + const exportIcon = screen.getAllByTitle('Export page contents'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByFilter).toHaveBeenCalled(); + + // move page contents to trashcan + const deleteIcon = screen.getAllByTitle('Move page contents to trashcan'); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteByFilter).toHaveBeenCalled(); + }); + + test('should allow to bulk action on selected notes', async () => { + const deleteByIds = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByIds = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + dashboard: { + getSetting: getDashboardSetting, + }, + notes: { + get: getNotes, + delete: deleteByIds, + export: exportByIds, + getActiveDaysAggregates: getAggregates, + getCreatedAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('note', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([note], filter, loadedFilter, counts), + ); + + const {element} = render(); + + await wait(); + + const selectFields = screen.getAllByTestId('select-open-button'); + fireEvent.click(selectFields[1]); + + const selectItems = screen.getAllByTestId('select-item'); + fireEvent.click(selectItems[1]); + + const selected = screen.getAllByTestId('select-selected-value'); + expect(selected[1]).toHaveTextContent('Apply to selection'); + + const inputs = element.querySelectorAll('input'); + + // select a note + fireEvent.click(inputs[1]); + await wait(); + + // export selected note + const exportIcon = screen.getAllByTitle('Export selection'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByIds).toHaveBeenCalled(); + + // move selected note to trashcan + const deleteIcon = screen.getAllByTitle('Move selection to trashcan'); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteByIds).toHaveBeenCalled(); + }); + + test('should allow to bulk action on filtered notes', async () => { + const deleteByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + dashboard: { + getSetting: getDashboardSetting, + }, + notes: { + get: getNotes, + deleteByFilter, + exportByFilter, + getActiveDaysAggregates: getAggregates, + getCreatedAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('note', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([note], filter, loadedFilter, counts), + ); + + render(); + + await wait(); + + const selectFields = screen.getAllByTestId('select-open-button'); + fireEvent.click(selectFields[1]); + + const selectItems = screen.getAllByTestId('select-item'); + fireEvent.click(selectItems[2]); + + await wait(); + + const selected = screen.getAllByTestId('select-selected-value'); + expect(selected[1]).toHaveTextContent('Apply to all filtered'); + + // export all filtered notes + const exportIcon = screen.getAllByTitle('Export all filtered'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByFilter).toHaveBeenCalled(); + + // move all filtered notes to trashcan + const deleteIcon = screen.getAllByTitle('Move all filtered to trashcan'); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteByFilter).toHaveBeenCalled(); + }); +}); + +describe('NotesPage ToolBarIcons test', () => { + test('should render', () => { + const handleNoteCreateClick = jest.fn(); + + const gmp = { + settings: {manualUrl}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + const {element} = render( + , + ); + + const links = element.querySelectorAll('a'); + + expect(screen.getAllByTitle('Help: Notes')[0]).toBeInTheDocument(); + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/reports.html#managing-notes', + ); + }); + + test('should call click handlers', () => { + const handleNoteCreateClick = jest.fn(); + + const gmp = { + settings: {manualUrl}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render(); + + const newIcon = screen.getAllByTitle('New Note'); + + expect(newIcon[0]).toBeInTheDocument(); + + fireEvent.click(newIcon[0]); + expect(handleNoteCreateClick).toHaveBeenCalled(); + }); + + test('should not show icons if user does not have the right permissions', () => { + const handleNoteCreateClick = jest.fn(); + + const gmp = { + settings: {manualUrl}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: wrongCaps, + router: true, + }); + + const {queryAllByTestId} = render( + , + ); + + const icons = queryAllByTestId('svg-icon'); // this test is probably approppriate to keep in the old format + expect(icons.length).toBe(1); + expect(icons[0]).toHaveAttribute('title', 'Help: Notes'); + }); +}); diff --git a/gsa/src/web/pages/notes/detailspage.js b/gsa/src/web/pages/notes/detailspage.js index c68f570eaa..abc4086133 100644 --- a/gsa/src/web/pages/notes/detailspage.js +++ b/gsa/src/web/pages/notes/detailspage.js @@ -78,7 +78,7 @@ import PropTypes from 'web/utils/proptypes'; import NoteDetails from './details'; import NoteComponent from './component'; -const ToolBarIcons = ({ +export const ToolBarIcons = ({ entity, onNoteCloneClick, onNoteCreateClick, diff --git a/gsa/src/web/pages/notes/listpage.js b/gsa/src/web/pages/notes/listpage.js index ab06f6c0cb..b5133a95c0 100644 --- a/gsa/src/web/pages/notes/listpage.js +++ b/gsa/src/web/pages/notes/listpage.js @@ -47,18 +47,20 @@ import NoteComponent from './component'; import NotesDashboard, {NOTES_DASHBOARD_ID} from './dashboard'; import NoteIcon from 'web/components/icon/noteicon'; -const ToolBarIcons = withCapabilities(({capabilities, onNoteCreateClick}) => ( - - - {capabilities.mayCreate('note') && ( - - )} - -)); +export const ToolBarIcons = withCapabilities( + ({capabilities, onNoteCreateClick}) => ( + + + {capabilities.mayCreate('note') && ( + + )} + + ), +); ToolBarIcons.propTypes = { onNoteCreateClick: PropTypes.func, diff --git a/gsa/src/web/pages/nvts/__tests__/detailspage.js b/gsa/src/web/pages/nvts/__tests__/detailspage.js new file mode 100644 index 0000000000..4bc170ec81 --- /dev/null +++ b/gsa/src/web/pages/nvts/__tests__/detailspage.js @@ -0,0 +1,537 @@ +/* Copyright (C) 2021 Greenbone Networks GmbH + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import React from 'react'; + +import Capabilities from 'gmp/capabilities/capabilities'; +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import {setLocale} from 'gmp/locale/lang'; + +import Filter from 'gmp/models/filter'; +import NVT from 'gmp/models/nvt'; +import Note from 'gmp/models/note'; +import Override from 'gmp/models/override'; + +import {isDefined} from 'gmp/utils/identity'; + +import {entityLoadingActions} from 'web/store/entities/nvts'; +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; + +import {rendererWith, fireEvent, screen, wait} from 'web/utils/testing'; + +import Detailspage, {ToolBarIcons} from '../detailspage'; + +setLocale('en'); + +if (!isDefined(window.URL)) { + window.URL = {}; +} +window.URL.createObjectURL = jest.fn(); + +const caps = new Capabilities(['everything']); + +const reloadInterval = -1; +const manualUrl = 'test/'; + +const nvt = NVT.fromElement({ + _id: '12345', + owner: { + name: '', + }, + name: '12345', + comment: '', + creation_time: '2019-06-24T11:55:30Z', + modification_time: '2019-06-24T10:12:27Z', + timezone: 'UTC', + writable: 0, + in_use: 0, + permissions: '', + update_time: '2020-10-30T11:44:00.000+0000', + nvt: { + _oid: '12345', + name: 'foo', + creation_time: '2019-06-24T11:55:30Z', + modification_time: '2019-06-24T10:12:27Z', + family: 'bar', + cvss_base: 4.9, + qod: { + value: 80, + type: 'remote_banner', + }, + tags: + 'cvss_base_vector=AV:N/AC:M/Au:S/C:P/I:N/A:P|summary=This is a description|solution_type=VendorFix|insight=Foo|impact=Bar|vuldetect=Baz|affected=foo', + solution: { + _type: 'VendorFix', + __text: 'This is a description', + }, + timeout: '', + refs: { + ref: [ + {_type: 'cve', _id: 'CVE-2020-1234'}, + {_type: 'cve', _id: 'CVE-2020-5678'}, + ], + }, + }, +}); + +const note1 = Note.fromElement({ + _id: '5221d57f-3e62-4114-8e19-135a79b6b102', + active: 1, + creation_time: '2021-01-14T06:35:57Z', + hosts: '127.0.01.1', + in_use: 0, + end_time: '2021-02-13T07:35:20+01:00', + modification_time: '2021-01-14T06:35:57Z', + new_severity: -1, + timezone: 'UTC', + new_threat: 'False Positive', + nvt: { + _oid: '12345', + name: 'foo', + type: 'nvt', + }, + orphan: 0, + owner: { + name: 'admin', + }, + permissions: { + permission: { + name: 'everything', + }, + }, + port: '', + result: { + _id: '', + }, + severity: '', + task: { + _id: '', + name: '', + trash: 0, + }, + text: 'test_note', + threat: 'Internal Error', + writable: 1, +}); + +const override1 = Override.fromElement({ + _id: '5221d57f-3e62-4114-8e19-000000000001', + active: 1, + creation_time: '2021-01-14T05:35:57Z', + hosts: '127.0.01.1', + in_use: 0, + end_time: '2021-03-13T11:35:20+01:00', + modification_time: '2021-01-14T06:20:57Z', + timezone: 'UTC', + new_severity: -1, + new_threat: 'False Positive', + nvt: { + _oid: '12345', + name: 'foo', + type: 'nvt', + }, + orphan: 0, + owner: { + name: 'admin', + }, + permissions: { + permission: { + name: 'everything', + }, + }, + port: '', + result: { + _id: '', + }, + severity: '', + task: { + _id: '', + name: '', + trash: 0, + }, + text: 'test_override_1', + threat: 'Internal Error', + writable: 1, +}); + +const override2 = Override.fromElement({ + _id: '5221d57f-3e62-4114-8e19-000000000000', + active: 1, + creation_time: '2020-01-14T06:35:57Z', + hosts: '127.0.01.1', + in_use: 0, + end_time: '2021-02-13T12:35:20+01:00', + modification_time: '2020-02-14T06:35:57Z', + timezone: 'UTC', + new_severity: -1, + new_threat: 'False Positive', + nvt: { + _oid: '12345', + name: 'foo', + type: 'nvt', + }, + orphan: 0, + owner: { + name: 'admin', + }, + permissions: { + permission: { + name: 'everything', + }, + }, + port: '', + result: { + _id: '', + }, + severity: '', + task: { + _id: '', + name: '', + trash: 0, + }, + text: 'test_override_2', + threat: 'Internal Error', + writable: 1, +}); + +let getNvt; +let getNotes; +let getOverrides; +let getEntities; +let currentSettings; +let renewSession; + +beforeEach(() => { + getNvt = jest.fn().mockResolvedValue({ + data: nvt, + }); + + getNotes = jest.fn().mockResolvedValue({ + data: [note1], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + + getOverrides = jest.fn().mockResolvedValue({ + data: [override1, override2], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + + getEntities = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + + currentSettings = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + renewSession = jest.fn().mockResolvedValue({ + foo: 'bar', + }); +}); + +describe('Nvt Detailspage tests', () => { + test('should render full Detailspage', async () => { + const gmp = { + nvt: { + get: getNvt, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + }, + notes: { + get: getNotes, + }, + overrides: { + get: getOverrides, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('UTC')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('12345', nvt)); + + const {baseElement, element} = render(); + await wait(); + + expect(element).toHaveTextContent('NVT: foo'); + + const links = baseElement.querySelectorAll('a'); + + expect(screen.getAllByTitle('Help: NVTs')[0]).toBeInTheDocument(); + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/managing-secinfo.html#network-vulnerability-tests-nvt', + ); + + expect(screen.getAllByTitle('NVT List')[0]).toBeInTheDocument(); + expect(links[1]).toHaveAttribute('href', '/nvts'); + + expect(element).toHaveTextContent('ID:12345'); + expect(element).toHaveTextContent('Mon, Jun 24, 2019 11:55 AM UTC'); + expect(element).toHaveTextContent('Mon, Jun 24, 2019 10:12 AM UTC'); + expect(element).toHaveTextContent('Owner:(Global Object)'); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[8]).toHaveTextContent('Preferences'); + expect(spans[10]).toHaveTextContent('User Tags'); + + expect(element).toHaveTextContent('Summary'); + expect(element).toHaveTextContent('This is a description'); + + expect(element).toHaveTextContent('Scoring'); + expect(element).toHaveTextContent('CVSS Base'); + expect(element).toHaveTextContent('4.9 (Medium)'); + expect(element).toHaveTextContent('CVSS Base Vector'); + expect(element).toHaveTextContent('AV:N/AC:M/Au:S/C:P/I:N/A:P'); + expect(element).toHaveTextContent('CVSS Origin'); + expect(element).toHaveTextContent('N/A'); + + expect(element).toHaveTextContent('Insight'); + expect(element).toHaveTextContent('Foo'); + + expect(element).toHaveTextContent('Detection Method'); + expect(element).toHaveTextContent('Baz'); + + expect(element).toHaveTextContent('Affected Software/OS'); + expect(element).toHaveTextContent('foo'); + + expect(element).toHaveTextContent('Impact'); + expect(element).toHaveTextContent('Bar'); + + expect(element).toHaveTextContent('Solution'); + + expect(element).toHaveTextContent('Family'); + expect(element).toHaveTextContent('bar'); + + expect(element).toHaveTextContent('References'); + expect(element).toHaveTextContent('CVECVE-2020-1234'); + + expect(element).toHaveTextContent('Overrides'); + expect(element).toHaveTextContent('Override from Any to False Positive'); + expect(element).toHaveTextContent('test_override_1'); + expect(element).toHaveTextContent('Active until'); + expect(element).toHaveTextContent('Sat, Mar 13, 2021 10:35 AM UTC'); + expect(element).toHaveTextContent('Modified'); + expect(element).toHaveTextContent('Thu, Jan 14, 2021 6:20 AM UTC'); + + expect(element).toHaveTextContent('test_override_2'); + expect(element).toHaveTextContent('Active until'); + expect(element).toHaveTextContent('Sat, Feb 13, 2021 11:35 AM UTC'); + expect(element).toHaveTextContent('Modified'); + expect(element).toHaveTextContent('Fri, Feb 14, 2020 6:35 AM UTC'); + + expect(element).toHaveTextContent('Notes'); + expect(element).toHaveTextContent('Note'); + expect(element).toHaveTextContent('test_note'); + expect(element).toHaveTextContent('Active until'); + expect(element).toHaveTextContent('Sat, Feb 13, 2021 6:35 AM UTC'); + expect(element).toHaveTextContent('Modified'); + expect(element).toHaveTextContent('Thu, Jan 14, 2021 6:35 AM UTC'); + }); + + test('should render preferences tab', () => { + const gmp = { + nvt: { + get: getNvt, + }, + notes: { + get: getEntities, + }, + overrides: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('UTC')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('12345', nvt)); + + const {baseElement} = render(); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[8]).toHaveTextContent('Preferences'); + + expect(spans[8]).toHaveTextContent('Preferences'); + fireEvent.click(spans[8]); + + expect(baseElement).toHaveTextContent('Name'); + expect(baseElement).toHaveTextContent('Timeout'); + expect(baseElement).toHaveTextContent('Default Value'); + expect(baseElement).toHaveTextContent('default'); + }); + + test('should render user tags tab', () => { + const gmp = { + nvt: { + get: getNvt, + }, + notes: { + get: getEntities, + }, + overrides: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('UTC')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('12345', nvt)); + + const {baseElement} = render(); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[10]).toHaveTextContent('User Tags'); + + fireEvent.click(spans[10]); + + expect(baseElement).toHaveTextContent('No user tags available'); + }); +}); + +describe('Nvt ToolBarIcons tests', () => { + test('should render', () => { + const handleNvtDownloadClick = jest.fn(); + const handleOnNoteCreateClick = jest.fn(); + const handleOnOverrideCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + const {element} = render( + , + ); + + const links = element.querySelectorAll('a'); + + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/managing-secinfo.html#network-vulnerability-tests-nvt', + ); + expect(screen.getAllByTitle('Help: NVTs')[0]).toBeInTheDocument(); + + expect(links[1]).toHaveAttribute('href', '/nvts'); + expect(screen.getAllByTitle('NVT List')[0]).toBeInTheDocument(); + + expect(links[2]).toHaveAttribute('href', '/results?filter=nvt%3D12345'); + expect( + screen.getAllByTitle('Corresponding Results')[0], + ).toBeInTheDocument(); + + expect(links[3]).toHaveAttribute( + 'href', + '/vulnerabilities?filter=uuid%3D12345', + ); + expect( + screen.getAllByTitle('Corresponding Vulnerabilities')[0], + ).toBeInTheDocument(); + }); + + test('should call click handlers', () => { + const handleNvtDownloadClick = jest.fn(); + const handleOnNoteCreateClick = jest.fn(); + const handleOnOverrideCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + + const exportIcon = screen.getAllByTitle('Export NVT'); + const addNewNoteIcon = screen.getAllByTitle('Add new Note'); + const addNewOverrideIcon = screen.getAllByTitle('Add new Override'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + expect(handleNvtDownloadClick).toHaveBeenCalledWith(nvt); + + expect(addNewNoteIcon[0]).toBeInTheDocument(); + fireEvent.click(addNewNoteIcon[0]); + expect(handleOnNoteCreateClick).toHaveBeenCalledWith(nvt); + + expect(addNewOverrideIcon[0]).toBeInTheDocument(); + fireEvent.click(addNewOverrideIcon[0]); + expect(handleOnOverrideCreateClick).toHaveBeenCalledWith(nvt); + }); +}); diff --git a/gsa/src/web/pages/nvts/__tests__/listpage.js b/gsa/src/web/pages/nvts/__tests__/listpage.js new file mode 100644 index 0000000000..ae059e20f1 --- /dev/null +++ b/gsa/src/web/pages/nvts/__tests__/listpage.js @@ -0,0 +1,478 @@ +/* Copyright (C) 2021 Greenbone Networks GmbH + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import React from 'react'; + +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import {setLocale} from 'gmp/locale/lang'; + +import Filter from 'gmp/models/filter'; +import NVT from 'gmp/models/nvt'; + +import {entitiesLoadingActions} from 'web/store/entities/nvts'; + +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; +import {defaultFilterLoadingActions} from 'web/store/usersettings/defaultfilters/actions'; +import {loadingActions} from 'web/store/usersettings/defaults/actions'; + +import {rendererWith, fireEvent, screen, wait} from 'web/utils/testing'; + +import NvtsPage, {ToolBarIcons} from '../listpage'; + +setLocale('en'); + +window.URL.createObjectURL = jest.fn(); + +const nvt = NVT.fromElement({ + _oid: '1.3.6.1.4.1.25623.1.0', + name: 'foo', + creation_time: '2019-06-24T11:55:30Z', + modification_time: '2019-06-24T10:12:27Z', + family: 'bar', + cvss_base: 5, + qod: {value: 80}, + tags: 'This is a description|solution_type=VendorFix', + solution: { + _type: 'VendorFix', + __text: 'This is a description', + }, + refs: { + ref: [ + {_type: 'cve', _id: 'CVE-2020-1234'}, + {_type: 'cve', _id: 'CVE-2020-5678'}, + ], + }, +}); + +const reloadInterval = -1; +const manualUrl = 'test/'; + +const currentSettings = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +const getSetting = jest.fn().mockResolvedValue({ + filter: null, +}); + +const getDashboardSetting = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const getAggregates = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const getFilters = jest.fn().mockReturnValue( + Promise.resolve({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }), +); + +const getNvts = jest.fn().mockResolvedValue({ + data: [nvt], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const renewSession = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +describe('NvtsPage tests', () => { + test('should render full NvtsPage', async () => { + const gmp = { + dashboard: { + getSetting: getDashboardSetting, + }, + nvts: { + get: getNvts, + getFamilyAggregates: getAggregates, + getSeverityAggregates: getAggregates, + getCreatedAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {currentSettings, getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('nvt', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([nvt], filter, loadedFilter, counts), + ); + + const {baseElement} = render(); + + await wait(); + + const display = screen.getAllByTestId('grid-item'); + const inputs = baseElement.querySelectorAll('input'); + const selects = screen.getAllByTestId('select-selected-value'); + + // Toolbar Icons + expect(screen.getAllByTitle('Help: NVTs')[0]).toBeInTheDocument(); + + // Powerfilter + expect(inputs[0]).toHaveAttribute('name', 'userFilterString'); + expect(screen.getAllByTitle('Update Filter')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Remove Filter')[0]).toBeInTheDocument(); + expect( + screen.getAllByTitle('Reset to Default Filter')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Help: Powerfilter')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Edit Filter')[0]).toBeInTheDocument(); + expect(selects[0]).toHaveAttribute('title', 'Loaded filter'); + expect(selects[0]).toHaveTextContent('--'); + + // Dashboard + expect( + screen.getAllByTitle('Add new Dashboard Display')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Reset to Defaults')[0]).toBeInTheDocument(); + expect(display[0]).toHaveTextContent('NVTs by Severity Class (Total: 0)'); + expect(display[1]).toHaveTextContent('NVTs by Creation Time'); + expect(display[2]).toHaveTextContent('NVTs by Family (Total: 0)'); + + // Table + const header = baseElement.querySelectorAll('th'); + + expect(header[0]).toHaveTextContent('Name'); + expect(header[1]).toHaveTextContent('Family'); + expect(header[2]).toHaveTextContent('Created'); + expect(header[3]).toHaveTextContent('Modified'); + expect(header[4]).toHaveTextContent('CVE'); + expect(header[5]).toHaveTextContent('solution_type.svg'); + expect(header[6]).toHaveTextContent('Severity'); + expect(header[7]).toHaveTextContent('QoD'); + + const row = baseElement.querySelectorAll('tr'); + + expect(row[1]).toHaveTextContent('foo'); + expect(row[1]).toHaveTextContent('bar'); + expect(row[1]).toHaveTextContent('Mon, Jun 24, 2019 1:55 PM CEST'); + expect(row[1]).toHaveTextContent('Mon, Jun 24, 2019 12:12 PM CEST'); + expect(row[1]).toHaveTextContent('CVE-2020-1234'); + expect(row[1]).toHaveTextContent('CVE-2020-5678'); + expect(row[1]).toHaveTextContent('st_vendorfix.svg'); + expect(row[1]).toHaveTextContent('80 %'); + }); + + test('should allow to bulk action on page contents', async () => { + const deleteByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + dashboard: { + getSetting: getDashboardSetting, + }, + nvts: { + get: getNvts, + deleteByFilter, + exportByFilter, + getActiveDaysAggregates: getAggregates, + getCreatedAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('nvt', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([nvt], filter, loadedFilter, counts), + ); + + render(); + + await wait(); + + // export page contents + const exportIcon = screen.getAllByTitle('Export page contents'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByFilter).toHaveBeenCalled(); + }); + + test('should allow to bulk action on selected nvts', async () => { + const deleteByIds = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByIds = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + dashboard: { + getSetting: getDashboardSetting, + }, + nvts: { + get: getNvts, + delete: deleteByIds, + export: exportByIds, + getActiveDaysAggregates: getAggregates, + getCreatedAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('nvt', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([nvt], filter, loadedFilter, counts), + ); + + const {element} = render(); + + await wait(); + + const selectFields = screen.getAllByTestId('select-open-button'); + fireEvent.click(selectFields[1]); + + const selectItems = screen.getAllByTestId('select-item'); + fireEvent.click(selectItems[1]); + + const selected = screen.getAllByTestId('select-selected-value'); + expect(selected[1]).toHaveTextContent('Apply to selection'); + + const inputs = element.querySelectorAll('input'); + + // select a nvt + fireEvent.click(inputs[1]); + await wait(); + + // export selected nvt + const exportIcon = screen.getAllByTitle('Export selection'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByIds).toHaveBeenCalled(); + }); + + test('should allow to bulk action on filtered nvts', async () => { + const deleteByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + dashboard: { + getSetting: getDashboardSetting, + }, + nvts: { + get: getNvts, + deleteByFilter, + exportByFilter, + getActiveDaysAggregates: getAggregates, + getCreatedAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('nvt', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([nvt], filter, loadedFilter, counts), + ); + + render(); + + await wait(); + + const selectFields = screen.getAllByTestId('select-open-button'); + fireEvent.click(selectFields[1]); + + const selectItems = screen.getAllByTestId('select-item'); + fireEvent.click(selectItems[2]); + + await wait(); + + const selected = screen.getAllByTestId('select-selected-value'); + expect(selected[1]).toHaveTextContent('Apply to all filtered'); + + // export all filtered nvts + const exportIcon = screen.getAllByTitle('Export all filtered'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByFilter).toHaveBeenCalled(); + }); +}); + +describe('NvtsPage ToolBarIcons test', () => { + test('should render', () => { + const gmp = { + settings: {manualUrl}, + }; + + const {render} = rendererWith({ + gmp, + router: true, + }); + + const {baseElement} = render(); + + const links = baseElement.querySelectorAll('a'); + expect(screen.getAllByTitle('Help: NVTs')[0]).toBeInTheDocument(); + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/managing-secinfo.html#network-vulnerability-tests-nvt', + ); + }); +}); diff --git a/gsa/src/web/pages/nvts/detailspage.js b/gsa/src/web/pages/nvts/detailspage.js index fefad6ead2..314244cdac 100644 --- a/gsa/src/web/pages/nvts/detailspage.js +++ b/gsa/src/web/pages/nvts/detailspage.js @@ -73,7 +73,7 @@ import NvtComponent from './component'; import NvtDetails from './details'; import Preferences from './preferences'; -let ToolBarIcons = ({ +export let ToolBarIcons = ({ capabilities, entity, onNoteCreateClick, diff --git a/gsa/src/web/pages/nvts/listpage.js b/gsa/src/web/pages/nvts/listpage.js index ff2a3a46de..4dac55e83c 100644 --- a/gsa/src/web/pages/nvts/listpage.js +++ b/gsa/src/web/pages/nvts/listpage.js @@ -42,7 +42,7 @@ import NvtsTable from './table'; import NvtsDashboard, {NVTS_DASHBOARD_ID} from './dashboard'; -const ToolBarIcons = () => ( +export const ToolBarIcons = () => ( . + */ +import React from 'react'; + +import Capabilities from 'gmp/capabilities/capabilities'; +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import {setLocale} from 'gmp/locale/lang'; + +import Filter from 'gmp/models/filter'; +import Override from 'gmp/models/override'; + +import {isDefined} from 'gmp/utils/identity'; + +import {entityLoadingActions} from 'web/store/entities/overrides'; +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; + +import {rendererWith, fireEvent, screen, wait} from 'web/utils/testing'; + +import Detailspage, {ToolBarIcons} from '../detailspage'; + +setLocale('en'); + +if (!isDefined(window.URL)) { + window.URL = {}; +} +window.URL.createObjectURL = jest.fn(); + +const caps = new Capabilities(['everything']); + +const reloadInterval = -1; +const manualUrl = 'test/'; + +const override = Override.fromElement({ + _id: '6d00d22f-551b-4fbe-8215-d8615eff73ea', + active: 1, + creation_time: '2020-12-23T14:14:11Z', + hosts: '127.0.0.1', + in_use: 0, + modification_time: '2021-01-04T11:54:12Z', + new_severity: '-1', // false positive + nvt: { + _oid: '123', + name: 'foo nvt', + type: 'nvt', + }, + owner: {name: 'admin'}, + permissions: {permission: {name: 'Everything'}}, + port: '666', + severity: '0.1', + task: { + name: 'task x', + _id: '42', + }, + text: 'override text', + writable: 1, +}); + +const overrideInUse = Override.fromElement({ + _id: '6d00d22f-551b-4fbe-8215-d8615eff73ea', + active: 1, + creation_time: '2020-12-23T14:14:11Z', + hosts: '127.0.0.1', + in_use: 1, + modification_time: '2021-01-04T11:54:12Z', + new_severity: '-1', // false positive + nvt: { + _oid: '123', + name: 'foo nvt', + type: 'nvt', + }, + owner: {name: 'admin'}, + permissions: {permission: {name: 'Everything'}}, + port: '666', + severity: '0.1', + text: 'override text', + writable: 1, +}); + +const noPermOverride = Override.fromElement({ + _id: '6d00d22f-551b-4fbe-8215-d8615eff73ea', + active: 1, + creation_time: '2020-12-23T14:14:11Z', + hosts: '127.0.0.1', + in_use: 0, + modification_time: '2021-01-04T11:54:12Z', + new_severity: '-1', // false positive + nvt: { + _oid: '123', + name: 'foo nvt', + type: 'nvt', + }, + owner: {name: 'admin'}, + permissions: {permission: {name: 'get_overrides'}}, + port: '666', + severity: '0.1', + text: 'override text', + writable: 1, +}); + +const getOverride = jest.fn().mockResolvedValue({ + data: override, +}); + +const getEntities = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const currentSettings = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +const renewSession = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +describe('Override detailspage tests', () => { + test('should render full detailspage', () => { + const gmp = { + override: { + get: getOverride, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch( + entityLoadingActions.success( + '6d00d22f-551b-4fbe-8215-d8615eff73ea', + override, + ), + ); + + const {baseElement, element} = render( + , + ); + + expect(element).toHaveTextContent('override text'); + + const links = baseElement.querySelectorAll('a'); + + expect(screen.getAllByTitle('Help: Overrides')[0]).toBeInTheDocument(); + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/reports.html#managing-overrides', + ); + + expect(screen.getAllByTitle('Override List')[0]).toBeInTheDocument(); + expect(links[1]).toHaveAttribute('href', '/overrides'); + + expect(element).toHaveTextContent( + 'ID:6d00d22f-551b-4fbe-8215-d8615eff73ea', + ); + expect(element).toHaveTextContent('Created:Wed, Dec 23, 2020 3:14 PM CET'); + expect(element).toHaveTextContent('Modified:Mon, Jan 4, 2021 12:54 PM CET'); + expect(element).toHaveTextContent('Owner:admin'); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[9]).toHaveTextContent('User Tags'); + expect(spans[11]).toHaveTextContent('Permissions'); + + expect(element).toHaveTextContent('NVT Name'); + expect(element).toHaveTextContent('foo nvt'); + + expect(element).toHaveTextContent('NVT OID'); + expect(element).toHaveTextContent('123'); + + expect(element).toHaveTextContent('Active'); + expect(element).toHaveTextContent('Yes'); + + expect(element).toHaveTextContent('Application'); + + expect(element).toHaveTextContent('Hosts'); + expect(element).toHaveTextContent('127.0.0.1'); + + expect(element).toHaveTextContent('Port'); + expect(element).toHaveTextContent('666'); + + expect(element).toHaveTextContent('Severity'); + expect(element).toHaveTextContent('Any'); + + expect(element).toHaveTextContent('Task'); + expect(element).toHaveTextContent('task x'); + + expect(element).toHaveTextContent('Result'); + expect(element).toHaveTextContent('Any'); + + expect(element).toHaveTextContent('Appearance'); + + expect(element).toHaveTextContent( + 'Override from Severity > 0.0 to False Positive', + ); + + expect(element).toHaveTextContent('override text'); + }); + + test('should render user tags tab', () => { + const gmp = { + override: { + get: getOverride, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch( + entityLoadingActions.success( + '6d00d22f-551b-4fbe-8215-d8615eff73ea', + override, + ), + ); + + const {baseElement} = render( + , + ); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[9]).toHaveTextContent('User Tags'); + + fireEvent.click(spans[9]); + + expect(baseElement).toHaveTextContent('No user tags available'); + }); + + test('should render permissions tab', () => { + const gmp = { + override: { + get: getOverride, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch( + entityLoadingActions.success( + '6d00d22f-551b-4fbe-8215-d8615eff73ea', + override, + ), + ); + + const {baseElement} = render( + , + ); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[11]).toHaveTextContent('Permissions'); + + fireEvent.click(spans[11]); + + expect(baseElement).toHaveTextContent('No permissions available'); + }); + + test('should call commands', async () => { + const clone = jest.fn().mockResolvedValue({ + data: {id: 'foo'}, + }); + + const deleteFunc = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportFunc = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + override: { + get: getOverride, + clone, + delete: deleteFunc, + export: exportFunc, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch( + entityLoadingActions.success( + '6d00d22f-551b-4fbe-8215-d8615eff73ea', + override, + ), + ); + + render(); + + await wait(); + + const cloneIcon = screen.getAllByTitle('Clone Override'); + expect(cloneIcon[0]).toBeInTheDocument(); + + fireEvent.click(cloneIcon[0]); + + await wait(); + + expect(clone).toHaveBeenCalledWith(override); + + const exportIcon = screen.getAllByTitle('Export Override as XML'); + expect(exportIcon[0]).toBeInTheDocument(); + + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportFunc).toHaveBeenCalledWith(override); + + const deleteIcon = screen.getAllByTitle('Move Override to trashcan'); + expect(deleteIcon[0]).toBeInTheDocument(); + + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteFunc).toHaveBeenCalledWith({id: override.id}); + }); +}); + +describe('Override ToolBarIcons tests', () => { + test('should render', () => { + const handleOverrideCloneClick = jest.fn(); + const handleOverrideDeleteClick = jest.fn(); + const handleOverrideDownloadClick = jest.fn(); + const handleOverrideEditClick = jest.fn(); + const handleOverrideCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + const {element} = render( + , + ); + + const links = element.querySelectorAll('a'); + + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/reports.html#managing-overrides', + ); + expect(screen.getAllByTitle('Help: Overrides')[0]).toBeInTheDocument(); + + expect(links[1]).toHaveAttribute('href', '/overrides'); + expect(screen.getAllByTitle('Override List')[0]).toBeInTheDocument(); + }); + + test('should call click handlers', () => { + const handleOverrideCloneClick = jest.fn(); + const handleOverrideDeleteClick = jest.fn(); + const handleOverrideDownloadClick = jest.fn(); + const handleOverrideEditClick = jest.fn(); + const handleOverrideCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + + const cloneIcon = screen.getAllByTitle('Clone Override'); + const editIcon = screen.getAllByTitle('Edit Override'); + const deleteIcon = screen.getAllByTitle('Move Override to trashcan'); + const exportIcon = screen.getAllByTitle('Export Override as XML'); + + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + expect(handleOverrideCloneClick).toHaveBeenCalledWith(override); + + expect(editIcon[0]).toBeInTheDocument(); + fireEvent.click(editIcon[0]); + expect(handleOverrideEditClick).toHaveBeenCalledWith(override); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + expect(handleOverrideDeleteClick).toHaveBeenCalledWith(override); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + expect(handleOverrideDownloadClick).toHaveBeenCalledWith(override); + }); + + test('should not call click handlers without permission', () => { + const handleOverrideCloneClick = jest.fn(); + const handleOverrideDeleteClick = jest.fn(); + const handleOverrideDownloadClick = jest.fn(); + const handleOverrideEditClick = jest.fn(); + const handleOverrideCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + + const cloneIcon = screen.getAllByTitle('Clone Override'); + const editIcon = screen.getAllByTitle('Permission to edit Override denied'); + const deleteIcon = screen.getAllByTitle( + 'Permission to move Override to trashcan denied', + ); + const exportIcon = screen.getAllByTitle('Export Override as XML'); + + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + + expect(handleOverrideCloneClick).toHaveBeenCalledWith(noPermOverride); + + expect(editIcon[0]).toBeInTheDocument(); + fireEvent.click(editIcon[0]); + + expect(handleOverrideEditClick).not.toHaveBeenCalled(); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + expect(handleOverrideDeleteClick).not.toHaveBeenCalled(); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + expect(handleOverrideDownloadClick).toHaveBeenCalledWith(noPermOverride); + }); + + test('should call correct click handlers for override in use', () => { + const handleOverrideCloneClick = jest.fn(); + const handleOverrideDeleteClick = jest.fn(); + const handleOverrideDownloadClick = jest.fn(); + const handleOverrideEditClick = jest.fn(); + const handleOverrideCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + const cloneIcon = screen.getAllByTitle('Clone Override'); + const editIcon = screen.getAllByTitle('Edit Override'); + const deleteIcon = screen.getAllByTitle('Override is still in use'); + const exportIcon = screen.getAllByTitle('Export Override as XML'); + + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + + expect(handleOverrideCloneClick).toHaveBeenCalledWith(overrideInUse); + + expect(editIcon[0]).toBeInTheDocument(); + fireEvent.click(editIcon[0]); + + expect(handleOverrideEditClick).toHaveBeenCalled(); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + expect(handleOverrideDeleteClick).not.toHaveBeenCalled(); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + expect(handleOverrideDownloadClick).toHaveBeenCalledWith(overrideInUse); + }); +}); diff --git a/gsa/src/web/pages/overrides/__tests__/listpage.js b/gsa/src/web/pages/overrides/__tests__/listpage.js new file mode 100644 index 0000000000..d81c7e617a --- /dev/null +++ b/gsa/src/web/pages/overrides/__tests__/listpage.js @@ -0,0 +1,570 @@ +/* Copyright (C) 2021 Greenbone Networks GmbH + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import React from 'react'; + +import Capabilities from 'gmp/capabilities/capabilities'; +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import {setLocale} from 'gmp/locale/lang'; + +import Filter from 'gmp/models/filter'; +import Override from 'gmp/models/override'; + +import {entitiesLoadingActions} from 'web/store/entities/overrides'; + +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; +import {defaultFilterLoadingActions} from 'web/store/usersettings/defaultfilters/actions'; +import {loadingActions} from 'web/store/usersettings/defaults/actions'; + +import {rendererWith, fireEvent, screen, wait} from 'web/utils/testing'; + +import OverridesPage, {ToolBarIcons} from '../listpage'; + +setLocale('en'); + +window.URL.createObjectURL = jest.fn(); + +const override = Override.fromElement({ + _id: '6d00d22f-551b-4fbe-8215-d8615eff73ea', + active: 1, + creation_time: '2020-12-23T14:14:11Z', + hosts: '127.0.0.1', + in_use: 0, + modification_time: '2021-01-04T11:54:12Z', + new_severity: '-1', // false positive + nvt: { + _oid: '123', + name: 'foo nvt', + type: 'nvt', + }, + owner: {name: 'admin'}, + permissions: {permission: {name: 'Everything'}}, + port: '666', + severity: '0.1', + text: 'override text', + writable: 1, +}); + +const caps = new Capabilities(['everything']); +const wrongCaps = new Capabilities(['get_config']); + +const reloadInterval = -1; +const manualUrl = 'test/'; + +const currentSettings = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +const getSetting = jest.fn().mockResolvedValue({ + filter: null, +}); + +const getDashboardSetting = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const getAggregates = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const getFilters = jest.fn().mockReturnValue( + Promise.resolve({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }), +); + +const getOverrides = jest.fn().mockResolvedValue({ + data: [override], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const renewSession = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +describe('OverridesPage tests', () => { + test('should render full OverridesPage', async () => { + const gmp = { + dashboard: { + getSetting: getDashboardSetting, + }, + overrides: { + get: getOverrides, + getActiveDaysAggregates: getAggregates, + getCreatedAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {currentSettings, getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('override', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([override], filter, loadedFilter, counts), + ); + + const {baseElement} = render(); + + await wait(); + + const display = screen.getAllByTestId('grid-item'); + const inputs = baseElement.querySelectorAll('input'); + const selects = screen.getAllByTestId('select-selected-value'); + + // Toolbar Icons + expect(screen.getAllByTitle('Help: Overrides')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('New Override')[0]).toBeInTheDocument(); + + // Powerfilter + expect(inputs[0]).toHaveAttribute('name', 'userFilterString'); + expect(screen.getAllByTitle('Update Filter')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Remove Filter')[0]).toBeInTheDocument(); + expect( + screen.getAllByTitle('Reset to Default Filter')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Help: Powerfilter')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Edit Filter')[0]).toBeInTheDocument(); + expect(selects[0]).toHaveAttribute('title', 'Loaded filter'); + expect(selects[0]).toHaveTextContent('--'); + + // Dashboard + expect( + screen.getAllByTitle('Add new Dashboard Display')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Reset to Defaults')[0]).toBeInTheDocument(); + expect(display[0]).toHaveTextContent('Overrides by Active Days (Total: 0)'); + expect(display[1]).toHaveTextContent('Overrides by Creation Time'); + expect(display[2]).toHaveTextContent('Overrides Text Word Cloud'); + + // Table + const header = baseElement.querySelectorAll('th'); + + expect(header[0]).toHaveTextContent('Text'); + expect(header[1]).toHaveTextContent('NVT'); + expect(header[2]).toHaveTextContent('Hosts'); + expect(header[3]).toHaveTextContent('Location'); + expect(header[4]).toHaveTextContent('From'); + expect(header[5]).toHaveTextContent('To'); + expect(header[6]).toHaveTextContent('Active'); + expect(header[7]).toHaveTextContent('Actions'); + + const row = baseElement.querySelectorAll('tr'); + + expect(row[1]).toHaveTextContent('override text'); + expect(row[1]).toHaveTextContent('foo nvt'); + expect(row[1]).toHaveTextContent('127.0.0.1'); + expect(row[1]).toHaveTextContent('666'); + expect(row[1]).toHaveTextContent('> 0.0'); + expect(row[1]).toHaveTextContent('False Positive'); + expect(row[1]).toHaveTextContent('yes'); + + expect( + screen.getAllByTitle('Move Override to trashcan')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Edit Override')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Clone Override')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Export Override')[0]).toBeInTheDocument(); + }); + + test('should allow to bulk action on page contents', async () => { + const deleteByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + dashboard: { + getSetting: getDashboardSetting, + }, + overrides: { + get: getOverrides, + deleteByFilter, + exportByFilter, + getActiveDaysAggregates: getAggregates, + getCreatedAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('override', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([override], filter, loadedFilter, counts), + ); + + render(); + + await wait(); + + // export page contents + const exportIcon = screen.getAllByTitle('Export page contents'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByFilter).toHaveBeenCalled(); + + // move page contents to trashcan + const deleteIcon = screen.getAllByTitle('Move page contents to trashcan'); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteByFilter).toHaveBeenCalled(); + }); + + test('should allow to bulk action on selected overrides', async () => { + const deleteByIds = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByIds = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + dashboard: { + getSetting: getDashboardSetting, + }, + overrides: { + get: getOverrides, + delete: deleteByIds, + export: exportByIds, + getActiveDaysAggregates: getAggregates, + getCreatedAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('override', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([override], filter, loadedFilter, counts), + ); + + const {element} = render(); + + await wait(); + + const selectFields = screen.getAllByTestId('select-open-button'); + fireEvent.click(selectFields[1]); + + const selectItems = screen.getAllByTestId('select-item'); + fireEvent.click(selectItems[1]); + + const selected = screen.getAllByTestId('select-selected-value'); + expect(selected[1]).toHaveTextContent('Apply to selection'); + + const inputs = element.querySelectorAll('input'); + + // select a override + fireEvent.click(inputs[1]); + await wait(); + + // export selected override + const exportIcon = screen.getAllByTitle('Export selection'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByIds).toHaveBeenCalled(); + + // move selected override to trashcan + const deleteIcon = screen.getAllByTitle('Move selection to trashcan'); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteByIds).toHaveBeenCalled(); + }); + + test('should allow to bulk action on filtered overrides', async () => { + const deleteByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + dashboard: { + getSetting: getDashboardSetting, + }, + overrides: { + get: getOverrides, + deleteByFilter, + exportByFilter, + getActiveDaysAggregates: getAggregates, + getCreatedAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('override', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([override], filter, loadedFilter, counts), + ); + + render(); + + await wait(); + + const selectFields = screen.getAllByTestId('select-open-button'); + fireEvent.click(selectFields[1]); + + const selectItems = screen.getAllByTestId('select-item'); + fireEvent.click(selectItems[2]); + + await wait(); + + const selected = screen.getAllByTestId('select-selected-value'); + expect(selected[1]).toHaveTextContent('Apply to all filtered'); + + // export all filtered overrides + const exportIcon = screen.getAllByTitle('Export all filtered'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByFilter).toHaveBeenCalled(); + + // move all filtered overrides to trashcan + const deleteIcon = screen.getAllByTitle('Move all filtered to trashcan'); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteByFilter).toHaveBeenCalled(); + }); +}); + +describe('OverridesPage ToolBarIcons test', () => { + test('should render', () => { + const handleOverrideCreateClick = jest.fn(); + + const gmp = { + settings: {manualUrl}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + const {element} = render( + , + ); + + const links = element.querySelectorAll('a'); + + expect(screen.getAllByTitle('Help: Overrides')[0]).toBeInTheDocument(); + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/reports.html#managing-overrides', + ); + }); + + test('should call click handlers', () => { + const handleOverrideCreateClick = jest.fn(); + + const gmp = { + settings: {manualUrl}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render(); + + const newIcon = screen.getAllByTitle('New Override'); + + expect(newIcon[0]).toBeInTheDocument(); + + fireEvent.click(newIcon[0]); + expect(handleOverrideCreateClick).toHaveBeenCalled(); + }); + + test('should not show icons if user does not have the right permissions', () => { + const handleOverrideCreateClick = jest.fn(); + + const gmp = { + settings: {manualUrl}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: wrongCaps, + router: true, + }); + + const {queryAllByTestId} = render( + , + ); + + const icons = queryAllByTestId('svg-icon'); // this test is probably approppriate to keep in the old format + expect(icons.length).toBe(1); + expect(icons[0]).toHaveAttribute('title', 'Help: Overrides'); + }); +}); diff --git a/gsa/src/web/pages/overrides/detailspage.js b/gsa/src/web/pages/overrides/detailspage.js index 2b16a3b5b0..6f87068f44 100644 --- a/gsa/src/web/pages/overrides/detailspage.js +++ b/gsa/src/web/pages/overrides/detailspage.js @@ -81,7 +81,7 @@ import {renderYesNo} from 'web/utils/render'; import OverrideDetails from './details'; import OverrideComponent from './component'; -const ToolBarIcons = ({ +export const ToolBarIcons = ({ entity, onOverrideCloneClick, onOverrideCreateClick, diff --git a/gsa/src/web/pages/overrides/listpage.js b/gsa/src/web/pages/overrides/listpage.js index 397bd3d9fa..5891fadd3e 100644 --- a/gsa/src/web/pages/overrides/listpage.js +++ b/gsa/src/web/pages/overrides/listpage.js @@ -47,7 +47,7 @@ import OverrideComponent from './component'; import OverridesDashboard, {OVERRIDES_DASHBOARD_ID} from './dashboard'; -const ToolBarIcons = withCapabilities( +export const ToolBarIcons = withCapabilities( ({capabilities, onOverrideCreateClick}) => ( . + */ +import React from 'react'; + +import Capabilities from 'gmp/capabilities/capabilities'; + +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import {setLocale} from 'gmp/locale/lang'; + +import Filter from 'gmp/models/filter'; +import Result from 'gmp/models/result'; + +import {isDefined} from 'gmp/utils/identity'; + +import {entityLoadingActions} from 'web/store/entities/results'; +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; + +import {rendererWith, fireEvent, screen, wait} from 'web/utils/testing'; + +import Detailspage, {ToolBarIcons} from '../detailspage'; + +// setup + +setLocale('en'); + +if (!isDefined(window.URL)) { + window.URL = {}; +} +window.URL.createObjectURL = jest.fn(); + +const reloadInterval = -1; +const manualUrl = 'test/'; + +// mock entity + +export const result = Result.fromElement({ + _id: '12345', + name: 'foo', + owner: {name: 'admin'}, + comment: 'bar', + creation_time: '2019-06-02T12:00:00Z', + modification_time: '2019-06-03T11:00:00Z', + host: {__text: '109.876.54.321'}, + port: '80/tcp', + nvt: { + _oid: '1.3.6.1.4.1.25623.1.12345', + type: 'nvt', + name: 'nvt1', + tags: + 'cvss_base_vector=AV:N/AC:M/Au:N/C:P/I:N/A:N|summary=This is a mock result|insight=This is just a test|affected=Affects test cases only|impact=No real impact|solution=Keep writing tests|vuldetect=This is the detection method|solution_type=Mitigation', + refs: { + ref: [ + {_type: 'cve', _id: 'CVE-2019-1234'}, + {_type: 'bid', _id: '75750'}, + {_type: 'cert-bund', _id: 'CB-K12/3456'}, + {_type: 'dfn-cert', _id: 'DFN-CERT-2019-1234'}, + {_type: 'url', _id: 'www.foo.bar'}, + ], + }, + solution: { + _type: 'Mitigation', + __text: 'Keep writing tests', + }, + }, + description: 'This is a description', + threat: 'Medium', + severity: 5.0, + qod: {value: 80}, + task: {id: '314', name: 'task 1'}, + report: {id: '159'}, + tickets: { + ticket: [{id: '265'}], + }, + scan_nvt_version: '2019-02-14T07:33:50Z', + notes: { + note: [ + { + _id: '358', + text: 'TestNote', + modification_time: '2021-03-11T13:00:32Z', + active: 1, + }, + ], + }, + overrides: { + override: [ + { + _id: '979', + text: 'TestOverride', + modification_time: '2021-03-12T13:00:32Z', + severity: 5.0, + new_severity: 6.0, + active: 1, + }, + ], + }, +}); + +// mock gmp commands +let getResult; +let getPermissions; +let currentSettings; +let renewSession; + +beforeEach(() => { + getResult = jest.fn().mockResolvedValue({ + data: result, + }); + + getPermissions = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + + currentSettings = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + renewSession = jest.fn().mockResolvedValue({ + foo: 'bar', + }); +}); + +describe('Result Detailspage tests', () => { + test('should render full Detailspage', () => { + const gmp = { + result: { + get: getResult, + }, + permissions: { + get: getPermissions, + }, + settings: {manualUrl, reloadInterval}, + user: {currentSettings, renewSession}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('12345', result)); + + const {baseElement, element} = render(); + + // Toolbar Icons + const links = baseElement.querySelectorAll('a'); + + expect(screen.getAllByTitle('Help: Results')[0]).toBeInTheDocument(); + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/reports.html#displaying-all-existing-results', + ); + + expect(screen.getAllByTitle('Results List')[0]).toBeInTheDocument(); + expect(links[1]).toHaveAttribute('href', '/results'); + + expect(screen.getAllByTitle('Export Result as XML')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Add new Note')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Add new Override')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Create new Ticket')[0]).toBeInTheDocument(); + expect( + screen.getAllByTitle('Corresponding Task (task 1)')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Corresponding Report')[0]).toBeInTheDocument(); + expect( + screen.getAllByTitle('Corresponding Tickets')[0], + ).toBeInTheDocument(); + + // Header + expect(element).toHaveTextContent('Result: foo'); + expect(element).toHaveTextContent('ID:12345'); + expect(element).toHaveTextContent('Created:Sun, Jun 2, 2019 2:00 PM CEST'); + expect(element).toHaveTextContent('Modified:Mon, Jun 3, 2019 1:00 PM CEST'); + expect(element).toHaveTextContent('Owner:admin'); + + // Tabs + const spans = baseElement.querySelectorAll('span'); + expect(spans[12]).toHaveTextContent('User Tags'); + + // Details + const heading = baseElement.querySelectorAll('h2'); + + expect(heading[1]).toHaveTextContent('Vulnerability'); + expect(baseElement).toHaveTextContent('Namefoo'); + expect(baseElement).toHaveTextContent('Severity5.0 (Medium)'); + expect( + screen.getAllByTitle('There are overrides for this result')[0], + ).toBeInTheDocument(); + expect(baseElement).toHaveTextContent('QoD80 %'); + expect(baseElement).toHaveTextContent('Host109.876.54.321'); + expect(baseElement).toHaveTextContent('Location80/tcp'); + + expect(heading[2]).toHaveTextContent('Summary'); + expect(baseElement).toHaveTextContent('This is a mock result'); + + expect(heading[3]).toHaveTextContent('Detection Result'); + expect(baseElement).toHaveTextContent('This is a description'); + + expect(heading[4]).toHaveTextContent('Insight'); + expect(baseElement).toHaveTextContent('This is just a test'); + + expect(heading[5]).toHaveTextContent('Detection Method'); + expect(baseElement).toHaveTextContent('This is the detection method'); + expect(baseElement).toHaveTextContent( + 'Details: nvt1 OID: 1.3.6.1.4.1.25623.1.12345', + ); + expect(baseElement).toHaveTextContent('Version used: 2019-02-14T07:33:50Z'); + + expect(heading[6]).toHaveTextContent('Affected Software/OS'); + expect(baseElement).toHaveTextContent('Affects test cases only'); + + expect(heading[7]).toHaveTextContent('Impact'); + expect(baseElement).toHaveTextContent('No real impact'); + + expect(heading[8]).toHaveTextContent('Solution'); + expect(baseElement).toHaveTextContent( + 'Solution Type: st_mitigate.svgMitigation', + ); + expect(baseElement).toHaveTextContent('Keep writing tests'); + + expect(heading[9]).toHaveTextContent('References'); + expect( + screen.getByTitle('View Details of CVE-2019-1234'), + ).toHaveTextContent('CVE-2019-1234'); + expect(baseElement).toHaveTextContent('BID75750'); + expect( + screen.getByTitle('View details of DFN-CERT Advisory DFN-CERT-2019-1234'), + ).toHaveTextContent('DFN-CERT-2019-1234'); + + expect( + screen.getByTitle('View details of CERT-Bund Advisory CB-K12/3456'), + ).toHaveTextContent('CB-K12/3456'); + expect(baseElement).toHaveTextContent('Otherhttp://www.foo.bar'); + + expect(screen.getAllByTitle('Override Details')[0]).toBeInTheDocument(); + expect(baseElement).toHaveTextContent('TestOverride'); + expect(baseElement).toHaveTextContent('ModifiedFri, Mar 12, 2021 2:00 PM'); + + expect(screen.getAllByTitle('Note Details')[0]).toBeInTheDocument(); + expect(baseElement).toHaveTextContent('TestNote'); + expect(baseElement).toHaveTextContent('ModifiedThu, Mar 11, 2021 2:00 PM'); + }); + + test('should render user tags tab', () => { + const gmp = { + result: { + get: getResult, + }, + permissions: { + get: getPermissions, + }, + settings: {manualUrl, reloadInterval}, + user: {currentSettings, renewSession}, + }; + + const {render, store} = rendererWith({ + capabilities: true, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('12345', result)); + + const {baseElement} = render(); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[12]).toHaveTextContent('User Tags'); + fireEvent.click(spans[12]); + + expect(baseElement).toHaveTextContent('No user tags available'); + }); + + test('should call commands', async () => { + const exportFunc = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const getUsers = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + + const gmp = { + result: { + get: getResult, + export: exportFunc, + }, + permissions: { + get: getPermissions, + }, + users: { + get: getUsers, + }, + settings: {manualUrl, reloadInterval}, + user: {currentSettings, renewSession}, + }; + + const {render, store} = rendererWith({ + capabilities: true, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('12345', result)); + + render(); + + await wait(); + + // export result + + fireEvent.click(screen.getAllByTitle('Export Result as XML')[0]); + + await wait(); + + expect(exportFunc).toHaveBeenCalledWith(result); + + // load users for create ticket dialog + + fireEvent.click(screen.getAllByTitle('Create new Ticket')[0]); + + await wait(); + + expect(getUsers).toHaveBeenCalled(); + }); +}); + +describe('Result ToolBarIcons tests', () => { + test('should render', () => { + const handleNoteCreateClick = jest.fn(); + const handleOverrideCreateClick = jest.fn(); + const handleResultDownloadClick = jest.fn(); + const handleTicketCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: true, + router: true, + }); + + const {element} = render( + , + ); + + const links = element.querySelectorAll('a'); + const icons = screen.getAllByTestId('svg-icon'); + + expect(icons.length).toBe(9); + + expect(screen.getAllByTitle('Help: Results')[0]).toBeInTheDocument(); + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/reports.html#displaying-all-existing-results', + ); + + expect(screen.getAllByTitle('Results List')[0]).toBeInTheDocument(); + expect(links[1]).toHaveAttribute('href', '/results'); + + expect(screen.getAllByTitle('Export Result as XML')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Add new Note')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Add new Override')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Create new Ticket')[0]).toBeInTheDocument(); + expect( + screen.getAllByTitle('Corresponding Task (task 1)')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Corresponding Report')[0]).toBeInTheDocument(); + expect( + screen.getAllByTitle('Corresponding Tickets')[0], + ).toBeInTheDocument(); + }); + + test('should call click handlers', () => { + const handleNoteCreateClick = jest.fn(); + const handleOverrideCreateClick = jest.fn(); + const handleResultDownloadClick = jest.fn(); + const handleTicketCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: true, + router: true, + }); + + render( + , + ); + + fireEvent.click(screen.getAllByTitle('Export Result as XML')[0]); + expect(handleResultDownloadClick).toHaveBeenCalledWith(result); + + fireEvent.click(screen.getAllByTitle('Add new Note')[0]); + expect(handleNoteCreateClick).toHaveBeenCalledWith(result); + + fireEvent.click(screen.getAllByTitle('Add new Override')[0]); + expect(handleOverrideCreateClick).toHaveBeenCalledWith(result); + + fireEvent.click(screen.getAllByTitle('Create new Ticket')[0]); + expect(handleTicketCreateClick).toHaveBeenCalledWith(result); + }); + + test('should not show icons without permission', () => { + const wrongCapabilities = new Capabilities(['get_results']); + + const handleNoteCreateClick = jest.fn(); + const handleOverrideCreateClick = jest.fn(); + const handleResultDownloadClick = jest.fn(); + const handleTicketCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: wrongCapabilities, + router: true, + }); + + const {element} = render( + , + ); + + const links = element.querySelectorAll('a'); + const icons = screen.getAllByTestId('svg-icon'); + + expect(icons.length).toBe(3); + + expect(screen.getAllByTitle('Help: Results')[0]).toBeInTheDocument(); + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/reports.html#displaying-all-existing-results', + ); + + expect(screen.getAllByTitle('Results List')[0]).toBeInTheDocument(); + expect(links[1]).toHaveAttribute('href', '/results'); + + expect(screen.getAllByTitle('Export Result as XML')[0]).toBeInTheDocument(); + expect(screen.queryByTitle('Add new Note')).not.toBeInTheDocument(); + expect(screen.queryByTitle('Add new Override')).not.toBeInTheDocument(); + expect(screen.queryByTitle('Create new Ticket')).not.toBeInTheDocument(); + expect( + screen.queryByTitle('Corresponding Task (task 1)'), + ).not.toBeInTheDocument(); + expect(screen.queryByTitle('Corresponding Report')).not.toBeInTheDocument(); + expect( + screen.queryByTitle('Corresponding Tickets'), + ).not.toBeInTheDocument(); + }); +}); diff --git a/gsa/src/web/pages/results/__tests__/listpage.js b/gsa/src/web/pages/results/__tests__/listpage.js new file mode 100644 index 0000000000..a2b6678725 --- /dev/null +++ b/gsa/src/web/pages/results/__tests__/listpage.js @@ -0,0 +1,520 @@ +/* Copyright (C) 2021 Greenbone Networks GmbH + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import React from 'react'; + +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import {setLocale} from 'gmp/locale/lang'; + +import Filter from 'gmp/models/filter'; +import Result from 'gmp/models/result'; + +import {entitiesLoadingActions} from 'web/store/entities/results'; + +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; +import {loadingActions} from 'web/store/usersettings/defaults/actions'; +import {defaultFilterLoadingActions} from 'web/store/usersettings/defaultfilters/actions'; + +import {rendererWith, fireEvent, screen, wait} from 'web/utils/testing'; + +import ResultsPage from '../listpage'; + +// setup + +setLocale('en'); + +window.URL.createObjectURL = jest.fn(); + +const reloadInterval = -1; +const manualUrl = 'test/'; + +// mock entities +export const result1 = Result.fromElement({ + _id: '101', + name: 'Result 1', + owner: {name: 'admin'}, + comment: 'Comment 1', + creation_time: '2019-06-03T11:06:31Z', + modification_time: '2019-06-03T11:06:31Z', + host: {__text: '123.456.78.910', hostname: 'foo'}, + port: '80/tcp', + nvt: { + _oid: '201', + type: 'nvt', + name: 'nvt1', + tags: 'solution_type=Mitigation', + refs: {ref: [{_type: 'cve', _id: 'CVE-2019-1234'}]}, + }, + threat: 'High', + severity: 10.0, + qod: {value: 80}, +}); + +export const result2 = Result.fromElement({ + _id: '102', + name: 'Result 2', + owner: {name: 'admin'}, + comment: 'Comment 2', + creation_time: '2019-06-03T11:06:31Z', + modification_time: '2019-06-03T11:06:31Z', + host: {__text: '109.876.54.321'}, + port: '80/tcp', + nvt: { + _oid: '202', + type: 'nvt', + name: 'nvt2', + tags: 'solution_type=VendorFix', + refs: {ref: [{_type: 'cve', _id: 'CVE-2019-5678'}]}, + }, + threat: 'Medium', + severity: 5.0, + qod: {value: 70}, +}); + +export const result3 = Result.fromElement({ + _id: '103', + name: 'Result 3', + owner: {name: 'admin'}, + comment: 'Comment 3', + creation_time: '2019-06-03T11:06:31Z', + modification_time: '2019-06-03T11:06:31Z', + host: {__text: '109.876.54.321', hostname: 'bar'}, + port: '80/tcp', + nvt: { + _oid: '201', + type: 'nvt', + name: 'nvt1', + tags: 'solution_type=Mitigation', + refs: {ref: [{_type: 'cve', _id: 'CVE-2019-1234'}]}, + solution: { + _type: 'Mitigation', + }, + }, + threat: 'Medium', + severity: 5.0, + qod: {value: 80}, +}); + +const results = [result1, result2, result3]; + +let currentSettings; +let getAggregates; +let getDashboardSetting; +let getFilters; +let getResults; +let getSetting; +let renewSession; + +beforeEach(() => { + // mock gmp commands + + getResults = jest.fn().mockResolvedValue({ + data: results, + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + + getFilters = jest.fn().mockReturnValue( + Promise.resolve({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }), + ); + + getDashboardSetting = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + + getAggregates = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + + currentSettings = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + getSetting = jest.fn().mockResolvedValue({ + filter: null, + }); + + renewSession = jest.fn().mockResolvedValue({ + foo: 'bar', + }); +}); + +describe('Results listpage tests', () => { + test('should render full results listpage', async () => { + const gmp = { + results: { + get: getResults, + getSeverityAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + dashboard: { + getSetting: getDashboardSetting, + }, + settings: {manualUrl, reloadInterval}, + user: {currentSettings}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('result', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success(results, filter, loadedFilter, counts), + ); + + const {baseElement} = render(); + + await wait(); + + const inputs = baseElement.querySelectorAll('input'); + const selects = screen.getAllByTestId('select-selected-value'); + + // Toolbar Icons + expect(screen.getAllByTitle('Help: Results')[0]).toBeInTheDocument(); + + // Powerfilter + expect(inputs[0]).toHaveAttribute('name', 'userFilterString'); + expect(screen.getAllByTitle('Update Filter')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Remove Filter')[0]).toBeInTheDocument(); + expect( + screen.getAllByTitle('Reset to Default Filter')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Help: Powerfilter')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Edit Filter')[0]).toBeInTheDocument(); + expect(selects[0]).toHaveAttribute('title', 'Loaded filter'); + expect(selects[0]).toHaveTextContent('--'); + + // Dashboard + expect( + screen.getAllByTitle('Add new Dashboard Display')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Reset to Defaults')[0]).toBeInTheDocument(); + + const display = screen.getAllByTestId('grid-item'); + expect(display[0]).toHaveTextContent( + 'Results by Severity Class (Total: 0)', + ); + expect(display[1]).toHaveTextContent('Results by CVSS (Total: 0)'); + + // Headings + const header = baseElement.querySelectorAll('th'); + + expect(header[0]).toHaveTextContent('Vulnerability'); + expect(header[1]).toHaveTextContent('solution_type.svg'); + expect(header[2]).toHaveTextContent('Severity'); + expect(header[3]).toHaveTextContent('QoD'); + expect(header[4]).toHaveTextContent('Host'); + expect(header[5]).toHaveTextContent('Location'); + expect(header[6]).toHaveTextContent('Created'); + expect(header[7]).toHaveTextContent('IP'); + expect(header[8]).toHaveTextContent('Name'); + + // Row 1 + const row = baseElement.querySelectorAll('tr'); + + expect(row[2]).toHaveTextContent('Result 1'); + expect(row[2]).toHaveTextContent('10.0 (High)'); + expect(row[2]).toHaveTextContent('80 %'); + expect(row[2]).toHaveTextContent('123.456.78.910'); + expect(row[2]).toHaveTextContent('foo'); + expect(row[2]).toHaveTextContent('80/tcp'); + expect(row[2]).toHaveTextContent('Mon, Jun 3, 2019 1:06 PM CEST'); + + // Row 2 + expect(row[3]).toHaveTextContent('Result 2'); + expect(row[3]).toHaveTextContent('5.0 (Medium)'); + expect(row[3]).toHaveTextContent('70 %'); + expect(row[3]).toHaveTextContent('109.876.54.321'); + expect(row[3]).toHaveTextContent('80/tcp'); + expect(row[3]).toHaveTextContent('Mon, Jun 3, 2019 1:06 PM CEST'); + + // Row 3 + expect(row[4]).toHaveTextContent('Result 3'); + expect(row[4]).toHaveTextContent('st_mitigate.svg'); + expect(row[4]).toHaveTextContent('5.0 (Medium)'); + expect(row[4]).toHaveTextContent('80 %'); + expect(row[4]).toHaveTextContent('109.876.54.321'); + expect(row[4]).toHaveTextContent('bar'); + expect(row[4]).toHaveTextContent('80/tcp'); + expect(row[4]).toHaveTextContent('Mon, Jun 3, 2019 1:06 PM CEST'); + + // Footer + expect( + screen.getAllByTitle('Add tag to page contents')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Export page contents')[0]).toBeInTheDocument(); + }); + + test('should allow to bulk action on page contents', async () => { + const exportByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + results: { + get: getResults, + exportByFilter, + getSeverityAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + dashboard: { + getSetting: getDashboardSetting, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('result', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success(results, filter, loadedFilter, counts), + ); + + render(); + + await wait(); + + fireEvent.click(screen.getAllByTitle('Export page contents')[0]); + + await wait(); + + expect(exportByFilter).toHaveBeenCalled(); + }); + + test('should allow to bulk action on selected results', async () => { + const exportByIds = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + results: { + get: getResults, + export: exportByIds, + getSeverityAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + dashboard: { + getSetting: getDashboardSetting, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('result', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success(results, filter, loadedFilter, counts), + ); + + const {element} = render(); + + await wait(); + + // open drop down menu + const selectFields = screen.getAllByTestId('select-open-button'); + fireEvent.click(selectFields[1]); + + // select option "Apply to selection" + const selectItems = screen.getAllByTestId('select-item'); + fireEvent.click(selectItems[1]); + + const selected = screen.getAllByTestId('select-selected-value'); + expect(selected[1]).toHaveTextContent('Apply to selection'); + + // select a result + const inputs = element.querySelectorAll('input'); + + fireEvent.click(inputs[1]); + await wait(); + + // export selected result + fireEvent.click(screen.getAllByTitle('Export selection')[0]); + + await wait(); + + expect(exportByIds).toHaveBeenCalled(); + }); + + test('should allow to bulk action on filtered results', async () => { + const exportByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + results: { + get: getResults, + exportByFilter, + getSeverityAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + dashboard: { + getSetting: getDashboardSetting, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('result', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success(results, filter, loadedFilter, counts), + ); + + render(); + + await wait(); + + // open drop down menu + const selectFields = screen.getAllByTestId('select-open-button'); + fireEvent.click(selectFields[1]); + + // select option "Apply to all filtered" + const selectItems = screen.getAllByTestId('select-item'); + fireEvent.click(selectItems[2]); + + await wait(); + + const selected = screen.getAllByTestId('select-selected-value'); + expect(selected[1]).toHaveTextContent('Apply to all filtered'); + + // export all filtered results + fireEvent.click(screen.getAllByTitle('Export all filtered')[0]); + + await wait(); + + expect(exportByFilter).toHaveBeenCalled(); + }); +}); diff --git a/gsa/src/web/pages/results/detailspage.js b/gsa/src/web/pages/results/detailspage.js index 05f374d52c..984b0f8af6 100644 --- a/gsa/src/web/pages/results/detailspage.js +++ b/gsa/src/web/pages/results/detailspage.js @@ -81,7 +81,7 @@ import {getUsername} from 'web/store/usersettings/selectors'; import compose from 'web/utils/compose'; import {generateFilename} from 'web/utils/render'; import PropTypes from 'web/utils/proptypes'; -import withCapabilities from 'web/utils/withCapabilities'; +import useCapabilities from 'web/utils/useCapabilities'; import NoteComponent from '../notes/component'; @@ -91,79 +91,81 @@ import TicketComponent from '../tickets/component'; import ResultDetails from './details'; -let ToolBarIcons = ({ - capabilities, +export const ToolBarIcons = ({ entity, onNoteCreateClick, onOverrideCreateClick, onResultDownloadClick, onTicketCreateClick, -}) => ( - - - - - - - - {capabilities.mayCreate('note') && ( - - )} - {capabilities.mayCreate('override') && ( - { + const capabilities = useCapabilities(); + + return ( + + + - )} - {capabilities.mayCreate('ticket') && ( - + - )} - - - {capabilities.mayAccess('tasks') && isDefined(entity.task) && ( - - - - )} - {capabilities.mayAccess('reports') && isDefined(entity.report) && ( - - - - )} - {capabilities.mayAccess('tickets') && entity.tickets.length > 0 && ( - - - - - - )} - - -); + + + {capabilities.mayCreate('note') && ( + + )} + {capabilities.mayCreate('override') && ( + + )} + {capabilities.mayCreate('ticket') && ( + + )} + + + {capabilities.mayAccess('tasks') && isDefined(entity.task) && ( + + + + )} + {capabilities.mayAccess('reports') && isDefined(entity.report) && ( + + + + )} + {capabilities.mayAccess('tickets') && entity.tickets.length > 0 && ( + + + + + + )} + + + ); +}; ToolBarIcons.propTypes = { - capabilities: PropTypes.capabilities.isRequired, entity: PropTypes.model.isRequired, onNoteCreateClick: PropTypes.func.isRequired, onOverrideCreateClick: PropTypes.func.isRequired, @@ -171,8 +173,6 @@ ToolBarIcons.propTypes = { onTicketCreateClick: PropTypes.func.isRequired, }; -ToolBarIcons = withCapabilities(ToolBarIcons); - const active_filter = entity => entity.isActive(); const Details = ({entity, ...props}) => { diff --git a/gsa/src/web/pages/results/listpage.js b/gsa/src/web/pages/results/listpage.js index 55fbcc286e..b07e21fcc6 100644 --- a/gsa/src/web/pages/results/listpage.js +++ b/gsa/src/web/pages/results/listpage.js @@ -43,7 +43,7 @@ import ResultsFilterDialog from './filterdialog'; import ResultsTable from './table'; import ResultsDashboard, {RESULTS_DASHBOARD_ID} from './dashboard'; -const ToolBarIcons = () => ( +export const ToolBarIcons = () => ( . + */ +import React from 'react'; + +import Capabilities from 'gmp/capabilities/capabilities'; +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import {setLocale} from 'gmp/locale/lang'; + +import Filter from 'gmp/models/filter'; +import Schedule from 'gmp/models/schedule'; + +import {isDefined} from 'gmp/utils/identity'; + +import {entityLoadingActions} from 'web/store/entities/schedules'; +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; + +import {rendererWith, fireEvent, screen, wait} from 'web/utils/testing'; + +import Detailspage, {ToolBarIcons} from '../detailspage'; + +setLocale('en'); + +if (!isDefined(window.URL)) { + window.URL = {}; +} +window.URL.createObjectURL = jest.fn(); + +const caps = new Capabilities(['everything']); + +const reloadInterval = -1; +const manualUrl = 'test/'; + +const schedule = Schedule.fromElement({ + comment: 'hello world', + creation_time: '2020-12-23T14:14:11Z', + icalendar: + 'BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Greenbone.net//NONSGML Greenbone Security Manager \n 21.4.0~dev1~git-5f8b6cf-master//EN\nBEGIN:VEVENT\nDTSTART:20210104T115400Z\nDURATION:PT0S\nUID:foo\nDTSTAMP:20210111T134141Z\nEND:VEVENT\nEND:VCALENDAR', + in_use: 0, + modification_time: '2021-01-04T11:54:12Z', + name: 'schedule 1', + owner: {name: 'admin'}, + permissions: {permission: {name: 'Everything'}}, + timezone: 'UTC', + writable: 1, + _id: '12345', +}); + +const scheduleInUse = Schedule.fromElement({ + comment: 'hello world', + creation_time: '2020-12-23T14:14:11Z', + icalendar: + 'BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Greenbone.net//NONSGML Greenbone Security Manager \n 21.04+alpha~git-bb97c86-master//EN\nBEGIN:VEVENT\nDTSTART:20210104T115400Z\nDURATION:PT0S\nRRULE:FREQ=WEEKLY\nUID:3dfd6e6f-4e79-4f18-a5c2-adb3fca56bd3\nDTSTAMP:20210104T115412Z\nEND:VEVENT\nEND:VCALENDAR\n', + in_use: 1, + modification_time: '2021-01-04T11:54:12Z', + name: 'schedule 1', + owner: {name: 'admin'}, + permissions: {permission: {name: 'Everything'}}, + timezone: 'UTC', + writable: 1, + _id: '23456', +}); + +const noPermSchedule = Schedule.fromElement({ + comment: 'hello world', + creation_time: '2020-12-23T14:14:11Z', + icalendar: + 'BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Greenbone.net//NONSGML Greenbone Security Manager \n 21.04+alpha~git-bb97c86-master//EN\nBEGIN:VEVENT\nDTSTART:20210104T115400Z\nDURATION:PT0S\nRRULE:FREQ=WEEKLY\nUID:3dfd6e6f-4e79-4f18-a5c2-adb3fca56bd3\nDTSTAMP:20210104T115412Z\nEND:VEVENT\nEND:VCALENDAR\n', + in_use: 0, + modification_time: '2021-01-04T11:54:12Z', + name: 'schedule 1', + owner: {name: 'admin'}, + permissions: {permission: {name: 'get_schedules'}}, + timezone: 'UTC', + writable: 1, + _id: '23456', +}); + +const getSchedule = jest.fn().mockResolvedValue({ + data: schedule, +}); + +const getEntities = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const currentSettings = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +const renewSession = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +describe('Schedule Detailspage tests', () => { + test('should render full Detailspage', () => { + const gmp = { + schedule: { + get: getSchedule, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('12345', schedule)); + + const {baseElement, element} = render(); + + expect(element).toHaveTextContent('Schedule: schedule 1'); + + const links = baseElement.querySelectorAll('a'); + + expect(screen.getAllByTitle('Help: Schedules')[0]).toBeInTheDocument(); + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/scanning.html#managing-schedules', + ); + + expect(screen.getAllByTitle('Schedules List')[0]).toBeInTheDocument(); + expect(links[1]).toHaveAttribute('href', '/schedules'); + + expect(element).toHaveTextContent('ID:1234'); + expect(element).toHaveTextContent('Created:Wed, Dec 23, 2020 3:14 PM CET'); + expect(element).toHaveTextContent('Modified:Mon, Jan 4, 2021 12:54 PM CET'); + expect(element).toHaveTextContent('Owner:admin'); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[9]).toHaveTextContent('User Tags'); + expect(spans[11]).toHaveTextContent('Permissions'); + + expect(element).toHaveTextContent('Comment'); + expect(element).toHaveTextContent('hello world'); + + expect(element).toHaveTextContent('First Run'); + expect(element).toHaveTextContent('Mon, Jan 4, 2021 11:54 AM UTC'); + + expect(element).toHaveTextContent('Next Run'); + expect(element).toHaveTextContent('-'); + + expect(element).toHaveTextContent('Timezone'); + expect(element).toHaveTextContent('UTC'); + + expect(element).toHaveTextContent('Recurrence'); + expect(element).toHaveTextContent('Once'); + + expect(element).toHaveTextContent('Duration'); + expect(element).toHaveTextContent('Entire Operation'); + }); + + test('should render user tags tab', () => { + const gmp = { + schedule: { + get: getSchedule, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('12345', schedule)); + + const {baseElement} = render(); + + const spans = baseElement.querySelectorAll('span'); + + expect(spans[9]).toHaveTextContent('User Tags'); + fireEvent.click(spans[9]); + + expect(baseElement).toHaveTextContent('No user tags available'); + }); + + test('should render permissions tab', () => { + const gmp = { + schedule: { + get: getSchedule, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('12345', schedule)); + + const {element, baseElement} = render(); + + const spans = baseElement.querySelectorAll('span'); + fireEvent.click(spans[11]); + + expect(element).toHaveTextContent('No permissions available'); + }); + + test('should call commands', async () => { + const clone = jest.fn().mockResolvedValue({ + data: {id: 'foo'}, + }); + + const deleteFunc = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportFunc = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + schedule: { + get: getSchedule, + clone, + delete: deleteFunc, + export: exportFunc, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('12345', schedule)); + + render(); + + await wait(); + + const cloneIcon = screen.getAllByTitle('Clone Schedule'); + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + + await wait(); + + expect(clone).toHaveBeenCalledWith(schedule); + + const exportIcon = screen.getAllByTitle('Export Schedule as XML'); + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportFunc).toHaveBeenCalledWith(schedule); + + const deleteIcon = screen.getAllByTitle('Move Schedule to trashcan'); + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteFunc).toHaveBeenCalledWith({id: schedule.id}); + }); +}); + +describe('Schedule ToolBarIcons tests', () => { + test('should render', () => { + const handleScheduleCloneClick = jest.fn(); + const handleScheduleDeleteClick = jest.fn(); + const handleScheduleDownloadClick = jest.fn(); + const handleScheduleEditClick = jest.fn(); + const handleScheduleCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + const {element} = render( + , + ); + + const links = element.querySelectorAll('a'); + + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/scanning.html#managing-schedules', + ); + expect(screen.getAllByTitle('Help: Schedules')[0]).toBeInTheDocument(); + + expect(links[1]).toHaveAttribute('href', '/schedules'); + expect(screen.getAllByTitle('Schedules List')[0]).toBeInTheDocument(); + }); + + test('should call click handlers', () => { + const handleScheduleCloneClick = jest.fn(); + const handleScheduleDeleteClick = jest.fn(); + const handleScheduleDownloadClick = jest.fn(); + const handleScheduleEditClick = jest.fn(); + const handleScheduleCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + + const cloneIcon = screen.getAllByTitle('Clone Schedule'); + const editIcon = screen.getAllByTitle('Edit Schedule'); + const deleteIcon = screen.getAllByTitle('Move Schedule to trashcan'); + const exportIcon = screen.getAllByTitle('Export Schedule as XML'); + + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + expect(handleScheduleCloneClick).toHaveBeenCalledWith(schedule); + + expect(editIcon[0]).toBeInTheDocument(); + fireEvent.click(editIcon[0]); + expect(handleScheduleEditClick).toHaveBeenCalledWith(schedule); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + expect(handleScheduleDeleteClick).toHaveBeenCalledWith(schedule); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + expect(handleScheduleDownloadClick).toHaveBeenCalledWith(schedule); + }); + + test('should not call click handlers without permission', () => { + const handleScheduleCloneClick = jest.fn(); + const handleScheduleDeleteClick = jest.fn(); + const handleScheduleDownloadClick = jest.fn(); + const handleScheduleEditClick = jest.fn(); + const handleScheduleCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + + const cloneIcon = screen.getAllByTitle('Clone Schedule'); + const editIcon = screen.getAllByTitle('Permission to edit Schedule denied'); + const deleteIcon = screen.getAllByTitle( + 'Permission to move Schedule to trashcan denied', + ); + const exportIcon = screen.getAllByTitle('Export Schedule as XML'); + + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + + expect(handleScheduleCloneClick).toHaveBeenCalledWith(noPermSchedule); + + expect(editIcon[0]).toBeInTheDocument(); + fireEvent.click(editIcon[0]); + + expect(handleScheduleEditClick).not.toHaveBeenCalled(); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + expect(handleScheduleDeleteClick).not.toHaveBeenCalled(); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + expect(handleScheduleDownloadClick).toHaveBeenCalledWith(noPermSchedule); + }); + + test('should (not) call click handlers for schedule in use', () => { + const handleScheduleCloneClick = jest.fn(); + const handleScheduleDeleteClick = jest.fn(); + const handleScheduleDownloadClick = jest.fn(); + const handleScheduleEditClick = jest.fn(); + const handleScheduleCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + const cloneIcon = screen.getAllByTitle('Clone Schedule'); + const editIcon = screen.getAllByTitle('Edit Schedule'); + const deleteIcon = screen.getAllByTitle('Schedule is still in use'); + const exportIcon = screen.getAllByTitle('Export Schedule as XML'); + + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + + expect(handleScheduleCloneClick).toHaveBeenCalledWith(scheduleInUse); + + expect(editIcon[0]).toBeInTheDocument(); + fireEvent.click(editIcon[0]); + + expect(handleScheduleEditClick).toHaveBeenCalled(); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + expect(handleScheduleDeleteClick).not.toHaveBeenCalled(); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + expect(handleScheduleDownloadClick).toHaveBeenCalledWith(scheduleInUse); + }); +}); diff --git a/gsa/src/web/pages/schedules/__tests__/listpage.js b/gsa/src/web/pages/schedules/__tests__/listpage.js new file mode 100644 index 0000000000..c82cfb636a --- /dev/null +++ b/gsa/src/web/pages/schedules/__tests__/listpage.js @@ -0,0 +1,509 @@ +/* Copyright (C) 2021 Greenbone Networks GmbH + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import React from 'react'; + +import Capabilities from 'gmp/capabilities/capabilities'; +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import {setLocale} from 'gmp/locale/lang'; + +import Filter from 'gmp/models/filter'; +import Schedule from 'gmp/models/schedule'; + +import {entitiesLoadingActions} from 'web/store/entities/schedules'; + +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; +import {defaultFilterLoadingActions} from 'web/store/usersettings/defaultfilters/actions'; +import {loadingActions} from 'web/store/usersettings/defaults/actions'; + +import {rendererWith, fireEvent, screen, wait} from 'web/utils/testing'; + +import SchedulePage, {ToolBarIcons} from '../listpage'; + +setLocale('en'); + +window.URL.createObjectURL = jest.fn(); + +const schedule = Schedule.fromElement({ + comment: 'hello world', + creation_time: '2020-12-23T14:14:11Z', + icalendar: + 'BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Greenbone.net//NONSGML Greenbone Security Manager \n 21.4.0~dev1~git-5f8b6cf-master//EN\nBEGIN:VEVENT\nDTSTART:20210104T115400Z\nDURATION:PT0S\nUID:foo\nDTSTAMP:20210111T134141Z\nEND:VEVENT\nEND:VCALENDAR', + in_use: 0, + modification_time: '2021-01-04T11:54:12Z', + name: 'schedule 1', + owner: {name: 'admin'}, + permissions: {permission: {name: 'Everything'}}, + timezone: 'UTC', + writable: 1, + _id: '41fc25b4-fc21-4b81-ab30-35c95adc032a', +}); + +const caps = new Capabilities(['everything']); +const wrongCaps = new Capabilities(['get_config']); + +const reloadInterval = -1; +const manualUrl = 'test/'; + +const currentSettings = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +const getSetting = jest.fn().mockResolvedValue({ + filter: null, +}); + +const getFilters = jest.fn().mockReturnValue( + Promise.resolve({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }), +); + +const getSchedules = jest.fn().mockResolvedValue({ + data: [schedule], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const renewSession = jest.fn().mockResolvedValue({ + foo: 'bar', +}); + +describe('SchedulePage tests', () => { + test('should render full SchedulePage', async () => { + const gmp = { + schedules: { + get: getSchedules, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {currentSettings, getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('schedule', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([schedule], filter, loadedFilter, counts), + ); + + const {baseElement} = render(); + + await wait(); + + const inputs = baseElement.querySelectorAll('input'); + const selects = screen.getAllByTestId('select-selected-value'); + + // Toolbar Icons + expect(screen.getAllByTitle('Help: Schedules')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('New Schedule')[0]).toBeInTheDocument(); + + // Powerfilter + expect(inputs[0]).toHaveAttribute('name', 'userFilterString'); + expect(screen.getAllByTitle('Update Filter')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Remove Filter')[0]).toBeInTheDocument(); + expect( + screen.getAllByTitle('Reset to Default Filter')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Help: Powerfilter')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Edit Filter')[0]).toBeInTheDocument(); + expect(selects[0]).toHaveAttribute('title', 'Loaded filter'); + expect(selects[0]).toHaveTextContent('--'); + + // Table + const header = baseElement.querySelectorAll('th'); + + expect(header[0]).toHaveTextContent('Name'); + expect(header[1]).toHaveTextContent('First Run'); + expect(header[2]).toHaveTextContent('Next Run'); + expect(header[3]).toHaveTextContent('Recurrence'); + expect(header[4]).toHaveTextContent('Duration'); + expect(header[5]).toHaveTextContent('Actions'); + + const row = baseElement.querySelectorAll('tr'); + + expect(row[1]).toHaveTextContent('schedule 1'); + expect(row[1]).toHaveTextContent('(hello world)'); + expect(row[1]).toHaveTextContent('Mon, Jan 4, 2021 11:54 AM UTC'); + expect(row[1]).toHaveTextContent('-'); + expect(row[1]).toHaveTextContent('Entire Operation'); + + expect( + screen.getAllByTitle('Move Schedule to trashcan')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Edit Schedule')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Clone Schedule')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Export Schedule')[0]).toBeInTheDocument(); + }); + test('should allow to bulk action on page contents', async () => { + const deleteByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + schedules: { + get: getSchedules, + deleteByFilter, + exportByFilter, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('schedule', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([schedule], filter, loadedFilter, counts), + ); + + render(); + + await wait(); + + // export page contents + const exportIcon = screen.getAllByTitle('Export page contents'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByFilter).toHaveBeenCalled(); + + // move page contents to trashcan + const deleteIcon = screen.getAllByTitle('Move page contents to trashcan'); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteByFilter).toHaveBeenCalled(); + }); + + test('should allow to bulk action on selected schedules', async () => { + const deleteByIds = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByIds = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + schedules: { + get: getSchedules, + delete: deleteByIds, + export: exportByIds, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('schedule', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([schedule], filter, loadedFilter, counts), + ); + + const {element} = render(); + + await wait(); + + const selectFields = screen.getAllByTestId('select-open-button'); + fireEvent.click(selectFields[1]); + + const selectItems = screen.getAllByTestId('select-item'); + fireEvent.click(selectItems[1]); + + const selected = screen.getAllByTestId('select-selected-value'); + expect(selected[1]).toHaveTextContent('Apply to selection'); + + const inputs = element.querySelectorAll('input'); + + // select an schedule + fireEvent.click(inputs[1]); + await wait(); + + // export selected schedule + const exportIcon = screen.getAllByTitle('Export selection'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByIds).toHaveBeenCalled(); + + // move selected schedule to trashcan + const deleteIcon = screen.getAllByTitle('Move selection to trashcan'); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteByIds).toHaveBeenCalled(); + }); + + test('should allow to bulk action on filtered schedules', async () => { + const deleteByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + schedules: { + get: getSchedules, + deleteByFilter, + exportByFilter, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('schedule', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([schedule], filter, loadedFilter, counts), + ); + + render(); + + await wait(); + + const selectFields = screen.getAllByTestId('select-open-button'); + fireEvent.click(selectFields[1]); + + const selectItems = screen.getAllByTestId('select-item'); + fireEvent.click(selectItems[2]); + + await wait(); + + const selected = screen.getAllByTestId('select-selected-value'); + expect(selected[1]).toHaveTextContent('Apply to all filtered'); + + // export all filtered schedules + const exportIcon = screen.getAllByTitle('Export all filtered'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByFilter).toHaveBeenCalled(); + + // move all filtered schedules to trashcan + const deleteIcon = screen.getAllByTitle('Move all filtered to trashcan'); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteByFilter).toHaveBeenCalled(); + }); +}); + +describe('SchedulePage ToolBarIcons test', () => { + test('should render', () => { + const handleScheduleCreateClick = jest.fn(); + + const gmp = { + settings: {manualUrl}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + const {element} = render( + , + ); + + const links = element.querySelectorAll('a'); + + expect(screen.getAllByTitle('Help: Schedules')[0]).toBeInTheDocument(); + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/scanning.html#managing-schedules', + ); + }); + + test('should call click handlers', () => { + const handleScheduleCreateClick = jest.fn(); + + const gmp = { + settings: {manualUrl}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render(); + + const newIcon = screen.getAllByTitle('New Schedule'); + + expect(newIcon[0]).toBeInTheDocument(); + + fireEvent.click(newIcon[0]); + expect(handleScheduleCreateClick).toHaveBeenCalled(); + }); + + test('should not show icons if user does not have the right permissions', () => { + const handleScheduleCreateClick = jest.fn(); + + const gmp = { + settings: {manualUrl}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: wrongCaps, + router: true, + }); + + const {queryAllByTestId} = render( + , + ); + + const icons = queryAllByTestId('svg-icon'); // this test is probably approppriate to keep in the old format + expect(icons.length).toBe(1); + expect(icons[0]).toHaveAttribute('title', 'Help: Schedules'); + }); +}); diff --git a/gsa/src/web/pages/schedules/detailspage.js b/gsa/src/web/pages/schedules/detailspage.js index 95ff2f0fb9..e2b2be569a 100644 --- a/gsa/src/web/pages/schedules/detailspage.js +++ b/gsa/src/web/pages/schedules/detailspage.js @@ -62,7 +62,7 @@ import PropTypes from 'web/utils/proptypes'; import ScheduleComponent from './component'; import ScheduleDetails from './details'; -const ToolBarIcons = ({ +export const ToolBarIcons = ({ entity, onScheduleCloneClick, onScheduleCreateClick, diff --git a/gsa/src/web/pages/schedules/listpage.js b/gsa/src/web/pages/schedules/listpage.js index cd4acda45c..24f7e4a130 100644 --- a/gsa/src/web/pages/schedules/listpage.js +++ b/gsa/src/web/pages/schedules/listpage.js @@ -44,7 +44,7 @@ import { import ScheduleComponent from './component'; import SchedulesTable, {SORT_FIELDS} from './table'; -const ToolBarIcons = withCapabilities( +export const ToolBarIcons = withCapabilities( ({capabilities, onScheduleCreateClick}) => ( . + */ +import React from 'react'; + +import Capabilities from 'gmp/capabilities/capabilities'; +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import {setLocale} from 'gmp/locale/lang'; + +import Filter from 'gmp/models/filter'; +import Target from 'gmp/models/target'; + +import {isDefined} from 'gmp/utils/identity'; + +import {entityLoadingActions} from 'web/store/entities/targets'; +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; + +import {rendererWith, fireEvent, screen, wait} from 'web/utils/testing'; + +import Detailspage, {ToolBarIcons} from '../detailspage'; + +setLocale('en'); + +if (!isDefined(window.URL)) { + window.URL = {}; +} +window.URL.createObjectURL = jest.fn(); + +const caps = new Capabilities(['everything']); + +const reloadInterval = -1; +const manualUrl = 'test/'; + +let getTarget; +let getEntities; +let currentSettings; +let renewSession; + +beforeEach(() => { + getTarget = jest.fn().mockResolvedValue({ + data: target, + }); + + getEntities = jest.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + + currentSettings = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + renewSession = jest.fn().mockResolvedValue({ + foo: 'bar', + }); +}); + +const target = Target.fromElement({ + _id: '46264', + name: 'target 1', + creation_time: '2020-12-23T14:14:11Z', + modification_time: '2021-01-04T11:54:12Z', + in_use: 0, + permissions: {permission: {name: 'Everything'}}, + owner: {name: 'admin'}, + writable: 1, + port_list: { + _id: '32323', + name: 'All IANA assigned TCP', + trash: 0, + }, + hosts: '127.0.0.1, 123.456.574.64', + exclude_hosts: '192.168.0.1', + max_hosts: 2, + reverse_lookup_only: 1, + reverse_lookup_unify: 0, + tasks: {task: {_id: '465', name: 'foo'}}, + alive_tests: 'Scan Config Default', + allow_simultaneous_ips: 1, + port_range: '1-5', + ssh_credential: { + _id: '1235', + name: 'ssh', + port: '22', + trash: '0', + }, + ssh_elevate_credential: { + _id: '3456', + name: 'ssh_elevate', + trash: '0', + }, + smb_credential: { + _id: '4784', + name: 'smb_credential', + }, + esxi_credential: { + _id: '', + name: '', + trash: '0', + }, + snmp_credential: { + _id: '', + name: '', + trash: '0', + }, +}); + +const targetInUse = Target.fromElement({ + _id: '46264', + name: 'target 1', + creation_time: '2020-12-23T14:14:11Z', + modification_time: '2021-01-04T11:54:12Z', + in_use: 1, + permissions: {permission: {name: 'Everything'}}, + owner: {name: 'admin'}, + writable: 1, + port_list: { + _id: '32323', + name: 'All IANA assigned TCP', + trash: 0, + }, + hosts: '127.0.0.1, 123.456.574.64', + exclude_hosts: '192.168.0.1', + max_hosts: 2, + reverse_lookup_only: 1, + reverse_lookup_unify: 0, + tasks: {task: {_id: '465', name: 'foo'}}, + alive_tests: 'Scan Config Default', + allow_simultaneous_ips: 1, + port_range: '1-5', +}); + +const noPermTarget = Target.fromElement({ + _id: '46264', + name: 'target 1', + creation_time: '2020-12-23T14:14:11Z', + modification_time: '2021-01-04T11:54:12Z', + in_use: 0, + permissions: {permission: {name: 'get_targets'}}, + owner: {name: 'admin'}, + writable: 1, + port_list: { + _id: '32323', + name: 'All IANA assigned TCP', + trash: 0, + }, + hosts: '127.0.0.1, 123.456.574.64', + exclude_hosts: '192.168.0.1', + max_hosts: 2, + reverse_lookup_only: 1, + reverse_lookup_unify: 0, + tasks: {task: {_id: '465', name: 'foo'}}, + alive_tests: 'Scan Config Default', + allow_simultaneous_ips: 1, + port_range: '1-5', +}); + +describe('Target Detailspage tests', () => { + test('should render full Detailspage', () => { + const gmp = { + target: { + get: getTarget, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('46264', target)); + + const {baseElement, element} = render(); + + expect(element).toHaveTextContent('Target: target 1'); + + const links = baseElement.querySelectorAll('a'); + + expect(screen.getAllByTitle('Help: Targets')[0]).toBeInTheDocument(); + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/scanning.html#managing-targets', + ); + + expect(screen.getAllByTitle('Target List')[0]).toBeInTheDocument(); + expect(links[1]).toHaveAttribute('href', '/targets'); + + expect(element).toHaveTextContent('ID:46264'); + expect(element).toHaveTextContent('Created:Wed, Dec 23, 2020 3:14 PM CET'); + expect(element).toHaveTextContent('Modified:Mon, Jan 4, 2021 12:54 PM CET'); + expect(element).toHaveTextContent('Owner:admin'); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[9]).toHaveTextContent('User Tags'); + expect(spans[11]).toHaveTextContent('Permissions'); + + expect(element).toHaveTextContent('Included'); + expect(element).toHaveTextContent('127.0.0.1'); + expect(element).toHaveTextContent('123.456.574.64'); + + expect(element).toHaveTextContent('Excluded'); + expect(element).toHaveTextContent('192.168.0.1'); + + expect(element).toHaveTextContent('Maximum Number of Hosts'); + expect(element).toHaveTextContent('2'); + + expect(element).toHaveTextContent('Reverse Lookup Only'); + expect(element).toHaveTextContent('Yes'); + + expect(element).toHaveTextContent('Reverse Lookup Unify'); + expect(element).toHaveTextContent('No'); + + expect(element).toHaveTextContent('Alive Test'); + expect(element).toHaveTextContent('Scan Config Default'); + + expect(element).toHaveTextContent('Port List'); + expect(links[2]).toHaveAttribute('href', '/portlist/32323'); + expect(element).toHaveTextContent('All IANA assigned TCP'); + + expect(element).toHaveTextContent('Credentials'); + + expect(element).toHaveTextContent('SSH'); + expect(element).toHaveTextContent('ssh'); + expect(links[3]).toHaveAttribute('href', '/credential/1235'); + expect(element).toHaveTextContent('on Port 22'); + + expect(element).toHaveTextContent('SSH elevate credential'); + expect(element).toHaveTextContent('ssh_elevate'); + expect(links[4]).toHaveAttribute('href', '/credential/3456'); + + expect(element).toHaveTextContent('SMB'); + expect(element).toHaveTextContent('smb_credential'); + expect(links[5]).toHaveAttribute('href', '/credential/4784'); + + expect(element).toHaveTextContent('Tasks using this Target (1)'); + expect(links[6]).toHaveAttribute('href', '/task/465'); + expect(element).toHaveTextContent('foo'); + }); + + test('should render user tags tab', () => { + const gmp = { + target: { + get: getTarget, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('12345', target)); + + const {baseElement} = render(); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[9]).toHaveTextContent('User Tags'); + + fireEvent.click(spans[9]); + + expect(baseElement).toHaveTextContent('No user tags available'); + }); + + test('should render permissions tab', () => { + const gmp = { + target: { + get: getTarget, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('46264', target)); + + const {baseElement} = render(); + + const spans = baseElement.querySelectorAll('span'); + expect(spans[11]).toHaveTextContent('Permissions'); + + fireEvent.click(spans[11]); + + expect(baseElement).toHaveTextContent('No permissions available'); + }); + + test('should call commands', async () => { + const clone = jest.fn().mockResolvedValue({ + data: {id: 'foo'}, + }); + + const deleteFunc = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportFunc = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + target: { + get: getTarget, + clone, + delete: deleteFunc, + export: exportFunc, + }, + permissions: { + get: getEntities, + }, + settings: {manualUrl, reloadInterval}, + user: { + currentSettings, + renewSession, + }, + }; + + const {render, store} = rendererWith({ + capabilities: caps, + gmp, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + store.dispatch(entityLoadingActions.success('46264', target)); + + render(); + + await wait(); + + const cloneIcon = screen.getAllByTitle('Clone Target'); + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + + await wait(); + + expect(clone).toHaveBeenCalledWith(target); + + const exportIcon = screen.getAllByTitle('Export Target as XML'); + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportFunc).toHaveBeenCalledWith(target); + + const deleteIcon = screen.getAllByTitle('Move Target to trashcan'); + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteFunc).toHaveBeenCalledWith({id: target.id}); + }); +}); + +describe('Target ToolBarIcons tests', () => { + test('should render', () => { + const handleTargetCloneClick = jest.fn(); + const handleTargetDeleteClick = jest.fn(); + const handleTargetDownloadClick = jest.fn(); + const handleTargetEditClick = jest.fn(); + const handleTargetCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + const {element} = render( + , + ); + + const links = element.querySelectorAll('a'); + + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/scanning.html#managing-targets', + ); + expect(screen.getAllByTitle('Help: Targets')[0]).toBeInTheDocument(); + + expect(links[1]).toHaveAttribute('href', '/targets'); + expect(screen.getAllByTitle('Target List')[0]).toBeInTheDocument(); + }); + + test('should call click handlers', () => { + const handleTargetCloneClick = jest.fn(); + const handleTargetDeleteClick = jest.fn(); + const handleTargetDownloadClick = jest.fn(); + const handleTargetEditClick = jest.fn(); + const handleTargetCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + + const cloneIcon = screen.getAllByTitle('Clone Target'); + const editIcon = screen.getAllByTitle('Edit Target'); + const deleteIcon = screen.getAllByTitle('Move Target to trashcan'); + const exportIcon = screen.getAllByTitle('Export Target as XML'); + + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + expect(handleTargetCloneClick).toHaveBeenCalledWith(target); + + expect(editIcon[0]).toBeInTheDocument(); + fireEvent.click(editIcon[0]); + expect(handleTargetEditClick).toHaveBeenCalledWith(target); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + expect(handleTargetDeleteClick).toHaveBeenCalledWith(target); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + expect(handleTargetDownloadClick).toHaveBeenCalledWith(target); + }); + + test('should not call click handlers without permission', () => { + const handleTargetCloneClick = jest.fn(); + const handleTargetDeleteClick = jest.fn(); + const handleTargetDownloadClick = jest.fn(); + const handleTargetEditClick = jest.fn(); + const handleTargetCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + + const cloneIcon = screen.getAllByTitle('Clone Target'); + const editIcon = screen.getAllByTitle('Permission to edit Target denied'); + const deleteIcon = screen.getAllByTitle( + 'Permission to move Target to trashcan denied', + ); + const exportIcon = screen.getAllByTitle('Export Target as XML'); + + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + + expect(handleTargetCloneClick).toHaveBeenCalledWith(noPermTarget); + + expect(editIcon[0]).toBeInTheDocument(); + fireEvent.click(editIcon[0]); + + expect(handleTargetEditClick).not.toHaveBeenCalled(); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + expect(handleTargetDeleteClick).not.toHaveBeenCalled(); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + expect(handleTargetDownloadClick).toHaveBeenCalledWith(noPermTarget); + }); + + test('should (not) call click handlers for target in use', () => { + const handleTargetCloneClick = jest.fn(); + const handleTargetDeleteClick = jest.fn(); + const handleTargetDownloadClick = jest.fn(); + const handleTargetEditClick = jest.fn(); + const handleTargetCreateClick = jest.fn(); + + const gmp = {settings: {manualUrl}}; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render( + , + ); + const cloneIcon = screen.getAllByTitle('Clone Target'); + const editIcon = screen.getAllByTitle('Edit Target'); + const deleteIcon = screen.getAllByTitle('Target is still in use'); + const exportIcon = screen.getAllByTitle('Export Target as XML'); + + expect(cloneIcon[0]).toBeInTheDocument(); + fireEvent.click(cloneIcon[0]); + + expect(handleTargetCloneClick).toHaveBeenCalledWith(targetInUse); + + expect(editIcon[0]).toBeInTheDocument(); + fireEvent.click(editIcon[0]); + + expect(handleTargetEditClick).toHaveBeenCalled(); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + expect(handleTargetDeleteClick).not.toHaveBeenCalled(); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + expect(handleTargetDownloadClick).toHaveBeenCalledWith(targetInUse); + }); +}); diff --git a/gsa/src/web/pages/targets/__tests__/listpage.js b/gsa/src/web/pages/targets/__tests__/listpage.js new file mode 100644 index 0000000000..829bdff829 --- /dev/null +++ b/gsa/src/web/pages/targets/__tests__/listpage.js @@ -0,0 +1,542 @@ +/* Copyright (C) 2021 Greenbone Networks GmbH + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import React from 'react'; + +import Capabilities from 'gmp/capabilities/capabilities'; +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import {setLocale} from 'gmp/locale/lang'; + +import Filter from 'gmp/models/filter'; +import Target from 'gmp/models/target'; + +import {entitiesLoadingActions} from 'web/store/entities/targets'; + +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; +import {defaultFilterLoadingActions} from 'web/store/usersettings/defaultfilters/actions'; +import {loadingActions} from 'web/store/usersettings/defaults/actions'; + +import {rendererWith, fireEvent, screen, wait} from 'web/utils/testing'; + +import TargetPage, {ToolBarIcons} from '../listpage'; + +setLocale('en'); + +window.URL.createObjectURL = jest.fn(); + +let currentSettings; +let getSetting; +let getFilters; +let getTargets; +let renewSession; + +beforeEach(() => { + currentSettings = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + getSetting = jest.fn().mockResolvedValue({ + filter: null, + }); + + getFilters = jest.fn().mockReturnValue( + Promise.resolve({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }), + ); + + getTargets = jest.fn().mockResolvedValue({ + data: [target], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + + renewSession = jest.fn().mockResolvedValue({ + foo: 'bar', + }); +}); + +const target = Target.fromElement({ + _id: '46264', + name: 'target 1', + commen: 'hello world', + creation_time: '2020-12-23T14:14:11Z', + modification_time: '2021-01-04T11:54:12Z', + in_use: 0, + permissions: {permission: {name: 'Everything'}}, + owner: {name: 'admin'}, + writable: 1, + port_list: { + _id: '32323', + name: 'All IANA assigned TCP', + trash: 0, + }, + hosts: '127.0.0.1, 123.456.574.64', + exclude_hosts: '192.168.0.1', + max_hosts: 2, + reverse_lookup_only: 1, + reverse_lookup_unify: 0, + tasks: {task: {_id: '465', name: 'foo'}}, + alive_tests: 'Scan Config Default', + allow_simultaneous_ips: 1, + port_range: '1-5', + ssh_credential: { + _id: '1235', + name: 'ssh', + port: '22', + trash: '0', + }, + ssh_elevate_credential: { + _id: '3456', + name: 'ssh_elevate', + trash: '0', + }, +}); + +const caps = new Capabilities(['everything']); +const wrongCaps = new Capabilities(['get_config']); + +const reloadInterval = -1; +const manualUrl = 'test/'; + +describe('TargetPage tests', () => { + test('should render full TargetPage', async () => { + const gmp = { + targets: { + get: getTargets, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {currentSettings, getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('target', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([target], filter, loadedFilter, counts), + ); + + const {baseElement} = render(); + + await wait(); + + const inputs = baseElement.querySelectorAll('input'); + const selects = screen.getAllByTestId('select-selected-value'); + + // Toolbar Icons + expect(screen.getAllByTitle('Help: Targets')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('New Target')[0]).toBeInTheDocument(); + + // Powerfilter + expect(inputs[0]).toHaveAttribute('name', 'userFilterString'); + expect(screen.getAllByTitle('Update Filter')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Remove Filter')[0]).toBeInTheDocument(); + expect( + screen.getAllByTitle('Reset to Default Filter')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Help: Powerfilter')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Edit Filter')[0]).toBeInTheDocument(); + expect(selects[0]).toHaveAttribute('title', 'Loaded filter'); + expect(selects[0]).toHaveTextContent('--'); + + // Table + const header = baseElement.querySelectorAll('th'); + + expect(header[0]).toHaveTextContent('Name'); + expect(header[1]).toHaveTextContent('Hosts'); + expect(header[2]).toHaveTextContent('IPs'); + expect(header[4]).toHaveTextContent('Credentials'); + expect(header[5]).toHaveTextContent('Actions'); + + const row = baseElement.querySelectorAll('tr'); + + expect(row[1]).toHaveTextContent('target 1'); + expect(row[1]).toHaveTextContent('127.0.0.1, 123.456.574.642'); + expect(row[1]).toHaveTextContent('2'); + expect(row[1]).toHaveTextContent('All IANA assigned TCP'); + + expect(row[1]).toHaveTextContent('SSH: ssh'); + expect(row[1]).toHaveTextContent('SSH Elevate: ssh_elevate'); + + expect( + screen.getAllByTitle('Move Target to trashcan')[0], + ).toBeInTheDocument(); + expect(screen.getAllByTitle('Edit Target')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Clone Target')[0]).toBeInTheDocument(); + expect(screen.getAllByTitle('Export Target')[0]).toBeInTheDocument(); + }); + test('should allow to bulk action on page contents', async () => { + const deleteByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + targets: { + get: getTargets, + deleteByFilter, + exportByFilter, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('target', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([target], filter, loadedFilter, counts), + ); + + render(); + + await wait(); + + // export page contents + const exportIcon = screen.getAllByTitle('Export page contents'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByFilter).toHaveBeenCalled(); + + // move page contents to trashcan + const deleteIcon = screen.getAllByTitle('Move page contents to trashcan'); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteByFilter).toHaveBeenCalled(); + }); + + test('should allow to bulk action on selected targets', async () => { + // mock cache issues will cause these tests to randomly fail. Will fix later. + const deleteByIds = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByIds = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + targets: { + get: getTargets, + delete: deleteByIds, + export: exportByIds, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('target', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([target], filter, loadedFilter, counts), + ); + + const {element} = render(); + + await wait(); + + const selectFields = screen.getAllByTestId('select-open-button'); + fireEvent.click(selectFields[1]); + + const selectItems = screen.getAllByTestId('select-item'); + fireEvent.click(selectItems[1]); + + const selected = screen.getAllByTestId('select-selected-value'); + expect(selected[1]).toHaveTextContent('Apply to selection'); + + const inputs = element.querySelectorAll('input'); + + // select an target + fireEvent.click(inputs[1]); + await wait(); + + // export selected target + const exportIcon = screen.getAllByTitle('Export selection'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByIds).toHaveBeenCalled(); + + // move selected target to trashcan + const deleteIcon = screen.getAllByTitle('Move selection to trashcan'); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteByIds).toHaveBeenCalled(); + }); + + test('should allow to bulk action on filtered targets', async () => { + // mock cache issues will cause these tests to randomly fail. Will fix later. + const deleteByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByFilter = jest.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + targets: { + get: getTargets, + deleteByFilter, + exportByFilter, + }, + filters: { + get: getFilters, + }, + settings: {manualUrl, reloadInterval}, + user: {renewSession, currentSettings, getSetting: getSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('target', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success([target], filter, loadedFilter, counts), + ); + + render(); + + await wait(); + + const selectFields = screen.getAllByTestId('select-open-button'); + fireEvent.click(selectFields[1]); + + const selectItems = screen.getAllByTestId('select-item'); + fireEvent.click(selectItems[2]); + + await wait(); + + const selected = screen.getAllByTestId('select-selected-value'); + expect(selected[1]).toHaveTextContent('Apply to all filtered'); + + // export all filtered targets + const exportIcon = screen.getAllByTitle('Export all filtered'); + + expect(exportIcon[0]).toBeInTheDocument(); + fireEvent.click(exportIcon[0]); + + await wait(); + + expect(exportByFilter).toHaveBeenCalled(); + + // move all filtered targets to trashcan + const deleteIcon = screen.getAllByTitle('Move all filtered to trashcan'); + + expect(deleteIcon[0]).toBeInTheDocument(); + fireEvent.click(deleteIcon[0]); + + await wait(); + + expect(deleteByFilter).toHaveBeenCalled(); + }); +}); + +describe('TargetPage ToolBarIcons test', () => { + test('should render', () => { + const handleTargetCreateClick = jest.fn(); + + const gmp = { + settings: {manualUrl}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + const {element} = render( + , + ); + + const links = element.querySelectorAll('a'); + + expect(screen.getAllByTitle('Help: Targets')[0]).toBeInTheDocument(); + expect(links[0]).toHaveAttribute( + 'href', + 'test/en/scanning.html#managing-targets', + ); + }); + + test('should call click handlers', () => { + const handleTargetCreateClick = jest.fn(); + + const gmp = { + settings: {manualUrl}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + router: true, + }); + + render(); + + const newIcon = screen.getAllByTitle('New Target'); + + expect(newIcon[0]).toBeInTheDocument(); + + fireEvent.click(newIcon[0]); + expect(handleTargetCreateClick).toHaveBeenCalled(); + }); + + test('should not show icons if user does not have the right permissions', () => { + const handleTargetCreateClick = jest.fn(); + + const gmp = { + settings: {manualUrl}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: wrongCaps, + router: true, + }); + + const {queryAllByTestId} = render( + , + ); + + const icons = queryAllByTestId('svg-icon'); // this test is probably approppriate to keep in the old format + expect(icons.length).toBe(1); + expect(icons[0]).toHaveAttribute('title', 'Help: Targets'); + }); +}); diff --git a/gsa/src/web/pages/targets/detailspage.js b/gsa/src/web/pages/targets/detailspage.js index 2ad7b78af3..05eadbc009 100644 --- a/gsa/src/web/pages/targets/detailspage.js +++ b/gsa/src/web/pages/targets/detailspage.js @@ -73,7 +73,7 @@ import withComponentDefaults from 'web/utils/withComponentDefaults'; import TargetDetails from './details'; import TargetComponent from './component'; -const ToolBarIcons = ({ +export const ToolBarIcons = ({ entity, onTargetCloneClick, onTargetCreateClick, diff --git a/gsa/src/web/pages/targets/listpage.js b/gsa/src/web/pages/targets/listpage.js index 3c00e236cb..2d61afce08 100644 --- a/gsa/src/web/pages/targets/listpage.js +++ b/gsa/src/web/pages/targets/listpage.js @@ -43,18 +43,20 @@ import TargetsFilterDialog from './filterdialog'; import TargetsTable from './table'; import TargetComponent from './component'; -const ToolBarIcons = withCapabilities(({capabilities, onTargetCreateClick}) => ( - - - {capabilities.mayCreate('target') && ( - - )} - -)); +export const ToolBarIcons = withCapabilities( + ({capabilities, onTargetCreateClick}) => ( + + + {capabilities.mayCreate('target') && ( + + )} + + ), +); ToolBarIcons.propTypes = { onTargetCreateClick: PropTypes.func.isRequired,