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'
|
||||
EventCollection.new(current_user.authorized_projects).all_project_events
|
||||
else
|
||||
# EventCollection is responsible for applying the feature flag
|
||||
apply_feature_flags(source.events)
|
||||
source.events
|
||||
end
|
||||
end
|
||||
|
||||
def apply_feature_flags(events)
|
||||
return events if ::Feature.enabled?(:wiki_events)
|
||||
|
||||
events.not_wiki_page
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def by_current_user_access(events)
|
||||
events.merge(Project.public_or_visible_to_user(current_user))
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ module EnvironmentsHelper
|
|||
"environment-name": environment.name,
|
||||
"environments-path": project_environments_path(project, format: :json),
|
||||
"environment-id": environment.id,
|
||||
"cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack')
|
||||
"cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack'),
|
||||
"clusters-path": project_clusters_path(project, format: :json)
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -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::Groups
|
||||
include Emails::Reviews
|
||||
include Emails::ServiceDesk
|
||||
|
||||
helper TimeboxesHelper
|
||||
helper MergeRequestsHelper
|
||||
|
|
|
|||
|
|
@ -165,6 +165,18 @@ class NotifyPreview < ActionMailer::Preview
|
|||
Notify.unknown_sign_in_email(user, '127.0.0.1', Time.current).message
|
||||
end
|
||||
|
||||
def service_desk_new_note_email
|
||||
cleanup do
|
||||
note = create_note(noteable_type: 'Issue', noteable_id: issue.id, note: 'Issue note content')
|
||||
|
||||
Notify.service_desk_new_note_email(issue.id, note.id).message
|
||||
end
|
||||
end
|
||||
|
||||
def service_desk_thank_you_email
|
||||
Notify.service_desk_thank_you_email(issue.id).message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project
|
||||
|
|
|
|||
|
|
@ -84,7 +84,6 @@ class Event < ApplicationRecord
|
|||
scope :for_design, -> { where(target_type: 'DesignManagement::Design') }
|
||||
|
||||
# Needed to implement feature flag: can be removed when feature flag is removed
|
||||
scope :not_wiki_page, -> { where('target_type IS NULL or target_type <> ?', 'WikiPage::Meta') }
|
||||
scope :not_design, -> { where('target_type IS NULL or target_type <> ?', 'DesignManagement::Design') }
|
||||
|
||||
scope :with_associations, -> do
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ class EventCollection
|
|||
private
|
||||
|
||||
def apply_feature_flags(events)
|
||||
events = events.not_wiki_page unless ::Feature.enabled?(:wiki_events)
|
||||
events = events.not_design unless ::Feature.enabled?(:design_activity_events)
|
||||
|
||||
events
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
module Clusters
|
||||
class ClusterPresenter < Gitlab::View::Presenter::Delegated
|
||||
include ::Gitlab::Utils::StrongMemoize
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
include ActionView::Helpers::UrlHelper
|
||||
include IconsHelper
|
||||
|
|
@ -60,6 +61,12 @@ module Clusters
|
|||
end
|
||||
end
|
||||
|
||||
def gitlab_managed_apps_logs_path
|
||||
return unless logs_project && can_read_cluster?
|
||||
|
||||
project_logs_path(logs_project, cluster_id: cluster.id)
|
||||
end
|
||||
|
||||
def read_only_kubernetes_platform_fields?
|
||||
!cluster.provided_by_user?
|
||||
end
|
||||
|
|
@ -85,6 +92,16 @@ module Clusters
|
|||
ActionController::Base.helpers.image_path(path)
|
||||
end
|
||||
|
||||
# currently log explorer is only available in the scope of the project
|
||||
# for group and instance level cluster selected project does not affects
|
||||
# fetching logs from gitlab managed apps namespace, therefore any project
|
||||
# available to user will be sufficient.
|
||||
def logs_project
|
||||
strong_memoize(:logs_project) do
|
||||
cluster.all_projects.first
|
||||
end
|
||||
end
|
||||
|
||||
def clusterable
|
||||
if cluster.group_type?
|
||||
cluster.group
|
||||
|
|
|
|||
|
|
@ -16,4 +16,8 @@ class ClusterEntity < Grape::Entity
|
|||
expose :path do |cluster|
|
||||
Clusters::ClusterPresenter.new(cluster).show_path # rubocop: disable CodeReuse/Presenter
|
||||
end
|
||||
|
||||
expose :gitlab_managed_apps_logs_path do |cluster|
|
||||
Clusters::ClusterPresenter.new(cluster, current_user: request.current_user).gitlab_managed_apps_logs_path # rubocop: disable CodeReuse/Presenter
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class ClusterSerializer < BaseSerializer
|
|||
:cluster_type,
|
||||
:enabled,
|
||||
:environment_scope,
|
||||
:gitlab_managed_apps_logs_path,
|
||||
:name,
|
||||
:nodes,
|
||||
:path,
|
||||
|
|
|
|||
|
|
@ -120,8 +120,6 @@ class EventCreateService
|
|||
#
|
||||
# @return a tuple of event and either :found or :created
|
||||
def wiki_event(wiki_page_meta, author, action)
|
||||
return unless Feature.enabled?(:wiki_events)
|
||||
|
||||
raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action)
|
||||
|
||||
if duplicate = existing_wiki_event(wiki_page_meta, action)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ module Git
|
|||
end
|
||||
|
||||
def can_process_wiki_events?
|
||||
Feature.enabled?(:wiki_events) && Feature.enabled?(:wiki_events_on_git_push, project)
|
||||
Feature.enabled?(:wiki_events_on_git_push, project)
|
||||
end
|
||||
|
||||
def push_changes
|
||||
|
|
|
|||
|
|
@ -44,8 +44,6 @@ module WikiPages
|
|||
end
|
||||
|
||||
def create_wiki_event(page)
|
||||
return unless ::Feature.enabled?(:wiki_events)
|
||||
|
||||
response = WikiPages::EventCreateService.new(current_user).execute(slug_for_page(page), page, event_action)
|
||||
|
||||
log_error(response.message) if response.error?
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ module WikiPages
|
|||
end
|
||||
|
||||
def execute(slug, page, action)
|
||||
return ServiceResponse.success(message: 'No event created as `wiki_events` feature is disabled') unless ::Feature.enabled?(:wiki_events)
|
||||
|
||||
event = Event.transaction do
|
||||
wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
- if comments_visible?
|
||||
= event_filter_link EventFilter::COMMENTS, _('Comments'), s_('EventFilterBy|Filter by comments')
|
||||
- if Feature.enabled?(:wiki_events) && (@project.nil? || @project.has_wiki?)
|
||||
- if @project.nil? || @project.has_wiki?
|
||||
= event_filter_link EventFilter::WIKI, _('Wiki'), s_('EventFilterBy|Filter by wiki')
|
||||
- if event_filter_visible(:designs)
|
||||
= event_filter_link EventFilter::DESIGNS, _('Designs'), s_('EventFilterBy|Filter by designs')
|
||||
|
|
|
|||
|
|
@ -1660,6 +1660,14 @@
|
|||
:weight: 2
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: service_desk_email_receiver
|
||||
:feature_category: :issue_tracking
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: system_hook_push
|
||||
:feature_category: :source_code_management
|
||||
:has_external_dependencies:
|
||||
|
|
|
|||
|
|
@ -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%.
|
||||
|
||||
NOTE: **Note:**
|
||||
Stickiness (consistent application behavior for the same user) is guaranteed for logged-in users, but not anonymous users.
|
||||
|
||||
CAUTION: **Caution:**
|
||||
If this strategy is selected, then the Unleash client **must** be given a user
|
||||
ID for the feature to be enabled. See the [Ruby example](#ruby-application-example) below.
|
||||
|
|
@ -120,6 +123,9 @@ activation strategy.
|
|||
Enter user IDs as a comma-separated list of values. For example,
|
||||
`user@example.com, user2@example.com`, or `username1,username2,username3`, and so on.
|
||||
|
||||
NOTE: **Note:**
|
||||
User IDs are identifiers for your application users. They do not need to be GitLab users.
|
||||
|
||||
CAUTION: **Caution:**
|
||||
The Unleash client **must** be given a user ID for the feature to be enabled for
|
||||
target users. See the [Ruby example](#ruby-application-example) below.
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ You can find the full documentation for the AsciiDoc syntax at <https://asciidoc
|
|||
|
||||
### Paragraphs
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
A normal paragraph.
|
||||
Line breaks are not preserved.
|
||||
```
|
||||
|
||||
Line comments, which are lines that start with `//`, are skipped:
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
// this is a comment
|
||||
```
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ A blank line separates paragraphs.
|
|||
|
||||
A paragraph with the `[%hardbreaks]` option will preserve line breaks:
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
[%hardbreaks]
|
||||
This paragraph carries the `hardbreaks` option.
|
||||
Notice how line breaks are now preserved.
|
||||
|
|
@ -35,7 +35,7 @@ An indented (literal) paragraph disables text formatting,
|
|||
preserves spaces and line breaks, and is displayed in a
|
||||
monospaced font:
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
This literal paragraph is indented with one space.
|
||||
As a consequence, *text formatting*, spaces,
|
||||
and lines breaks will be preserved.
|
||||
|
|
@ -43,7 +43,7 @@ monospaced font:
|
|||
|
||||
An admonition paragraph grabs the reader's attention:
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
NOTE: This is a brief reference, please read the full documentation at https://asciidoctor.org/docs/.
|
||||
|
||||
TIP: Lists can be indented. Leading whitespace is not significant.
|
||||
|
|
@ -53,7 +53,7 @@ TIP: Lists can be indented. Leading whitespace is not significant.
|
|||
|
||||
**Constrained (applied at word boundaries)**
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
*strong importance* (aka bold)
|
||||
_stress emphasis_ (aka italic)
|
||||
`monospaced` (aka typewriter text)
|
||||
|
|
@ -64,7 +64,7 @@ _stress emphasis_ (aka italic)
|
|||
|
||||
**Unconstrained (applied anywhere)**
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
**C**reate+**R**ead+**U**pdate+**D**elete
|
||||
fan__freakin__tastic
|
||||
``mono``culture
|
||||
|
|
@ -72,7 +72,7 @@ fan__freakin__tastic
|
|||
|
||||
**Replacements**
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
A long time ago in a galaxy far, far away...
|
||||
(C) 1976 Arty Artisan
|
||||
I believe I shall--no, actually I won't.
|
||||
|
|
@ -80,7 +80,7 @@ I believe I shall--no, actually I won't.
|
|||
|
||||
**Macros**
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
// where c=specialchars, q=quotes, a=attributes, r=replacements, m=macros, p=post_replacements, etc.
|
||||
The European icon:flag[role=blue] is blue & contains pass:[************] arranged in a icon:circle-o[role=yellow].
|
||||
The pass:c[->] operator is often referred to as the stabby lambda.
|
||||
|
|
@ -93,12 +93,12 @@ stem:[sqrt(4) = 2]
|
|||
|
||||
**User-defined attributes**
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
// define attributes in the document header
|
||||
:name: value
|
||||
```
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
:url-gem: https://rubygems.org/gems/asciidoctor
|
||||
|
||||
You can download and install Asciidoctor {asciidoctor-version} from {url-gem}.
|
||||
|
|
@ -117,7 +117,7 @@ GitLab sets the following environment attributes:
|
|||
|
||||
### Links
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
https://example.org/page[A webpage]
|
||||
link:../path/to/file.txt[A local file]
|
||||
xref:document.adoc[A sibling document]
|
||||
|
|
@ -126,7 +126,7 @@ mailto:hello@example.org[Email to say hello!]
|
|||
|
||||
### Anchors
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
[[idname,reference text]]
|
||||
// or written using normal block attributes as `[#idname,reftext=reference text]`
|
||||
A paragraph (or any block) with an anchor (aka ID) and reftext.
|
||||
|
|
@ -142,7 +142,7 @@ This paragraph has a footnote.footnote:[This is the text of the footnote.]
|
|||
|
||||
#### Unordered
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
* level 1
|
||||
** level 2
|
||||
*** level 3
|
||||
|
|
@ -161,7 +161,7 @@ Attach a block or paragraph to a list item using a list continuation (which you
|
|||
|
||||
#### Ordered
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
. Step 1
|
||||
. Step 2
|
||||
.. Step 2a
|
||||
|
|
@ -177,14 +177,14 @@ Attach a block or paragraph to a list item using a list continuation (which you
|
|||
|
||||
#### Checklist
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
* [x] checked
|
||||
* [ ] not checked
|
||||
```
|
||||
|
||||
#### Callout
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
// enable callout bubbles by adding `:icons: font` to the document header
|
||||
[,ruby]
|
||||
----
|
||||
|
|
@ -195,7 +195,7 @@ puts 'Hello, World!' # <1>
|
|||
|
||||
#### Description
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
first term:: description of first term
|
||||
second term::
|
||||
description of second term
|
||||
|
|
@ -205,7 +205,7 @@ description of second term
|
|||
|
||||
#### Header
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
= Document Title
|
||||
Author Name <author@example.org>
|
||||
v1.0, 2019-01-01
|
||||
|
|
@ -213,7 +213,7 @@ v1.0, 2019-01-01
|
|||
|
||||
#### Sections
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
= Document Title (Level 0)
|
||||
== Level 1
|
||||
=== Level 2
|
||||
|
|
@ -225,7 +225,7 @@ v1.0, 2019-01-01
|
|||
|
||||
#### Includes
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
include::basics.adoc[]
|
||||
|
||||
// define -a allow-uri-read to allow content to be read from URI
|
||||
|
|
@ -239,13 +239,13 @@ included, a number that is inclusive of transitive dependencies.
|
|||
|
||||
### Blocks
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
--
|
||||
open - a general-purpose content wrapper; useful for enclosing content to attach to a list item
|
||||
--
|
||||
```
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
// recognized types include CAUTION, IMPORTANT, NOTE, TIP, and WARNING
|
||||
// enable admonition icons by setting `:icons: font` in the document header
|
||||
[NOTE]
|
||||
|
|
@ -254,13 +254,13 @@ admonition - a notice for the reader, ranging in severity from a tip to an alert
|
|||
====
|
||||
```
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
====
|
||||
example - a demonstration of the concept being documented
|
||||
====
|
||||
```
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
.Toggle Me
|
||||
[%collapsible]
|
||||
====
|
||||
|
|
@ -268,58 +268,58 @@ collapsible - these details are revealed by clicking the title
|
|||
====
|
||||
```
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
****
|
||||
sidebar - auxiliary content that can be read independently of the main content
|
||||
****
|
||||
```
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
....
|
||||
literal - an exhibit that features program output
|
||||
....
|
||||
```
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
----
|
||||
listing - an exhibit that features program input, source code, or the contents of a file
|
||||
----
|
||||
```
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
[,language]
|
||||
----
|
||||
source - a listing that is embellished with (colorized) syntax highlighting
|
||||
----
|
||||
```
|
||||
|
||||
````asciidoc
|
||||
````plaintext
|
||||
\```language
|
||||
fenced code - a shorthand syntax for the source block
|
||||
\```
|
||||
````
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
[,attribution,citetitle]
|
||||
____
|
||||
quote - a quotation or excerpt; attribution with title of source are optional
|
||||
____
|
||||
```
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
[verse,attribution,citetitle]
|
||||
____
|
||||
verse - a literary excerpt, often a poem; attribution with title of source are optional
|
||||
____
|
||||
```
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
++++
|
||||
pass - content passed directly to the output document; often raw HTML
|
||||
++++
|
||||
```
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
// activate stem support by adding `:stem:` to the document header
|
||||
[stem]
|
||||
++++
|
||||
|
|
@ -327,7 +327,7 @@ x = y^2
|
|||
++++
|
||||
```
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
////
|
||||
comment - content which is not included in the output document
|
||||
////
|
||||
|
|
@ -335,7 +335,7 @@ comment - content which is not included in the output document
|
|||
|
||||
### Tables
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
.Table Attributes
|
||||
[cols=>1h;2d,width=50%,frame=topbot]
|
||||
|===
|
||||
|
|
@ -366,7 +366,7 @@ comment - content which is not included in the output document
|
|||
|
||||
### Multimedia
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
image::screenshot.png[block image,800,450]
|
||||
|
||||
Press image:reload.svg[reload,16,opts=interactive] to reload the page.
|
||||
|
|
@ -380,12 +380,12 @@ video::300817511[vimeo]
|
|||
|
||||
### Breaks
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
// thematic break (aka horizontal rule)
|
||||
---
|
||||
```
|
||||
|
||||
```asciidoc
|
||||
```plaintext
|
||||
// page break
|
||||
<<<
|
||||
```
|
||||
|
|
|
|||
|
|
@ -90,6 +90,22 @@ Example payload:
|
|||
}
|
||||
```
|
||||
|
||||
## Triggering test alerts
|
||||
|
||||
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3066) in GitLab Core in 13.2.
|
||||
|
||||
After a [project maintainer or owner](#setting-up-generic-alerts)
|
||||
[configures generic alerts](#setting-up-generic-alerts), you can trigger a
|
||||
test alert to confirm your integration works properly.
|
||||
|
||||
1. Sign in as a user with Developer or greater [permissions](../../../user/permissions.md).
|
||||
1. Navigate to **{settings}** **Settings > Operations** in your project.
|
||||
1. Click **Alerts endpoint** to expand the section.
|
||||
1. Enter a sample payload in **Alert test payload** (valid JSON is required).
|
||||
1. Click **Test alert payload**.
|
||||
|
||||
GitLab displays an error or success message, depending on the outcome of your test.
|
||||
|
||||
## Automatic grouping of identical alerts **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214557) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ file path fragments to start seeing results.
|
|||
|
||||
## Syntax highlighting
|
||||
|
||||
> Support for `.gitlab.ci.yml` validation [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218472) in GitLab 13.2.
|
||||
|
||||
As expected from an IDE, syntax highlighting for many languages within
|
||||
the Web IDE will make your direct editing even easier.
|
||||
|
||||
|
|
@ -35,6 +37,13 @@ The Web IDE currently provides:
|
|||
- IntelliSense and validation support (displaying errors and warnings, providing
|
||||
smart completions, formatting, and outlining) for some languages. For example:
|
||||
TypeScript, JavaScript, CSS, LESS, SCSS, JSON, and HTML.
|
||||
- Validation support for certain JSON and YAML files using schemas based on the
|
||||
[JSON Schema Store](https://www.schemastore.org/json/). This feature
|
||||
is only supported for the `.gitlab-ci.yml` file.
|
||||
|
||||
NOTE: **Note:** Validation support based on schemas is hidden behind
|
||||
the feature flag `:schema_linting` on self-managed installations. To enable the
|
||||
feature, you can [turn on the feature flag in Rails console](../../../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags).
|
||||
|
||||
Because the Web IDE is based on the [Monaco Editor](https://microsoft.github.io/monaco-editor/),
|
||||
you can find a more complete list of supported languages in the
|
||||
|
|
|
|||
|
|
@ -154,39 +154,48 @@ Similar to versioned diff file views, you can see the changes made in a given Wi
|
|||
|
||||
## Wiki activity records
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14902) in GitLab 12.10.
|
||||
> - It's deployed behind a feature flag, disabled by default.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14902) in **GitLab 12.10.**
|
||||
> - Git events were [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216014) in **GitLab 13.0.**
|
||||
> - It's enabled on GitLab.com.
|
||||
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-wiki-events-core-only). **(CORE ONLY)**
|
||||
> - Git access activity creation is managed by a feature flag.
|
||||
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-wiki-events-in-git-core-only). **(CORE ONLY)**
|
||||
|
||||
Wiki events (creation, deletion, and updates) are tracked by GitLab and
|
||||
displayed on the [user profile](../../profile/index.md#user-profile),
|
||||
[group](../../group/index.md#view-group-activity),
|
||||
and [project](../index.md#project-activity) activity pages.
|
||||
|
||||
### Limitations
|
||||
### Enable or disable Wiki events in Git **(CORE ONLY)**
|
||||
|
||||
Only edits made in the browser or through the API have their activity recorded.
|
||||
Edits made and pushed through Git are not currently listed in the activity list.
|
||||
|
||||
### Enable or disable Wiki Events **(CORE ONLY)**
|
||||
|
||||
Wiki event activity is under development and not ready for production use. It is
|
||||
Tracking wiki events through Git is under development and not ready for production use. It is
|
||||
deployed behind a feature flag that is **disabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session)
|
||||
can enable it for your instance. You're welcome to test it, but use it at your
|
||||
own risk.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
|
||||
can enable it for your instance.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:wiki_events)
|
||||
Feature.enable(:wiki_events_on_git_push)
|
||||
```
|
||||
|
||||
To enable for just a particular project:
|
||||
|
||||
```ruby
|
||||
project = Project.find_by_full_path('your-group/your-project')
|
||||
Feature.enable(:wiki_events_on_git_push, project)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:wiki_events)
|
||||
Feature.disable(:wiki_events_on_git_push)
|
||||
```
|
||||
|
||||
To disable for just a particular project:
|
||||
|
||||
```ruby
|
||||
project = Project.find_by_full_path('your-group/your-project')
|
||||
Feature.disable(:wiki_events_on_git_push, project)
|
||||
```
|
||||
|
||||
## Adding and editing wiki pages locally
|
||||
|
|
|
|||
|
|
@ -52,15 +52,12 @@ class EventFilter
|
|||
private
|
||||
|
||||
def apply_feature_flags(events)
|
||||
events = events.not_wiki_page unless Feature.enabled?(:wiki_events)
|
||||
events = events.not_design unless can_view_design_activity?
|
||||
|
||||
events
|
||||
end
|
||||
|
||||
def wiki_events(events)
|
||||
return events unless Feature.enabled?(:wiki_events)
|
||||
|
||||
events.for_wiki_page
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ module Gitlab
|
|||
|
||||
validates :action,
|
||||
type: String,
|
||||
inclusion: { in: %w[start stop], message: 'should be start or stop' },
|
||||
inclusion: { in: %w[start stop prepare], message: 'should be start, stop or prepare' },
|
||||
allow_nil: true
|
||||
|
||||
validates :on_stop, type: String, allow_nil: true
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ module Gitlab
|
|||
CreateNoteHandler,
|
||||
CreateIssueHandler,
|
||||
UnsubscribeHandler,
|
||||
CreateMergeRequestHandler
|
||||
CreateMergeRequestHandler,
|
||||
ServiceDeskHandler
|
||||
]
|
||||
end
|
||||
|
||||
|
|
@ -25,5 +26,3 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Gitlab::Email::Handler.prepend_if_ee('::EE::Gitlab::Email::Handler')
|
||||
|
|
|
|||
|
|
@ -37,7 +37,11 @@ module Gitlab
|
|||
|
||||
def process_message(**kwargs)
|
||||
message = ReplyParser.new(mail, **kwargs).execute.strip
|
||||
add_attachments(message)
|
||||
message_with_attachments = add_attachments(message)
|
||||
|
||||
# Support bot is specifically forbidden
|
||||
# from using slash commands.
|
||||
strip_quick_actions(message_with_attachments)
|
||||
end
|
||||
|
||||
def add_attachments(reply)
|
||||
|
|
@ -82,6 +86,15 @@ module Gitlab
|
|||
def valid_project_slug?(found_project)
|
||||
project_slug == found_project.full_path_slug
|
||||
end
|
||||
|
||||
def strip_quick_actions(content)
|
||||
return content unless author.support_bot?
|
||||
|
||||
command_definitions = ::QuickActions::InterpretService.command_definitions
|
||||
extractor = ::Gitlab::QuickActions::Extractor.new(command_definitions)
|
||||
|
||||
extractor.redact_commands(content)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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/preset-env": "^7.10.1",
|
||||
"@gitlab/at.js": "1.5.5",
|
||||
"@gitlab/svgs": "1.151.0",
|
||||
"@gitlab/ui": "17.22.1",
|
||||
"@gitlab/svgs": "1.152.0",
|
||||
"@gitlab/ui": "17.26.0",
|
||||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@rails/actioncable": "^6.0.3-1",
|
||||
"@sentry/browser": "^5.10.2",
|
||||
|
|
|
|||
|
|
@ -66,29 +66,13 @@ RSpec.describe EventsFinder do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'wiki events feature flag' do
|
||||
describe 'wiki events' do
|
||||
let_it_be(:events) { create_list(:wiki_page_event, 3, project: public_project) }
|
||||
|
||||
subject(:finder) { described_class.new(source: public_project, target_type: 'wiki', current_user: user) }
|
||||
|
||||
context 'the wiki_events feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(wiki_events: false)
|
||||
end
|
||||
|
||||
it 'omits the wiki page events' do
|
||||
expect(finder.execute).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'the wiki_events feature flag is enabled' do
|
||||
before do
|
||||
stub_feature_flags(wiki_events: true)
|
||||
end
|
||||
|
||||
it 'can find the wiki events' do
|
||||
expect(finder.execute).to match_array(events)
|
||||
end
|
||||
it 'can find the wiki events' do
|
||||
expect(finder.execute).to match_array(events)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -80,16 +80,6 @@ RSpec.describe EventFilter do
|
|||
it 'returns all events' do
|
||||
expect(filtered_events).to eq(Event.all)
|
||||
end
|
||||
|
||||
context 'the :wiki_events filter is disabled' do
|
||||
before do
|
||||
stub_feature_flags(wiki_events: false)
|
||||
end
|
||||
|
||||
it 'does not return wiki events' do
|
||||
expect(filtered_events).to eq(Event.not_wiki_page)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the "design" filter' do
|
||||
|
|
@ -116,16 +106,6 @@ RSpec.describe EventFilter do
|
|||
it 'returns only wiki page events' do
|
||||
expect(filtered_events).to contain_exactly(wiki_page_event, wiki_page_update_event)
|
||||
end
|
||||
|
||||
context 'the :wiki_events filter is disabled' do
|
||||
before do
|
||||
stub_feature_flags(wiki_events: false)
|
||||
end
|
||||
|
||||
it 'does not return wiki events' do
|
||||
expect(filtered_events).not_to include(wiki_page_event, wiki_page_update_event)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an unknown filter' do
|
||||
|
|
@ -134,16 +114,6 @@ RSpec.describe EventFilter do
|
|||
it 'returns all events' do
|
||||
expect(filtered_events).to eq(Event.all)
|
||||
end
|
||||
|
||||
context 'the :wiki_events filter is disabled' do
|
||||
before do
|
||||
stub_feature_flags(wiki_events: false)
|
||||
end
|
||||
|
||||
it 'does not return wiki events' do
|
||||
expect(filtered_events).to eq(Event.not_wiki_page)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a nil filter' do
|
||||
|
|
@ -152,16 +122,6 @@ RSpec.describe EventFilter do
|
|||
it 'returns all events' do
|
||||
expect(filtered_events).to eq(Event.all)
|
||||
end
|
||||
|
||||
context 'the :wiki_events filter is disabled' do
|
||||
before do
|
||||
stub_feature_flags(wiki_events: false)
|
||||
end
|
||||
|
||||
it 'does not return wiki events' do
|
||||
expect(filtered_events).to eq(Event.not_wiki_page)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,17 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when prepare action is used' do
|
||||
let(:config) do
|
||||
{ name: 'production',
|
||||
action: 'prepare' }
|
||||
end
|
||||
|
||||
it 'is valid' do
|
||||
expect(entry).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context 'when wrong action type is used' do
|
||||
let(:config) do
|
||||
{ name: 'production',
|
||||
|
|
@ -137,7 +148,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do
|
|||
describe '#errors' do
|
||||
it 'contains error about invalid action' do
|
||||
expect(entry.errors)
|
||||
.to include 'environment action should be start or stop'
|
||||
.to include 'environment action should be start, stop or prepare'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -102,6 +102,19 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Deployment do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when job has environment attribute with prepare action' do
|
||||
let(:attributes) do
|
||||
{
|
||||
environment: 'production',
|
||||
options: { environment: { name: 'production', action: 'prepare' } }
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns nothing' do
|
||||
is_expected.to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when job does not have environment attribute' do
|
||||
let(:attributes) { { name: 'test' } }
|
||||
|
||||
|
|
|
|||
|
|
@ -242,4 +242,70 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do
|
|||
|
||||
it_behaves_like 'a reply to existing comment'
|
||||
end
|
||||
|
||||
context 'when the service desk' do
|
||||
let(:project) { create(:project, :public, service_desk_enabled: true) }
|
||||
let(:support_bot) { User.support_bot }
|
||||
let(:noteable) { create(:issue, project: project, author: support_bot, title: 'service desk issue') }
|
||||
let(:note) { create(:note, project: project, noteable: noteable) }
|
||||
let(:email_raw) { fixture_file('emails/valid_reply_with_quick_actions.eml') }
|
||||
|
||||
let!(:sent_notification) do
|
||||
SentNotification.record_note(note, support_bot.id, mail_key)
|
||||
end
|
||||
|
||||
context 'is enabled' do
|
||||
before do
|
||||
allow(Gitlab::ServiceDesk).to receive(:enabled?).with(project: project).and_return(true)
|
||||
project.project_feature.update!(issues_access_level: issues_access_level)
|
||||
end
|
||||
|
||||
context 'when issues are enabled for everyone' do
|
||||
let(:issues_access_level) { ProjectFeature::ENABLED }
|
||||
|
||||
it 'creates a comment' do
|
||||
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
|
||||
end
|
||||
|
||||
context 'when quick actions are present' do
|
||||
it 'encloses quick actions with code span markdown' do
|
||||
receiver.execute
|
||||
noteable.reload
|
||||
|
||||
note = Note.last
|
||||
expect(note.note).to include("Jake out\n\n`/close`\n`/title test`")
|
||||
expect(noteable.title).to eq('service desk issue')
|
||||
expect(noteable).to be_opened
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issues are protected members only' do
|
||||
let(:issues_access_level) { ProjectFeature::PRIVATE }
|
||||
|
||||
it 'creates a comment' do
|
||||
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issues are disabled' do
|
||||
let(:issues_access_level) { ProjectFeature::DISABLED }
|
||||
|
||||
it 'does not create a comment' do
|
||||
expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotAuthorizedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'is disabled' do
|
||||
before do
|
||||
allow(Gitlab::ServiceDesk).to receive(:enabled?).and_return(false)
|
||||
allow(Gitlab::ServiceDesk).to receive(:enabled?).with(project: project).and_return(false)
|
||||
end
|
||||
|
||||
it 'does not create a comment' do
|
||||
expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
expect(described_class.for(nil, '')).to be_nil
|
||||
end
|
||||
|
||||
context 'new issue email' do
|
||||
def handler_for(fixture, mail_key)
|
||||
described_class.for(fixture_file(fixture), mail_key)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
|
||||
stub_config_setting(host: 'localhost')
|
||||
end
|
||||
|
||||
let!(:user) { create(:user, email: 'jake@adventuretime.ooo', incoming_email_token: 'auth_token') }
|
||||
|
||||
context 'a Service Desk email' do
|
||||
it 'uses the Service Desk handler' do
|
||||
expect(handler_for('emails/service_desk.eml', 'some/project')).to be_instance_of(Gitlab::Email::Handler::ServiceDeskHandler)
|
||||
end
|
||||
end
|
||||
|
||||
it 'return new issue handler' do
|
||||
expect(handler_for('emails/valid_new_issue.eml', 'some/project+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'regexps are set properly' do
|
||||
let(:addresses) do
|
||||
%W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX} sent_notification_key path-to-project-123-user_email_token-merge-request path-to-project-123-user_email_token-issue) +
|
||||
%W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY} sent_notification_key path/to/project+merge-request+user_email_token path/to/project+user_email_token)
|
||||
%W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX} sent_notification_key path-to-project-123-user_email_token-merge-request) +
|
||||
%W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY} sent_notification_key path-to-project-123-user_email_token-issue) +
|
||||
%w(path/to/project+user_email_token path/to/project+merge-request+user_email_token some/project)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Gitlab::ServiceDesk).to receive(:supported?).and_return(true)
|
||||
end
|
||||
|
||||
it 'picks each handler at least once' do
|
||||
|
|
@ -46,12 +74,12 @@ RSpec.describe Gitlab::Email::Handler do
|
|||
described_class.for(email, address).class
|
||||
end
|
||||
|
||||
expect(matched_handlers.uniq).to match_array(ce_handlers)
|
||||
expect(matched_handlers.uniq).to match_array(Gitlab::Email::Handler.handlers)
|
||||
end
|
||||
|
||||
it 'can pick exactly one handler for each address' do
|
||||
addresses.each do |address|
|
||||
matched_handlers = ce_handlers.select do |handler|
|
||||
matched_handlers = Gitlab::Email::Handler.handlers.select do |handler|
|
||||
handler.new(email, address).can_handle?
|
||||
end
|
||||
|
||||
|
|
@ -59,10 +87,4 @@ RSpec.describe Gitlab::Email::Handler do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def ce_handlers
|
||||
@ce_handlers ||= Gitlab::Email::Handler.handlers.reject do |handler|
|
||||
handler.name.start_with?('Gitlab::Email::Handler::EE::')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
require 'fast_spec_helper'
|
||||
require 'support/helpers/fixture_helpers'
|
||||
|
||||
RSpec.describe Sentry::PaginationParser do
|
||||
include FixtureHelpers
|
||||
|
||||
describe '.parse' do
|
||||
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'
|
||||
end
|
||||
end
|
||||
|
||||
context 'for service desk issues' do
|
||||
before do
|
||||
issue.update!(service_desk_reply_to: 'service.desk@example.com')
|
||||
end
|
||||
|
||||
def expect_sender(username)
|
||||
sender = subject.header[:from].addrs[0]
|
||||
expect(sender.display_name).to eq(username)
|
||||
expect(sender.address).to eq(gitlab_sender)
|
||||
end
|
||||
|
||||
describe 'thank you email' do
|
||||
subject { described_class.service_desk_thank_you_email(issue.id) }
|
||||
|
||||
it_behaves_like 'an unsubscribeable thread'
|
||||
|
||||
it 'has the correct recipient' do
|
||||
is_expected.to deliver_to('service.desk@example.com')
|
||||
end
|
||||
|
||||
it 'has the correct subject and body' do
|
||||
aggregate_failures do
|
||||
is_expected.to have_referable_subject(issue, include_project: false, reply: true)
|
||||
is_expected.to have_body_text("Thank you for your support request! We are tracking your request as ticket #{issue.to_reference}, and will respond as soon as we can.")
|
||||
end
|
||||
end
|
||||
|
||||
it 'uses service bot name by default' do
|
||||
expect_sender(User.support_bot.name)
|
||||
end
|
||||
|
||||
context 'when custom outgoing name is set' do
|
||||
let_it_be(:settings) { create(:service_desk_setting, project: project, outgoing_name: 'some custom name') }
|
||||
|
||||
it 'uses custom name in "from" header' do
|
||||
expect_sender('some custom name')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when custom outgoing name is empty' do
|
||||
let_it_be(:settings) { create(:service_desk_setting, project: project, outgoing_name: '') }
|
||||
|
||||
it 'uses service bot name' do
|
||||
expect_sender(User.support_bot.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'new note email' do
|
||||
let_it_be(:first_note) { create(:discussion_note_on_issue, note: 'Hello world') }
|
||||
|
||||
subject { described_class.service_desk_new_note_email(issue.id, first_note.id) }
|
||||
|
||||
it_behaves_like 'an unsubscribeable thread'
|
||||
|
||||
it 'has the correct recipient' do
|
||||
is_expected.to deliver_to('service.desk@example.com')
|
||||
end
|
||||
|
||||
it 'uses author\'s name in "from" header' do
|
||||
expect_sender(first_note.author.name)
|
||||
end
|
||||
|
||||
it 'has the correct subject and body' do
|
||||
aggregate_failures do
|
||||
is_expected.to have_referable_subject(issue, include_project: false, reply: true)
|
||||
is_expected.to have_body_text(first_note.note)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a group' do
|
||||
|
|
|
|||
|
|
@ -44,22 +44,10 @@ RSpec.describe EventCollection do
|
|||
expect(events).to match_array(most_recent_20_events)
|
||||
end
|
||||
|
||||
context 'the wiki_events feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(wiki_events: false)
|
||||
end
|
||||
it 'includes the wiki page events when using to_a' do
|
||||
events = described_class.new(projects).to_a
|
||||
|
||||
it 'omits the wiki page events when using to_a' do
|
||||
events = described_class.new(projects).to_a
|
||||
|
||||
expect(events).not_to include(wiki_page_event)
|
||||
end
|
||||
|
||||
it 'omits the wiki page events when using all_project_events' do
|
||||
events = described_class.new(projects).all_project_events
|
||||
|
||||
expect(events).not_to include(wiki_page_event)
|
||||
end
|
||||
expect(events).to include(wiki_page_event)
|
||||
end
|
||||
|
||||
context 'the design_activity_events feature flag is disabled' do
|
||||
|
|
@ -87,22 +75,10 @@ RSpec.describe EventCollection do
|
|||
expect(collection.all_project_events).to include(design_event)
|
||||
end
|
||||
|
||||
context 'the wiki_events feature flag is enabled' do
|
||||
before do
|
||||
stub_feature_flags(wiki_events: true)
|
||||
end
|
||||
it 'includes the wiki page events when using all_project_events' do
|
||||
events = described_class.new(projects).all_project_events
|
||||
|
||||
it 'includes the wiki page events when using to_a' do
|
||||
events = described_class.new(projects).to_a
|
||||
|
||||
expect(events).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
|
||||
expect(events).to include(wiki_page_event)
|
||||
end
|
||||
|
||||
it 'applies a limit to the number of events' do
|
||||
|
|
|
|||
|
|
@ -661,15 +661,6 @@ RSpec.describe Event do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.not_wiki_page' do
|
||||
it 'does not contain the wiki page events' do
|
||||
non_wiki_events = events.reject(&:wiki_page?)
|
||||
|
||||
expect(events).not_to match_array(non_wiki_events)
|
||||
expect(described_class.not_wiki_page).to match_array(non_wiki_events)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.for_wiki_meta' do
|
||||
it 'finds events for a given wiki page metadata object' do
|
||||
event = events.select(&:wiki_page?).first
|
||||
|
|
|
|||
|
|
@ -289,4 +289,74 @@ RSpec.describe Clusters::ClusterPresenter do
|
|||
it_behaves_like 'cluster health data'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#gitlab_managed_apps_logs_path' do
|
||||
context 'user can read logs' do
|
||||
let(:project) { cluster.project }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'returns path to logs' do
|
||||
expect(presenter.gitlab_managed_apps_logs_path).to eq project_logs_path(project, cluster_id: cluster.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'group cluster' do
|
||||
let(:cluster) { create(:cluster, cluster_type: :group_type, groups: [group]) }
|
||||
let(:group) { create(:group, name: 'Foo') }
|
||||
|
||||
context 'user can read logs' do
|
||||
before do
|
||||
group.add_maintainer(user)
|
||||
end
|
||||
|
||||
context 'there are projects within group' do
|
||||
let!(:project) { create(:project, namespace: group) }
|
||||
|
||||
it 'returns path to logs' do
|
||||
expect(presenter.gitlab_managed_apps_logs_path).to eq project_logs_path(project, cluster_id: cluster.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'there are no projects within group' do
|
||||
it 'returns nil' do
|
||||
expect(presenter.gitlab_managed_apps_logs_path).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'instance cluster' do
|
||||
let(:cluster) { create(:cluster, cluster_type: :instance_type) }
|
||||
let!(:project) { create(:project) }
|
||||
let(:user) { create(:admin) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
stub_feature_flags(user_mode_in_session: false)
|
||||
end
|
||||
|
||||
context 'user can read logs' do
|
||||
it 'returns path to logs' do
|
||||
expect(presenter.gitlab_managed_apps_logs_path).to eq project_logs_path(project, cluster_id: cluster.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'user can NOT read logs' do
|
||||
let(:cluster) { create(:cluster, cluster_type: :instance_type) }
|
||||
let!(:project) { create(:project) }
|
||||
|
||||
before do
|
||||
project.add_developer(user)
|
||||
stub_feature_flags(user_mode_in_session: false)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(presenter.gitlab_managed_apps_logs_path).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1928,6 +1928,13 @@ RSpec.describe API::Projects do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'exposes service desk attributes' do
|
||||
get api("/projects/#{project.id}", user)
|
||||
|
||||
expect(json_response).to have_key 'service_desk_enabled'
|
||||
expect(json_response).to have_key 'service_desk_address'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/users' do
|
||||
|
|
|
|||
|
|
@ -3,8 +3,13 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ClusterEntity do
|
||||
include Gitlab::Routing.url_helpers
|
||||
|
||||
describe '#as_json' do
|
||||
subject { described_class.new(cluster).as_json }
|
||||
let(:user) { nil }
|
||||
let(:request) { EntityRequest.new({ current_user: user }) }
|
||||
|
||||
subject { described_class.new(cluster, request: request).as_json }
|
||||
|
||||
context 'when provider type is gcp' do
|
||||
let(:cluster) { create(:cluster, :instance, provider_type: :gcp, provider_gcp: provider) }
|
||||
|
|
@ -40,7 +45,7 @@ RSpec.describe ClusterEntity do
|
|||
context 'when no application has been installed' do
|
||||
let(:cluster) { create(:cluster, :instance) }
|
||||
|
||||
subject { described_class.new(cluster).as_json[:applications]}
|
||||
subject { described_class.new(cluster, request: request).as_json[:applications]}
|
||||
|
||||
it 'contains helm as not_installable' do
|
||||
expect(subject).not_to be_empty
|
||||
|
|
@ -50,5 +55,28 @@ RSpec.describe ClusterEntity do
|
|||
expect(helm[:status]).to eq(:not_installable)
|
||||
end
|
||||
end
|
||||
|
||||
context 'gitlab_managed_apps_logs_path' do
|
||||
let(:cluster) { create(:cluster, :project) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
subject { described_class.new(cluster, request: request).as_json }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(Clusters::ClusterPresenter) do |presenter|
|
||||
allow(presenter).to receive(:show_path).and_return(nil)
|
||||
end
|
||||
end
|
||||
|
||||
it 'return projects log explorer path' do
|
||||
log_explorer_path = project_logs_path(cluster.project, cluster_id: cluster.id)
|
||||
|
||||
expect_next_instance_of(Clusters::ClusterPresenter, cluster, current_user: user) do |presenter|
|
||||
expect(presenter).to receive(:gitlab_managed_apps_logs_path).and_return(log_explorer_path)
|
||||
end
|
||||
|
||||
expect(subject[:gitlab_managed_apps_logs_path]).to eq(log_explorer_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ RSpec.describe ClusterSerializer do
|
|||
let(:cluster) { create(:cluster, :project, provider_type: :user) }
|
||||
|
||||
describe '#represent_list' do
|
||||
subject { described_class.new.represent_list(cluster).keys }
|
||||
subject { described_class.new(current_user: nil).represent_list(cluster).keys }
|
||||
|
||||
it 'serializes attrs correctly' do
|
||||
is_expected.to contain_exactly(
|
||||
:cluster_type,
|
||||
:enabled,
|
||||
:environment_scope,
|
||||
:gitlab_managed_apps_logs_path,
|
||||
:name,
|
||||
:nodes,
|
||||
:path,
|
||||
|
|
@ -22,7 +23,7 @@ RSpec.describe ClusterSerializer do
|
|||
end
|
||||
|
||||
describe '#represent_status' do
|
||||
subject { described_class.new.represent_status(cluster).keys }
|
||||
subject { described_class.new(current_user: nil).represent_status(cluster).keys }
|
||||
|
||||
context 'when provider type is gcp and cluster is errored' do
|
||||
let(:cluster) do
|
||||
|
|
|
|||
|
|
@ -200,16 +200,6 @@ RSpec.describe EventCreateService do
|
|||
|
||||
expect(duplicate).to eq(event)
|
||||
end
|
||||
|
||||
context 'the feature is disabled' do
|
||||
before do
|
||||
stub_feature_flags(wiki_events: false)
|
||||
end
|
||||
|
||||
it 'does not create the event' do
|
||||
expect { event }.not_to change(Event, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -247,14 +247,6 @@ RSpec.describe Git::WikiPushService, services: true do
|
|||
end
|
||||
end
|
||||
|
||||
context 'the wiki_events feature is disabled' do
|
||||
before do
|
||||
stub_feature_flags(wiki_events: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'a no-op push'
|
||||
end
|
||||
|
||||
context 'the wiki_events_on_git_push feature is disabled' do
|
||||
before do
|
||||
stub_feature_flags(wiki_events_on_git_push: false)
|
||||
|
|
|
|||
|
|
@ -14,21 +14,6 @@ RSpec.describe WikiPages::EventCreateService do
|
|||
let(:action) { :created }
|
||||
let(:response) { subject.execute(slug, page, action) }
|
||||
|
||||
context 'feature flag is not enabled' do
|
||||
before do
|
||||
stub_feature_flags(wiki_events: false)
|
||||
end
|
||||
|
||||
it 'does not error' do
|
||||
expect(response).to be_success
|
||||
.and have_attributes(message: /No event created/)
|
||||
end
|
||||
|
||||
it 'does not create an event' do
|
||||
expect { response }.not_to change(Event, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'the user is nil' do
|
||||
subject { described_class.new(nil) }
|
||||
|
||||
|
|
|
|||
|
|
@ -63,16 +63,6 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type|
|
|||
include_examples 'correct event created'
|
||||
end
|
||||
|
||||
context 'the feature is disabled' do
|
||||
before do
|
||||
stub_feature_flags(wiki_events: false)
|
||||
end
|
||||
|
||||
it 'does not record the activity' do
|
||||
expect { service.execute }.not_to change(Event, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the options are bad' do
|
||||
let(:page_title) { '' }
|
||||
|
||||
|
|
|
|||
|
|
@ -37,14 +37,4 @@ RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type|
|
|||
|
||||
expect { service.execute(nil) }.not_to change { counter.read(:delete) }
|
||||
end
|
||||
|
||||
context 'the feature is disabled' do
|
||||
before do
|
||||
stub_feature_flags(wiki_events: false)
|
||||
end
|
||||
|
||||
it 'does not record the activity' do
|
||||
expect { service.execute(page) }.not_to change(Event, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -67,16 +67,6 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type|
|
|||
include_examples 'adds activity event'
|
||||
end
|
||||
|
||||
context 'the feature is disabled' do
|
||||
before do
|
||||
stub_feature_flags(wiki_events: false)
|
||||
end
|
||||
|
||||
it 'does not record the activity' do
|
||||
expect { service.execute(page) }.not_to change(Event, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the options are bad' do
|
||||
let(:page_title) { '' }
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
vue-eslint-parser "^7.0.0"
|
||||
|
||||
"@gitlab/svgs@1.151.0":
|
||||
version "1.151.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.151.0.tgz#099905295d33eb31033f4a48eb3652da2f686239"
|
||||
integrity sha512-2PTSM8CFhUjeTFKfcq6E/YwPpOVdSVWupf3NhKO/bz/cisSBS5P7aWxaXKIaxy28ySyBKEfKaAT6b4rXTwvVgg==
|
||||
"@gitlab/svgs@1.152.0":
|
||||
version "1.152.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.152.0.tgz#663c9a5f073f59b66f4241ef2d3fea2205846905"
|
||||
integrity sha512-daZHOBVAwjsU6n60IycanoO/JymfQ36vrr46OUdWjHdp0ATYrgh+01LcxiSNLdlyndIRqHWGtwmuilokM9q6Vg==
|
||||
|
||||
"@gitlab/ui@17.22.1":
|
||||
version "17.22.1"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.22.1.tgz#368578d04bb49011690911599c22a7d306f5fe99"
|
||||
integrity sha512-elcu2gdvt1Afz3GMrIBQR+eujlA6JetLn44T1UzPHUhlaodT/w+TIj0+uPIbPiD7Oz6uR/sYwBqlZXQdBcVv3Q==
|
||||
"@gitlab/ui@17.26.0":
|
||||
version "17.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.26.0.tgz#d8efad47c3f4dc32e0586f3f5e4e2e3e0c2babf6"
|
||||
integrity sha512-0QgzMK8MFGaqBB8yYntjYjUnzKFQ9a8d4mjufIyeKq6WomuMYHTFJgUj0+cEQ6uuTRtNk3MMuy3ZHBJg1wGzTw==
|
||||
dependencies:
|
||||
"@babel/standalone" "^7.0.0"
|
||||
"@gitlab/vue-toasted" "^1.3.0"
|
||||
|
|
|
|||
Loading…
Reference in New Issue