diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/actions.js b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/actions.js new file mode 100644 index 0000000000000000000000000000000000000000..f016f1a1a9ae1eea8b8d234e205e197c897a78ed --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/actions.js @@ -0,0 +1,173 @@ +import Api from '~/api'; +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import { __, s__, sprintf } from '~/locale'; +import * as types from './mutation_types'; + +const API_MINIMUM_QUERY_LENGTH = 3; + +export const toggleSelectedProject = ({ commit, state }, project) => { + const isProject = ({ id }) => id === project.id; + + if (state.selectedProjects.some(isProject)) { + commit(types.DESELECT_PROJECT, project); + } else { + commit(types.SELECT_PROJECT, project); + } +}; + +export const clearSearchResults = ({ commit }) => { + commit(types.CLEAR_SEARCH_RESULTS); +}; + +export const setSearchQuery = ({ commit }, query) => { + commit(types.SET_SEARCH_QUERY, query); +}; + +export const setProjectEndpoints = ({ commit }, endpoints) => { + commit(types.SET_PROJECT_ENDPOINTS, endpoints); +}; + +export const addProjects = ({ state, dispatch }) => { + dispatch('requestAddProjects'); + + return axios + .post(state.projectEndpoints.add, { + project_ids: state.selectedProjects.map(p => p.id), + }) + .then(response => dispatch('receiveAddProjectsSuccess', response.data)) + .catch(() => dispatch('receiveAddProjectsError')); +}; + +export const requestAddProjects = ({ commit }) => { + commit(types.REQUEST_ADD_PROJECTS); +}; + +export const receiveAddProjectsSuccess = ({ commit, dispatch, state }, data) => { + const { added, invalid } = data; + + commit(types.RECEIVE_ADD_PROJECTS_SUCCESS); + + if (invalid.length) { + const [firstProject, secondProject, ...rest] = state.selectedProjects + .filter(project => invalid.includes(project.id)) + .map(project => project.name); + const translationValues = { + firstProject, + secondProject, + rest: rest.join(', '), + }; + let invalidProjects; + if (rest.length > 0) { + invalidProjects = sprintf( + s__('SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}'), + translationValues, + ); + } else if (secondProject) { + invalidProjects = sprintf( + s__('SecurityDashboard|%{firstProject} and %{secondProject}'), + translationValues, + ); + } else { + invalidProjects = firstProject; + } + createFlash( + sprintf(s__('SecurityDashboard|Unable to add %{invalidProjects}'), { + invalidProjects, + }), + ); + } + + if (added.length) { + dispatch('fetchProjects'); + } +}; + +export const receiveAddProjectsError = ({ commit }) => { + commit(types.RECEIVE_ADD_PROJECTS_ERROR); + + createFlash(__('Something went wrong, unable to add projects to dashboard')); +}; + +export const fetchProjects = ({ state, dispatch }) => { + dispatch('requestProjects'); + + return axios + .get(state.projectEndpoints.list) + .then(({ data }) => { + dispatch('receiveProjectsSuccess', data); + }) + .catch(() => dispatch('receiveProjectsError')); +}; + +export const requestProjects = ({ commit }) => { + commit(types.REQUEST_PROJECTS); +}; + +export const receiveProjectsSuccess = ({ commit }, { projects }) => { + commit(types.RECEIVE_PROJECTS_SUCCESS, projects); +}; + +export const receiveProjectsError = ({ commit }) => { + commit(types.RECEIVE_PROJECTS_ERROR); + + createFlash(__('Something went wrong, unable to get projects')); +}; + +export const removeProject = ({ dispatch }, removePath) => { + dispatch('requestRemoveProject'); + + return axios + .delete(removePath) + .then(() => { + dispatch('receiveRemoveProjectSuccess'); + }) + .catch(() => dispatch('receiveRemoveProjectError')); +}; + +export const requestRemoveProject = ({ commit }) => { + commit(types.REQUEST_REMOVE_PROJECT); +}; + +export const receiveRemoveProjectSuccess = ({ commit, dispatch }) => { + commit(types.RECEIVE_REMOVE_PROJECT_SUCCESS); + dispatch('fetchProjects'); +}; + +export const receiveRemoveProjectError = ({ commit }) => { + commit(types.RECEIVE_REMOVE_PROJECT_ERROR); + + createFlash(__('Something went wrong, unable to remove project')); +}; + +export const fetchSearchResults = ({ state, dispatch }) => { + const { searchQuery } = state; + dispatch('requestSearchResults'); + + if (!searchQuery || searchQuery.length < API_MINIMUM_QUERY_LENGTH) { + return dispatch('setMinimumQueryMessage'); + } + + return Api.projects(searchQuery, {}) + .then(results => dispatch('receiveSearchResultsSuccess', results)) + .catch(() => dispatch('receiveSearchResultsError')); +}; + +export const requestSearchResults = ({ commit }) => { + commit(types.REQUEST_SEARCH_RESULTS); +}; + +export const receiveSearchResultsSuccess = ({ commit }, results) => { + commit(types.RECEIVE_SEARCH_RESULTS_SUCCESS, results); +}; + +export const receiveSearchResultsError = ({ commit }) => { + commit(types.RECEIVE_SEARCH_RESULTS_ERROR); +}; + +export const setMinimumQueryMessage = ({ commit }) => { + commit(types.SET_MINIMUM_QUERY_MESSAGE); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/index.js b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/index.js new file mode 100644 index 0000000000000000000000000000000000000000..636092ce1a9aab25cda419c7e2c9bb1d6145029e --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/index.js @@ -0,0 +1,10 @@ +import state from './state'; +import mutations from './mutations'; +import * as actions from './actions'; + +export default () => ({ + namespaced: true, + state, + mutations, + actions, +}); diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/mutation_types.js b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/mutation_types.js new file mode 100644 index 0000000000000000000000000000000000000000..52c7bdc4a3e3c26b8304712d2b752fe3038ebd6e --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/mutation_types.js @@ -0,0 +1,26 @@ +export const SET_PROJECT_ENDPOINTS = 'SET_PROJECT_ENDPOINTS'; + +export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY'; + +export const SELECT_PROJECT = 'SELECT_PROJECT'; +export const DESELECT_PROJECT = 'DESELECT_PROJECT'; + +export const REQUEST_ADD_PROJECTS = 'REQUEST_ADD_PROJECTS'; +export const RECEIVE_ADD_PROJECTS_SUCCESS = 'RECEIVE_ADD_PROJECTS_SUCCESS'; +export const RECEIVE_ADD_PROJECTS_ERROR = 'RECEIVE_ADD_PROJECTS_ERROR'; + +export const REQUEST_REMOVE_PROJECT = 'REQUEST_REMOVE_PROJECT'; +export const RECEIVE_REMOVE_PROJECT_SUCCESS = 'RECEIVE_REMOVE_PROJECT_SUCCESS'; +export const RECEIVE_REMOVE_PROJECT_ERROR = 'RECEIVE_REMOVE_PROJECT_ERROR'; + +export const REQUEST_PROJECTS = 'REQUEST_PROJECTS'; +export const RECEIVE_PROJECTS_SUCCESS = 'RECEIVE_PROJECTS_SUCCESS'; +export const RECEIVE_PROJECTS_ERROR = 'RECEIVE_PROJECTS_ERROR'; + +export const CLEAR_SEARCH_RESULTS = 'CLEAR_SEARCH_RESULTS'; + +export const REQUEST_SEARCH_RESULTS = 'REQUEST_SEARCH_RESULTS'; +export const RECEIVE_SEARCH_RESULTS_SUCCESS = 'RECEIVE_SEARCH_RESULTS_SUCCESS'; +export const RECEIVE_SEARCH_RESULTS_ERROR = 'RECEIVE_SEARCH_RESULTS_ERROR'; + +export const SET_MINIMUM_QUERY_MESSAGE = 'SET_MINIMUM_QUERY_MESSAGE'; diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/mutations.js b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/mutations.js new file mode 100644 index 0000000000000000000000000000000000000000..0a58738761173ca24ba3b7f3cf6f6721bb1d1bb9 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/mutations.js @@ -0,0 +1,83 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_PROJECT_ENDPOINTS](state, endpoints) { + state.projectEndpoints.add = endpoints.add; + state.projectEndpoints.list = endpoints.list; + }, + [types.SET_SEARCH_QUERY](state, query) { + state.searchQuery = query; + }, + [types.SELECT_PROJECT](state, project) { + if (!state.selectedProjects.some(p => p.id === project.id)) { + state.selectedProjects.push(project); + } + }, + [types.DESELECT_PROJECT](state, project) { + state.selectedProjects = state.selectedProjects.filter(p => p.id !== project.id); + }, + [types.REQUEST_ADD_PROJECTS](state) { + state.isAddingProjects = true; + }, + [types.RECEIVE_ADD_PROJECTS_SUCCESS](state) { + state.isAddingProjects = false; + }, + [types.RECEIVE_ADD_PROJECTS_ERROR](state) { + state.isAddingProjects = false; + }, + [types.REQUEST_REMOVE_PROJECT](state) { + state.isRemovingProject = true; + }, + [types.RECEIVE_REMOVE_PROJECT_SUCCESS](state) { + state.isRemovingProject = false; + }, + [types.RECEIVE_REMOVE_PROJECT_ERROR](state) { + state.isRemovingProject = false; + }, + [types.REQUEST_PROJECTS](state) { + state.isLoadingProjects = true; + }, + [types.RECEIVE_PROJECTS_SUCCESS](state, projects) { + state.projects = projects; + state.isLoadingProjects = false; + }, + [types.RECEIVE_PROJECTS_ERROR](state) { + state.projects = []; + state.isLoadingProjects = false; + }, + [types.CLEAR_SEARCH_RESULTS](state) { + state.projectSearchResults = []; + state.selectedProjects = []; + }, + [types.REQUEST_SEARCH_RESULTS](state) { + state.messages.minimumQuery = false; + state.searchCount += 1; + }, + [types.RECEIVE_SEARCH_RESULTS_SUCCESS](state, results) { + state.projectSearchResults = results; + + state.messages.noResults = state.projectSearchResults.length === 0; + state.messages.searchError = false; + state.messages.minimumQuery = false; + + state.searchCount = Math.max(0, state.searchCount - 1); + }, + [types.RECEIVE_SEARCH_RESULTS_ERROR](state) { + state.projectSearchResults = []; + + state.messages.noResults = false; + state.messages.searchError = true; + state.messages.minimumQuery = false; + + state.searchCount = Math.max(0, state.searchCount - 1); + }, + [types.SET_MINIMUM_QUERY_MESSAGE](state) { + state.projectSearchResults = []; + + state.messages.noResults = false; + state.messages.searchError = false; + state.messages.minimumQuery = true; + + state.searchCount = Math.max(0, state.searchCount - 1); + }, +}; diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/state.js b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/state.js new file mode 100644 index 0000000000000000000000000000000000000000..1247b322a7e2259cfc80bb613e9f7bb95dbb19b9 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/store/modules/project_selector/state.js @@ -0,0 +1,20 @@ +export default () => ({ + inputValue: '', + isLoadingProjects: false, + isAddingProjects: false, + isRemovingProject: false, + projectEndpoints: { + list: null, + add: null, + }, + searchQuery: '', + projects: [], + projectSearchResults: [], + selectedProjects: [], + messages: { + noResults: false, + searchError: false, + minimumQuery: false, + }, + searchCount: 0, +}); diff --git a/ee/spec/frontend/security_dashboard/store/modules/project_selector/actions_spec.js b/ee/spec/frontend/security_dashboard/store/modules/project_selector/actions_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..56b1086200c83f4d1b628ac7fcc0af107e7fb918 --- /dev/null +++ b/ee/spec/frontend/security_dashboard/store/modules/project_selector/actions_spec.js @@ -0,0 +1,557 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import testAction from 'helpers/vuex_action_helper'; + +import Api from '~/api'; +import createFlash from '~/flash'; + +import createState from 'ee/security_dashboard/store/modules/project_selector/state'; +import * as types from 'ee/security_dashboard/store/modules/project_selector/mutation_types'; +import * as actions from 'ee/security_dashboard/store/modules/project_selector/actions'; + +jest.mock('~/api'); +jest.mock('~/flash'); + +describe('projectSelector actions', () => { + const getMockProjects = n => [...Array(n).keys()].map(i => ({ id: i, name: `project-${i}` })); + + const mockAddEndpoint = 'mock-add_endpoint'; + const mockListEndpoint = 'mock-list_endpoint'; + const mockResponse = { data: 'mock-data' }; + + let mockAxios; + let mockDispatchContext; + let state; + + beforeEach(() => { + mockAxios = new MockAdapter(axios); + mockDispatchContext = { dispatch: () => {}, commit: () => {}, state }; + state = createState(); + }); + + afterEach(() => { + jest.clearAllMocks(); + mockAxios.restore(); + }); + + describe('toggleSelectedProject', () => { + it('adds a project to selectedProjects if it does not already exist in the list', done => { + const payload = getMockProjects(1); + + testAction( + actions.toggleSelectedProject, + payload, + state, + [ + { + type: types.SELECT_PROJECT, + payload, + }, + ], + [], + done, + ); + }); + + it('removes a project from selectedProjects if it already exist in the list', () => { + const payload = getMockProjects(1)[0]; + state.selectedProjects = getMockProjects(1); + + return testAction( + actions.toggleSelectedProject, + payload, + state, + [ + { + type: types.DESELECT_PROJECT, + payload, + }, + ], + [], + ); + }); + }); + + describe('addProjects', () => { + it('posts selected project ids to project add endpoint', () => { + state.projectEndpoints.add = mockAddEndpoint; + + mockAxios.onPost(mockAddEndpoint).replyOnce(200, mockResponse); + + return testAction( + actions.addProjects, + null, + state, + [], + [ + { + type: 'requestAddProjects', + }, + { + type: 'receiveAddProjectsSuccess', + payload: mockResponse, + }, + ], + ); + }); + + it('calls addProjects error handler on error', () => { + mockAxios.onPost(mockAddEndpoint).replyOnce(500); + + return testAction( + actions.addProjects, + null, + state, + [], + [{ type: 'requestAddProjects' }, { type: 'receiveAddProjectsError' }], + ); + }); + }); + + describe('requestAddProjects', () => { + it('commits the REQUEST_ADD_PROJECTS mutation', () => + testAction( + actions.requestAddProjects, + null, + state, + [ + { + type: types.REQUEST_ADD_PROJECTS, + }, + ], + [], + )); + }); + + describe('receiveAddProjectsSuccess', () => { + beforeEach(() => { + state.selectedProjects = getMockProjects(3); + }); + + it('fetches projects when new projects are added to the dashboard', () => { + const addedProject = state.selectedProjects[0]; + const payload = { + added: [addedProject.id], + invalid: [], + duplicate: [], + }; + + return testAction( + actions.receiveAddProjectsSuccess, + payload, + state, + [{ type: types.RECEIVE_ADD_PROJECTS_SUCCESS }], + [ + { + type: 'fetchProjects', + }, + ], + ); + }); + + it('displays an error when user tries to add one invalid project to dashboard', () => { + const invalidProject = state.selectedProjects[0]; + const payload = { + added: [], + invalid: [invalidProject.id], + }; + + return testAction( + actions.receiveAddProjectsSuccess, + payload, + state, + [{ type: types.RECEIVE_ADD_PROJECTS_SUCCESS }], + [], + ).then(() => { + expect(createFlash).toHaveBeenCalledWith(`Unable to add ${invalidProject.name}`); + }); + }); + + it('displays an error when user tries to add two invalid projects to dashboard', () => { + const invalidProject1 = state.selectedProjects[0]; + const invalidProject2 = state.selectedProjects[1]; + const payload = { + added: [], + invalid: [invalidProject1.id, invalidProject2.id], + }; + + return testAction( + actions.receiveAddProjectsSuccess, + payload, + state, + [{ type: types.RECEIVE_ADD_PROJECTS_SUCCESS }], + [], + ).then(() => { + expect(createFlash).toHaveBeenCalledWith( + `Unable to add ${invalidProject1.name} and ${invalidProject2.name}`, + ); + }); + }); + + it('displays an error when user tries to add more than two invalid projects to dashboard', () => { + const invalidProject1 = state.selectedProjects[0]; + const invalidProject2 = state.selectedProjects[1]; + const invalidProject3 = state.selectedProjects[2]; + const payload = { + added: [], + invalid: [invalidProject1.id, invalidProject2.id, invalidProject3.id], + }; + + return testAction( + actions.receiveAddProjectsSuccess, + payload, + state, + [{ type: types.RECEIVE_ADD_PROJECTS_SUCCESS }], + [], + ).then(() => { + expect(createFlash).toHaveBeenCalledWith( + `Unable to add ${invalidProject1.name}, ${invalidProject2.name}, and ${invalidProject3.name}`, + ); + }); + }); + }); + + describe('receiveAddProjectsError', () => { + it('commits RECEIVE_ADD_PROJECTS_ERROR', () => + testAction( + actions.receiveAddProjectsError, + null, + state, + [ + { + type: types.RECEIVE_ADD_PROJECTS_ERROR, + }, + ], + [], + )); + + it('shows error message', () => { + actions.receiveAddProjectsError(mockDispatchContext); + + expect(createFlash).toHaveBeenCalledTimes(1); + expect(createFlash).toHaveBeenCalledWith( + 'Something went wrong, unable to add projects to dashboard', + ); + }); + }); + + describe('clearSearchResults', () => { + it('clears all project search results', () => + testAction( + actions.clearSearchResults, + null, + state, + [ + { + type: types.CLEAR_SEARCH_RESULTS, + }, + ], + [], + )); + }); + + describe('fetchProjects', () => { + it('calls project list endpoint', () => { + state.projectEndpoints.list = mockListEndpoint; + mockAxios.onGet(mockListEndpoint).replyOnce(200, mockResponse); + + return testAction( + actions.fetchProjects, + null, + state, + [], + [{ type: 'requestProjects' }, { type: 'receiveProjectsSuccess', payload: mockResponse }], + ); + }); + + it('handles response errors', () => { + state.projectEndpoints.list = mockListEndpoint; + mockAxios.onGet(mockListEndpoint).replyOnce(500); + + return testAction( + actions.fetchProjects, + null, + state, + [], + [{ type: 'requestProjects' }, { type: 'receiveProjectsError' }], + ); + }); + }); + + describe('requestProjects', () => { + it('toggles project loading state', () => + testAction(actions.requestProjects, null, state, [{ type: types.REQUEST_PROJECTS }], [])); + }); + + describe('receiveProjectsSuccess', () => { + it('sets projects from data on success', () => { + const payload = { + projects: [{ id: 0, name: 'mock-name1' }], + }; + + return testAction( + actions.receiveProjectsSuccess, + payload, + state, + [ + { + type: types.RECEIVE_PROJECTS_SUCCESS, + payload: payload.projects, + }, + ], + [], + ); + }); + }); + + describe('receiveProjectsError', () => { + it('clears projects and alerts user of error', () => + testAction( + actions.receiveProjectsError, + null, + state, + [ + { + type: types.RECEIVE_PROJECTS_ERROR, + }, + ], + [], + ).then(() => { + expect(createFlash).toHaveBeenCalledWith('Something went wrong, unable to get projects'); + })); + }); + + describe('removeProject', () => { + const mockRemovePath = 'mock-removePath'; + + it('calls project removal path and fetches projects on success', () => { + mockAxios.onDelete(mockRemovePath).replyOnce(200); + + return testAction( + actions.removeProject, + mockRemovePath, + null, + [], + [{ type: 'requestRemoveProject' }, { type: 'receiveRemoveProjectSuccess' }], + ); + }); + + it('passes off handling of project removal errors', () => { + mockAxios.onDelete(mockRemovePath).replyOnce(500); + + return testAction( + actions.removeProject, + mockRemovePath, + null, + [], + [{ type: 'requestRemoveProject' }, { type: 'receiveRemoveProjectError' }], + ); + }); + }); + + describe('requestRemoveProject', () => { + it('commits REQUEST_REMOVE_PROJECT mutation', () => + testAction( + actions.requestRemoveProject, + null, + state, + [ + { + type: types.REQUEST_REMOVE_PROJECT, + }, + ], + [], + )); + }); + + describe('receiveRemoveProjectSuccess', () => { + it('commits RECEIVE_REMOVE_PROJECT_SUCCESS mutation and dispatches fetchProjects', () => + testAction( + actions.receiveRemoveProjectSuccess, + null, + state, + [ + { + type: types.RECEIVE_REMOVE_PROJECT_SUCCESS, + }, + ], + [{ type: 'fetchProjects' }], + )); + }); + + describe('receiveRemoveProjectError', () => { + it('commits REQUEST_REMOVE_PROJECT mutation', () => + testAction( + actions.receiveRemoveProjectError, + null, + state, + [ + { + type: types.RECEIVE_REMOVE_PROJECT_ERROR, + }, + ], + [], + )); + + it('displays project removal error', () => { + actions.receiveRemoveProjectError(mockDispatchContext); + + expect(createFlash).toHaveBeenCalledTimes(1); + expect(createFlash).toHaveBeenCalledWith('Something went wrong, unable to remove project'); + }); + }); + + describe('fetchSearchResults', () => { + it.each([null, undefined, false, NaN, 0, ''])( + 'dispatches setMinimumQueryMessage if the search query is falsy', + searchQuery => { + state.searchQuery = searchQuery; + + return testAction( + actions.fetchSearchResults, + null, + state, + [], + [ + { + type: 'requestSearchResults', + }, + { + type: 'setMinimumQueryMessage', + }, + ], + ); + }, + ); + + it.each(['a', 'aa'])( + 'dispatches setMinimumQueryMessage if the search query was not long enough', + shortSearchQuery => { + state.searchQuery = shortSearchQuery; + + return testAction( + actions.fetchSearchResults, + null, + state, + [], + [ + { + type: 'requestSearchResults', + }, + { + type: 'setMinimumQueryMessage', + }, + ], + ); + }, + ); + + it('dispatches the correct actions when the query is valid', () => { + const mockProjects = [{ id: 0, name: 'mock-name1' }]; + Api.projects.mockResolvedValueOnce(mockProjects); + state.searchQuery = 'mock-query'; + + return testAction( + actions.fetchSearchResults, + null, + state, + [], + [ + { + type: 'requestSearchResults', + }, + { + type: 'receiveSearchResultsSuccess', + payload: mockProjects, + }, + ], + ); + }); + }); + + describe('requestSearchResults', () => { + it('commits the REQUEST_SEARCH_RESULTS mutation', () => + testAction( + actions.requestSearchResults, + null, + state, + [ + { + type: types.REQUEST_SEARCH_RESULTS, + }, + ], + [], + )); + }); + + describe('receiveSearchResultsSuccess', () => { + it('commits the RECEIVE_SEARCH_RESULTS_SUCCESS mutation', () => { + const mockProjects = [{ id: 0, name: 'mock-project1' }]; + + return testAction( + actions.receiveSearchResultsSuccess, + mockProjects, + state, + [ + { + type: types.RECEIVE_SEARCH_RESULTS_SUCCESS, + payload: mockProjects, + }, + ], + [], + ); + }); + }); + + describe('receiveSearchResultsError', () => { + it('commits the RECEIVE_SEARCH_RESULTS_ERROR mutation', () => + testAction( + actions.receiveSearchResultsError, + ['error'], + state, + [ + { + type: types.RECEIVE_SEARCH_RESULTS_ERROR, + }, + ], + [], + )); + }); + + describe('setProjectEndpoints', () => { + it('commits project list and add endpoints', () => { + const payload = { + add: 'add', + list: 'list', + }; + + return testAction( + actions.setProjectEndpoints, + payload, + state, + [ + { + type: types.SET_PROJECT_ENDPOINTS, + payload, + }, + ], + [], + ); + }); + }); + + describe('setMinimumQueryMessage', () => { + it('commits the SET_MINIMUM_QUERY_MESSAGE mutation', () => + testAction( + actions.setMinimumQueryMessage, + null, + state, + [ + { + type: types.SET_MINIMUM_QUERY_MESSAGE, + }, + ], + [], + )); + }); +}); diff --git a/ee/spec/frontend/security_dashboard/store/modules/project_selector/mutations_spec.js b/ee/spec/frontend/security_dashboard/store/modules/project_selector/mutations_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..740eb6f45625c1afe6d88b594a6619c2c31d8b2f --- /dev/null +++ b/ee/spec/frontend/security_dashboard/store/modules/project_selector/mutations_spec.js @@ -0,0 +1,357 @@ +import createState from 'ee/security_dashboard/store/modules/project_selector/state'; +import mutations from 'ee/security_dashboard/store/modules/project_selector/mutations'; +import * as types from 'ee/security_dashboard/store/modules/project_selector/mutation_types'; + +describe('projectsSelector mutations', () => { + let state; + + beforeEach(() => { + state = createState(); + }); + + describe('SET_PROJECT_ENDPOINTS', () => { + it('sets "projectEndpoints.list" and "projectEndpoints.add"', () => { + const payload = { list: 'list', add: 'add' }; + + state.projectEndpoints = {}; + + mutations[types.SET_PROJECT_ENDPOINTS](state, payload); + + expect(state.projectEndpoints.list).toBe(payload.list); + expect(state.projectEndpoints.add).toBe(payload.add); + }); + }); + + describe('SET_SEARCH_QUERY', () => { + it('sets "searchQuery" to be the given payload', () => { + const payload = 'searchQuery'; + state.searchQuery = ''; + + mutations[types.SET_SEARCH_QUERY](state, payload); + + expect(state.searchQuery).toBe(payload); + }); + }); + + describe('SELECT_PROJECT', () => { + it('adds the given project to "selectedProjects"', () => { + const payload = {}; + state.selectedProjects = []; + + mutations[types.SELECT_PROJECT](state, payload); + + expect(state.selectedProjects[0]).toBe(payload); + }); + + it('prevents projects from being added to "selectedProjects" twice', () => { + const payload1 = { id: 1 }; + const payload2 = { id: 2 }; + + mutations[types.SELECT_PROJECT](state, payload1); + mutations[types.SELECT_PROJECT](state, payload1); + + expect(state.selectedProjects).toHaveLength(1); + + mutations[types.SELECT_PROJECT](state, payload2); + + expect(state.selectedProjects).toHaveLength(2); + }); + }); + + describe('DESELECT_PROJECT', () => { + it('removes the project with the given id from "selectedProjects"', () => { + state.selectedProjects = [{ id: 1 }, { id: 2 }]; + const payload = { id: 1 }; + + mutations[types.DESELECT_PROJECT](state, payload); + + expect(state.selectedProjects).toHaveLength(1); + expect(state.selectedProjects[0].id).toBe(2); + }); + }); + + describe('REQUEST_ADD_PROJECTS', () => { + it('sets "isAddingProjects" to be true', () => { + state.isAddingProjects = false; + + mutations[types.REQUEST_ADD_PROJECTS](state); + + expect(state.isAddingProjects).toBe(true); + }); + }); + + describe('RECEIVE_ADD_PROJECTS_SUCCESS', () => { + it('sets "isAddingProjects" to be true', () => { + state.isAddingProjects = true; + + mutations[types.RECEIVE_ADD_PROJECTS_SUCCESS](state); + + expect(state.isAddingProjects).toBe(false); + }); + }); + + describe('RECEIVE_ADD_PROJECTS_ERROR', () => { + it('sets "isAddingProjects" to be true', () => { + state.isAddingProjects = true; + + mutations[types.RECEIVE_ADD_PROJECTS_ERROR](state); + + expect(state.isAddingProjects).toBe(false); + }); + }); + + describe('REQUEST_REMOVE_PROJECT', () => { + it('sets "isRemovingProjects" to be true', () => { + state.isRemovingProject = false; + + mutations[types.REQUEST_REMOVE_PROJECT](state); + + expect(state.isRemovingProject).toBe(true); + }); + }); + + describe('RECEIVE_REMOVE_PROJECT_SUCCESS', () => { + it('sets "isRemovingProjects" to be true', () => { + state.isRemovingProject = true; + + mutations[types.RECEIVE_REMOVE_PROJECT_SUCCESS](state); + + expect(state.isRemovingProject).toBe(false); + }); + }); + + describe('RECEIVE_REMOVE_PROJECT_ERROR', () => { + it('sets "isRemovingProjects" to be true', () => { + state.isRemovingProject = true; + + mutations[types.RECEIVE_REMOVE_PROJECT_ERROR](state); + + expect(state.isRemovingProject).toBe(false); + }); + }); + + describe('REQUEST_PROJECTS', () => { + it('sets "isLoadingProjects" to be true', () => { + state.isLoadingProjects = false; + + mutations[types.REQUEST_PROJECTS](state); + + expect(state.isLoadingProjects).toBe(true); + }); + }); + + describe('RECEIVE_PROJECTS_SUCCESS', () => { + it('sets "projects" to be the payload', () => { + const payload = []; + state.projects = []; + + mutations[types.RECEIVE_PROJECTS_SUCCESS](state, payload); + + expect(state.projects).toBe(payload); + }); + + it('sets "isLoadingProjects" to be false', () => { + state.isLoadingProjects = true; + + mutations[types.RECEIVE_PROJECTS_SUCCESS](state, []); + + expect(state.isLoadingProjects).toBe(false); + }); + }); + + describe('RECEIVE_PROJECTS_ERROR', () => { + it('sets "projects" to be an empty array', () => { + state.projects = []; + + mutations[types.RECEIVE_PROJECTS_ERROR](state); + + expect(state.projects).toEqual([]); + }); + + it('sets "isLoadingProjects" to be false', () => { + state.isLoadingProjects = true; + + mutations[types.RECEIVE_PROJECTS_ERROR](state); + + expect(state.isLoadingProjects).toBe(false); + }); + }); + + describe('CLEAR_SEARCH_RESULTS', () => { + it('sets "projectSearchResults" to be an empty array', () => { + state.projectSearchResults = ['']; + + mutations[types.CLEAR_SEARCH_RESULTS](state); + + expect(state.projectSearchResults).toHaveLength(0); + }); + + it('sets "selectedProjects" to be an empty array', () => { + state.selectedProjects = ['']; + + mutations[types.CLEAR_SEARCH_RESULTS](state); + + expect(state.selectedProjects).toHaveLength(0); + }); + }); + + describe('REQUEST_SEARCH_RESULTS', () => { + it('sets "messages.minimumQuery" to be false', () => { + state.messages.minimumQuery = true; + + mutations[types.REQUEST_SEARCH_RESULTS](state); + + expect(state.messages.minimumQuery).toBe(false); + }); + + it('increments "searchCount" by one', () => { + state.searchCount = 0; + + mutations[types.REQUEST_SEARCH_RESULTS](state); + + expect(state.searchCount).toBe(1); + }); + }); + + describe('RECEIVE_SEARCH_RESULTS_SUCCESS', () => { + it('sets "projectSearchResults" to be the payload', () => { + const payload = []; + + mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](state, payload); + + expect(state.projectSearchResults).toBe(payload); + }); + + it('sets "messages.noResults" to be false if the payload is not empty', () => { + state.messages.noResults = true; + + mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](state, ['']); + + expect(state.messages.noResults).toBe(false); + }); + + it('sets "messages.searchError" to be false', () => { + state.messages.searchError = true; + + mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](state, ['']); + + expect(state.messages.searchError).toBe(false); + }); + + it('sets "messages.minimumQuery" to be false', () => { + state.messages.minimumQuery = true; + + mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](state, ['']); + + expect(state.messages.minimumQuery).toBe(false); + }); + + it('decrements "searchCount" by one', () => { + state.searchCount = 1; + + mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](state, ['']); + + expect(state.searchCount).toBe(0); + }); + + it('does not decrement "searchCount" into negative', () => { + state.searchCount = 0; + + mutations[types.RECEIVE_SEARCH_RESULTS_SUCCESS](state, ['']); + + expect(state.searchCount).toBe(0); + }); + }); + + describe('RECEIVE_SEARCH_RESULTS_ERROR', () => { + it('sets "projectSearchResult" to be empty', () => { + state.projectSearchResults = ['']; + + mutations[types.RECEIVE_SEARCH_RESULTS_ERROR](state); + + expect(state.projectSearchResults).toHaveLength(0); + }); + + it('sets "messages.noResults" to be false', () => { + state.messages.noResults = true; + + mutations[types.RECEIVE_SEARCH_RESULTS_ERROR](state); + + expect(state.messages.noResults).toBe(false); + }); + + it('sets "messages.searchError" to be true', () => { + state.messages.searchError = false; + + mutations[types.RECEIVE_SEARCH_RESULTS_ERROR](state); + + expect(state.messages.searchError).toBe(true); + }); + + it('sets "messages.minimumQuery" to be false', () => { + state.messages.minimumQuery = true; + + mutations[types.RECEIVE_SEARCH_RESULTS_ERROR](state); + + expect(state.messages.minimumQuery).toBe(false); + }); + + it('decrements "searchCount" by one', () => { + state.searchCount = 1; + + mutations[types.RECEIVE_SEARCH_RESULTS_ERROR](state); + + expect(state.searchCount).toBe(0); + }); + + it('does not decrement "searchCount" into negative', () => { + state.searchCount = 0; + + mutations[types.RECEIVE_SEARCH_RESULTS_ERROR](state); + + expect(state.searchCount).toBe(0); + }); + }); + + describe('SET_MINIMUM_QUERY_MESSAGE', () => { + it('sets "projectSearchResult" to be an empty array', () => { + state.projectSearchResults = ['']; + + mutations[types.SET_MINIMUM_QUERY_MESSAGE](state); + + expect(state.projectSearchResults).toHaveLength(0); + }); + + it('sets "messages.noResults" to be false', () => { + state.messages.noResults = true; + + mutations[types.SET_MINIMUM_QUERY_MESSAGE](state); + + expect(state.messages.noResults).toBe(false); + }); + + it('sets "messages.searchError" to be false', () => { + state.messages.searchError = true; + + mutations[types.SET_MINIMUM_QUERY_MESSAGE](state); + + expect(state.messages.searchError).toBe(false); + }); + + it('sets "messages.minimumQuery" to true', () => { + state.messages.minimumQuery = false; + + mutations[types.SET_MINIMUM_QUERY_MESSAGE](state); + + expect(state.messages.minimumQuery).toBe(true); + }); + + it('does not decrement "searchCount" into negative', () => { + state.searchCount = 0; + + mutations[types.RECEIVE_SEARCH_RESULTS_ERROR](state); + + expect(state.searchCount).toBe(0); + }); + }); +}); diff --git a/ee/spec/frontend/security_dashboard/store/modules/project_selector/state_spec.js b/ee/spec/frontend/security_dashboard/store/modules/project_selector/state_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..dda48d77a7e6029bd159df1ed963d40c8ae78cfc --- /dev/null +++ b/ee/spec/frontend/security_dashboard/store/modules/project_selector/state_spec.js @@ -0,0 +1,52 @@ +import createState from 'ee/security_dashboard/store/modules/project_selector/state'; + +describe('projectsSelector default state', () => { + const state = createState(); + + it('has "inputValue" set to be an empty string', () => { + expect(state.inputValue).toBe(''); + }); + + it('has "isLoadingProjects" set to be false', () => { + expect(state.isLoadingProjects).toBe(false); + }); + + it('has "isAddingProjects" set to be false', () => { + expect(state.isAddingProjects).toBe(false); + }); + + it('has "isRemovingProject" set to be false', () => { + expect(state.isRemovingProject).toBe(false); + }); + + it('has all "projectEndpoints" set to be null', () => { + expect(state.projectEndpoints.list).toBe(null); + expect(state.projectEndpoints.add).toBe(null); + }); + + it('has "searchQuery" set to an empty string', () => { + expect(state.searchQuery).toBe(''); + }); + + it('has "projects" set to be an empty array', () => { + expect(state.projects).toEqual([]); + }); + + it('has "projectSearchResults" set to be an empty array', () => { + expect(state.projectSearchResults).toEqual([]); + }); + + it('has "selectedProjects" set to be an empty array', () => { + expect(state.selectedProjects).toEqual([]); + }); + + it('has all "messages" set to be false', () => { + expect(state.messages.noResults).toBe(false); + expect(state.messages.searchError).toBe(false); + expect(state.messages.minimumQuery).toBe(false); + }); + + it('has "searchCount" set to be 0', () => { + expect(state.searchCount).toBe(0); + }); +}); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bf696f2f414b498cfbcea96034ce7187ee0cba60..51da4b6a4cf196ccca70fcfac5c9a1b9d68492b0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -14039,6 +14039,12 @@ msgstr "" msgid "SecurityDashboard| The security dashboard displays the latest security report. Use it to find and fix vulnerabilities." msgstr "" +msgid "SecurityDashboard|%{firstProject} and %{secondProject}" +msgstr "" + +msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}" +msgstr "" + msgid "SecurityDashboard|Confidence" msgstr "" @@ -14060,6 +14066,9 @@ msgstr "" msgid "SecurityDashboard|Severity" msgstr "" +msgid "SecurityDashboard|Unable to add %{invalidProjects}" +msgstr "" + msgid "See metrics" msgstr "" @@ -14785,6 +14794,9 @@ msgstr "" msgid "Something went wrong, unable to add %{project} to dashboard" msgstr "" +msgid "Something went wrong, unable to add projects to dashboard" +msgstr "" + msgid "Something went wrong, unable to get projects" msgstr ""