Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-20 12:08:30 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-20 12:08:30 +0300
commitbd28d0fa02dc73794e013159512900f8d10fa10b (patch)
tree57ca25e2ecb1f6d379f9738ccc140fa1c624e6c3
parent24e54a8f10e88aafa48c8a7dc548576939e6612b (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue89
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js33
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue106
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js22
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/event_hub.js5
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/index.js4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue21
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/store/index.js12
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/store/mutations.js10
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/store/state.js4
-rw-r--r--app/finders/abuse_reports_finder.rb38
-rw-r--r--app/models/abuse_report.rb8
-rw-r--r--app/serializers/admin/abuse_report_entity.rb16
-rw-r--r--app/serializers/admin/abuse_report_serializer.rb7
-rw-r--r--app/services/issues/after_create_service.rb5
-rw-r--r--app/services/issues/base_service.rb12
-rw-r--r--app/services/issues/build_service.rb5
-rw-r--r--app/services/issues/close_service.rb10
-rw-r--r--app/services/issues/create_service.rb9
-rw-r--r--app/services/issues/duplicate_service.rb5
-rw-r--r--app/services/issues/referenced_merge_requests_service.rb5
-rw-r--r--app/services/issues/related_branches_service.rb5
-rw-r--r--app/services/issues/reopen_service.rb13
-rw-r--r--app/services/issues/reorder_service.rb5
-rw-r--r--app/services/issues/update_service.rb11
-rw-r--r--app/services/issues/zoom_link_service.rb2
-rw-r--r--app/workers/issues/placement_worker.rb2
-rw-r--r--db/migrate/20230216040505_add_status_and_resolved_at_to_abuse_reports.rb8
-rw-r--r--db/migrate/20230216071312_add_status_category_and_id_index_to_abuse_reports.rb15
-rw-r--r--db/migrate/20230220035034_add_status_and_id_index_to_abuse_reports.rb15
-rw-r--r--db/post_migrate/20230213103019_add_index_for_next_over_limit_check_at.rb17
-rw-r--r--db/schema_migrations/202302131030191
-rw-r--r--db/schema_migrations/202302160405051
-rw-r--r--db/schema_migrations/202302160713121
-rw-r--r--db/schema_migrations/202302200350341
-rw-r--r--db/structure.sql6
-rw-r--r--doc/ci/yaml/includes.md20
-rw-r--r--doc/development/documentation/styleguide/word_list.md5
-rw-r--r--doc/development/documentation/topic_types/task.md16
-rw-r--r--doc/user/application_security/policies/index.md13
-rw-r--r--lib/api/entities/application_with_secret.rb8
-rw-r--r--locale/gitlab.pot15
-rw-r--r--qa/Gemfile2
-rw-r--r--qa/Gemfile.lock4
-rw-r--r--qa/qa/resource/members.rb6
-rw-r--r--qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb15
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/group/group_member_access_request_spec.rb4
-rw-r--r--spec/factories/abuse_reports.rb4
-rw-r--r--spec/finders/abuse_reports_finder_spec.rb34
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js61
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js86
-rw-r--r--spec/frontend/ci/pipeline_editor/mock_data.js85
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js7
-rw-r--r--spec/models/abuse_report_spec.rb28
-rw-r--r--spec/requests/api/applications_spec.rb2
-rw-r--r--spec/serializers/admin/abuse_report_entity_spec.rb32
-rw-r--r--spec/serializers/admin/abuse_report_serializer_spec.rb23
-rw-r--r--spec/services/issues/resolve_discussions_spec.rb12
59 files changed, 881 insertions, 132 deletions
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue
new file mode 100644
index 00000000000..a25b3ca09fd
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue
@@ -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>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js
index 1c122fd5e38..05a616596e2 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/constants.js
@@ -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'),
};
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue
index 65c87df21cb..d9df32ad84e 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue
@@ -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>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js
new file mode 100644
index 00000000000..83e7574c4de
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js
@@ -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);
+};
diff --git a/app/assets/javascripts/ci/pipeline_editor/event_hub.js b/app/assets/javascripts/ci/pipeline_editor/event_hub.js
new file mode 100644
index 00000000000..c64eaf5ef5c
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_editor/event_hub.js
@@ -0,0 +1,5 @@
+import createEventHub from '~/helpers/event_hub_factory';
+
+export default createEventHub();
+
+export const SCROLL_EDITOR_TO_BOTTOM = Symbol('scrollEditorToBottom');
diff --git a/app/assets/javascripts/ci/pipeline_editor/index.js b/app/assets/javascripts/ci/pipeline_editor/index.js
index 6d91c339833..50d1cb42f5c 100644
--- a/app/assets/javascripts/ci/pipeline_editor/index.js
+++ b/app/assets/javascripts/ci/pipeline_editor/index.js
@@ -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,
diff --git a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue
index ff848a973e3..7b3c4d6f74f 100644
--- a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue
@@ -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();
diff --git a/app/assets/javascripts/ci/pipeline_editor/store/index.js b/app/assets/javascripts/ci/pipeline_editor/store/index.js
new file mode 100644
index 00000000000..d7d5aed79e2
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_editor/store/index.js
@@ -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(),
+ });
diff --git a/app/assets/javascripts/ci/pipeline_editor/store/mutation_types.js b/app/assets/javascripts/ci/pipeline_editor/store/mutation_types.js
new file mode 100644
index 00000000000..035d3c90c14
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_editor/store/mutation_types.js
@@ -0,0 +1,2 @@
+export const UPDATE_CI_CONFIG = 'UPDATE_CI_CONFIG';
+export const UPDATE_AVAILABLE_STAGES = 'UPDATE_AVAILABLE_STAGES';
diff --git a/app/assets/javascripts/ci/pipeline_editor/store/mutations.js b/app/assets/javascripts/ci/pipeline_editor/store/mutations.js
new file mode 100644
index 00000000000..552c1df9a2c
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_editor/store/mutations.js
@@ -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 || [];
+ },
+};
diff --git a/app/assets/javascripts/ci/pipeline_editor/store/state.js b/app/assets/javascripts/ci/pipeline_editor/store/state.js
new file mode 100644
index 00000000000..34146cd54c4
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_editor/store/state.js
@@ -0,0 +1,4 @@
+export default () => ({
+ currentCiFileContent: '',
+ availableStages: [],
+});
diff --git a/app/finders/abuse_reports_finder.rb b/app/finders/abuse_reports_finder.rb
index 04043f36426..90d09a2d6ed 100644
--- a/app/finders/abuse_reports_finder.rb
+++ b/app/finders/abuse_reports_finder.rb
@@ -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
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index dbcdfa5e946..1d2d077e0ff 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -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
diff --git a/app/serializers/admin/abuse_report_entity.rb b/app/serializers/admin/abuse_report_entity.rb
new file mode 100644
index 00000000000..a550763f0ff
--- /dev/null
+++ b/app/serializers/admin/abuse_report_entity.rb
@@ -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
diff --git a/app/serializers/admin/abuse_report_serializer.rb b/app/serializers/admin/abuse_report_serializer.rb
new file mode 100644
index 00000000000..af43e459482
--- /dev/null
+++ b/app/serializers/admin/abuse_report_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Admin
+ class AbuseReportSerializer < BaseSerializer
+ entity Admin::AbuseReportEntity
+ end
+end
diff --git a/app/services/issues/after_create_service.rb b/app/services/issues/after_create_service.rb
index 011a78029c8..5d10eca2979 100644
--- a/app/services/issues/after_create_service.rb
+++ b/app/services/issues/after_create_service.rb
@@ -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)
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 553fb6e2ac9..fa46ba748e8 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -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
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
index 877ce09e065..75bd2b88e86 100644
--- a/app/services/issues/build_service.rb
+++ b/app/services/issues/build_service.rb
@@ -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
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 9fde1cc2ac2..4f6a859e20e 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -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
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index fa5233da489..f03599242c1 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -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.
diff --git a/app/services/issues/duplicate_service.rb b/app/services/issues/duplicate_service.rb
index a3213c50f86..1fff9a4a684 100644
--- a/app/services/issues/duplicate_service.rb
+++ b/app/services/issues/duplicate_service.rb
@@ -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)
diff --git a/app/services/issues/referenced_merge_requests_service.rb b/app/services/issues/referenced_merge_requests_service.rb
index ba03927136a..a69cd324b1e 100644
--- a/app/services/issues/referenced_merge_requests_service.rb
+++ b/app/services/issues/referenced_merge_requests_service.rb
@@ -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)
diff --git a/app/services/issues/related_branches_service.rb b/app/services/issues/related_branches_service.rb
index 3f4413fdfd7..ef6de83fcf4 100644
--- a/app/services/issues/related_branches_service.rb
+++ b/app/services/issues/related_branches_service.rb
@@ -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]) }
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index ebcf2fb5c83..f4f81e9455a 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -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
diff --git a/app/services/issues/reorder_service.rb b/app/services/issues/reorder_service.rb
index 059b4196b23..1afec4c94f4 100644
--- a/app/services/issues/reorder_service.rb
+++ b/app/services/issues/reorder_service.rb
@@ -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
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 71324b3f044..322065c5b7c 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -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.
diff --git a/app/services/issues/zoom_link_service.rb b/app/services/issues/zoom_link_service.rb
index 4144c293990..bfd3e6a945f 100644
--- a/app/services/issues/zoom_link_service.rb
+++ b/app/services/issues/zoom_link_service.rb
@@ -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)
diff --git a/app/workers/issues/placement_worker.rb b/app/workers/issues/placement_worker.rb
index ec29a754128..0a4f2612912 100644
--- a/app/workers/issues/placement_worker.rb
+++ b/app/workers/issues/placement_worker.rb
@@ -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)
diff --git a/db/migrate/20230216040505_add_status_and_resolved_at_to_abuse_reports.rb b/db/migrate/20230216040505_add_status_and_resolved_at_to_abuse_reports.rb
new file mode 100644
index 00000000000..3cfd082b465
--- /dev/null
+++ b/db/migrate/20230216040505_add_status_and_resolved_at_to_abuse_reports.rb
@@ -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
diff --git a/db/migrate/20230216071312_add_status_category_and_id_index_to_abuse_reports.rb b/db/migrate/20230216071312_add_status_category_and_id_index_to_abuse_reports.rb
new file mode 100644
index 00000000000..0c529f15b1b
--- /dev/null
+++ b/db/migrate/20230216071312_add_status_category_and_id_index_to_abuse_reports.rb
@@ -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
diff --git a/db/migrate/20230220035034_add_status_and_id_index_to_abuse_reports.rb b/db/migrate/20230220035034_add_status_and_id_index_to_abuse_reports.rb
new file mode 100644
index 00000000000..cea01572e37
--- /dev/null
+++ b/db/migrate/20230220035034_add_status_and_id_index_to_abuse_reports.rb
@@ -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
diff --git a/db/post_migrate/20230213103019_add_index_for_next_over_limit_check_at.rb b/db/post_migrate/20230213103019_add_index_for_next_over_limit_check_at.rb
new file mode 100644
index 00000000000..29c59cea3ff
--- /dev/null
+++ b/db/post_migrate/20230213103019_add_index_for_next_over_limit_check_at.rb
@@ -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
diff --git a/db/schema_migrations/20230213103019 b/db/schema_migrations/20230213103019
new file mode 100644
index 00000000000..e28052b7f58
--- /dev/null
+++ b/db/schema_migrations/20230213103019
@@ -0,0 +1 @@
+23979065610c4f361a639cdcf81e7ce491d111ed3752bd11081f9645b31e21f6 \ No newline at end of file
diff --git a/db/schema_migrations/20230216040505 b/db/schema_migrations/20230216040505
new file mode 100644
index 00000000000..d3cc858827f
--- /dev/null
+++ b/db/schema_migrations/20230216040505
@@ -0,0 +1 @@
+c6a905e29792b88f87810d267a4472886e0a1a22fe9531e3d7998abbd1035552 \ No newline at end of file
diff --git a/db/schema_migrations/20230216071312 b/db/schema_migrations/20230216071312
new file mode 100644
index 00000000000..2e92ecc19e6
--- /dev/null
+++ b/db/schema_migrations/20230216071312
@@ -0,0 +1 @@
+204503fcf9e5da7255677a9a82f11e860410048efc1ed75cc7ba97b3cdd273c3 \ No newline at end of file
diff --git a/db/schema_migrations/20230220035034 b/db/schema_migrations/20230220035034
new file mode 100644
index 00000000000..4cb8be66d8f
--- /dev/null
+++ b/db/schema_migrations/20230220035034
@@ -0,0 +1 @@
+f5636e464b16bfc201a3f3a21269c6d8686d2bc829aa80491bea120fd10e138a \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 7e394f6bb7e..61b0e28c8ad 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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));
diff --git a/doc/ci/yaml/includes.md b/doc/ci/yaml/includes.md
index bf0b7444e78..79f0e870740 100644
--- a/doc/ci/yaml/includes.md
+++ b/doc/ci/yaml/includes.md
@@ -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:
diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md
index e64fd4df7ff..d13f255d2c6 100644
--- a/doc/development/documentation/styleguide/word_list.md
+++ b/doc/development/documentation/styleguide/word_list.md
@@ -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).
diff --git a/doc/development/documentation/topic_types/task.md b/doc/development/documentation/topic_types/task.md
index 8d23a5f322e..f596ffc9b8b 100644
--- a/doc/development/documentation/topic_types/task.md
+++ b/doc/development/documentation/topic_types/task.md
@@ -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
diff --git a/doc/user/application_security/policies/index.md b/doc/user/application_security/policies/index.md
index a214d0d2cec..f1aafc3d38f 100644
--- a/doc/user/application_security/policies/index.md
+++ b/doc/user/application_security/policies/index.md
@@ -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>'`
diff --git a/lib/api/entities/application_with_secret.rb b/lib/api/entities/application_with_secret.rb
index 1d0acee8624..5679ab4253d 100644
--- a/lib/api/entities/application_with_secret.rb
+++ b/lib/api/entities/application_with_secret.rb
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 725c587b198..200a8079a7b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/qa/Gemfile b/qa/Gemfile
index a55fb975a04..d5d9b77b82a 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -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'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 96c5bb5f595..10fff3de272 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -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)
diff --git a/qa/qa/resource/members.rb b/qa/qa/resource/members.rb
index 7f31808d2ff..adb5acc77f1 100644
--- a/qa/qa/resource/members.rb
+++ b/qa/qa/resource/members.rb
@@ -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}))
diff --git a/qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb b/qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb
index 124b6c9cd44..af78b112be4 100644
--- a/qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb
@@ -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
diff --git a/qa/qa/specs/features/browser_ui/1_manage/group/group_member_access_request_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/group/group_member_access_request_spec.rb
index 3a84646977f..62f596c8915 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/group/group_member_access_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/group/group_member_access_request_spec.rb
@@ -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
diff --git a/spec/factories/abuse_reports.rb b/spec/factories/abuse_reports.rb
index 355fb142994..9f05d183ba4 100644
--- a/spec/factories/abuse_reports.rb
+++ b/spec/factories/abuse_reports.rb
@@ -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
diff --git a/spec/finders/abuse_reports_finder_spec.rb b/spec/finders/abuse_reports_finder_spec.rb
index 52620b3e421..297f3487227 100644
--- a/spec/finders/abuse_reports_finder_spec.rb
+++ b/spec/finders/abuse_reports_finder_spec.rb
@@ -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
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js
new file mode 100644
index 00000000000..eaad0dae90d
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js
@@ -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]);
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
index 79200d92598..f29b6b00e0b 100644
--- a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
@@ -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);
+ });
+ });
});
diff --git a/spec/frontend/ci/pipeline_editor/mock_data.js b/spec/frontend/ci/pipeline_editor/mock_data.js
index 541123d7efc..93208090d70 100644
--- a/spec/frontend/ci/pipeline_editor/mock_data.js
+++ b/spec/frontend/ci/pipeline_editor/mock_data.js
@@ -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: {
diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
index a103acb33bc..5855b805fc9 100644
--- a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
@@ -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);
});
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 7995cc36383..87a0a2a492c 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -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']) }
diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb
index b81cdcfea8e..0f7df6661a9 100644
--- a/spec/requests/api/applications_spec.rb
+++ b/spec/requests/api/applications_spec.rb
@@ -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')
diff --git a/spec/serializers/admin/abuse_report_entity_spec.rb b/spec/serializers/admin/abuse_report_entity_spec.rb
new file mode 100644
index 00000000000..7d18310977c
--- /dev/null
+++ b/spec/serializers/admin/abuse_report_entity_spec.rb
@@ -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
diff --git a/spec/serializers/admin/abuse_report_serializer_spec.rb b/spec/serializers/admin/abuse_report_serializer_spec.rb
new file mode 100644
index 00000000000..5b9c229584b
--- /dev/null
+++ b/spec/serializers/admin/abuse_report_serializer_spec.rb
@@ -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
diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb
index 1ac71b966bc..2c8de5ec570 100644
--- a/spec/services/issues/resolve_discussions_spec.rb
+++ b/spec/services/issues/resolve_discussions_spec.rb
@@ -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,