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