summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-14 15:09:05 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-14 15:09:05 +0000
commit66bd1f0fdcaf84fa3412c70d7962b49eb8a48fde (patch)
tree23f451b4e60a6e28bcc15043d7756bb27dcc2970
parent49089d4fb1f5c17328ac61c955d95a68c6d4d545 (diff)
downloadgitlab-ce-66bd1f0fdcaf84fa3412c70d7962b49eb8a48fde.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/actions.js30
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/getters.js58
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/index.js16
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/mutations.js24
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/state.js15
-rw-r--r--app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js41
-rw-r--r--app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js28
-rw-r--r--app/finders/events_finder.rb9
-rw-r--r--app/helpers/environments_helper.rb3
-rw-r--r--app/mailers/emails/service_desk.rb92
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/mailers/previews/notify_preview.rb12
-rw-r--r--app/models/event.rb1
-rw-r--r--app/models/event_collection.rb1
-rw-r--r--app/presenters/clusters/cluster_presenter.rb17
-rw-r--r--app/serializers/cluster_entity.rb4
-rw-r--r--app/serializers/cluster_serializer.rb1
-rw-r--r--app/services/event_create_service.rb2
-rw-r--r--app/services/git/wiki_push_service.rb2
-rw-r--r--app/services/wiki_pages/base_service.rb2
-rw-r--r--app/services/wiki_pages/event_create_service.rb2
-rw-r--r--app/views/layouts/service_desk.html.haml24
-rw-r--r--app/views/notify/service_desk_new_note_email.html.haml5
-rw-r--r--app/views/notify/service_desk_new_note_email.text.erb6
-rw-r--r--app/views/notify/service_desk_thank_you_email.html.haml2
-rw-r--r--app/views/notify/service_desk_thank_you_email.text.erb6
-rw-r--r--app/views/shared/_event_filter.html.haml2
-rw-r--r--app/workers/all_queues.yml8
-rw-r--r--app/workers/service_desk_email_receiver_worker.rb15
-rw-r--r--changelogs/unreleased/208655-introduce-prepare-keyword-to-environment-action-to-annotate-non-de.yml5
-rw-r--r--changelogs/unreleased/ajk-ff-remove-wiki_events.yml5
-rw-r--r--changelogs/unreleased/alert-intergration-trigger-test-docs.yml5
-rw-r--r--doc/operations/feature_flags.md6
-rw-r--r--doc/user/asciidoc.md78
-rw-r--r--doc/user/project/integrations/generic_alerts.md16
-rw-r--r--doc/user/project/web_ide/index.md9
-rw-r--r--doc/user/project/wiki/index.md39
-rw-r--r--lib/event_filter.rb3
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb2
-rw-r--r--lib/gitlab/email/handler.rb5
-rw-r--r--lib/gitlab/email/handler/reply_processing.rb15
-rw-r--r--lib/gitlab/email/handler/service_desk_handler.rb152
-rw-r--r--lib/gitlab/email/service_desk_receiver.rb23
-rw-r--r--package.json4
-rw-r--r--spec/finders/events_finder_spec.rb22
-rw-r--r--spec/fixtures/emails/service_desk.eml28
-rw-r--r--spec/fixtures/emails/service_desk_custom_address.eml27
-rw-r--r--spec/fixtures/emails/service_desk_forwarded.eml30
-rw-r--r--spec/fixtures/emails/service_desk_forwarded_new_issue.eml29
-rw-r--r--spec/fixtures/emails/service_desk_legacy.eml28
-rw-r--r--spec/fixtures/emails/service_desk_sender_and_from.eml27
-rw-r--r--spec/fixtures/emails/valid_reply_with_quick_actions.eml45
-rw-r--r--spec/frontend/reports/codequality_report/mock_data.js90
-rw-r--r--spec/frontend/reports/codequality_report/store/actions_spec.js151
-rw-r--r--spec/frontend/reports/codequality_report/store/getters_spec.js95
-rw-r--r--spec/frontend/reports/codequality_report/store/mutations_spec.js80
-rw-r--r--spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js139
-rw-r--r--spec/helpers/environments_helper_spec.rb14
-rw-r--r--spec/lib/event_filter_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/config/entry/environment_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb13
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb66
-rw-r--r--spec/lib/gitlab/email/handler/service_desk_handler_spec.rb311
-rw-r--r--spec/lib/gitlab/email/handler_spec.rb42
-rw-r--r--spec/lib/gitlab/email/service_desk_receiver_spec.rb37
-rw-r--r--spec/lib/sentry/pagination_parser_spec.rb3
-rw-r--r--spec/mailers/emails/service_desk_spec.rb188
-rw-r--r--spec/mailers/notify_spec.rb72
-rw-r--r--spec/models/event_collection_spec.rb36
-rw-r--r--spec/models/event_spec.rb9
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb70
-rw-r--r--spec/requests/api/projects_spec.rb7
-rw-r--r--spec/serializers/cluster_entity_spec.rb32
-rw-r--r--spec/serializers/cluster_serializer_spec.rb5
-rw-r--r--spec/services/event_create_service_spec.rb10
-rw-r--r--spec/services/git/wiki_push_service_spec.rb8
-rw-r--r--spec/services/wiki_pages/event_create_service_spec.rb15
-rw-r--r--spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb10
-rw-r--r--spec/workers/service_desk_email_receiver_worker_spec.rb53
-rw-r--r--yarn.lock18
83 files changed, 2413 insertions, 271 deletions
diff --git a/app/assets/javascripts/reports/codequality_report/store/actions.js b/app/assets/javascripts/reports/codequality_report/store/actions.js
new file mode 100644
index 00000000000..bf84d27b5ea
--- /dev/null
+++ b/app/assets/javascripts/reports/codequality_report/store/actions.js
@@ -0,0 +1,30 @@
+import axios from '~/lib/utils/axios_utils';
+import * as types from './mutation_types';
+import { parseCodeclimateMetrics, doCodeClimateComparison } from './utils/codequality_comparison';
+
+export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths);
+
+export const fetchReports = ({ state, dispatch, commit }) => {
+ commit(types.REQUEST_REPORTS);
+
+ if (!state.basePath) {
+ return dispatch('receiveReportsError');
+ }
+ return Promise.all([axios.get(state.headPath), axios.get(state.basePath)])
+ .then(results =>
+ doCodeClimateComparison(
+ parseCodeclimateMetrics(results[0].data, state.headBlobPath),
+ parseCodeclimateMetrics(results[1].data, state.baseBlobPath),
+ ),
+ )
+ .then(data => dispatch('receiveReportsSuccess', data))
+ .catch(() => dispatch('receiveReportsError'));
+};
+
+export const receiveReportsSuccess = ({ commit }, data) => {
+ commit(types.RECEIVE_REPORTS_SUCCESS, data);
+};
+
+export const receiveReportsError = ({ commit }) => {
+ commit(types.RECEIVE_REPORTS_ERROR);
+};
diff --git a/app/assets/javascripts/reports/codequality_report/store/getters.js b/app/assets/javascripts/reports/codequality_report/store/getters.js
new file mode 100644
index 00000000000..5df58c7f85f
--- /dev/null
+++ b/app/assets/javascripts/reports/codequality_report/store/getters.js
@@ -0,0 +1,58 @@
+import { LOADING, ERROR, SUCCESS } from '../../constants';
+import { sprintf, __, s__, n__ } from '~/locale';
+
+export const hasCodequalityIssues = state =>
+ Boolean(state.newIssues?.length || state.resolvedIssues?.length);
+
+export const codequalityStatus = state => {
+ if (state.isLoading) {
+ return LOADING;
+ }
+ if (state.hasError) {
+ return ERROR;
+ }
+
+ return SUCCESS;
+};
+
+export const codequalityText = state => {
+ const { newIssues, resolvedIssues } = state;
+ const text = [];
+
+ if (!newIssues.length && !resolvedIssues.length) {
+ text.push(s__('ciReport|No changes to code quality'));
+ } else {
+ text.push(s__('ciReport|Code quality'));
+
+ if (resolvedIssues.length) {
+ text.push(n__(' improved on %d point', ' improved on %d points', resolvedIssues.length));
+ }
+
+ if (newIssues.length && resolvedIssues.length) {
+ text.push(__(' and'));
+ }
+
+ if (newIssues.length) {
+ text.push(n__(' degraded on %d point', ' degraded on %d points', newIssues.length));
+ }
+ }
+
+ return text.join('');
+};
+
+export const codequalityPopover = state => {
+ if (state.headPath && !state.basePath) {
+ return {
+ title: s__('ciReport|Base pipeline codequality artifact not found'),
+ content: sprintf(
+ s__('ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}'),
+ {
+ linkStartTag: `<a href="${state.helpPath}" target="_blank" rel="noopener noreferrer">`,
+ linkEndTag: '<i class="fa fa-external-link" aria-hidden="true"></i></a>',
+ },
+ false,
+ ),
+ };
+ }
+ return {};
+};
diff --git a/app/assets/javascripts/reports/codequality_report/store/index.js b/app/assets/javascripts/reports/codequality_report/store/index.js
new file mode 100644
index 00000000000..047964260ad
--- /dev/null
+++ b/app/assets/javascripts/reports/codequality_report/store/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export default initialState =>
+ new Vuex.Store({
+ actions,
+ getters,
+ mutations,
+ state: state(initialState),
+ });
diff --git a/app/assets/javascripts/reports/codequality_report/store/mutation_types.js b/app/assets/javascripts/reports/codequality_report/store/mutation_types.js
new file mode 100644
index 00000000000..c362c973ae1
--- /dev/null
+++ b/app/assets/javascripts/reports/codequality_report/store/mutation_types.js
@@ -0,0 +1,5 @@
+export const SET_PATHS = 'SET_PATHS';
+
+export const REQUEST_REPORTS = 'REQUEST_REPORTS';
+export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS';
+export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
diff --git a/app/assets/javascripts/reports/codequality_report/store/mutations.js b/app/assets/javascripts/reports/codequality_report/store/mutations.js
new file mode 100644
index 00000000000..7ef4f3ce2db
--- /dev/null
+++ b/app/assets/javascripts/reports/codequality_report/store/mutations.js
@@ -0,0 +1,24 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_PATHS](state, paths) {
+ state.basePath = paths.basePath;
+ state.headPath = paths.headPath;
+ state.baseBlobPath = paths.baseBlobPath;
+ state.headBlobPath = paths.headBlobPath;
+ state.helpPath = paths.helpPath;
+ },
+ [types.REQUEST_REPORTS](state) {
+ state.isLoading = true;
+ },
+ [types.RECEIVE_REPORTS_SUCCESS](state, data) {
+ state.hasError = false;
+ state.isLoading = false;
+ state.newIssues = data.newIssues;
+ state.resolvedIssues = data.resolvedIssues;
+ },
+ [types.RECEIVE_REPORTS_ERROR](state) {
+ state.isLoading = false;
+ state.hasError = true;
+ },
+};
diff --git a/app/assets/javascripts/reports/codequality_report/store/state.js b/app/assets/javascripts/reports/codequality_report/store/state.js
new file mode 100644
index 00000000000..38ab53b432e
--- /dev/null
+++ b/app/assets/javascripts/reports/codequality_report/store/state.js
@@ -0,0 +1,15 @@
+export default () => ({
+ basePath: null,
+ headPath: null,
+
+ baseBlobPath: null,
+ headBlobPath: null,
+
+ isLoading: false,
+ hasError: false,
+
+ newIssues: [],
+ resolvedIssues: [],
+
+ helpPath: null,
+});
diff --git a/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js b/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js
new file mode 100644
index 00000000000..eba9e340c4e
--- /dev/null
+++ b/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js
@@ -0,0 +1,41 @@
+import CodeQualityComparisonWorker from '../../workers/codequality_comparison_worker';
+
+export const parseCodeclimateMetrics = (issues = [], path = '') => {
+ return issues.map(issue => {
+ const parsedIssue = {
+ ...issue,
+ name: issue.description,
+ };
+
+ if (issue?.location?.path) {
+ let parseCodeQualityUrl = `${path}/${issue.location.path}`;
+ parsedIssue.path = issue.location.path;
+
+ if (issue?.location?.lines?.begin) {
+ parsedIssue.line = issue.location.lines.begin;
+ parseCodeQualityUrl += `#L${issue.location.lines.begin}`;
+ } else if (issue?.location?.positions?.begin?.line) {
+ parsedIssue.line = issue.location.positions.begin.line;
+ parseCodeQualityUrl += `#L${issue.location.positions.begin.line}`;
+ }
+
+ parsedIssue.urlPath = parseCodeQualityUrl;
+ }
+
+ return parsedIssue;
+ });
+};
+
+export const doCodeClimateComparison = (headIssues, baseIssues) => {
+ // Do these comparisons in worker threads to avoid blocking the main thread
+ return new Promise((resolve, reject) => {
+ const worker = new CodeQualityComparisonWorker();
+ worker.addEventListener('message', ({ data }) =>
+ data.newIssues && data.resolvedIssues ? resolve(data) : reject(data),
+ );
+ worker.postMessage({
+ headIssues,
+ baseIssues,
+ });
+ });
+};
diff --git a/app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js b/app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js
new file mode 100644
index 00000000000..fc55602f95c
--- /dev/null
+++ b/app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js
@@ -0,0 +1,28 @@
+import { differenceBy } from 'lodash';
+
+const KEY_TO_FILTER_BY = 'fingerprint';
+
+// eslint-disable-next-line no-restricted-globals
+self.addEventListener('message', e => {
+ const { data } = e;
+
+ if (data === undefined) {
+ return null;
+ }
+
+ const { headIssues, baseIssues } = data;
+
+ if (!headIssues || !baseIssues) {
+ // eslint-disable-next-line no-restricted-globals
+ return self.postMessage({});
+ }
+
+ // eslint-disable-next-line no-restricted-globals
+ self.postMessage({
+ newIssues: differenceBy(headIssues, baseIssues, KEY_TO_FILTER_BY),
+ resolvedIssues: differenceBy(baseIssues, headIssues, KEY_TO_FILTER_BY),
+ });
+
+ // eslint-disable-next-line no-restricted-globals
+ return self.close();
+});
diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb
index 004fbc4cd22..4c619f3d7ea 100644
--- a/app/finders/events_finder.rb
+++ b/app/finders/events_finder.rb
@@ -54,17 +54,10 @@ class EventsFinder
if current_user && scope == 'all'
EventCollection.new(current_user.authorized_projects).all_project_events
else
- # EventCollection is responsible for applying the feature flag
- apply_feature_flags(source.events)
+ source.events
end
end
- def apply_feature_flags(events)
- return events if ::Feature.enabled?(:wiki_events)
-
- events.not_wiki_page
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def by_current_user_access(events)
events.merge(Project.public_or_visible_to_user(current_user))
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 8dbbec8c21d..90ebab731ea 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -36,7 +36,8 @@ module EnvironmentsHelper
"environment-name": environment.name,
"environments-path": project_environments_path(project, format: :json),
"environment-id": environment.id,
- "cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack')
+ "cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack'),
+ "clusters-path": project_clusters_path(project, format: :json)
}
end
diff --git a/app/mailers/emails/service_desk.rb b/app/mailers/emails/service_desk.rb
new file mode 100644
index 00000000000..29fe608472d
--- /dev/null
+++ b/app/mailers/emails/service_desk.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+module Emails
+ module ServiceDesk
+ extend ActiveSupport::Concern
+ include MarkupHelper
+
+ included do
+ layout 'service_desk', only: [:service_desk_thank_you_email, :service_desk_new_note_email]
+ end
+
+ def service_desk_thank_you_email(issue_id)
+ setup_service_desk_mail(issue_id)
+
+ email_sender = sender(
+ @support_bot.id,
+ send_from_user_email: false,
+ sender_name: @project.service_desk_setting&.outgoing_name
+ )
+ options = service_desk_options(email_sender, 'thank_you')
+ .merge(subject: "Re: #{subject_base}")
+
+ mail_new_thread(@issue, options)
+ end
+
+ def service_desk_new_note_email(issue_id, note_id)
+ @note = Note.find(note_id)
+ setup_service_desk_mail(issue_id)
+
+ email_sender = sender(@note.author_id)
+ options = service_desk_options(email_sender, 'new_note')
+ .merge(subject: subject_base)
+
+ mail_answer_thread(@issue, options)
+ end
+
+ private
+
+ def setup_service_desk_mail(issue_id)
+ @issue = Issue.find(issue_id)
+ @project = @issue.project
+ @support_bot = User.support_bot
+
+ @sent_notification = SentNotification.record(@issue, @support_bot.id, reply_key)
+ end
+
+ def service_desk_options(email_sender, email_type)
+ {
+ from: email_sender,
+ to: @issue.service_desk_reply_to
+ }.tap do |options|
+ next unless template_body = template_content(email_type)
+
+ options[:body] = template_body
+ options[:content_type] = 'text/html'
+ end
+ end
+
+ def template_content(email_type)
+ template = Gitlab::Template::ServiceDeskTemplate.find(email_type, @project)
+
+ text = substitute_template_replacements(template.content)
+
+ markdown(text, project: @project)
+ rescue Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
+ nil
+ end
+
+ def substitute_template_replacements(template_body)
+ template_body
+ .gsub(/%\{\s*ISSUE_ID\s*\}/, issue_id)
+ .gsub(/%\{\s*ISSUE_PATH\s*\}/, issue_path)
+ .gsub(/%\{\s*NOTE_TEXT\s*\}/, note_text)
+ end
+
+ def issue_id
+ "#{Issue.reference_prefix}#{@issue.iid}"
+ end
+
+ def issue_path
+ @issue.to_reference(full: true)
+ end
+
+ def note_text
+ @note&.note.to_s
+ end
+
+ def subject_base
+ "#{@issue.title} (##{@issue.iid})"
+ end
+ end
+end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 2cf72d40635..f9aba3fe4f2 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -19,6 +19,7 @@ class Notify < ApplicationMailer
include Emails::Releases
include Emails::Groups
include Emails::Reviews
+ include Emails::ServiceDesk
helper TimeboxesHelper
helper MergeRequestsHelper
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index cb7c6a36c27..f3a4076e69c 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -165,6 +165,18 @@ class NotifyPreview < ActionMailer::Preview
Notify.unknown_sign_in_email(user, '127.0.0.1', Time.current).message
end
+ def service_desk_new_note_email
+ cleanup do
+ note = create_note(noteable_type: 'Issue', noteable_id: issue.id, note: 'Issue note content')
+
+ Notify.service_desk_new_note_email(issue.id, note.id).message
+ end
+ end
+
+ def service_desk_thank_you_email
+ Notify.service_desk_thank_you_email(issue.id).message
+ end
+
private
def project
diff --git a/app/models/event.rb b/app/models/event.rb
index 9c0fcbb354b..6cd091ca217 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -84,7 +84,6 @@ class Event < ApplicationRecord
scope :for_design, -> { where(target_type: 'DesignManagement::Design') }
# Needed to implement feature flag: can be removed when feature flag is removed
- scope :not_wiki_page, -> { where('target_type IS NULL or target_type <> ?', 'WikiPage::Meta') }
scope :not_design, -> { where('target_type IS NULL or target_type <> ?', 'DesignManagement::Design') }
scope :with_associations, -> do
diff --git a/app/models/event_collection.rb b/app/models/event_collection.rb
index 6c268bf6c36..ce062abeaaf 100644
--- a/app/models/event_collection.rb
+++ b/app/models/event_collection.rb
@@ -45,7 +45,6 @@ class EventCollection
private
def apply_feature_flags(events)
- events = events.not_wiki_page unless ::Feature.enabled?(:wiki_events)
events = events.not_design unless ::Feature.enabled?(:design_activity_events)
events
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index 85a62fefd8f..3dfa9626a79 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -2,6 +2,7 @@
module Clusters
class ClusterPresenter < Gitlab::View::Presenter::Delegated
+ include ::Gitlab::Utils::StrongMemoize
include ActionView::Helpers::SanitizeHelper
include ActionView::Helpers::UrlHelper
include IconsHelper
@@ -60,6 +61,12 @@ module Clusters
end
end
+ def gitlab_managed_apps_logs_path
+ return unless logs_project && can_read_cluster?
+
+ project_logs_path(logs_project, cluster_id: cluster.id)
+ end
+
def read_only_kubernetes_platform_fields?
!cluster.provided_by_user?
end
@@ -85,6 +92,16 @@ module Clusters
ActionController::Base.helpers.image_path(path)
end
+ # currently log explorer is only available in the scope of the project
+ # for group and instance level cluster selected project does not affects
+ # fetching logs from gitlab managed apps namespace, therefore any project
+ # available to user will be sufficient.
+ def logs_project
+ strong_memoize(:logs_project) do
+ cluster.all_projects.first
+ end
+ end
+
def clusterable
if cluster.group_type?
cluster.group
diff --git a/app/serializers/cluster_entity.rb b/app/serializers/cluster_entity.rb
index 8a1d41dbd96..a46f2889a96 100644
--- a/app/serializers/cluster_entity.rb
+++ b/app/serializers/cluster_entity.rb
@@ -16,4 +16,8 @@ class ClusterEntity < Grape::Entity
expose :path do |cluster|
Clusters::ClusterPresenter.new(cluster).show_path # rubocop: disable CodeReuse/Presenter
end
+
+ expose :gitlab_managed_apps_logs_path do |cluster|
+ Clusters::ClusterPresenter.new(cluster, current_user: request.current_user).gitlab_managed_apps_logs_path # rubocop: disable CodeReuse/Presenter
+ end
end
diff --git a/app/serializers/cluster_serializer.rb b/app/serializers/cluster_serializer.rb
index 27156d3178f..92363a4942c 100644
--- a/app/serializers/cluster_serializer.rb
+++ b/app/serializers/cluster_serializer.rb
@@ -10,6 +10,7 @@ class ClusterSerializer < BaseSerializer
:cluster_type,
:enabled,
:environment_scope,
+ :gitlab_managed_apps_logs_path,
:name,
:nodes,
:path,
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 7f71906bc89..5e184e41885 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -120,8 +120,6 @@ class EventCreateService
#
# @return a tuple of event and either :found or :created
def wiki_event(wiki_page_meta, author, action)
- return unless Feature.enabled?(:wiki_events)
-
raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action)
if duplicate = existing_wiki_event(wiki_page_meta, action)
diff --git a/app/services/git/wiki_push_service.rb b/app/services/git/wiki_push_service.rb
index 8bdbc28f3e8..b3937a10a70 100644
--- a/app/services/git/wiki_push_service.rb
+++ b/app/services/git/wiki_push_service.rb
@@ -23,7 +23,7 @@ module Git
end
def can_process_wiki_events?
- Feature.enabled?(:wiki_events) && Feature.enabled?(:wiki_events_on_git_push, project)
+ Feature.enabled?(:wiki_events_on_git_push, project)
end
def push_changes
diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb
index a0256ea5e69..2967684f7bc 100644
--- a/app/services/wiki_pages/base_service.rb
+++ b/app/services/wiki_pages/base_service.rb
@@ -44,8 +44,6 @@ module WikiPages
end
def create_wiki_event(page)
- return unless ::Feature.enabled?(:wiki_events)
-
response = WikiPages::EventCreateService.new(current_user).execute(slug_for_page(page), page, event_action)
log_error(response.message) if response.error?
diff --git a/app/services/wiki_pages/event_create_service.rb b/app/services/wiki_pages/event_create_service.rb
index 18a45d057a9..0453c90d693 100644
--- a/app/services/wiki_pages/event_create_service.rb
+++ b/app/services/wiki_pages/event_create_service.rb
@@ -10,8 +10,6 @@ module WikiPages
end
def execute(slug, page, action)
- return ServiceResponse.success(message: 'No event created as `wiki_events` feature is disabled') unless ::Feature.enabled?(:wiki_events)
-
event = Event.transaction do
wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)
diff --git a/app/views/layouts/service_desk.html.haml b/app/views/layouts/service_desk.html.haml
new file mode 100644
index 00000000000..26d15a74403
--- /dev/null
+++ b/app/views/layouts/service_desk.html.haml
@@ -0,0 +1,24 @@
+%html{ lang: "en" }
+ %head
+ %meta{ content: "text/html; charset=utf-8", "http-equiv" => "Content-Type" }
+ -# haml-lint:disable NoPlainNodes
+ %title
+ GitLab
+ -# haml-lint:enable NoPlainNodes
+ = stylesheet_link_tag 'notify'
+ = yield :head
+ %body
+ .content
+ = yield
+ .footer{ style: "margin-top: 10px;" }
+ %p
+ &mdash;
+ %br
+ = link_to "Unsubscribe", @unsubscribe_url
+
+ -# EE-specific start
+ - if Gitlab::CurrentSettings.email_additional_text.present?
+ %br
+ %br
+ = Gitlab::Utils.nlbr(Gitlab::CurrentSettings.email_additional_text)
+ -# EE-specific end
diff --git a/app/views/notify/service_desk_new_note_email.html.haml b/app/views/notify/service_desk_new_note_email.html.haml
new file mode 100644
index 00000000000..7c6be6688d0
--- /dev/null
+++ b/app/views/notify/service_desk_new_note_email.html.haml
@@ -0,0 +1,5 @@
+- if Gitlab::CurrentSettings.email_author_in_body
+ %div
+ #{link_to @note.author_name, user_url(@note.author)} wrote:
+%div
+ = markdown(@note.note, pipeline: :email, author: @note.author)
diff --git a/app/views/notify/service_desk_new_note_email.text.erb b/app/views/notify/service_desk_new_note_email.text.erb
new file mode 100644
index 00000000000..208953a437d
--- /dev/null
+++ b/app/views/notify/service_desk_new_note_email.text.erb
@@ -0,0 +1,6 @@
+New response for issue #<%= @issue.iid %>:
+
+Author: <%= sanitize_name(@note.author_name) %>
+
+<%= @note.note %>
+<%# EE-specific start %><%= render_if_exists 'layouts/mailer/additional_text'%><%# EE-specific end %>
diff --git a/app/views/notify/service_desk_thank_you_email.html.haml b/app/views/notify/service_desk_thank_you_email.html.haml
new file mode 100644
index 00000000000..a3407acd9ba
--- /dev/null
+++ b/app/views/notify/service_desk_thank_you_email.html.haml
@@ -0,0 +1,2 @@
+%p
+ Thank you for your support request! We are tracking your request as ticket ##{@issue.iid}, and will respond as soon as we can.
diff --git a/app/views/notify/service_desk_thank_you_email.text.erb b/app/views/notify/service_desk_thank_you_email.text.erb
new file mode 100644
index 00000000000..8281607a4a8
--- /dev/null
+++ b/app/views/notify/service_desk_thank_you_email.text.erb
@@ -0,0 +1,6 @@
+Thank you for your support request! We are tracking your request as ticket #<%= @issue.iid %>, and will respond as soon as we can.
+
+To unsubscribe from this issue, please paste the following link into your browser:
+
+<%= @unsubscribe_url %>
+<%# EE-specific start %><%= render_if_exists 'layouts/mailer/additional_text' %><%# EE-specific end %>
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index afa344e87f7..03534bf78d1 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -15,7 +15,7 @@
= render_if_exists 'events/epics_filter'
- if comments_visible?
= event_filter_link EventFilter::COMMENTS, _('Comments'), s_('EventFilterBy|Filter by comments')
- - if Feature.enabled?(:wiki_events) && (@project.nil? || @project.has_wiki?)
+ - if @project.nil? || @project.has_wiki?
= event_filter_link EventFilter::WIKI, _('Wiki'), s_('EventFilterBy|Filter by wiki')
- if event_filter_visible(:designs)
= event_filter_link EventFilter::DESIGNS, _('Designs'), s_('EventFilterBy|Filter by designs')
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 62c0bcf0093..4b308f0cb3b 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1660,6 +1660,14 @@
:weight: 2
:idempotent:
:tags: []
+- :name: service_desk_email_receiver
+ :feature_category: :issue_tracking
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+ :tags: []
- :name: system_hook_push
:feature_category: :source_code_management
:has_external_dependencies:
diff --git a/app/workers/service_desk_email_receiver_worker.rb b/app/workers/service_desk_email_receiver_worker.rb
new file mode 100644
index 00000000000..8649034445c
--- /dev/null
+++ b/app/workers/service_desk_email_receiver_worker.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class ServiceDeskEmailReceiverWorker < EmailReceiverWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ def perform(raw)
+ return unless ::Gitlab::ServiceDeskEmail.enabled?
+
+ begin
+ Gitlab::Email::ServiceDeskReceiver.new(raw).execute
+ rescue => e
+ handle_failure(raw, e)
+ end
+ end
+end
diff --git a/changelogs/unreleased/208655-introduce-prepare-keyword-to-environment-action-to-annotate-non-de.yml b/changelogs/unreleased/208655-introduce-prepare-keyword-to-environment-action-to-annotate-non-de.yml
new file mode 100644
index 00000000000..efb63195aff
--- /dev/null
+++ b/changelogs/unreleased/208655-introduce-prepare-keyword-to-environment-action-to-annotate-non-de.yml
@@ -0,0 +1,5 @@
+---
+title: Introduce prepare environment action to annotate non-deployment jobs
+merge_request: 35642
+author:
+type: added
diff --git a/changelogs/unreleased/ajk-ff-remove-wiki_events.yml b/changelogs/unreleased/ajk-ff-remove-wiki_events.yml
new file mode 100644
index 00000000000..777d16cc45b
--- /dev/null
+++ b/changelogs/unreleased/ajk-ff-remove-wiki_events.yml
@@ -0,0 +1,5 @@
+---
+title: Enable display of wiki events in activity streams
+merge_request: 32475
+author:
+type: changed
diff --git a/changelogs/unreleased/alert-intergration-trigger-test-docs.yml b/changelogs/unreleased/alert-intergration-trigger-test-docs.yml
new file mode 100644
index 00000000000..7fd7930e5dc
--- /dev/null
+++ b/changelogs/unreleased/alert-intergration-trigger-test-docs.yml
@@ -0,0 +1,5 @@
+---
+title: Add docs for Alert trigger test alerts
+merge_request: 36647
+author:
+type: added
diff --git a/doc/operations/feature_flags.md b/doc/operations/feature_flags.md
index 3fcd2f91626..116dc0b6b96 100644
--- a/doc/operations/feature_flags.md
+++ b/doc/operations/feature_flags.md
@@ -105,6 +105,9 @@ For example, set a value of 15% to enable the feature for 15% of authenticated u
The rollout percentage can be from 0% to 100%.
+NOTE: **Note:**
+Stickiness (consistent application behavior for the same user) is guaranteed for logged-in users, but not anonymous users.
+
CAUTION: **Caution:**
If this strategy is selected, then the Unleash client **must** be given a user
ID for the feature to be enabled. See the [Ruby example](#ruby-application-example) below.
@@ -120,6 +123,9 @@ activation strategy.
Enter user IDs as a comma-separated list of values. For example,
`user@example.com, user2@example.com`, or `username1,username2,username3`, and so on.
+NOTE: **Note:**
+User IDs are identifiers for your application users. They do not need to be GitLab users.
+
CAUTION: **Caution:**
The Unleash client **must** be given a user ID for the feature to be enabled for
target users. See the [Ruby example](#ruby-application-example) below.
diff --git a/doc/user/asciidoc.md b/doc/user/asciidoc.md
index 8834deb8d50..512a98d567b 100644
--- a/doc/user/asciidoc.md
+++ b/doc/user/asciidoc.md
@@ -10,14 +10,14 @@ You can find the full documentation for the AsciiDoc syntax at <https://asciidoc
### Paragraphs
-```asciidoc
+```plaintext
A normal paragraph.
Line breaks are not preserved.
```
Line comments, which are lines that start with `//`, are skipped:
-```asciidoc
+```plaintext
// this is a comment
```
@@ -25,7 +25,7 @@ A blank line separates paragraphs.
A paragraph with the `[%hardbreaks]` option will preserve line breaks:
-```asciidoc
+```plaintext
[%hardbreaks]
This paragraph carries the `hardbreaks` option.
Notice how line breaks are now preserved.
@@ -35,7 +35,7 @@ An indented (literal) paragraph disables text formatting,
preserves spaces and line breaks, and is displayed in a
monospaced font:
-```asciidoc
+```plaintext
This literal paragraph is indented with one space.
As a consequence, *text formatting*, spaces,
and lines breaks will be preserved.
@@ -43,7 +43,7 @@ monospaced font:
An admonition paragraph grabs the reader's attention:
-```asciidoc
+```plaintext
NOTE: This is a brief reference, please read the full documentation at https://asciidoctor.org/docs/.
TIP: Lists can be indented. Leading whitespace is not significant.
@@ -53,7 +53,7 @@ TIP: Lists can be indented. Leading whitespace is not significant.
**Constrained (applied at word boundaries)**
-```asciidoc
+```plaintext
*strong importance* (aka bold)
_stress emphasis_ (aka italic)
`monospaced` (aka typewriter text)
@@ -64,7 +64,7 @@ _stress emphasis_ (aka italic)
**Unconstrained (applied anywhere)**
-```asciidoc
+```plaintext
**C**reate+**R**ead+**U**pdate+**D**elete
fan__freakin__tastic
``mono``culture
@@ -72,7 +72,7 @@ fan__freakin__tastic
**Replacements**
-```asciidoc
+```plaintext
A long time ago in a galaxy far, far away...
(C) 1976 Arty Artisan
I believe I shall--no, actually I won't.
@@ -80,7 +80,7 @@ I believe I shall--no, actually I won't.
**Macros**
-```asciidoc
+```plaintext
// where c=specialchars, q=quotes, a=attributes, r=replacements, m=macros, p=post_replacements, etc.
The European icon:flag[role=blue] is blue & contains pass:[************] arranged in a icon:circle-o[role=yellow].
The pass:c[->] operator is often referred to as the stabby lambda.
@@ -93,12 +93,12 @@ stem:[sqrt(4) = 2]
**User-defined attributes**
-```asciidoc
+```plaintext
// define attributes in the document header
:name: value
```
-```asciidoc
+```plaintext
:url-gem: https://rubygems.org/gems/asciidoctor
You can download and install Asciidoctor {asciidoctor-version} from {url-gem}.
@@ -117,7 +117,7 @@ GitLab sets the following environment attributes:
### Links
-```asciidoc
+```plaintext
https://example.org/page[A webpage]
link:../path/to/file.txt[A local file]
xref:document.adoc[A sibling document]
@@ -126,7 +126,7 @@ mailto:hello@example.org[Email to say hello!]
### Anchors
-```asciidoc
+```plaintext
[[idname,reference text]]
// or written using normal block attributes as `[#idname,reftext=reference text]`
A paragraph (or any block) with an anchor (aka ID) and reftext.
@@ -142,7 +142,7 @@ This paragraph has a footnote.footnote:[This is the text of the footnote.]
#### Unordered
-```asciidoc
+```plaintext
* level 1
** level 2
*** level 3
@@ -161,7 +161,7 @@ Attach a block or paragraph to a list item using a list continuation (which you
#### Ordered
-```asciidoc
+```plaintext
. Step 1
. Step 2
.. Step 2a
@@ -177,14 +177,14 @@ Attach a block or paragraph to a list item using a list continuation (which you
#### Checklist
-```asciidoc
+```plaintext
* [x] checked
* [ ] not checked
```
#### Callout
-```asciidoc
+```plaintext
// enable callout bubbles by adding `:icons: font` to the document header
[,ruby]
----
@@ -195,7 +195,7 @@ puts 'Hello, World!' # <1>
#### Description
-```asciidoc
+```plaintext
first term:: description of first term
second term::
description of second term
@@ -205,7 +205,7 @@ description of second term
#### Header
-```asciidoc
+```plaintext
= Document Title
Author Name <author@example.org>
v1.0, 2019-01-01
@@ -213,7 +213,7 @@ v1.0, 2019-01-01
#### Sections
-```asciidoc
+```plaintext
= Document Title (Level 0)
== Level 1
=== Level 2
@@ -225,7 +225,7 @@ v1.0, 2019-01-01
#### Includes
-```asciidoc
+```plaintext
include::basics.adoc[]
// define -a allow-uri-read to allow content to be read from URI
@@ -239,13 +239,13 @@ included, a number that is inclusive of transitive dependencies.
### Blocks
-```asciidoc
+```plaintext
--
open - a general-purpose content wrapper; useful for enclosing content to attach to a list item
--
```
-```asciidoc
+```plaintext
// recognized types include CAUTION, IMPORTANT, NOTE, TIP, and WARNING
// enable admonition icons by setting `:icons: font` in the document header
[NOTE]
@@ -254,13 +254,13 @@ admonition - a notice for the reader, ranging in severity from a tip to an alert
====
```
-```asciidoc
+```plaintext
====
example - a demonstration of the concept being documented
====
```
-```asciidoc
+```plaintext
.Toggle Me
[%collapsible]
====
@@ -268,58 +268,58 @@ collapsible - these details are revealed by clicking the title
====
```
-```asciidoc
+```plaintext
****
sidebar - auxiliary content that can be read independently of the main content
****
```
-```asciidoc
+```plaintext
....
literal - an exhibit that features program output
....
```
-```asciidoc
+```plaintext
----
listing - an exhibit that features program input, source code, or the contents of a file
----
```
-```asciidoc
+```plaintext
[,language]
----
source - a listing that is embellished with (colorized) syntax highlighting
----
```
-````asciidoc
+````plaintext
\```language
fenced code - a shorthand syntax for the source block
\```
````
-```asciidoc
+```plaintext
[,attribution,citetitle]
____
quote - a quotation or excerpt; attribution with title of source are optional
____
```
-```asciidoc
+```plaintext
[verse,attribution,citetitle]
____
verse - a literary excerpt, often a poem; attribution with title of source are optional
____
```
-```asciidoc
+```plaintext
++++
pass - content passed directly to the output document; often raw HTML
++++
```
-```asciidoc
+```plaintext
// activate stem support by adding `:stem:` to the document header
[stem]
++++
@@ -327,7 +327,7 @@ x = y^2
++++
```
-```asciidoc
+```plaintext
////
comment - content which is not included in the output document
////
@@ -335,7 +335,7 @@ comment - content which is not included in the output document
### Tables
-```asciidoc
+```plaintext
.Table Attributes
[cols=>1h;2d,width=50%,frame=topbot]
|===
@@ -366,7 +366,7 @@ comment - content which is not included in the output document
### Multimedia
-```asciidoc
+```plaintext
image::screenshot.png[block image,800,450]
Press image:reload.svg[reload,16,opts=interactive] to reload the page.
@@ -380,12 +380,12 @@ video::300817511[vimeo]
### Breaks
-```asciidoc
+```plaintext
// thematic break (aka horizontal rule)
---
```
-```asciidoc
+```plaintext
// page break
<<<
```
diff --git a/doc/user/project/integrations/generic_alerts.md b/doc/user/project/integrations/generic_alerts.md
index 8515008efbd..f8b7cd2acd9 100644
--- a/doc/user/project/integrations/generic_alerts.md
+++ b/doc/user/project/integrations/generic_alerts.md
@@ -90,6 +90,22 @@ Example payload:
}
```
+## Triggering test alerts
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3066) in GitLab Core in 13.2.
+
+After a [project maintainer or owner](#setting-up-generic-alerts)
+[configures generic alerts](#setting-up-generic-alerts), you can trigger a
+test alert to confirm your integration works properly.
+
+1. Sign in as a user with Developer or greater [permissions](../../../user/permissions.md).
+1. Navigate to **{settings}** **Settings > Operations** in your project.
+1. Click **Alerts endpoint** to expand the section.
+1. Enter a sample payload in **Alert test payload** (valid JSON is required).
+1. Click **Test alert payload**.
+
+GitLab displays an error or success message, depending on the outcome of your test.
+
## Automatic grouping of identical alerts **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214557) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index 4ae2a3c0d79..835ce58a766 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -24,6 +24,8 @@ file path fragments to start seeing results.
## Syntax highlighting
+> Support for `.gitlab.ci.yml` validation [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218472) in GitLab 13.2.
+
As expected from an IDE, syntax highlighting for many languages within
the Web IDE will make your direct editing even easier.
@@ -35,6 +37,13 @@ The Web IDE currently provides:
- IntelliSense and validation support (displaying errors and warnings, providing
smart completions, formatting, and outlining) for some languages. For example:
TypeScript, JavaScript, CSS, LESS, SCSS, JSON, and HTML.
+- Validation support for certain JSON and YAML files using schemas based on the
+ [JSON Schema Store](https://www.schemastore.org/json/). This feature
+ is only supported for the `.gitlab-ci.yml` file.
+
+ NOTE: **Note:** Validation support based on schemas is hidden behind
+ the feature flag `:schema_linting` on self-managed installations. To enable the
+ feature, you can [turn on the feature flag in Rails console](../../../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags).
Because the Web IDE is based on the [Monaco Editor](https://microsoft.github.io/monaco-editor/),
you can find a more complete list of supported languages in the
diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md
index 5b336fffa6a..9044ee0765f 100644
--- a/doc/user/project/wiki/index.md
+++ b/doc/user/project/wiki/index.md
@@ -154,39 +154,48 @@ Similar to versioned diff file views, you can see the changes made in a given Wi
## Wiki activity records
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14902) in GitLab 12.10.
-> - It's deployed behind a feature flag, disabled by default.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14902) in **GitLab 12.10.**
+> - Git events were [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216014) in **GitLab 13.0.**
> - It's enabled on GitLab.com.
-> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-wiki-events-core-only). **(CORE ONLY)**
+> - Git access activity creation is managed by a feature flag.
+> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-wiki-events-in-git-core-only). **(CORE ONLY)**
Wiki events (creation, deletion, and updates) are tracked by GitLab and
displayed on the [user profile](../../profile/index.md#user-profile),
[group](../../group/index.md#view-group-activity),
and [project](../index.md#project-activity) activity pages.
-### Limitations
+### Enable or disable Wiki events in Git **(CORE ONLY)**
-Only edits made in the browser or through the API have their activity recorded.
-Edits made and pushed through Git are not currently listed in the activity list.
-
-### Enable or disable Wiki Events **(CORE ONLY)**
-
-Wiki event activity is under development and not ready for production use. It is
+Tracking wiki events through Git is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
-[GitLab administrators with access to the GitLab Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session)
-can enable it for your instance. You're welcome to test it, but use it at your
-own risk.
+[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
+can enable it for your instance.
To enable it:
```ruby
-Feature.enable(:wiki_events)
+Feature.enable(:wiki_events_on_git_push)
+```
+
+To enable for just a particular project:
+
+```ruby
+project = Project.find_by_full_path('your-group/your-project')
+Feature.enable(:wiki_events_on_git_push, project)
```
To disable it:
```ruby
-Feature.disable(:wiki_events)
+Feature.disable(:wiki_events_on_git_push)
+```
+
+To disable for just a particular project:
+
+```ruby
+project = Project.find_by_full_path('your-group/your-project')
+Feature.disable(:wiki_events_on_git_push, project)
```
## Adding and editing wiki pages locally
diff --git a/lib/event_filter.rb b/lib/event_filter.rb
index eab64d05114..73bdc8f0649 100644
--- a/lib/event_filter.rb
+++ b/lib/event_filter.rb
@@ -52,15 +52,12 @@ class EventFilter
private
def apply_feature_flags(events)
- events = events.not_wiki_page unless Feature.enabled?(:wiki_events)
events = events.not_design unless can_view_design_activity?
events
end
def wiki_events(events)
- return events unless Feature.enabled?(:wiki_events)
-
events.for_wiki_page
end
diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
index fc62cca58ff..64e6d48133f 100644
--- a/lib/gitlab/ci/config/entry/environment.rb
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -44,7 +44,7 @@ module Gitlab
validates :action,
type: String,
- inclusion: { in: %w[start stop], message: 'should be start or stop' },
+ inclusion: { in: %w[start stop prepare], message: 'should be start, stop or prepare' },
allow_nil: true
validates :on_stop, type: String, allow_nil: true
diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb
index 7f8dd815103..1b8421d34f3 100644
--- a/lib/gitlab/email/handler.rb
+++ b/lib/gitlab/email/handler.rb
@@ -12,7 +12,8 @@ module Gitlab
CreateNoteHandler,
CreateIssueHandler,
UnsubscribeHandler,
- CreateMergeRequestHandler
+ CreateMergeRequestHandler,
+ ServiceDeskHandler
]
end
@@ -25,5 +26,3 @@ module Gitlab
end
end
end
-
-Gitlab::Email::Handler.prepend_if_ee('::EE::Gitlab::Email::Handler')
diff --git a/lib/gitlab/email/handler/reply_processing.rb b/lib/gitlab/email/handler/reply_processing.rb
index 312a9fdfbae..1beea4f9054 100644
--- a/lib/gitlab/email/handler/reply_processing.rb
+++ b/lib/gitlab/email/handler/reply_processing.rb
@@ -37,7 +37,11 @@ module Gitlab
def process_message(**kwargs)
message = ReplyParser.new(mail, **kwargs).execute.strip
- add_attachments(message)
+ message_with_attachments = add_attachments(message)
+
+ # Support bot is specifically forbidden
+ # from using slash commands.
+ strip_quick_actions(message_with_attachments)
end
def add_attachments(reply)
@@ -82,6 +86,15 @@ module Gitlab
def valid_project_slug?(found_project)
project_slug == found_project.full_path_slug
end
+
+ def strip_quick_actions(content)
+ return content unless author.support_bot?
+
+ command_definitions = ::QuickActions::InterpretService.command_definitions
+ extractor = ::Gitlab::QuickActions::Extractor.new(command_definitions)
+
+ extractor.redact_commands(content)
+ end
end
end
end
diff --git a/lib/gitlab/email/handler/service_desk_handler.rb b/lib/gitlab/email/handler/service_desk_handler.rb
new file mode 100644
index 00000000000..bcd8b98a06f
--- /dev/null
+++ b/lib/gitlab/email/handler/service_desk_handler.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+
+# handles service desk issue creation emails with these formats:
+# incoming+gitlab-org-gitlab-ce-20-issue-@incoming.gitlab.com
+# incoming+gitlab-org/gitlab-ce@incoming.gitlab.com (legacy)
+module Gitlab
+ module Email
+ module Handler
+ class ServiceDeskHandler < BaseHandler
+ include ReplyProcessing
+ include Gitlab::Utils::StrongMemoize
+
+ HANDLER_REGEX = /\A#{HANDLER_ACTION_BASE_REGEX}-issue-\z/.freeze
+ HANDLER_REGEX_LEGACY = /\A(?<project_path>[^\+]*)\z/.freeze
+ PROJECT_KEY_PATTERN = /\A(?<slug>.+)-(?<key>[a-z0-9_]+)\z/.freeze
+
+ def initialize(mail, mail_key, service_desk_key: nil)
+ super(mail, mail_key)
+
+ if service_desk_key.present?
+ @service_desk_key = service_desk_key
+ elsif !mail_key&.include?('/') && (matched = HANDLER_REGEX.match(mail_key.to_s))
+ @project_slug = matched[:project_slug]
+ @project_id = matched[:project_id]&.to_i
+ elsif matched = HANDLER_REGEX_LEGACY.match(mail_key.to_s)
+ @project_path = matched[:project_path]
+ end
+ end
+
+ def can_handle?
+ Gitlab::ServiceDesk.supported? && (project_id || can_handle_legacy_format? || service_desk_key)
+ end
+
+ def execute
+ raise ProjectNotFound if project.nil?
+
+ create_issue!
+ send_thank_you_email! if from_address
+ end
+
+ def metrics_params
+ super.merge(project: project&.full_path)
+ end
+
+ def metrics_event
+ :receive_email_service_desk
+ end
+
+ private
+
+ attr_reader :project_id, :project_path, :service_desk_key
+
+ def project
+ strong_memoize(:project) do
+ @project = service_desk_key ? project_from_key : super
+ @project = nil unless @project&.service_desk_enabled?
+ @project
+ end
+ end
+
+ def project_from_key
+ return unless match = service_desk_key.match(PROJECT_KEY_PATTERN)
+
+ project = Project.find_by_service_desk_project_key(match[:key])
+ return unless valid_project_key?(project, match[:slug])
+
+ project
+ end
+
+ def valid_project_key?(project, slug)
+ project.present? && slug == project.full_path_slug && Feature.enabled?(:service_desk_custom_address, project)
+ end
+
+ def create_issue!
+ @issue = Issues::CreateService.new(
+ project,
+ User.support_bot,
+ title: issue_title,
+ description: message_including_template,
+ confidential: true,
+ service_desk_reply_to: from_address
+ ).execute
+
+ raise InvalidIssueError unless @issue.persisted?
+
+ if service_desk_setting&.issue_template_missing?
+ create_template_not_found_note(@issue)
+ end
+ end
+
+ def send_thank_you_email!
+ Notify.service_desk_thank_you_email(@issue.id).deliver_later!
+ end
+
+ def message_including_template
+ description = message_including_reply
+ template_content = service_desk_setting&.issue_template_content
+
+ if template_content.present?
+ description += " \n" + template_content
+ end
+
+ description
+ end
+
+ def service_desk_setting
+ strong_memoize(:service_desk_setting) do
+ project.service_desk_setting
+ end
+ end
+
+ def create_template_not_found_note(issue)
+ issue_template_key = service_desk_setting&.issue_template_key
+
+ warning_note = <<-MD.strip_heredoc
+ WARNING: The template file #{issue_template_key}.md used for service desk issues is empty or could not be found.
+ Please check service desk settings and update the file to be used.
+ MD
+
+ note_params = {
+ noteable: issue,
+ note: warning_note
+ }
+
+ ::Notes::CreateService.new(
+ project,
+ User.support_bot,
+ note_params
+ ).execute
+ end
+
+ def from_address
+ (mail.reply_to || []).first || mail.from.first || mail.sender
+ end
+
+ def issue_title
+ from = "(from #{from_address})" if from_address
+
+ "Service Desk #{from}: #{mail.subject}"
+ end
+
+ def can_handle_legacy_format?
+ project_path && project_path.include?('/') && !mail_key.include?('+')
+ end
+
+ def author
+ User.support_bot
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/service_desk_receiver.rb b/lib/gitlab/email/service_desk_receiver.rb
new file mode 100644
index 00000000000..1ee5c10097b
--- /dev/null
+++ b/lib/gitlab/email/service_desk_receiver.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Email
+ class ServiceDeskReceiver < Receiver
+ private
+
+ def find_handler(mail)
+ key = service_desk_key(mail)
+ return unless key
+
+ Gitlab::Email::Handler::ServiceDeskHandler.new(mail, nil, service_desk_key: key)
+ end
+
+ def service_desk_key(mail)
+ mail.to.find do |address|
+ key = ::Gitlab::ServiceDeskEmail.key_from_address(address)
+ break key if key
+ end
+ end
+ end
+ end
+end
diff --git a/package.json b/package.json
index 4f03dda3220..e023f094a7f 100644
--- a/package.json
+++ b/package.json
@@ -42,8 +42,8 @@
"@babel/plugin-syntax-import-meta": "^7.10.1",
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.5",
- "@gitlab/svgs": "1.151.0",
- "@gitlab/ui": "17.22.1",
+ "@gitlab/svgs": "1.152.0",
+ "@gitlab/ui": "17.26.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-1",
"@sentry/browser": "^5.10.2",
diff --git a/spec/finders/events_finder_spec.rb b/spec/finders/events_finder_spec.rb
index b13ef7e94e7..fe2e449f03d 100644
--- a/spec/finders/events_finder_spec.rb
+++ b/spec/finders/events_finder_spec.rb
@@ -66,29 +66,13 @@ RSpec.describe EventsFinder do
end
end
- describe 'wiki events feature flag' do
+ describe 'wiki events' do
let_it_be(:events) { create_list(:wiki_page_event, 3, project: public_project) }
subject(:finder) { described_class.new(source: public_project, target_type: 'wiki', current_user: user) }
- context 'the wiki_events feature flag is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'omits the wiki page events' do
- expect(finder.execute).to be_empty
- end
- end
-
- context 'the wiki_events feature flag is enabled' do
- before do
- stub_feature_flags(wiki_events: true)
- end
-
- it 'can find the wiki events' do
- expect(finder.execute).to match_array(events)
- end
+ it 'can find the wiki events' do
+ expect(finder.execute).to match_array(events)
end
end
diff --git a/spec/fixtures/emails/service_desk.eml b/spec/fixtures/emails/service_desk.eml
new file mode 100644
index 00000000000..0db1270bc64
--- /dev/null
+++ b/spec/fixtures/emails/service_desk.eml
@@ -0,0 +1,28 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+email-test-project_id-issue-@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+email-test-project_id-issue-@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: The message subject! @all
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+Service desk stuff!
+
+```
+a = b
+```
+
+/label ~label1
+/assign @user1
+/close
diff --git a/spec/fixtures/emails/service_desk_custom_address.eml b/spec/fixtures/emails/service_desk_custom_address.eml
new file mode 100644
index 00000000000..3293dd48303
--- /dev/null
+++ b/spec/fixtures/emails/service_desk_custom_address.eml
@@ -0,0 +1,27 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <support+project_slug-project_key@example.com>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: support+project_slug-project_key@example.com
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: The message subject! @all
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+Service desk stuff!
+
+```
+a = b
+```
+
+/label ~label1
+/assign @user1
+/close
diff --git a/spec/fixtures/emails/service_desk_forwarded.eml b/spec/fixtures/emails/service_desk_forwarded.eml
new file mode 100644
index 00000000000..56987972808
--- /dev/null
+++ b/spec/fixtures/emails/service_desk_forwarded.eml
@@ -0,0 +1,30 @@
+Delivered-To: incoming+email-test-project_id-issue-@appmail.adventuretime.ooo
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+email-test-project_id-issue-@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: support@adventuretime.ooo
+Delivered-To: support@adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: The message subject! @all
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+Service desk stuff!
+
+```
+a = b
+```
+
+/label ~label1
+/assign @user1
+/close
diff --git a/spec/fixtures/emails/service_desk_forwarded_new_issue.eml b/spec/fixtures/emails/service_desk_forwarded_new_issue.eml
new file mode 100644
index 00000000000..4eedb24b32d
--- /dev/null
+++ b/spec/fixtures/emails/service_desk_forwarded_new_issue.eml
@@ -0,0 +1,29 @@
+Delivered-To: incoming+email-test-project_id-issue-@appmail.adventuretime.ooo
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+email-test-project_id-issue-@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: support@adventuretime.ooo
+Delivered-To: support@adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: The message subject! @all
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+Service desk stuff!
+
+---------- Forwarded message ---------
+From: Jake the Dog <jake@adventuretime.ooo>
+To: <jake@adventuretime.ooo>
+
+
+forwarded content
diff --git a/spec/fixtures/emails/service_desk_legacy.eml b/spec/fixtures/emails/service_desk_legacy.eml
new file mode 100644
index 00000000000..fc9178c937b
--- /dev/null
+++ b/spec/fixtures/emails/service_desk_legacy.eml
@@ -0,0 +1,28 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+email/test@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: The message subject! @all
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+Service desk stuff!
+
+```
+a = b
+```
+
+/label ~label1
+/assign @user1
+/close
diff --git a/spec/fixtures/emails/service_desk_sender_and_from.eml b/spec/fixtures/emails/service_desk_sender_and_from.eml
new file mode 100644
index 00000000000..987c24d70bf
--- /dev/null
+++ b/spec/fixtures/emails/service_desk_sender_and_from.eml
@@ -0,0 +1,27 @@
+Delivered-To: incoming+email-test-project_id-issue-@appmail.adventuretime.ooo
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+email-test-project_id-issue-@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Finn the Human <finn@adventuretime.ooo>
+Sender: Jake the Dog <jake@adventuretime.ooo>
+To: support@adventuretime.ooo
+Delivered-To: support@adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: The message subject! @all
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+Service desk stuff!
+
+```
+a = b
+```
diff --git a/spec/fixtures/emails/valid_reply_with_quick_actions.eml b/spec/fixtures/emails/valid_reply_with_quick_actions.eml
new file mode 100644
index 00000000000..cd00b6eb8b8
--- /dev/null
+++ b/spec/fixtures/emails/valid_reply_with_quick_actions.eml
@@ -0,0 +1,45 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+In-Reply-To: <issue_1@localhost>
+References: <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost> <issue_1@localhost>
+Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+I could not disagree more. I am obviously biased but adventure time is the
+greatest show ever created. Everyone should watch it.
+
+- Jake out
+
+/close
+/title test
+
+
+On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
+<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
+>
+>
+>
+> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
+>
+> ---
+> hey guys everyone knows adventure time sucks!
+>
+> ---
+> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
+>
+> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
+>
diff --git a/spec/frontend/reports/codequality_report/mock_data.js b/spec/frontend/reports/codequality_report/mock_data.js
new file mode 100644
index 00000000000..9bd61527d3f
--- /dev/null
+++ b/spec/frontend/reports/codequality_report/mock_data.js
@@ -0,0 +1,90 @@
+export const headIssues = [
+ {
+ check_name: 'Rubocop/Lint/UselessAssignment',
+ description: 'Insecure Dependency',
+ location: {
+ path: 'lib/six.rb',
+ lines: {
+ begin: 6,
+ end: 7,
+ },
+ },
+ fingerprint: 'e879dd9bbc0953cad5037cde7ff0f627',
+ },
+ {
+ categories: ['Security'],
+ check_name: 'Insecure Dependency',
+ description: 'Insecure Dependency',
+ location: {
+ path: 'Gemfile.lock',
+ lines: {
+ begin: 22,
+ end: 22,
+ },
+ },
+ fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5',
+ },
+];
+
+export const mockParsedHeadIssues = [
+ {
+ ...headIssues[1],
+ name: 'Insecure Dependency',
+ path: 'lib/six.rb',
+ urlPath: 'headPath/lib/six.rb#L6',
+ line: 6,
+ },
+];
+
+export const baseIssues = [
+ {
+ categories: ['Security'],
+ check_name: 'Insecure Dependency',
+ description: 'Insecure Dependency',
+ location: {
+ path: 'Gemfile.lock',
+ lines: {
+ begin: 22,
+ end: 22,
+ },
+ },
+ fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5',
+ },
+ {
+ categories: ['Security'],
+ check_name: 'Insecure Dependency',
+ description: 'Insecure Dependency',
+ location: {
+ path: 'Gemfile.lock',
+ lines: {
+ begin: 21,
+ end: 21,
+ },
+ },
+ fingerprint: 'ca2354534dee94ae60ba2f54e3857c50e5',
+ },
+];
+
+export const mockParsedBaseIssues = [
+ {
+ ...baseIssues[1],
+ name: 'Insecure Dependency',
+ path: 'Gemfile.lock',
+ line: 21,
+ urlPath: 'basePath/Gemfile.lock#L21',
+ },
+];
+
+export const issueDiff = [
+ {
+ categories: ['Security'],
+ check_name: 'Insecure Dependency',
+ description: 'Insecure Dependency',
+ fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5',
+ line: 6,
+ location: { lines: { begin: 22, end: 22 }, path: 'Gemfile.lock' },
+ name: 'Insecure Dependency',
+ path: 'lib/six.rb',
+ urlPath: 'headPath/lib/six.rb#L6',
+ },
+];
diff --git a/spec/frontend/reports/codequality_report/store/actions_spec.js b/spec/frontend/reports/codequality_report/store/actions_spec.js
new file mode 100644
index 00000000000..6c30fdb7871
--- /dev/null
+++ b/spec/frontend/reports/codequality_report/store/actions_spec.js
@@ -0,0 +1,151 @@
+import axios from '~/lib/utils/axios_utils';
+import MockAdapter from 'axios-mock-adapter';
+import * as actions from '~/reports/codequality_report/store/actions';
+import * as types from '~/reports/codequality_report/store/mutation_types';
+import createStore from '~/reports/codequality_report/store';
+import { TEST_HOST } from 'spec/test_constants';
+import testAction from 'helpers/vuex_action_helper';
+import { headIssues, baseIssues, mockParsedHeadIssues, mockParsedBaseIssues } from '../mock_data';
+
+// mock codequality comparison worker
+jest.mock('~/reports/codequality_report/workers/codequality_comparison_worker', () =>
+ jest.fn().mockImplementation(() => {
+ return {
+ addEventListener: (eventName, callback) => {
+ callback({
+ data: {
+ newIssues: [mockParsedHeadIssues[0]],
+ resolvedIssues: [mockParsedBaseIssues[0]],
+ },
+ });
+ },
+ };
+ }),
+);
+
+describe('Codequality Reports actions', () => {
+ let localState;
+ let localStore;
+
+ beforeEach(() => {
+ localStore = createStore();
+ localState = localStore.state;
+ });
+
+ describe('setPaths', () => {
+ it('should commit SET_PATHS mutation', done => {
+ const paths = {
+ basePath: 'basePath',
+ headPath: 'headPath',
+ baseBlobPath: 'baseBlobPath',
+ headBlobPath: 'headBlobPath',
+ helpPath: 'codequalityHelpPath',
+ };
+
+ testAction(
+ actions.setPaths,
+ paths,
+ localState,
+ [{ type: types.SET_PATHS, payload: paths }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchReports', () => {
+ let mock;
+
+ beforeEach(() => {
+ localState.headPath = `${TEST_HOST}/head.json`;
+ localState.basePath = `${TEST_HOST}/base.json`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('on success', () => {
+ it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', done => {
+ mock.onGet(`${TEST_HOST}/head.json`).reply(200, headIssues);
+ mock.onGet(`${TEST_HOST}/base.json`).reply(200, baseIssues);
+
+ testAction(
+ actions.fetchReports,
+ null,
+ localState,
+ [{ type: types.REQUEST_REPORTS }],
+ [
+ {
+ payload: {
+ newIssues: [mockParsedHeadIssues[0]],
+ resolvedIssues: [mockParsedBaseIssues[0]],
+ },
+ type: 'receiveReportsSuccess',
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('on error', () => {
+ it('commits REQUEST_REPORTS and dispatches receiveReportsError', done => {
+ mock.onGet(`${TEST_HOST}/head.json`).reply(500);
+
+ testAction(
+ actions.fetchReports,
+ null,
+ localState,
+ [{ type: types.REQUEST_REPORTS }],
+ [{ type: 'receiveReportsError' }],
+ done,
+ );
+ });
+ });
+
+ describe('with no base path', () => {
+ it('commits REQUEST_REPORTS and dispatches receiveReportsError', done => {
+ localState.basePath = null;
+
+ testAction(
+ actions.fetchReports,
+ null,
+ localState,
+ [{ type: types.REQUEST_REPORTS }],
+ [{ type: 'receiveReportsError' }],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('receiveReportsSuccess', () => {
+ it('commits RECEIVE_REPORTS_SUCCESS', done => {
+ const data = { issues: [] };
+
+ testAction(
+ actions.receiveReportsSuccess,
+ data,
+ localState,
+ [{ type: types.RECEIVE_REPORTS_SUCCESS, payload: data }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveReportsError', () => {
+ it('commits RECEIVE_REPORTS_ERROR', done => {
+ testAction(
+ actions.receiveReportsError,
+ null,
+ localState,
+ [{ type: types.RECEIVE_REPORTS_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/reports/codequality_report/store/getters_spec.js b/spec/frontend/reports/codequality_report/store/getters_spec.js
new file mode 100644
index 00000000000..a641e2fe74f
--- /dev/null
+++ b/spec/frontend/reports/codequality_report/store/getters_spec.js
@@ -0,0 +1,95 @@
+import * as getters from '~/reports/codequality_report/store/getters';
+import createStore from '~/reports/codequality_report/store';
+import { LOADING, ERROR, SUCCESS } from '~/reports/constants';
+
+describe('Codequality reports store getters', () => {
+ let localState;
+ let localStore;
+
+ beforeEach(() => {
+ localStore = createStore();
+ localState = localStore.state;
+ });
+
+ describe('hasCodequalityIssues', () => {
+ describe('when there are issues', () => {
+ it('returns true', () => {
+ localState.newIssues = [{ reason: 'repetitive code' }];
+ localState.resolvedIssues = [];
+
+ expect(getters.hasCodequalityIssues(localState)).toEqual(true);
+
+ localState.newIssues = [];
+ localState.resolvedIssues = [{ reason: 'repetitive code' }];
+
+ expect(getters.hasCodequalityIssues(localState)).toEqual(true);
+ });
+ });
+
+ describe('when there are no issues', () => {
+ it('returns false when there are no issues', () => {
+ expect(getters.hasCodequalityIssues(localState)).toEqual(false);
+ });
+ });
+ });
+
+ describe('codequalityStatus', () => {
+ describe('when loading', () => {
+ it('returns loading status', () => {
+ localState.isLoading = true;
+
+ expect(getters.codequalityStatus(localState)).toEqual(LOADING);
+ });
+ });
+
+ describe('on error', () => {
+ it('returns error status', () => {
+ localState.hasError = true;
+
+ expect(getters.codequalityStatus(localState)).toEqual(ERROR);
+ });
+ });
+
+ describe('when successfully loaded', () => {
+ it('returns error status', () => {
+ expect(getters.codequalityStatus(localState)).toEqual(SUCCESS);
+ });
+ });
+ });
+
+ describe('codequalityText', () => {
+ it.each`
+ resolvedIssues | newIssues | expectedText
+ ${0} | ${0} | ${'No changes to code quality'}
+ ${0} | ${1} | ${'Code quality degraded on 1 point'}
+ ${2} | ${0} | ${'Code quality improved on 2 points'}
+ ${1} | ${2} | ${'Code quality improved on 1 point and degraded on 2 points'}
+ `(
+ 'returns a summary containing $resolvedIssues resolved issues and $newIssues new issues',
+ ({ newIssues, resolvedIssues, expectedText }) => {
+ localState.newIssues = new Array(newIssues).fill({ reason: 'Repetitive code' });
+ localState.resolvedIssues = new Array(resolvedIssues).fill({ reason: 'Repetitive code' });
+
+ expect(getters.codequalityText(localState)).toEqual(expectedText);
+ },
+ );
+ });
+
+ describe('codequalityPopover', () => {
+ describe('when head report is available but base report is not', () => {
+ it('returns a popover with a documentation link', () => {
+ localState.headPath = 'head.json';
+ localState.basePath = undefined;
+ localState.helpPath = 'codequality_help.html';
+
+ expect(getters.codequalityPopover(localState).title).toEqual(
+ 'Base pipeline codequality artifact not found',
+ );
+ expect(getters.codequalityPopover(localState).content).toContain(
+ 'Learn more about codequality reports',
+ 'href="codequality_help.html"',
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/reports/codequality_report/store/mutations_spec.js b/spec/frontend/reports/codequality_report/store/mutations_spec.js
new file mode 100644
index 00000000000..658abf3088c
--- /dev/null
+++ b/spec/frontend/reports/codequality_report/store/mutations_spec.js
@@ -0,0 +1,80 @@
+import mutations from '~/reports/codequality_report/store/mutations';
+import createStore from '~/reports/codequality_report/store';
+
+describe('Codequality Reports mutations', () => {
+ let localState;
+ let localStore;
+
+ beforeEach(() => {
+ localStore = createStore();
+ localState = localStore.state;
+ });
+
+ describe('SET_PATHS', () => {
+ it('sets paths to given values', () => {
+ const basePath = 'base.json';
+ const headPath = 'head.json';
+ const baseBlobPath = 'base/blob/path/';
+ const headBlobPath = 'head/blob/path/';
+ const helpPath = 'help.html';
+
+ mutations.SET_PATHS(localState, {
+ basePath,
+ headPath,
+ baseBlobPath,
+ headBlobPath,
+ helpPath,
+ });
+
+ expect(localState.basePath).toEqual(basePath);
+ expect(localState.headPath).toEqual(headPath);
+ expect(localState.baseBlobPath).toEqual(baseBlobPath);
+ expect(localState.headBlobPath).toEqual(headBlobPath);
+ expect(localState.helpPath).toEqual(helpPath);
+ });
+ });
+
+ describe('REQUEST_REPORTS', () => {
+ it('sets isLoading to true', () => {
+ mutations.REQUEST_REPORTS(localState);
+
+ expect(localState.isLoading).toEqual(true);
+ });
+ });
+
+ describe('RECEIVE_REPORTS_SUCCESS', () => {
+ it('sets isLoading to false', () => {
+ mutations.RECEIVE_REPORTS_SUCCESS(localState, {});
+
+ expect(localState.isLoading).toEqual(false);
+ });
+
+ it('sets hasError to false', () => {
+ mutations.RECEIVE_REPORTS_SUCCESS(localState, {});
+
+ expect(localState.hasError).toEqual(false);
+ });
+
+ it('sets newIssues and resolvedIssues from response data', () => {
+ const data = { newIssues: [{ id: 1 }], resolvedIssues: [{ id: 2 }] };
+ mutations.RECEIVE_REPORTS_SUCCESS(localState, data);
+
+ expect(localState.newIssues).toEqual(data.newIssues);
+ expect(localState.resolvedIssues).toEqual(data.resolvedIssues);
+ });
+ });
+
+ describe('RECEIVE_REPORTS_ERROR', () => {
+ it('sets isLoading to false', () => {
+ mutations.RECEIVE_REPORTS_ERROR(localState);
+
+ expect(localState.isLoading).toEqual(false);
+ });
+
+ it('sets hasError to true', () => {
+ mutations.RECEIVE_REPORTS_ERROR(localState);
+
+ expect(localState.hasError).toEqual(true);
+ });
+ });
+});
diff --git a/spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js b/spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js
new file mode 100644
index 00000000000..5dd69d3c4d4
--- /dev/null
+++ b/spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js
@@ -0,0 +1,139 @@
+import {
+ parseCodeclimateMetrics,
+ doCodeClimateComparison,
+} from '~/reports/codequality_report/store/utils/codequality_comparison';
+import { baseIssues, mockParsedHeadIssues, mockParsedBaseIssues } from '../../mock_data';
+
+jest.mock('~/reports/codequality_report/workers/codequality_comparison_worker', () => {
+ let mockPostMessageCallback;
+ return jest.fn().mockImplementation(() => {
+ return {
+ addEventListener: (_, callback) => {
+ mockPostMessageCallback = callback;
+ },
+ postMessage: data => {
+ if (!data.headIssues) return mockPostMessageCallback({ data: {} });
+ if (!data.baseIssues) throw new Error();
+ const key = 'fingerprint';
+ return mockPostMessageCallback({
+ data: {
+ newIssues: data.headIssues.filter(
+ item => !data.baseIssues.find(el => el[key] === item[key]),
+ ),
+ resolvedIssues: data.baseIssues.filter(
+ item => !data.headIssues.find(el => el[key] === item[key]),
+ ),
+ },
+ });
+ },
+ };
+ });
+});
+
+describe('Codequality report store utils', () => {
+ let result;
+
+ describe('parseCodeclimateMetrics', () => {
+ it('should parse the received issues', () => {
+ [result] = parseCodeclimateMetrics(baseIssues, 'path');
+
+ expect(result.name).toEqual(baseIssues[0].check_name);
+ expect(result.path).toEqual(baseIssues[0].location.path);
+ expect(result.line).toEqual(baseIssues[0].location.lines.begin);
+ });
+
+ describe('when an issue has no location or path', () => {
+ const issue = { description: 'Insecure Dependency' };
+
+ beforeEach(() => {
+ [result] = parseCodeclimateMetrics([issue], 'path');
+ });
+
+ it('is parsed', () => {
+ expect(result.name).toEqual(issue.description);
+ });
+ });
+
+ describe('when an issue has a path but no line', () => {
+ const issue = { description: 'Insecure Dependency', location: { path: 'Gemfile.lock' } };
+
+ beforeEach(() => {
+ [result] = parseCodeclimateMetrics([issue], 'path');
+ });
+
+ it('is parsed', () => {
+ expect(result.name).toEqual(issue.description);
+ expect(result.path).toEqual(issue.location.path);
+ expect(result.urlPath).toEqual(`path/${issue.location.path}`);
+ });
+ });
+
+ describe('when an issue has a line nested in positions', () => {
+ const issue = {
+ description: 'Insecure Dependency',
+ location: {
+ path: 'Gemfile.lock',
+ positions: { begin: { line: 84 } },
+ },
+ };
+
+ beforeEach(() => {
+ [result] = parseCodeclimateMetrics([issue], 'path');
+ });
+
+ it('is parsed', () => {
+ expect(result.name).toEqual(issue.description);
+ expect(result.path).toEqual(issue.location.path);
+ expect(result.urlPath).toEqual(
+ `path/${issue.location.path}#L${issue.location.positions.begin.line}`,
+ );
+ });
+ });
+
+ describe('with an empty issue array', () => {
+ beforeEach(() => {
+ result = parseCodeclimateMetrics([], 'path');
+ });
+
+ it('returns an empty array', () => {
+ expect(result).toEqual([]);
+ });
+ });
+ });
+
+ describe('doCodeClimateComparison', () => {
+ describe('when the comparison worker finds changed issues', () => {
+ beforeEach(async () => {
+ result = await doCodeClimateComparison(mockParsedHeadIssues, mockParsedBaseIssues);
+ });
+
+ it('returns the new and resolved issues', () => {
+ expect(result.resolvedIssues[0]).toEqual(mockParsedBaseIssues[0]);
+ expect(result.newIssues[0]).toEqual(mockParsedHeadIssues[0]);
+ });
+ });
+
+ describe('when the comparison worker finds no changed issues', () => {
+ beforeEach(async () => {
+ result = await doCodeClimateComparison([], []);
+ });
+
+ it('returns the empty issue arrays', () => {
+ expect(result.newIssues).toEqual([]);
+ expect(result.resolvedIssues).toEqual([]);
+ });
+ });
+
+ describe('when the comparison worker is given malformed data', () => {
+ it('rejects the promise', () => {
+ return expect(doCodeClimateComparison(null)).rejects.toEqual({});
+ });
+ });
+
+ describe('when the comparison worker encounters an error', () => {
+ it('rejects the promise and throws an error', () => {
+ return expect(doCodeClimateComparison([], null)).rejects.toThrow();
+ });
+ });
+ });
+});
diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb
index 9386ec2ac71..33dca04fc57 100644
--- a/spec/helpers/environments_helper_spec.rb
+++ b/spec/helpers/environments_helper_spec.rb
@@ -114,4 +114,18 @@ RSpec.describe EnvironmentsHelper do
expect(subject).to eq(true)
end
end
+
+ describe '#environment_logs_data' do
+ it 'returns logs data' do
+ expected_data = {
+ "environment-name": environment.name,
+ "environments-path": project_environments_path(project, format: :json),
+ "environment-id": environment.id,
+ "cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack'),
+ "clusters-path": project_clusters_path(project, format: :json)
+ }
+
+ expect(helper.environment_logs_data(project, environment)).to eq(expected_data)
+ end
+ end
end
diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb
index f2d3a99bb19..0125f171ecb 100644
--- a/spec/lib/event_filter_spec.rb
+++ b/spec/lib/event_filter_spec.rb
@@ -80,16 +80,6 @@ RSpec.describe EventFilter do
it 'returns all events' do
expect(filtered_events).to eq(Event.all)
end
-
- context 'the :wiki_events filter is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'does not return wiki events' do
- expect(filtered_events).to eq(Event.not_wiki_page)
- end
- end
end
context 'with the "design" filter' do
@@ -116,16 +106,6 @@ RSpec.describe EventFilter do
it 'returns only wiki page events' do
expect(filtered_events).to contain_exactly(wiki_page_event, wiki_page_update_event)
end
-
- context 'the :wiki_events filter is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'does not return wiki events' do
- expect(filtered_events).not_to include(wiki_page_event, wiki_page_update_event)
- end
- end
end
context 'with an unknown filter' do
@@ -134,16 +114,6 @@ RSpec.describe EventFilter do
it 'returns all events' do
expect(filtered_events).to eq(Event.all)
end
-
- context 'the :wiki_events filter is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'does not return wiki events' do
- expect(filtered_events).to eq(Event.not_wiki_page)
- end
- end
end
context 'with a nil filter' do
@@ -152,16 +122,6 @@ RSpec.describe EventFilter do
it 'returns all events' do
expect(filtered_events).to eq(Event.all)
end
-
- context 'the :wiki_events filter is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'does not return wiki events' do
- expect(filtered_events).to eq(Event.not_wiki_page)
- end
- end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
index 22d38913787..0c18a7fb71e 100644
--- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
@@ -102,6 +102,17 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do
end
end
+ context 'when prepare action is used' do
+ let(:config) do
+ { name: 'production',
+ action: 'prepare' }
+ end
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
context 'when wrong action type is used' do
let(:config) do
{ name: 'production',
@@ -137,7 +148,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do
describe '#errors' do
it 'contains error about invalid action' do
expect(entry.errors)
- .to include 'environment action should be start or stop'
+ .to include 'environment action should be start, stop or prepare'
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb
index 0268d6e2650..1f38c7aec63 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/deployment_spec.rb
@@ -102,6 +102,19 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Deployment do
end
end
+ context 'when job has environment attribute with prepare action' do
+ let(:attributes) do
+ {
+ environment: 'production',
+ options: { environment: { name: 'production', action: 'prepare' } }
+ }
+ end
+
+ it 'returns nothing' do
+ is_expected.to be_nil
+ end
+ end
+
context 'when job does not have environment attribute' do
let(:attributes) { { name: 'test' } }
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index f7e1d891403..e5598bbd10f 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -242,4 +242,70 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do
it_behaves_like 'a reply to existing comment'
end
+
+ context 'when the service desk' do
+ let(:project) { create(:project, :public, service_desk_enabled: true) }
+ let(:support_bot) { User.support_bot }
+ let(:noteable) { create(:issue, project: project, author: support_bot, title: 'service desk issue') }
+ let(:note) { create(:note, project: project, noteable: noteable) }
+ let(:email_raw) { fixture_file('emails/valid_reply_with_quick_actions.eml') }
+
+ let!(:sent_notification) do
+ SentNotification.record_note(note, support_bot.id, mail_key)
+ end
+
+ context 'is enabled' do
+ before do
+ allow(Gitlab::ServiceDesk).to receive(:enabled?).with(project: project).and_return(true)
+ project.project_feature.update!(issues_access_level: issues_access_level)
+ end
+
+ context 'when issues are enabled for everyone' do
+ let(:issues_access_level) { ProjectFeature::ENABLED }
+
+ it 'creates a comment' do
+ expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+ end
+
+ context 'when quick actions are present' do
+ it 'encloses quick actions with code span markdown' do
+ receiver.execute
+ noteable.reload
+
+ note = Note.last
+ expect(note.note).to include("Jake out\n\n`/close`\n`/title test`")
+ expect(noteable.title).to eq('service desk issue')
+ expect(noteable).to be_opened
+ end
+ end
+ end
+
+ context 'when issues are protected members only' do
+ let(:issues_access_level) { ProjectFeature::PRIVATE }
+
+ it 'creates a comment' do
+ expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+ end
+ end
+
+ context 'when issues are disabled' do
+ let(:issues_access_level) { ProjectFeature::DISABLED }
+
+ it 'does not create a comment' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotAuthorizedError)
+ end
+ end
+ end
+
+ context 'is disabled' do
+ before do
+ allow(Gitlab::ServiceDesk).to receive(:enabled?).and_return(false)
+ allow(Gitlab::ServiceDesk).to receive(:enabled?).with(project: project).and_return(false)
+ end
+
+ it 'does not create a comment' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
new file mode 100644
index 00000000000..a38fe2c51ca
--- /dev/null
+++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb
@@ -0,0 +1,311 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
+ include_context :email_shared_context
+
+ before do
+ stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
+ stub_config_setting(host: 'localhost')
+ end
+
+ let(:email_raw) { email_fixture('emails/service_desk.eml') }
+ let_it_be(:namespace) { create(:namespace, name: "email") }
+ let(:expected_description) do
+ "Service desk stuff!\n\n```\na = b\n```\n\n`/label ~label1`\n`/assign @user1`\n`/close`\n![image](uploads/image.png)"
+ end
+
+ context 'service desk is enabled for the project' do
+ let_it_be(:project) { create(:project, :repository, :public, namespace: namespace, path: 'test', service_desk_enabled: true) }
+
+ before do
+ allow(Gitlab::ServiceDesk).to receive(:supported?).and_return(true)
+ end
+
+ shared_examples 'a new issue request' do
+ before do
+ setup_attachment
+ end
+
+ it 'creates a new issue' do
+ expect { receiver.execute }.to change { Issue.count }.by(1)
+
+ new_issue = Issue.last
+
+ expect(new_issue.author).to eql(User.support_bot)
+ expect(new_issue.confidential?).to be true
+ expect(new_issue.all_references.all).to be_empty
+ expect(new_issue.title).to eq("Service Desk (from jake@adventuretime.ooo): The message subject! @all")
+ expect(new_issue.description).to eq(expected_description.strip)
+ end
+
+ it 'sends thank you email' do
+ expect { receiver.execute }.to have_enqueued_job.on_queue('mailers')
+ end
+ end
+
+ context 'when everything is fine' do
+ it_behaves_like 'a new issue request'
+
+ context 'with legacy incoming email address' do
+ let(:email_raw) { fixture_file('emails/service_desk_legacy.eml') }
+
+ it_behaves_like 'a new issue request'
+ end
+
+ context 'when using issue templates' do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ setup_attachment
+ end
+
+ context 'and template is present' do
+ let_it_be(:settings) { create(:service_desk_setting, project: project) }
+
+ def set_template_file(file_name, content)
+ file_path = ".gitlab/issue_templates/#{file_name}.md"
+ project.repository.create_file(user, file_path, content, message: 'message', branch_name: 'master')
+ settings.update!(issue_template_key: file_name)
+ end
+
+ it 'appends template text to issue description' do
+ set_template_file('service_desk', 'text from template')
+
+ receiver.execute
+
+ issue_description = Issue.last.description
+ expect(issue_description).to include(expected_description)
+ expect(issue_description.lines.last).to eq('text from template')
+ end
+
+ context 'when quick actions are present' do
+ let(:label) { create(:label, project: project, title: 'label1') }
+ let(:milestone) { create(:milestone, project: project) }
+ let!(:user) { create(:user, username: 'user1') }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'applies quick action commands present on templates' do
+ file_content = %(Text from template \n/label ~#{label.title} \n/milestone %"#{milestone.name}"")
+ set_template_file('with_slash_commands', file_content)
+
+ receiver.execute
+
+ issue = Issue.last
+ expect(issue.description).to include('Text from template')
+ expect(issue.label_ids).to include(label.id)
+ expect(issue.milestone).to eq(milestone)
+ end
+
+ it 'redacts quick actions present on user email body' do
+ set_template_file('service_desk1', 'text from template')
+
+ receiver.execute
+
+ issue = Issue.last
+ expect(issue).to be_opened
+ expect(issue.description).to include('`/label ~label1`')
+ expect(issue.description).to include('`/assign @user1`')
+ expect(issue.description).to include('`/close`')
+ expect(issue.assignees).to be_empty
+ expect(issue.milestone).to be_nil
+ end
+ end
+ end
+
+ context 'and template cannot be found' do
+ before do
+ service = ServiceDeskSetting.new(project_id: project.id, issue_template_key: 'unknown')
+ service.save!(validate: false)
+ end
+
+ it 'does not append template text to issue description' do
+ receiver.execute
+
+ new_issue = Issue.last
+
+ expect(new_issue.description).to eq(expected_description.strip)
+ end
+
+ it 'creates support bot note on issue' do
+ receiver.execute
+
+ note = Note.last
+
+ expect(note.note).to include("WARNING: The template file unknown.md used for service desk issues is empty or could not be found.")
+ expect(note.author).to eq(User.support_bot)
+ end
+
+ it 'does not send warning note email' do
+ ActionMailer::Base.deliveries = []
+
+ perform_enqueued_jobs do
+ expect { receiver.execute }.to change { ActionMailer::Base.deliveries.size }.by(1)
+ end
+
+ # Only sends created issue email
+ expect(ActionMailer::Base.deliveries.last.text_part.body).to include("Thank you for your support request!")
+ end
+ end
+ end
+
+ context 'when using service desk key' do
+ let_it_be(:service_desk_settings) { create(:service_desk_setting, project: project, project_key: 'mykey') }
+ let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml') }
+ let(:receiver) { Gitlab::Email::ServiceDeskReceiver.new(email_raw) }
+
+ before do
+ stub_service_desk_email_setting(enabled: true, address: 'support+%{key}@example.com')
+ end
+
+ it_behaves_like 'a new issue request'
+
+ context 'when there is no project with the key' do
+ let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', key: 'some_key') }
+
+ it 'bounces the email' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
+ end
+ end
+
+ context 'when the project slug does not match' do
+ let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', slug: 'some-slug') }
+
+ it 'bounces the email' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
+ end
+ end
+
+ context 'when service_desk_custom_address feature is disabled' do
+ before do
+ stub_feature_flags(service_desk_custom_address: false)
+ end
+
+ it 'bounces the email' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
+ end
+ end
+ end
+ end
+
+ describe '#can_handle?' do
+ let(:mail) { Mail::Message.new(email_raw) }
+
+ it 'handles the new email key format' do
+ handler = described_class.new(mail, "h5bp-html5-boilerplate-#{project.project_id}-issue-")
+
+ expect(handler.instance_variable_get(:@project_id).to_i).to eq project.project_id
+ expect(handler.can_handle?).to be_truthy
+ end
+
+ it 'handles the legacy email key format' do
+ handler = described_class.new(mail, "h5bp/html5-boilerplate")
+
+ expect(handler.instance_variable_get(:@project_path)).to eq 'h5bp/html5-boilerplate'
+ expect(handler.can_handle?).to be_truthy
+ end
+
+ it "doesn't handle invalid email key" do
+ handler = described_class.new(mail, "h5bp-html5-boilerplate-invalid")
+
+ expect(handler.can_handle?).to be_falsey
+ end
+ end
+
+ context 'when there is no from address' do
+ before do
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:from_address).and_return(nil)
+ end
+ end
+
+ it "creates a new issue" do
+ expect { receiver.execute }.to change { Issue.count }.by(1)
+ end
+
+ it 'does not send thank you email' do
+ expect { receiver.execute }.not_to have_enqueued_job.on_queue('mailers')
+ end
+ end
+
+ context 'when there is a sender address and a from address' do
+ let(:email_raw) { email_fixture('emails/service_desk_sender_and_from.eml') }
+
+ it 'prefers the from address' do
+ setup_attachment
+
+ expect { receiver.execute }.to change { Issue.count }.by(1)
+
+ new_issue = Issue.last
+
+ expect(new_issue.service_desk_reply_to).to eq('finn@adventuretime.ooo')
+ end
+ end
+
+ context 'when service desk is not enabled for project' do
+ before do
+ allow(Gitlab::ServiceDesk).to receive(:enabled?).and_return(false)
+ end
+
+ it 'does not create an issue' do
+ expect { receiver.execute rescue nil }.not_to change { Issue.count }
+ end
+
+ it 'does not send thank you email' do
+ expect { receiver.execute rescue nil }.not_to have_enqueued_job.on_queue('mailers')
+ end
+ end
+
+ context 'when the email is forwarded through an alias' do
+ let(:email_raw) { email_fixture('emails/service_desk_forwarded.eml') }
+
+ it_behaves_like 'a new issue request'
+ end
+
+ context 'when the email is forwarded' do
+ let(:email_raw) { email_fixture('emails/service_desk_forwarded_new_issue.eml') }
+
+ it_behaves_like 'a new issue request' do
+ let(:expected_description) do
+ <<~EOF
+ Service desk stuff!
+
+ ---------- Forwarded message ---------
+ From: Jake the Dog <jake@adventuretime.ooo>
+ To: <jake@adventuretime.ooo>
+
+
+ forwarded content
+
+ ![image](uploads/image.png)
+ EOF
+ end
+ end
+ end
+ end
+
+ context 'service desk is disabled for the project' do
+ let(:project) { create(:project, :public, namespace: namespace, path: 'test', service_desk_enabled: false) }
+
+ it 'bounces the email' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::ProcessingError)
+ end
+
+ it "doesn't create an issue" do
+ expect { receiver.execute rescue nil }.not_to change { Issue.count }
+ end
+ end
+
+ def email_fixture(path)
+ fixture_file(path).gsub('project_id', project.project_id.to_s)
+ end
+
+ def service_desk_fixture(path, slug: nil, key: 'mykey')
+ slug ||= project.full_path_slug.to_s
+ fixture_file(path).gsub('project_slug', slug).gsub('project_key', key)
+ end
+end
diff --git a/spec/lib/gitlab/email/handler_spec.rb b/spec/lib/gitlab/email/handler_spec.rb
index 477965db999..2cd8c31e6b2 100644
--- a/spec/lib/gitlab/email/handler_spec.rb
+++ b/spec/lib/gitlab/email/handler_spec.rb
@@ -33,12 +33,40 @@ RSpec.describe Gitlab::Email::Handler do
it 'returns nil if provided email is nil' do
expect(described_class.for(nil, '')).to be_nil
end
+
+ context 'new issue email' do
+ def handler_for(fixture, mail_key)
+ described_class.for(fixture_file(fixture), mail_key)
+ end
+
+ before do
+ stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
+ stub_config_setting(host: 'localhost')
+ end
+
+ let!(:user) { create(:user, email: 'jake@adventuretime.ooo', incoming_email_token: 'auth_token') }
+
+ context 'a Service Desk email' do
+ it 'uses the Service Desk handler' do
+ expect(handler_for('emails/service_desk.eml', 'some/project')).to be_instance_of(Gitlab::Email::Handler::ServiceDeskHandler)
+ end
+ end
+
+ it 'return new issue handler' do
+ expect(handler_for('emails/valid_new_issue.eml', 'some/project+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
+ end
+ end
end
describe 'regexps are set properly' do
let(:addresses) do
- %W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX} sent_notification_key path-to-project-123-user_email_token-merge-request path-to-project-123-user_email_token-issue) +
- %W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY} sent_notification_key path/to/project+merge-request+user_email_token path/to/project+user_email_token)
+ %W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX} sent_notification_key path-to-project-123-user_email_token-merge-request) +
+ %W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY} sent_notification_key path-to-project-123-user_email_token-issue) +
+ %w(path/to/project+user_email_token path/to/project+merge-request+user_email_token some/project)
+ end
+
+ before do
+ allow(Gitlab::ServiceDesk).to receive(:supported?).and_return(true)
end
it 'picks each handler at least once' do
@@ -46,12 +74,12 @@ RSpec.describe Gitlab::Email::Handler do
described_class.for(email, address).class
end
- expect(matched_handlers.uniq).to match_array(ce_handlers)
+ expect(matched_handlers.uniq).to match_array(Gitlab::Email::Handler.handlers)
end
it 'can pick exactly one handler for each address' do
addresses.each do |address|
- matched_handlers = ce_handlers.select do |handler|
+ matched_handlers = Gitlab::Email::Handler.handlers.select do |handler|
handler.new(email, address).can_handle?
end
@@ -59,10 +87,4 @@ RSpec.describe Gitlab::Email::Handler do
end
end
end
-
- def ce_handlers
- @ce_handlers ||= Gitlab::Email::Handler.handlers.reject do |handler|
- handler.name.start_with?('Gitlab::Email::Handler::EE::')
- end
- end
end
diff --git a/spec/lib/gitlab/email/service_desk_receiver_spec.rb b/spec/lib/gitlab/email/service_desk_receiver_spec.rb
new file mode 100644
index 00000000000..6ba58ad5e93
--- /dev/null
+++ b/spec/lib/gitlab/email/service_desk_receiver_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Email::ServiceDeskReceiver do
+ let(:email) { fixture_file('emails/service_desk_custom_address.eml') }
+ let(:receiver) { described_class.new(email) }
+
+ context 'when the email contains a valid email address' do
+ before do
+ stub_service_desk_email_setting(enabled: true, address: 'support+%{key}@example.com')
+ end
+
+ it 'finds the service desk key' do
+ handler = double(execute: true, metrics_event: true, metrics_params: true)
+ expected_params = [
+ an_instance_of(Mail::Message), nil,
+ { service_desk_key: 'project_slug-project_key' }
+ ]
+
+ expect(Gitlab::Email::Handler::ServiceDeskHandler)
+ .to receive(:new).with(*expected_params).and_return(handler)
+
+ receiver.execute
+ end
+ end
+
+ context 'when the email does not contain a valid email address' do
+ before do
+ stub_service_desk_email_setting(enabled: true, address: 'other_support+%{key}@example.com')
+ end
+
+ it 'raises an error' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail)
+ end
+ end
+end
diff --git a/spec/lib/sentry/pagination_parser_spec.rb b/spec/lib/sentry/pagination_parser_spec.rb
index 1984c7fc676..c4ed24827bb 100644
--- a/spec/lib/sentry/pagination_parser_spec.rb
+++ b/spec/lib/sentry/pagination_parser_spec.rb
@@ -1,11 +1,8 @@
# frozen_string_literal: true
require 'fast_spec_helper'
-require 'support/helpers/fixture_helpers'
RSpec.describe Sentry::PaginationParser do
- include FixtureHelpers
-
describe '.parse' do
subject { described_class.parse(headers) }
diff --git a/spec/mailers/emails/service_desk_spec.rb b/spec/mailers/emails/service_desk_spec.rb
new file mode 100644
index 00000000000..842f82539cb
--- /dev/null
+++ b/spec/mailers/emails/service_desk_spec.rb
@@ -0,0 +1,188 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'email_spec'
+
+RSpec.describe Emails::ServiceDesk do
+ include EmailSpec::Helpers
+ include EmailSpec::Matchers
+ include EmailHelpers
+
+ include_context 'gitlab email notification'
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let(:template) { double(content: template_content) }
+
+ before do
+ stub_const('ServiceEmailClass', Class.new(ApplicationMailer))
+
+ ServiceEmailClass.class_eval do
+ include GitlabRoutingHelper
+ include EmailsHelper
+ include Emails::ServiceDesk
+
+ helper GitlabRoutingHelper
+ helper EmailsHelper
+
+ # this method is implemented in Notify class, we don't need to test it
+ def reply_key
+ 'test-key'
+ end
+
+ # this method is implemented in Notify class, we don't need to test it
+ def sender(author_id, params = {})
+ author_id
+ end
+
+ # this method is implemented in Notify class
+ #
+ # We do not need to test the Notify method, it is already tested in notify_spec
+ def mail_new_thread(issue, options)
+ # we need to rewrite this in order to look up templates in the correct directory
+ self.class.mailer_name = 'notify'
+
+ # this is needed for default layout
+ @unsubscribe_url = 'http://unsubscribe.example.com'
+
+ mail(options)
+ end
+ alias_method :mail_answer_thread, :mail_new_thread
+ end
+ end
+
+ shared_examples 'handle template content' do |template_key|
+ before do
+ expect(Gitlab::Template::ServiceDeskTemplate).to receive(:find)
+ .with(template_key, issue.project)
+ .and_return(template)
+ end
+
+ it 'builds the email correctly' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, include_project: false, reply: reply_in_subject)
+ is_expected.to have_body_text(expected_body)
+ expect(subject.content_type).to include('text/html')
+ end
+ end
+ end
+
+ shared_examples 'read template from repository' do |template_key|
+ let(:template_content) { 'custom text' }
+ let(:issue) { create(:issue, project: project)}
+
+ context 'when a template is in the repository' do
+ let(:project) { create(:project, :custom_repo, files: { ".gitlab/service_desk_templates/#{template_key}.md" => template_content }) }
+
+ it 'uses the text template from the template' do
+ is_expected.to have_body_text(template_content)
+ end
+ end
+
+ context 'when the service_desk_templates directory does not contain correct template' do
+ let(:project) { create(:project, :custom_repo, files: { ".gitlab/service_desk_templates/another_file.md" => template_content }) }
+
+ it 'uses the default template' do
+ is_expected.to have_body_text(default_text)
+ end
+ end
+
+ context 'when the service_desk_templates directory does not exist' do
+ let(:project) { create(:project, :custom_repo, files: { "other_directory/another_file.md" => template_content }) }
+
+ it 'uses the default template' do
+ is_expected.to have_body_text(default_text)
+ end
+ end
+
+ context 'when the project does not have a repo' do
+ let(:project) { create(:project) }
+
+ it 'uses the default template' do
+ is_expected.to have_body_text(default_text)
+ end
+ end
+ end
+
+ describe '.service_desk_thank_you_email' do
+ let_it_be(:reply_in_subject) { true }
+ let_it_be(:default_text) do
+ "Thank you for your support request! We are tracking your request as ticket #{issue.to_reference}, and will respond as soon as we can."
+ end
+
+ subject { ServiceEmailClass.service_desk_thank_you_email(issue.id) }
+
+ it_behaves_like 'read template from repository', 'thank_you'
+
+ context 'handling template markdown' do
+ context 'with a simple text' do
+ let(:template_content) { 'thank you, **your new issue** has been created.' }
+ let(:expected_body) { 'thank you, <strong>your new issue</strong> has been created.' }
+
+ it_behaves_like 'handle template content', 'thank_you'
+ end
+
+ context 'with an issue id and issue path placeholders' do
+ let(:template_content) { 'thank you, **your new issue:** %{ISSUE_ID}, path: %{ISSUE_PATH}' }
+ let(:expected_body) { "thank you, <strong>your new issue:</strong> ##{issue.iid}, path: #{project.full_path}##{issue.iid}" }
+
+ it_behaves_like 'handle template content', 'thank_you'
+ end
+
+ context 'with an issue id placeholder with whitespace' do
+ let(:template_content) { 'thank you, **your new issue:** %{ ISSUE_ID}' }
+ let(:expected_body) { "thank you, <strong>your new issue:</strong> ##{issue.iid}" }
+
+ it_behaves_like 'handle template content', 'thank_you'
+ end
+
+ context 'with unexpected placeholder' do
+ let(:template_content) { 'thank you, **your new issue:** %{this is issue}' }
+ let(:expected_body) { "thank you, <strong>your new issue:</strong> %{this is issue}" }
+
+ it_behaves_like 'handle template content', 'thank_you'
+ end
+ end
+ end
+
+ describe '.service_desk_new_note_email' do
+ let_it_be(:reply_in_subject) { false }
+ let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project) }
+ let_it_be(:default_text) { note.note }
+
+ subject { ServiceEmailClass.service_desk_new_note_email(issue.id, note.id) }
+
+ it_behaves_like 'read template from repository', 'new_note'
+
+ context 'handling template markdown' do
+ context 'with a simple text' do
+ let(:template_content) { 'thank you, **new note on issue** has been created.' }
+ let(:expected_body) { 'thank you, <strong>new note on issue</strong> has been created.' }
+
+ it_behaves_like 'handle template content', 'new_note'
+ end
+
+ context 'with an issue id, issue path and note placeholders' do
+ let(:template_content) { 'thank you, **new note on issue:** %{ISSUE_ID}, path: %{ISSUE_PATH}: %{NOTE_TEXT}' }
+ let(:expected_body) { "thank you, <strong>new note on issue:</strong> ##{issue.iid}, path: #{project.full_path}##{issue.iid}: #{note.note}" }
+
+ it_behaves_like 'handle template content', 'new_note'
+ end
+
+ context 'with an issue id placeholder with whitespace' do
+ let(:template_content) { 'thank you, **new note on issue:** %{ ISSUE_ID}: %{ NOTE_TEXT }' }
+ let(:expected_body) { "thank you, <strong>new note on issue:</strong> ##{issue.iid}: #{note.note}" }
+
+ it_behaves_like 'handle template content', 'new_note'
+ end
+
+ context 'with unexpected placeholder' do
+ let(:template_content) { 'thank you, **new note on issue:** %{this is issue}' }
+ let(:expected_body) { "thank you, <strong>new note on issue:</strong> %{this is issue}" }
+
+ it_behaves_like 'handle template content', 'new_note'
+ end
+ end
+ end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 7c1eb66b543..5286028d9c6 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -1253,6 +1253,78 @@ RSpec.describe Notify do
it_behaves_like 'appearance header and footer not enabled'
end
end
+
+ context 'for service desk issues' do
+ before do
+ issue.update!(service_desk_reply_to: 'service.desk@example.com')
+ end
+
+ def expect_sender(username)
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(username)
+ expect(sender.address).to eq(gitlab_sender)
+ end
+
+ describe 'thank you email' do
+ subject { described_class.service_desk_thank_you_email(issue.id) }
+
+ it_behaves_like 'an unsubscribeable thread'
+
+ it 'has the correct recipient' do
+ is_expected.to deliver_to('service.desk@example.com')
+ end
+
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, include_project: false, reply: true)
+ is_expected.to have_body_text("Thank you for your support request! We are tracking your request as ticket #{issue.to_reference}, and will respond as soon as we can.")
+ end
+ end
+
+ it 'uses service bot name by default' do
+ expect_sender(User.support_bot.name)
+ end
+
+ context 'when custom outgoing name is set' do
+ let_it_be(:settings) { create(:service_desk_setting, project: project, outgoing_name: 'some custom name') }
+
+ it 'uses custom name in "from" header' do
+ expect_sender('some custom name')
+ end
+ end
+
+ context 'when custom outgoing name is empty' do
+ let_it_be(:settings) { create(:service_desk_setting, project: project, outgoing_name: '') }
+
+ it 'uses service bot name' do
+ expect_sender(User.support_bot.name)
+ end
+ end
+ end
+
+ describe 'new note email' do
+ let_it_be(:first_note) { create(:discussion_note_on_issue, note: 'Hello world') }
+
+ subject { described_class.service_desk_new_note_email(issue.id, first_note.id) }
+
+ it_behaves_like 'an unsubscribeable thread'
+
+ it 'has the correct recipient' do
+ is_expected.to deliver_to('service.desk@example.com')
+ end
+
+ it 'uses author\'s name in "from" header' do
+ expect_sender(first_note.author.name)
+ end
+
+ it 'has the correct subject and body' do
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, include_project: false, reply: true)
+ is_expected.to have_body_text(first_note.note)
+ end
+ end
+ end
+ end
end
context 'for a group' do
diff --git a/spec/models/event_collection_spec.rb b/spec/models/event_collection_spec.rb
index cf95a8f8d32..a1773378073 100644
--- a/spec/models/event_collection_spec.rb
+++ b/spec/models/event_collection_spec.rb
@@ -44,22 +44,10 @@ RSpec.describe EventCollection do
expect(events).to match_array(most_recent_20_events)
end
- context 'the wiki_events feature flag is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'omits the wiki page events when using to_a' do
- events = described_class.new(projects).to_a
-
- expect(events).not_to include(wiki_page_event)
- end
-
- it 'omits the wiki page events when using all_project_events' do
- events = described_class.new(projects).all_project_events
+ it 'includes the wiki page events when using to_a' do
+ events = described_class.new(projects).to_a
- expect(events).not_to include(wiki_page_event)
- end
+ expect(events).to include(wiki_page_event)
end
context 'the design_activity_events feature flag is disabled' do
@@ -87,22 +75,10 @@ RSpec.describe EventCollection do
expect(collection.all_project_events).to include(design_event)
end
- context 'the wiki_events feature flag is enabled' do
- before do
- stub_feature_flags(wiki_events: true)
- end
-
- it 'includes the wiki page events when using to_a' do
- events = described_class.new(projects).to_a
-
- expect(events).to include(wiki_page_event)
- end
+ it 'includes the wiki page events when using all_project_events' do
+ events = described_class.new(projects).all_project_events
- it 'includes the wiki page events when using all_project_events' do
- events = described_class.new(projects).all_project_events
-
- expect(events).to include(wiki_page_event)
- end
+ expect(events).to include(wiki_page_event)
end
it 'applies a limit to the number of events' do
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 51929c02c2f..a05ae188ef6 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -661,15 +661,6 @@ RSpec.describe Event do
end
end
- describe '.not_wiki_page' do
- it 'does not contain the wiki page events' do
- non_wiki_events = events.reject(&:wiki_page?)
-
- expect(events).not_to match_array(non_wiki_events)
- expect(described_class.not_wiki_page).to match_array(non_wiki_events)
- end
- end
-
describe '.for_wiki_meta' do
it 'finds events for a given wiki page metadata object' do
event = events.select(&:wiki_page?).first
diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index d998660a3f9..541e96f5f07 100644
--- a/spec/presenters/clusters/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -289,4 +289,74 @@ RSpec.describe Clusters::ClusterPresenter do
it_behaves_like 'cluster health data'
end
end
+
+ describe '#gitlab_managed_apps_logs_path' do
+ context 'user can read logs' do
+ let(:project) { cluster.project }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'returns path to logs' do
+ expect(presenter.gitlab_managed_apps_logs_path).to eq project_logs_path(project, cluster_id: cluster.id)
+ end
+ end
+
+ context 'group cluster' do
+ let(:cluster) { create(:cluster, cluster_type: :group_type, groups: [group]) }
+ let(:group) { create(:group, name: 'Foo') }
+
+ context 'user can read logs' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ context 'there are projects within group' do
+ let!(:project) { create(:project, namespace: group) }
+
+ it 'returns path to logs' do
+ expect(presenter.gitlab_managed_apps_logs_path).to eq project_logs_path(project, cluster_id: cluster.id)
+ end
+ end
+
+ context 'there are no projects within group' do
+ it 'returns nil' do
+ expect(presenter.gitlab_managed_apps_logs_path).to be_nil
+ end
+ end
+ end
+ end
+
+ context 'instance cluster' do
+ let(:cluster) { create(:cluster, cluster_type: :instance_type) }
+ let!(:project) { create(:project) }
+ let(:user) { create(:admin) }
+
+ before do
+ project.add_maintainer(user)
+ stub_feature_flags(user_mode_in_session: false)
+ end
+
+ context 'user can read logs' do
+ it 'returns path to logs' do
+ expect(presenter.gitlab_managed_apps_logs_path).to eq project_logs_path(project, cluster_id: cluster.id)
+ end
+ end
+ end
+
+ context 'user can NOT read logs' do
+ let(:cluster) { create(:cluster, cluster_type: :instance_type) }
+ let!(:project) { create(:project) }
+
+ before do
+ project.add_developer(user)
+ stub_feature_flags(user_mode_in_session: false)
+ end
+
+ it 'returns nil' do
+ expect(presenter.gitlab_managed_apps_logs_path).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 022cab63440..80be5425b23 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1928,6 +1928,13 @@ RSpec.describe API::Projects do
end
end
end
+
+ it 'exposes service desk attributes' do
+ get api("/projects/#{project.id}", user)
+
+ expect(json_response).to have_key 'service_desk_enabled'
+ expect(json_response).to have_key 'service_desk_address'
+ end
end
describe 'GET /projects/:id/users' do
diff --git a/spec/serializers/cluster_entity_spec.rb b/spec/serializers/cluster_entity_spec.rb
index 6c692dbdbfc..223d37b6acd 100644
--- a/spec/serializers/cluster_entity_spec.rb
+++ b/spec/serializers/cluster_entity_spec.rb
@@ -3,8 +3,13 @@
require 'spec_helper'
RSpec.describe ClusterEntity do
+ include Gitlab::Routing.url_helpers
+
describe '#as_json' do
- subject { described_class.new(cluster).as_json }
+ let(:user) { nil }
+ let(:request) { EntityRequest.new({ current_user: user }) }
+
+ subject { described_class.new(cluster, request: request).as_json }
context 'when provider type is gcp' do
let(:cluster) { create(:cluster, :instance, provider_type: :gcp, provider_gcp: provider) }
@@ -40,7 +45,7 @@ RSpec.describe ClusterEntity do
context 'when no application has been installed' do
let(:cluster) { create(:cluster, :instance) }
- subject { described_class.new(cluster).as_json[:applications]}
+ subject { described_class.new(cluster, request: request).as_json[:applications]}
it 'contains helm as not_installable' do
expect(subject).not_to be_empty
@@ -50,5 +55,28 @@ RSpec.describe ClusterEntity do
expect(helm[:status]).to eq(:not_installable)
end
end
+
+ context 'gitlab_managed_apps_logs_path' do
+ let(:cluster) { create(:cluster, :project) }
+ let(:user) { create(:user) }
+
+ subject { described_class.new(cluster, request: request).as_json }
+
+ before do
+ allow_next_instance_of(Clusters::ClusterPresenter) do |presenter|
+ allow(presenter).to receive(:show_path).and_return(nil)
+ end
+ end
+
+ it 'return projects log explorer path' do
+ log_explorer_path = project_logs_path(cluster.project, cluster_id: cluster.id)
+
+ expect_next_instance_of(Clusters::ClusterPresenter, cluster, current_user: user) do |presenter|
+ expect(presenter).to receive(:gitlab_managed_apps_logs_path).and_return(log_explorer_path)
+ end
+
+ expect(subject[:gitlab_managed_apps_logs_path]).to eq(log_explorer_path)
+ end
+ end
end
end
diff --git a/spec/serializers/cluster_serializer_spec.rb b/spec/serializers/cluster_serializer_spec.rb
index 8034edbe599..ea1cf6ff59a 100644
--- a/spec/serializers/cluster_serializer_spec.rb
+++ b/spec/serializers/cluster_serializer_spec.rb
@@ -6,13 +6,14 @@ RSpec.describe ClusterSerializer do
let(:cluster) { create(:cluster, :project, provider_type: :user) }
describe '#represent_list' do
- subject { described_class.new.represent_list(cluster).keys }
+ subject { described_class.new(current_user: nil).represent_list(cluster).keys }
it 'serializes attrs correctly' do
is_expected.to contain_exactly(
:cluster_type,
:enabled,
:environment_scope,
+ :gitlab_managed_apps_logs_path,
:name,
:nodes,
:path,
@@ -22,7 +23,7 @@ RSpec.describe ClusterSerializer do
end
describe '#represent_status' do
- subject { described_class.new.represent_status(cluster).keys }
+ subject { described_class.new(current_user: nil).represent_status(cluster).keys }
context 'when provider type is gcp and cluster is errored' do
let(:cluster) do
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 95ee6fe556c..e9ed0493c21 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -200,16 +200,6 @@ RSpec.describe EventCreateService do
expect(duplicate).to eq(event)
end
-
- context 'the feature is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'does not create the event' do
- expect { event }.not_to change(Event, :count)
- end
- end
end
end
diff --git a/spec/services/git/wiki_push_service_spec.rb b/spec/services/git/wiki_push_service_spec.rb
index 623949c0806..f338b7a5709 100644
--- a/spec/services/git/wiki_push_service_spec.rb
+++ b/spec/services/git/wiki_push_service_spec.rb
@@ -247,14 +247,6 @@ RSpec.describe Git::WikiPushService, services: true do
end
end
- context 'the wiki_events feature is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it_behaves_like 'a no-op push'
- end
-
context 'the wiki_events_on_git_push feature is disabled' do
before do
stub_feature_flags(wiki_events_on_git_push: false)
diff --git a/spec/services/wiki_pages/event_create_service_spec.rb b/spec/services/wiki_pages/event_create_service_spec.rb
index ddaf36d108a..abf3bcb4c4d 100644
--- a/spec/services/wiki_pages/event_create_service_spec.rb
+++ b/spec/services/wiki_pages/event_create_service_spec.rb
@@ -14,21 +14,6 @@ RSpec.describe WikiPages::EventCreateService do
let(:action) { :created }
let(:response) { subject.execute(slug, page, action) }
- context 'feature flag is not enabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'does not error' do
- expect(response).to be_success
- .and have_attributes(message: /No event created/)
- end
-
- it 'does not create an event' do
- expect { response }.not_to change(Event, :count)
- end
- end
-
context 'the user is nil' do
subject { described_class.new(nil) }
diff --git a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
index efcb83a34af..ebe78c299a5 100644
--- a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
@@ -63,16 +63,6 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type|
include_examples 'correct event created'
end
- context 'the feature is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'does not record the activity' do
- expect { service.execute }.not_to change(Event, :count)
- end
- end
-
context 'when the options are bad' do
let(:page_title) { '' }
diff --git a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
index 1231c012c31..db1b50fdf3c 100644
--- a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
@@ -37,14 +37,4 @@ RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type|
expect { service.execute(nil) }.not_to change { counter.read(:delete) }
end
-
- context 'the feature is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'does not record the activity' do
- expect { service.execute(page) }.not_to change(Event, :count)
- end
- end
end
diff --git a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
index 77354fec069..0191a6dfbc9 100644
--- a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
@@ -67,16 +67,6 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type|
include_examples 'adds activity event'
end
- context 'the feature is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'does not record the activity' do
- expect { service.execute(page) }.not_to change(Event, :count)
- end
- end
-
context 'when the options are bad' do
let(:page_title) { '' }
diff --git a/spec/workers/service_desk_email_receiver_worker_spec.rb b/spec/workers/service_desk_email_receiver_worker_spec.rb
new file mode 100644
index 00000000000..d3bfa51348e
--- /dev/null
+++ b/spec/workers/service_desk_email_receiver_worker_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe ServiceDeskEmailReceiverWorker, :mailer do
+ describe '#perform' do
+ let(:worker) { described_class.new }
+ let(:email) { fixture_file('emails/service_desk_custom_address.eml') }
+
+ context 'when service_desk_email config is enabled' do
+ before do
+ stub_service_desk_email_setting(enabled: true, address: 'foo')
+ end
+
+ it 'does not ignore the email' do
+ expect(Gitlab::Email::ServiceDeskReceiver).to receive(:new)
+
+ worker.perform(email)
+ end
+
+ context 'when service desk receiver raises an exception' do
+ before do
+ allow_next_instance_of(Gitlab::Email::ServiceDeskReceiver) do |receiver|
+ allow(receiver).to receive(:find_handler).and_return(nil)
+ end
+ end
+
+ it 'sends a rejection email' do
+ perform_enqueued_jobs do
+ worker.perform(email)
+ end
+
+ reply = ActionMailer::Base.deliveries.last
+ expect(reply).not_to be_nil
+ expect(reply.to).to eq(['jake@adventuretime.ooo'])
+ expect(reply.subject).to include('Rejected')
+ end
+ end
+ end
+
+ context 'when service_desk_email config is disabled' do
+ before do
+ stub_service_desk_email_setting(enabled: false, address: 'foo')
+ end
+
+ it 'ignores the email' do
+ expect(Gitlab::Email::ServiceDeskReceiver).not_to receive(:new)
+
+ worker.perform(email)
+ end
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 73479e3a36d..e1f3aba4bab 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -843,15 +843,15 @@
eslint-plugin-vue "^6.2.1"
vue-eslint-parser "^7.0.0"
-"@gitlab/svgs@1.151.0":
- version "1.151.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.151.0.tgz#099905295d33eb31033f4a48eb3652da2f686239"
- integrity sha512-2PTSM8CFhUjeTFKfcq6E/YwPpOVdSVWupf3NhKO/bz/cisSBS5P7aWxaXKIaxy28ySyBKEfKaAT6b4rXTwvVgg==
-
-"@gitlab/ui@17.22.1":
- version "17.22.1"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.22.1.tgz#368578d04bb49011690911599c22a7d306f5fe99"
- integrity sha512-elcu2gdvt1Afz3GMrIBQR+eujlA6JetLn44T1UzPHUhlaodT/w+TIj0+uPIbPiD7Oz6uR/sYwBqlZXQdBcVv3Q==
+"@gitlab/svgs@1.152.0":
+ version "1.152.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.152.0.tgz#663c9a5f073f59b66f4241ef2d3fea2205846905"
+ integrity sha512-daZHOBVAwjsU6n60IycanoO/JymfQ36vrr46OUdWjHdp0ATYrgh+01LcxiSNLdlyndIRqHWGtwmuilokM9q6Vg==
+
+"@gitlab/ui@17.26.0":
+ version "17.26.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.26.0.tgz#d8efad47c3f4dc32e0586f3f5e4e2e3e0c2babf6"
+ integrity sha512-0QgzMK8MFGaqBB8yYntjYjUnzKFQ9a8d4mjufIyeKq6WomuMYHTFJgUj0+cEQ6uuTRtNk3MMuy3ZHBJg1wGzTw==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"