Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-24 03:09:04 +00:00
parent 27f3465d8a
commit 93b0b77287
29 changed files with 180 additions and 538 deletions

View File

@ -238,7 +238,7 @@ export default {
: ''; : '';
}, },
statusIcon() { statusIcon() {
return this.isClosed ? 'mobile-issue-close' : 'issue-open-m'; return this.isClosed ? 'issue-close' : 'issue-open-m';
}, },
statusText() { statusText() {
return IssuableStatusText[this.issuableStatus]; return IssuableStatusText[this.issuableStatus];

View File

@ -1,146 +0,0 @@
<script>
/* eslint-disable vue/require-default-prop */
import { __ } from '~/locale';
export default {
name: 'DeprecatedModal', // use GlModal instead
props: {
id: {
type: String,
required: false,
},
title: {
type: String,
required: false,
},
text: {
type: String,
required: false,
},
hideFooter: {
type: Boolean,
required: false,
default: false,
},
kind: {
type: String,
required: false,
default: 'primary',
},
modalDialogClass: {
type: String,
required: false,
default: '',
},
closeKind: {
type: String,
required: false,
default: 'default',
},
closeButtonLabel: {
type: String,
required: false,
default: __('Cancel'),
},
primaryButtonLabel: {
type: String,
required: false,
default: '',
},
secondaryButtonLabel: {
type: String,
required: false,
default: '',
},
submitDisabled: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
btnKindClass() {
return {
[`btn-${this.kind}`]: true,
};
},
btnCancelKindClass() {
return {
[`btn-${this.closeKind}`]: true,
};
},
},
methods: {
emitCancel(event) {
this.$emit('cancel', event);
},
emitSubmit(event) {
this.$emit('submit', event);
},
},
};
</script>
<template>
<div class="modal-open">
<div :id="id" :class="id ? '' : 'd-block'" class="modal" role="dialog" tabindex="-1">
<div :class="modalDialogClass" class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<slot name="header">
<h4 class="modal-title float-left">{{ title }}</h4>
<button
type="button"
class="close float-right"
data-dismiss="modal"
:aria-label="__('Close')"
@click="emitCancel($event)"
>
<span aria-hidden="true">&times;</span>
</button>
</slot>
</div>
<div class="modal-body">
<slot :text="text" name="body">
<p>{{ text }}</p>
</slot>
</div>
<div v-if="!hideFooter" class="modal-footer">
<button
:class="btnCancelKindClass"
type="button"
class="btn"
data-dismiss="modal"
@click="emitCancel($event)"
>
{{ closeButtonLabel }}
</button>
<slot v-if="secondaryButtonLabel" name="secondary-button">
<button v-if="secondaryButtonLabel" type="button" class="btn" data-dismiss="modal">
{{ secondaryButtonLabel }}
</button>
</slot>
<button
v-if="primaryButtonLabel"
:disabled="submitDisabled"
:class="btnKindClass"
type="button"
class="btn js-primary-button"
data-dismiss="modal"
data-qa-selector="save_changes_button"
@click="emitSubmit($event)"
>
{{ primaryButtonLabel }}
</button>
</div>
</div>
</div>
</div>
<div v-if="!id" class="modal-backdrop fade show"></div>
</div>
</template>

View File

@ -1,21 +0,0 @@
import createEventHub from '~/helpers/event_hub_factory';
// see recaptcha_tags in app/views/shared/_recaptcha_form.html.haml
export const callbackName = 'recaptchaDialogCallback';
export const eventHub = createEventHub();
const throwDuplicateCallbackError = () => {
throw new Error(`${callbackName} is already defined!`);
};
if (window[callbackName]) {
throwDuplicateCallbackError();
}
const callback = () => eventHub.$emit('submit');
Object.defineProperty(window, callbackName, {
get: () => callback,
set: throwDuplicateCallbackError,
});

View File

@ -1,90 +0,0 @@
<script>
/* eslint-disable vue/no-v-html */
import DeprecatedModal from './deprecated_modal.vue';
import { eventHub } from './recaptcha_eventhub';
export default {
name: 'RecaptchaModal',
components: {
DeprecatedModal,
},
props: {
html: {
type: String,
required: false,
default: '',
},
},
data() {
return {
script: {},
scriptSrc: 'https://www.recaptcha.net/recaptcha/api.js',
};
},
watch: {
html() {
this.appendRecaptchaScript();
},
},
mounted() {
eventHub.$on('submit', this.submit);
if (this.html) {
this.appendRecaptchaScript();
}
},
beforeDestroy() {
eventHub.$off('submit', this.submit);
},
methods: {
appendRecaptchaScript() {
this.removeRecaptchaScript();
const script = document.createElement('script');
script.src = this.scriptSrc;
script.classList.add('js-recaptcha-script');
script.async = true;
script.defer = true;
this.script = script;
document.body.appendChild(script);
},
removeRecaptchaScript() {
if (this.script instanceof Element) this.script.remove();
},
close() {
this.removeRecaptchaScript();
this.$emit('close');
},
submit() {
this.$el.querySelector('form').submit();
},
},
};
</script>
<template>
<deprecated-modal
:hide-footer="true"
:title="__('Please solve the reCAPTCHA')"
kind="warning"
class="recaptcha-modal js-recaptcha-modal"
@cancel="close"
>
<div slot="body">
<p>{{ __('We want to be sure it is you, please confirm you are not a robot.') }}</p>
<div ref="recaptcha" v-html="html"></div>
</div>
</deprecated-modal>
</template>

View File

@ -1,36 +0,0 @@
import recaptchaModal from '../components/recaptcha_modal.vue';
export default {
data() {
return {
showRecaptcha: false,
recaptchaHTML: '',
};
},
components: {
recaptchaModal,
},
methods: {
openRecaptcha() {
this.showRecaptcha = true;
},
closeRecaptcha() {
this.showRecaptcha = false;
},
checkForSpam(data) {
if (!data.recaptcha_html) return data;
this.recaptchaHTML = data.recaptcha_html;
const spamError = new Error(data.error_message);
spamError.name = 'SpamError';
spamError.message = 'SpamError';
throw spamError;
},
},
};

View File

@ -124,7 +124,7 @@ class GitlabSchema < GraphQL::Schema
raise Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab ID." unless gid raise Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab ID." unless gid
if expected_type && !gid.model_class.ancestors.include?(expected_type) if expected_type && gid.model_class.ancestors.exclude?(expected_type)
vars = { global_id: global_id, expected_type: expected_type } vars = { global_id: global_id, expected_type: expected_type }
msg = _('%{global_id} is not a valid ID for %{expected_type}.') % vars msg = _('%{global_id} is not a valid ID for %{expected_type}.') % vars
raise Gitlab::Graphql::Errors::ArgumentError, msg raise Gitlab::Graphql::Errors::ArgumentError, msg

View File

@ -39,9 +39,7 @@ module Resolvers
as_single << block as_single << block
# Have we been called after defining the single version of this resolver? # Have we been called after defining the single version of this resolver?
if @single.present? @single.instance_exec(&block) if @single.present?
@single.instance_exec(&block)
end
end end
def self.as_single def self.as_single
@ -90,7 +88,7 @@ module Resolvers
def self.last def self.last
parent = self parent = self
@last ||= Class.new(self.single) do @last ||= Class.new(single) do
type parent.singular_type, null: true type parent.singular_type, null: true
def select_result(results) def select_result(results)

View File

@ -9,9 +9,9 @@ module Resolvers
type ::Types::MergeRequestType, null: true type ::Types::MergeRequestType, null: true
argument :iid, GraphQL::STRING_TYPE, argument :iid, GraphQL::STRING_TYPE,
required: true, required: true,
as: :iids, as: :iids,
description: 'IID of the merge request, for example `1`.' description: 'IID of the merge request, for example `1`.'
def no_results_possible?(args) def no_results_possible?(args)
project.nil? project.nil?

View File

@ -10,35 +10,41 @@ module Resolvers
def self.accept_assignee def self.accept_assignee
argument :assignee_username, GraphQL::STRING_TYPE, argument :assignee_username, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'Username of the assignee.' description: 'Username of the assignee.'
end end
def self.accept_author def self.accept_author
argument :author_username, GraphQL::STRING_TYPE, argument :author_username, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'Username of the author.' description: 'Username of the author.'
end end
def self.accept_reviewer def self.accept_reviewer
argument :reviewer_username, GraphQL::STRING_TYPE, argument :reviewer_username, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'Username of the reviewer.' description: 'Username of the reviewer.'
end end
argument :iids, [GraphQL::STRING_TYPE], argument :iids, [GraphQL::STRING_TYPE],
required: false, required: false,
description: 'Array of IIDs of merge requests, for example `[1, 2]`.' description: 'Array of IIDs of merge requests, for example `[1, 2]`.'
argument :source_branches, [GraphQL::STRING_TYPE], argument :source_branches, [GraphQL::STRING_TYPE],
required: false, required: false,
as: :source_branch, as: :source_branch,
description: 'Array of source branch names. All resolved merge requests will have one of these branches as their source.' description: <<~DESC
Array of source branch names.
All resolved merge requests will have one of these branches as their source.
DESC
argument :target_branches, [GraphQL::STRING_TYPE], argument :target_branches, [GraphQL::STRING_TYPE],
required: false, required: false,
as: :target_branch, as: :target_branch,
description: 'Array of target branch names. All resolved merge requests will have one of these branches as their target.' description: <<~DESC
Array of target branch names.
All resolved merge requests will have one of these branches as their target.
DESC
argument :state, ::Types::MergeRequestStateEnum, argument :state, ::Types::MergeRequestStateEnum,
required: false, required: false,

View File

@ -4,13 +4,21 @@ module Resolvers
class UserMergeRequestsResolverBase < MergeRequestsResolver class UserMergeRequestsResolverBase < MergeRequestsResolver
include ResolvesProject include ResolvesProject
argument :project_path, GraphQL::STRING_TYPE, argument :project_path,
required: false, type: GraphQL::STRING_TYPE,
description: 'The full-path of the project the authored merge requests should be in. Incompatible with projectId.' required: false,
description: <<~DESC
The full-path of the project the authored merge requests should be in.
Incompatible with projectId.
DESC
argument :project_id, ::Types::GlobalIDType[::Project], argument :project_id,
required: false, type: ::Types::GlobalIDType[::Project],
description: 'The global ID of the project the authored merge requests should be in. Incompatible with projectPath.' required: false,
description: <<~DESC
The global ID of the project the authored merge requests should be in.
Incompatible with projectPath.
DESC
attr_reader :project attr_reader :project
alias_method :user, :object alias_method :user, :object
@ -22,8 +30,7 @@ module Resolvers
load_project(project_path, project_id) load_project(project_path, project_id)
return early_return unless can_read_project? return early_return unless can_read_project?
elsif args[:iids].present? elsif args[:iids].present?
raise ::Gitlab::Graphql::Errors::ArgumentError, raise ::Gitlab::Graphql::Errors::ArgumentError, 'iids requires projectPath or projectId'
'iids requires projectPath or projectId'
end end
super(**args) super(**args)

View File

@ -21,7 +21,10 @@ module Types
graphql_name(enum_mod.name) if use_name graphql_name(enum_mod.name) if use_name
description(enum_mod.description) if use_description description(enum_mod.description) if use_description
enum_mod.definition.each { |key, content| value(key.to_s.upcase, **content) } enum_mod.definition.each do |key, content|
desc = content.delete(:description)
value(key.to_s.upcase, description: desc, **content)
end
end end
def value(*args, **kwargs, &block) def value(*args, **kwargs, &block)

View File

@ -23,7 +23,7 @@
%strong.file-title-name.has-tooltip.gl-word-break-all{ data: { title: diff_file.new_path, container: 'body' } } %strong.file-title-name.has-tooltip.gl-word-break-all{ data: { title: diff_file.new_path, container: 'body' } }
= new_path = new_path
- else - else
%strong.file-title-name.has-tooltip.gl-word-break-all{ data: { title: diff_file.file_path, container: 'body' } } %strong.file-title-name.has-tooltip.gl-word-break-all{ data: { title: diff_file.file_path, container: 'body', qa_selector: 'file_name_content' } }
= diff_file.file_path = diff_file.file_path
- if diff_file.deleted_file? - if diff_file.deleted_file?
@ -37,3 +37,4 @@
- if diff_file.stored_externally? && diff_file.external_storage == :lfs - if diff_file.stored_externally? && diff_file.external_storage == :lfs
%span.badge.label-lfs.gl-mr-2 LFS %span.badge.label-lfs.gl-mr-2 LFS

View File

@ -33,7 +33,7 @@
Pipelines Pipelines
%span.badge.badge-pill= @pipelines.size %span.badge.badge-pill= @pipelines.size
%li.diffs-tab %li.diffs-tab
= link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tabvue'} do = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tabvue', qa_selector: 'diffs_tab'} do
Changes Changes
%span.badge.badge-pill= @merge_request.diff_size %span.badge.badge-pill= @merge_request.diff_size

View File

@ -19,9 +19,10 @@
= render layout: 'shared/md_preview', locals: { url: preview_url, referenced_users: true } do = render layout: 'shared/md_preview', locals: { url: preview_url, referenced_users: true } do
= render 'shared/zen', f: form, attr: :description, = render 'shared/zen', f: form, attr: :description,
classes: 'note-textarea qa-issuable-form-description rspec-issuable-form-description', classes: 'note-textarea rspec-issuable-form-description',
placeholder: placeholder, placeholder: placeholder,
supports_quick_actions: supports_quick_actions supports_quick_actions: supports_quick_actions,
qa_selector: 'issuable_form_description'
= render 'shared/notes/hints', supports_quick_actions: supports_quick_actions = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
.clearfix .clearfix
.error-alert .error-alert

View File

@ -1,7 +1,7 @@
.detail-page-header .detail-page-header
.detail-page-header-body .detail-page-header-body
.issuable-status-box.status-box.status-box-issue-closed{ class: issue_status_visibility(issuable, status_box: :closed) } .issuable-status-box.status-box.status-box-issue-closed{ class: issue_status_visibility(issuable, status_box: :closed) }
= sprite_icon('mobile-issue-close', css_class: 'gl-display-block gl-sm-display-none!') = sprite_icon('issue-close', css_class: 'gl-display-block gl-sm-display-none!')
.gl-display-none.gl-sm-display-block! .gl-display-none.gl-sm-display-block!
= issue_closed_text(issuable, current_user) = issue_closed_text(issuable, current_user)
.issuable-status-box.status-box.status-box-open{ class: issue_status_visibility(issuable, status_box: :open) } .issuable-status-box.status-box.status-box-open{ class: issue_status_visibility(issuable, status_box: :open) }

View File

@ -22993,9 +22993,6 @@ msgstr ""
msgid "Please solve the captcha" msgid "Please solve the captcha"
msgstr "" msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
msgid "Please try again" msgid "Please try again"
msgstr "" msgstr ""

View File

@ -11,6 +11,7 @@ module QA
view 'app/assets/javascripts/boards/components/board_form.vue' do view 'app/assets/javascripts/boards/components/board_form.vue' do
element :board_name_field element :board_name_field
element :save_changes_button
end end
view 'app/assets/javascripts/boards/components/board_list.vue' do view 'app/assets/javascripts/boards/components/board_list.vue' do
@ -23,10 +24,6 @@ module QA
element :create_new_board_button element :create_new_board_button
end end
view 'app/assets/javascripts/vue_shared/components/deprecated_modal.vue' do
element :save_changes_button
end
view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue' do view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue' do
element :labels_dropdown_content element :labels_dropdown_content
end end

View File

@ -8,8 +8,33 @@ module QA
element :issuable_create_button, required: true element :issuable_create_button, required: true
end end
view 'app/views/shared/form_elements/_description.html.haml' do
element :issuable_form_description
end
view 'app/views/projects/merge_requests/show.html.haml' do
element :diffs_tab
end
view 'app/assets/javascripts/diffs/components/diff_file_header.vue' do
element :file_name_content
end
def create_merge_request def create_merge_request
click_element :issuable_create_button, Page::MergeRequest::Show click_element(:issuable_create_button, Page::MergeRequest::Show)
end
def has_description?(description)
has_element?(:issuable_form_description, text: description)
end
def click_diffs_tab
click_element(:diffs_tab)
click_element(:dismiss_popover_button) if has_element?(:dismiss_popover_button, wait: 1)
end
def has_file?(file_name)
has_element?(:file_name_content, text: file_name)
end end
end end
end end

View File

@ -1,73 +0,0 @@
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
const modalComponent = Vue.extend(DeprecatedModal);
describe('DeprecatedModal', () => {
let vm;
afterEach(() => {
vm.$destroy();
});
describe('props', () => {
describe('without primaryButtonLabel', () => {
beforeEach(() => {
vm = mountComponent(modalComponent, {
primaryButtonLabel: null,
});
});
it('does not render a primary button', () => {
expect(vm.$el.querySelector('.js-primary-button')).toBeNull();
});
});
describe('with id', () => {
describe('does not render a primary button', () => {
beforeEach(() => {
vm = mountComponent(modalComponent, {
id: 'my-modal',
});
});
it('assigns the id to the modal', () => {
expect(vm.$el.querySelector('#my-modal.modal')).not.toBeNull();
});
it('does not show the modal immediately', () => {
expect(vm.$el.querySelector('#my-modal.modal')).not.toHaveClass('show');
});
it('does not show a backdrop', () => {
expect(vm.$el.querySelector('modal-backdrop')).toBeNull();
});
});
});
it('works with data-toggle="modal"', () => {
setFixtures(`
<button id="modal-button" data-toggle="modal" data-target="#my-modal"></button>
<div id="modal-container"></div>
`);
const modalContainer = document.getElementById('modal-container');
const modalButton = document.getElementById('modal-button');
vm = mountComponent(
modalComponent,
{
id: 'my-modal',
},
modalContainer,
);
const modalElement = vm.$el.querySelector('#my-modal');
expect(modalElement).not.toHaveClass('show');
modalButton.click();
expect(modalElement).toHaveClass('show');
});
});
});

View File

@ -1,21 +0,0 @@
import { eventHub, callbackName } from '~/vue_shared/components/recaptcha_eventhub';
describe('reCAPTCHA event hub', () => {
// the following test case currently crashes
// see https://gitlab.com/gitlab-org/gitlab/issues/29192#note_217840035
// eslint-disable-next-line jest/no-disabled-tests
it.skip('throws an error for overriding the callback', () => {
expect(() => {
window[callbackName] = 'something';
}).toThrow();
});
it('triggering callback emits a submit event', () => {
const eventHandler = jest.fn();
eventHub.$once('submit', eventHandler);
window[callbackName]();
expect(eventHandler).toHaveBeenCalled();
});
});

View File

@ -1,35 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import { eventHub } from '~/vue_shared/components/recaptcha_eventhub';
import RecaptchaModal from '~/vue_shared/components/recaptcha_modal.vue';
describe('RecaptchaModal', () => {
const recaptchaFormId = 'recaptcha-form';
const recaptchaHtml = `<form id="${recaptchaFormId}"></form>`;
let wrapper;
const findRecaptchaForm = () => wrapper.find(`#${recaptchaFormId}`).element;
beforeEach(() => {
wrapper = shallowMount(RecaptchaModal, {
propsData: {
html: recaptchaHtml,
},
});
});
afterEach(() => {
wrapper.destroy();
});
it('submits the form if event hub emits submit event', () => {
const form = findRecaptchaForm();
jest.spyOn(form, 'submit').mockImplementation();
eventHub.$emit('submit');
expect(form.submit).toHaveBeenCalled();
});
});

View File

@ -84,9 +84,9 @@ RSpec.describe 'DeclarativePolicy authorization in GraphQL ' do
permissions = permission_collection permissions = permission_collection
query_factory do |qt| query_factory do |qt|
qt.field :item, type, qt.field :item, type,
null: true, null: true,
resolver: new_resolver(test_object), resolver: new_resolver(test_object),
authorize: permissions authorize: permissions
end end
end end
@ -123,8 +123,9 @@ RSpec.describe 'DeclarativePolicy authorization in GraphQL ' do
let(:type) do let(:type) do
permissions = permission_collection permissions = permission_collection
type_factory do |type| type_factory do |type|
type.field :name, GraphQL::STRING_TYPE, null: true, type.field :name, GraphQL::STRING_TYPE,
authorize: permissions null: true,
authorize: permissions
end end
end end
@ -201,9 +202,10 @@ RSpec.describe 'DeclarativePolicy authorization in GraphQL ' do
let(:query_type) do let(:query_type) do
query_factory do |query| query_factory do |query|
query.field :item, type, null: true, query.field :item, type,
resolver: resolver, null: true,
authorize: permission_2 resolver: resolver,
authorize: permission_2
end end
end end
@ -288,8 +290,12 @@ RSpec.describe 'DeclarativePolicy authorization in GraphQL ' do
let(:query_string) { '{ item(first: 1) { edges { node { name } } } }' } let(:query_string) { '{ item(first: 1) { edges { node { name } } } }' }
it 'only checks permissions for the first object' do it 'only checks permissions for the first object' do
expect(Ability).to receive(:allowed?).with(user, permission_single, test_object) { true } expect(Ability)
expect(Ability).not_to receive(:allowed?).with(user, permission_single, second_test_object) .to receive(:allowed?)
.with(user, permission_single, test_object)
.and_return(true)
expect(Ability)
.not_to receive(:allowed?).with(user, permission_single, second_test_object)
expect(subject.size).to eq(1) expect(subject.size).to eq(1)
end end
@ -330,10 +336,12 @@ RSpec.describe 'DeclarativePolicy authorization in GraphQL ' do
end end
let(:project_type) do |type| let(:project_type) do |type|
issues = Issue.where(project: [visible_project, other_project]).order(id: :asc)
type_factory do |type| type_factory do |type|
type.graphql_name 'FakeProjectType' type.graphql_name 'FakeProjectType'
type.field :test_issues, issue_type.connection_type, null: false, type.field :test_issues, issue_type.connection_type,
resolver: new_resolver(Issue.where(project: [visible_project, other_project]).order(id: :asc)) null: false,
resolver: new_resolver(issues)
end end
end end

View File

@ -56,10 +56,10 @@ RSpec.describe Mutations::DesignManagement::Upload do
.map { |f| RenameableUpload.unique_file(f) } .map { |f| RenameableUpload.unique_file(f) }
end end
def creates_designs def creates_designs(&block)
prior_count = DesignManagement::Design.count prior_count = DesignManagement::Design.count
expect { yield }.not_to raise_error expect(&block).not_to raise_error
expect(DesignManagement::Design.count).to eq(prior_count + files.size) expect(DesignManagement::Design.count).to eq(prior_count + files.size)
end end

View File

@ -7,6 +7,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
include SortingHelper include SortingHelper
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:other_project) { create(:project, :repository) }
let_it_be(:milestone) { create(:milestone, project: project) } let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let_it_be(:other_user) { create(:user) } let_it_be(:other_user) { create(:user) }
@ -16,10 +17,17 @@ RSpec.describe Resolvers::MergeRequestsResolver do
let_it_be(:merge_request_3) { create(:merge_request, :unique_branches, **common_attrs) } let_it_be(:merge_request_3) { create(:merge_request, :unique_branches, **common_attrs) }
let_it_be(:merge_request_4) { create(:merge_request, :unique_branches, :locked, **common_attrs) } let_it_be(:merge_request_4) { create(:merge_request, :unique_branches, :locked, **common_attrs) }
let_it_be(:merge_request_5) { create(:merge_request, :simple, :locked, **common_attrs) } let_it_be(:merge_request_5) { create(:merge_request, :simple, :locked, **common_attrs) }
let_it_be(:merge_request_6) { create(:labeled_merge_request, :unique_branches, labels: create_list(:label, 2, project: project), **common_attrs) } let_it_be(:merge_request_6) do
let_it_be(:merge_request_with_milestone) { create(:merge_request, :unique_branches, **common_attrs, milestone: milestone) } create(:labeled_merge_request, :unique_branches, **common_attrs, labels: create_list(:label, 2, project: project))
let_it_be(:other_project) { create(:project, :repository) } end
let_it_be(:other_merge_request) { create(:merge_request, source_project: other_project, target_project: other_project) }
let_it_be(:merge_request_with_milestone) do
create(:merge_request, :unique_branches, **common_attrs, milestone: milestone)
end
let_it_be(:other_merge_request) do
create(:merge_request, source_project: other_project, target_project: other_project)
end
let(:iid_1) { merge_request_1.iid } let(:iid_1) { merge_request_1.iid }
let(:iid_2) { merge_request_2.iid } let(:iid_2) { merge_request_2.iid }
@ -43,11 +51,14 @@ RSpec.describe Resolvers::MergeRequestsResolver do
# SELECT "project_features".* FROM "project_features" WHERE "project_features"."project_id" = 2 # SELECT "project_features".* FROM "project_features" WHERE "project_features"."project_id" = 2
let(:queries_per_project) { 4 } let(:queries_per_project) { 4 }
context 'no arguments' do context 'without arguments' do
it 'returns all merge requests' do it 'returns all merge requests' do
result = resolve_mr(project) result = resolve_mr(project)
expect(result).to contain_exactly(merge_request_1, merge_request_2, merge_request_3, merge_request_4, merge_request_5, merge_request_6, merge_request_with_milestone) expect(result).to contain_exactly(
merge_request_1, merge_request_2, merge_request_3, merge_request_4, merge_request_5,
merge_request_6, merge_request_with_milestone
)
end end
it 'returns only merge requests that the current user can see' do it 'returns only merge requests that the current user can see' do
@ -57,7 +68,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end end
end end
context 'by iid alone' do context 'with iid alone' do
it 'batch-resolves by target project full path and individual IID', :request_store do it 'batch-resolves by target project full path and individual IID', :request_store do
# 1 query for project_authorizations, and 1 for merge_requests # 1 query for project_authorizations, and 1 for merge_requests
result = batch_sync(max_queries: queries_per_project) do result = batch_sync(max_queries: queries_per_project) do
@ -83,7 +94,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
expect(result).to contain_exactly(merge_request_1, merge_request_2, merge_request_3) expect(result).to contain_exactly(merge_request_1, merge_request_2, merge_request_3)
end end
it 'can batch-resolve merge requests from different projects', :request_store, :use_clean_rails_memory_store_caching do it 'can batch-resolve merge requests from different projects', :request_store do
# 2 queries for project_authorizations, and 2 for merge_requests # 2 queries for project_authorizations, and 2 for merge_requests
results = batch_sync(max_queries: queries_per_project * 2) do results = batch_sync(max_queries: queries_per_project * 2) do
a = resolve_mr(project, iids: [iid_1]) a = resolve_mr(project, iids: [iid_1])
@ -121,7 +132,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end end
end end
context 'by source branches' do context 'with source branches argument' do
it 'takes one argument' do it 'takes one argument' do
result = resolve_mr(project, source_branches: [merge_request_3.source_branch]) result = resolve_mr(project, source_branches: [merge_request_3.source_branch])
@ -131,13 +142,13 @@ RSpec.describe Resolvers::MergeRequestsResolver do
it 'takes more than one argument' do it 'takes more than one argument' do
mrs = [merge_request_3, merge_request_4] mrs = [merge_request_3, merge_request_4]
branches = mrs.map(&:source_branch) branches = mrs.map(&:source_branch)
result = resolve_mr(project, source_branches: branches ) result = resolve_mr(project, source_branches: branches)
expect(result).to match_array(mrs) expect(result).to match_array(mrs)
end end
end end
context 'by target branches' do context 'with target branches argument' do
it 'takes one argument' do it 'takes one argument' do
result = resolve_mr(project, target_branches: [merge_request_3.target_branch]) result = resolve_mr(project, target_branches: [merge_request_3.target_branch])
@ -153,7 +164,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end end
end end
context 'by state' do context 'with state argument' do
it 'takes one argument' do it 'takes one argument' do
result = resolve_mr(project, state: 'locked') result = resolve_mr(project, state: 'locked')
@ -161,7 +172,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end end
end end
context 'by label' do context 'with label argument' do
let_it_be(:label) { merge_request_6.labels.first } let_it_be(:label) { merge_request_6.labels.first }
let_it_be(:with_label) { create(:labeled_merge_request, :closed, labels: [label], **common_attrs) } let_it_be(:with_label) { create(:labeled_merge_request, :closed, labels: [label], **common_attrs) }
@ -178,7 +189,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end end
end end
context 'by merged_after and merged_before' do context 'with merged_after and merged_before arguments' do
before do before do
merge_request_1.metrics.update!(merged_at: 10.days.ago) merge_request_1.metrics.update!(merged_at: 10.days.ago)
end end
@ -196,7 +207,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end end
end end
context 'by milestone' do context 'with milestone argument' do
it 'filters merge requests by milestone title' do it 'filters merge requests by milestone title' do
result = resolve_mr(project, milestone_title: milestone.title) result = resolve_mr(project, milestone_title: milestone.title)
@ -212,7 +223,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
describe 'combinations' do describe 'combinations' do
it 'requires all filters' do it 'requires all filters' do
create(:merge_request, :closed, source_project: project, target_project: project, source_branch: merge_request_4.source_branch) create(:merge_request, :closed, **common_attrs, source_branch: merge_request_4.source_branch)
result = resolve_mr(project, source_branches: [merge_request_4.source_branch], state: 'locked') result = resolve_mr(project, source_branches: [merge_request_4.source_branch], state: 'locked')

View File

@ -48,14 +48,14 @@ RSpec.describe GitlabSchema.types['AlertManagementPrometheusIntegration'] do
end end
end end
context 'group integration' do describe 'a group integration' do
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:integration) { create(:prometheus_service, project: nil, group: group) } let_it_be(:integration) { create(:prometheus_service, project: nil, group: group) }
# Since it is impossible to authorize the parent here, given that the # Since it is impossible to authorize the parent here, given that the
# project is nil, all fields should be redacted: # project is nil, all fields should be redacted:
described_class.fields.keys.each do |field_name| described_class.fields.each_key do |field_name|
context "field: #{field_name}" do context "field: #{field_name}" do
it 'is redacted' do it 'is redacted' do
expect do expect do

View File

@ -196,20 +196,20 @@ RSpec.describe Types::BaseObject do
end end
# For example a batchloaded association # For example a batchloaded association
context 'a lazy list' do describe 'a lazy list' do
it_behaves_like 'array member redaction', %w[lazyListOfYs] it_behaves_like 'array member redaction', %w[lazyListOfYs]
end end
# For example using a batchloader to map over a set of IDs # For example using a batchloader to map over a set of IDs
context 'a list of lazy items' do describe 'a list of lazy items' do
it_behaves_like 'array member redaction', %w[listOfLazyYs] it_behaves_like 'array member redaction', %w[listOfLazyYs]
end end
context 'an array connection of items' do describe 'an array connection of items' do
it_behaves_like 'array member redaction', %w[arrayYsConn nodes] it_behaves_like 'array member redaction', %w[arrayYsConn nodes]
end end
context 'an array connection of items, selecting edges' do describe 'an array connection of items, selecting edges' do
it_behaves_like 'array member redaction', %w[arrayYsConn edges node] it_behaves_like 'array member redaction', %w[arrayYsConn edges node]
end end
@ -222,7 +222,7 @@ RSpec.describe Types::BaseObject do
} }
} }
doc = ->(after) do doc = lambda do |after|
GraphQL.parse(<<~GQL) GraphQL.parse(<<~GQL)
query { query {
x { x {
@ -238,9 +238,7 @@ RSpec.describe Types::BaseObject do
} }
GQL GQL
end end
returned_items = ->(ids) do returned_items = ->(ids) { ids.to_a.map { |id| eq({ 'id' => id }) } }
ids.to_a.map { |id| eq({ 'id' => id }) }
end
query = GraphQL::Query.new(test_schema, document: doc[nil], context: data) query = GraphQL::Query.new(test_schema, document: doc[nil], context: data)
result = query.result.to_h result = query.result.to_h

View File

@ -106,7 +106,8 @@ RSpec.describe GitlabSchema.types['Project'] do
expect(secure_analyzers_prefix['type']).to eq('string') expect(secure_analyzers_prefix['type']).to eq('string')
expect(secure_analyzers_prefix['field']).to eq('SECURE_ANALYZERS_PREFIX') expect(secure_analyzers_prefix['field']).to eq('SECURE_ANALYZERS_PREFIX')
expect(secure_analyzers_prefix['label']).to eq('Image prefix') expect(secure_analyzers_prefix['label']).to eq('Image prefix')
expect(secure_analyzers_prefix['defaultValue']).to eq('registry.gitlab.com/gitlab-org/security-products/analyzers') expect(secure_analyzers_prefix['defaultValue'])
.to eq('registry.gitlab.com/gitlab-org/security-products/analyzers')
expect(secure_analyzers_prefix['value']).to eq('registry.gitlab.com/gitlab-org/security-products/analyzers') expect(secure_analyzers_prefix['value']).to eq('registry.gitlab.com/gitlab-org/security-products/analyzers')
expect(secure_analyzers_prefix['size']).to eq('LARGE') expect(secure_analyzers_prefix['size']).to eq('LARGE')
expect(secure_analyzers_prefix['options']).to be_nil expect(secure_analyzers_prefix['options']).to be_nil
@ -184,9 +185,11 @@ RSpec.describe GitlabSchema.types['Project'] do
context 'when repository is accessible only by team members' do context 'when repository is accessible only by team members' do
it "returns no configuration" do it "returns no configuration" do
project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED, project.project_feature.update!(
builds_access_level: ProjectFeature::DISABLED, merge_requests_access_level: ProjectFeature::DISABLED,
repository_access_level: ProjectFeature::PRIVATE) builds_access_level: ProjectFeature::DISABLED,
repository_access_level: ProjectFeature::PRIVATE
)
secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration') secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration')
expect(secure_analyzers_prefix).to be_nil expect(secure_analyzers_prefix).to be_nil
@ -342,8 +345,13 @@ RSpec.describe GitlabSchema.types['Project'] do
let_it_be(:project) { create(:project, :public) } let_it_be(:project) { create(:project, :public) }
context 'when project has Jira imports' do context 'when project has Jira imports' do
let_it_be(:jira_import1) { create(:jira_import_state, :finished, project: project, jira_project_key: 'AA', created_at: 2.days.ago) } let_it_be(:jira_import1) do
let_it_be(:jira_import2) { create(:jira_import_state, :finished, project: project, jira_project_key: 'BB', created_at: 5.days.ago) } create(:jira_import_state, :finished, project: project, jira_project_key: 'AA', created_at: 2.days.ago)
end
let_it_be(:jira_import2) do
create(:jira_import_state, :finished, project: project, jira_project_key: 'BB', created_at: 5.days.ago)
end
it 'retrieves the imports' do it 'retrieves the imports' do
expect(subject).to contain_exactly(jira_import1, jira_import2) expect(subject).to contain_exactly(jira_import1, jira_import2)

View File

@ -12,7 +12,8 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeResource do
authorize :read_the_thing authorize :read_the_thing
def initialize(user, found_object) def initialize(user, found_object)
@user, @found_object = user, found_object @user = user
@found_object = found_object
end end
def find_object def find_object
@ -40,16 +41,12 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeResource do
before do before do
# don't allow anything by default # don't allow anything by default
allow(Ability).to receive(:allowed?) do allow(Ability).to receive(:allowed?).and_return(false)
false
end
end end
context 'when the user is allowed to perform the action' do context 'when the user is allowed to perform the action' do
before do before do
allow(Ability).to receive(:allowed?).with(user, :read_the_thing, project) do allow(Ability).to receive(:allowed?).with(user, :read_the_thing, project).and_return(true)
true
end
end end
describe '#authorized_find!' do describe '#authorized_find!' do

View File

@ -3,10 +3,11 @@ require 'spec_helper'
RSpec.describe 'GraphQL' do RSpec.describe 'GraphQL' do
include GraphqlHelpers include GraphqlHelpers
include AfterNextHelpers
let(:query) { graphql_query_for('echo', text: 'Hello world' ) } let(:query) { graphql_query_for('echo', text: 'Hello world') }
context 'logging' do describe 'logging' do
shared_examples 'logging a graphql query' do shared_examples 'logging a graphql query' do
let(:expected_params) do let(:expected_params) do
{ {
@ -51,19 +52,25 @@ RSpec.describe 'GraphQL' do
context 'when there is an error in the logger' do context 'when there is an error in the logger' do
before do before do
allow_any_instance_of(Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer).to receive(:process_variables).and_raise(StandardError.new("oh noes!")) logger_analyzer = GitlabSchema.query_analyzers.find do |qa|
qa.is_a? Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer
end
allow(logger_analyzer).to receive(:process_variables)
.and_raise(StandardError.new("oh noes!"))
end end
it 'logs the exception in Sentry and continues with the request' do it 'logs the exception in Sentry and continues with the request' do
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once) expect(Gitlab::ErrorTracking)
expect(Gitlab::GraphqlLogger).to receive(:info) .to receive(:track_and_raise_for_dev_exception).at_least(:once)
expect(Gitlab::GraphqlLogger)
.to receive(:info)
post_graphql(query, variables: {}) post_graphql(query, variables: {})
end end
end end
end end
context 'invalid variables' do context 'with invalid variables' do
it 'returns an error' do it 'returns an error' do
post_graphql(query, variables: "This is not JSON") post_graphql(query, variables: "This is not JSON")
@ -72,7 +79,7 @@ RSpec.describe 'GraphQL' do
end end
end end
context 'authentication', :allow_forgery_protection do describe 'authentication', :allow_forgery_protection do
let(:user) { create(:user) } let(:user) { create(:user) }
it 'allows access to public data without authentication' do it 'allows access to public data without authentication' do
@ -99,7 +106,7 @@ RSpec.describe 'GraphQL' do
expect(graphql_data['echo']).to eq("\"#{user.username}\" says: Hello world") expect(graphql_data['echo']).to eq("\"#{user.username}\" says: Hello world")
end end
context 'token authentication' do context 'with token authentication' do
let(:token) { create(:personal_access_token) } let(:token) { create(:personal_access_token) }
before do before do
@ -119,7 +126,7 @@ RSpec.describe 'GraphQL' do
context 'when the personal access token has no api scope' do context 'when the personal access token has no api scope' do
it 'does not log the user in' do it 'does not log the user in' do
token.update(scopes: [:read_user]) token.update!(scopes: [:read_user])
post_graphql(query, headers: { 'PRIVATE-TOKEN' => token.token }) post_graphql(query, headers: { 'PRIVATE-TOKEN' => token.token })
@ -136,7 +143,11 @@ RSpec.describe 'GraphQL' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:query) do let(:query) do
graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id)) graphql_query_for(
:project,
{ full_path: project.full_path },
'id'
)
end end
before do before do
@ -202,8 +213,8 @@ RSpec.describe 'GraphQL' do
let_it_be(:issues) { create_list(:issue, 10, project: project, created_at: Time.now.change(usec: 200)) } let_it_be(:issues) { create_list(:issue, 10, project: project, created_at: Time.now.change(usec: 200)) }
let(:page_size) { 6 } let(:page_size) { 6 }
let(:issues_edges) { %w(data project issues edges) } let(:issues_edges) { %w[project issues edges] }
let(:end_cursor) { %w(data project issues pageInfo endCursor) } let(:end_cursor) { %w[project issues pageInfo endCursor] }
let(:query) do let(:query) do
<<~GRAPHQL <<~GRAPHQL
query project($fullPath: ID!, $first: Int, $after: String) { query project($fullPath: ID!, $first: Int, $after: String) {
@ -217,16 +228,10 @@ RSpec.describe 'GraphQL' do
GRAPHQL GRAPHQL
end end
# TODO: Switch this to use `post_graphql`
# This is not performing an actual GraphQL request because the
# variables end up being strings when passed through the `post_graphql`
# helper.
#
# https://gitlab.com/gitlab-org/gitlab/-/issues/222432
def execute_query(after: nil) def execute_query(after: nil)
GitlabSchema.execute( post_graphql(
query, query,
context: { current_user: nil }, current_user: nil,
variables: { variables: {
fullPath: project.full_path, fullPath: project.full_path,
first: page_size, first: page_size,
@ -240,14 +245,16 @@ RSpec.describe 'GraphQL' do
expect(Gitlab::Graphql::Pagination::Keyset::QueryBuilder) expect(Gitlab::Graphql::Pagination::Keyset::QueryBuilder)
.to receive(:new).with(anything, anything, hash_including('created_at'), anything).and_call_original .to receive(:new).with(anything, anything, hash_including('created_at'), anything).and_call_original
first_page = execute_query execute_query
first_page = graphql_data
edges = first_page.dig(*issues_edges) edges = first_page.dig(*issues_edges)
cursor = first_page.dig(*end_cursor) cursor = first_page.dig(*end_cursor)
expect(edges.count).to eq(6) expect(edges.count).to eq(6)
expect(edges.last['node']['iid']).to eq(issues[4].iid.to_s) expect(edges.last['node']['iid']).to eq(issues[4].iid.to_s)
second_page = execute_query(after: cursor) execute_query(after: cursor)
second_page = graphql_data
edges = second_page.dig(*issues_edges) edges = second_page.dig(*issues_edges)
expect(edges.count).to eq(4) expect(edges.count).to eq(4)