diff --git a/ee/app/assets/javascripts/security_dashboard/components/app.vue b/ee/app/assets/javascripts/security_dashboard/components/app.vue
index cd1804fe1312e70daa2dfba759e69a9d21bcc89b..748e462b53ebf089ec9e8a412c134760e3404cf9 100644
--- a/ee/app/assets/javascripts/security_dashboard/components/app.vue
+++ b/ee/app/assets/javascripts/security_dashboard/components/app.vue
@@ -24,6 +24,10 @@ export default {
type: String,
required: true,
},
+ projectsEndpoint: {
+ type: String,
+ required: true,
+ },
vulnerabilitiesEndpoint: {
type: String,
required: true,
@@ -43,13 +47,16 @@ export default {
},
computed: {
...mapState('vulnerabilities', ['modal']),
+ ...mapState('projects', ['projects']),
...mapGetters('filters', ['activeFilters']),
},
created() {
+ this.setProjectsEndpoint(this.projectsEndpoint);
this.setVulnerabilitiesEndpoint(this.vulnerabilitiesEndpoint);
this.setVulnerabilitiesCountEndpoint(this.vulnerabilitiesCountEndpoint);
this.setVulnerabilitiesHistoryEndpoint(this.vulnerabilitiesHistoryEndpoint);
this.fetchVulnerabilitiesCount();
+ this.fetchProjects();
},
methods: {
...mapActions('vulnerabilities', [
@@ -63,6 +70,7 @@ export default {
'setVulnerabilitiesEndpoint',
'setVulnerabilitiesHistoryEndpoint',
]),
+ ...mapActions('projects', ['setProjectsEndpoint', 'fetchProjects']),
filterChange() {
this.fetchVulnerabilities(this.activeFilters);
this.fetchVulnerabilitiesCount(this.activeFilters);
diff --git a/ee/app/assets/javascripts/security_dashboard/components/report_type_popover.vue b/ee/app/assets/javascripts/security_dashboard/components/report_type_popover.vue
index d415376598a1bb47da6a406da087e026bca2430a..820b34fe44d865ca698bc16366850d134940c42f 100644
--- a/ee/app/assets/javascripts/security_dashboard/components/report_type_popover.vue
+++ b/ee/app/assets/javascripts/security_dashboard/components/report_type_popover.vue
@@ -1,5 +1,6 @@
@@ -33,12 +35,10 @@ export default {
v-if="dashboardDocumentation"
target="_blank"
rel="noopener noreferrer"
- :title="s__('Security Reports|Security dashboard documentation')"
+ :title="$options.linkTitle"
:href="dashboardDocumentation"
>
- {{
- s__('Security Reports|Security dashboard documentation')
- }}
+ {{ $options.linkTitle }}
diff --git a/ee/app/assets/javascripts/security_dashboard/index.js b/ee/app/assets/javascripts/security_dashboard/index.js
index d421fc1569a174d965ad314e2ccb8846f53e2c74..4a555d5d31f7c4c9eca611a8fc02450e0e517aae 100644
--- a/ee/app/assets/javascripts/security_dashboard/index.js
+++ b/ee/app/assets/javascripts/security_dashboard/index.js
@@ -16,6 +16,7 @@ export default () => {
props: {
dashboardDocumentation: el.dataset.dashboardDocumentation,
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
+ projectsEndpoint: el.dataset.projectsEndpoint,
vulnerabilityFeedbackHelpPath: el.dataset.vulnerabilityFeedbackHelpPath,
vulnerabilitiesEndpoint: el.dataset.vulnerabilitiesEndpoint,
vulnerabilitiesCountEndpoint: el.dataset.vulnerabilitiesSummaryEndpoint,
diff --git a/ee/app/assets/javascripts/security_dashboard/store/index.js b/ee/app/assets/javascripts/security_dashboard/store/index.js
index 6f51dfde52bce155e4564e230ec985214c9093da..2fc3fc755cc1acc68c2ce0152d162acf41bba8a7 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/index.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/index.js
@@ -1,14 +1,20 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import vulnerabilities from './modules/vulnerabilities/index';
+import configureModerator from './moderator';
import filters from './modules/filters/index';
+import projects from './modules/projects/index';
+import vulnerabilities from './modules/vulnerabilities/index';
Vue.use(Vuex);
-export default () =>
- new Vuex.Store({
- modules: {
- vulnerabilities,
- filters,
- },
- });
+const store = new Vuex.Store({
+ modules: {
+ filters,
+ projects,
+ vulnerabilities,
+ },
+});
+
+configureModerator(store);
+
+export default () => store;
diff --git a/ee/app/assets/javascripts/security_dashboard/store/moderator.js b/ee/app/assets/javascripts/security_dashboard/store/moderator.js
new file mode 100644
index 0000000000000000000000000000000000000000..909e5ec6ee4592f7c54f71889ee8b453e686343a
--- /dev/null
+++ b/ee/app/assets/javascripts/security_dashboard/store/moderator.js
@@ -0,0 +1,26 @@
+import * as projectsMutationTypes from './modules/projects/mutation_types';
+
+export default function configureModerator(store) {
+ store.subscribe(({ type, payload }) => {
+ switch (type) {
+ case `projects/${projectsMutationTypes.RECEIVE_PROJECTS_SUCCESS}`:
+ return store.dispatch('filters/setFilterOptions', {
+ filterId: 'project',
+ options: [
+ {
+ name: 'All',
+ id: 'all',
+ selected: true,
+ },
+ ...payload.projects.map(project => ({
+ name: project.name,
+ id: project.id.toString(),
+ selected: false,
+ })),
+ ],
+ });
+ default:
+ return null;
+ }
+ });
+}
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/actions.js b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/actions.js
index 2b06257ba68e968de41db06f3057552ec815392a..21153d0e380cf2736559e481e3079a893af81601 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/actions.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/actions.js
@@ -4,6 +4,10 @@ export const setFilter = ({ commit }, payload) => {
commit(types.SET_FILTER, payload);
};
+export const setFilterOptions = ({ commit }, payload) => {
+ commit(types.SET_FILTER_OPTIONS, payload);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
// This is no longer needed after gitlab-ce#52179 is merged
export default () => {};
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutation_types.js b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutation_types.js
index a92b48b9e2aacfaf9ea57ed72019f74ed0fe23a5..4a880d1a6bb62998f356f66f03835bd5870b8d43 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutation_types.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutation_types.js
@@ -1,4 +1,2 @@
export const SET_FILTER = 'SET_FILTER';
-
-// This is here because es-lint requires a default export when there are less than two named exports
-export default SET_FILTER;
+export const SET_FILTER_OPTIONS = 'SET_FILTER_OPTIONS';
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutations.js b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutations.js
index 634628b856f1e7742850aa44b2354c9df431da1c..b02c3010dba9f04138d2ecdd2e6c6198f3333784 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutations.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/mutations.js
@@ -41,4 +41,8 @@ export default {
activeFilter.options = activeOptions;
}
},
+ [types.SET_FILTER_OPTIONS](state, payload) {
+ const { filterId, options } = payload;
+ state.filters.find(filter => filter.id === filterId).options = options;
+ },
};
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/state.js b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/state.js
index 38068d044bbf0f9935350888ca6196a3101a549f..ce266cd769443c1ef9a67a39c482491ca68a83fc 100644
--- a/ee/app/assets/javascripts/security_dashboard/store/modules/filters/state.js
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/filters/state.js
@@ -32,5 +32,16 @@ export default () => ({
}),
],
},
+ {
+ name: 'Project',
+ id: 'project',
+ options: [
+ {
+ name: 'All',
+ id: 'all',
+ selected: true,
+ },
+ ],
+ },
],
});
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/actions.js b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/actions.js
new file mode 100644
index 0000000000000000000000000000000000000000..73b1d574de84f581c1537f700827dd42819b7598
--- /dev/null
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/actions.js
@@ -0,0 +1,37 @@
+import axios from '~/lib/utils/axios_utils';
+import * as types from './mutation_types';
+
+export const setProjectsEndpoint = ({ commit }, endpoint) => {
+ commit(types.SET_PROJECTS_ENDPOINT, endpoint);
+};
+
+export const fetchProjects = ({ state, dispatch }) => {
+ dispatch('requestProjects');
+
+ axios({
+ method: 'GET',
+ url: state.projectsEndpoint,
+ })
+ .then(({ data }) => {
+ dispatch('receiveProjectsSuccess', { projects: 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);
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+// This is no longer needed after gitlab-ce#52179 is merged
+export default () => {};
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/index.js b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..68c81bb45096f154036c07678c400afaec9bb260
--- /dev/null
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/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/projects/mutation_types.js b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/mutation_types.js
new file mode 100644
index 0000000000000000000000000000000000000000..7fcf0ea6a49bdec96e91189f820c6be3f246bf96
--- /dev/null
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/mutation_types.js
@@ -0,0 +1,4 @@
+export const SET_PROJECTS_ENDPOINT = 'SET_PROJECTS_ENDPOINT';
+export const REQUEST_PROJECTS = 'REQUEST_PROJECTS';
+export const RECEIVE_PROJECTS_SUCCESS = 'RECEIVE_PROJECTS_SUCCESS';
+export const RECEIVE_PROJECTS_ERROR = 'RECEIVE_PROJECTS_ERROR';
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/mutations.js b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/mutations.js
new file mode 100644
index 0000000000000000000000000000000000000000..d176e5700f6db6a60b5a63cd64ac789ea98ba0d8
--- /dev/null
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/mutations.js
@@ -0,0 +1,19 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_PROJECTS_ENDPOINT](state, payload) {
+ state.projectsEndpoint = payload;
+ },
+ [types.REQUEST_PROJECTS](state) {
+ state.isLoadingProjects = true;
+ state.errorLoadingProjects = false;
+ },
+ [types.RECEIVE_PROJECTS_SUCCESS](state, payload) {
+ state.projects = payload.projects;
+ state.isLoadingProjects = false;
+ },
+ [types.RECEIVE_PROJECTS_ERROR](state) {
+ state.isLoadingProjects = false;
+ state.errorLoadingProjects = true;
+ },
+};
diff --git a/ee/app/assets/javascripts/security_dashboard/store/modules/projects/state.js b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/state.js
new file mode 100644
index 0000000000000000000000000000000000000000..f31b49f35f65acab8049d7635d759fc0db8b7337
--- /dev/null
+++ b/ee/app/assets/javascripts/security_dashboard/store/modules/projects/state.js
@@ -0,0 +1,6 @@
+export default () => ({
+ endpoint: '',
+ projects: [],
+ isLoadingProjects: false,
+ errorLoadingProjects: false,
+});
diff --git a/ee/app/views/groups/security/dashboard/show.html.haml b/ee/app/views/groups/security/dashboard/show.html.haml
index b5a5f07855757464efac589d4020f5e5b10c9d8a..300a3e4783744a18cd3f344a2f78ab9539c12533 100644
--- a/ee/app/views/groups/security/dashboard/show.html.haml
+++ b/ee/app/views/groups/security/dashboard/show.html.haml
@@ -4,6 +4,7 @@
#js-group-security-dashboard{ data: { vulnerabilities_endpoint: group_security_vulnerabilities_path(@group),
vulnerabilities_summary_endpoint: summary_group_security_vulnerabilities_path(@group),
vulnerabilities_history_endpoint: history_group_security_vulnerabilities_path(@group),
+ projects_endpoint: expose_url(api_v4_groups_projects_path(id: @group.id)),
vulnerability_feedback_help_path: help_page_path("user/project/merge_requests/index", anchor: "interacting-with-security-reports-ultimate"),
empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
dashboard_documentation: help_page_path('user/group/security_dashboard/index') } }
diff --git a/ee/changelogs/unreleased/6240-project-filter-for-gsd.yml b/ee/changelogs/unreleased/6240-project-filter-for-gsd.yml
new file mode 100644
index 0000000000000000000000000000000000000000..50bec364860623b67abccd13076d15d535738bc4
--- /dev/null
+++ b/ee/changelogs/unreleased/6240-project-filter-for-gsd.yml
@@ -0,0 +1,5 @@
+---
+title: Adds project filtering to the GSD
+merge_request: 8944
+author:
+type: added
diff --git a/ee/spec/javascripts/security_dashboard/components/filters_spec.js b/ee/spec/javascripts/security_dashboard/components/filters_spec.js
index ea00a30a102c8837695b37b3c047dde3abcd0326..8fd344d7659f3139e5f0d4d0642f1067cbea6c31 100644
--- a/ee/spec/javascripts/security_dashboard/components/filters_spec.js
+++ b/ee/spec/javascripts/security_dashboard/components/filters_spec.js
@@ -18,8 +18,8 @@ describe('Filter component', () => {
vm.$destroy();
});
- it('should display both filters', () => {
- expect(vm.$el.querySelectorAll('.js-filter').length).toEqual(2);
+ it('should display all filters', () => {
+ expect(vm.$el.querySelectorAll('.js-filter').length).toEqual(3);
});
});
});
diff --git a/ee/spec/javascripts/security_dashboard/store/filters/actions_spec.js b/ee/spec/javascripts/security_dashboard/store/filters/actions_spec.js
index 03d00185d2307dc51758faa5956ebb138538fa59..a4d74acc68183afd193cea27e2b6a686ce4ff038 100644
--- a/ee/spec/javascripts/security_dashboard/store/filters/actions_spec.js
+++ b/ee/spec/javascripts/security_dashboard/store/filters/actions_spec.js
@@ -25,4 +25,25 @@ describe('filters actions', () => {
);
});
});
+
+ describe('setFilterOptions', () => {
+ it('should commit the SET_FILTER_OPTIONS mutuation', done => {
+ const state = createState();
+ const payload = { filterId: 'project', options: [] };
+
+ testAction(
+ actions.setFilterOptions,
+ payload,
+ state,
+ [
+ {
+ type: types.SET_FILTER_OPTIONS,
+ payload,
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/ee/spec/javascripts/security_dashboard/store/filters/getters_spec.js b/ee/spec/javascripts/security_dashboard/store/filters/getters_spec.js
index 76fcfb8f00486b576f3c8cbc0bf063c33c235af9..a1f6c2bc15fa5e90ecf1c8c8e0eea8b7835a27ec 100644
--- a/ee/spec/javascripts/security_dashboard/store/filters/getters_spec.js
+++ b/ee/spec/javascripts/security_dashboard/store/filters/getters_spec.js
@@ -17,10 +17,14 @@ describe('filters module getters', () => {
getFilterIds,
};
};
+ let state;
+
+ beforeEach(() => {
+ state = createState();
+ });
describe('getFilter', () => {
it('should return the type filter information', () => {
- const state = createState();
const typeFilter = getters.getFilter(state)('report_type');
expect(typeFilter.name).toEqual('Report type');
@@ -30,7 +34,6 @@ describe('filters module getters', () => {
describe('getSelectedOptions', () => {
describe('with one selected option', () => {
it('should return "All" as the selected option', () => {
- const state = createState();
const selectedOptions = getters.getSelectedOptions(state, mockedGetters(state))(
'report_type',
);
@@ -42,7 +45,7 @@ describe('filters module getters', () => {
describe('with multiple selected options', () => {
it('should return both "High" and "Critical" ', () => {
- const state = {
+ state = {
filters: [
{
id: 'severity',
@@ -58,16 +61,13 @@ describe('filters module getters', () => {
});
describe('getSelectedOptionIds', () => {
- it('should return "one" as the selcted project ID', () => {
- const state = createState();
- const projectFilter = {
- id: 'project',
+ it('should return "one" as the selcted dummy ID', () => {
+ const dummyFilter = {
+ id: 'dummy',
options: [{ id: 'one', selected: true }, { id: 'anotherone', selected: false }],
};
- state.filters.push(projectFilter);
- const selectedOptionIds = getters.getSelectedOptionIds(state, mockedGetters(state))(
- 'project',
- );
+ state.filters.push(dummyFilter);
+ const selectedOptionIds = getters.getSelectedOptionIds(state, mockedGetters(state))('dummy');
expect(selectedOptionIds).toHaveLength(1);
expect(selectedOptionIds[0]).toEqual('one');
@@ -76,7 +76,6 @@ describe('filters module getters', () => {
describe('getSelectedOptionNames', () => {
it('should return "All" as the selected option', () => {
- const state = createState();
const selectedOptionNames = getters.getSelectedOptionNames(state, mockedGetters(state))(
'severity',
);
@@ -85,7 +84,7 @@ describe('filters module getters', () => {
});
it('should return the correct message when multiple filters are selected', () => {
- const state = {
+ state = {
filters: [
{
id: 'severity',
@@ -103,22 +102,20 @@ describe('filters module getters', () => {
describe('activeFilters', () => {
it('should return no severity filters', () => {
- const state = createState();
const activeFilters = getters.activeFilters(state, mockedGetters(state));
expect(activeFilters.severity).toHaveLength(0);
});
- it('should return multiple project filters"', () => {
- const state = createState();
- const projectFilter = {
- id: 'project',
+ it('should return multiple dummy filters"', () => {
+ const dummyFilter = {
+ id: 'dummy',
options: [{ id: 'one', selected: true }, { id: 'anotherone', selected: true }],
};
- state.filters.push(projectFilter);
+ state.filters.push(dummyFilter);
const activeFilters = getters.activeFilters(state, mockedGetters(state));
- expect(activeFilters.project).toHaveLength(2);
+ expect(activeFilters.dummy).toHaveLength(2);
});
});
});
diff --git a/ee/spec/javascripts/security_dashboard/store/filters/mutations_spec.js b/ee/spec/javascripts/security_dashboard/store/filters/mutations_spec.js
index 6bca9e13c61c172bb4bceaf7d636a7e8fb0b4c5c..aff84fdaf8b49e80f3a8fac80e8ef95a545a8888 100644
--- a/ee/spec/javascripts/security_dashboard/store/filters/mutations_spec.js
+++ b/ee/spec/javascripts/security_dashboard/store/filters/mutations_spec.js
@@ -40,4 +40,22 @@ describe('filters module mutations', () => {
});
});
});
+
+ describe('SET_FILTER_OPTIONS', () => {
+ let state;
+ let firstFilter;
+ const options = [{ id: 0, name: 'c' }, { id: 3, name: 'c' }];
+
+ beforeEach(() => {
+ state = createState();
+ [firstFilter] = state.filters;
+ const filterId = firstFilter.id;
+
+ mutations[types.SET_FILTER_OPTIONS](state, { filterId, options });
+ });
+
+ it('should add all the options to the type filter', () => {
+ expect(firstFilter.options).toEqual(options);
+ });
+ });
});
diff --git a/ee/spec/javascripts/security_dashboard/store/projects/actions_spec.js b/ee/spec/javascripts/security_dashboard/store/projects/actions_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..d3ee398ffc46c8ed03d4f8f5e361cef30ed2ae21
--- /dev/null
+++ b/ee/spec/javascripts/security_dashboard/store/projects/actions_spec.js
@@ -0,0 +1,132 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import testAction from 'spec/helpers/vuex_action_helper';
+import { TEST_HOST } from 'spec/test_constants';
+
+import createState from 'ee/security_dashboard/store/modules/projects/state';
+import * as types from 'ee/security_dashboard/store/modules/projects/mutation_types';
+import * as actions from 'ee/security_dashboard/store/modules/projects/actions';
+
+import mockData from './data/mock_data.json';
+
+describe('projects actions', () => {
+ const data = mockData;
+ const endpoint = `${TEST_HOST}/projects.json`;
+
+ describe('fetchProjects', () => {
+ let mock;
+ const state = createState();
+
+ beforeEach(() => {
+ state.projectsEndpoint = endpoint;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('on success', () => {
+ beforeEach(() => {
+ mock.onGet(state.projectsEndpoint).replyOnce(200, data);
+ });
+
+ it('should dispatch the request and success actions', done => {
+ testAction(
+ actions.fetchProjects,
+ {},
+ state,
+ [],
+ [
+ { type: 'requestProjects' },
+ {
+ type: 'receiveProjectsSuccess',
+ payload: { projects: data },
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('on error', () => {
+ beforeEach(() => {
+ mock.onGet(state.projectsEndpoint).replyOnce(404, {});
+ });
+
+ it('should dispatch the request and error actions', done => {
+ testAction(
+ actions.fetchProjects,
+ {},
+ state,
+ [],
+ [{ type: 'requestProjects' }, { type: 'receiveProjectsError' }],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('receiveProjectsSuccess', () => {
+ it('should commit the success mutation', done => {
+ const state = createState();
+
+ testAction(
+ actions.receiveProjectsSuccess,
+ { projects: data },
+ state,
+ [
+ {
+ type: types.RECEIVE_PROJECTS_SUCCESS,
+ payload: { projects: data },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveProjectsError', () => {
+ it('should commit the error mutation', done => {
+ const state = createState();
+
+ testAction(
+ actions.receiveProjectsError,
+ {},
+ state,
+ [{ type: types.RECEIVE_PROJECTS_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestProjects', () => {
+ it('should commit the request mutation', done => {
+ const state = createState();
+
+ testAction(actions.requestProjects, {}, state, [{ type: types.REQUEST_PROJECTS }], [], done);
+ });
+ });
+
+ describe('setProjectsEndpoint', () => {
+ it('should commit the correct mutuation', done => {
+ const state = createState();
+
+ testAction(
+ actions.setProjectsEndpoint,
+ endpoint,
+ state,
+ [
+ {
+ type: types.SET_PROJECTS_ENDPOINT,
+ payload: endpoint,
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/ee/spec/javascripts/security_dashboard/store/projects/data/mock_data.json b/ee/spec/javascripts/security_dashboard/store/projects/data/mock_data.json
new file mode 100644
index 0000000000000000000000000000000000000000..e834a0c58f1c6158e88b9d993e170846a7b43463
--- /dev/null
+++ b/ee/spec/javascripts/security_dashboard/store/projects/data/mock_data.json
@@ -0,0 +1,39 @@
+[
+ {
+ "id": 9,
+ "description": "foo",
+ "default_branch": "master",
+ "tag_list": [],
+ "archived": false,
+ "visibility": "internal",
+ "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
+ "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
+ "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
+ "name": "Html5 Boilerplate",
+ "name_with_namespace": "Experimental / Html5 Boilerplate",
+ "path": "html5-boilerplate",
+ "path_with_namespace": "h5bp/html5-boilerplate",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "jobs_enabled": true,
+ "snippets_enabled": true,
+ "created_at": "2016-04-05T21:40:50.169Z",
+ "last_activity_at": "2016-04-06T16:52:08.432Z",
+ "shared_runners_enabled": true,
+ "creator_id": 1,
+ "namespace": {
+ "id": 5,
+ "name": "Experimental",
+ "path": "h5bp",
+ "kind": "group"
+ },
+ "avatar_url": null,
+ "star_count": 1,
+ "forks_count": 0,
+ "open_issues_count": 3,
+ "public_jobs": true,
+ "shared_with_groups": [],
+ "request_access_enabled": false
+ }
+]
diff --git a/ee/spec/javascripts/security_dashboard/store/projects/mutations_spec.js b/ee/spec/javascripts/security_dashboard/store/projects/mutations_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..df2b21dea2166ec551e70eb4411cf1a517f5ec00
--- /dev/null
+++ b/ee/spec/javascripts/security_dashboard/store/projects/mutations_spec.js
@@ -0,0 +1,72 @@
+import createState from 'ee/security_dashboard/store/modules/projects/state';
+import * as types from 'ee/security_dashboard/store/modules/projects/mutation_types';
+import mutations from 'ee/security_dashboard/store/modules/projects/mutations';
+import mockData from './data/mock_data.json';
+
+describe('projects module mutations', () => {
+ describe('SET_PROJECTS_ENDPOINT', () => {
+ it('should set `projectsEndpoint` to `fakepath.json`', () => {
+ const state = createState();
+ const endpoint = 'fakepath.json';
+
+ mutations[types.SET_PROJECTS_ENDPOINT](state, endpoint);
+
+ expect(state.projectsEndpoint).toEqual(endpoint);
+ });
+ });
+
+ describe('REQUEST_PROJECTS', () => {
+ let state;
+
+ beforeEach(() => {
+ state = {
+ ...createState(),
+ errorLoadingProjects: true,
+ };
+ mutations[types.REQUEST_PROJECTS](state);
+ });
+
+ it('should set `isLoadingProjects` to `true`', () => {
+ expect(state.isLoadingProjects).toBe(true);
+ });
+
+ it('should set `errorLoadingProjects` to `false`', () => {
+ expect(state.errorLoadingProjects).toBe(false);
+ });
+ });
+
+ describe('RECEIVE_PROJECTS_SUCCESS', () => {
+ let payload;
+ let state;
+
+ beforeEach(() => {
+ payload = {
+ projects: mockData,
+ };
+ state = createState();
+ mutations[types.RECEIVE_PROJECTS_SUCCESS](state, payload);
+ });
+
+ it('should set `isLoadingProjects` to `false`', () => {
+ expect(state.isLoadingProjects).toBe(false);
+ });
+
+ it('should set `pageInfo`', () => {
+ expect(state.pageInfo).toBe(payload.pageInfo);
+ });
+
+ it('should set `projects`', () => {
+ expect(state.projects).toBe(payload.projects);
+ });
+ });
+
+ describe('RECEIVE_PROJECTS_ERROR', () => {
+ it('should set `isLoadingProjects` to `false`', () => {
+ const state = createState();
+
+ mutations[types.RECEIVE_PROJECTS_ERROR](state);
+
+ expect(state.isLoadingProjects).toBe(false);
+ });
+ });
+});