Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
27f3465d8a
commit
93b0b77287
|
@ -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];
|
||||||
|
|
|
@ -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">×</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>
|
|
|
@ -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,
|
|
||||||
});
|
|
|
@ -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>
|
|
|
@ -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;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue