diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-14 15:09:05 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-14 15:09:05 +0000 |
commit | 66bd1f0fdcaf84fa3412c70d7962b49eb8a48fde (patch) | |
tree | 23f451b4e60a6e28bcc15043d7756bb27dcc2970 | |
parent | 49089d4fb1f5c17328ac61c955d95a68c6d4d545 (diff) | |
download | gitlab-ce-66bd1f0fdcaf84fa3412c70d7962b49eb8a48fde.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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 + — + %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" |