Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-02-20 09:08:30 +00:00
parent 24e54a8f10
commit bd28d0fa02
59 changed files with 881 additions and 132 deletions

View File

@ -0,0 +1,89 @@
<script>
import {
GlAccordionItem,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlTokenSelector,
GlFormCombobox,
} from '@gitlab/ui';
import { mapState } from 'vuex';
import { i18n } from '../constants';
export default {
i18n,
components: {
GlAccordionItem,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlFormCombobox,
GlTokenSelector,
},
props: {
tagOptions: {
type: Array,
required: true,
},
job: {
type: Object,
required: true,
},
isNameValid: {
type: Boolean,
required: true,
},
isScriptValid: {
type: Boolean,
required: true,
},
},
computed: {
...mapState(['availableStages']),
},
};
</script>
<template>
<gl-accordion-item :title="$options.i18n.JOB_SETUP" visible>
<gl-form-group
:invalid-feedback="$options.i18n.THIS_FIELD_IS_REQUIRED"
:state="isNameValid"
:label="$options.i18n.JOB_NAME"
>
<gl-form-input
:value="job.name"
:state="isNameValid"
data-testid="job-name-input"
@input="$emit('update-job', 'name', $event)"
/>
</gl-form-group>
<gl-form-combobox
:value="job.stage"
:token-list="availableStages"
:label-text="$options.i18n.STAGE"
data-testid="job-stage-input"
@input="$emit('update-job', 'stage', $event)"
/>
<gl-form-group
:invalid-feedback="$options.i18n.THIS_FIELD_IS_REQUIRED"
:state="isScriptValid"
:label="$options.i18n.SCRIPT"
>
<gl-form-textarea
:value="job.script"
:state="isScriptValid"
:no-resize="false"
data-testid="job-script-input"
@input="$emit('update-job', 'script', $event)"
/>
</gl-form-group>
<gl-form-group :label="$options.i18n.TAGS">
<gl-token-selector
:dropdown-items="tagOptions"
:selected-tokens="job.tags"
data-testid="job-tags-input"
@input="$emit('update-job', 'tags', $event)"
/>
</gl-form-group>
</gl-accordion-item>
</template>

View File

@ -1,7 +1,38 @@
import { s__ } from '~/locale';
import { __, s__ } from '~/locale';
export const DRAWER_CONTAINER_CLASS = '.content-wrapper';
export const JOB_TEMPLATE = {
name: '',
stage: '',
script: '',
tags: [],
image: {
name: '',
entrypoint: '',
},
services: [
{
name: '',
entrypoint: '',
},
],
artifacts: {
paths: [''],
exclude: [''],
},
cache: {
paths: [''],
key: '',
},
};
export const i18n = {
ADD_JOB: s__('JobAssistant|Add job'),
SCRIPT: s__('JobAssistant|Script'),
JOB_NAME: s__('JobAssistant|Job name'),
JOB_SETUP: s__('JobAssistant|Job Setup'),
STAGE: s__('JobAssistant|Stage (optional)'),
TAGS: s__('JobAssistant|Tags (optional)'),
THIS_FIELD_IS_REQUIRED: __('This field is required'),
};

View File

@ -1,13 +1,23 @@
<script>
import { GlDrawer, GlButton } from '@gitlab/ui';
import { GlDrawer, GlAccordion, GlButton } from '@gitlab/ui';
import { stringify } from 'yaml';
import { mapMutations, mapState } from 'vuex';
import { set, omit, trim } from 'lodash';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { DRAWER_CONTAINER_CLASS, i18n } from './constants';
import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub';
import { UPDATE_CI_CONFIG } from '~/ci/pipeline_editor/store/mutation_types';
import getAllRunners from '~/ci/runner/graphql/list/all_runners.query.graphql';
import { DRAWER_CONTAINER_CLASS, JOB_TEMPLATE, i18n } from './constants';
import { removeEmptyObj, trimFields } from './utils';
import JobSetupItem from './accordion_items/job_setup_item.vue';
export default {
i18n,
components: {
GlDrawer,
GlAccordion,
GlButton,
JobSetupItem,
},
props: {
isVisible: {
@ -21,15 +31,84 @@ export default {
default: 200,
},
},
data() {
return {
isNameValid: true,
isScriptValid: true,
job: JSON.parse(JSON.stringify(JOB_TEMPLATE)),
};
},
apollo: {
runners: {
query: getAllRunners,
update(data) {
return data?.runners?.nodes || [];
},
},
},
computed: {
...mapState(['currentCiFileContent']),
tagOptions() {
const options = [];
this.runners?.forEach((runner) => options.push(...runner.tagList));
return [...new Set(options)].map((tag) => {
return {
id: tag,
name: tag,
};
});
},
drawerHeightOffset() {
return getContentWrapperHeight(DRAWER_CONTAINER_CLASS);
},
},
methods: {
...mapMutations({
updateCiConfig: UPDATE_CI_CONFIG,
}),
closeDrawer() {
this.clearJob();
this.$emit('close-job-assistant-drawer');
},
addCiConfig() {
this.isNameValid = this.validate(this.job.name);
this.isScriptValid = this.validate(this.job.script);
if (!this.isNameValid || !this.isScriptValid) {
return;
}
const newJobString = this.generateYmlString();
this.updateCiConfig(`${this.currentCiFileContent}\n${newJobString}`);
eventHub.$emit(SCROLL_EDITOR_TO_BOTTOM);
this.closeDrawer();
},
generateYmlString() {
let job = JSON.parse(JSON.stringify(this.job));
const jobName = job.name;
job = omit(job, ['name']);
job.tags = job.tags.map((tag) => tag.name); // Tag item is originally an option object, we need a string here to match `.gitlab-ci.yml` rules
const cleanedJob = trimFields(removeEmptyObj(job));
return stringify({ [jobName]: cleanedJob });
},
clearJob() {
this.job = JSON.parse(JSON.stringify(JOB_TEMPLATE));
this.isNameValid = true;
this.isScriptValid = true;
},
updateJob(key, value) {
set(this.job, key, value);
if (key === 'name') {
this.isNameValid = this.validate(this.job.name);
}
if (key === 'script') {
this.isScriptValid = this.validate(this.job.script);
}
},
validate(value) {
return trim(value) !== '';
},
},
};
</script>
@ -44,6 +123,15 @@ export default {
<template #title>
<h2 class="gl-m-0 gl-font-lg">{{ $options.i18n.ADD_JOB }}</h2>
</template>
<gl-accordion :header-level="3">
<job-setup-item
:tag-options="tagOptions"
:job="job"
:is-name-valid="isNameValid"
:is-script-valid="isScriptValid"
@update-job="updateJob"
/>
</gl-accordion>
<template #footer>
<div class="gl-display-flex gl-justify-content-end">
<gl-button
@ -51,11 +139,15 @@ export default {
class="gl-mr-3"
data-testid="cancel-button"
@click="closeDrawer"
>{{ __('Cancel') }}</gl-button
>
<gl-button category="primary" variant="confirm" data-testid="confirm-button">{{
__('Add')
}}</gl-button>
>{{ __('Cancel') }}
</gl-button>
<gl-button
category="primary"
variant="confirm"
data-testid="confirm-button"
@click="addCiConfig"
>{{ __('Add') }}
</gl-button>
</div>
</template>
</gl-drawer>

View File

@ -0,0 +1,22 @@
import { isEmpty, isObject, isArray, isString, reject, omitBy, mapValues, map, trim } from 'lodash';
const isEmptyValue = (val) => (isObject(val) || isString(val)) && isEmpty(val);
const trimText = (val) => (isString(val) ? trim(val) : val);
export const removeEmptyObj = (obj) => {
if (isArray(obj)) {
return reject(map(obj, removeEmptyObj), isEmptyValue);
} else if (isObject(obj)) {
return omitBy(mapValues(obj, removeEmptyObj), isEmptyValue);
}
return obj;
};
export const trimFields = (data) => {
if (isArray(data)) {
return data.map(trimFields);
} else if (isObject(data)) {
return mapValues(data, trimFields);
}
return trimText(data);
};

View File

@ -0,0 +1,5 @@
import createEventHub from '~/helpers/event_hub_factory';
export default createEventHub();
export const SCROLL_EDITOR_TO_BOTTOM = Symbol('scrollEditorToBottom');

View File

@ -12,6 +12,7 @@ import getPipelineEtag from './graphql/queries/client/pipeline_etag.query.graphq
import { resolvers } from './graphql/resolvers';
import typeDefs from './graphql/typedefs.graphql';
import PipelineEditorApp from './pipeline_editor_app.vue';
import createStore from './store';
export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
const el = document.querySelector(selector);
@ -111,8 +112,11 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
},
});
const store = createStore();
return new Vue({
el,
store,
apolloProvider,
provide: {
ciConfigPath,

View File

@ -1,10 +1,12 @@
<script>
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import { mapState, mapMutations } from 'vuex';
import { parse } from 'yaml';
import { fetchPolicies } from '~/lib/graphql';
import { mergeUrlParams, queryToObject, redirectTo } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
import { UPDATE_CI_CONFIG, UPDATE_AVAILABLE_STAGES } from './store/mutation_types';
import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue';
import PipelineEditorEmptyState from './components/ui/pipeline_editor_empty_state.vue';
@ -44,7 +46,6 @@ export default {
data() {
return {
ciConfigData: {},
currentCiFileContent: '',
failureType: null,
failureReasons: [],
hasBranchLoaded: false,
@ -94,7 +95,7 @@ export default {
const fileContent = rawBlob ?? '';
this.lastCommittedContent = fileContent;
this.currentCiFileContent = fileContent;
this.updateCiConfig(fileContent);
// If rawBlob is defined and returns a string, it means that there is
// a CI config file with empty content. If `rawBlob` is not defined
@ -155,6 +156,10 @@ export default {
this.isLintUnavailable = false;
}
}
if (data?.ciConfig?.mergedYaml) {
this.updateAvailableStages(parse(data.ciConfig.mergedYaml).stages);
}
},
error() {
// We are not using `reportFailure` here because we don't
@ -231,6 +236,7 @@ export default {
},
},
computed: {
...mapState(['currentCiFileContent']),
hasUnsavedChanges() {
return this.lastCommittedContent !== this.currentCiFileContent;
},
@ -294,6 +300,10 @@ export default {
this.checkShouldSkipStartScreen();
},
methods: {
...mapMutations({
updateCiConfig: UPDATE_CI_CONFIG,
updateAvailableStages: UPDATE_AVAILABLE_STAGES,
}),
checkShouldSkipStartScreen() {
const params = queryToObject(window.location.search);
this.shouldSkipStartScreen = Boolean(params?.add_new_config_file);
@ -344,7 +354,7 @@ export default {
},
resetContent() {
this.showResetConfirmationModal = false;
this.currentCiFileContent = this.lastCommittedContent;
this.updateCiConfig(this.lastCommittedContent);
},
setAppStatus(appStatus) {
if (EDITOR_APP_VALID_STATUSES.includes(appStatus)) {
@ -361,9 +371,6 @@ export default {
showErrorAlert({ type, reasons = [] }) {
this.reportFailure(type, reasons);
},
updateCiConfig(ciFileContent) {
this.currentCiFileContent = ciFileContent;
},
updateCommitSha() {
this.isFetchingCommitSha = true;
this.$apollo.queries.commitSha.refetch();

View File

@ -0,0 +1,12 @@
import Vue from 'vue';
import Vuex from 'vuex';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
mutations,
state: state(),
});

View File

@ -0,0 +1,2 @@
export const UPDATE_CI_CONFIG = 'UPDATE_CI_CONFIG';
export const UPDATE_AVAILABLE_STAGES = 'UPDATE_AVAILABLE_STAGES';

View File

@ -0,0 +1,10 @@
import * as types from './mutation_types';
export default {
[types.UPDATE_CI_CONFIG](state, content) {
state.currentCiFileContent = content;
},
[types.UPDATE_AVAILABLE_STAGES](state, stages) {
state.availableStages = stages || [];
},
};

View File

@ -0,0 +1,4 @@
export default () => ({
currentCiFileContent: '',
availableStages: [],
});

View File

@ -1,18 +1,50 @@
# frozen_string_literal: true
class AbuseReportsFinder
attr_reader :params
attr_reader :params, :reports
def initialize(params = {})
@params = params
@reports = AbuseReport.all
end
def execute
reports = AbuseReport.all
reports = reports.by_user(params[:user_id]) if params[:user_id].present?
filter_reports
reports.with_order_id_desc
.with_users
.page(params[:page])
end
private
def filter_reports
filter_by_user_id
filter_by_status
filter_by_category
end
def filter_by_status
return unless params[:status].present?
case params[:status]
when 'open'
@reports = @reports.open
when 'closed'
@reports = @reports.closed
end
end
def filter_by_category
return unless params[:category].present?
@reports = @reports.by_category(params[:category])
end
def filter_by_user_id
return unless params[:user_id].present?
@reports = @reports.by_user_id(params[:user_id])
end
end

View File

@ -42,7 +42,8 @@ class AbuseReport < ApplicationRecord
before_validation :filter_empty_strings_from_links_to_spam
validate :links_to_spam_contains_valid_urls
scope :by_user, ->(user) { where(user_id: user) }
scope :by_user_id, ->(id) { where(user_id: id) }
scope :by_category, ->(category) { where(category: category) }
scope :with_users, -> { includes(:reporter, :user) }
enum category: {
@ -56,6 +57,11 @@ class AbuseReport < ApplicationRecord
other: 8
}
enum status: {
open: 1,
closed: 2
}
# For CacheMarkdownField
alias_method :author, :reporter

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Admin
class AbuseReportEntity < Grape::Entity
expose :category
expose :updated_at
expose :reported_user do |report|
UserEntity.represent(report.user, only: [:name])
end
expose :reporter do |report|
UserEntity.represent(report.reporter, only: [:name])
end
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
module Admin
class AbuseReportSerializer < BaseSerializer
entity Admin::AbuseReportEntity
end
end

View File

@ -2,11 +2,6 @@
module Issues
class AfterCreateService < Issues::BaseService
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
def execute(issue)
todo_service.new_issue(issue, current_user)
delete_milestone_total_issue_counter_cache(issue.milestone)

View File

@ -6,6 +6,10 @@ module Issues
include IncidentManagement::UsageData
include IssueTypeHelpers
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
def hook_data(issue, action, old_associations: {})
hook_data = issue.to_hook_data(current_user, old_associations: old_associations)
hook_data[:object_attributes][:action] = action
@ -33,6 +37,14 @@ module Issues
private
# overriding this because IssuableBaseService#constructor_container_arg returns { project: value }
# Issues::ReopenService constructor signature is different now, it takes container instead of project also
# IssuableBaseService#change_state dynamically picks one of the `Issues::ReopenService`, `Epics::ReopenService` or
# MergeRequests::ReopenService, so we need this method to return { }container: value } for Issues::ReopenService
def self.constructor_container_arg(value)
{ container: value }
end
def find_work_item_type_id(issue_type)
work_item_type = WorkItems::Type.default_by_type(issue_type)
work_item_type ||= WorkItems::Type.default_issue_type

View File

@ -4,11 +4,6 @@ module Issues
class BuildService < Issues::BaseService
include ResolveDiscussions
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
def execute
filter_resolve_discussion_params

View File

@ -2,11 +2,6 @@
module Issues
class CloseService < Issues::BaseService
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
# Closes the supplied issue if the current user is able to do so.
def execute(issue, commit: nil, notifications: true, system_note: true, skip_authorization: false)
return issue unless can_close?(issue, skip_authorization: skip_authorization)
@ -56,11 +51,6 @@ module Issues
private
# TODO: remove once MergeRequests::CloseService or IssuableBaseService method is changed.
def self.constructor_container_arg(value)
{ container: value }
end
def can_close?(issue, skip_authorization: false)
skip_authorization || can?(current_user, :update_issue, issue) || issue.is_a?(ExternalIssue)
end

View File

@ -15,9 +15,10 @@ module Issues
# SpamParams constructor are not otherwise available, spam_params: must be explicitly passed as nil.
def initialize(container:, spam_params:, current_user: nil, params: {}, build_service: nil)
@extra_params = params.delete(:extra_params) || {}
super(project: container, current_user: current_user, params: params)
super(container: container, current_user: current_user, params: params)
@spam_params = spam_params
@build_service = build_service || BuildService.new(container: project, current_user: current_user, params: params)
@build_service = build_service ||
BuildService.new(container: container, current_user: current_user, params: params)
end
def execute(skip_system_notes: false)
@ -100,10 +101,6 @@ module Issues
private
def self.constructor_container_arg(value)
{ container: value }
end
def handle_quick_actions(issue)
# Do not handle quick actions unless the work item is the default Issue.
# The available quick actions for a work item depend on its type and widgets.

View File

@ -2,11 +2,6 @@
module Issues
class DuplicateService < Issues::BaseService
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
def execute(duplicate_issue, canonical_issue)
return if canonical_issue == duplicate_issue
return unless can?(current_user, :update_issue, duplicate_issue)

View File

@ -2,11 +2,6 @@
module Issues
class ReferencedMergeRequestsService < Issues::BaseService
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
# rubocop: disable CodeReuse/ActiveRecord
def execute(issue)
referenced = referenced_merge_requests(issue)

View File

@ -4,11 +4,6 @@
# those with a merge request open referencing the current issue.
module Issues
class RelatedBranchesService < Issues::BaseService
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
def execute(issue)
branch_names_with_mrs = branches_with_merge_request_for(issue)
branches = branches_with_iid_of(issue).reject { |b| branch_names_with_mrs.include?(b[:name]) }

View File

@ -2,11 +2,6 @@
module Issues
class ReopenService < Issues::BaseService
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
def execute(issue, skip_authorization: false)
return issue unless can_reopen?(issue, skip_authorization: skip_authorization)
@ -27,14 +22,6 @@ module Issues
private
# overriding this because IssuableBaseService#constructor_container_arg returns { project: value }
# Issues::ReopenService constructor signature is different now, it takes container instead of project also
# IssuableBaseService#change_state dynamically picks one of the `Issues::ReopenService`, `Epics::ReopenService` or
# MergeRequests::ReopenService, so we need this method to return { }container: value } for Issues::ReopenService
def self.constructor_container_arg(value)
{ container: value }
end
def can_reopen?(issue, skip_authorization: false)
skip_authorization || can?(current_user, :reopen_issue, issue)
end

View File

@ -4,11 +4,6 @@ module Issues
class ReorderService < Issues::BaseService
include Gitlab::Utils::StrongMemoize
# TODO: this is to be removed once we get to rename the IssuableBaseService project param to container
def initialize(container:, current_user: nil, params: {})
super(project: container, current_user: current_user, params: params)
end
def execute(issue)
return false unless can?(current_user, :update_issue, issue)
return false unless move_between_ids

View File

@ -6,7 +6,7 @@ module Issues
# necessary in many cases, and we don't want to require every caller to explicitly pass it as nil
# to disable spam checking.
def initialize(container:, current_user: nil, params: {}, spam_params: nil)
super(project: container, current_user: current_user, params: params)
super(container: container, current_user: current_user, params: params)
@spam_params = spam_params
end
@ -116,15 +116,6 @@ module Issues
attr_reader :spam_params
# TODO: remove this once MergeRequests::UpdateService#initialize is changed to take container as named argument.
#
# Issues::UpdateService is used together with MergeRequests::UpdateService in Mutations::Assignable#assign! method
# however MergeRequests::UpdateService#initialize still takes `project` as param and Issues::UpdateService is being
# changed to take `container` as param. So we are adding this workaround in the meantime.
def self.constructor_container_arg(value)
{ container: value }
end
def handle_quick_actions(issue)
# Do not handle quick actions unless the work item is the default Issue.
# The available quick actions for a work item depend on its type and widgets.

View File

@ -3,7 +3,7 @@
module Issues
class ZoomLinkService < Issues::BaseService
def initialize(container:, current_user:, params:)
super(project: container, current_user: current_user, params: params)
super
@issue = params.fetch(:issue)
@added_meeting = ZoomMeeting.canonical_meeting(@issue)

View File

@ -40,7 +40,7 @@ module Issues
leftover = to_place.pop if to_place.count > QUERY_LIMIT
Issue.move_nulls_to_end(to_place)
Issues::BaseService.new(project: nil).rebalance_if_needed(to_place.max_by(&:relative_position))
Issues::BaseService.new(container: nil).rebalance_if_needed(to_place.max_by(&:relative_position))
Issues::PlacementWorker.perform_async(nil, leftover.project_id) if leftover.present?
rescue RelativePositioning::NoSpaceLeft => e
Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id, project_id: project_id)

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
class AddStatusAndResolvedAtToAbuseReports < Gitlab::Database::Migration[2.1]
def change
add_column :abuse_reports, :status, :integer, limit: 2, default: 1, null: false
add_timestamps_with_timezone(:abuse_reports, columns: [:resolved_at], null: true)
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddStatusCategoryAndIdIndexToAbuseReports < Gitlab::Database::Migration[2.1]
INDEX_NAME = 'index_abuse_reports_on_status_category_and_id'
disable_ddl_transaction!
def up
add_concurrent_index :abuse_reports, [:status, :category, :id], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :abuse_reports, INDEX_NAME
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddStatusAndIdIndexToAbuseReports < Gitlab::Database::Migration[2.1]
INDEX_NAME = 'index_abuse_reports_on_status_and_id'
disable_ddl_transaction!
def up
add_concurrent_index :abuse_reports, [:status, :id], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :abuse_reports, INDEX_NAME
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddIndexForNextOverLimitCheckAt < Gitlab::Database::Migration[2.1]
TABLE_NAME = 'namespace_details'
INDEX_NAME = 'index_next_over_limit_check_at_asc_order'
def up
prepare_async_index TABLE_NAME,
:next_over_limit_check_at,
order: { next_over_limit_check_at: 'ASC NULLS FIRST' },
name: INDEX_NAME
end
def down
unprepare_async_index TABLE_NAME, INDEX_NAME
end
end

View File

@ -0,0 +1 @@
23979065610c4f361a639cdcf81e7ce491d111ed3752bd11081f9645b31e21f6

View File

@ -0,0 +1 @@
c6a905e29792b88f87810d267a4472886e0a1a22fe9531e3d7998abbd1035552

View File

@ -0,0 +1 @@
204503fcf9e5da7255677a9a82f11e860410048efc1ed75cc7ba97b3cdd273c3

View File

@ -0,0 +1 @@
f5636e464b16bfc201a3f3a21269c6d8686d2bc829aa80491bea120fd10e138a

View File

@ -10733,6 +10733,8 @@ CREATE TABLE abuse_reports (
category smallint DEFAULT 1 NOT NULL,
reported_from_url text DEFAULT ''::text NOT NULL,
links_to_spam text[] DEFAULT '{}'::text[] NOT NULL,
status smallint DEFAULT 1 NOT NULL,
resolved_at timestamp with time zone,
CONSTRAINT abuse_reports_links_to_spam_length_check CHECK ((cardinality(links_to_spam) <= 20)),
CONSTRAINT check_ab1260fa6c CHECK ((char_length(reported_from_url) <= 512))
);
@ -28991,6 +28993,10 @@ CREATE UNIQUE INDEX idx_vulnerability_issue_links_on_vulnerability_id_and_link_t
CREATE UNIQUE INDEX idx_work_item_types_on_namespace_id_and_name_null_namespace ON work_item_types USING btree (btrim(lower(name)), ((namespace_id IS NULL))) WHERE (namespace_id IS NULL);
CREATE INDEX index_abuse_reports_on_status_and_id ON abuse_reports USING btree (status, id);
CREATE INDEX index_abuse_reports_on_status_category_and_id ON abuse_reports USING btree (status, category, id);
CREATE INDEX index_abuse_reports_on_user_id ON abuse_reports USING btree (user_id);
CREATE UNIQUE INDEX "index_achievements_on_namespace_id_LOWER_name" ON achievements USING btree (namespace_id, lower(name));

View File

@ -159,12 +159,18 @@ do not change. This method is called *merging*.
### Merge method for `include`
For a file containing `include` directives, the included files are read in order (possibly
recursively), and the configuration in these files is likewise merged in order. If the parameters overlap, the last included file takes precedence. Finally, the directives in the
file itself are merged with the configuration from the included files.
The `include` configuration merges with the main configuration file with this process:
- Included files are read in the order defined in the configuration file, and
the included configuration is merged together in the same order.
- If an included file also uses `include`, that nested `include` configuration is merged first (recursively).
- If parameters overlap, the last included file takes precedence when merging the configuration
from the included files.
- After all configuration added with `include` is merged together, the main configuration
is merged with the included configuration.
This merge method is a _deep merge_, where hash maps are merged at any depth in the
configuration. To merge hash map A (containing the configuration merged so far) and B (the next piece
configuration. To merge hash map "A" (that contains the configuration merged so far) and "B" (the next piece
of configuration), the keys and values are processed as follows:
- When the key only exists in A, use the key and value from A.
@ -172,9 +178,7 @@ of configuration), the keys and values are processed as follows:
- When the key exists in both A and B, and one of the values is not a hash map, use the value from B.
- Otherwise, use the key and value from B.
For example:
We have a configuration consisting of two files.
For example, with a configuration that consists of two files:
- The `.gitlab-ci.yml` file:
@ -211,7 +215,7 @@ We have a configuration consisting of two files.
dotenv: deploy.env
```
The merged result:
The merged result is:
```yaml
variables:

View File

@ -1314,7 +1314,10 @@ See also [downgrade](#downgrade) and [roll back](#roll-back).
## upper left, upper right
Use **upper left** and **upper right** instead of **top left** and **top right**. Hyphenate as adjectives (for example, **upper-left corner**).
Use **upper-left corner** and **upper-right corner** to provide direction in the UI.
If the UI element is not in a corner, use **upper left** and **upper right**.
Do not use **top left** and **top right**.
For details, see the [Microsoft style guide](https://learn.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/u/upper-left-upper-right).

View File

@ -60,6 +60,22 @@ For example, `Create an issue`.
If several tasks on a page share prerequisites, you can create a separate
topic with the title `Prerequisites`.
## When a task has only one step
If you need to write a task that has only one step, make that step an unordered list item.
This format helps the step stand out, while keeping it consistent with the rules
for lists.
For example:
```markdown
# Create a merge request
To create a merge request:
- In the upper-right corner, select **New merge request**.
```
### When more than one way exists to perform a task
If more than one way exists to perform a task in the UI, you should

View File

@ -129,19 +129,6 @@ time that the first policy merge request is created.
You can use the [Vulnerability-Check Migration](https://gitlab.com/gitlab-org/gitlab/-/snippets/2328089) script to bulk create policies or associate security policy projects with development projects. For instructions and a demonstration of how to use the Vulnerability-Check Migration script, see [this video](https://youtu.be/biU1N26DfBc).
## Scan execution policies
See [Scan execution policies](scan-execution-policies.md).
## Scan result policy editor
See [Scan result policies](scan-result-policies.md).
## Roadmap
See the [Category Direction page](https://about.gitlab.com/direction/govern/security_policies/security_policy_management/)
for more information on the product direction of security policies within GitLab.
## Troubleshooting
### `Branch name 'update-policy-<timestamp>' does not follow the pattern '<branch_name_regex>'`

View File

@ -4,8 +4,12 @@ module API
module Entities
# Use with care, this exposes the secret
class ApplicationWithSecret < Entities::Application
expose :secret, documentation: { type: 'string',
example: 'ee1dd64b6adc89cf7e2c23099301ccc2c61b441064e9324d963c46902a85ec34' }
expose :secret, documentation: {
type: 'string',
example: 'ee1dd64b6adc89cf7e2c23099301ccc2c61b441064e9324d963c46902a85ec34'
} do |application, _options|
application.plaintext_secret
end
end
end
end

View File

@ -24326,9 +24326,24 @@ msgstr ""
msgid "JobAssistant|Add job"
msgstr ""
msgid "JobAssistant|Job Setup"
msgstr ""
msgid "JobAssistant|Job assistant"
msgstr ""
msgid "JobAssistant|Job name"
msgstr ""
msgid "JobAssistant|Script"
msgstr ""
msgid "JobAssistant|Stage (optional)"
msgstr ""
msgid "JobAssistant|Tags (optional)"
msgstr ""
msgid "Jobs"
msgstr ""

View File

@ -2,7 +2,7 @@
source 'https://rubygems.org'
gem 'gitlab-qa', '~> 9', require: 'gitlab/qa'
gem 'gitlab-qa', '~> 9', '>= 9.1.0', require: 'gitlab/qa'
gem 'activesupport', '~> 6.1.7.2' # This should stay in sync with the root's Gemfile
gem 'allure-rspec', '~> 2.20.0'
gem 'capybara', '~> 3.38.0'

View File

@ -102,7 +102,7 @@ GEM
gitlab (4.18.0)
httparty (~> 0.18)
terminal-table (>= 1.5.1)
gitlab-qa (9.0.0)
gitlab-qa (9.1.0)
activesupport (~> 6.1)
gitlab (~> 4.18.0)
http (~> 5.0)
@ -317,7 +317,7 @@ DEPENDENCIES
faraday-retry (~> 2.0)
fog-core (= 2.1.0)
fog-google (~> 1.19)
gitlab-qa (~> 9)
gitlab-qa (~> 9, >= 9.1.0)
influxdb-client (~> 2.9)
knapsack (~> 4.0)
nokogiri (~> 1.14, >= 1.14.2)

View File

@ -31,10 +31,14 @@ module QA
parse_body(api_get_from("#{api_members_path}/all"))
end
def find_member(username)
def find_direct_member(username)
list_members.find { |member| member[:username] == username }
end
def find_direct_or_inherited_member(username)
list_all_members.find { |member| member[:username] == username }
end
def invite_group(group, access_level = AccessLevel::GUEST)
Support::Retrier.retry_until do
QA::Runtime::Logger.info(%(Sharing #{self.class.name} with #{group.name}))

View File

@ -39,6 +39,11 @@ module QA
before do
parent_group.add_member(parent_group_user)
# Due to the async nature of project authorization refreshes,
# we wait to confirm the user has been added as a member and
# their access level has been updated before proceeding with the test
wait_for_membership_update(parent_group_user, sub_group_project, Resource::Members::AccessLevel::DEVELOPER)
end
it(
@ -177,6 +182,16 @@ module QA
sub_group_user.remove_via_api!
end
end
private
def wait_for_membership_update(user, project, access_level)
Support::Retrier.retry_until(sleep_interval: 1, message: 'Waiting for user membership to be updated') do
found_member = project.reload!.find_direct_or_inherited_member(user.username)
found_member && found_member.fetch(:access_level) == access_level
end
end
end
end
end

View File

@ -65,7 +65,7 @@ module QA
it 'adds user to the group',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/386792' do
found_member = group.reload!.find_member(user.username)
found_member = group.reload!.find_direct_member(user.username)
expect(found_member).not_to be_nil
expect(found_member.fetch(:access_level))
@ -82,7 +82,7 @@ module QA
it 'does not add user to the group',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/386793' do
found_member = group.reload!.find_member(user.username)
found_member = group.reload!.find_direct_member(user.username)
expect(found_member).to be_nil
end

View File

@ -7,5 +7,9 @@ FactoryBot.define do
message { 'User sends spam' }
reported_from_url { 'http://gitlab.com' }
links_to_spam { ['https://gitlab.com/issue1', 'https://gitlab.com/issue2'] }
trait :closed do
status { 'closed' }
end
end
end

View File

@ -6,22 +6,48 @@ RSpec.describe AbuseReportsFinder, '#execute' do
let(:params) { {} }
let!(:user1) { create(:user) }
let!(:user2) { create(:user) }
let!(:abuse_report_1) { create(:abuse_report, user: user1) }
let!(:abuse_report_2) { create(:abuse_report, user: user2) }
let!(:abuse_report_1) { create(:abuse_report, category: 'spam', user: user1, reporter: user2) }
let!(:abuse_report_2) { create(:abuse_report, :closed, category: 'phishing', user: user2) }
subject { described_class.new(params).execute }
context 'empty params' do
context 'when params is empty' do
it 'returns all abuse reports' do
expect(subject).to match_array([abuse_report_1, abuse_report_2])
end
end
context 'params[:user_id] is present' do
context 'when params[:user_id] is present' do
let(:params) { { user_id: user2 } }
it 'returns abuse reports for the specified user' do
expect(subject).to match_array([abuse_report_2])
end
end
context 'when params[:status] is present' do
context 'when value is "open"' do
let(:params) { { status: 'open' } }
it 'returns only open abuse reports' do
expect(subject).to match_array([abuse_report_1])
end
end
context 'when value is "closed"' do
let(:params) { { status: 'closed' } }
it 'returns only closed abuse reports' do
expect(subject).to match_array([abuse_report_2])
end
end
end
context 'when params[:category] is present' do
let(:params) { { category: 'phishing' } }
it 'returns abuse reports with the specified category' do
expect(subject).to match_array([abuse_report_2])
end
end
end

View File

@ -0,0 +1,61 @@
import createStore from '~/ci/pipeline_editor/store';
import JobSetupItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { JOB_TEMPLATE } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
describe('Job setup item', () => {
let wrapper;
const findJobNameInput = () => wrapper.findByTestId('job-name-input');
const findJobScriptInput = () => wrapper.findByTestId('job-script-input');
const findJobTagsInput = () => wrapper.findByTestId('job-tags-input');
const findJobStageInput = () => wrapper.findByTestId('job-stage-input');
const dummyJobName = 'dummyJobName';
const dummyJobScript = 'dummyJobScript';
const dummyJobStage = 'dummyJobStage';
const dummyJobTags = ['tag1'];
const createComponent = () => {
wrapper = shallowMountExtended(JobSetupItem, {
store: createStore(),
propsData: {
tagOptions: [
{ id: 'tag1', name: 'tag1' },
{ id: 'tag2', name: 'tag2' },
],
isNameValid: true,
isScriptValid: true,
job: JSON.parse(JSON.stringify(JOB_TEMPLATE)),
},
});
};
beforeEach(() => {
createComponent();
});
it('should emit update job event when filling inputs', () => {
expect(wrapper.emitted('update-job')).toBeUndefined();
findJobNameInput().vm.$emit('input', dummyJobName);
expect(wrapper.emitted('update-job')).toHaveLength(1);
expect(wrapper.emitted('update-job')[0]).toEqual(['name', dummyJobName]);
findJobScriptInput().vm.$emit('input', dummyJobScript);
expect(wrapper.emitted('update-job')).toHaveLength(2);
expect(wrapper.emitted('update-job')[1]).toEqual(['script', dummyJobScript]);
findJobStageInput().vm.$emit('input', dummyJobStage);
expect(wrapper.emitted('update-job')).toHaveLength(3);
expect(wrapper.emitted('update-job')[2]).toEqual(['stage', dummyJobStage]);
findJobTagsInput().vm.$emit('input', dummyJobTags);
expect(wrapper.emitted('update-job')).toHaveLength(4);
expect(wrapper.emitted('update-job')[3]).toEqual(['tags', dummyJobTags]);
});
});

View File

@ -1,24 +1,43 @@
import { GlDrawer } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import { stringify } from 'yaml';
import JobAssistantDrawer from '~/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue';
import JobSetupItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue';
import getAllRunners from '~/ci/runner/graphql/list/all_runners.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createStore from '~/ci/pipeline_editor/store';
import { mockAllRunnersQueryResponse } from 'jest/ci/pipeline_editor/mock_data';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub';
Vue.use(VueApollo);
describe('Job assistant drawer', () => {
let wrapper;
let mockApollo;
const dummyJobName = 'a';
const dummyJobScript = 'b';
const findDrawer = () => wrapper.findComponent(GlDrawer);
const findJobSetupItem = () => wrapper.findComponent(JobSetupItem);
const findConfirmButton = () => wrapper.findByTestId('confirm-button');
const findCancelButton = () => wrapper.findByTestId('cancel-button');
const createComponent = () => {
mockApollo = createMockApollo([
[getAllRunners, jest.fn().mockResolvedValue(mockAllRunnersQueryResponse)],
]);
wrapper = mountExtended(JobAssistantDrawer, {
store: createStore(),
propsData: {
isVisible: true,
},
apolloProvider: mockApollo,
});
};
@ -42,4 +61,69 @@ describe('Job assistant drawer', () => {
expect(wrapper.emitted('close-job-assistant-drawer')).toHaveLength(1);
});
it('trigger validate if job name is empty', async () => {
const updateCiConfigSpy = jest.spyOn(wrapper.vm, 'updateCiConfig');
findJobSetupItem().vm.$emit('update-job', 'script', 'b');
findConfirmButton().trigger('click');
await nextTick();
expect(findJobSetupItem().props('isNameValid')).toBe(false);
expect(findJobSetupItem().props('isScriptValid')).toBe(true);
expect(updateCiConfigSpy).toHaveBeenCalledTimes(0);
});
describe('when enter valid input', () => {
beforeEach(() => {
findJobSetupItem().vm.$emit('update-job', 'name', dummyJobName);
findJobSetupItem().vm.$emit('update-job', 'script', dummyJobScript);
});
it('job name and script have correct value', () => {
expect(findJobSetupItem().props('job')).toMatchObject({
name: dummyJobName,
script: dummyJobScript,
});
});
it('job name and script state should be valid', () => {
expect(findJobSetupItem().props('isNameValid')).toBe(true);
expect(findJobSetupItem().props('isScriptValid')).toBe(true);
});
it('should clear job data when click confirm button', async () => {
findConfirmButton().trigger('click');
await nextTick();
expect(findJobSetupItem().props('job')).toMatchObject({ name: '', script: '' });
});
it('should clear job data when click cancel button', async () => {
findCancelButton().trigger('click');
await nextTick();
expect(findJobSetupItem().props('job')).toMatchObject({ name: '', script: '' });
});
it('should update correct ci content when click add button', () => {
const updateCiConfigSpy = jest.spyOn(wrapper.vm, 'updateCiConfig');
findConfirmButton().trigger('click');
expect(updateCiConfigSpy).toHaveBeenCalledWith(
`\n${stringify({ [dummyJobName]: { script: dummyJobScript } })}`,
);
});
it('should emit scroll editor to button event when click add button', () => {
const eventHubSpy = jest.spyOn(eventHub, '$emit');
findConfirmButton().trigger('click');
expect(eventHubSpy).toHaveBeenCalledWith(SCROLL_EDITOR_TO_BOTTOM);
});
});
});

View File

@ -583,6 +583,91 @@ export const mockCommitCreateResponse = {
},
};
export const mockAllRunnersQueryResponse = {
data: {
runners: {
nodes: [
{
id: 'gid://gitlab/Ci::Runner/1',
description: 'test',
runnerType: 'PROJECT_TYPE',
shortSha: 'DdTYMQGS',
version: '15.6.1',
ipAddress: '127.0.0.1',
active: true,
locked: true,
jobCount: 0,
jobExecutionStatus: 'IDLE',
tagList: ['tag1', 'tag2', 'tag3'],
createdAt: '2022-11-29T09:37:43Z',
contactedAt: null,
status: 'NEVER_CONTACTED',
userPermissions: {
updateRunner: true,
deleteRunner: true,
__typename: 'RunnerPermissions',
},
groups: null,
ownerProject: {
id: 'gid://gitlab/Project/1',
name: '123',
nameWithNamespace: 'Administrator / 123',
webUrl: 'http://127.0.0.1:3000/root/test',
__typename: 'Project',
},
__typename: 'CiRunner',
upgradeStatus: 'NOT_AVAILABLE',
adminUrl: 'http://127.0.0.1:3000/admin/runners/1',
editAdminUrl: 'http://127.0.0.1:3000/admin/runners/1/edit',
},
{
id: 'gid://gitlab/Ci::Runner/2',
description: 'test',
runnerType: 'PROJECT_TYPE',
shortSha: 'DdTYMQGA',
version: '15.6.1',
ipAddress: '127.0.0.1',
active: true,
locked: true,
jobCount: 0,
jobExecutionStatus: 'IDLE',
tagList: ['tag3', 'tag4'],
createdAt: '2022-11-29T09:37:43Z',
contactedAt: null,
status: 'NEVER_CONTACTED',
userPermissions: {
updateRunner: true,
deleteRunner: true,
__typename: 'RunnerPermissions',
},
groups: null,
ownerProject: {
id: 'gid://gitlab/Project/1',
name: '123',
nameWithNamespace: 'Administrator / 123',
webUrl: 'http://127.0.0.1:3000/root/test',
__typename: 'Project',
},
__typename: 'CiRunner',
upgradeStatus: 'NOT_AVAILABLE',
adminUrl: 'http://127.0.0.1:3000/admin/runners/2',
editAdminUrl: 'http://127.0.0.1:3000/admin/runners/2/edit',
},
],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor:
'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0yOSAwOTozNzo0My40OTEwNTEwMDAgKzAwMDAiLCJpZCI6IjIifQ',
endCursor:
'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0yOSAwOTozNzo0My40OTEwNTEwMDAgKzAwMDAiLCJpZCI6IjIifQ',
__typename: 'PageInfo',
},
__typename: 'CiRunnerConnection',
},
},
};
export const mockCommitCreateResponseNewEtag = {
data: {
commitCreate: {

View File

@ -8,6 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import createStore from '~/ci/pipeline_editor/store';
import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue';
import PipelineEditorEmptyState from '~/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
import PipelineEditorMessages from '~/ci/pipeline_editor/components/ui/pipeline_editor_messages.vue';
@ -80,7 +81,9 @@ describe('Pipeline editor app component', () => {
provide = {},
stubs = {},
} = {}) => {
const store = createStore();
wrapper = shallowMount(PipelineEditorApp, {
store,
provide: { ...defaultProvide, ...provide },
stubs,
mocks: {
@ -256,6 +259,10 @@ describe('Pipeline editor app component', () => {
.mockImplementation(jest.fn());
});
it('available stages is updated', () => {
expect(wrapper.vm.$store.state.availableStages).toStrictEqual(['test', 'build']);
});
it('shows pipeline editor home component', () => {
expect(findEditorHome().exists()).toBe(true);
});

View File

@ -70,6 +70,34 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do
}
end
describe 'scopes' do
let!(:reporter) { create(:user, username: 'reporter') }
let!(:report1) { create(:abuse_report) }
let!(:report2) { create(:abuse_report, :closed, reporter: reporter, category: 'phishing') }
describe '.open' do
subject(:results) { described_class.open }
it 'returns reports without resolved_at value' do
expect(subject).to match_array([report, report1])
end
end
describe '.closed' do
subject(:results) { described_class.closed }
it 'returns reports with resolved_at value' do
expect(subject).to match_array([report2])
end
end
describe '.by_category' do
it 'returns abuse reports with the specified category' do
expect(described_class.by_category('phishing')).to match_array([report2])
end
end
end
describe 'before_validation' do
context 'when links to spam contains empty strings' do
let(:report) { create(:abuse_report, links_to_spam: ['', 'https://gitlab.com']) }

View File

@ -22,7 +22,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
expect(json_response).to be_a Hash
expect(json_response['application_id']).to eq application.uid
expect(json_response['secret']).to eq application.secret
expect(application.secret_matches?(json_response['secret'])).to eq(true)
expect(json_response['callback_url']).to eq application.redirect_uri
expect(json_response['confidential']).to eq application.confidential
expect(application.scopes.to_s).to eq('api')

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Admin::AbuseReportEntity, feature_category: :insider_threat do
let_it_be(:abuse_report) { build_stubbed(:abuse_report) }
let(:entity) do
described_class.new(abuse_report)
end
describe '#as_json' do
subject(:entity_hash) { entity.as_json }
it 'exposes correct attributes' do
expect(entity_hash.keys).to include(
:category,
:updated_at,
:reported_user,
:reporter
)
end
it 'correctly exposes `reported user`' do
expect(entity_hash[:reported_user].keys).to match_array([:name])
end
it 'correctly exposes `reporter`' do
expect(entity_hash[:reporter].keys).to match_array([:name])
end
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Admin::AbuseReportSerializer, feature_category: :insider_threat do
let(:resource) { build(:abuse_report) }
subject { described_class.new.represent(resource) }
describe '#represent' do
it 'serializes an abuse report' do
expect(subject[:id]).to eq resource.id
end
context 'when multiple objects are being serialized' do
let(:resource) { build_list(:abuse_report, 2) }
it 'serializers the array of abuse reports' do
expect(subject).not_to be_empty
end
end
end
end

View File

@ -11,7 +11,7 @@ RSpec.describe Issues::ResolveDiscussions do
DummyService.class_eval do
include ::Issues::ResolveDiscussions
def initialize(project:, current_user: nil, params: {})
def initialize(container:, current_user: nil, params: {})
super
filter_resolve_discussion_params
end
@ -26,7 +26,7 @@ RSpec.describe Issues::ResolveDiscussions do
let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "fix") }
describe "#merge_request_for_resolving_discussion" do
let(:service) { DummyService.new(project: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid }) }
let(:service) { DummyService.new(container: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid }) }
it "finds the merge request" do
expect(service.merge_request_to_resolve_discussions_of).to eq(merge_request)
@ -45,7 +45,7 @@ RSpec.describe Issues::ResolveDiscussions do
describe "#discussions_to_resolve" do
it "contains a single discussion when matching merge request and discussion are passed" do
service = DummyService.new(
project: project,
container: project,
current_user: user,
params: {
discussion_to_resolve: discussion.id,
@ -65,7 +65,7 @@ RSpec.describe Issues::ResolveDiscussions do
project: merge_request.target_project,
line_number: 15)])
service = DummyService.new(
project: project,
container: project,
current_user: user,
params: { merge_request_to_resolve_discussions_of: merge_request.iid }
)
@ -83,7 +83,7 @@ RSpec.describe Issues::ResolveDiscussions do
line_number: 15
)])
service = DummyService.new(
project: project,
container: project,
current_user: user,
params: { merge_request_to_resolve_discussions_of: merge_request.iid }
)
@ -96,7 +96,7 @@ RSpec.describe Issues::ResolveDiscussions do
it "is empty when a discussion and another merge request are passed" do
service = DummyService.new(
project: project,
container: project,
current_user: user,
params: {
discussion_to_resolve: discussion.id,