Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									49089d4fb1
								
							
						
					
					
						commit
						66bd1f0fdc
					
				|  | @ -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); | ||||||
|  | }; | ||||||
|  | @ -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 {}; | ||||||
|  | }; | ||||||
|  | @ -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), | ||||||
|  |   }); | ||||||
|  | @ -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'; | ||||||
|  | @ -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; | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | @ -0,0 +1,15 @@ | ||||||
|  | export default () => ({ | ||||||
|  |   basePath: null, | ||||||
|  |   headPath: null, | ||||||
|  | 
 | ||||||
|  |   baseBlobPath: null, | ||||||
|  |   headBlobPath: null, | ||||||
|  | 
 | ||||||
|  |   isLoading: false, | ||||||
|  |   hasError: false, | ||||||
|  | 
 | ||||||
|  |   newIssues: [], | ||||||
|  |   resolvedIssues: [], | ||||||
|  | 
 | ||||||
|  |   helpPath: null, | ||||||
|  | }); | ||||||
|  | @ -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, | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | @ -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(); | ||||||
|  | }); | ||||||
|  | @ -54,17 +54,10 @@ class EventsFinder | ||||||
|     if current_user && scope == 'all' |     if current_user && scope == 'all' | ||||||
|       EventCollection.new(current_user.authorized_projects).all_project_events |       EventCollection.new(current_user.authorized_projects).all_project_events | ||||||
|     else |     else | ||||||
|       # EventCollection is responsible for applying the feature flag |       source.events | ||||||
|       apply_feature_flags(source.events) |  | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def apply_feature_flags(events) |  | ||||||
|     return events if ::Feature.enabled?(:wiki_events) |  | ||||||
| 
 |  | ||||||
|     events.not_wiki_page |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   # rubocop: disable CodeReuse/ActiveRecord |   # rubocop: disable CodeReuse/ActiveRecord | ||||||
|   def by_current_user_access(events) |   def by_current_user_access(events) | ||||||
|     events.merge(Project.public_or_visible_to_user(current_user)) |     events.merge(Project.public_or_visible_to_user(current_user)) | ||||||
|  |  | ||||||
|  | @ -36,7 +36,8 @@ module EnvironmentsHelper | ||||||
|       "environment-name": environment.name, |       "environment-name": environment.name, | ||||||
|       "environments-path": project_environments_path(project, format: :json), |       "environments-path": project_environments_path(project, format: :json), | ||||||
|       "environment-id": environment.id, |       "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 |   end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | @ -19,6 +19,7 @@ class Notify < ApplicationMailer | ||||||
|   include Emails::Releases |   include Emails::Releases | ||||||
|   include Emails::Groups |   include Emails::Groups | ||||||
|   include Emails::Reviews |   include Emails::Reviews | ||||||
|  |   include Emails::ServiceDesk | ||||||
| 
 | 
 | ||||||
|   helper TimeboxesHelper |   helper TimeboxesHelper | ||||||
|   helper MergeRequestsHelper |   helper MergeRequestsHelper | ||||||
|  |  | ||||||
|  | @ -165,6 +165,18 @@ class NotifyPreview < ActionMailer::Preview | ||||||
|     Notify.unknown_sign_in_email(user, '127.0.0.1', Time.current).message |     Notify.unknown_sign_in_email(user, '127.0.0.1', Time.current).message | ||||||
|   end |   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 |   private | ||||||
| 
 | 
 | ||||||
|   def project |   def project | ||||||
|  |  | ||||||
|  | @ -84,7 +84,6 @@ class Event < ApplicationRecord | ||||||
|   scope :for_design, -> { where(target_type: 'DesignManagement::Design') } |   scope :for_design, -> { where(target_type: 'DesignManagement::Design') } | ||||||
| 
 | 
 | ||||||
|   # Needed to implement feature flag: can be removed when feature flag is removed |   # 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 :not_design, -> { where('target_type IS NULL or target_type <> ?', 'DesignManagement::Design') } | ||||||
| 
 | 
 | ||||||
|   scope :with_associations, -> do |   scope :with_associations, -> do | ||||||
|  |  | ||||||
|  | @ -45,7 +45,6 @@ class EventCollection | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def apply_feature_flags(events) |   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 = events.not_design unless ::Feature.enabled?(:design_activity_events) | ||||||
| 
 | 
 | ||||||
|     events |     events | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| module Clusters | module Clusters | ||||||
|   class ClusterPresenter < Gitlab::View::Presenter::Delegated |   class ClusterPresenter < Gitlab::View::Presenter::Delegated | ||||||
|  |     include ::Gitlab::Utils::StrongMemoize | ||||||
|     include ActionView::Helpers::SanitizeHelper |     include ActionView::Helpers::SanitizeHelper | ||||||
|     include ActionView::Helpers::UrlHelper |     include ActionView::Helpers::UrlHelper | ||||||
|     include IconsHelper |     include IconsHelper | ||||||
|  | @ -60,6 +61,12 @@ module Clusters | ||||||
|       end |       end | ||||||
|     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? |     def read_only_kubernetes_platform_fields? | ||||||
|       !cluster.provided_by_user? |       !cluster.provided_by_user? | ||||||
|     end |     end | ||||||
|  | @ -85,6 +92,16 @@ module Clusters | ||||||
|       ActionController::Base.helpers.image_path(path) |       ActionController::Base.helpers.image_path(path) | ||||||
|     end |     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 |     def clusterable | ||||||
|       if cluster.group_type? |       if cluster.group_type? | ||||||
|         cluster.group |         cluster.group | ||||||
|  |  | ||||||
|  | @ -16,4 +16,8 @@ class ClusterEntity < Grape::Entity | ||||||
|   expose :path do |cluster| |   expose :path do |cluster| | ||||||
|     Clusters::ClusterPresenter.new(cluster).show_path # rubocop: disable CodeReuse/Presenter |     Clusters::ClusterPresenter.new(cluster).show_path # rubocop: disable CodeReuse/Presenter | ||||||
|   end |   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 | end | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ class ClusterSerializer < BaseSerializer | ||||||
|         :cluster_type, |         :cluster_type, | ||||||
|         :enabled, |         :enabled, | ||||||
|         :environment_scope, |         :environment_scope, | ||||||
|  |         :gitlab_managed_apps_logs_path, | ||||||
|         :name, |         :name, | ||||||
|         :nodes, |         :nodes, | ||||||
|         :path, |         :path, | ||||||
|  |  | ||||||
|  | @ -120,8 +120,6 @@ class EventCreateService | ||||||
|   # |   # | ||||||
|   # @return a tuple of event and either :found or :created |   # @return a tuple of event and either :found or :created | ||||||
|   def wiki_event(wiki_page_meta, author, action) |   def wiki_event(wiki_page_meta, author, action) | ||||||
|     return unless Feature.enabled?(:wiki_events) |  | ||||||
| 
 |  | ||||||
|     raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action) |     raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action) | ||||||
| 
 | 
 | ||||||
|     if duplicate = existing_wiki_event(wiki_page_meta, action) |     if duplicate = existing_wiki_event(wiki_page_meta, action) | ||||||
|  |  | ||||||
|  | @ -23,7 +23,7 @@ module Git | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def can_process_wiki_events? |     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 |     end | ||||||
| 
 | 
 | ||||||
|     def push_changes |     def push_changes | ||||||
|  |  | ||||||
|  | @ -44,8 +44,6 @@ module WikiPages | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def create_wiki_event(page) |     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) |       response = WikiPages::EventCreateService.new(current_user).execute(slug_for_page(page), page, event_action) | ||||||
| 
 | 
 | ||||||
|       log_error(response.message) if response.error? |       log_error(response.message) if response.error? | ||||||
|  |  | ||||||
|  | @ -10,8 +10,6 @@ module WikiPages | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def execute(slug, page, action) |     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 |       event = Event.transaction do | ||||||
|         wiki_page_meta = WikiPage::Meta.find_or_create(slug, page) |         wiki_page_meta = WikiPage::Meta.find_or_create(slug, page) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | @ -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) | ||||||
|  | @ -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 %> | ||||||
|  | @ -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. | ||||||
|  | @ -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 %> | ||||||
|  | @ -15,7 +15,7 @@ | ||||||
|       = render_if_exists 'events/epics_filter' |       = render_if_exists 'events/epics_filter' | ||||||
|     - if comments_visible? |     - if comments_visible? | ||||||
|       = event_filter_link EventFilter::COMMENTS, _('Comments'), s_('EventFilterBy|Filter by comments') |       = 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') |       = event_filter_link EventFilter::WIKI, _('Wiki'), s_('EventFilterBy|Filter by wiki') | ||||||
|     - if event_filter_visible(:designs) |     - if event_filter_visible(:designs) | ||||||
|       = event_filter_link EventFilter::DESIGNS, _('Designs'), s_('EventFilterBy|Filter by designs') |       = event_filter_link EventFilter::DESIGNS, _('Designs'), s_('EventFilterBy|Filter by designs') | ||||||
|  |  | ||||||
|  | @ -1660,6 +1660,14 @@ | ||||||
|   :weight: 2 |   :weight: 2 | ||||||
|   :idempotent: |   :idempotent: | ||||||
|   :tags: [] |   :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 | - :name: system_hook_push | ||||||
|   :feature_category: :source_code_management |   :feature_category: :source_code_management | ||||||
|   :has_external_dependencies: |   :has_external_dependencies: | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | --- | ||||||
|  | title: Introduce prepare environment action to annotate non-deployment jobs | ||||||
|  | merge_request: 35642 | ||||||
|  | author: | ||||||
|  | type: added | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | --- | ||||||
|  | title: Enable display of wiki events in activity streams | ||||||
|  | merge_request: 32475 | ||||||
|  | author: | ||||||
|  | type: changed | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | --- | ||||||
|  | title: Add docs for Alert trigger test alerts | ||||||
|  | merge_request: 36647 | ||||||
|  | author: | ||||||
|  | type: added | ||||||
|  | @ -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%. | 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:** | CAUTION: **Caution:** | ||||||
| If this strategy is selected, then the Unleash client **must** be given a user | 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. | 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, | 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. | `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:** | CAUTION: **Caution:** | ||||||
| The Unleash client **must** be given a user ID for the feature to be enabled for | 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. | target users. See the [Ruby example](#ruby-application-example) below. | ||||||
|  |  | ||||||
|  | @ -10,14 +10,14 @@ You can find the full documentation for the AsciiDoc syntax at <https://asciidoc | ||||||
| 
 | 
 | ||||||
| ### Paragraphs | ### Paragraphs | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| A normal paragraph. | A normal paragraph. | ||||||
| Line breaks are not preserved. | Line breaks are not preserved. | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Line comments, which are lines that start with `//`, are skipped: | Line comments, which are lines that start with `//`, are skipped: | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| // this is a comment | // this is a comment | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | @ -25,7 +25,7 @@ A blank line separates paragraphs. | ||||||
| 
 | 
 | ||||||
| A paragraph with the `[%hardbreaks]` option will preserve line breaks: | A paragraph with the `[%hardbreaks]` option will preserve line breaks: | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| [%hardbreaks] | [%hardbreaks] | ||||||
| This paragraph carries the `hardbreaks` option. | This paragraph carries the `hardbreaks` option. | ||||||
| Notice how line breaks are now preserved. | 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 | preserves spaces and line breaks, and is displayed in a | ||||||
| monospaced font: | monospaced font: | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
|  This literal paragraph is indented with one space. |  This literal paragraph is indented with one space. | ||||||
|  As a consequence, *text formatting*, spaces, |  As a consequence, *text formatting*, spaces, | ||||||
|  and lines breaks will be preserved. |  and lines breaks will be preserved. | ||||||
|  | @ -43,7 +43,7 @@ monospaced font: | ||||||
| 
 | 
 | ||||||
| An admonition paragraph grabs the reader's attention: | 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/. | 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. | 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)** | **Constrained (applied at word boundaries)** | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| *strong importance* (aka bold) | *strong importance* (aka bold) | ||||||
| _stress emphasis_ (aka italic) | _stress emphasis_ (aka italic) | ||||||
| `monospaced` (aka typewriter text) | `monospaced` (aka typewriter text) | ||||||
|  | @ -64,7 +64,7 @@ _stress emphasis_ (aka italic) | ||||||
| 
 | 
 | ||||||
| **Unconstrained (applied anywhere)** | **Unconstrained (applied anywhere)** | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| **C**reate+**R**ead+**U**pdate+**D**elete | **C**reate+**R**ead+**U**pdate+**D**elete | ||||||
| fan__freakin__tastic | fan__freakin__tastic | ||||||
| ``mono``culture | ``mono``culture | ||||||
|  | @ -72,7 +72,7 @@ fan__freakin__tastic | ||||||
| 
 | 
 | ||||||
| **Replacements** | **Replacements** | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| A long time ago in a galaxy far, far away... | A long time ago in a galaxy far, far away... | ||||||
| (C) 1976 Arty Artisan | (C) 1976 Arty Artisan | ||||||
| I believe I shall--no, actually I won't. | I believe I shall--no, actually I won't. | ||||||
|  | @ -80,7 +80,7 @@ I believe I shall--no, actually I won't. | ||||||
| 
 | 
 | ||||||
| **Macros** | **Macros** | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| // where c=specialchars, q=quotes, a=attributes, r=replacements, m=macros, p=post_replacements, etc. | // 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 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. | The pass:c[->] operator is often referred to as the stabby lambda. | ||||||
|  | @ -93,12 +93,12 @@ stem:[sqrt(4) = 2] | ||||||
| 
 | 
 | ||||||
| **User-defined attributes** | **User-defined attributes** | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| // define attributes in the document header | // define attributes in the document header | ||||||
| :name: value | :name: value | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| :url-gem: https://rubygems.org/gems/asciidoctor | :url-gem: https://rubygems.org/gems/asciidoctor | ||||||
| 
 | 
 | ||||||
| You can download and install Asciidoctor {asciidoctor-version} from {url-gem}. | You can download and install Asciidoctor {asciidoctor-version} from {url-gem}. | ||||||
|  | @ -117,7 +117,7 @@ GitLab sets the following environment attributes: | ||||||
| 
 | 
 | ||||||
| ### Links | ### Links | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| https://example.org/page[A webpage] | https://example.org/page[A webpage] | ||||||
| link:../path/to/file.txt[A local file] | link:../path/to/file.txt[A local file] | ||||||
| xref:document.adoc[A sibling document] | xref:document.adoc[A sibling document] | ||||||
|  | @ -126,7 +126,7 @@ mailto:hello@example.org[Email to say hello!] | ||||||
| 
 | 
 | ||||||
| ### Anchors | ### Anchors | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| [[idname,reference text]] | [[idname,reference text]] | ||||||
| // or written using normal block attributes as `[#idname,reftext=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. | 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 | #### Unordered | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| * level 1 | * level 1 | ||||||
| ** level 2 | ** level 2 | ||||||
| *** level 3 | *** level 3 | ||||||
|  | @ -161,7 +161,7 @@ Attach a block or paragraph to a list item using a list continuation (which you | ||||||
| 
 | 
 | ||||||
| #### Ordered | #### Ordered | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| . Step 1 | . Step 1 | ||||||
| . Step 2 | . Step 2 | ||||||
| .. Step 2a | .. Step 2a | ||||||
|  | @ -177,14 +177,14 @@ Attach a block or paragraph to a list item using a list continuation (which you | ||||||
| 
 | 
 | ||||||
| #### Checklist | #### Checklist | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| * [x] checked | * [x] checked | ||||||
| * [ ] not checked | * [ ] not checked | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| #### Callout | #### Callout | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| // enable callout bubbles by adding `:icons: font` to the document header | // enable callout bubbles by adding `:icons: font` to the document header | ||||||
| [,ruby] | [,ruby] | ||||||
| ---- | ---- | ||||||
|  | @ -195,7 +195,7 @@ puts 'Hello, World!' # <1> | ||||||
| 
 | 
 | ||||||
| #### Description | #### Description | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| first term:: description of first term | first term:: description of first term | ||||||
| second term:: | second term:: | ||||||
| description of second term | description of second term | ||||||
|  | @ -205,7 +205,7 @@ description of second term | ||||||
| 
 | 
 | ||||||
| #### Header | #### Header | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| = Document Title | = Document Title | ||||||
| Author Name <author@example.org> | Author Name <author@example.org> | ||||||
| v1.0, 2019-01-01 | v1.0, 2019-01-01 | ||||||
|  | @ -213,7 +213,7 @@ v1.0, 2019-01-01 | ||||||
| 
 | 
 | ||||||
| #### Sections | #### Sections | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| = Document Title (Level 0) | = Document Title (Level 0) | ||||||
| == Level 1 | == Level 1 | ||||||
| === Level 2 | === Level 2 | ||||||
|  | @ -225,7 +225,7 @@ v1.0, 2019-01-01 | ||||||
| 
 | 
 | ||||||
| #### Includes | #### Includes | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| include::basics.adoc[] | include::basics.adoc[] | ||||||
| 
 | 
 | ||||||
| // define -a allow-uri-read to allow content to be read from URI | // 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 | ### Blocks | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| -- | -- | ||||||
| open - a general-purpose content wrapper; useful for enclosing content to attach to a list item | 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 | // recognized types include CAUTION, IMPORTANT, NOTE, TIP, and WARNING | ||||||
| // enable admonition icons by setting `:icons: font` in the document header | // enable admonition icons by setting `:icons: font` in the document header | ||||||
| [NOTE] | [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 | example - a demonstration of the concept being documented | ||||||
| ==== | ==== | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| .Toggle Me | .Toggle Me | ||||||
| [%collapsible] | [%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 | sidebar - auxiliary content that can be read independently of the main content | ||||||
| **** | **** | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| .... | .... | ||||||
| literal - an exhibit that features program output | literal - an exhibit that features program output | ||||||
| .... | .... | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| ---- | ---- | ||||||
| listing - an exhibit that features program input, source code, or the contents of a file | listing - an exhibit that features program input, source code, or the contents of a file | ||||||
| ---- | ---- | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| [,language] | [,language] | ||||||
| ---- | ---- | ||||||
| source - a listing that is embellished with (colorized) syntax highlighting | source - a listing that is embellished with (colorized) syntax highlighting | ||||||
| ---- | ---- | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ````asciidoc | ````plaintext | ||||||
| \```language | \```language | ||||||
| fenced code - a shorthand syntax for the source block | fenced code - a shorthand syntax for the source block | ||||||
| \``` | \``` | ||||||
| ```` | ```` | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| [,attribution,citetitle] | [,attribution,citetitle] | ||||||
| ____ | ____ | ||||||
| quote - a quotation or excerpt; attribution with title of source are optional | quote - a quotation or excerpt; attribution with title of source are optional | ||||||
| ____ | ____ | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| [verse,attribution,citetitle] | [verse,attribution,citetitle] | ||||||
| ____ | ____ | ||||||
| verse - a literary excerpt, often a poem; attribution with title of source are optional | 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 | pass - content passed directly to the output document; often raw HTML | ||||||
| ++++ | ++++ | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| // activate stem support by adding `:stem:` to the document header | // activate stem support by adding `:stem:` to the document header | ||||||
| [stem] | [stem] | ||||||
| ++++ | ++++ | ||||||
|  | @ -327,7 +327,7 @@ x = y^2 | ||||||
| ++++ | ++++ | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| //// | //// | ||||||
| comment - content which is not included in the output document | 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 | ### Tables | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| .Table Attributes | .Table Attributes | ||||||
| [cols=>1h;2d,width=50%,frame=topbot] | [cols=>1h;2d,width=50%,frame=topbot] | ||||||
| |=== | |=== | ||||||
|  | @ -366,7 +366,7 @@ comment - content which is not included in the output document | ||||||
| 
 | 
 | ||||||
| ### Multimedia | ### Multimedia | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| image::screenshot.png[block image,800,450] | image::screenshot.png[block image,800,450] | ||||||
| 
 | 
 | ||||||
| Press image:reload.svg[reload,16,opts=interactive] to reload the page. | Press image:reload.svg[reload,16,opts=interactive] to reload the page. | ||||||
|  | @ -380,12 +380,12 @@ video::300817511[vimeo] | ||||||
| 
 | 
 | ||||||
| ### Breaks | ### Breaks | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| // thematic break (aka horizontal rule) | // thematic break (aka horizontal rule) | ||||||
| --- | --- | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ```asciidoc | ```plaintext | ||||||
| // page break | // page break | ||||||
| <<< | <<< | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | @ -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)** | ## 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. | > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214557) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2. | ||||||
|  |  | ||||||
|  | @ -24,6 +24,8 @@ file path fragments to start seeing results. | ||||||
| 
 | 
 | ||||||
| ## Syntax highlighting | ## 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 | As expected from an IDE, syntax highlighting for many languages within | ||||||
| the Web IDE will make your direct editing even easier. | 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 | - IntelliSense and validation support (displaying errors and warnings, providing | ||||||
|   smart completions, formatting, and outlining) for some languages. For example: |   smart completions, formatting, and outlining) for some languages. For example: | ||||||
|   TypeScript, JavaScript, CSS, LESS, SCSS, JSON, and HTML. |   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/), | 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 | you can find a more complete list of supported languages in the | ||||||
|  |  | ||||||
|  | @ -154,39 +154,48 @@ Similar to versioned diff file views, you can see the changes made in a given Wi | ||||||
| 
 | 
 | ||||||
| ## Wiki activity records | ## Wiki activity records | ||||||
| 
 | 
 | ||||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14902) in GitLab 12.10. | > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14902) in **GitLab 12.10.** | ||||||
| > - It's deployed behind a feature flag, disabled by default. | > - Git events were [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216014) in **GitLab 13.0.** | ||||||
| > - It's enabled on GitLab.com. | > - 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 | Wiki events (creation, deletion, and updates) are tracked by GitLab and | ||||||
| displayed on the [user profile](../../profile/index.md#user-profile), | displayed on the [user profile](../../profile/index.md#user-profile), | ||||||
| [group](../../group/index.md#view-group-activity), | [group](../../group/index.md#view-group-activity), | ||||||
| and [project](../index.md#project-activity) activity pages. | 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. | Tracking wiki events through Git is under development and not ready for production use. It is | ||||||
| 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 |  | ||||||
| deployed behind a feature flag that is **disabled by default**. | 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) | [GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) | ||||||
| can enable it for your instance. You're welcome to test it, but use it at your | can enable it for your instance. | ||||||
| own risk. |  | ||||||
| 
 | 
 | ||||||
| To enable it: | To enable it: | ||||||
| 
 | 
 | ||||||
| ```ruby | ```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: | To disable it: | ||||||
| 
 | 
 | ||||||
| ```ruby | ```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 | ## Adding and editing wiki pages locally | ||||||
|  |  | ||||||
|  | @ -52,15 +52,12 @@ class EventFilter | ||||||
|   private |   private | ||||||
| 
 | 
 | ||||||
|   def apply_feature_flags(events) |   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 = events.not_design unless can_view_design_activity? | ||||||
| 
 | 
 | ||||||
|     events |     events | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def wiki_events(events) |   def wiki_events(events) | ||||||
|     return events unless Feature.enabled?(:wiki_events) |  | ||||||
| 
 |  | ||||||
|     events.for_wiki_page |     events.for_wiki_page | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -44,7 +44,7 @@ module Gitlab | ||||||
| 
 | 
 | ||||||
|               validates :action, |               validates :action, | ||||||
|                         type: String, |                         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 |                         allow_nil: true | ||||||
| 
 | 
 | ||||||
|               validates :on_stop, type: String, allow_nil: true |               validates :on_stop, type: String, allow_nil: true | ||||||
|  |  | ||||||
|  | @ -12,7 +12,8 @@ module Gitlab | ||||||
|           CreateNoteHandler, |           CreateNoteHandler, | ||||||
|           CreateIssueHandler, |           CreateIssueHandler, | ||||||
|           UnsubscribeHandler, |           UnsubscribeHandler, | ||||||
|           CreateMergeRequestHandler |           CreateMergeRequestHandler, | ||||||
|  |           ServiceDeskHandler | ||||||
|         ] |         ] | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  | @ -25,5 +26,3 @@ module Gitlab | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
| 
 |  | ||||||
| Gitlab::Email::Handler.prepend_if_ee('::EE::Gitlab::Email::Handler') |  | ||||||
|  |  | ||||||
|  | @ -37,7 +37,11 @@ module Gitlab | ||||||
| 
 | 
 | ||||||
|         def process_message(**kwargs) |         def process_message(**kwargs) | ||||||
|           message = ReplyParser.new(mail, **kwargs).execute.strip |           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 |         end | ||||||
| 
 | 
 | ||||||
|         def add_attachments(reply) |         def add_attachments(reply) | ||||||
|  | @ -82,6 +86,15 @@ module Gitlab | ||||||
|         def valid_project_slug?(found_project) |         def valid_project_slug?(found_project) | ||||||
|           project_slug == found_project.full_path_slug |           project_slug == found_project.full_path_slug | ||||||
|         end |         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 |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | @ -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 | ||||||
|  | @ -42,8 +42,8 @@ | ||||||
|     "@babel/plugin-syntax-import-meta": "^7.10.1", |     "@babel/plugin-syntax-import-meta": "^7.10.1", | ||||||
|     "@babel/preset-env": "^7.10.1", |     "@babel/preset-env": "^7.10.1", | ||||||
|     "@gitlab/at.js": "1.5.5", |     "@gitlab/at.js": "1.5.5", | ||||||
|     "@gitlab/svgs": "1.151.0", |     "@gitlab/svgs": "1.152.0", | ||||||
|     "@gitlab/ui": "17.22.1", |     "@gitlab/ui": "17.26.0", | ||||||
|     "@gitlab/visual-review-tools": "1.6.1", |     "@gitlab/visual-review-tools": "1.6.1", | ||||||
|     "@rails/actioncable": "^6.0.3-1", |     "@rails/actioncable": "^6.0.3-1", | ||||||
|     "@sentry/browser": "^5.10.2", |     "@sentry/browser": "^5.10.2", | ||||||
|  |  | ||||||
|  | @ -66,29 +66,13 @@ RSpec.describe EventsFinder do | ||||||
|     end |     end | ||||||
|   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) } |     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) } |     subject(:finder) { described_class.new(source: public_project, target_type: 'wiki', current_user: user) } | ||||||
| 
 | 
 | ||||||
|     context 'the wiki_events feature flag is disabled' do |     it 'can find the wiki events' do | ||||||
|       before do |       expect(finder.execute).to match_array(events) | ||||||
|         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 |  | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | @ -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 | ||||||
|  | @ -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 | ||||||
|  | @ -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 | ||||||
|  | @ -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 | ||||||
|  | @ -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 | ||||||
|  | ``` | ||||||
|  | @ -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). | ||||||
|  | > | ||||||
|  | @ -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', | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | @ -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, | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | @ -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"', | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | @ -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); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | @ -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(); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | @ -114,4 +114,18 @@ RSpec.describe EnvironmentsHelper do | ||||||
|       expect(subject).to eq(true) |       expect(subject).to eq(true) | ||||||
|     end |     end | ||||||
|   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 | end | ||||||
|  |  | ||||||
|  | @ -80,16 +80,6 @@ RSpec.describe EventFilter do | ||||||
|       it 'returns all events' do |       it 'returns all events' do | ||||||
|         expect(filtered_events).to eq(Event.all) |         expect(filtered_events).to eq(Event.all) | ||||||
|       end |       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 | ||||||
| 
 | 
 | ||||||
|     context 'with the "design" filter' do |     context 'with the "design" filter' do | ||||||
|  | @ -116,16 +106,6 @@ RSpec.describe EventFilter do | ||||||
|       it 'returns only wiki page events' do |       it 'returns only wiki page events' do | ||||||
|         expect(filtered_events).to contain_exactly(wiki_page_event, wiki_page_update_event) |         expect(filtered_events).to contain_exactly(wiki_page_event, wiki_page_update_event) | ||||||
|       end |       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 |     end | ||||||
| 
 | 
 | ||||||
|     context 'with an unknown filter' do |     context 'with an unknown filter' do | ||||||
|  | @ -134,16 +114,6 @@ RSpec.describe EventFilter do | ||||||
|       it 'returns all events' do |       it 'returns all events' do | ||||||
|         expect(filtered_events).to eq(Event.all) |         expect(filtered_events).to eq(Event.all) | ||||||
|       end |       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 | ||||||
| 
 | 
 | ||||||
|     context 'with a nil filter' do |     context 'with a nil filter' do | ||||||
|  | @ -152,16 +122,6 @@ RSpec.describe EventFilter do | ||||||
|       it 'returns all events' do |       it 'returns all events' do | ||||||
|         expect(filtered_events).to eq(Event.all) |         expect(filtered_events).to eq(Event.all) | ||||||
|       end |       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 | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -102,6 +102,17 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do | ||||||
|     end |     end | ||||||
|   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 |   context 'when wrong action type is used' do | ||||||
|     let(:config) do |     let(:config) do | ||||||
|       { name: 'production', |       { name: 'production', | ||||||
|  | @ -137,7 +148,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do | ||||||
|     describe '#errors' do |     describe '#errors' do | ||||||
|       it 'contains error about invalid action' do |       it 'contains error about invalid action' do | ||||||
|         expect(entry.errors) |         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 |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -102,6 +102,19 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Deployment do | ||||||
|       end |       end | ||||||
|     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 |     context 'when job does not have environment attribute' do | ||||||
|       let(:attributes) { { name: 'test' } } |       let(:attributes) { { name: 'test' } } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -242,4 +242,70 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do | ||||||
| 
 | 
 | ||||||
|     it_behaves_like 'a reply to existing comment' |     it_behaves_like 'a reply to existing comment' | ||||||
|   end |   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 | end | ||||||
|  |  | ||||||
|  | @ -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" | ||||||
|  |   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 | ||||||
|  | 
 | ||||||
|  |              | ||||||
|  |           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 | ||||||
|  | @ -33,12 +33,40 @@ RSpec.describe Gitlab::Email::Handler do | ||||||
|     it 'returns nil if provided email is nil' do |     it 'returns nil if provided email is nil' do | ||||||
|       expect(described_class.for(nil, '')).to be_nil |       expect(described_class.for(nil, '')).to be_nil | ||||||
|     end |     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 |   end | ||||||
| 
 | 
 | ||||||
|   describe 'regexps are set properly' do |   describe 'regexps are set properly' do | ||||||
|     let(:addresses) 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} 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+merge-request+user_email_token path/to/project+user_email_token) |         %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 |     end | ||||||
| 
 | 
 | ||||||
|     it 'picks each handler at least once' do |     it 'picks each handler at least once' do | ||||||
|  | @ -46,12 +74,12 @@ RSpec.describe Gitlab::Email::Handler do | ||||||
|         described_class.for(email, address).class |         described_class.for(email, address).class | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       expect(matched_handlers.uniq).to match_array(ce_handlers) |       expect(matched_handlers.uniq).to match_array(Gitlab::Email::Handler.handlers) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     it 'can pick exactly one handler for each address' do |     it 'can pick exactly one handler for each address' do | ||||||
|       addresses.each do |address| |       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? |           handler.new(email, address).can_handle? | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|  | @ -59,10 +87,4 @@ RSpec.describe Gitlab::Email::Handler do | ||||||
|       end |       end | ||||||
|     end |     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 | end | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | @ -1,11 +1,8 @@ | ||||||
| # frozen_string_literal: true | # frozen_string_literal: true | ||||||
| 
 | 
 | ||||||
| require 'fast_spec_helper' | require 'fast_spec_helper' | ||||||
| require 'support/helpers/fixture_helpers' |  | ||||||
| 
 | 
 | ||||||
| RSpec.describe Sentry::PaginationParser do | RSpec.describe Sentry::PaginationParser do | ||||||
|   include FixtureHelpers |  | ||||||
| 
 |  | ||||||
|   describe '.parse' do |   describe '.parse' do | ||||||
|     subject { described_class.parse(headers) } |     subject { described_class.parse(headers) } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | @ -1253,6 +1253,78 @@ RSpec.describe Notify do | ||||||
|         it_behaves_like 'appearance header and footer not enabled' |         it_behaves_like 'appearance header and footer not enabled' | ||||||
|       end |       end | ||||||
|     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 |   end | ||||||
| 
 | 
 | ||||||
|   context 'for a group' do |   context 'for a group' do | ||||||
|  |  | ||||||
|  | @ -44,22 +44,10 @@ RSpec.describe EventCollection do | ||||||
|         expect(events).to match_array(most_recent_20_events) |         expect(events).to match_array(most_recent_20_events) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'the wiki_events feature flag is disabled' do |       it 'includes the wiki page events when using to_a' do | ||||||
|         before do |         events = described_class.new(projects).to_a | ||||||
|           stub_feature_flags(wiki_events: false) |  | ||||||
|         end |  | ||||||
| 
 | 
 | ||||||
|         it 'omits the wiki page events when using to_a' do |         expect(events).to include(wiki_page_event) | ||||||
|           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 |  | ||||||
| 
 |  | ||||||
|           expect(events).not_to include(wiki_page_event) |  | ||||||
|         end |  | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'the design_activity_events feature flag is disabled' do |       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) |         expect(collection.all_project_events).to include(design_event) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'the wiki_events feature flag is enabled' do |       it 'includes the wiki page events when using all_project_events' do | ||||||
|         before do |         events = described_class.new(projects).all_project_events | ||||||
|           stub_feature_flags(wiki_events: true) |  | ||||||
|         end |  | ||||||
| 
 | 
 | ||||||
|         it 'includes the wiki page events when using to_a' do |         expect(events).to include(wiki_page_event) | ||||||
|           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 |  | ||||||
| 
 |  | ||||||
|           expect(events).to include(wiki_page_event) |  | ||||||
|         end |  | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'applies a limit to the number of events' do |       it 'applies a limit to the number of events' do | ||||||
|  |  | ||||||
|  | @ -661,15 +661,6 @@ RSpec.describe Event do | ||||||
|       end |       end | ||||||
|     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 |     describe '.for_wiki_meta' do | ||||||
|       it 'finds events for a given wiki page metadata object' do |       it 'finds events for a given wiki page metadata object' do | ||||||
|         event = events.select(&:wiki_page?).first |         event = events.select(&:wiki_page?).first | ||||||
|  |  | ||||||
|  | @ -289,4 +289,74 @@ RSpec.describe Clusters::ClusterPresenter do | ||||||
|       it_behaves_like 'cluster health data' |       it_behaves_like 'cluster health data' | ||||||
|     end |     end | ||||||
|   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 | end | ||||||
|  |  | ||||||
|  | @ -1928,6 +1928,13 @@ RSpec.describe API::Projects do | ||||||
|         end |         end | ||||||
|       end |       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 |   end | ||||||
| 
 | 
 | ||||||
|   describe 'GET /projects/:id/users' do |   describe 'GET /projects/:id/users' do | ||||||
|  |  | ||||||
|  | @ -3,8 +3,13 @@ | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe ClusterEntity do | RSpec.describe ClusterEntity do | ||||||
|  |   include Gitlab::Routing.url_helpers | ||||||
|  | 
 | ||||||
|   describe '#as_json' do |   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 |     context 'when provider type is gcp' do | ||||||
|       let(:cluster) { create(:cluster, :instance, provider_type: :gcp, provider_gcp: provider) } |       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 |     context 'when no application has been installed' do | ||||||
|       let(:cluster) { create(:cluster, :instance) } |       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 |       it 'contains helm as not_installable' do | ||||||
|         expect(subject).not_to be_empty |         expect(subject).not_to be_empty | ||||||
|  | @ -50,5 +55,28 @@ RSpec.describe ClusterEntity do | ||||||
|         expect(helm[:status]).to eq(:not_installable) |         expect(helm[:status]).to eq(:not_installable) | ||||||
|       end |       end | ||||||
|     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 | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -6,13 +6,14 @@ RSpec.describe ClusterSerializer do | ||||||
|   let(:cluster) { create(:cluster, :project, provider_type: :user) } |   let(:cluster) { create(:cluster, :project, provider_type: :user) } | ||||||
| 
 | 
 | ||||||
|   describe '#represent_list' do |   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 |     it 'serializes attrs correctly' do | ||||||
|       is_expected.to contain_exactly( |       is_expected.to contain_exactly( | ||||||
|         :cluster_type, |         :cluster_type, | ||||||
|         :enabled, |         :enabled, | ||||||
|         :environment_scope, |         :environment_scope, | ||||||
|  |         :gitlab_managed_apps_logs_path, | ||||||
|         :name, |         :name, | ||||||
|         :nodes, |         :nodes, | ||||||
|         :path, |         :path, | ||||||
|  | @ -22,7 +23,7 @@ RSpec.describe ClusterSerializer do | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   describe '#represent_status' do |   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 |     context 'when provider type is gcp and cluster is errored' do | ||||||
|       let(:cluster) do |       let(:cluster) do | ||||||
|  |  | ||||||
|  | @ -200,16 +200,6 @@ RSpec.describe EventCreateService do | ||||||
| 
 | 
 | ||||||
|           expect(duplicate).to eq(event) |           expect(duplicate).to eq(event) | ||||||
|         end |         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 | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -247,14 +247,6 @@ RSpec.describe Git::WikiPushService, services: true do | ||||||
|         end |         end | ||||||
|       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 |       context 'the wiki_events_on_git_push feature is disabled' do | ||||||
|         before do |         before do | ||||||
|           stub_feature_flags(wiki_events_on_git_push: false) |           stub_feature_flags(wiki_events_on_git_push: false) | ||||||
|  |  | ||||||
|  | @ -14,21 +14,6 @@ RSpec.describe WikiPages::EventCreateService do | ||||||
|     let(:action) { :created } |     let(:action) { :created } | ||||||
|     let(:response) { subject.execute(slug, page, action) } |     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 |     context 'the user is nil' do | ||||||
|       subject { described_class.new(nil) } |       subject { described_class.new(nil) } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -63,16 +63,6 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type| | ||||||
|     include_examples 'correct event created' |     include_examples 'correct event created' | ||||||
|   end |   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 |   context 'when the options are bad' do | ||||||
|     let(:page_title) { '' } |     let(:page_title) { '' } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -37,14 +37,4 @@ RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type| | ||||||
| 
 | 
 | ||||||
|     expect { service.execute(nil) }.not_to change { counter.read(:delete) } |     expect { service.execute(nil) }.not_to change { counter.read(:delete) } | ||||||
|   end |   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 | end | ||||||
|  |  | ||||||
|  | @ -67,16 +67,6 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type| | ||||||
|     include_examples 'adds activity event' |     include_examples 'adds activity event' | ||||||
|   end |   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 |   context 'when the options are bad' do | ||||||
|     let(:page_title) { '' } |     let(:page_title) { '' } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
							
								
								
									
										16
									
								
								yarn.lock
								
								
								
								
							
							
						
						
									
										16
									
								
								yarn.lock
								
								
								
								
							|  | @ -843,15 +843,15 @@ | ||||||
|     eslint-plugin-vue "^6.2.1" |     eslint-plugin-vue "^6.2.1" | ||||||
|     vue-eslint-parser "^7.0.0" |     vue-eslint-parser "^7.0.0" | ||||||
| 
 | 
 | ||||||
| "@gitlab/svgs@1.151.0": | "@gitlab/svgs@1.152.0": | ||||||
|   version "1.151.0" |   version "1.152.0" | ||||||
|   resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.151.0.tgz#099905295d33eb31033f4a48eb3652da2f686239" |   resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.152.0.tgz#663c9a5f073f59b66f4241ef2d3fea2205846905" | ||||||
|   integrity sha512-2PTSM8CFhUjeTFKfcq6E/YwPpOVdSVWupf3NhKO/bz/cisSBS5P7aWxaXKIaxy28ySyBKEfKaAT6b4rXTwvVgg== |   integrity sha512-daZHOBVAwjsU6n60IycanoO/JymfQ36vrr46OUdWjHdp0ATYrgh+01LcxiSNLdlyndIRqHWGtwmuilokM9q6Vg== | ||||||
| 
 | 
 | ||||||
| "@gitlab/ui@17.22.1": | "@gitlab/ui@17.26.0": | ||||||
|   version "17.22.1" |   version "17.26.0" | ||||||
|   resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.22.1.tgz#368578d04bb49011690911599c22a7d306f5fe99" |   resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.26.0.tgz#d8efad47c3f4dc32e0586f3f5e4e2e3e0c2babf6" | ||||||
|   integrity sha512-elcu2gdvt1Afz3GMrIBQR+eujlA6JetLn44T1UzPHUhlaodT/w+TIj0+uPIbPiD7Oz6uR/sYwBqlZXQdBcVv3Q== |   integrity sha512-0QgzMK8MFGaqBB8yYntjYjUnzKFQ9a8d4mjufIyeKq6WomuMYHTFJgUj0+cEQ6uuTRtNk3MMuy3ZHBJg1wGzTw== | ||||||
|   dependencies: |   dependencies: | ||||||
|     "@babel/standalone" "^7.0.0" |     "@babel/standalone" "^7.0.0" | ||||||
|     "@gitlab/vue-toasted" "^1.3.0" |     "@gitlab/vue-toasted" "^1.3.0" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue