Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-03-07 03:07:43 +00:00
parent defeeba1a8
commit d0db908485
53 changed files with 500 additions and 296 deletions

View File

@ -108,7 +108,6 @@ Layout/ArgumentAlignment:
- 'app/graphql/mutations/jira_import/import_users.rb'
- 'app/graphql/mutations/jira_import/start.rb'
- 'app/graphql/mutations/labels/create.rb'
- 'app/graphql/mutations/members/groups/bulk_update.rb'
- 'app/graphql/mutations/merge_requests/accept.rb'
- 'app/graphql/mutations/merge_requests/base.rb'
- 'app/graphql/mutations/merge_requests/create.rb'

View File

@ -19,7 +19,6 @@ Lint/UnusedMethodArgument:
- 'app/graphql/mutations/base_mutation.rb'
- 'app/graphql/mutations/ci/runner/delete.rb'
- 'app/graphql/mutations/concerns/mutations/assignable.rb'
- 'app/graphql/mutations/members/groups/bulk_update.rb'
- 'app/graphql/mutations/notes/create/base.rb'
- 'app/graphql/mutations/notes/create/diff_note.rb'
- 'app/graphql/mutations/notes/create/image_diff_note.rb'

View File

@ -5,7 +5,6 @@ Style/MutableConstant:
Exclude:
- 'app/finders/group_members_finder.rb'
- 'app/graphql/mutations/container_repositories/destroy_tags.rb'
- 'app/graphql/mutations/members/groups/bulk_update.rb'
- 'app/graphql/mutations/packages/bulk_destroy.rb'
- 'app/helpers/blame_helper.rb'
- 'app/models/ci/build_trace_chunks/redis_base.rb'

View File

@ -15,6 +15,10 @@ export default function syntaxHighlight($els = null) {
const els = $els.get ? $els.get() : $els;
const handler = (el) => {
if (el.classList === undefined) {
return el;
}
if (el.classList.contains('js-syntax-highlight')) {
// Given the element itself, apply highlighting
return el.classList.add(gon.user_color_scheme);

View File

@ -0,0 +1,88 @@
# frozen_string_literal: true
module Mutations
module Members
class BulkUpdateBase < BaseMutation
include ::API::Helpers::MembersHelpers
argument :user_ids,
[::Types::GlobalIDType[::User]],
required: true,
description: 'Global IDs of the members.'
argument :access_level,
::Types::MemberAccessLevelEnum,
required: true,
description: 'Access level to update the members to.'
argument :expires_at,
Types::TimeType,
required: false,
description: 'Date and time the membership expires.'
MAX_MEMBERS_UPDATE_LIMIT = 50
MAX_MEMBERS_UPDATE_ERROR = "Count of members to be updated should be less than #{MAX_MEMBERS_UPDATE_LIMIT}."
.freeze
INVALID_MEMBERS_ERROR = 'Only access level of direct members can be updated.'
def resolve(**args)
result = ::Members::UpdateService
.new(current_user, args.except(:user_ids, source_id_param_name))
.execute(@updatable_members)
{
source_members_key => result[:members],
errors: Array.wrap(result[:message])
}
rescue Gitlab::Access::AccessDeniedError
{
errors: ["Unable to update members, please check user permissions."]
}
end
private
def ready?(**args)
source = authorized_find!(source_id: args[source_id_param_name])
user_ids = args.fetch(:user_ids, {}).map(&:model_id)
@updatable_members = only_direct_members(source, user_ids)
if @updatable_members.size > MAX_MEMBERS_UPDATE_LIMIT
raise Gitlab::Graphql::Errors::InvalidMemberCountError, MAX_MEMBERS_UPDATE_ERROR
end
if @updatable_members.size != user_ids.size
raise Gitlab::Graphql::Errors::InvalidMembersError, INVALID_MEMBERS_ERROR
end
super
end
def find_object(source_id:)
GitlabSchema.object_from_id(source_id, expected_type: source_type)
end
def only_direct_members(source, user_ids)
source_members(source)
.with_user(user_ids)
.to_a
end
def source_id_param_name
"#{source_name}_id".to_sym
end
def source_members_key
"#{source_name}_members".to_sym
end
def source_name
source_type.name.downcase
end
def source_type
raise NotImplementedError
end
end
end
end

View File

@ -3,81 +3,22 @@
module Mutations
module Members
module Groups
class BulkUpdate < ::Mutations::BaseMutation
class BulkUpdate < BulkUpdateBase
graphql_name 'GroupMemberBulkUpdate'
include Gitlab::Utils::StrongMemoize
authorize :admin_group_member
field :group_members,
[Types::GroupMemberType],
null: true,
description: 'Group members after mutation.'
[Types::GroupMemberType],
null: true,
description: 'Group members after mutation.'
argument :group_id,
::Types::GlobalIDType[::Group],
required: true,
description: 'Global ID of the group.'
::Types::GlobalIDType[::Group],
required: true,
description: 'Global ID of the group.'
argument :user_ids,
[::Types::GlobalIDType[::User]],
required: true,
description: 'Global IDs of the group members.'
argument :access_level,
::Types::MemberAccessLevelEnum,
required: true,
description: 'Access level to update the members to.'
argument :expires_at,
Types::TimeType,
required: false,
description: 'Date and time the membership expires.'
MAX_MEMBERS_UPDATE_LIMIT = 50
MAX_MEMBERS_UPDATE_ERROR = "Count of members to be updated should be less than #{MAX_MEMBERS_UPDATE_LIMIT}."
INVALID_MEMBERS_ERROR = 'Only access level of direct members can be updated.'
def resolve(group_id:, **args)
result = ::Members::UpdateService.new(current_user, args.except(:user_ids)).execute(@updatable_group_members)
{
group_members: result[:members],
errors: Array.wrap(result[:message])
}
rescue Gitlab::Access::AccessDeniedError
{
errors: ["Unable to update members, please check user permissions."]
}
end
private
def ready?(**args)
group = authorized_find!(group_id: args[:group_id])
user_ids = args.fetch(:user_ids, {}).map(&:model_id)
@updatable_group_members = only_direct_group_members(group, user_ids)
if @updatable_group_members.size > MAX_MEMBERS_UPDATE_LIMIT
raise Gitlab::Graphql::Errors::InvalidMemberCountError, MAX_MEMBERS_UPDATE_ERROR
end
if @updatable_group_members.size != user_ids.size
raise Gitlab::Graphql::Errors::InvalidMembersError, INVALID_MEMBERS_ERROR
end
super
end
def find_object(group_id:)
GitlabSchema.object_from_id(group_id, expected_type: ::Group)
end
def only_direct_group_members(group, user_ids)
group
.members
.with_user(user_ids).to_a
def source_type
::Group
end
end
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
module Mutations
module Members
module Projects
class BulkUpdate < BulkUpdateBase
graphql_name 'ProjectMemberBulkUpdate'
authorize :admin_project_member
field :project_members,
[Types::ProjectMemberType],
null: true,
description: 'Project members after mutation.'
argument :project_id,
::Types::GlobalIDType[::Project],
required: true,
description: 'Global ID of the project.'
def source_type
::Project
end
end
end
end
end

View File

@ -70,6 +70,7 @@ module Types
mount_mutation Mutations::Issues::BulkUpdate, alpha: { milestone: '15.9' }
mount_mutation Mutations::Labels::Create
mount_mutation Mutations::Members::Groups::BulkUpdate
mount_mutation Mutations::Members::Projects::BulkUpdate
mount_mutation Mutations::MergeRequests::Accept
mount_mutation Mutations::MergeRequests::Create
mount_mutation Mutations::MergeRequests::Update

View File

@ -34,6 +34,12 @@ module DiffHelper
options[:expanded] = true
options[:paths] = params.values_at(:old_path, :new_path)
options[:use_extra_viewer_as_main] = false
if Feature.enabled?(:large_ipynb_diffs, @project) && params[:file_identifier]&.include?('.ipynb')
options[:max_patch_bytes_for_file_extension] = {
'.ipynb' => 1.megabyte
}
end
end
options

View File

@ -1,5 +1,5 @@
- breadcrumb_title _("Compare Revisions")
- page_title _("Compare")
- breadcrumb_title _("Compare revisions")
- page_title _("Compare revisions")
%h1.page-title.gl-font-size-h-display
= _("Compare Git revisions")

View File

@ -1,4 +1,4 @@
- add_to_breadcrumbs _("Compare Revisions"), project_compare_index_path(@project)
- add_to_breadcrumbs _("Compare revisions"), project_compare_index_path(@project)
- page_title "#{params[:from]}...#{params[:to]}"
.sub-header-block.gl-border-b-0.gl-mb-0

View File

@ -1,4 +1,4 @@
- page_title _('Contributors')
- page_title _('Contributor statistics')
- graph_path = project_graph_path(@project, current_ref, ref_type: @ref_type, format: :json)
- commits_path = project_commits_path(@project, current_ref, ref_type: @ref_type)

View File

@ -0,0 +1,8 @@
---
name: large_ipynb_diffs
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113370
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/393886
milestone: '15.10'
type: development
group: group::incubation
default_enabled: false

View File

@ -3185,7 +3185,7 @@ Input type: `GroupMemberBulkUpdateInput`
| <a id="mutationgroupmemberbulkupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationgroupmemberbulkupdateexpiresat"></a>`expiresAt` | [`Time`](#time) | Date and time the membership expires. |
| <a id="mutationgroupmemberbulkupdategroupid"></a>`groupId` | [`GroupID!`](#groupid) | Global ID of the group. |
| <a id="mutationgroupmemberbulkupdateuserids"></a>`userIds` | [`[UserID!]!`](#userid) | Global IDs of the group members. |
| <a id="mutationgroupmemberbulkupdateuserids"></a>`userIds` | [`[UserID!]!`](#userid) | Global IDs of the members. |
#### Fields
@ -4715,6 +4715,28 @@ Input type: `ProjectInitializeProductAnalyticsInput`
| <a id="mutationprojectinitializeproductanalyticserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationprojectinitializeproductanalyticsproject"></a>`project` | [`Project`](#project) | Project on which the initialization took place. |
### `Mutation.projectMemberBulkUpdate`
Input type: `ProjectMemberBulkUpdateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationprojectmemberbulkupdateaccesslevel"></a>`accessLevel` | [`MemberAccessLevel!`](#memberaccesslevel) | Access level to update the members to. |
| <a id="mutationprojectmemberbulkupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationprojectmemberbulkupdateexpiresat"></a>`expiresAt` | [`Time`](#time) | Date and time the membership expires. |
| <a id="mutationprojectmemberbulkupdateprojectid"></a>`projectId` | [`ProjectID!`](#projectid) | Global ID of the project. |
| <a id="mutationprojectmemberbulkupdateuserids"></a>`userIds` | [`[UserID!]!`](#userid) | Global IDs of the members. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationprojectmemberbulkupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationprojectmemberbulkupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationprojectmemberbulkupdateprojectmembers"></a>`projectMembers` | [`[ProjectMember!]`](#projectmember) | Project members after mutation. |
### `Mutation.projectSetComplianceFramework`
Assign (or unset) a compliance framework to a project.

View File

@ -221,7 +221,7 @@ To view the user who locked the file (if it was not you), hover over the button.
To view and remove file locks:
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Repository > Locked Files**.
1. On the left sidebar, select **Repository > Locked files**.
This list shows all the files locked either through LFS or GitLab UI.

View File

@ -62,7 +62,7 @@ issue number. GitLab uses the issue number to import data into the merge request
To compare branches in a repository:
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Repository > Compare**.
1. On the left sidebar, select **Repository > Compare revisions**.
1. Select the **Source** branch to search for your desired branch. Exact matches are
shown first. You can refine your search with operators:
- `^` matches the beginning of the branch name: `^feat` matches `feat/user-authentication`.

View File

@ -235,9 +235,9 @@ The size can differ slightly from one instance to another due to compression, ho
Administrators can set a [repository size limit](../../admin_area/settings/account_and_limit_settings.md).
[GitLab sets the size limits for GitLab.com](../../gitlab_com/index.md#account-and-limit-settings).
## Repository contributor graph
## Repository contributor statistics
All code contributors are displayed under your project's **Repository > Contributors**.
All code contributors are displayed under your project's **Repository > Contributor statistics**.
The graph shows the contributor with the most commits to the fewest.

View File

@ -24,6 +24,8 @@ module Gitlab
limits[:safe_max_lines] = [limits[:max_lines], defaults[:max_lines]].min
limits[:safe_max_bytes] = limits[:safe_max_files] * 5.kilobytes # Average 5 KB per file
limits[:max_patch_bytes] = Gitlab::Git::Diff.patch_hard_limit_bytes
limits[:max_patch_bytes_for_file_extension] = options.fetch(:max_patch_bytes_for_file_extension, {})
limits
end

View File

@ -92,7 +92,7 @@ module Sidebars
link = project_graph_path(context.project, context.current_ref, ref_type: ref_type_from_context(context))
::Sidebars::MenuItem.new(
title: _('Contributors'),
title: _('Contributor statistics'),
link: link,
active_routes: { path: 'graphs#show' },
item_id: :contributors
@ -112,7 +112,7 @@ module Sidebars
def compare_menu_item
::Sidebars::MenuItem.new(
title: _('Compare'),
title: _('Compare revisions'),
link: project_compare_index_path(context.project, from: context.project.repository.root_ref, to: context.current_ref),
active_routes: { controller: :compare },
item_id: :compare

View File

@ -10390,9 +10390,6 @@ msgstr ""
msgid "Compare GitLab plans"
msgstr ""
msgid "Compare Revisions"
msgstr ""
msgid "Compare branches and continue"
msgstr ""
@ -10405,6 +10402,9 @@ msgstr ""
msgid "Compare changes with the merge request target branch"
msgstr ""
msgid "Compare revisions"
msgstr ""
msgid "Compare submodule commit revisions"
msgstr ""
@ -11401,7 +11401,7 @@ msgstr ""
msgid "Contributor"
msgstr ""
msgid "Contributors"
msgid "Contributor statistics"
msgstr ""
msgid "Control emails linked to your account"
@ -25756,9 +25756,6 @@ msgstr ""
msgid "Locked"
msgstr ""
msgid "Locked Files"
msgstr ""
msgid "Locked by %{fileLockUserName}"
msgstr ""

View File

@ -40,7 +40,7 @@ module QA
def go_to_repository_contributors
hover_repository do
within_submenu do
click_element(:sidebar_menu_item_link, menu_item: 'Contributors')
click_element(:sidebar_menu_item_link, menu_item: 'Contributor statistics')
end
end
end

View File

@ -1,13 +1,8 @@
# frozen_string_literal: true
# TODO: remove this test when 'vscode_web_ide' feature flag is default enabled
module QA
RSpec.describe 'Create', :skip_live_env, except: { job: 'review-qa-*' },
feature_flag: { name: 'vscode_web_ide', scope: :global },
product_group: :editor,
quarantine: {
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/387928',
type: :stale
} do
RSpec.describe 'Create', :skip_live_env, except: { job: 'review-qa-*' }, product_group: :editor do
describe 'Git Server Hooks' do
let(:file_path) { File.join(Runtime::Path.fixtures_path, 'web_ide', 'README.md') }
@ -20,15 +15,10 @@ module QA
end
before do
Runtime::Feature.disable(:vscode_web_ide)
Flow::Login.sign_in
project.visit!
end
after do
Runtime::Feature.enable(:vscode_web_ide)
end
context 'with custom error messages' do
it 'renders preconfigured error message when user hook failed on commit in WebIDE',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/364751' do

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Merge request > Real-time reviewers', feature_category: :code_review_workflow do
let_it_be(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user) }
before do
sign_in(user)
visit project_merge_request_path(project, merge_request)
end
it 'updates in real-time', :js do
wait_for_requests
# Simulate a real-time update of reviewers
merge_request.update!(reviewer_ids: [user.id])
GraphqlTriggers.merge_request_reviewers_updated(merge_request)
expect(find('.reviewer')).to have_content(user.name)
end
end

View File

@ -4,12 +4,12 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue';
import { EVENT_ERROR, EVENT_SUCCESS, FORM_SELECTOR } from '~/access_tokens/components/constants';
import { createAlert, VARIANT_INFO } from '~/flash';
import { createAlert, VARIANT_INFO } from '~/alert';
import { __, sprintf } from '~/locale';
import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
jest.mock('~/flash');
jest.mock('~/alert');
describe('~/access_tokens/components/new_access_token_app', () => {
let wrapper;

View File

@ -2,11 +2,11 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/contributors/stores/actions';
import * as types from '~/contributors/stores/mutation_types';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/flash');
jest.mock('~/alert');
describe('Contributors store actions', () => {
describe('fetchChartData', () => {
@ -38,7 +38,7 @@ describe('Contributors store actions', () => {
);
});
it('should show flash on API error', async () => {
it('should show alert on API error', async () => {
mock.onGet().reply(HTTP_STATUS_BAD_REQUEST, 'Not Found');
await testAction(

View File

@ -4,14 +4,14 @@ import Api from '~/api';
import * as actions from '~/deploy_freeze/store/actions';
import * as types from '~/deploy_freeze/store/mutation_types';
import getInitialState from '~/deploy_freeze/store/state';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import * as logger from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
import { freezePeriodsFixture } from '../helpers';
import { timezoneDataFixture } from '../../vue_shared/components/timezone_dropdown/helpers';
jest.mock('~/api.js');
jest.mock('~/flash');
jest.mock('~/alert');
describe('deploy freeze store actions', () => {
const freezePeriodFixture = freezePeriodsFixture[0];

View File

@ -8,11 +8,11 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { BACKSPACE_KEY_CODE, DELETE_KEY_CODE } from '~/lib/utils/keycodes';
import { visitUrl, getParameterByName } from '~/lib/utils/url_utility';
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
getParameterByName: jest.fn(),

View File

@ -5,11 +5,11 @@ import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import { TEST_HOST } from 'helpers/test_constants';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import VisualTokenValue from '~/filtered_search/visual_token_value';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import AjaxCache from '~/lib/utils/ajax_cache';
import UsersCache from '~/lib/utils/users_cache';
jest.mock('~/flash');
jest.mock('~/alert');
describe('Filtered Search Visual Tokens', () => {
const findElements = (tokenElement) => {

View File

@ -3,7 +3,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import appComponent from '~/groups/components/app.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
@ -34,7 +34,7 @@ import {
const $toast = {
show: jest.fn(),
};
jest.mock('~/flash');
jest.mock('~/alert');
describe('AppComponent', () => {
let wrapper;
@ -117,7 +117,7 @@ describe('AppComponent', () => {
});
});
it('should show flash error when request fails', () => {
it('should show alert error when request fails', () => {
mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_BAD_REQUEST);
jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
@ -325,7 +325,7 @@ describe('AppComponent', () => {
});
});
it('should show error flash message if request failed to leave group', () => {
it('should show error alert message if request failed to leave group', () => {
const message = 'An error occurred. Please try again.';
jest
.spyOn(vm.service, 'leaveGroup')
@ -342,7 +342,7 @@ describe('AppComponent', () => {
});
});
it('should show appropriate error flash message if request forbids to leave group', () => {
it('should show appropriate error alert message if request forbids to leave group', () => {
const message = 'Failed to leave the group. Please make sure you are not the only owner.';
jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: HTTP_STATUS_FORBIDDEN });
jest.spyOn(vm.store, 'removeGroup');

View File

@ -7,11 +7,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import GroupNameAndPath from '~/groups/components/group_name_and_path.vue';
import { getGroupPathAvailability } from '~/rest_api';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { helpPagePath } from '~/helpers/help_page_helper';
import searchGroupsWhereUserCanCreateSubgroups from '~/groups/queries/search_groups_where_user_can_create_subgroups.query.graphql';
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/rest_api', () => ({
getGroupPathAvailability: jest.fn(),
}));

View File

@ -16,7 +16,7 @@ import {
MOCK_ISSUE_PATH,
} from '../mock_data';
jest.mock('~/flash');
jest.mock('~/alert');
describe('Header Search Store Actions', () => {
let state;

View File

@ -7,7 +7,7 @@ import {
issuable1,
issuable2,
} from 'jest/issuable/components/related_issuable_mock_data';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import {
HTTP_STATUS_CONFLICT,
@ -19,7 +19,7 @@ import RelatedIssuesBlock from '~/related_issues/components/related_issues_block
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
import relatedIssuesService from '~/related_issues/services/related_issues_service';
jest.mock('~/flash');
jest.mock('~/alert');
describe('RelatedIssuesRoot', () => {
let wrapper;

View File

@ -3,12 +3,12 @@ import $ from 'jquery';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'spec/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_CONFLICT, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MergeRequest from '~/merge_request';
jest.mock('~/flash');
jest.mock('~/alert');
describe('MergeRequest', () => {
const test = {};

View File

@ -4,7 +4,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import FailedJobsApp from '~/pipelines/components/jobs/failed_jobs_app.vue';
import FailedJobsTable from '~/pipelines/components/jobs/failed_jobs_table.vue';
import GetFailedJobsQuery from '~/pipelines/graphql/queries/get_failed_jobs.query.graphql';
@ -12,7 +12,7 @@ import { mockFailedJobsQueryResponse, mockFailedJobsSummaryData } from '../../mo
Vue.use(VueApollo);
jest.mock('~/flash');
jest.mock('~/alert');
describe('Failed Jobs App', () => {
let wrapper;

View File

@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { redirectTo } from '~/lib/utils/url_utility';
import FailedJobsTable from '~/pipelines/components/jobs/failed_jobs_table.vue';
import RetryFailedJobMutation from '~/pipelines/graphql/mutations/retry_failed_job.mutation.graphql';
@ -15,7 +15,7 @@ import {
mockPreparedFailedJobsDataNoPermission,
} from '../../mock_data';
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
Vue.use(VueApollo);

View File

@ -4,7 +4,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import JobsApp from '~/pipelines/components/jobs/jobs_app.vue';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
import getPipelineJobsQuery from '~/pipelines/graphql/queries/get_pipeline_jobs.query.graphql';
@ -12,7 +12,7 @@ import { mockPipelineJobsQueryResponse } from '../../mock_data';
Vue.use(VueApollo);
jest.mock('~/flash');
jest.mock('~/alert');
describe('Jobs app', () => {
let wrapper;

View File

@ -5,7 +5,7 @@ import { nextTick } from 'vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
@ -13,7 +13,7 @@ import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipeli
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import { TRACKING_CATEGORIES } from '~/pipelines/constants';
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
describe('Pipelines Actions dropdown', () => {

View File

@ -11,7 +11,7 @@ import { mockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import { createAlert, VARIANT_WARNING } from '~/flash';
import { createAlert, VARIANT_WARNING } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
@ -25,7 +25,7 @@ import TablePagination from '~/vue_shared/components/pagination/table_pagination
import { stageReply, users, mockSearch, branches } from './mock_data';
jest.mock('~/flash');
jest.mock('~/alert');
const mockProjectPath = 'twitter/flight';
const mockProjectId = '21';

View File

@ -2,13 +2,13 @@ import MockAdapter from 'axios-mock-adapter';
import testReports from 'test_fixtures/pipelines/test_report.json';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as actions from '~/pipelines/stores/test_reports/actions';
import * as types from '~/pipelines/stores/test_reports/mutation_types';
jest.mock('~/flash');
jest.mock('~/alert');
describe('Actions TestReports Store', () => {
let mock;
@ -49,7 +49,7 @@ describe('Actions TestReports Store', () => {
);
});
it('should create flash on API error', async () => {
it('should create alert on API error', async () => {
await testAction(
actions.fetchSummary,
null,

View File

@ -1,9 +1,9 @@
import testReports from 'test_fixtures/pipelines/test_report.json';
import * as types from '~/pipelines/stores/test_reports/mutation_types';
import mutations from '~/pipelines/stores/test_reports/mutations';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
jest.mock('~/flash');
jest.mock('~/alert');
describe('Mutations TestReports Store', () => {
let mockState;
@ -58,7 +58,7 @@ describe('Mutations TestReports Store', () => {
expect(mockState.errorMessage).toBe(message);
});
it('should show a flash message otherwise', () => {
it('should show an alert message otherwise', () => {
mutations[types.SET_SUITE_ERROR](mockState, {});
expect(createAlert).toHaveBeenCalled();

View File

@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import * as logger from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
@ -33,7 +33,7 @@ import {
MOCK_AGGREGATIONS,
} from '../mock_data';
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
setUrlParams: jest.fn(),
joinPaths: jest.fn().mockReturnValue(''),
@ -47,7 +47,7 @@ describe('Global Search Store Actions', () => {
let mock;
let state;
const flashCallback = (callCount) => {
const alertCallback = (callCount) => {
expect(createAlert).toHaveBeenCalledTimes(callCount);
createAlert.mockClear();
};
@ -63,12 +63,12 @@ describe('Global Search Store Actions', () => {
});
describe.each`
action | axiosMock | type | expectedMutations | flashCallCount
action | axiosMock | type | expectedMutations | alertCallCount
${actions.fetchGroups} | ${{ method: 'onGet', code: HTTP_STATUS_OK, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${0}
${actions.fetchGroups} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${1}
${actions.fetchProjects} | ${{ method: 'onGet', code: HTTP_STATUS_OK, res: MOCK_PROJECTS }} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${0}
${actions.fetchProjects} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${1}
`(`axios calls`, ({ action, axiosMock, type, expectedMutations, flashCallCount }) => {
`(`axios calls`, ({ action, axiosMock, type, expectedMutations, alertCallCount }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
beforeEach(() => {
@ -76,7 +76,7 @@ describe('Global Search Store Actions', () => {
});
it(`should dispatch the correct mutations`, () => {
return testAction({ action, state, expectedMutations }).then(() =>
flashCallback(flashCallCount),
alertCallback(alertCallCount),
);
});
});
@ -84,12 +84,12 @@ describe('Global Search Store Actions', () => {
});
describe.each`
action | axiosMock | type | expectedMutations | flashCallCount
action | axiosMock | type | expectedMutations | alertCallCount
${actions.loadFrequentGroups} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resGroups]} | ${0}
${actions.loadFrequentGroups} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${[]} | ${1}
${actions.loadFrequentProjects} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resProjects]} | ${0}
${actions.loadFrequentProjects} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${[]} | ${1}
`('Promise.all calls', ({ action, axiosMock, type, expectedMutations, flashCallCount }) => {
`('Promise.all calls', ({ action, axiosMock, type, expectedMutations, alertCallCount }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
beforeEach(() => {
@ -103,7 +103,7 @@ describe('Global Search Store Actions', () => {
it(`should dispatch the correct mutations`, () => {
return testAction({ action, state, expectedMutations }).then(() => {
flashCallback(flashCallCount);
alertCallback(alertCallCount);
});
});
});

View File

@ -6,7 +6,7 @@ import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approval
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
import ApprovalsSummaryOptional from '~/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue';
@ -21,7 +21,7 @@ import { createCanApproveResponse } from 'jest/approvals/mock_data';
Vue.use(VueApollo);
const mockAlertDismiss = jest.fn();
jest.mock('~/flash', () => ({
jest.mock('~/alert', () => ({
createAlert: jest.fn().mockImplementation(() => ({
dismiss: mockAlertDismiss,
})),
@ -295,7 +295,7 @@ describe('MRWidget approvals', () => {
return nextTick();
});
it('flashes error message', () => {
it('alerts error message', () => {
expect(createAlert).toHaveBeenCalledWith({ message: UNAPPROVE_ERROR });
});
});

View File

@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import WorkInProgress, {
MSG_SOMETHING_WENT_WRONG,
MSG_MARK_READY,
@ -22,7 +22,7 @@ const TEST_MR_IID = '23';
const TEST_MR_TITLE = 'Test MR Title';
const TEST_PROJECT_PATH = 'lorem/ipsum';
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/merge_request');
describe('~/vue_merge_request_widget/components/states/work_in_progress.vue', () => {

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Members::BulkUpdateBase, feature_category: :subgroups do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group).tap { |group| group.add_owner(user) } }
it 'raises a NotImplementedError error if the source_type method is called on the base class' do
mutation = described_class.new(context: { current_user: user }, object: nil, field: nil)
expect { mutation.resolve(group_id: group.to_gid.to_s) }.to raise_error(NotImplementedError)
end
end

View File

@ -47,6 +47,12 @@ RSpec.describe DiffHelper do
end
describe 'diff_options' do
let(:large_notebooks_enabled) { false }
before do
stub_feature_flags(large_ipynb_diffs: large_notebooks_enabled)
end
it 'returns no collapse false' do
expect(diff_options).to include(expanded: false)
end
@ -56,21 +62,48 @@ RSpec.describe DiffHelper do
expect(diff_options).to include(expanded: true)
end
it 'returns no collapse true if action name diff_for_path' do
allow(controller).to receive(:action_name) { 'diff_for_path' }
expect(diff_options).to include(expanded: true)
end
context 'when action name is diff_for_path' do
before do
allow(controller).to receive(:action_name) { 'diff_for_path' }
end
it 'returns paths if action name diff_for_path and param old path' do
allow(controller).to receive(:params) { { old_path: 'lib/wadus.rb' } }
allow(controller).to receive(:action_name) { 'diff_for_path' }
expect(diff_options[:paths]).to include('lib/wadus.rb')
end
it 'returns expanded true' do
expect(diff_options).to include(expanded: true)
end
it 'returns paths if action name diff_for_path and param new path' do
allow(controller).to receive(:params) { { new_path: 'lib/wadus.rb' } }
allow(controller).to receive(:action_name) { 'diff_for_path' }
expect(diff_options[:paths]).to include('lib/wadus.rb')
it 'returns paths if param old path' do
allow(controller).to receive(:params) { { old_path: 'lib/wadus.rb' } }
expect(diff_options[:paths]).to include('lib/wadus.rb')
end
it 'returns paths if param new path' do
allow(controller).to receive(:params) { { new_path: 'lib/wadus.rb' } }
expect(diff_options[:paths]).to include('lib/wadus.rb')
end
it 'does not set max_patch_bytes_for_file_extension' do
expect(diff_options[:max_patch_bytes_for_file_extension]).to be_nil
end
context 'when file_identifier include .ipynb' do
before do
allow(controller).to receive(:params) { { file_identifier: 'something.ipynb' } }
end
context 'when large_ipynb_diffs is disabled' do
it 'does not set max_patch_bytes_for_file_extension' do
expect(diff_options[:max_patch_bytes_for_file_extension]).to be_nil
end
end
context 'when large_ipynb_diffs is enabled' do
let(:large_notebooks_enabled) { true }
it 'sets max_patch_bytes_for_file_extension' do
expect(diff_options[:max_patch_bytes_for_file_extension]).to eq({ '.ipynb' => 1.megabyte })
end
end
end
end
end

View File

@ -777,6 +777,26 @@ RSpec.describe Gitlab::Git::DiffCollection do
end
end
describe '.limits' do
let(:options) { {} }
subject { described_class.limits(options) }
context 'when options do not include max_patch_bytes_for_file_extension' do
it 'sets max_patch_bytes_for_file_extension as empty' do
expect(subject[:max_patch_bytes_for_file_extension]).to eq({})
end
end
context 'when options include max_patch_bytes_for_file_extension' do
let(:options) { { max_patch_bytes_for_file_extension: { '.file' => 1 } } }
it 'sets value for max_patch_bytes_for_file_extension' do
expect(subject[:max_patch_bytes_for_file_extension]).to eq({ '.file' => 1 })
end
end
end
def fake_diff(line_length, line_count)
{ 'diff' => "#{'a' * line_length}\n" * line_count }
end

View File

@ -85,7 +85,7 @@ RSpec.describe Sidebars::Projects::Menus::RepositoryMenu, feature_category: :sou
end
end
describe 'Contributors' do
describe 'Contributor statistics' do
let_it_be(:item_id) { :contributors }
context 'when analytics is disabled' do

View File

@ -5,126 +5,14 @@ require 'spec_helper'
RSpec.describe 'GroupMemberBulkUpdate', feature_category: :subgroups do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:group_member1) { create(:group_member, group: group, user: user1) }
let_it_be(:group_member2) { create(:group_member, group: group, user: user2) }
let_it_be(:parent_group) { create(:group) }
let_it_be(:parent_group_member) { create(:group_member, group: parent_group) }
let_it_be(:group) { create(:group, parent: parent_group) }
let_it_be(:source) { group }
let_it_be(:member_type) { :group_member }
let_it_be(:mutation_name) { :group_member_bulk_update }
let_it_be(:source_id_key) { 'group_id' }
let_it_be(:response_member_field) { 'groupMembers' }
let(:input) do
{
'group_id' => group.to_global_id.to_s,
'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s],
'access_level' => 'GUEST'
}
end
let(:extra_params) { { expires_at: 10.days.from_now } }
let(:input_params) { input.merge(extra_params) }
let(:mutation) { graphql_mutation(mutation_name, input_params) }
let(:mutation_response) { graphql_mutation_response(mutation_name) }
context 'when user is not logged-in' do
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user is not an owner' do
before do
group.add_maintainer(current_user)
end
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user is an owner' do
before do
group.add_owner(current_user)
end
shared_examples 'updates the user access role' do
specify do
post_graphql_mutation(mutation, current_user: current_user)
new_access_levels = mutation_response['groupMembers'].map { |member| member['accessLevel']['integerValue'] }
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['errors']).to be_empty
expect(new_access_levels).to all(be Gitlab::Access::GUEST)
end
end
it_behaves_like 'updates the user access role'
context 'when inherited members are passed' do
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:subgroup_member) { create(:group_member, group: subgroup) }
let(:input) do
{
'group_id' => group.to_global_id.to_s,
'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s, subgroup_member.user.to_global_id.to_s],
'access_level' => 'GUEST'
}
end
it 'does not update the members' do
post_graphql_mutation(mutation, current_user: current_user)
error = Mutations::Members::Groups::BulkUpdate::INVALID_MEMBERS_ERROR
expect(json_response['errors'].first['message']).to include(error)
end
end
context 'when members count is more than the allowed limit' do
let(:max_members_update_limit) { 1 }
before do
stub_const('Mutations::Members::Groups::BulkUpdate::MAX_MEMBERS_UPDATE_LIMIT', max_members_update_limit)
end
it 'does not update the members' do
post_graphql_mutation(mutation, current_user: current_user)
error = Mutations::Members::Groups::BulkUpdate::MAX_MEMBERS_UPDATE_ERROR
expect(json_response['errors'].first['message']).to include(error)
end
end
context 'when the update service raises access denied error' do
before do
allow_next_instance_of(Members::UpdateService) do |instance|
allow(instance).to receive(:execute).and_raise(Gitlab::Access::AccessDeniedError)
end
end
it 'does not update the members' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['groupMembers']).to be_nil
expect(mutation_response['errors'])
.to contain_exactly("Unable to update members, please check user permissions.")
end
end
context 'when the update service returns an error message' do
before do
allow_next_instance_of(Members::UpdateService) do |instance|
error_result = {
message: 'Expires at cannot be a date in the past',
status: :error,
members: [group_member1]
}
allow(instance).to receive(:execute).and_return(error_result)
end
end
it 'will pass through the error' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['groupMembers'].first['id']).to eq(group_member1.to_global_id.to_s)
expect(mutation_response['errors']).to contain_exactly('Expires at cannot be a date in the past')
end
end
end
it_behaves_like 'members bulk update mutation'
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'ProjectMemberBulkUpdate', feature_category: :projects do
include GraphqlHelpers
let_it_be(:parent_group) { create(:group) }
let_it_be(:parent_group_member) { create(:group_member, group: parent_group) }
let_it_be(:project) { create(:project, group: parent_group) }
let_it_be(:source) { project }
let_it_be(:member_type) { :project_member }
let_it_be(:mutation_name) { :project_member_bulk_update }
let_it_be(:source_id_key) { 'project_id' }
let_it_be(:response_member_field) { 'projectMembers' }
it_behaves_like 'members bulk update mutation'
end

View File

@ -34,10 +34,10 @@ RSpec.shared_context 'project navbar structure' do
_('Commits'),
_('Branches'),
_('Tags'),
_('Contributors'),
_('Contributor statistics'),
_('Graph'),
_('Compare'),
(_('Locked Files') if Gitlab.ee?)
_('Compare revisions'),
(_('Locked files') if Gitlab.ee?)
]
},
{

View File

@ -0,0 +1,123 @@
# frozen_string_literal: true
RSpec.shared_examples 'members bulk update mutation' do
let_it_be(:current_user) { create(:user) }
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:member1) { create(member_type, source: source, user: user1) }
let_it_be(:member2) { create(member_type, source: source, user: user2) }
let(:extra_params) { { expires_at: 10.days.from_now } }
let(:input_params) { input.merge(extra_params) }
let(:mutation) { graphql_mutation(mutation_name, input_params) }
let(:mutation_response) { graphql_mutation_response(mutation_name) }
let(:input) do
{
source_id_key => source.to_global_id.to_s,
'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s],
'access_level' => 'GUEST'
}
end
context 'when user is not logged-in' do
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user is not an owner' do
before do
source.add_developer(current_user)
end
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user is an owner' do
before do
source.add_owner(current_user)
end
shared_examples 'updates the user access role' do
specify do
post_graphql_mutation(mutation, current_user: current_user)
new_access_levels = mutation_response[response_member_field].map do |member|
member['accessLevel']['integerValue']
end
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['errors']).to be_empty
expect(new_access_levels).to all(be Gitlab::Access::GUEST)
end
end
it_behaves_like 'updates the user access role'
context 'when inherited members are passed' do
let(:input) do
{
source_id_key => source.to_global_id.to_s,
'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s, parent_group_member.user.to_global_id.to_s],
'access_level' => 'GUEST'
}
end
it 'does not update the members' do
post_graphql_mutation(mutation, current_user: current_user)
error = Mutations::Members::BulkUpdateBase::INVALID_MEMBERS_ERROR
expect(json_response['errors'].first['message']).to include(error)
end
end
context 'when members count is more than the allowed limit' do
let(:max_members_update_limit) { 1 }
before do
stub_const('Mutations::Members::BulkUpdateBase::MAX_MEMBERS_UPDATE_LIMIT', max_members_update_limit)
end
it 'does not update the members' do
post_graphql_mutation(mutation, current_user: current_user)
error = Mutations::Members::BulkUpdateBase::MAX_MEMBERS_UPDATE_ERROR
expect(json_response['errors'].first['message']).to include(error)
end
end
context 'when the update service raises access denied error' do
before do
allow_next_instance_of(Members::UpdateService) do |instance|
allow(instance).to receive(:execute).and_raise(Gitlab::Access::AccessDeniedError)
end
end
it 'does not update the members' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response[response_member_field]).to be_nil
expect(mutation_response['errors'])
.to contain_exactly("Unable to update members, please check user permissions.")
end
end
context 'when the update service returns an error message' do
before do
allow_next_instance_of(Members::UpdateService) do |instance|
error_result = {
message: 'Expires at cannot be a date in the past',
status: :error,
members: [member1]
}
allow(instance).to receive(:execute).and_return(error_result)
end
end
it 'will pass through the error' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response[response_member_field].first['id']).to eq(member1.to_global_id.to_s)
expect(mutation_response['errors']).to contain_exactly('Expires at cannot be a date in the past')
end
end
end
end

View File

@ -106,11 +106,11 @@ RSpec.describe 'layouts/nav/sidebar/_project', feature_category: :navigation do
end
end
describe 'Contributors' do
describe 'Contributor statistics' do
it 'has a link to the project contributors path' do
render
expect(rendered).to have_link('Contributors', href: project_graph_path(project, current_ref, ref_type: 'heads'))
expect(rendered).to have_link('Contributor statistics', href: project_graph_path(project, current_ref, ref_type: 'heads'))
end
end
@ -122,11 +122,11 @@ RSpec.describe 'layouts/nav/sidebar/_project', feature_category: :navigation do
end
end
describe 'Compare' do
describe 'Compare revisions' do
it 'has a link to the project compare path' do
render
expect(rendered).to have_link('Compare', href: project_compare_index_path(project, from: project.repository.root_ref, to: current_ref))
expect(rendered).to have_link('Compare revisions', href: project_compare_index_path(project, from: project.repository.root_ref, to: current_ref))
end
end
end