Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-02 12:12:25 +00:00
parent 157c4d9279
commit d67ccb290a
32 changed files with 348 additions and 93 deletions

View file

@ -148,5 +148,5 @@ codebase.
/chatops run feature set <feature-flag-name> false
```
/label ~"feature flag"
/label ~"feature flag" ~"type::feature" ~"feature::addition"
/assign DRI

View file

@ -2211,7 +2211,6 @@ Performance/OpenStruct:
- 'lib/gitlab/ci/ansi2html.rb'
- 'lib/gitlab/git/diff_collection.rb'
- 'lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb'
- 'lib/gitlab/testing/request_inspector_middleware.rb'
- 'lib/mattermost/session.rb'
# WIP: https://gitlab.com/gitlab-org/gitlab/-/issues/324629

View file

@ -0,0 +1,47 @@
<script>
import { GlButton, GlModalDirective } from '@gitlab/ui';
import { formType } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
export default {
components: {
GlButton,
GitlabExperiment,
},
directives: {
GlModalDirective,
},
mixins: [Tracking.mixin()],
inject: ['multipleIssueBoardsAvailable', 'canAdminBoard'],
computed: {
canShowCreateButton() {
return this.canAdminBoard && this.multipleIssueBoardsAvailable;
},
createButtonText() {
return s__('Boards|New board');
},
},
methods: {
showDialog() {
this.track('click_button', { label: 'create_board' });
eventHub.$emit('showBoardModal', formType.new);
},
},
};
</script>
<template>
<gitlab-experiment name="prominent_create_board_btn">
<template #control> </template>
<template #candidate>
<div v-if="canShowCreateButton" class="gl-ml-1 gl-mr-3 gl-display-flex gl-align-items-center">
<gl-button data-qa-selector="new_board_button" @click.prevent="showDialog">
{{ createButtonText }}
</gl-button>
</div>
</template>
</gitlab-experiment>
</template>

View file

@ -16,6 +16,7 @@ import toggleFocusMode from '~/boards/toggle_focus';
import { NavigationType, parseBoolean } from '~/lib/utils/common_utils';
import { fullBoardId } from './boards_util';
import boardConfigToggle from './config_toggle';
import initNewBoard from './new_board';
import { gqlClient } from './graphql';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
@ -130,6 +131,7 @@ export default () => {
}
boardConfigToggle();
initNewBoard();
toggleFocusMode();
toggleLabels();

View file

@ -0,0 +1,29 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { getExperimentVariant } from '~/experimentation/utils';
import { CANDIDATE_VARIANT } from '~/experimentation/constants';
import NewBoardButton from './components/new_board_button.vue';
export default () => {
if (getExperimentVariant('prominent_create_board_btn') !== CANDIDATE_VARIANT) {
return;
}
const el = document.querySelector('.js-new-board');
if (!el) {
return;
}
// eslint-disable-next-line no-new
new Vue({
el,
provide: {
multipleIssueBoardsAvailable: parseBoolean(el.dataset.multipleIssueBoardsAvailable),
canAdminBoard: parseBoolean(el.dataset.canAdminBoard),
},
render(h) {
return h(NewBoardButton);
},
});
};

View file

@ -1,5 +1,4 @@
import $ from 'jquery';
import { hide } from '~/tooltips';
export const addTooltipToEl = (el) => {
const textEl = el.querySelector('.js-breadcrumb-item-text');
@ -19,16 +18,23 @@ export default () => {
.filter((el) => !el.classList.contains('dropdown'))
.map((el) => el.querySelector('a'))
.filter((el) => el);
const $expander = $('.js-breadcrumbs-collapsed-expander');
const $expanderBtn = $('.js-breadcrumbs-collapsed-expander');
topLevelLinks.forEach((el) => addTooltipToEl(el));
$expander.closest('.dropdown').on('show.bs.dropdown hide.bs.dropdown', (e) => {
const $el = $('.js-breadcrumbs-collapsed-expander', e.currentTarget);
$expanderBtn.on('click', () => {
const detailItems = $('.breadcrumbs-detail-item');
const hiddenClass = 'gl-display-none!';
$el.toggleClass('open');
$.each(detailItems, (_key, item) => {
$(item).toggleClass(hiddenClass);
});
hide($el);
// remove the ellipsis
$('li.expander').remove();
// set focus on first breadcrumb item
$('.breadcrumb-item-text').first().focus();
});
}
};

View file

@ -63,3 +63,6 @@ export const timeRanges = [
export const defaultTimeRange = timeRanges.find((tr) => tr.default);
export const getTimeWindow = (timeWindowName) =>
timeRanges.find((tr) => tr.name === timeWindowName);
export const AVATAR_SHAPE_OPTION_CIRCLE = 'circle';
export const AVATAR_SHAPE_OPTION_RECT = 'rect';

View file

@ -12,6 +12,10 @@ class Groups::BoardsController < Groups::ApplicationController
push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml)
push_frontend_feature_flag(:iteration_cadences, group, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, group, default_enabled: :yaml)
experiment(:prominent_create_board_btn, subject: current_user) do |e|
e.use { }
e.try { }
end.run
end
feature_category :team_planning

View file

@ -12,6 +12,10 @@ class Projects::BoardsController < Projects::ApplicationController
push_frontend_feature_flag(:board_multi_select, project, default_enabled: :yaml)
push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, project, default_enabled: :yaml)
experiment(:prominent_create_board_btn, subject: current_user) do |e|
e.use { }
e.try { }
end.run
end
feature_category :team_planning

View file

@ -10,8 +10,7 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_download_code!
before_action :authorize_admin_tag!, only: [:new, :create, :destroy]
feature_category :source_code_management, [:index, :show, :new, :destroy]
feature_category :release_evidence, [:create]
feature_category :source_code_management
# rubocop: disable CodeReuse/ActiveRecord
def index

View file

@ -27,10 +27,10 @@ module BreadcrumbsHelper
end
end
def add_to_breadcrumb_dropdown(link, location: :before)
@breadcrumb_dropdown_links ||= {}
@breadcrumb_dropdown_links[location] ||= []
@breadcrumb_dropdown_links[location] << link
def add_to_breadcrumb_collapsed_links(link, location: :before)
@breadcrumb_collapsed_links ||= {}
@breadcrumb_collapsed_links[location] ||= []
@breadcrumb_collapsed_links[location] << link
end
def push_to_schema_breadcrumb(text, link)

View file

@ -39,7 +39,7 @@ module GroupsHelper
sorted_ancestors(group).with_route.reverse_each.with_index do |parent, index|
if index > 0
add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true, for_dropdown: true), location: :before)
add_to_breadcrumb_collapsed_links(group_title_link(parent), location: :before)
else
full_title << breadcrumb_list_item(group_title_link(parent, hidable: false))
end
@ -47,7 +47,7 @@ module GroupsHelper
push_to_schema_breadcrumb(simple_sanitize(parent.name), group_path(parent))
end
full_title << render("layouts/nav/breadcrumbs/collapsed_dropdown", location: :before, title: _("Show parent subgroups"))
full_title << render("layouts/nav/breadcrumbs/collapsed_inline_list", location: :before, title: _("Show all breadcrumbs"))
full_title << breadcrumb_list_item(group_title_link(group))
push_to_schema_breadcrumb(simple_sanitize(group.name), group_path(group))

View file

@ -81,32 +81,11 @@ module UserCalloutsHelper
def user_dismissed_for_group(feature_name, group, ignore_dismissal_earlier_than = nil)
return false unless current_user
set_dismissed_from_cookie(group)
current_user.dismissed_callout_for_group?(feature_name: feature_name,
group: group,
ignore_dismissal_earlier_than: ignore_dismissal_earlier_than)
end
def set_dismissed_from_cookie(group)
# bridge function for one milestone to try and not annoy users who might have already dismissed this alert
# remove in 14.4 or 14.5? https://gitlab.com/gitlab-org/gitlab/-/issues/340322
dismissed_key = "invite_#{group.id}_#{current_user.id}"
if cookies[dismissed_key].present?
params = {
feature_name: INVITE_MEMBERS_BANNER,
group_id: group.id
}
Users::DismissGroupCalloutService.new(
container: nil, current_user: current_user, params: params
).execute
cookies.delete dismissed_key
end
end
def just_created?
flash[:notice]&.include?('successfully created')
end

View file

@ -9,7 +9,7 @@ module WikiHelper
if page.persisted?
titles << page.human_title
breadcrumb_title(page.human_title)
wiki_breadcrumb_dropdown_links(page.slug)
wiki_breadcrumb_collapsed_links(page.slug)
end
titles << action if action
@ -39,14 +39,14 @@ module WikiHelper
.join(' / ')
end
def wiki_breadcrumb_dropdown_links(page_slug)
def wiki_breadcrumb_collapsed_links(page_slug)
page_slug_split = page_slug.split('/')
page_slug_split.pop(1)
current_slug = ""
page_slug_split
.map do |dir_or_page|
current_slug = "#{current_slug}#{dir_or_page}/"
add_to_breadcrumb_dropdown link_to(WikiPage.unhyphenize(dir_or_page).capitalize, wiki_page_path(@wiki, current_slug)), location: :after
add_to_breadcrumb_collapsed_links link_to(WikiPage.unhyphenize(dir_or_page).capitalize, wiki_page_path(@wiki, current_slug)), location: :after
end
end

View file

@ -125,7 +125,7 @@ class Namespace < ApplicationRecord
scope :user_namespaces, -> { where(type: [nil, Namespaces::UserNamespace.sti_name]) }
# TODO: this can be simplified with `type != 'Project'` when working on issue
# https://gitlab.com/gitlab-org/gitlab/-/issues/341070
scope :without_project_namespaces, -> { where("type IS DISTINCT FROM ?", Namespaces::ProjectNamespace.sti_name) }
scope :without_project_namespaces, -> { where(Namespace.arel_table[:type].is_distinct_from(Namespaces::ProjectNamespace.sti_name)) }
scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) }
scope :include_route, -> { includes(:route) }
scope :by_parent, -> (parent) { where(parent_id: parent) }
@ -192,9 +192,9 @@ class Namespace < ApplicationRecord
# Returns an ActiveRecord::Relation.
def search(query, include_parents: false)
if include_parents
where(id: Route.for_routable_type(Namespace.name).fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name]]).select(:source_id))
without_project_namespaces.where(id: Route.for_routable_type(Namespace.name).fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name]]).select(:source_id))
else
fuzzy_search(query, [:path, :name])
without_project_namespaces.fuzzy_search(query, [:path, :name])
end
end

View file

@ -16,7 +16,7 @@
- if @breadcrumbs_extra_links
- @breadcrumbs_extra_links.each do |extra|
= breadcrumb_list_item link_to(extra[:text], extra[:link])
= render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :after
= render "layouts/nav/breadcrumbs/collapsed_inline_list", location: :after
- unless @skip_current_level_breadcrumb
%li
%h2.breadcrumbs-sub-title{ data: { qa_selector: 'breadcrumb_sub_title_content' } }

View file

@ -1,11 +0,0 @@
- dropdown_location = local_assigns.fetch(:location, nil)
- button_tooltip = local_assigns.fetch(:title, _("Show parent pages"))
- if defined?(@breadcrumb_dropdown_links) && @breadcrumb_dropdown_links.key?(dropdown_location)
%li.dropdown
%button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { toggle: "dropdown", container: "body" }, "aria-label": button_tooltip, title: button_tooltip }
= sprite_icon("ellipsis_h", size: 12)
= sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle")
.dropdown-menu
%ul
- @breadcrumb_dropdown_links[dropdown_location].each_with_index do |link, index|
%li{ style: "text-indent: #{[index * 16, 60].min}px;" }= link

View file

@ -0,0 +1,11 @@
- dropdown_location = local_assigns.fetch(:location, nil)
- button_tooltip = local_assigns.fetch(:title, _("Show all breadcrumbs"))
- if defined?(@breadcrumb_collapsed_links) && @breadcrumb_collapsed_links.key?(dropdown_location)
%li.expander
%button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { container: "body" }, "aria-label": button_tooltip, title: button_tooltip }
= sprite_icon("ellipsis_h", size: 12)
= sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle")
- @breadcrumb_collapsed_links[dropdown_location].each_with_index do |link, index|
%li{ :class => "gl-display-none! breadcrumbs-detail-item" }
= link
= sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle")

View file

@ -15,6 +15,7 @@
.d-flex.flex-column.flex-md-row.flex-grow-1.mb-lg-0.mb-md-2.mb-sm-0.w-100
- if type == :boards
= render "shared/boards/switcher", board: board
.js-new-board{ data: { multiple_issue_boards_available: parent.multiple_issue_boards_available?.to_s, can_admin_board: can?(current_user, :admin_issue_board, parent).to_s, } }
= form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form w-100' do
- if params[:search].present?
= hidden_field_tag :search, params[:search]

View file

@ -0,0 +1,8 @@
---
name: prominent_create_board_btn
introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72683"
rollout_issue_url: "https://gitlab.com/gitlab-org/gitlab/-/issues/343415"
milestone: "14.5"
type: experiment
group: group::product planning
default_enabled: false

View file

@ -1,8 +1,8 @@
---
name: additional_snowplow_tracking
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/12088
rollout_issue_url:
rollout_issue_url:
milestone: '11.11'
type: development
group: group::product intelligence
default_enabled: false
type: ops

View file

@ -23,6 +23,30 @@ as much as possible.
After a merge request has been approved, the pipeline would contain the full RSpec & Jest tests. This will ensure that all tests
have been run before a merge request is merged.
### Overview of the GitLab project test dependency
To understand how the minimal test jobs are executed, we need to understand the dependency between
GitLab code (frontend and backend) and the respective tests (Jest and RSpec).
This dependency can be visualized in the following diagram:
```mermaid
flowchart LR
subgraph frontend
fe["Frontend code"]--tested with-->jest
end
subgraph backend
be["Backend code"]--tested with-->rspec
end
be--generates-->fixtures["frontend fixtures"]
fixtures--used in-->jest
```
In summary:
- RSpec tests are dependent on the backend code.
- Jest tests are dependent on both frontend and backend code, the latter through the frontend fixtures.
### RSpec minimal jobs
#### Determining related RSpec test files in a merge request
@ -57,7 +81,7 @@ In this mode, `jest` would resolve all the dependencies of related to the change
In addition, there are a few circumstances where we would always run the full Jest tests:
- when the `pipeline:run-all-rspec` label is set on the merge request
- when the `pipeline:run-all-jest` label is set on the merge request
- when the merge request is created by an automation (e.g. Gitaly update or MR targeting a stable branch)
- when any CI config file is changed (i.e. `.gitlab-ci.yml` or `.gitlab/ci/**/*`)
- when any frontend "core" file is changed (i.e. `package.json`, `yarn.lock`, `babel.config.js`, `jest.config.*.js`, `config/helpers/**/*.js`)

View file

@ -4,10 +4,21 @@ module BulkImports
module Groups
module Loaders
class GroupLoader
def load(context, data)
return unless user_can_create_group?(context.current_user, data)
GroupCreationError = Class.new(StandardError)
group = ::Groups::CreateService.new(context.current_user, data).execute
def load(context, data)
path = data['path']
current_user = context.current_user
destination_namespace = context.entity.destination_namespace
raise(GroupCreationError, 'Path is missing') unless path.present?
raise(GroupCreationError, 'Destination is not a group') if user_namespace_destination?(destination_namespace)
raise(GroupCreationError, 'User not allowed to create group') unless user_can_create_group?(current_user, data)
raise(GroupCreationError, 'Group exists') if group_exists?(destination_namespace, path)
group = ::Groups::CreateService.new(current_user, data).execute
raise(GroupCreationError, group.errors.full_messages.to_sentence) if group.errors.any?
context.entity.update!(group: group)
@ -25,6 +36,18 @@ module BulkImports
Ability.allowed?(current_user, :create_group)
end
end
def group_exists?(destination_namespace, path)
full_path = destination_namespace.present? ? File.join(destination_namespace, path) : path
Group.find_by_full_path(full_path).present?
end
def user_namespace_destination?(destination_namespace)
return false unless destination_namespace.present?
Namespace.find_by_full_path(destination_namespace)&.user_namespace?
end
end
end
end

View file

@ -9,6 +9,8 @@ module Gitlab
@@logged_requests = Concurrent::Array.new
@@inject_headers = Concurrent::Hash.new
Request = Struct.new(:url, :status_code, :request_headers, :response_headers, :body, keyword_init: true)
# Resets the current request log and starts logging requests
def self.log_requests!(headers = {})
@@inject_headers.replace(headers)
@ -40,7 +42,7 @@ module Gitlab
full_body = +''
body.each { |b| full_body << b }
request = OpenStruct.new(
request = Request.new(
url: url,
status_code: status,
request_headers: request_headers,

View file

@ -18,7 +18,7 @@ module Gitlab
end
def options(group)
additional_features = Feature.enabled?(:additional_snowplow_tracking, group)
additional_features = Feature.enabled?(:additional_snowplow_tracking, group, type: :ops)
{
namespace: SNOWPLOW_NAMESPACE,
hostname: Gitlab::CurrentSettings.snowplow_collector_hostname,

View file

@ -5624,6 +5624,9 @@ msgstr ""
msgid "Boards|Failed to fetch blocking %{issuableType}s"
msgstr ""
msgid "Boards|New board"
msgstr ""
msgid "Boards|New epic"
msgstr ""
@ -23910,6 +23913,9 @@ msgstr ""
msgid "OnCallSchedules|Your schedule has been successfully created. To add individual users to this schedule, use the Add a rotation button. To enable notifications for this schedule, you must also create an %{linkStart}escalation policy%{linkEnd}."
msgstr ""
msgid "OnDemandScans|Could not fetch on-demand scans. Please refresh the page, or try again later."
msgstr ""
msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later."
msgstr ""
@ -31611,6 +31617,9 @@ msgstr ""
msgid "Show all activity"
msgstr ""
msgid "Show all breadcrumbs"
msgstr ""
msgid "Show all issues."
msgstr ""
@ -31665,12 +31674,6 @@ msgstr ""
msgid "Show one file at a time"
msgstr ""
msgid "Show parent pages"
msgstr ""
msgid "Show parent subgroups"
msgstr ""
msgid "Show the Closed list"
msgstr ""

View file

@ -0,0 +1,71 @@
import { mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import NewBoardButton from '~/boards/components/new_board_button.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { assignGitlabExperiment } from 'helpers/experimentation_helper';
import eventHub from '~/boards/eventhub';
const FEATURE = 'prominent_create_board_btn';
describe('NewBoardButton', () => {
let wrapper;
const createComponent = (args = {}) =>
extendedWrapper(
mount(NewBoardButton, {
provide: {
canAdminBoard: true,
multipleIssueBoardsAvailable: true,
...args,
},
}),
);
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
describe('control variant', () => {
assignGitlabExperiment(FEATURE, 'control');
it('renders nothing', () => {
wrapper = createComponent();
expect(wrapper.text()).toBe('');
});
});
describe('candidate variant', () => {
assignGitlabExperiment(FEATURE, 'candidate');
it('renders New board button when `candidate` variant', () => {
wrapper = createComponent();
expect(wrapper.text()).toBe('New board');
});
it('renders nothing when `canAdminBoard` is `false`', () => {
wrapper = createComponent({ canAdminBoard: false });
expect(wrapper.find(GlButton).exists()).toBe(false);
});
it('renders nothing when `multipleIssueBoardsAvailable` is `false`', () => {
wrapper = createComponent({ multipleIssueBoardsAvailable: false });
expect(wrapper.find(GlButton).exists()).toBe(false);
});
it('emits `showBoardModal` when button is clicked', () => {
jest.spyOn(eventHub, '$emit').mockImplementation();
wrapper = createComponent();
wrapper.find(GlButton).vm.$emit('click', { preventDefault: () => {} });
expect(eventHub.$emit).toHaveBeenCalledWith('showBoardModal', 'new');
});
});
});

View file

@ -92,7 +92,7 @@ RSpec.describe GroupsHelper do
shared_examples 'correct ancestor order' do
it 'outputs the groups in the correct order' do
expect(subject)
.to match(%r{<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*</li>.*<a.*>#{very_deep_nested_group.name}</a>}m)
.to match(%r{<li><a.*>#{deep_nested_group.name}.*</li>.*<a.*>#{very_deep_nested_group.name}</a>}m)
end
end

View file

@ -216,20 +216,6 @@ RSpec.describe UserCalloutsHelper do
context 'when the invite_members_banner has not been dismissed' do
it { is_expected.to eq(true) }
context 'when a user has dismissed this banner via cookies already' do
before do
helper.request.cookies["invite_#{group.id}_#{user.id}"] = 'true'
end
it { is_expected.to eq(false) }
it 'creates the callout from cookie', :aggregate_failures do
expect { subject }.to change { Users::GroupCallout.count }.by(1)
expect(Users::GroupCallout.last).to have_attributes(group_id: group.id,
feature_name: described_class::INVITE_MEMBERS_BANNER)
end
end
context 'when the group was just created' do
before do
flash[:notice] = "Group #{group.name} was successfully created"

View file

@ -8,7 +8,7 @@ RSpec.describe WikiHelper do
it 'sets the title for the show action' do
expect(helper).to receive(:breadcrumb_title).with(page.human_title)
expect(helper).to receive(:wiki_breadcrumb_dropdown_links).with(page.slug)
expect(helper).to receive(:wiki_breadcrumb_collapsed_links).with(page.slug)
expect(helper).to receive(:page_title).with(page.human_title, 'Wiki')
expect(helper).to receive(:add_to_breadcrumbs).with('Wiki', helper.wiki_path(page.wiki))
@ -17,7 +17,7 @@ RSpec.describe WikiHelper do
it 'sets the title for a custom action' do
expect(helper).to receive(:breadcrumb_title).with(page.human_title)
expect(helper).to receive(:wiki_breadcrumb_dropdown_links).with(page.slug)
expect(helper).to receive(:wiki_breadcrumb_collapsed_links).with(page.slug)
expect(helper).to receive(:page_title).with('Edit', page.human_title, 'Wiki')
expect(helper).to receive(:add_to_breadcrumbs).with('Wiki', helper.wiki_path(page.wiki))
@ -27,7 +27,7 @@ RSpec.describe WikiHelper do
it 'sets the title for an unsaved page' do
expect(page).to receive(:persisted?).and_return(false)
expect(helper).not_to receive(:breadcrumb_title)
expect(helper).not_to receive(:wiki_breadcrumb_dropdown_links)
expect(helper).not_to receive(:wiki_breadcrumb_collapsed_links)
expect(helper).to receive(:page_title).with('Wiki')
expect(helper).to receive(:add_to_breadcrumbs).with('Wiki', helper.wiki_path(page.wiki))

View file

@ -11,20 +11,66 @@ RSpec.describe BulkImports::Groups::Loaders::GroupLoader do
let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) }
let(:service_double) { instance_double(::Groups::CreateService) }
let(:data) { { foo: :bar } }
let(:data) { { 'path' => 'test' } }
subject { described_class.new }
context 'when path is missing' do
it 'raises an error' do
expect { subject.load(context, {}) }.to raise_error(described_class::GroupCreationError, 'Path is missing')
end
end
context 'when destination namespace is not a group' do
it 'raises an error' do
entity.update!(destination_namespace: user.namespace.path)
expect { subject.load(context, data) }.to raise_error(described_class::GroupCreationError, 'Destination is not a group')
end
end
context 'when group exists' do
it 'raises an error' do
group1 = create(:group)
group2 = create(:group, parent: group1)
entity.update!(destination_namespace: group1.full_path)
data = { 'path' => group2.path }
expect { subject.load(context, data) }.to raise_error(described_class::GroupCreationError, 'Group exists')
end
end
context 'when there are other group errors' do
it 'raises an error with those errors' do
group = ::Group.new
group.validate
expected_errors = group.errors.full_messages.to_sentence
expect(::Groups::CreateService)
.to receive(:new)
.with(context.current_user, data)
.and_return(service_double)
expect(service_double).to receive(:execute).and_return(group)
expect(entity).not_to receive(:update!)
expect { subject.load(context, data) }.to raise_error(described_class::GroupCreationError, expected_errors)
end
end
context 'when user can create group' do
shared_examples 'calls Group Create Service to create a new group' do
it 'calls Group Create Service to create a new group' do
group_double = instance_double(::Group)
expect(::Groups::CreateService)
.to receive(:new)
.with(context.current_user, data)
.and_return(service_double)
expect(service_double).to receive(:execute)
expect(entity).to receive(:update!)
expect(service_double).to receive(:execute).and_return(group_double)
expect(group_double).to receive(:errors).and_return([])
expect(entity).to receive(:update!).with(group: group_double)
subject.load(context, data)
end
@ -40,7 +86,7 @@ RSpec.describe BulkImports::Groups::Loaders::GroupLoader do
context 'when there is parent group' do
let(:parent) { create(:group) }
let(:data) { { 'parent_id' => parent.id } }
let(:data) { { 'parent_id' => parent.id, 'path' => 'test' } }
before do
allow(Ability).to receive(:allowed?).with(user, :create_subgroup, parent).and_return(true)
@ -55,7 +101,7 @@ RSpec.describe BulkImports::Groups::Loaders::GroupLoader do
it 'does not create new group' do
expect(::Groups::CreateService).not_to receive(:new)
subject.load(context, data)
expect { subject.load(context, data) }.to raise_error(described_class::GroupCreationError, 'User not allowed to create group')
end
end
@ -69,7 +115,7 @@ RSpec.describe BulkImports::Groups::Loaders::GroupLoader do
context 'when there is parent group' do
let(:parent) { create(:group) }
let(:data) { { 'parent_id' => parent.id } }
let(:data) { { 'parent_id' => parent.id, 'path' => 'test' } }
before do
allow(Ability).to receive(:allowed?).with(user, :create_subgroup, parent).and_return(false)

View file

@ -559,6 +559,25 @@ RSpec.describe Namespace do
it 'returns namespaces with a matching route path regardless of the casing' do
expect(described_class.search('PARENT-PATH/NEW-PATH', include_parents: true)).to eq([second_group])
end
context 'with project namespaces' do
let_it_be(:project) { create(:project, namespace: parent_group, path: 'some-new-path') }
let_it_be(:project_namespace) { create(:project_namespace, project: project) }
it 'does not return project namespace' do
search_result = described_class.search('path')
expect(search_result).not_to include(project_namespace)
expect(search_result).to match_array([first_group, parent_group, second_group])
end
it 'does not return project namespace when including parents' do
search_result = described_class.search('path', include_parents: true)
expect(search_result).not_to include(project_namespace)
expect(search_result).to match_array([first_group, parent_group, second_group])
end
end
end
describe '.with_statistics' do