Commit d8003b58 authored by Mark Florian's avatar Mark Florian Committed by Clement Ho

Add vulnerable dependency list

This adds a second dependency list which only displays dependencies with
vulnerabilities. The user is able to switch between the two lists via
the associated tabs.

This contributes towards the larger [feature][1] for adding dependency
scanning results to the Dependency List, behind the
`dependency_list_vulnerabilities` feature flag.

[1]: https://gitlab.com/gitlab-org/gitlab-ee/issues/10077
parent 7ed07685
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { GlBadge, GlEmptyState, GlLoadingIcon } from '@gitlab/ui'; import { GlBadge, GlEmptyState, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui';
import DependenciesActions from './dependencies_actions.vue'; import DependenciesActions from './dependencies_actions.vue';
import DependencyListIncompleteAlert from './dependency_list_incomplete_alert.vue'; import DependencyListIncompleteAlert from './dependency_list_incomplete_alert.vue';
import DependencyListJobFailedAlert from './dependency_list_job_failed_alert.vue'; import DependencyListJobFailedAlert from './dependency_list_job_failed_alert.vue';
...@@ -14,10 +14,18 @@ export default { ...@@ -14,10 +14,18 @@ export default {
GlBadge, GlBadge,
GlEmptyState, GlEmptyState,
GlLoadingIcon, GlLoadingIcon,
GlTab,
GlTabs,
DependencyListIncompleteAlert, DependencyListIncompleteAlert,
DependencyListJobFailedAlert, DependencyListJobFailedAlert,
PaginatedDependenciesTable, PaginatedDependenciesTable,
}, },
inject: {
dependencyListVulnerabilities: {
from: 'dependencyListVulnerabilities',
default: false,
},
},
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
...@@ -39,28 +47,47 @@ export default { ...@@ -39,28 +47,47 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['currentList']), ...mapState(['currentList', 'listTypes']),
...mapGetters(DEPENDENCY_LIST_TYPES.all, ['isJobNotSetUp', 'isJobFailed', 'isIncomplete']), ...mapGetters([
...mapState(DEPENDENCY_LIST_TYPES.all, ['initialized', 'pageInfo', 'reportInfo']), 'isInitialized',
'isJobNotSetUp',
'isJobFailed',
'isIncomplete',
'reportInfo',
'totals',
]),
...mapState(DEPENDENCY_LIST_TYPES.all.namespace, ['pageInfo']),
currentListIndex: {
get() {
return this.listTypes.map(({ namespace }) => namespace).indexOf(this.currentList);
},
set(index) {
const { namespace } = this.listTypes[index] || {};
this.setCurrentList(namespace);
},
},
}, },
created() { created() {
this.setDependenciesEndpoint(this.endpoint); this.setDependenciesEndpoint(this.endpoint);
this.fetchDependencies(); this.fetchDependencies();
}, },
methods: { methods: {
...mapActions(DEPENDENCY_LIST_TYPES.all, ['setDependenciesEndpoint', 'fetchDependencies']), ...mapActions(['setDependenciesEndpoint', 'fetchDependencies', 'setCurrentList']),
dismissIncompleteListAlert() { dismissIncompleteListAlert() {
this.isIncompleteAlertDismissed = true; this.isIncompleteAlertDismissed = true;
}, },
dismissJobFailedAlert() { dismissJobFailedAlert() {
this.isJobFailedAlertDismissed = true; this.isJobFailedAlertDismissed = true;
}, },
isTabDisabled(namespace) {
return this.totals[namespace] <= 0;
},
}, },
}; };
</script> </script>
<template> <template>
<gl-loading-icon v-if="!initialized" size="md" class="mt-4" /> <gl-loading-icon v-if="!isInitialized" size="md" class="mt-4" />
<gl-empty-state <gl-empty-state
v-else-if="isJobNotSetUp" v-else-if="isJobNotSetUp"
...@@ -85,15 +112,41 @@ export default { ...@@ -85,15 +112,41 @@ export default {
@close="dismissJobFailedAlert" @close="dismissJobFailedAlert"
/> />
<div class="d-sm-flex justify-content-between align-items-baseline my-2"> <template v-if="dependencyListVulnerabilities">
<h4 class="h5"> <h3 class="h5">{{ __('Dependencies') }}</h3>
{{ __('Dependencies') }}
<gl-badge v-if="pageInfo.total" pill>{{ pageInfo.total }}</gl-badge> <gl-tabs v-model="currentListIndex">
</h4> <gl-tab
v-for="listType in listTypes"
:key="listType.namespace"
:disabled="isTabDisabled(listType.namespace)"
title-link-class="js-tab"
>
<template v-slot:title>
{{ listType.label }}
<gl-badge pill>{{ totals[listType.namespace] }}</gl-badge>
</template>
<paginated-dependencies-table :namespace="listType.namespace" />
</gl-tab>
<template v-slot:tabs>
<li class="d-flex align-items-center ml-sm-auto">
<dependencies-actions :namespace="currentList" class="my-2 my-sm-0" />
</li>
</template>
</gl-tabs>
</template>
<template v-else>
<div class="d-sm-flex justify-content-between align-items-baseline my-2">
<h3 class="h5">
{{ __('Dependencies') }}
<gl-badge v-if="pageInfo.total" pill>{{ pageInfo.total }}</gl-badge>
</h3>
<dependencies-actions :namespace="currentList" /> <dependencies-actions :namespace="currentList" />
</div> </div>
<paginated-dependencies-table :namespace="currentList" /> <paginated-dependencies-table :namespace="currentList" />
</template>
</div> </div>
</template> </template>
...@@ -30,7 +30,8 @@ export default { ...@@ -30,7 +30,8 @@ export default {
namespace: { namespace: {
type: String, type: String,
required: true, required: true,
validator: value => Object.values(DEPENDENCY_LIST_TYPES).includes(value), validator: value =>
Object.values(DEPENDENCY_LIST_TYPES).some(({ namespace }) => value === namespace),
}, },
}, },
data() { data() {
......
...@@ -14,7 +14,8 @@ export default { ...@@ -14,7 +14,8 @@ export default {
namespace: { namespace: {
type: String, type: String,
required: true, required: true,
validator: value => Object.values(DEPENDENCY_LIST_TYPES).includes(value), validator: value =>
Object.values(DEPENDENCY_LIST_TYPES).some(({ namespace }) => value === namespace),
}, },
}, },
computed: { computed: {
......
import Vue from 'vue'; import Vue from 'vue';
import DependenciesApp from './components/app.vue'; import DependenciesApp from './components/app.vue';
import createStore from './store'; import createStore from './store';
import { DEPENDENCY_LIST_TYPES } from './store/constants';
import { addListType } from './store/utils';
export default () => { export default () => {
const el = document.querySelector('#js-dependencies-app'); const el = document.querySelector('#js-dependencies-app');
...@@ -9,6 +11,10 @@ export default () => { ...@@ -9,6 +11,10 @@ export default () => {
const store = createStore(); const store = createStore();
if (dependencyListVulnerabilities) {
addListType(store, DEPENDENCY_LIST_TYPES.vulnerable);
}
return new Vue({ return new Vue({
el, el,
store, store,
......
import * as types from './mutation_types';
export const addListType = ({ commit }, payload) => commit(types.ADD_LIST_TYPE, payload);
export const setDependenciesEndpoint = ({ state, dispatch }, endpoint) =>
Promise.all(
state.listTypes.map(({ namespace }) =>
dispatch(`${namespace}/setDependenciesEndpoint`, endpoint),
),
);
export const fetchDependencies = ({ state, dispatch }, payload) =>
Promise.all(
state.listTypes.map(({ namespace }) => dispatch(`${namespace}/fetchDependencies`, payload)),
);
export const setCurrentList = ({ state, commit }, payload) => {
if (state.listTypes.map(({ namespace }) => namespace).includes(payload)) {
commit(types.SET_CURRENT_LIST, payload);
}
};
import { s__ } from '~/locale';
import { FILTER } from './modules/list/constants';
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export const DEPENDENCY_LIST_TYPES = { export const DEPENDENCY_LIST_TYPES = {
all: 'allDependencies', all: {
namespace: 'allDependencies',
label: s__('Dependencies|All'),
initialState: {
filter: FILTER.all,
},
},
vulnerable: {
namespace: 'vulnerableDependencies',
label: s__('Dependencies|Vulnerable components'),
initialState: {
filter: FILTER.vulnerable,
},
},
}; };
export const isInitialized = ({ currentList, ...state }) => state[currentList].initialized;
export const reportInfo = ({ currentList, ...state }) => state[currentList].reportInfo;
export const isJobNotSetUp = ({ currentList }, getters) => getters[`${currentList}/isJobNotSetUp`];
export const isJobFailed = ({ currentList }, getters) => getters[`${currentList}/isJobFailed`];
export const isIncomplete = ({ currentList }, getters) => getters[`${currentList}/isIncomplete`];
export const totals = state =>
state.listTypes.reduce(
(acc, { namespace }) => ({
...acc,
[namespace]: state[namespace].pageInfo.total || 0,
}),
{},
);
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import listModule from './modules/list'; import listModule from './modules/list';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state'; import state from './state';
import { DEPENDENCY_LIST_TYPES } from './constants';
Vue.use(Vuex); Vue.use(Vuex);
export default () => { export default () =>
const allDependencies = listModule(); new Vuex.Store({
return new Vuex.Store({
modules: { modules: {
allDependencies, [DEPENDENCY_LIST_TYPES.all.namespace]: listModule(),
}, },
actions,
getters,
mutations,
state, state,
}); });
};
...@@ -9,6 +9,8 @@ import { __ } from '~/locale'; ...@@ -9,6 +9,8 @@ import { __ } from '~/locale';
export const setDependenciesEndpoint = ({ commit }, endpoint) => export const setDependenciesEndpoint = ({ commit }, endpoint) =>
commit(types.SET_DEPENDENCIES_ENDPOINT, endpoint); commit(types.SET_DEPENDENCIES_ENDPOINT, endpoint);
export const setInitialState = ({ commit }, payload) => commit(types.SET_INITIAL_STATE, payload);
export const requestDependencies = ({ commit }) => commit(types.REQUEST_DEPENDENCIES); export const requestDependencies = ({ commit }) => commit(types.REQUEST_DEPENDENCIES);
export const receiveDependenciesSuccess = ({ commit }, { headers, data }) => { export const receiveDependenciesSuccess = ({ commit }, { headers, data }) => {
...@@ -35,6 +37,7 @@ export const fetchDependencies = ({ state, dispatch }, params = {}) => { ...@@ -35,6 +37,7 @@ export const fetchDependencies = ({ state, dispatch }, params = {}) => {
sort_by: state.sortField, sort_by: state.sortField,
sort: state.sortOrder, sort: state.sortOrder,
page: state.pageInfo.page || 1, page: state.pageInfo.page || 1,
filter: state.filter,
...params, ...params,
}, },
}) })
......
...@@ -23,6 +23,11 @@ export const REPORT_STATUS = { ...@@ -23,6 +23,11 @@ export const REPORT_STATUS = {
incomplete: 'no_dependency_files', incomplete: 'no_dependency_files',
}; };
export const FILTER = {
all: 'all',
vulnerable: 'vulnerable',
};
export const FETCH_ERROR_MESSAGE = __( export const FETCH_ERROR_MESSAGE = __(
'Error fetching the dependency list. Please check your network connection and try again.', 'Error fetching the dependency list. Please check your network connection and try again.',
); );
export const SET_DEPENDENCIES_ENDPOINT = 'SET_DEPENDENCIES_ENDPOINT'; export const SET_DEPENDENCIES_ENDPOINT = 'SET_DEPENDENCIES_ENDPOINT';
export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
export const REQUEST_DEPENDENCIES = 'REQUEST_DEPENDENCIES'; export const REQUEST_DEPENDENCIES = 'REQUEST_DEPENDENCIES';
export const RECEIVE_DEPENDENCIES_SUCCESS = 'RECEIVE_DEPENDENCIES_SUCCESS'; export const RECEIVE_DEPENDENCIES_SUCCESS = 'RECEIVE_DEPENDENCIES_SUCCESS';
......
...@@ -5,6 +5,9 @@ export default { ...@@ -5,6 +5,9 @@ export default {
[types.SET_DEPENDENCIES_ENDPOINT](state, payload) { [types.SET_DEPENDENCIES_ENDPOINT](state, payload) {
state.endpoint = payload; state.endpoint = payload;
}, },
[types.SET_INITIAL_STATE](state, payload) {
Object.assign(state, payload);
},
[types.REQUEST_DEPENDENCIES](state) { [types.REQUEST_DEPENDENCIES](state) {
state.isLoading = true; state.isLoading = true;
state.errorLoading = false; state.errorLoading = false;
......
import { REPORT_STATUS, SORT_ORDER } from './constants'; import { FILTER, REPORT_STATUS, SORT_ORDER } from './constants';
export default () => ({ export default () => ({
endpoint: '', endpoint: '',
...@@ -6,11 +6,14 @@ export default () => ({ ...@@ -6,11 +6,14 @@ export default () => ({
isLoading: false, isLoading: false,
errorLoading: false, errorLoading: false,
dependencies: [], dependencies: [],
pageInfo: {}, pageInfo: {
total: 0,
},
reportInfo: { reportInfo: {
status: REPORT_STATUS.ok, status: REPORT_STATUS.ok,
jobPath: '', jobPath: '',
}, },
filter: FILTER.all,
sortField: 'name', sortField: 'name',
sortOrder: SORT_ORDER.ascending, sortOrder: SORT_ORDER.ascending,
}); });
export const ADD_LIST_TYPE = 'ADD_LIST_TYPE';
export const SET_CURRENT_LIST = 'SET_CURRENT_LIST';
import * as types from './mutation_types';
export default {
[types.ADD_LIST_TYPE](state, payload) {
state.listTypes.push(payload);
},
[types.SET_CURRENT_LIST](state, payload) {
state.currentList = payload;
},
};
import { DEPENDENCY_LIST_TYPES } from './constants'; import { DEPENDENCY_LIST_TYPES } from './constants';
export default () => ({ export default () => ({
currentList: DEPENDENCY_LIST_TYPES.all, listTypes: [DEPENDENCY_LIST_TYPES.all],
currentList: DEPENDENCY_LIST_TYPES.all.namespace,
}); });
import listModule from './modules/list';
// eslint-disable-next-line import/prefer-default-export
export const addListType = (store, listType) => {
const { initialState, namespace } = listType;
store.registerModule(namespace, listModule());
store.dispatch('addListType', listType);
store.dispatch(`${namespace}/setInitialState`, initialState);
};
...@@ -13,8 +13,8 @@ exports[`DependenciesApp component on creation given a dependency list which is ...@@ -13,8 +13,8 @@ exports[`DependenciesApp component on creation given a dependency list which is
class="h5" class="h5"
> >
Dependencies Dependencies
<glbadge-stub <glbadge-stub
pill="" pill=""
> >
...@@ -46,8 +46,8 @@ exports[`DependenciesApp component on creation given a fetch error matches the s ...@@ -46,8 +46,8 @@ exports[`DependenciesApp component on creation given a fetch error matches the s
class="h5" class="h5"
> >
Dependencies Dependencies
<!----> <!---->
</h4> </h4>
...@@ -75,8 +75,8 @@ exports[`DependenciesApp component on creation given a list of dependencies and ...@@ -75,8 +75,8 @@ exports[`DependenciesApp component on creation given a list of dependencies and
class="h5" class="h5"
> >
Dependencies Dependencies
<glbadge-stub <glbadge-stub
pill="" pill=""
> >
...@@ -110,8 +110,8 @@ exports[`DependenciesApp component on creation given the dependency list job fai ...@@ -110,8 +110,8 @@ exports[`DependenciesApp component on creation given the dependency list job fai
class="h5" class="h5"
> >
Dependencies Dependencies
<!----> <!---->
</h4> </h4>
......
...@@ -16,7 +16,7 @@ describe.each` ...@@ -16,7 +16,7 @@ describe.each`
`('DependenciesActions component$context', ({ isFeatureFlagEnabled, sortFields }) => { `('DependenciesActions component$context', ({ isFeatureFlagEnabled, sortFields }) => {
let store; let store;
let wrapper; let wrapper;
const listType = DEPENDENCY_LIST_TYPES.all; const { namespace } = DEPENDENCY_LIST_TYPES.all;
const factory = ({ propsData, ...options } = {}) => { const factory = ({ propsData, ...options } = {}) => {
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -35,10 +35,10 @@ describe.each` ...@@ -35,10 +35,10 @@ describe.each`
beforeEach(() => { beforeEach(() => {
factory({ factory({
propsData: { namespace: listType }, propsData: { namespace },
provide: { dependencyListVulnerabilities: isFeatureFlagEnabled }, provide: { dependencyListVulnerabilities: isFeatureFlagEnabled },
}); });
store.state[listType].endpoint = `${TEST_HOST}/dependencies`; store.state[namespace].endpoint = `${TEST_HOST}/dependencies`;
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
...@@ -61,7 +61,7 @@ describe.each` ...@@ -61,7 +61,7 @@ describe.each`
expect(store.dispatch.mock.calls).toEqual( expect(store.dispatch.mock.calls).toEqual(
expect.arrayContaining( expect.arrayContaining(
Object.keys(sortFields).map(field => [`${listType}/setSortField`, field]), Object.keys(sortFields).map(field => [`${namespace}/setSortField`, field]),
), ),
); );
}); });
...@@ -69,14 +69,14 @@ describe.each` ...@@ -69,14 +69,14 @@ describe.each`
it('dispatches the toggleSortOrder action on clicking the sort order button', () => { it('dispatches the toggleSortOrder action on clicking the sort order button', () => {
const sortButton = wrapper.find('.js-sort-order'); const sortButton = wrapper.find('.js-sort-order');
sortButton.vm.$emit('click'); sortButton.vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith(`${listType}/toggleSortOrder`); expect(store.dispatch).toHaveBeenCalledWith(`${namespace}/toggleSortOrder`);
}); });
it('has a button to export the dependency list', () => { it('has a button to export the dependency list', () => {
const download = wrapper.find('.js-download'); const download = wrapper.find('.js-download');
expect(download.attributes()).toEqual( expect(download.attributes()).toEqual(
expect.objectContaining({ expect.objectContaining({
href: store.getters[`${listType}/downloadEndpoint`], href: store.getters[`${namespace}/downloadEndpoint`],
download: expect.any(String), download: expect.any(String),
}), }),
); );
......
...@@ -9,7 +9,7 @@ import mockDependenciesResponse from '../store/modules/list/data/mock_dependenci ...@@ -9,7 +9,7 @@ import mockDependenciesResponse from '../store/modules/list/data/mock_dependenci
describe('PaginatedDependenciesTable component', () => { describe('PaginatedDependenciesTable component', () => {
let store; let store;
let wrapper; let wrapper;
const listType = DEPENDENCY_LIST_TYPES.all; const { namespace } = DEPENDENCY_LIST_TYPES.all;
const factory = (props = {}) => { const factory = (props = {}) => {
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -31,11 +31,11 @@ describe('PaginatedDependenciesTable component', () => { ...@@ -31,11 +31,11 @@ describe('PaginatedDependenciesTable component', () => {
}; };
beforeEach(() => { beforeEach(() => {
factory({ namespace: listType }); factory({ namespace });
const originalDispatch = store.dispatch; const originalDispatch = store.dispatch;
jest.spyOn(store, 'dispatch').mockImplementation(); jest.spyOn(store, 'dispatch').mockImplementation();
originalDispatch(`${listType}/receiveDependenciesSuccess`, { originalDispatch(`${namespace}/receiveDependenciesSuccess`, {
data: mockDependenciesResponse, data: mockDependenciesResponse,
headers: { 'X-Total': mockDependenciesResponse.dependencies.length }, headers: { 'X-Total': mockDependenciesResponse.dependencies.length },
}); });
...@@ -50,14 +50,14 @@ describe('PaginatedDependenciesTable component', () => { ...@@ -50,14 +50,14 @@ describe('PaginatedDependenciesTable component', () => {
it('passes the correct props to the dependencies table', () => { it('passes the correct props to the dependencies table', () => {
expectComponentWithProps(DependenciesTable, { expectComponentWithProps(DependenciesTable, {
dependencies: mockDependenciesResponse.dependencies, dependencies: mockDependenciesResponse.dependencies,
isLoading: store.state[listType].isLoading, isLoading: store.state[namespace].isLoading,
}); });
}); });
it('passes the correct props to the pagination', () => { it('passes the correct props to the pagination', () => {
expectComponentWithProps(Pagination, { expectComponentWithProps(Pagination, {
change: wrapper.vm.fetchPage, change: wrapper.vm.fetchPage,
pageInfo: store.state[listType].pageInfo, pageInfo: store.state[namespace].pageInfo,
}); });
}); });
...@@ -65,7 +65,7 @@ describe('PaginatedDependenciesTable component', () => { ...@@ -65,7 +65,7 @@ describe('PaginatedDependenciesTable component', () => {
const page = 2; const page = 2;
wrapper.vm.fetchPage(page); wrapper.vm.fetchPage(page);
expect(store.dispatch).toHaveBeenCalledTimes(1); expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith(`${listType}/fetchDependencies`, { page }); expect(store.dispatch).toHaveBeenCalledWith(`${namespace}/fetchDependencies`, { page });
}); });
describe.each` describe.each`
...@@ -77,7 +77,7 @@ describe('PaginatedDependenciesTable component', () => { ...@@ -77,7 +77,7 @@ describe('PaginatedDependenciesTable component', () => {
let module; let module;
beforeEach(() => { beforeEach(() => {
module = store.state[listType]; module = store.state[namespace];
if (isListEmpty) { if (isListEmpty) {
module.dependencies = []; module.dependencies = [];
module.pageInfo.total = 0; module.pageInfo.total = 0;
......
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants';
import * as actions from 'ee/dependencies/store/actions';
import * as types from 'ee/dependencies/store/mutation_types';
import createState from 'ee/dependencies/store/state';
import { DEPENDENCY_LIST_TYPES } from 'ee/dependencies/store/constants';
describe('Dependencies actions', () => {
describe('addListType', () => {
it('commits the ADD_LIST_TYPE mutation', () => {
const payload = DEPENDENCY_LIST_TYPES.vulnerable;
return testAction(
actions.addListType,
payload,
createState(),
[
{
type: types.ADD_LIST_TYPE,
payload,
},
],
[],
);
});
});
describe.each`
actionName | payload
${'setDependenciesEndpoint'} | ${TEST_HOST}
${'fetchDependencies'} | ${undefined}
`('$actionName', ({ actionName, payload }) => {
it(`dispatches the ${actionName} action on each list module`, () => {
const state = createState();
state.listTypes.push({ namespace: 'foo' });
return testAction(
actions[actionName],
payload,
state,
[],
[
{
type: `allDependencies/${actionName}`,
payload,
},
{
type: `foo/${actionName}`,
payload,
},
],
);
});
});
describe('setCurrentList', () => {
let payload;
let state;
beforeEach(() => {
state = {
listTypes: [{ namespace: 'foo' }, { namespace: 'bar' }],
};
});
describe('given an existing namespace', () => {
beforeEach(() => {
payload = 'bar';
});
it('commits the SET_CURRENT_LIST mutation if given a valid list', () =>
testAction(
actions.setCurrentList,
payload,
state,
[
{
type: types.SET_CURRENT_LIST,
payload: 'bar',
},
],
[],
));
});
describe('given a non-existent namespace', () => {
beforeEach(() => {
payload = 'qux';
});
it('does nothing', () => testAction(actions.setCurrentList, payload, state, [], []));
});
});
});
import * as getters from 'ee/dependencies/store/getters';
describe('Dependencies getters', () => {
describe.each`
getterName | propertyName
${'isInitialized'} | ${'initialized'}
${'reportInfo'} | ${'reportInfo'}
`('$getterName', ({ getterName, propertyName }) => {
it(`returns the value from the current list module's state`, () => {
const mockValue = {};
const state = {
listFoo: {
[propertyName]: mockValue,
},
currentList: 'listFoo',
};
expect(getters[getterName](state)).toBe(mockValue);
});
});
describe.each`
getterName
${'isJobNotSetUp'}
${'isJobFailed'}
${'isIncomplete'}
`('$getterName', ({ getterName }) => {
it(`delegates to the current list module's ${getterName} getter`, () => {
const mockValue = {};
const currentList = 'fooList';
const state = { currentList };
const rootGetters = {
[`${currentList}/${getterName}`]: mockValue,
};
expect(getters[getterName](state, rootGetters)).toBe(mockValue);
});
});
describe('totals', () => {
it('returns a map of list module namespaces to total counts', () => {
const state = {
listTypes: [
{ namespace: 'foo' },
{ namespace: 'bar' },
{ namespace: 'qux' },
{ namespace: 'foobar' },
],
foo: { pageInfo: { total: 1 } },
bar: { pageInfo: { total: 2 } },
qux: { pageInfo: { total: NaN } },
foobar: { pageInfo: {} },
};
expect(getters.totals(state)).toEqual({
foo: 1,
bar: 2,
qux: 0,
foobar: 0,
});
});
});
});
...@@ -6,7 +6,11 @@ import { TEST_HOST } from 'helpers/test_constants'; ...@@ -6,7 +6,11 @@ import { TEST_HOST } from 'helpers/test_constants';
import * as actions from 'ee/dependencies/store/modules/list/actions'; import * as actions from 'ee/dependencies/store/modules/list/actions';
import * as types from 'ee/dependencies/store/modules/list/mutation_types'; import * as types from 'ee/dependencies/store/modules/list/mutation_types';
import getInitialState from 'ee/dependencies/store/modules/list/state'; import getInitialState from 'ee/dependencies/store/modules/list/state';
import { SORT_ORDER, FETCH_ERROR_MESSAGE } from 'ee/dependencies/store/modules/list/constants'; import {
FILTER,
SORT_ORDER,
FETCH_ERROR_MESSAGE,
} from 'ee/dependencies/store/modules/list/constants';
import createFlash from '~/flash'; import createFlash from '~/flash';
import mockDependenciesResponse from './data/mock_dependencies'; import mockDependenciesResponse from './data/mock_dependencies';
...@@ -52,6 +56,25 @@ describe('Dependencies actions', () => { ...@@ -52,6 +56,25 @@ describe('Dependencies actions', () => {
)); ));
}); });
describe('setInitialState', () => {
it('commits the SET_INITIAL_STATE mutation', () => {
const payload = { filter: 'foo' };
return testAction(
actions.setInitialState,
payload,
getInitialState(),
[
{
type: types.SET_INITIAL_STATE,
payload,
},
],
[],
);
});
});
describe('requestDependencies', () => { describe('requestDependencies', () => {
it('commits the REQUEST_DEPENDENCIES mutation', () => it('commits the REQUEST_DEPENDENCIES mutation', () =>
testAction( testAction(
...@@ -141,6 +164,7 @@ describe('Dependencies actions', () => { ...@@ -141,6 +164,7 @@ describe('Dependencies actions', () => {
sort_by: state.sortField, sort_by: state.sortField,
sort: state.sortOrder, sort: state.sortOrder,
page: state.pageInfo.page, page: state.pageInfo.page,
filter: state.filter,
}; };
mock mock
...@@ -167,7 +191,12 @@ describe('Dependencies actions', () => { ...@@ -167,7 +191,12 @@ describe('Dependencies actions', () => {
}); });
describe('given params', () => { describe('given params', () => {
const paramsGiven = { sort_by: 'packager', sort: SORT_ORDER.descending, page: 4 }; const paramsGiven = {
sort_by: 'packager',
sort: SORT_ORDER.descending,
page: 4,
filter: FILTER.vulnerable,
};
beforeEach(() => { beforeEach(() => {
mock mock
......
...@@ -19,6 +19,15 @@ describe('Dependencies mutations', () => { ...@@ -19,6 +19,15 @@ describe('Dependencies mutations', () => {
}); });
}); });
describe(types.SET_INITIAL_STATE, () => {
it('correctly mutates the state', () => {
const filter = 'foo';
mutations[types.SET_INITIAL_STATE](state, { filter });
expect(state.filter).toBe(filter);
});
});
describe(types.REQUEST_DEPENDENCIES, () => { describe(types.REQUEST_DEPENDENCIES, () => {
beforeEach(() => { beforeEach(() => {
mutations[types.REQUEST_DEPENDENCIES](state); mutations[types.REQUEST_DEPENDENCIES](state);
......
import * as types from 'ee/dependencies/store/mutation_types';
import mutations from 'ee/dependencies/store/mutations';
import getInitialState from 'ee/dependencies/store/state';
import { DEPENDENCY_LIST_TYPES } from 'ee/dependencies/store/constants';
describe('Dependencies mutations', () => {
let state;
beforeEach(() => {
state = getInitialState();
});
describe(types.ADD_LIST_TYPE, () => {
it('adds the given module type to the list of modules', () => {
const typeToAdd = DEPENDENCY_LIST_TYPES.vulnerable;
mutations[types.ADD_LIST_TYPE](state, typeToAdd);
const lastAdded = state.listTypes[state.listTypes.length - 1];
expect(lastAdded).toEqual(typeToAdd);
});
});
describe(types.SET_CURRENT_LIST, () => {
it('sets the current list namespace', () => {
const namespace = 'foobar';
mutations[types.SET_CURRENT_LIST](state, namespace);
expect(state.currentList).toBe(namespace);
});
});
});
import { addListType } from 'ee/dependencies/store/utils';
import listModule from 'ee/dependencies/store/modules/list';
const mockModule = { mock: true };
jest.mock('ee/dependencies/store/modules/list', () => ({
// `__esModule: true` is required when mocking modules with default exports:
// https://jestjs.io/docs/en/jest-object#jestmockmodulename-factory-options
__esModule: true,
default: jest.fn(() => mockModule),
}));
describe('Dependencies store utils', () => {
describe('addListType', () => {
it('calls the correct store methods', () => {
const store = {
dispatch: jest.fn(),
registerModule: jest.fn(),
};
const listType = {
namespace: 'foo',
initialState: { bar: true },
};
addListType(store, listType);
expect(listModule).toHaveBeenCalled();
expect(store.registerModule.mock.calls).toEqual([[listType.namespace, mockModule]]);
expect(store.dispatch.mock.calls).toEqual([
['addListType', listType],
[`${listType.namespace}/setInitialState`, listType.initialState],
]);
});
});
});
...@@ -4513,6 +4513,9 @@ msgid_plural "Dependencies|%d vulnerabilities" ...@@ -4513,6 +4513,9 @@ msgid_plural "Dependencies|%d vulnerabilities"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Dependencies|All"
msgstr ""
msgid "Dependencies|Component" msgid "Dependencies|Component"
msgstr "" msgstr ""
...@@ -4546,6 +4549,9 @@ msgstr "" ...@@ -4546,6 +4549,9 @@ msgstr ""
msgid "Dependencies|Version" msgid "Dependencies|Version"
msgstr "" msgstr ""
msgid "Dependencies|Vulnerable components"
msgstr ""
msgid "Dependency List" msgid "Dependency List"
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment