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() {
return this.isClosed ? 'mobile-issue-close' : 'issue-open-m';
return this.isClosed ? 'issue-close' : 'issue-open-m';
},
statusText() {
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
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 }
msg = _('%{global_id} is not a valid ID for %{expected_type}.') % vars
raise Gitlab::Graphql::Errors::ArgumentError, msg

View File

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

View File

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

View File

@ -10,35 +10,41 @@ module Resolvers
def self.accept_assignee
argument :assignee_username, GraphQL::STRING_TYPE,
required: false,
description: 'Username of the assignee.'
required: false,
description: 'Username of the assignee.'
end
def self.accept_author
argument :author_username, GraphQL::STRING_TYPE,
required: false,
description: 'Username of the author.'
required: false,
description: 'Username of the author.'
end
def self.accept_reviewer
argument :reviewer_username, GraphQL::STRING_TYPE,
required: false,
description: 'Username of the reviewer.'
required: false,
description: 'Username of the reviewer.'
end
argument :iids, [GraphQL::STRING_TYPE],
required: false,
description: 'Array of IIDs of merge requests, for example `[1, 2]`.'
required: false,
description: 'Array of IIDs of merge requests, for example `[1, 2]`.'
argument :source_branches, [GraphQL::STRING_TYPE],
required: false,
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],
required: false,
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,
required: false,

View File

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

View File

@ -21,7 +21,10 @@ module Types
graphql_name(enum_mod.name) if use_name
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
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' } }
= new_path
- 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
- if diff_file.deleted_file?
@ -37,3 +37,4 @@
- if diff_file.stored_externally? && diff_file.external_storage == :lfs
%span.badge.label-lfs.gl-mr-2 LFS

View File

@ -33,7 +33,7 @@
Pipelines
%span.badge.badge-pill= @pipelines.size
%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
%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 '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,
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
.clearfix
.error-alert

View File

@ -1,7 +1,7 @@
.detail-page-header
.detail-page-header-body
.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!
= issue_closed_text(issuable, current_user)
.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"
msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
msgid "Please try again"
msgstr ""

View File

@ -11,6 +11,7 @@ module QA
view 'app/assets/javascripts/boards/components/board_form.vue' do
element :board_name_field
element :save_changes_button
end
view 'app/assets/javascripts/boards/components/board_list.vue' do
@ -23,10 +24,6 @@ module QA
element :create_new_board_button
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
element :labels_dropdown_content
end

View File

@ -8,8 +8,33 @@ module QA
element :issuable_create_button, required: true
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
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

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

View File

@ -56,10 +56,10 @@ RSpec.describe Mutations::DesignManagement::Upload do
.map { |f| RenameableUpload.unique_file(f) }
end
def creates_designs
def creates_designs(&block)
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)
end

View File

@ -7,6 +7,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
include SortingHelper
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(:current_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_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_6) { create(:labeled_merge_request, :unique_branches, labels: create_list(:label, 2, project: project), **common_attrs) }
let_it_be(:merge_request_with_milestone) { create(:merge_request, :unique_branches, **common_attrs, milestone: milestone) }
let_it_be(:other_project) { create(:project, :repository) }
let_it_be(:other_merge_request) { create(:merge_request, source_project: other_project, target_project: other_project) }
let_it_be(:merge_request_6) do
create(:labeled_merge_request, :unique_branches, **common_attrs, labels: create_list(:label, 2, project: project))
end
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_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
let(:queries_per_project) { 4 }
context 'no arguments' do
context 'without arguments' do
it 'returns all merge requests' do
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
it 'returns only merge requests that the current user can see' do
@ -57,7 +68,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
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
# 1 query for project_authorizations, and 1 for merge_requests
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)
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
results = batch_sync(max_queries: queries_per_project * 2) do
a = resolve_mr(project, iids: [iid_1])
@ -121,7 +132,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
end
context 'by source branches' do
context 'with source branches argument' do
it 'takes one argument' do
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
mrs = [merge_request_3, merge_request_4]
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)
end
end
context 'by target branches' do
context 'with target branches argument' do
it 'takes one argument' do
result = resolve_mr(project, target_branches: [merge_request_3.target_branch])
@ -153,7 +164,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
end
context 'by state' do
context 'with state argument' do
it 'takes one argument' do
result = resolve_mr(project, state: 'locked')
@ -161,7 +172,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
end
context 'by label' do
context 'with label argument' do
let_it_be(:label) { merge_request_6.labels.first }
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
context 'by merged_after and merged_before' do
context 'with merged_after and merged_before arguments' do
before do
merge_request_1.metrics.update!(merged_at: 10.days.ago)
end
@ -196,7 +207,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
end
context 'by milestone' do
context 'with milestone argument' do
it 'filters merge requests by milestone title' do
result = resolve_mr(project, milestone_title: milestone.title)
@ -212,7 +223,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
describe 'combinations' 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')

View File

@ -48,14 +48,14 @@ RSpec.describe GitlabSchema.types['AlertManagementPrometheusIntegration'] do
end
end
context 'group integration' do
describe 'a group integration' do
let_it_be(:group) { create(:group) }
let_it_be(:integration) { create(:prometheus_service, project: nil, group: group) }
# Since it is impossible to authorize the parent here, given that the
# 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
it 'is redacted' do
expect do

View File

@ -196,20 +196,20 @@ RSpec.describe Types::BaseObject do
end
# For example a batchloaded association
context 'a lazy list' do
describe 'a lazy list' do
it_behaves_like 'array member redaction', %w[lazyListOfYs]
end
# 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]
end
context 'an array connection of items' do
describe 'an array connection of items' do
it_behaves_like 'array member redaction', %w[arrayYsConn nodes]
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]
end
@ -222,7 +222,7 @@ RSpec.describe Types::BaseObject do
}
}
doc = ->(after) do
doc = lambda do |after|
GraphQL.parse(<<~GQL)
query {
x {
@ -238,9 +238,7 @@ RSpec.describe Types::BaseObject do
}
GQL
end
returned_items = ->(ids) do
ids.to_a.map { |id| eq({ 'id' => id }) }
end
returned_items = ->(ids) { ids.to_a.map { |id| eq({ 'id' => id }) } }
query = GraphQL::Query.new(test_schema, document: doc[nil], context: data)
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['field']).to eq('SECURE_ANALYZERS_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['size']).to eq('LARGE')
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
it "returns no configuration" do
project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED,
builds_access_level: ProjectFeature::DISABLED,
repository_access_level: ProjectFeature::PRIVATE)
project.project_feature.update!(
merge_requests_access_level: ProjectFeature::DISABLED,
builds_access_level: ProjectFeature::DISABLED,
repository_access_level: ProjectFeature::PRIVATE
)
secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration')
expect(secure_analyzers_prefix).to be_nil
@ -342,8 +345,13 @@ RSpec.describe GitlabSchema.types['Project'] do
let_it_be(:project) { create(:project, :public) }
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_import2) { create(:jira_import_state, :finished, project: project, jira_project_key: 'BB', created_at: 5.days.ago) }
let_it_be(:jira_import1) do
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
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
def initialize(user, found_object)
@user, @found_object = user, found_object
@user = user
@found_object = found_object
end
def find_object
@ -40,16 +41,12 @@ RSpec.describe Gitlab::Graphql::Authorize::AuthorizeResource do
before do
# don't allow anything by default
allow(Ability).to receive(:allowed?) do
false
end
allow(Ability).to receive(:allowed?).and_return(false)
end
context 'when the user is allowed to perform the action' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_the_thing, project) do
true
end
allow(Ability).to receive(:allowed?).with(user, :read_the_thing, project).and_return(true)
end
describe '#authorized_find!' do

View File

@ -3,10 +3,11 @@ require 'spec_helper'
RSpec.describe 'GraphQL' do
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
let(:expected_params) do
{
@ -51,19 +52,25 @@ RSpec.describe 'GraphQL' do
context 'when there is an error in the logger' 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
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::GraphqlLogger).to receive(:info)
expect(Gitlab::ErrorTracking)
.to receive(:track_and_raise_for_dev_exception).at_least(:once)
expect(Gitlab::GraphqlLogger)
.to receive(:info)
post_graphql(query, variables: {})
end
end
end
context 'invalid variables' do
context 'with invalid variables' do
it 'returns an error' do
post_graphql(query, variables: "This is not JSON")
@ -72,7 +79,7 @@ RSpec.describe 'GraphQL' do
end
end
context 'authentication', :allow_forgery_protection do
describe 'authentication', :allow_forgery_protection do
let(:user) { create(:user) }
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")
end
context 'token authentication' do
context 'with token authentication' do
let(:token) { create(:personal_access_token) }
before do
@ -119,7 +126,7 @@ RSpec.describe 'GraphQL' do
context 'when the personal access token has no api scope' 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 })
@ -136,7 +143,11 @@ RSpec.describe 'GraphQL' do
let(:user) { create(:user) }
let(:query) do
graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id))
graphql_query_for(
:project,
{ full_path: project.full_path },
'id'
)
end
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(:page_size) { 6 }
let(:issues_edges) { %w(data project issues edges) }
let(:end_cursor) { %w(data project issues pageInfo endCursor) }
let(:issues_edges) { %w[project issues edges] }
let(:end_cursor) { %w[project issues pageInfo endCursor] }
let(:query) do
<<~GRAPHQL
query project($fullPath: ID!, $first: Int, $after: String) {
@ -217,16 +228,10 @@ RSpec.describe 'GraphQL' do
GRAPHQL
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)
GitlabSchema.execute(
post_graphql(
query,
context: { current_user: nil },
current_user: nil,
variables: {
fullPath: project.full_path,
first: page_size,
@ -240,14 +245,16 @@ RSpec.describe 'GraphQL' do
expect(Gitlab::Graphql::Pagination::Keyset::QueryBuilder)
.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)
cursor = first_page.dig(*end_cursor)
expect(edges.count).to eq(6)
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)
expect(edges.count).to eq(4)