Commit 84c5d346 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 7c7bd057 ae3776b8
...@@ -108,7 +108,7 @@ export default { ...@@ -108,7 +108,7 @@ export default {
show show
:target="target" :target="target"
placement="right" placement="right"
trigger="manual" triggers="manual"
container="viewport" container="viewport"
:css-classes="['suggest-gitlab-ci-yml', 'ml-4']" :css-classes="['suggest-gitlab-ci-yml', 'ml-4']"
> >
......
...@@ -5,7 +5,6 @@ import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form ...@@ -5,7 +5,6 @@ import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form
import { ListType } from '~/boards/constants'; import { ListType } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store'; import boardsStore from '~/boards/stores/boards_store';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils';
export default { export default {
components: { components: {
...@@ -20,17 +19,12 @@ export default { ...@@ -20,17 +19,12 @@ export default {
data() { data() {
return { return {
selectedId: null, selectedId: null,
selectedLabel: null,
}; };
}, },
computed: { computed: {
...mapState(['labels', 'labelsLoading']), ...mapState(['labels', 'labelsLoading']),
...mapGetters(['getListByLabelId', 'shouldUseGraphQL']), ...mapGetters(['getListByLabelId', 'shouldUseGraphQL']),
selectedLabel() {
if (!this.selectedId) {
return null;
}
return this.labels.find(({ id }) => id === this.selectedId);
},
columnForSelected() { columnForSelected() {
return this.getListByLabelId(this.selectedId); return this.getListByLabelId(this.selectedId);
}, },
...@@ -83,8 +77,13 @@ export default { ...@@ -83,8 +77,13 @@ export default {
this.fetchLabels(searchTerm); this.fetchLabels(searchTerm);
}, },
showScopedLabels(label) { setSelectedItem(selectedId) {
return this.scopedLabelsAvailable && isScopedLabel(label); const label = this.labels.find(({ id }) => id === selectedId);
if (!selectedId || !label) {
this.selectedLabel = null;
} else {
this.selectedLabel = { ...label };
}
}, },
}, },
}; };
...@@ -116,6 +115,7 @@ export default { ...@@ -116,6 +115,7 @@ export default {
v-if="labels.length > 0" v-if="labels.length > 0"
v-model="selectedId" v-model="selectedId"
class="gl-overflow-y-auto gl-px-5 gl-pt-3" class="gl-overflow-y-auto gl-px-5 gl-pt-3"
@change="setSelectedItem"
> >
<label <label
v-for="label in labels" v-for="label in labels"
......
...@@ -21,28 +21,13 @@ class GroupMembersFinder < UnionFinder ...@@ -21,28 +21,13 @@ class GroupMembersFinder < UnionFinder
end end
def execute(include_relations: DEFAULT_RELATIONS) def execute(include_relations: DEFAULT_RELATIONS)
group_members = group_members_list return filter_members(group_members_list) if include_relations == [:direct]
relations = []
return filter_members(group_members) if include_relations == [:direct] groups = groups_by_relations(include_relations)
return GroupMember.none unless groups
relations << group_members if include_relations.include?(:direct) members = all_group_members(groups).distinct_on_user_with_max_access_level
if include_relations.include?(:inherited) && group.parent
parents_members = relation_group_members(group.ancestors)
relations << parents_members
end
if include_relations.include?(:descendants)
descendant_members = relation_group_members(group.descendants)
relations << descendant_members
end
return GroupMember.none if relations.empty?
members = find_union(relations, GroupMember)
filter_members(members) filter_members(members)
end end
...@@ -50,6 +35,25 @@ class GroupMembersFinder < UnionFinder ...@@ -50,6 +35,25 @@ class GroupMembersFinder < UnionFinder
attr_reader :user, :group attr_reader :user, :group
def groups_by_relations(include_relations)
case include_relations.sort
when [:inherited]
group.ancestors
when [:descendants]
group.descendants
when [:direct, :inherited]
group.self_and_ancestors
when [:descendants, :direct]
group.self_and_descendants
when [:descendants, :inherited]
find_union([group.ancestors, group.descendants], Group)
when [:descendants, :direct, :inherited]
group.self_and_hierarchy
else
nil
end
end
def filter_members(members) def filter_members(members)
members = members.search(params[:search]) if params[:search].present? members = members.search(params[:search]) if params[:search].present?
members = members.sort_by_attribute(params[:sort]) if params[:sort].present? members = members.sort_by_attribute(params[:sort]) if params[:sort].present?
...@@ -69,17 +73,13 @@ class GroupMembersFinder < UnionFinder ...@@ -69,17 +73,13 @@ class GroupMembersFinder < UnionFinder
group.members group.members
end end
def relation_group_members(relation) def all_group_members(groups)
all_group_members(relation).non_minimal_access members_of_groups(groups).non_minimal_access
end end
# rubocop: disable CodeReuse/ActiveRecord def members_of_groups(groups)
def all_group_members(relation) GroupMember.non_request.of_groups(groups)
GroupMember.non_request
.where(source_id: relation.select(:id))
.where.not(user_id: group.users.select(:id))
end end
# rubocop: enable CodeReuse/ActiveRecord
end end
GroupMembersFinder.prepend_if_ee('EE::GroupMembersFinder') GroupMembersFinder.prepend_if_ee('EE::GroupMembersFinder')
...@@ -137,6 +137,12 @@ class Member < ApplicationRecord ...@@ -137,6 +137,12 @@ class Member < ApplicationRecord
scope :with_source_id, ->(source_id) { where(source_id: source_id) } scope :with_source_id, ->(source_id) { where(source_id: source_id) }
scope :including_source, -> { includes(:source) } scope :including_source, -> { includes(:source) }
scope :distinct_on_user_with_max_access_level, -> do
distinct_members = select('DISTINCT ON (user_id, invite_email) *')
.order('user_id, invite_email, access_level DESC, expires_at DESC, created_at ASC')
Member.from(distinct_members, :members)
end
scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) } scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) } scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) } scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) }
......
---
title: Fix derivation of effective permissions (access level) of group members
merge_request: 56677
author: Jonas Wälter @wwwjon
type: fixed
...@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323891 ...@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323891
milestone: '13.10' milestone: '13.10'
type: development type: development
group: group::pipeline authoring group: group::pipeline authoring
default_enabled: false default_enabled: true
...@@ -90,8 +90,8 @@ Example response: ...@@ -90,8 +90,8 @@ Example response:
Gets a list of group or project members viewable by the authenticated user, including inherited members and permissions through ancestor groups. Gets a list of group or project members viewable by the authenticated user, including inherited members and permissions through ancestor groups.
WARNING: If a user is a member of this group or project and also of one or more ancestor groups, only its membership with the highest `access_level` is returned.
Due to [an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/249523), the users effective `access_level` may actually be higher than returned value when listing group members. This represents the effective permission of the user.
This function takes pagination parameters `page` and `per_page` to restrict the list of users. This function takes pagination parameters `page` and `per_page` to restrict the list of users.
......
...@@ -2149,10 +2149,11 @@ To download artifacts from a job in the current pipeline, use the basic form of ...@@ -2149,10 +2149,11 @@ To download artifacts from a job in the current pipeline, use the basic form of
#### Optional `needs` #### Optional `needs`
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30680) in GitLab 13.10. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30680) in GitLab 13.10.
> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default. > - [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
> - It's disabled on GitLab.com. > - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/323891) in GitLab 13.11.
> - It's not recommended for production use. > - Enabled on GitLab.com.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-optional-needs). **(FREE SELF)** > - Recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-optional-needs). **(FREE SELF)**
WARNING: WARNING:
This feature might not be available to you. Check the **version history** note above for details. This feature might not be available to you. Check the **version history** note above for details.
...@@ -2191,10 +2192,10 @@ rspec: ...@@ -2191,10 +2192,10 @@ rspec:
#### Enable or disable optional needs **(FREE SELF)** #### Enable or disable optional needs **(FREE SELF)**
Optional needs is under development and not ready for production use. It is Optional needs is under development but ready for production use.
deployed behind a feature flag that is **disabled by default**. It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) [GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can enable it. can opt to disable it.
To enable it: To enable it:
......
...@@ -72,6 +72,7 @@ export default { ...@@ -72,6 +72,7 @@ export default {
data() { data() {
return { return {
selectedId: null, selectedId: null,
selectedItem: null,
columnType: ListType.label, columnType: ListType.label,
}; };
}, },
...@@ -113,10 +114,6 @@ export default { ...@@ -113,10 +114,6 @@ export default {
return this.columnType === ListType.iteration; return this.columnType === ListType.iteration;
}, },
selectedItem() {
return this.items.find(({ id }) => id === this.selectedId);
},
hasLabelSelection() { hasLabelSelection() {
return this.labelTypeSelected && this.selectedItem; return this.labelTypeSelected && this.selectedItem;
}, },
...@@ -262,11 +259,17 @@ export default { ...@@ -262,11 +259,17 @@ export default {
setColumnType(type) { setColumnType(type) {
this.columnType = type; this.columnType = type;
this.selectedId = null; this.selectedId = null;
this.setSelectedItem(null);
this.filterItems(); this.filterItems();
}, },
hideDropdown() { setSelectedItem(selectedId) {
this.$root.$emit('bv::dropdown::hide'); const item = this.items.find(({ id }) => id === selectedId);
if (!selectedId || !item) {
this.selectedItem = null;
} else {
this.selectedItem = { ...item };
}
}, },
}, },
}; };
...@@ -337,7 +340,8 @@ export default { ...@@ -337,7 +340,8 @@ export default {
<gl-form-radio-group <gl-form-radio-group
v-model="selectedId" v-model="selectedId"
class="gl-overflow-y-auto gl-px-5" class="gl-overflow-y-auto gl-px-5"
@change="hideDropdown" data-testid="selectItem"
@change="setSelectedItem"
> >
<label <label
v-for="item in items" v-for="item in items"
......
<script> <script>
import { GlPopover, GlIcon, GlLink, GlButton, GlTooltipDirective } from '@gitlab/ui'; import { GlPopover, GlIcon, GlLink, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import AccessorUtils from '~/lib/utils/accessor';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
import download from '~/lib/utils/downloader'; import download from '~/lib/utils/downloader';
...@@ -19,12 +20,7 @@ export default { ...@@ -19,12 +20,7 @@ export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
props: { inject: ['vulnerabilitiesExportEndpoint'],
vulnerabilitiesExportEndpoint: {
type: String,
required: true,
},
},
data() { data() {
return { return {
isPreparingCsvExport: false, isPreparingCsvExport: false,
...@@ -45,10 +41,8 @@ export default { ...@@ -45,10 +41,8 @@ export default {
closePopover() { closePopover() {
this.showPopover = false; this.showPopover = false;
try { if (AccessorUtils.isLocalStorageAccessSafe()) {
localStorage.setItem(STORAGE_KEY, 'true'); localStorage.setItem(STORAGE_KEY, 'true');
} catch (e) {
// Ignore the error - this is just a safety measure.
} }
}, },
initiateCsvExport() { initiateCsvExport() {
...@@ -87,9 +81,10 @@ export default { ...@@ -87,9 +81,10 @@ export default {
> >
{{ __('Export') }} {{ __('Export') }}
<gl-popover <gl-popover
v-if="showPopover"
ref="popover" ref="popover"
:target="() => $refs.csvExportButton.$el" :target="() => $refs.csvExportButton.$el"
:show="showPopover" show
placement="left" placement="left"
triggers="manual" triggers="manual"
> >
......
...@@ -20,12 +20,6 @@ export default { ...@@ -20,12 +20,6 @@ export default {
VulnerabilitiesCountList, VulnerabilitiesCountList,
}, },
inject: ['groupFullPath'], inject: ['groupFullPath'],
props: {
vulnerabilitiesExportEndpoint: {
type: String,
required: true,
},
},
apollo: { apollo: {
projects: { projects: {
query: vulnerableProjectsQuery, query: vulnerableProjectsQuery,
...@@ -74,7 +68,7 @@ export default { ...@@ -74,7 +68,7 @@ export default {
<h2 class="gl-flex-grow-1 gl-my-0"> <h2 class="gl-flex-grow-1 gl-my-0">
{{ s__('SecurityReports|Vulnerability Report') }} {{ s__('SecurityReports|Vulnerability Report') }}
</h2> </h2>
<csv-export-button :vulnerabilities-export-endpoint="vulnerabilitiesExportEndpoint" /> <csv-export-button />
</header> </header>
<vulnerabilities-count-list <vulnerabilities-count-list
:scope="$options.vulnerabilitiesSeverityCountScopes.group" :scope="$options.vulnerabilitiesSeverityCountScopes.group"
......
...@@ -19,12 +19,6 @@ export default { ...@@ -19,12 +19,6 @@ export default {
DashboardNotConfigured, DashboardNotConfigured,
VulnerabilitiesCountList, VulnerabilitiesCountList,
}, },
props: {
vulnerabilitiesExportEndpoint: {
type: String,
required: true,
},
},
apollo: { apollo: {
projects: { projects: {
query: projectsQuery, query: projectsQuery,
...@@ -75,7 +69,7 @@ export default { ...@@ -75,7 +69,7 @@ export default {
<h2 class="gl-flex-grow-1 gl-my-0"> <h2 class="gl-flex-grow-1 gl-my-0">
{{ s__('SecurityReports|Vulnerability Report') }} {{ s__('SecurityReports|Vulnerability Report') }}
</h2> </h2>
<csv-export-button :vulnerabilities-export-endpoint="vulnerabilitiesExportEndpoint" /> <csv-export-button />
</header> </header>
<vulnerabilities-count-list <vulnerabilities-count-list
:scope="$options.vulnerabilitiesSeverityCountScopes.instance" :scope="$options.vulnerabilitiesSeverityCountScopes.instance"
......
...@@ -34,11 +34,6 @@ export default { ...@@ -34,11 +34,6 @@ export default {
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
vulnerabilitiesExportEndpoint: {
type: String,
required: false,
default: '',
},
}, },
data() { data() {
const shouldShowAutoFixUserCallout = const shouldShowAutoFixUserCallout =
...@@ -73,7 +68,7 @@ export default { ...@@ -73,7 +68,7 @@ export default {
<template #header> <template #header>
<div class="gl-mt-6 gl-display-flex"> <div class="gl-mt-6 gl-display-flex">
<h4 class="gl-flex-grow-1 gl-my-0">{{ __('Vulnerability Report') }}</h4> <h4 class="gl-flex-grow-1 gl-my-0">{{ __('Vulnerability Report') }}</h4>
<csv-export-button :vulnerabilities-export-endpoint="vulnerabilitiesExportEndpoint" /> <csv-export-button />
</div> </div>
<project-pipeline-status :pipeline="pipeline" /> <project-pipeline-status :pipeline="pipeline" />
<vulnerabilities-count-list <vulnerabilities-count-list
......
...@@ -65,6 +65,7 @@ export default (el, dashboardType) => { ...@@ -65,6 +65,7 @@ export default (el, dashboardType) => {
groupFullPath, groupFullPath,
securityConfigurationPath, securityConfigurationPath,
surveyRequestSvgPath, surveyRequestSvgPath,
vulnerabilitiesExportEndpoint,
hasVulnerabilities: parseBoolean(hasVulnerabilities), hasVulnerabilities: parseBoolean(hasVulnerabilities),
scanners: scanners ? JSON.parse(scanners) : [], scanners: scanners ? JSON.parse(scanners) : [],
hasJiraVulnerabilitiesIntegrationEnabled: parseBoolean( hasJiraVulnerabilitiesIntegrationEnabled: parseBoolean(
...@@ -76,7 +77,6 @@ export default (el, dashboardType) => { ...@@ -76,7 +77,6 @@ export default (el, dashboardType) => {
securityDashboardHelpPath, securityDashboardHelpPath,
projectAddEndpoint, projectAddEndpoint,
projectListEndpoint, projectListEndpoint,
vulnerabilitiesExportEndpoint,
}; };
let component; let component;
......
<script> <script>
import { GlButton, GlIcon } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import DismissButton from 'ee/vue_shared/security_reports/components/dismiss_button.vue'; import DismissButton from 'ee/vue_shared/security_reports/components/dismiss_button.vue';
import SplitButton from 'ee/vue_shared/security_reports/components/split_button.vue'; import SplitButton from 'ee/vue_shared/security_reports/components/split_button.vue';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
...@@ -9,7 +9,6 @@ export default { ...@@ -9,7 +9,6 @@ export default {
DismissButton, DismissButton,
GlButton, GlButton,
SplitButton, SplitButton,
GlIcon,
}, },
props: { props: {
modal: { modal: {
...@@ -75,15 +74,16 @@ export default { ...@@ -75,15 +74,16 @@ export default {
const issueButton = { const issueButton = {
name: this.createIssueButtonText, name: this.createIssueButtonText,
tagline: s__('ciReport|Investigate this vulnerability by creating an issue'), tagline: s__('ciReport|Investigate this vulnerability by creating an issue'),
isLoading: this.isCreatingIssue, icon: this.vulnerability.create_jira_issue_url ? 'external-link' : undefined,
loading: this.isCreatingIssue,
target: this.vulnerability.create_jira_issue_url ? '_blank' : undefined,
action: this.vulnerability.create_jira_issue_url ? undefined : 'createNewIssue', action: this.vulnerability.create_jira_issue_url ? undefined : 'createNewIssue',
href: this.vulnerability.create_jira_issue_url, href: this.vulnerability.create_jira_issue_url,
icon: this.vulnerability.create_jira_issue_url ? 'external-link' : undefined,
}; };
const MRButton = { const MRButton = {
name: s__('ciReport|Resolve with merge request'), name: s__('ciReport|Resolve with merge request'),
tagline: s__('ciReport|Automatically apply the patch in a new branch'), tagline: s__('ciReport|Automatically apply the patch in a new branch'),
isLoading: this.isCreatingMergeRequest, loading: this.isCreatingMergeRequest,
action: 'createMergeRequest', action: 'createMergeRequest',
}; };
const DownloadButton = { const DownloadButton = {
...@@ -139,21 +139,14 @@ export default { ...@@ -139,21 +139,14 @@ export default {
<gl-button <gl-button
v-else-if="actionButtons.length > 0" v-else-if="actionButtons.length > 0"
:loading="actionButtons[0].isLoading" v-bind="actionButtons[0]"
:disabled="disabled" :disabled="disabled"
variant="success" variant="success"
category="secondary" category="secondary"
data-testid="create-issue-button" data-testid="create-issue-button"
data-qa-selector="create_issue_button" data-qa-selector="create_issue_button"
:target="actionButtons[0].href ? '_blank' : undefined"
:href="actionButtons[0].href"
@click="$emit(actionButtons[0].action)" @click="$emit(actionButtons[0].action)"
> >
<gl-icon
v-if="actionButtons[0].icon"
:name="actionButtons[0].icon"
class="gl-vertical-align-middle"
/>
{{ __(actionButtons[0].name) }} {{ __(actionButtons[0].name) }}
</gl-button> </gl-button>
</div> </div>
......
...@@ -21,9 +21,9 @@ module EE::GroupMembersFinder ...@@ -21,9 +21,9 @@ module EE::GroupMembersFinder
super super
end end
override :relation_group_members override :all_group_members
def relation_group_members(relation) def all_group_members(groups)
return all_group_members(relation) if group.minimal_access_role_allowed? return members_of_groups(groups) if group.minimal_access_role_allowed?
super super
end end
......
---
title: Use provide/inject for vulnerabilities export endpoint
merge_request: 57639
author:
type: other
...@@ -17,6 +17,10 @@ describe('BoardAddNewColumn', () => { ...@@ -17,6 +17,10 @@ describe('BoardAddNewColumn', () => {
let wrapper; let wrapper;
let shouldUseGraphQL; let shouldUseGraphQL;
const selectItem = (id) => {
wrapper.findByTestId('selectItem').vm.$emit('change', id);
};
const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => { const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => {
return new Vuex.Store({ return new Vuex.Store({
state: { state: {
...@@ -76,6 +80,11 @@ describe('BoardAddNewColumn', () => { ...@@ -76,6 +80,11 @@ describe('BoardAddNewColumn', () => {
}, },
}), }),
); );
// trigger change event
if (selectedId) {
selectItem(selectedId);
}
}; };
afterEach(() => { afterEach(() => {
......
...@@ -4,8 +4,10 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -4,8 +4,10 @@ import MockAdapter from 'axios-mock-adapter';
import CsvExportButton, { import CsvExportButton, {
STORAGE_KEY, STORAGE_KEY,
} from 'ee/security_dashboard/components/csv_export_button.vue'; } from 'ee/security_dashboard/components/csv_export_button.vue';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import AccessorUtils from '~/lib/utils/accessor';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
import downloader from '~/lib/utils/downloader'; import downloader from '~/lib/utils/downloader';
...@@ -14,6 +16,8 @@ import statusCodes from '~/lib/utils/http_status'; ...@@ -14,6 +16,8 @@ import statusCodes from '~/lib/utils/http_status';
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/lib/utils/downloader'); jest.mock('~/lib/utils/downloader');
useLocalStorageSpy();
const mockReportDate = formatDate(new Date(), 'isoDateTime'); const mockReportDate = formatDate(new Date(), 'isoDateTime');
const vulnerabilitiesExportEndpoint = `${TEST_HOST}/vulnerability_findings.csv`; const vulnerabilitiesExportEndpoint = `${TEST_HOST}/vulnerability_findings.csv`;
...@@ -29,7 +33,7 @@ describe('Csv Button Export', () => { ...@@ -29,7 +33,7 @@ describe('Csv Button Export', () => {
const createComponent = () => { const createComponent = () => {
return shallowMount(CsvExportButton, { return shallowMount(CsvExportButton, {
propsData: { provide: {
vulnerabilitiesExportEndpoint, vulnerabilitiesExportEndpoint,
}, },
stubs: { stubs: {
...@@ -49,7 +53,7 @@ describe('Csv Button Export', () => { ...@@ -49,7 +53,7 @@ describe('Csv Button Export', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
localStorage.removeItem(STORAGE_KEY); localStorage.clear();
}); });
describe('when the user sees the button for the first time', () => { describe('when the user sees the button for the first time', () => {
...@@ -117,16 +121,33 @@ describe('Csv Button Export', () => { ...@@ -117,16 +121,33 @@ describe('Csv Button Export', () => {
}); });
it('displays the popover by default', () => { it('displays the popover by default', () => {
expect(findPopover().attributes('show')).toBeTruthy(); expect(findPopover().exists()).toBe(true);
}); });
describe('closing the popover', () => {
it('closes the popover when the button is clicked', async () => { it('closes the popover when the button is clicked', async () => {
const button = findPopoverButton(); expect(findPopoverButton().text()).toBe('Got it!');
expect(button.text().trim()).toBe('Got it!'); findPopoverButton().vm.$emit('click');
button.vm.$emit('click'); await wrapper.vm.$nextTick();
expect(findPopover().exists()).toBe(false);
});
it('sets localStorage', async () => {
jest.spyOn(AccessorUtils, 'isLocalStorageAccessSafe').mockImplementation(() => true);
findPopoverButton().vm.$emit('click');
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(findPopover().attributes('show')).toBeUndefined(); expect(localStorage.setItem).toHaveBeenCalledTimes(1);
});
it(`does not set localStorage if it's not available`, async () => {
jest.spyOn(AccessorUtils, 'isLocalStorageAccessSafe').mockImplementation(() => false);
findPopoverButton().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(localStorage.setItem).toHaveBeenCalledTimes(0);
});
}); });
}); });
...@@ -137,7 +158,7 @@ describe('Csv Button Export', () => { ...@@ -137,7 +158,7 @@ describe('Csv Button Export', () => {
}); });
it('does not display the popover anymore', () => { it('does not display the popover anymore', () => {
expect(findPopover().attributes('show')).toBeFalsy(); expect(findPopover().exists()).toBe(false);
}); });
}); });
}); });
...@@ -14,7 +14,6 @@ describe('First Class Group Dashboard Component', () => { ...@@ -14,7 +14,6 @@ describe('First Class Group Dashboard Component', () => {
const dashboardDocumentation = 'dashboard-documentation'; const dashboardDocumentation = 'dashboard-documentation';
const emptyStateSvgPath = 'empty-state-path'; const emptyStateSvgPath = 'empty-state-path';
const groupFullPath = 'group-full-path'; const groupFullPath = 'group-full-path';
const vulnerabilitiesExportEndpoint = '/vulnerabilities/exports';
const findDashboardLayout = () => wrapper.find(SecurityDashboardLayout); const findDashboardLayout = () => wrapper.find(SecurityDashboardLayout);
const findGroupVulnerabilities = () => wrapper.find(FirstClassGroupVulnerabilities); const findGroupVulnerabilities = () => wrapper.find(FirstClassGroupVulnerabilities);
...@@ -29,7 +28,6 @@ describe('First Class Group Dashboard Component', () => { ...@@ -29,7 +28,6 @@ describe('First Class Group Dashboard Component', () => {
propsData: { propsData: {
dashboardDocumentation, dashboardDocumentation,
emptyStateSvgPath, emptyStateSvgPath,
vulnerabilitiesExportEndpoint,
}, },
provide: { groupFullPath }, provide: { groupFullPath },
data, data,
...@@ -89,9 +87,7 @@ describe('First Class Group Dashboard Component', () => { ...@@ -89,9 +87,7 @@ describe('First Class Group Dashboard Component', () => {
}); });
it('displays the csv export button', () => { it('displays the csv export button', () => {
expect(findCsvExportButton().props('vulnerabilitiesExportEndpoint')).toBe( expect(findCsvExportButton().exists()).toBe(true);
vulnerabilitiesExportEndpoint,
);
}); });
it('loading button should not be rendered', () => { it('loading button should not be rendered', () => {
......
...@@ -14,8 +14,6 @@ describe('First Class Instance Dashboard Component', () => { ...@@ -14,8 +14,6 @@ describe('First Class Instance Dashboard Component', () => {
$apollo: { queries: { projects: { loading } } }, $apollo: { queries: { projects: { loading } } },
}); });
const vulnerabilitiesExportEndpoint = '/vulnerabilities/exports';
const findInstanceVulnerabilities = () => wrapper.find(FirstClassInstanceVulnerabilities); const findInstanceVulnerabilities = () => wrapper.find(FirstClassInstanceVulnerabilities);
const findCsvExportButton = () => wrapper.find(CsvExportButton); const findCsvExportButton = () => wrapper.find(CsvExportButton);
const findEmptyState = () => wrapper.find(DashboardNotConfigured); const findEmptyState = () => wrapper.find(DashboardNotConfigured);
...@@ -29,9 +27,6 @@ describe('First Class Instance Dashboard Component', () => { ...@@ -29,9 +27,6 @@ describe('First Class Instance Dashboard Component', () => {
return { ...data }; return { ...data };
}, },
mocks, mocks,
propsData: {
vulnerabilitiesExportEndpoint,
},
stubs: { stubs: {
...stubs, ...stubs,
SecurityDashboardLayout, SecurityDashboardLayout,
...@@ -77,9 +72,7 @@ describe('First Class Instance Dashboard Component', () => { ...@@ -77,9 +72,7 @@ describe('First Class Instance Dashboard Component', () => {
}); });
it('displays the csv export button', () => { it('displays the csv export button', () => {
expect(findCsvExportButton().props('vulnerabilitiesExportEndpoint')).toBe( expect(findCsvExportButton().exists()).toBe(true);
vulnerabilitiesExportEndpoint,
);
}); });
it('displays the vulnerability count list with the correct data', () => { it('displays the vulnerability count list with the correct data', () => {
......
...@@ -21,7 +21,6 @@ const props = { ...@@ -21,7 +21,6 @@ const props = {
path: '/mixed-vulnerabilities/dependency-list-test-01/-/pipelines/214', path: '/mixed-vulnerabilities/dependency-list-test-01/-/pipelines/214',
}, },
securityDashboardHelpPath: '/security/dashboard/help-path', securityDashboardHelpPath: '/security/dashboard/help-path',
vulnerabilitiesExportEndpoint: '/vulnerabilities/exports',
}; };
const provide = { const provide = {
......
...@@ -4,80 +4,83 @@ require 'spec_helper' ...@@ -4,80 +4,83 @@ require 'spec_helper'
RSpec.describe GroupMembersFinder, '#execute' do RSpec.describe GroupMembersFinder, '#execute' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) } let(:sub_group) { create(:group, parent: group) }
let(:deeper_nested_group) { create(:group, parent: nested_group) } let(:sub_sub_group) { create(:group, parent: sub_group) }
let(:user1) { create(:user) } let(:user1) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:user3) { create(:user) } let(:user3) { create(:user) }
let(:user4) { create(:user) } let(:user4) { create(:user) }
let(:user5) { create(:user, :two_factor_via_otp) } let(:user5) { create(:user, :two_factor_via_otp) }
it 'returns members for top-level group' do let(:groups) do
member1 = group.add_maintainer(user1) {
member2 = group.add_maintainer(user2) group: group,
member3 = group.add_maintainer(user3) sub_group: sub_group,
create(:group_member, :minimal_access, user: create(:user), source: group) sub_sub_group: sub_sub_group
}
result = described_class.new(group).execute
expect(result.to_a).to match_array([member3, member2, member1])
end end
it 'returns members & inherited members for nested group by default' do context 'relations' do
group.add_developer(user2) let!(:members) do
nested_group.request_access(user4) {
member1 = group.add_maintainer(user1) user1_sub_sub_group: create(:group_member, :maintainer, group: sub_sub_group, user: user1),
member3 = nested_group.add_maintainer(user2) user1_sub_group: create(:group_member, :developer, group: sub_group, user: user1),
member4 = nested_group.add_maintainer(user3) user1_group: create(:group_member, :reporter, group: group, user: user1),
user2_sub_sub_group: create(:group_member, :reporter, group: sub_sub_group, user: user2),
result = described_class.new(nested_group).execute user2_sub_group: create(:group_member, :developer, group: sub_group, user: user2),
user2_group: create(:group_member, :maintainer, group: group, user: user2),
expect(result.to_a).to match_array([member1, member3, member4]) user3_sub_sub_group: create(:group_member, :developer, group: sub_sub_group, user: user3, expires_at: 1.day.from_now),
user3_sub_group: create(:group_member, :developer, group: sub_group, user: user3, expires_at: 2.days.from_now),
user3_group: create(:group_member, :reporter, group: group, user: user3),
user4_sub_sub_group: create(:group_member, :reporter, group: sub_sub_group, user: user4),
user4_sub_group: create(:group_member, :developer, group: sub_group, user: user4, expires_at: 1.day.from_now),
user4_group: create(:group_member, :developer, group: group, user: user4, expires_at: 2.days.from_now)
}
end end
it 'does not return inherited members for nested group if requested' do using RSpec::Parameterized::TableSyntax
group.add_maintainer(user1)
group.add_developer(user2) where(:subject_relations, :subject_group, :expected_members) do
member2 = nested_group.add_maintainer(user2) nil | :group | [:user1_group, :user2_group, :user3_group, :user4_group]
member3 = nested_group.add_maintainer(user3) [:direct] | :group | [:user1_group, :user2_group, :user3_group, :user4_group]
[:inherited] | :group | []
result = described_class.new(nested_group).execute(include_relations: [:direct]) [:descendants] | :group | [:user1_sub_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
[:direct, :inherited] | :group | [:user1_group, :user2_group, :user3_group, :user4_group]
expect(result.to_a).to match_array([member2, member3]) [:direct, :descendants] | :group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
[:descendants, :inherited] | :group | [:user1_sub_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
[:direct, :descendants, :inherited] | :group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
nil | :sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
[:direct] | :sub_group | [:user1_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
[:inherited] | :sub_group | [:user1_group, :user2_group, :user3_group, :user4_group]
[:descendants] | :sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
[:direct, :inherited] | :sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
[:direct, :descendants] | :sub_group | [:user1_sub_sub_group, :user2_sub_group, :user3_sub_group, :user4_sub_group]
[:descendants, :inherited] | :sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_sub_group, :user4_group]
[:direct, :descendants, :inherited] | :sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
nil | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
[:direct] | :sub_sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
[:inherited] | :sub_sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
[:descendants] | :sub_sub_group | []
[:direct, :inherited] | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
[:direct, :descendants] | :sub_sub_group | [:user1_sub_sub_group, :user2_sub_sub_group, :user3_sub_sub_group, :user4_sub_sub_group]
[:descendants, :inherited] | :sub_sub_group | [:user1_sub_group, :user2_group, :user3_sub_group, :user4_group]
[:direct, :descendants, :inherited] | :sub_sub_group | [:user1_sub_sub_group, :user2_group, :user3_sub_group, :user4_group]
end end
it 'returns only inherited members for nested group if requested' do with_them do
group.add_developer(user2) it 'returns correct members' do
nested_group.request_access(user4) result = if subject_relations
member1 = group.add_maintainer(user1) described_class.new(groups[subject_group]).execute(include_relations: subject_relations)
nested_group.add_maintainer(user2) else
nested_group.add_maintainer(user3) described_class.new(groups[subject_group]).execute
result = described_class.new(nested_group).execute(include_relations: [:inherited])
expect(result.to_a).to match_array([member1])
end end
it 'does not return nil if `inherited only` relation is requested on root group' do expect(result.to_a).to match_array(expected_members.map { |name| members[name] })
group.add_developer(user2) end
result = described_class.new(group).execute(include_relations: [:inherited])
expect(result).not_to be_nil
end end
it 'returns members for descendant groups if requested' do
member1 = group.add_maintainer(user2)
member2 = group.add_maintainer(user1)
nested_group.add_maintainer(user2)
member3 = nested_group.add_maintainer(user3)
member4 = nested_group.add_maintainer(user4)
result = described_class.new(group).execute(include_relations: [:direct, :descendants])
expect(result.to_a).to match_array([member1, member2, member3, member4])
end end
context 'search' do
it 'returns searched members if requested' do it 'returns searched members if requested' do
group.add_maintainer(user2) group.add_maintainer(user2)
group.add_developer(user3) group.add_developer(user3)
...@@ -98,17 +101,19 @@ RSpec.describe GroupMembersFinder, '#execute' do ...@@ -98,17 +101,19 @@ RSpec.describe GroupMembersFinder, '#execute' do
expect(result.to_a).to match_array([]) expect(result.to_a).to match_array([])
end end
it 'returns searched member only from nested_group if search only in inherited relation' do it 'returns searched member only from sub_group if search only in inherited relation' do
group.add_maintainer(user2) group.add_maintainer(user2)
group.add_developer(user3) group.add_developer(user3)
nested_group.add_maintainer(create(:user, name: user1.name)) sub_group.add_maintainer(create(:user, name: user1.name))
member = group.add_maintainer(user1) member = group.add_maintainer(user1)
result = described_class.new(nested_group, params: { search: member.user.name }).execute(include_relations: [:inherited]) result = described_class.new(sub_group, params: { search: member.user.name }).execute(include_relations: [:inherited])
expect(result.to_a).to contain_exactly(member) expect(result.to_a).to contain_exactly(member)
end end
end
context 'filter by two-factor' do
it 'returns members with two-factor auth if requested by owner' do it 'returns members with two-factor auth if requested by owner' do
group.add_owner(user2) group.add_owner(user2)
group.add_maintainer(user1) group.add_maintainer(user1)
...@@ -133,10 +138,10 @@ RSpec.describe GroupMembersFinder, '#execute' do ...@@ -133,10 +138,10 @@ RSpec.describe GroupMembersFinder, '#execute' do
it 'returns direct members with two-factor auth if requested by owner' do it 'returns direct members with two-factor auth if requested by owner' do
group.add_owner(user1) group.add_owner(user1)
group.add_maintainer(user2) group.add_maintainer(user2)
nested_group.add_maintainer(user3) sub_group.add_maintainer(user3)
member_with_2fa = nested_group.add_maintainer(user5) member_with_2fa = sub_group.add_maintainer(user5)
result = described_class.new(nested_group, user1, params: { two_factor: 'enabled' }).execute(include_relations: [:direct]) result = described_class.new(sub_group, user1, params: { two_factor: 'enabled' }).execute(include_relations: [:direct])
expect(result.to_a).to match_array([member_with_2fa]) expect(result.to_a).to match_array([member_with_2fa])
end end
...@@ -144,10 +149,10 @@ RSpec.describe GroupMembersFinder, '#execute' do ...@@ -144,10 +149,10 @@ RSpec.describe GroupMembersFinder, '#execute' do
it 'returns inherited members with two-factor auth if requested by owner' do it 'returns inherited members with two-factor auth if requested by owner' do
group.add_owner(user1) group.add_owner(user1)
member_with_2fa = group.add_maintainer(user5) member_with_2fa = group.add_maintainer(user5)
nested_group.add_maintainer(user2) sub_group.add_maintainer(user2)
nested_group.add_maintainer(user3) sub_group.add_maintainer(user3)
result = described_class.new(nested_group, user1, params: { two_factor: 'enabled' }).execute(include_relations: [:inherited]) result = described_class.new(sub_group, user1, params: { two_factor: 'enabled' }).execute(include_relations: [:inherited])
expect(result.to_a).to match_array([member_with_2fa]) expect(result.to_a).to match_array([member_with_2fa])
end end
...@@ -155,10 +160,10 @@ RSpec.describe GroupMembersFinder, '#execute' do ...@@ -155,10 +160,10 @@ RSpec.describe GroupMembersFinder, '#execute' do
it 'returns direct members without two-factor auth if requested by owner' do it 'returns direct members without two-factor auth if requested by owner' do
group.add_owner(user1) group.add_owner(user1)
group.add_maintainer(user2) group.add_maintainer(user2)
member3 = nested_group.add_maintainer(user3) member3 = sub_group.add_maintainer(user3)
nested_group.add_maintainer(user5) sub_group.add_maintainer(user5)
result = described_class.new(nested_group, user1, params: { two_factor: 'disabled' }).execute(include_relations: [:direct]) result = described_class.new(sub_group, user1, params: { two_factor: 'disabled' }).execute(include_relations: [:direct])
expect(result.to_a).to match_array([member3]) expect(result.to_a).to match_array([member3])
end end
...@@ -166,11 +171,12 @@ RSpec.describe GroupMembersFinder, '#execute' do ...@@ -166,11 +171,12 @@ RSpec.describe GroupMembersFinder, '#execute' do
it 'returns inherited members without two-factor auth if requested by owner' do it 'returns inherited members without two-factor auth if requested by owner' do
member1 = group.add_owner(user1) member1 = group.add_owner(user1)
group.add_maintainer(user5) group.add_maintainer(user5)
nested_group.add_maintainer(user2) sub_group.add_maintainer(user2)
nested_group.add_maintainer(user3) sub_group.add_maintainer(user3)
result = described_class.new(nested_group, user1, params: { two_factor: 'disabled' }).execute(include_relations: [:inherited]) result = described_class.new(sub_group, user1, params: { two_factor: 'disabled' }).execute(include_relations: [:inherited])
expect(result.to_a).to match_array([member1]) expect(result.to_a).to match_array([member1])
end end
end
end end
import { GlFormRadioGroup } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
...@@ -12,6 +13,10 @@ Vue.use(Vuex); ...@@ -12,6 +13,10 @@ Vue.use(Vuex);
describe('Board card layout', () => { describe('Board card layout', () => {
let wrapper; let wrapper;
const selectLabel = (id) => {
wrapper.findComponent(GlFormRadioGroup).vm.$emit('change', id);
};
const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => { const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => {
return new Vuex.Store({ return new Vuex.Store({
state: { state: {
...@@ -57,6 +62,11 @@ describe('Board card layout', () => { ...@@ -57,6 +62,11 @@ describe('Board card layout', () => {
}, },
}), }),
); );
// trigger change event
if (selectedId) {
selectLabel(selectedId);
}
}; };
afterEach(() => { afterEach(() => {
......
...@@ -52,7 +52,6 @@ describe('ErrorTrackingList', () => { ...@@ -52,7 +52,6 @@ describe('ErrorTrackingList', () => {
beforeEach(() => { beforeEach(() => {
actions = { actions = {
getErrorList: () => {},
startPolling: jest.fn(), startPolling: jest.fn(),
restartPolling: jest.fn().mockName('restartPolling'), restartPolling: jest.fn().mockName('restartPolling'),
addRecentSearch: jest.fn(), addRecentSearch: jest.fn(),
......
...@@ -413,6 +413,24 @@ RSpec.describe Member do ...@@ -413,6 +413,24 @@ RSpec.describe Member do
it { is_expected.not_to include @blocked_developer } it { is_expected.not_to include @blocked_developer }
it { is_expected.not_to include @member_with_minimal_access } it { is_expected.not_to include @member_with_minimal_access }
end end
describe '.distinct_on_user_with_max_access_level' do
let_it_be(:other_group) { create(:group) }
let_it_be(:member_with_lower_access_level) { create(:group_member, :developer, group: other_group, user: @owner_user) }
subject { described_class.default_scoped.distinct_on_user_with_max_access_level.to_a }
it { is_expected.not_to include member_with_lower_access_level }
it { is_expected.to include @owner }
it { is_expected.to include @maintainer }
it { is_expected.to include @invited_member }
it { is_expected.to include @accepted_invite_member }
it { is_expected.to include @requested_member }
it { is_expected.to include @accepted_request_member }
it { is_expected.to include @blocked_maintainer }
it { is_expected.to include @blocked_developer }
it { is_expected.to include @member_with_minimal_access }
end
end end
describe "Delegate methods" do describe "Delegate methods" do
......
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