Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
49089d4fb1
commit
66bd1f0fdc
|
|
@ -0,0 +1,30 @@
|
||||||
|
import axios from '~/lib/utils/axios_utils';
|
||||||
|
import * as types from './mutation_types';
|
||||||
|
import { parseCodeclimateMetrics, doCodeClimateComparison } from './utils/codequality_comparison';
|
||||||
|
|
||||||
|
export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths);
|
||||||
|
|
||||||
|
export const fetchReports = ({ state, dispatch, commit }) => {
|
||||||
|
commit(types.REQUEST_REPORTS);
|
||||||
|
|
||||||
|
if (!state.basePath) {
|
||||||
|
return dispatch('receiveReportsError');
|
||||||
|
}
|
||||||
|
return Promise.all([axios.get(state.headPath), axios.get(state.basePath)])
|
||||||
|
.then(results =>
|
||||||
|
doCodeClimateComparison(
|
||||||
|
parseCodeclimateMetrics(results[0].data, state.headBlobPath),
|
||||||
|
parseCodeclimateMetrics(results[1].data, state.baseBlobPath),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then(data => dispatch('receiveReportsSuccess', data))
|
||||||
|
.catch(() => dispatch('receiveReportsError'));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const receiveReportsSuccess = ({ commit }, data) => {
|
||||||
|
commit(types.RECEIVE_REPORTS_SUCCESS, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const receiveReportsError = ({ commit }) => {
|
||||||
|
commit(types.RECEIVE_REPORTS_ERROR);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { LOADING, ERROR, SUCCESS } from '../../constants';
|
||||||
|
import { sprintf, __, s__, n__ } from '~/locale';
|
||||||
|
|
||||||
|
export const hasCodequalityIssues = state =>
|
||||||
|
Boolean(state.newIssues?.length || state.resolvedIssues?.length);
|
||||||
|
|
||||||
|
export const codequalityStatus = state => {
|
||||||
|
if (state.isLoading) {
|
||||||
|
return LOADING;
|
||||||
|
}
|
||||||
|
if (state.hasError) {
|
||||||
|
return ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SUCCESS;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const codequalityText = state => {
|
||||||
|
const { newIssues, resolvedIssues } = state;
|
||||||
|
const text = [];
|
||||||
|
|
||||||
|
if (!newIssues.length && !resolvedIssues.length) {
|
||||||
|
text.push(s__('ciReport|No changes to code quality'));
|
||||||
|
} else {
|
||||||
|
text.push(s__('ciReport|Code quality'));
|
||||||
|
|
||||||
|
if (resolvedIssues.length) {
|
||||||
|
text.push(n__(' improved on %d point', ' improved on %d points', resolvedIssues.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newIssues.length && resolvedIssues.length) {
|
||||||
|
text.push(__(' and'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newIssues.length) {
|
||||||
|
text.push(n__(' degraded on %d point', ' degraded on %d points', newIssues.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const codequalityPopover = state => {
|
||||||
|
if (state.headPath && !state.basePath) {
|
||||||
|
return {
|
||||||
|
title: s__('ciReport|Base pipeline codequality artifact not found'),
|
||||||
|
content: sprintf(
|
||||||
|
s__('ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}'),
|
||||||
|
{
|
||||||
|
linkStartTag: `<a href="${state.helpPath}" target="_blank" rel="noopener noreferrer">`,
|
||||||
|
linkEndTag: '<i class="fa fa-external-link" aria-hidden="true"></i></a>',
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Vuex from 'vuex';
|
||||||
|
import * as actions from './actions';
|
||||||
|
import * as getters from './getters';
|
||||||
|
import mutations from './mutations';
|
||||||
|
import state from './state';
|
||||||
|
|
||||||
|
Vue.use(Vuex);
|
||||||
|
|
||||||
|
export default initialState =>
|
||||||
|
new Vuex.Store({
|
||||||
|
actions,
|
||||||
|
getters,
|
||||||
|
mutations,
|
||||||
|
state: state(initialState),
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export const SET_PATHS = 'SET_PATHS';
|
||||||
|
|
||||||
|
export const REQUEST_REPORTS = 'REQUEST_REPORTS';
|
||||||
|
export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS';
|
||||||
|
export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as types from './mutation_types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
[types.SET_PATHS](state, paths) {
|
||||||
|
state.basePath = paths.basePath;
|
||||||
|
state.headPath = paths.headPath;
|
||||||
|
state.baseBlobPath = paths.baseBlobPath;
|
||||||
|
state.headBlobPath = paths.headBlobPath;
|
||||||
|
state.helpPath = paths.helpPath;
|
||||||
|
},
|
||||||
|
[types.REQUEST_REPORTS](state) {
|
||||||
|
state.isLoading = true;
|
||||||
|
},
|
||||||
|
[types.RECEIVE_REPORTS_SUCCESS](state, data) {
|
||||||
|
state.hasError = false;
|
||||||
|
state.isLoading = false;
|
||||||
|
state.newIssues = data.newIssues;
|
||||||
|
state.resolvedIssues = data.resolvedIssues;
|
||||||
|
},
|
||||||
|
[types.RECEIVE_REPORTS_ERROR](state) {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.hasError = true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
export default () => ({
|
||||||
|
basePath: null,
|
||||||
|
headPath: null,
|
||||||
|
|
||||||
|
baseBlobPath: null,
|
||||||
|
headBlobPath: null,
|
||||||
|
|
||||||
|
isLoading: false,
|
||||||
|
hasError: false,
|
||||||
|
|
||||||
|
newIssues: [],
|
||||||
|
resolvedIssues: [],
|
||||||
|
|
||||||
|
helpPath: null,
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import CodeQualityComparisonWorker from '../../workers/codequality_comparison_worker';
|
||||||
|
|
||||||
|
export const parseCodeclimateMetrics = (issues = [], path = '') => {
|
||||||
|
return issues.map(issue => {
|
||||||
|
const parsedIssue = {
|
||||||
|
...issue,
|
||||||
|
name: issue.description,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (issue?.location?.path) {
|
||||||
|
let parseCodeQualityUrl = `${path}/${issue.location.path}`;
|
||||||
|
parsedIssue.path = issue.location.path;
|
||||||
|
|
||||||
|
if (issue?.location?.lines?.begin) {
|
||||||
|
parsedIssue.line = issue.location.lines.begin;
|
||||||
|
parseCodeQualityUrl += `#L${issue.location.lines.begin}`;
|
||||||
|
} else if (issue?.location?.positions?.begin?.line) {
|
||||||
|
parsedIssue.line = issue.location.positions.begin.line;
|
||||||
|
parseCodeQualityUrl += `#L${issue.location.positions.begin.line}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedIssue.urlPath = parseCodeQualityUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedIssue;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doCodeClimateComparison = (headIssues, baseIssues) => {
|
||||||
|
// Do these comparisons in worker threads to avoid blocking the main thread
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const worker = new CodeQualityComparisonWorker();
|
||||||
|
worker.addEventListener('message', ({ data }) =>
|
||||||
|
data.newIssues && data.resolvedIssues ? resolve(data) : reject(data),
|
||||||
|
);
|
||||||
|
worker.postMessage({
|
||||||
|
headIssues,
|
||||||
|
baseIssues,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { differenceBy } from 'lodash';
|
||||||
|
|
||||||
|
const KEY_TO_FILTER_BY = 'fingerprint';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
self.addEventListener('message', e => {
|
||||||
|
const { data } = e;
|
||||||
|
|
||||||
|
if (data === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { headIssues, baseIssues } = data;
|
||||||
|
|
||||||
|
if (!headIssues || !baseIssues) {
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
return self.postMessage({});
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
self.postMessage({
|
||||||
|
newIssues: differenceBy(headIssues, baseIssues, KEY_TO_FILTER_BY),
|
||||||
|
resolvedIssues: differenceBy(baseIssues, headIssues, KEY_TO_FILTER_BY),
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
return self.close();
|
||||||
|
});
|
||||||
|
|
@ -54,17 +54,10 @@ class EventsFinder
|
||||||
if current_user && scope == 'all'
|
if current_user && scope == 'all'
|
||||||
EventCollection.new(current_user.authorized_projects).all_project_events
|
EventCollection.new(current_user.authorized_projects).all_project_events
|
||||||
else
|
else
|
||||||
# EventCollection is responsible for applying the feature flag
|
source.events
|
||||||
apply_feature_flags(source.events)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def apply_feature_flags(events)
|
|
||||||
return events if ::Feature.enabled?(:wiki_events)
|
|
||||||
|
|
||||||
events.not_wiki_page
|
|
||||||
end
|
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def by_current_user_access(events)
|
def by_current_user_access(events)
|
||||||
events.merge(Project.public_or_visible_to_user(current_user))
|
events.merge(Project.public_or_visible_to_user(current_user))
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ module EnvironmentsHelper
|
||||||
"environment-name": environment.name,
|
"environment-name": environment.name,
|
||||||
"environments-path": project_environments_path(project, format: :json),
|
"environments-path": project_environments_path(project, format: :json),
|
||||||
"environment-id": environment.id,
|
"environment-id": environment.id,
|
||||||
"cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack')
|
"cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack'),
|
||||||
|
"clusters-path": project_clusters_path(project, format: :json)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Emails
|
||||||
|
module ServiceDesk
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
include MarkupHelper
|
||||||
|
|
||||||
|
included do
|
||||||
|
layout 'service_desk', only: [:service_desk_thank_you_email, :service_desk_new_note_email]
|
||||||
|
end
|
||||||
|
|
||||||
|
def service_desk_thank_you_email(issue_id)
|
||||||
|
setup_service_desk_mail(issue_id)
|
||||||
|
|
||||||
|
email_sender = sender(
|
||||||
|
@support_bot.id,
|
||||||
|
send_from_user_email: false,
|
||||||
|
sender_name: @project.service_desk_setting&.outgoing_name
|
||||||
|
)
|
||||||
|
options = service_desk_options(email_sender, 'thank_you')
|
||||||
|
.merge(subject: "Re: #{subject_base}")
|
||||||
|
|
||||||
|
mail_new_thread(@issue, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def service_desk_new_note_email(issue_id, note_id)
|
||||||
|
@note = Note.find(note_id)
|
||||||
|
setup_service_desk_mail(issue_id)
|
||||||
|
|
||||||
|
email_sender = sender(@note.author_id)
|
||||||
|
options = service_desk_options(email_sender, 'new_note')
|
||||||
|
.merge(subject: subject_base)
|
||||||
|
|
||||||
|
mail_answer_thread(@issue, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def setup_service_desk_mail(issue_id)
|
||||||
|
@issue = Issue.find(issue_id)
|
||||||
|
@project = @issue.project
|
||||||
|
@support_bot = User.support_bot
|
||||||
|
|
||||||
|
@sent_notification = SentNotification.record(@issue, @support_bot.id, reply_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def service_desk_options(email_sender, email_type)
|
||||||
|
{
|
||||||
|
from: email_sender,
|
||||||
|
to: @issue.service_desk_reply_to
|
||||||
|
}.tap do |options|
|
||||||
|
next unless template_body = template_content(email_type)
|
||||||
|
|
||||||
|
options[:body] = template_body
|
||||||
|
options[:content_type] = 'text/html'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def template_content(email_type)
|
||||||
|
template = Gitlab::Template::ServiceDeskTemplate.find(email_type, @project)
|
||||||
|
|
||||||
|
text = substitute_template_replacements(template.content)
|
||||||
|
|
||||||
|
markdown(text, project: @project)
|
||||||
|
rescue Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def substitute_template_replacements(template_body)
|
||||||
|
template_body
|
||||||
|
.gsub(/%\{\s*ISSUE_ID\s*\}/, issue_id)
|
||||||
|
.gsub(/%\{\s*ISSUE_PATH\s*\}/, issue_path)
|
||||||
|
.gsub(/%\{\s*NOTE_TEXT\s*\}/, note_text)
|
||||||
|
end
|
||||||
|
|
||||||
|
def issue_id
|
||||||
|
"#{Issue.reference_prefix}#{@issue.iid}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def issue_path
|
||||||
|
@issue.to_reference(full: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def note_text
|
||||||
|
@note&.note.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def subject_base
|
||||||
|
"#{@issue.title} (##{@issue.iid})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -19,6 +19,7 @@ class Notify < ApplicationMailer
|
||||||
include Emails::Releases
|
include Emails::Releases
|
||||||
include Emails::Groups
|
include Emails::Groups
|
||||||
include Emails::Reviews
|
include Emails::Reviews
|
||||||
|
include Emails::ServiceDesk
|
||||||
|
|
||||||
helper TimeboxesHelper
|
helper TimeboxesHelper
|
||||||
helper MergeRequestsHelper
|
helper MergeRequestsHelper
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,18 @@ class NotifyPreview < ActionMailer::Preview
|
||||||
Notify.unknown_sign_in_email(user, '127.0.0.1', Time.current).message
|
Notify.unknown_sign_in_email(user, '127.0.0.1', Time.current).message
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def service_desk_new_note_email
|
||||||
|
cleanup do
|
||||||
|
note = create_note(noteable_type: 'Issue', noteable_id: issue.id, note: 'Issue note content')
|
||||||
|
|
||||||
|
Notify.service_desk_new_note_email(issue.id, note.id).message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def service_desk_thank_you_email
|
||||||
|
Notify.service_desk_thank_you_email(issue.id).message
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def project
|
def project
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,6 @@ class Event < ApplicationRecord
|
||||||
scope :for_design, -> { where(target_type: 'DesignManagement::Design') }
|
scope :for_design, -> { where(target_type: 'DesignManagement::Design') }
|
||||||
|
|
||||||
# Needed to implement feature flag: can be removed when feature flag is removed
|
# Needed to implement feature flag: can be removed when feature flag is removed
|
||||||
scope :not_wiki_page, -> { where('target_type IS NULL or target_type <> ?', 'WikiPage::Meta') }
|
|
||||||
scope :not_design, -> { where('target_type IS NULL or target_type <> ?', 'DesignManagement::Design') }
|
scope :not_design, -> { where('target_type IS NULL or target_type <> ?', 'DesignManagement::Design') }
|
||||||
|
|
||||||
scope :with_associations, -> do
|
scope :with_associations, -> do
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ class EventCollection
|
||||||
private
|
private
|
||||||
|
|
||||||
def apply_feature_flags(events)
|
def apply_feature_flags(events)
|
||||||
events = events.not_wiki_page unless ::Feature.enabled?(:wiki_events)
|
|
||||||
events = events.not_design unless ::Feature.enabled?(:design_activity_events)
|
events = events.not_design unless ::Feature.enabled?(:design_activity_events)
|
||||||
|
|
||||||
events
|
events
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
module Clusters
|
module Clusters
|
||||||
class ClusterPresenter < Gitlab::View::Presenter::Delegated
|
class ClusterPresenter < Gitlab::View::Presenter::Delegated
|
||||||
|
include ::Gitlab::Utils::StrongMemoize
|
||||||
include ActionView::Helpers::SanitizeHelper
|
include ActionView::Helpers::SanitizeHelper
|
||||||
include ActionView::Helpers::UrlHelper
|
include ActionView::Helpers::UrlHelper
|
||||||
include IconsHelper
|
include IconsHelper
|
||||||
|
|
@ -60,6 +61,12 @@ module Clusters
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def gitlab_managed_apps_logs_path
|
||||||
|
return unless logs_project && can_read_cluster?
|
||||||
|
|
||||||
|
project_logs_path(logs_project, cluster_id: cluster.id)
|
||||||
|
end
|
||||||
|
|
||||||
def read_only_kubernetes_platform_fields?
|
def read_only_kubernetes_platform_fields?
|
||||||
!cluster.provided_by_user?
|
!cluster.provided_by_user?
|
||||||
end
|
end
|
||||||
|
|
@ -85,6 +92,16 @@ module Clusters
|
||||||
ActionController::Base.helpers.image_path(path)
|
ActionController::Base.helpers.image_path(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# currently log explorer is only available in the scope of the project
|
||||||
|
# for group and instance level cluster selected project does not affects
|
||||||
|
# fetching logs from gitlab managed apps namespace, therefore any project
|
||||||
|
# available to user will be sufficient.
|
||||||
|
def logs_project
|
||||||
|
strong_memoize(:logs_project) do
|
||||||
|
cluster.all_projects.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def clusterable
|
def clusterable
|
||||||
if cluster.group_type?
|
if cluster.group_type?
|
||||||
cluster.group
|
cluster.group
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,8 @@ class ClusterEntity < Grape::Entity
|
||||||
expose :path do |cluster|
|
expose :path do |cluster|
|
||||||
Clusters::ClusterPresenter.new(cluster).show_path # rubocop: disable CodeReuse/Presenter
|
Clusters::ClusterPresenter.new(cluster).show_path # rubocop: disable CodeReuse/Presenter
|
||||||
end
|
end
|
||||||
|
|
||||||
|
expose :gitlab_managed_apps_logs_path do |cluster|
|
||||||
|
Clusters::ClusterPresenter.new(cluster, current_user: request.current_user).gitlab_managed_apps_logs_path # rubocop: disable CodeReuse/Presenter
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ class ClusterSerializer < BaseSerializer
|
||||||
:cluster_type,
|
:cluster_type,
|
||||||
:enabled,
|
:enabled,
|
||||||
:environment_scope,
|
:environment_scope,
|
||||||
|
:gitlab_managed_apps_logs_path,
|
||||||
:name,
|
:name,
|
||||||
:nodes,
|
:nodes,
|
||||||
:path,
|
:path,
|
||||||
|
|
|
||||||
|
|
@ -120,8 +120,6 @@ class EventCreateService
|
||||||
#
|
#
|
||||||
# @return a tuple of event and either :found or :created
|
# @return a tuple of event and either :found or :created
|
||||||
def wiki_event(wiki_page_meta, author, action)
|
def wiki_event(wiki_page_meta, author, action)
|
||||||
return unless Feature.enabled?(:wiki_events)
|
|
||||||
|
|
||||||
raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action)
|
raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action)
|
||||||
|
|
||||||
if duplicate = existing_wiki_event(wiki_page_meta, action)
|
if duplicate = existing_wiki_event(wiki_page_meta, action)
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ module Git
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_process_wiki_events?
|
def can_process_wiki_events?
|
||||||
Feature.enabled?(:wiki_events) && Feature.enabled?(:wiki_events_on_git_push, project)
|
Feature.enabled?(:wiki_events_on_git_push, project)
|
||||||
end
|
end
|
||||||
|
|
||||||
def push_changes
|
def push_changes
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,6 @@ module WikiPages
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_wiki_event(page)
|
def create_wiki_event(page)
|
||||||
return unless ::Feature.enabled?(:wiki_events)
|
|
||||||
|
|
||||||
response = WikiPages::EventCreateService.new(current_user).execute(slug_for_page(page), page, event_action)
|
response = WikiPages::EventCreateService.new(current_user).execute(slug_for_page(page), page, event_action)
|
||||||
|
|
||||||
log_error(response.message) if response.error?
|
log_error(response.message) if response.error?
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ module WikiPages
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute(slug, page, action)
|
def execute(slug, page, action)
|
||||||
return ServiceResponse.success(message: 'No event created as `wiki_events` feature is disabled') unless ::Feature.enabled?(:wiki_events)
|
|
||||||
|
|
||||||
event = Event.transaction do
|
event = Event.transaction do
|
||||||
wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)
|
wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
%html{ lang: "en" }
|
||||||
|
%head
|
||||||
|
%meta{ content: "text/html; charset=utf-8", "http-equiv" => "Content-Type" }
|
||||||
|
-# haml-lint:disable NoPlainNodes
|
||||||
|
%title
|
||||||
|
GitLab
|
||||||
|
-# haml-lint:enable NoPlainNodes
|
||||||
|
= stylesheet_link_tag 'notify'
|
||||||
|
= yield :head
|
||||||
|
%body
|
||||||
|
.content
|
||||||
|
= yield
|
||||||
|
.footer{ style: "margin-top: 10px;" }
|
||||||
|
%p
|
||||||
|
—
|
||||||
|
%br
|
||||||
|
= link_to "Unsubscribe", @unsubscribe_url
|
||||||
|
|
||||||
|
-# EE-specific start
|
||||||
|
- if Gitlab::CurrentSettings.email_additional_text.present?
|
||||||
|
%br
|
||||||
|
%br
|
||||||
|
= Gitlab::Utils.nlbr(Gitlab::CurrentSettings.email_additional_text)
|
||||||
|
-# EE-specific end
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
- if Gitlab::CurrentSettings.email_author_in_body
|
||||||
|
%div
|
||||||
|
#{link_to @note.author_name, user_url(@note.author)} wrote:
|
||||||
|
%div
|
||||||
|
= markdown(@note.note, pipeline: :email, author: @note.author)
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
New response for issue #<%= @issue.iid %>:
|
||||||
|
|
||||||
|
Author: <%= sanitize_name(@note.author_name) %>
|
||||||
|
|
||||||
|
<%= @note.note %>
|
||||||
|
<%# EE-specific start %><%= render_if_exists 'layouts/mailer/additional_text'%><%# EE-specific end %>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
%p
|
||||||
|
Thank you for your support request! We are tracking your request as ticket ##{@issue.iid}, and will respond as soon as we can.
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
Thank you for your support request! We are tracking your request as ticket #<%= @issue.iid %>, and will respond as soon as we can.
|
||||||
|
|
||||||
|
To unsubscribe from this issue, please paste the following link into your browser:
|
||||||
|
|
||||||
|
<%= @unsubscribe_url %>
|
||||||
|
<%# EE-specific start %><%= render_if_exists 'layouts/mailer/additional_text' %><%# EE-specific end %>
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
= render_if_exists 'events/epics_filter'
|
= render_if_exists 'events/epics_filter'
|
||||||
- if comments_visible?
|
- if comments_visible?
|
||||||
= event_filter_link EventFilter::COMMENTS, _('Comments'), s_('EventFilterBy|Filter by comments')
|
= event_filter_link EventFilter::COMMENTS, _('Comments'), s_('EventFilterBy|Filter by comments')
|
||||||
- if Feature.enabled?(:wiki_events) && (@project.nil? || @project.has_wiki?)
|
- if @project.nil? || @project.has_wiki?
|
||||||
= event_filter_link EventFilter::WIKI, _('Wiki'), s_('EventFilterBy|Filter by wiki')
|
= event_filter_link EventFilter::WIKI, _('Wiki'), s_('EventFilterBy|Filter by wiki')
|
||||||
- if event_filter_visible(:designs)
|
- if event_filter_visible(:designs)
|
||||||
= event_filter_link EventFilter::DESIGNS, _('Designs'), s_('EventFilterBy|Filter by designs')
|
= event_filter_link EventFilter::DESIGNS, _('Designs'), s_('EventFilterBy|Filter by designs')
|
||||||
|
|
|
||||||
|
|
@ -1660,6 +1660,14 @@
|
||||||
:weight: 2
|
:weight: 2
|
||||||
:idempotent:
|
:idempotent:
|
||||||
:tags: []
|
:tags: []
|
||||||
|
- :name: service_desk_email_receiver
|
||||||
|
:feature_category: :issue_tracking
|
||||||
|
:has_external_dependencies:
|
||||||
|
:urgency: :low
|
||||||
|
:resource_boundary: :unknown
|
||||||
|
:weight: 1
|
||||||
|
:idempotent:
|
||||||
|
:tags: []
|
||||||
- :name: system_hook_push
|
- :name: system_hook_push
|
||||||
:feature_category: :source_code_management
|
:feature_category: :source_code_management
|
||||||
:has_external_dependencies:
|
:has_external_dependencies:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ServiceDeskEmailReceiverWorker < EmailReceiverWorker # rubocop:disable Scalability/IdempotentWorker
|
||||||
|
include ApplicationWorker
|
||||||
|
|
||||||
|
def perform(raw)
|
||||||
|
return unless ::Gitlab::ServiceDeskEmail.enabled?
|
||||||
|
|
||||||
|
begin
|
||||||
|
Gitlab::Email::ServiceDeskReceiver.new(raw).execute
|
||||||
|
rescue => e
|
||||||
|
handle_failure(raw, e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Introduce prepare environment action to annotate non-deployment jobs
|
||||||
|
merge_request: 35642
|
||||||
|
author:
|
||||||
|
type: added
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Enable display of wiki events in activity streams
|
||||||
|
merge_request: 32475
|
||||||
|
author:
|
||||||
|
type: changed
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add docs for Alert trigger test alerts
|
||||||
|
merge_request: 36647
|
||||||
|
author:
|
||||||
|
type: added
|
||||||
|
|
@ -105,6 +105,9 @@ For example, set a value of 15% to enable the feature for 15% of authenticated u
|
||||||
|
|
||||||
The rollout percentage can be from 0% to 100%.
|
The rollout percentage can be from 0% to 100%.
|
||||||
|
|
||||||
|
NOTE: **Note:**
|
||||||
|
Stickiness (consistent application behavior for the same user) is guaranteed for logged-in users, but not anonymous users.
|
||||||
|
|
||||||
CAUTION: **Caution:**
|
CAUTION: **Caution:**
|
||||||
If this strategy is selected, then the Unleash client **must** be given a user
|
If this strategy is selected, then the Unleash client **must** be given a user
|
||||||
ID for the feature to be enabled. See the [Ruby example](#ruby-application-example) below.
|
ID for the feature to be enabled. See the [Ruby example](#ruby-application-example) below.
|
||||||
|
|
@ -120,6 +123,9 @@ activation strategy.
|
||||||
Enter user IDs as a comma-separated list of values. For example,
|
Enter user IDs as a comma-separated list of values. For example,
|
||||||
`user@example.com, user2@example.com`, or `username1,username2,username3`, and so on.
|
`user@example.com, user2@example.com`, or `username1,username2,username3`, and so on.
|
||||||
|
|
||||||
|
NOTE: **Note:**
|
||||||
|
User IDs are identifiers for your application users. They do not need to be GitLab users.
|
||||||
|
|
||||||
CAUTION: **Caution:**
|
CAUTION: **Caution:**
|
||||||
The Unleash client **must** be given a user ID for the feature to be enabled for
|
The Unleash client **must** be given a user ID for the feature to be enabled for
|
||||||
target users. See the [Ruby example](#ruby-application-example) below.
|
target users. See the [Ruby example](#ruby-application-example) below.
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,14 @@ You can find the full documentation for the AsciiDoc syntax at <https://asciidoc
|
||||||
|
|
||||||
### Paragraphs
|
### Paragraphs
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
A normal paragraph.
|
A normal paragraph.
|
||||||
Line breaks are not preserved.
|
Line breaks are not preserved.
|
||||||
```
|
```
|
||||||
|
|
||||||
Line comments, which are lines that start with `//`, are skipped:
|
Line comments, which are lines that start with `//`, are skipped:
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
// this is a comment
|
// this is a comment
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ A blank line separates paragraphs.
|
||||||
|
|
||||||
A paragraph with the `[%hardbreaks]` option will preserve line breaks:
|
A paragraph with the `[%hardbreaks]` option will preserve line breaks:
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
[%hardbreaks]
|
[%hardbreaks]
|
||||||
This paragraph carries the `hardbreaks` option.
|
This paragraph carries the `hardbreaks` option.
|
||||||
Notice how line breaks are now preserved.
|
Notice how line breaks are now preserved.
|
||||||
|
|
@ -35,7 +35,7 @@ An indented (literal) paragraph disables text formatting,
|
||||||
preserves spaces and line breaks, and is displayed in a
|
preserves spaces and line breaks, and is displayed in a
|
||||||
monospaced font:
|
monospaced font:
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
This literal paragraph is indented with one space.
|
This literal paragraph is indented with one space.
|
||||||
As a consequence, *text formatting*, spaces,
|
As a consequence, *text formatting*, spaces,
|
||||||
and lines breaks will be preserved.
|
and lines breaks will be preserved.
|
||||||
|
|
@ -43,7 +43,7 @@ monospaced font:
|
||||||
|
|
||||||
An admonition paragraph grabs the reader's attention:
|
An admonition paragraph grabs the reader's attention:
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
NOTE: This is a brief reference, please read the full documentation at https://asciidoctor.org/docs/.
|
NOTE: This is a brief reference, please read the full documentation at https://asciidoctor.org/docs/.
|
||||||
|
|
||||||
TIP: Lists can be indented. Leading whitespace is not significant.
|
TIP: Lists can be indented. Leading whitespace is not significant.
|
||||||
|
|
@ -53,7 +53,7 @@ TIP: Lists can be indented. Leading whitespace is not significant.
|
||||||
|
|
||||||
**Constrained (applied at word boundaries)**
|
**Constrained (applied at word boundaries)**
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
*strong importance* (aka bold)
|
*strong importance* (aka bold)
|
||||||
_stress emphasis_ (aka italic)
|
_stress emphasis_ (aka italic)
|
||||||
`monospaced` (aka typewriter text)
|
`monospaced` (aka typewriter text)
|
||||||
|
|
@ -64,7 +64,7 @@ _stress emphasis_ (aka italic)
|
||||||
|
|
||||||
**Unconstrained (applied anywhere)**
|
**Unconstrained (applied anywhere)**
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
**C**reate+**R**ead+**U**pdate+**D**elete
|
**C**reate+**R**ead+**U**pdate+**D**elete
|
||||||
fan__freakin__tastic
|
fan__freakin__tastic
|
||||||
``mono``culture
|
``mono``culture
|
||||||
|
|
@ -72,7 +72,7 @@ fan__freakin__tastic
|
||||||
|
|
||||||
**Replacements**
|
**Replacements**
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
A long time ago in a galaxy far, far away...
|
A long time ago in a galaxy far, far away...
|
||||||
(C) 1976 Arty Artisan
|
(C) 1976 Arty Artisan
|
||||||
I believe I shall--no, actually I won't.
|
I believe I shall--no, actually I won't.
|
||||||
|
|
@ -80,7 +80,7 @@ I believe I shall--no, actually I won't.
|
||||||
|
|
||||||
**Macros**
|
**Macros**
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
// where c=specialchars, q=quotes, a=attributes, r=replacements, m=macros, p=post_replacements, etc.
|
// where c=specialchars, q=quotes, a=attributes, r=replacements, m=macros, p=post_replacements, etc.
|
||||||
The European icon:flag[role=blue] is blue & contains pass:[************] arranged in a icon:circle-o[role=yellow].
|
The European icon:flag[role=blue] is blue & contains pass:[************] arranged in a icon:circle-o[role=yellow].
|
||||||
The pass:c[->] operator is often referred to as the stabby lambda.
|
The pass:c[->] operator is often referred to as the stabby lambda.
|
||||||
|
|
@ -93,12 +93,12 @@ stem:[sqrt(4) = 2]
|
||||||
|
|
||||||
**User-defined attributes**
|
**User-defined attributes**
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
// define attributes in the document header
|
// define attributes in the document header
|
||||||
:name: value
|
:name: value
|
||||||
```
|
```
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
:url-gem: https://rubygems.org/gems/asciidoctor
|
:url-gem: https://rubygems.org/gems/asciidoctor
|
||||||
|
|
||||||
You can download and install Asciidoctor {asciidoctor-version} from {url-gem}.
|
You can download and install Asciidoctor {asciidoctor-version} from {url-gem}.
|
||||||
|
|
@ -117,7 +117,7 @@ GitLab sets the following environment attributes:
|
||||||
|
|
||||||
### Links
|
### Links
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
https://example.org/page[A webpage]
|
https://example.org/page[A webpage]
|
||||||
link:../path/to/file.txt[A local file]
|
link:../path/to/file.txt[A local file]
|
||||||
xref:document.adoc[A sibling document]
|
xref:document.adoc[A sibling document]
|
||||||
|
|
@ -126,7 +126,7 @@ mailto:hello@example.org[Email to say hello!]
|
||||||
|
|
||||||
### Anchors
|
### Anchors
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
[[idname,reference text]]
|
[[idname,reference text]]
|
||||||
// or written using normal block attributes as `[#idname,reftext=reference text]`
|
// or written using normal block attributes as `[#idname,reftext=reference text]`
|
||||||
A paragraph (or any block) with an anchor (aka ID) and reftext.
|
A paragraph (or any block) with an anchor (aka ID) and reftext.
|
||||||
|
|
@ -142,7 +142,7 @@ This paragraph has a footnote.footnote:[This is the text of the footnote.]
|
||||||
|
|
||||||
#### Unordered
|
#### Unordered
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
* level 1
|
* level 1
|
||||||
** level 2
|
** level 2
|
||||||
*** level 3
|
*** level 3
|
||||||
|
|
@ -161,7 +161,7 @@ Attach a block or paragraph to a list item using a list continuation (which you
|
||||||
|
|
||||||
#### Ordered
|
#### Ordered
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
. Step 1
|
. Step 1
|
||||||
. Step 2
|
. Step 2
|
||||||
.. Step 2a
|
.. Step 2a
|
||||||
|
|
@ -177,14 +177,14 @@ Attach a block or paragraph to a list item using a list continuation (which you
|
||||||
|
|
||||||
#### Checklist
|
#### Checklist
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
* [x] checked
|
* [x] checked
|
||||||
* [ ] not checked
|
* [ ] not checked
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Callout
|
#### Callout
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
// enable callout bubbles by adding `:icons: font` to the document header
|
// enable callout bubbles by adding `:icons: font` to the document header
|
||||||
[,ruby]
|
[,ruby]
|
||||||
----
|
----
|
||||||
|
|
@ -195,7 +195,7 @@ puts 'Hello, World!' # <1>
|
||||||
|
|
||||||
#### Description
|
#### Description
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
first term:: description of first term
|
first term:: description of first term
|
||||||
second term::
|
second term::
|
||||||
description of second term
|
description of second term
|
||||||
|
|
@ -205,7 +205,7 @@ description of second term
|
||||||
|
|
||||||
#### Header
|
#### Header
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
= Document Title
|
= Document Title
|
||||||
Author Name <author@example.org>
|
Author Name <author@example.org>
|
||||||
v1.0, 2019-01-01
|
v1.0, 2019-01-01
|
||||||
|
|
@ -213,7 +213,7 @@ v1.0, 2019-01-01
|
||||||
|
|
||||||
#### Sections
|
#### Sections
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
= Document Title (Level 0)
|
= Document Title (Level 0)
|
||||||
== Level 1
|
== Level 1
|
||||||
=== Level 2
|
=== Level 2
|
||||||
|
|
@ -225,7 +225,7 @@ v1.0, 2019-01-01
|
||||||
|
|
||||||
#### Includes
|
#### Includes
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
include::basics.adoc[]
|
include::basics.adoc[]
|
||||||
|
|
||||||
// define -a allow-uri-read to allow content to be read from URI
|
// define -a allow-uri-read to allow content to be read from URI
|
||||||
|
|
@ -239,13 +239,13 @@ included, a number that is inclusive of transitive dependencies.
|
||||||
|
|
||||||
### Blocks
|
### Blocks
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
--
|
--
|
||||||
open - a general-purpose content wrapper; useful for enclosing content to attach to a list item
|
open - a general-purpose content wrapper; useful for enclosing content to attach to a list item
|
||||||
--
|
--
|
||||||
```
|
```
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
// recognized types include CAUTION, IMPORTANT, NOTE, TIP, and WARNING
|
// recognized types include CAUTION, IMPORTANT, NOTE, TIP, and WARNING
|
||||||
// enable admonition icons by setting `:icons: font` in the document header
|
// enable admonition icons by setting `:icons: font` in the document header
|
||||||
[NOTE]
|
[NOTE]
|
||||||
|
|
@ -254,13 +254,13 @@ admonition - a notice for the reader, ranging in severity from a tip to an alert
|
||||||
====
|
====
|
||||||
```
|
```
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
====
|
====
|
||||||
example - a demonstration of the concept being documented
|
example - a demonstration of the concept being documented
|
||||||
====
|
====
|
||||||
```
|
```
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
.Toggle Me
|
.Toggle Me
|
||||||
[%collapsible]
|
[%collapsible]
|
||||||
====
|
====
|
||||||
|
|
@ -268,58 +268,58 @@ collapsible - these details are revealed by clicking the title
|
||||||
====
|
====
|
||||||
```
|
```
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
****
|
****
|
||||||
sidebar - auxiliary content that can be read independently of the main content
|
sidebar - auxiliary content that can be read independently of the main content
|
||||||
****
|
****
|
||||||
```
|
```
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
....
|
....
|
||||||
literal - an exhibit that features program output
|
literal - an exhibit that features program output
|
||||||
....
|
....
|
||||||
```
|
```
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
----
|
----
|
||||||
listing - an exhibit that features program input, source code, or the contents of a file
|
listing - an exhibit that features program input, source code, or the contents of a file
|
||||||
----
|
----
|
||||||
```
|
```
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
[,language]
|
[,language]
|
||||||
----
|
----
|
||||||
source - a listing that is embellished with (colorized) syntax highlighting
|
source - a listing that is embellished with (colorized) syntax highlighting
|
||||||
----
|
----
|
||||||
```
|
```
|
||||||
|
|
||||||
````asciidoc
|
````plaintext
|
||||||
\```language
|
\```language
|
||||||
fenced code - a shorthand syntax for the source block
|
fenced code - a shorthand syntax for the source block
|
||||||
\```
|
\```
|
||||||
````
|
````
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
[,attribution,citetitle]
|
[,attribution,citetitle]
|
||||||
____
|
____
|
||||||
quote - a quotation or excerpt; attribution with title of source are optional
|
quote - a quotation or excerpt; attribution with title of source are optional
|
||||||
____
|
____
|
||||||
```
|
```
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
[verse,attribution,citetitle]
|
[verse,attribution,citetitle]
|
||||||
____
|
____
|
||||||
verse - a literary excerpt, often a poem; attribution with title of source are optional
|
verse - a literary excerpt, often a poem; attribution with title of source are optional
|
||||||
____
|
____
|
||||||
```
|
```
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
++++
|
++++
|
||||||
pass - content passed directly to the output document; often raw HTML
|
pass - content passed directly to the output document; often raw HTML
|
||||||
++++
|
++++
|
||||||
```
|
```
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
// activate stem support by adding `:stem:` to the document header
|
// activate stem support by adding `:stem:` to the document header
|
||||||
[stem]
|
[stem]
|
||||||
++++
|
++++
|
||||||
|
|
@ -327,7 +327,7 @@ x = y^2
|
||||||
++++
|
++++
|
||||||
```
|
```
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
////
|
////
|
||||||
comment - content which is not included in the output document
|
comment - content which is not included in the output document
|
||||||
////
|
////
|
||||||
|
|
@ -335,7 +335,7 @@ comment - content which is not included in the output document
|
||||||
|
|
||||||
### Tables
|
### Tables
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
.Table Attributes
|
.Table Attributes
|
||||||
[cols=>1h;2d,width=50%,frame=topbot]
|
[cols=>1h;2d,width=50%,frame=topbot]
|
||||||
|===
|
|===
|
||||||
|
|
@ -366,7 +366,7 @@ comment - content which is not included in the output document
|
||||||
|
|
||||||
### Multimedia
|
### Multimedia
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
image::screenshot.png[block image,800,450]
|
image::screenshot.png[block image,800,450]
|
||||||
|
|
||||||
Press image:reload.svg[reload,16,opts=interactive] to reload the page.
|
Press image:reload.svg[reload,16,opts=interactive] to reload the page.
|
||||||
|
|
@ -380,12 +380,12 @@ video::300817511[vimeo]
|
||||||
|
|
||||||
### Breaks
|
### Breaks
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
// thematic break (aka horizontal rule)
|
// thematic break (aka horizontal rule)
|
||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
```asciidoc
|
```plaintext
|
||||||
// page break
|
// page break
|
||||||
<<<
|
<<<
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,22 @@ Example payload:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Triggering test alerts
|
||||||
|
|
||||||
|
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3066) in GitLab Core in 13.2.
|
||||||
|
|
||||||
|
After a [project maintainer or owner](#setting-up-generic-alerts)
|
||||||
|
[configures generic alerts](#setting-up-generic-alerts), you can trigger a
|
||||||
|
test alert to confirm your integration works properly.
|
||||||
|
|
||||||
|
1. Sign in as a user with Developer or greater [permissions](../../../user/permissions.md).
|
||||||
|
1. Navigate to **{settings}** **Settings > Operations** in your project.
|
||||||
|
1. Click **Alerts endpoint** to expand the section.
|
||||||
|
1. Enter a sample payload in **Alert test payload** (valid JSON is required).
|
||||||
|
1. Click **Test alert payload**.
|
||||||
|
|
||||||
|
GitLab displays an error or success message, depending on the outcome of your test.
|
||||||
|
|
||||||
## Automatic grouping of identical alerts **(PREMIUM)**
|
## Automatic grouping of identical alerts **(PREMIUM)**
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214557) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214557) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ file path fragments to start seeing results.
|
||||||
|
|
||||||
## Syntax highlighting
|
## Syntax highlighting
|
||||||
|
|
||||||
|
> Support for `.gitlab.ci.yml` validation [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218472) in GitLab 13.2.
|
||||||
|
|
||||||
As expected from an IDE, syntax highlighting for many languages within
|
As expected from an IDE, syntax highlighting for many languages within
|
||||||
the Web IDE will make your direct editing even easier.
|
the Web IDE will make your direct editing even easier.
|
||||||
|
|
||||||
|
|
@ -35,6 +37,13 @@ The Web IDE currently provides:
|
||||||
- IntelliSense and validation support (displaying errors and warnings, providing
|
- IntelliSense and validation support (displaying errors and warnings, providing
|
||||||
smart completions, formatting, and outlining) for some languages. For example:
|
smart completions, formatting, and outlining) for some languages. For example:
|
||||||
TypeScript, JavaScript, CSS, LESS, SCSS, JSON, and HTML.
|
TypeScript, JavaScript, CSS, LESS, SCSS, JSON, and HTML.
|
||||||
|
- Validation support for certain JSON and YAML files using schemas based on the
|
||||||
|
[JSON Schema Store](https://www.schemastore.org/json/). This feature
|
||||||
|
is only supported for the `.gitlab-ci.yml` file.
|
||||||
|
|
||||||
|
NOTE: **Note:** Validation support based on schemas is hidden behind
|
||||||
|
the feature flag `:schema_linting` on self-managed installations. To enable the
|
||||||
|
feature, you can [turn on the feature flag in Rails console](../../../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags).
|
||||||
|
|
||||||
Because the Web IDE is based on the [Monaco Editor](https://microsoft.github.io/monaco-editor/),
|
Because the Web IDE is based on the [Monaco Editor](https://microsoft.github.io/monaco-editor/),
|
||||||
you can find a more complete list of supported languages in the
|
you can find a more complete list of supported languages in the
|
||||||
|
|
|
||||||
|
|
@ -154,39 +154,48 @@ Similar to versioned diff file views, you can see the changes made in a given Wi
|
||||||
|
|
||||||
## Wiki activity records
|
## Wiki activity records
|
||||||
|
|
||||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14902) in GitLab 12.10.
|
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14902) in **GitLab 12.10.**
|
||||||
> - It's deployed behind a feature flag, disabled by default.
|
> - Git events were [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216014) in **GitLab 13.0.**
|
||||||
> - It's enabled on GitLab.com.
|
> - It's enabled on GitLab.com.
|
||||||
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-wiki-events-core-only). **(CORE ONLY)**
|
> - Git access activity creation is managed by a feature flag.
|
||||||
|
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-wiki-events-in-git-core-only). **(CORE ONLY)**
|
||||||
|
|
||||||
Wiki events (creation, deletion, and updates) are tracked by GitLab and
|
Wiki events (creation, deletion, and updates) are tracked by GitLab and
|
||||||
displayed on the [user profile](../../profile/index.md#user-profile),
|
displayed on the [user profile](../../profile/index.md#user-profile),
|
||||||
[group](../../group/index.md#view-group-activity),
|
[group](../../group/index.md#view-group-activity),
|
||||||
and [project](../index.md#project-activity) activity pages.
|
and [project](../index.md#project-activity) activity pages.
|
||||||
|
|
||||||
### Limitations
|
### Enable or disable Wiki events in Git **(CORE ONLY)**
|
||||||
|
|
||||||
Only edits made in the browser or through the API have their activity recorded.
|
Tracking wiki events through Git is under development and not ready for production use. It is
|
||||||
Edits made and pushed through Git are not currently listed in the activity list.
|
|
||||||
|
|
||||||
### Enable or disable Wiki Events **(CORE ONLY)**
|
|
||||||
|
|
||||||
Wiki event activity is under development and not ready for production use. It is
|
|
||||||
deployed behind a feature flag that is **disabled by default**.
|
deployed behind a feature flag that is **disabled by default**.
|
||||||
[GitLab administrators with access to the GitLab Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session)
|
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
|
||||||
can enable it for your instance. You're welcome to test it, but use it at your
|
can enable it for your instance.
|
||||||
own risk.
|
|
||||||
|
|
||||||
To enable it:
|
To enable it:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Feature.enable(:wiki_events)
|
Feature.enable(:wiki_events_on_git_push)
|
||||||
|
```
|
||||||
|
|
||||||
|
To enable for just a particular project:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
project = Project.find_by_full_path('your-group/your-project')
|
||||||
|
Feature.enable(:wiki_events_on_git_push, project)
|
||||||
```
|
```
|
||||||
|
|
||||||
To disable it:
|
To disable it:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Feature.disable(:wiki_events)
|
Feature.disable(:wiki_events_on_git_push)
|
||||||
|
```
|
||||||
|
|
||||||
|
To disable for just a particular project:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
project = Project.find_by_full_path('your-group/your-project')
|
||||||
|
Feature.disable(:wiki_events_on_git_push, project)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Adding and editing wiki pages locally
|
## Adding and editing wiki pages locally
|
||||||
|
|
|
||||||
|
|
@ -52,15 +52,12 @@ class EventFilter
|
||||||
private
|
private
|
||||||
|
|
||||||
def apply_feature_flags(events)
|
def apply_feature_flags(events)
|
||||||
events = events.not_wiki_page unless Feature.enabled?(:wiki_events)
|
|
||||||
events = events.not_design unless can_view_design_activity?
|
events = events.not_design unless can_view_design_activity?
|
||||||
|
|
||||||
events
|
events
|
||||||
end
|
end
|
||||||
|
|
||||||
def wiki_events(events)
|
def wiki_events(events)
|
||||||
return events unless Feature.enabled?(:wiki_events)
|
|
||||||
|
|
||||||
events.for_wiki_page
|
events.for_wiki_page
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ module Gitlab
|
||||||
|
|
||||||
validates :action,
|
validates :action,
|
||||||
type: String,
|
type: String,
|
||||||
inclusion: { in: %w[start stop], message: 'should be start or stop' },
|
inclusion: { in: %w[start stop prepare], message: 'should be start, stop or prepare' },
|
||||||
allow_nil: true
|
allow_nil: true
|
||||||
|
|
||||||
validates :on_stop, type: String, allow_nil: true
|
validates :on_stop, type: String, allow_nil: true
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ module Gitlab
|
||||||
CreateNoteHandler,
|
CreateNoteHandler,
|
||||||
CreateIssueHandler,
|
CreateIssueHandler,
|
||||||
UnsubscribeHandler,
|
UnsubscribeHandler,
|
||||||
CreateMergeRequestHandler
|
CreateMergeRequestHandler,
|
||||||
|
ServiceDeskHandler
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -25,5 +26,3 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Gitlab::Email::Handler.prepend_if_ee('::EE::Gitlab::Email::Handler')
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,11 @@ module Gitlab
|
||||||
|
|
||||||
def process_message(**kwargs)
|
def process_message(**kwargs)
|
||||||
message = ReplyParser.new(mail, **kwargs).execute.strip
|
message = ReplyParser.new(mail, **kwargs).execute.strip
|
||||||
add_attachments(message)
|
message_with_attachments = add_attachments(message)
|
||||||
|
|
||||||
|
# Support bot is specifically forbidden
|
||||||
|
# from using slash commands.
|
||||||
|
strip_quick_actions(message_with_attachments)
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_attachments(reply)
|
def add_attachments(reply)
|
||||||
|
|
@ -82,6 +86,15 @@ module Gitlab
|
||||||
def valid_project_slug?(found_project)
|
def valid_project_slug?(found_project)
|
||||||
project_slug == found_project.full_path_slug
|
project_slug == found_project.full_path_slug
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def strip_quick_actions(content)
|
||||||
|
return content unless author.support_bot?
|
||||||
|
|
||||||
|
command_definitions = ::QuickActions::InterpretService.command_definitions
|
||||||
|
extractor = ::Gitlab::QuickActions::Extractor.new(command_definitions)
|
||||||
|
|
||||||
|
extractor.redact_commands(content)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# handles service desk issue creation emails with these formats:
|
||||||
|
# incoming+gitlab-org-gitlab-ce-20-issue-@incoming.gitlab.com
|
||||||
|
# incoming+gitlab-org/gitlab-ce@incoming.gitlab.com (legacy)
|
||||||
|
module Gitlab
|
||||||
|
module Email
|
||||||
|
module Handler
|
||||||
|
class ServiceDeskHandler < BaseHandler
|
||||||
|
include ReplyProcessing
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
|
HANDLER_REGEX = /\A#{HANDLER_ACTION_BASE_REGEX}-issue-\z/.freeze
|
||||||
|
HANDLER_REGEX_LEGACY = /\A(?<project_path>[^\+]*)\z/.freeze
|
||||||
|
PROJECT_KEY_PATTERN = /\A(?<slug>.+)-(?<key>[a-z0-9_]+)\z/.freeze
|
||||||
|
|
||||||
|
def initialize(mail, mail_key, service_desk_key: nil)
|
||||||
|
super(mail, mail_key)
|
||||||
|
|
||||||
|
if service_desk_key.present?
|
||||||
|
@service_desk_key = service_desk_key
|
||||||
|
elsif !mail_key&.include?('/') && (matched = HANDLER_REGEX.match(mail_key.to_s))
|
||||||
|
@project_slug = matched[:project_slug]
|
||||||
|
@project_id = matched[:project_id]&.to_i
|
||||||
|
elsif matched = HANDLER_REGEX_LEGACY.match(mail_key.to_s)
|
||||||
|
@project_path = matched[:project_path]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_handle?
|
||||||
|
Gitlab::ServiceDesk.supported? && (project_id || can_handle_legacy_format? || service_desk_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
raise ProjectNotFound if project.nil?
|
||||||
|
|
||||||
|
create_issue!
|
||||||
|
send_thank_you_email! if from_address
|
||||||
|
end
|
||||||
|
|
||||||
|
def metrics_params
|
||||||
|
super.merge(project: project&.full_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def metrics_event
|
||||||
|
:receive_email_service_desk
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :project_id, :project_path, :service_desk_key
|
||||||
|
|
||||||
|
def project
|
||||||
|
strong_memoize(:project) do
|
||||||
|
@project = service_desk_key ? project_from_key : super
|
||||||
|
@project = nil unless @project&.service_desk_enabled?
|
||||||
|
@project
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def project_from_key
|
||||||
|
return unless match = service_desk_key.match(PROJECT_KEY_PATTERN)
|
||||||
|
|
||||||
|
project = Project.find_by_service_desk_project_key(match[:key])
|
||||||
|
return unless valid_project_key?(project, match[:slug])
|
||||||
|
|
||||||
|
project
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_project_key?(project, slug)
|
||||||
|
project.present? && slug == project.full_path_slug && Feature.enabled?(:service_desk_custom_address, project)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_issue!
|
||||||
|
@issue = Issues::CreateService.new(
|
||||||
|
project,
|
||||||
|
User.support_bot,
|
||||||
|
title: issue_title,
|
||||||
|
description: message_including_template,
|
||||||
|
confidential: true,
|
||||||
|
service_desk_reply_to: from_address
|
||||||
|
).execute
|
||||||
|
|
||||||
|
raise InvalidIssueError unless @issue.persisted?
|
||||||
|
|
||||||
|
if service_desk_setting&.issue_template_missing?
|
||||||
|
create_template_not_found_note(@issue)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_thank_you_email!
|
||||||
|
Notify.service_desk_thank_you_email(@issue.id).deliver_later!
|
||||||
|
end
|
||||||
|
|
||||||
|
def message_including_template
|
||||||
|
description = message_including_reply
|
||||||
|
template_content = service_desk_setting&.issue_template_content
|
||||||
|
|
||||||
|
if template_content.present?
|
||||||
|
description += " \n" + template_content
|
||||||
|
end
|
||||||
|
|
||||||
|
description
|
||||||
|
end
|
||||||
|
|
||||||
|
def service_desk_setting
|
||||||
|
strong_memoize(:service_desk_setting) do
|
||||||
|
project.service_desk_setting
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_template_not_found_note(issue)
|
||||||
|
issue_template_key = service_desk_setting&.issue_template_key
|
||||||
|
|
||||||
|
warning_note = <<-MD.strip_heredoc
|
||||||
|
WARNING: The template file #{issue_template_key}.md used for service desk issues is empty or could not be found.
|
||||||
|
Please check service desk settings and update the file to be used.
|
||||||
|
MD
|
||||||
|
|
||||||
|
note_params = {
|
||||||
|
noteable: issue,
|
||||||
|
note: warning_note
|
||||||
|
}
|
||||||
|
|
||||||
|
::Notes::CreateService.new(
|
||||||
|
project,
|
||||||
|
User.support_bot,
|
||||||
|
note_params
|
||||||
|
).execute
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_address
|
||||||
|
(mail.reply_to || []).first || mail.from.first || mail.sender
|
||||||
|
end
|
||||||
|
|
||||||
|
def issue_title
|
||||||
|
from = "(from #{from_address})" if from_address
|
||||||
|
|
||||||
|
"Service Desk #{from}: #{mail.subject}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_handle_legacy_format?
|
||||||
|
project_path && project_path.include?('/') && !mail_key.include?('+')
|
||||||
|
end
|
||||||
|
|
||||||
|
def author
|
||||||
|
User.support_bot
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module Email
|
||||||
|
class ServiceDeskReceiver < Receiver
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_handler(mail)
|
||||||
|
key = service_desk_key(mail)
|
||||||
|
return unless key
|
||||||
|
|
||||||
|
Gitlab::Email::Handler::ServiceDeskHandler.new(mail, nil, service_desk_key: key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def service_desk_key(mail)
|
||||||
|
mail.to.find do |address|
|
||||||
|
key = ::Gitlab::ServiceDeskEmail.key_from_address(address)
|
||||||
|
break key if key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -42,8 +42,8 @@
|
||||||
"@babel/plugin-syntax-import-meta": "^7.10.1",
|
"@babel/plugin-syntax-import-meta": "^7.10.1",
|
||||||
"@babel/preset-env": "^7.10.1",
|
"@babel/preset-env": "^7.10.1",
|
||||||
"@gitlab/at.js": "1.5.5",
|
"@gitlab/at.js": "1.5.5",
|
||||||
"@gitlab/svgs": "1.151.0",
|
"@gitlab/svgs": "1.152.0",
|
||||||
"@gitlab/ui": "17.22.1",
|
"@gitlab/ui": "17.26.0",
|
||||||
"@gitlab/visual-review-tools": "1.6.1",
|
"@gitlab/visual-review-tools": "1.6.1",
|
||||||
"@rails/actioncable": "^6.0.3-1",
|
"@rails/actioncable": "^6.0.3-1",
|
||||||
"@sentry/browser": "^5.10.2",
|
"@sentry/browser": "^5.10.2",
|
||||||
|
|
|
||||||
|
|
@ -66,31 +66,15 @@ RSpec.describe EventsFinder do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'wiki events feature flag' do
|
describe 'wiki events' do
|
||||||
let_it_be(:events) { create_list(:wiki_page_event, 3, project: public_project) }
|
let_it_be(:events) { create_list(:wiki_page_event, 3, project: public_project) }
|
||||||
|
|
||||||
subject(:finder) { described_class.new(source: public_project, target_type: 'wiki', current_user: user) }
|
subject(:finder) { described_class.new(source: public_project, target_type: 'wiki', current_user: user) }
|
||||||
|
|
||||||
context 'the wiki_events feature flag is disabled' do
|
|
||||||
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
|
it 'can find the wiki events' do
|
||||||
expect(finder.execute).to match_array(events)
|
expect(finder.execute).to match_array(events)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
context 'dashboard events' do
|
context 'dashboard events' do
|
||||||
before do
|
before do
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
Return-Path: <jake@adventuretime.ooo>
|
||||||
|
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
|
||||||
|
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
|
||||||
|
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+email-test-project_id-issue-@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
|
||||||
|
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
|
||||||
|
Date: Thu, 13 Jun 2013 17:03:48 -0400
|
||||||
|
From: Jake the Dog <jake@adventuretime.ooo>
|
||||||
|
To: incoming+email-test-project_id-issue-@appmail.adventuretime.ooo
|
||||||
|
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
|
||||||
|
Subject: The message subject! @all
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: text/plain;
|
||||||
|
charset=ISO-8859-1
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
X-Sieve: CMU Sieve 2.2
|
||||||
|
X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
|
||||||
|
13 Jun 2013 14:03:48 -0700 (PDT)
|
||||||
|
X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
|
||||||
|
|
||||||
|
Service desk stuff!
|
||||||
|
|
||||||
|
```
|
||||||
|
a = b
|
||||||
|
```
|
||||||
|
|
||||||
|
/label ~label1
|
||||||
|
/assign @user1
|
||||||
|
/close
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
Return-Path: <jake@adventuretime.ooo>
|
||||||
|
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
|
||||||
|
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <support+project_slug-project_key@example.com>; Thu, 13 Jun 2013 14:03:48 -0700
|
||||||
|
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
|
||||||
|
Date: Thu, 13 Jun 2013 17:03:48 -0400
|
||||||
|
From: Jake the Dog <jake@adventuretime.ooo>
|
||||||
|
To: support+project_slug-project_key@example.com
|
||||||
|
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
|
||||||
|
Subject: The message subject! @all
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: text/plain;
|
||||||
|
charset=ISO-8859-1
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
X-Sieve: CMU Sieve 2.2
|
||||||
|
X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
|
||||||
|
13 Jun 2013 14:03:48 -0700 (PDT)
|
||||||
|
X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
|
||||||
|
|
||||||
|
Service desk stuff!
|
||||||
|
|
||||||
|
```
|
||||||
|
a = b
|
||||||
|
```
|
||||||
|
|
||||||
|
/label ~label1
|
||||||
|
/assign @user1
|
||||||
|
/close
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
Delivered-To: incoming+email-test-project_id-issue-@appmail.adventuretime.ooo
|
||||||
|
Return-Path: <jake@adventuretime.ooo>
|
||||||
|
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
|
||||||
|
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
|
||||||
|
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+email-test-project_id-issue-@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
|
||||||
|
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
|
||||||
|
Date: Thu, 13 Jun 2013 17:03:48 -0400
|
||||||
|
From: Jake the Dog <jake@adventuretime.ooo>
|
||||||
|
To: support@adventuretime.ooo
|
||||||
|
Delivered-To: support@adventuretime.ooo
|
||||||
|
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
|
||||||
|
Subject: The message subject! @all
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: text/plain;
|
||||||
|
charset=ISO-8859-1
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
X-Sieve: CMU Sieve 2.2
|
||||||
|
X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
|
||||||
|
13 Jun 2013 14:03:48 -0700 (PDT)
|
||||||
|
X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
|
||||||
|
|
||||||
|
Service desk stuff!
|
||||||
|
|
||||||
|
```
|
||||||
|
a = b
|
||||||
|
```
|
||||||
|
|
||||||
|
/label ~label1
|
||||||
|
/assign @user1
|
||||||
|
/close
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
Delivered-To: incoming+email-test-project_id-issue-@appmail.adventuretime.ooo
|
||||||
|
Return-Path: <jake@adventuretime.ooo>
|
||||||
|
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
|
||||||
|
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
|
||||||
|
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+email-test-project_id-issue-@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
|
||||||
|
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
|
||||||
|
Date: Thu, 13 Jun 2013 17:03:48 -0400
|
||||||
|
From: Jake the Dog <jake@adventuretime.ooo>
|
||||||
|
To: support@adventuretime.ooo
|
||||||
|
Delivered-To: support@adventuretime.ooo
|
||||||
|
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
|
||||||
|
Subject: The message subject! @all
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: text/plain;
|
||||||
|
charset=ISO-8859-1
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
X-Sieve: CMU Sieve 2.2
|
||||||
|
X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
|
||||||
|
13 Jun 2013 14:03:48 -0700 (PDT)
|
||||||
|
X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
|
||||||
|
|
||||||
|
Service desk stuff!
|
||||||
|
|
||||||
|
---------- Forwarded message ---------
|
||||||
|
From: Jake the Dog <jake@adventuretime.ooo>
|
||||||
|
To: <jake@adventuretime.ooo>
|
||||||
|
|
||||||
|
|
||||||
|
forwarded content
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
Return-Path: <jake@adventuretime.ooo>
|
||||||
|
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
|
||||||
|
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
|
||||||
|
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
|
||||||
|
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
|
||||||
|
Date: Thu, 13 Jun 2013 17:03:48 -0400
|
||||||
|
From: Jake the Dog <jake@adventuretime.ooo>
|
||||||
|
To: incoming+email/test@appmail.adventuretime.ooo
|
||||||
|
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
|
||||||
|
Subject: The message subject! @all
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: text/plain;
|
||||||
|
charset=ISO-8859-1
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
X-Sieve: CMU Sieve 2.2
|
||||||
|
X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
|
||||||
|
13 Jun 2013 14:03:48 -0700 (PDT)
|
||||||
|
X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
|
||||||
|
|
||||||
|
Service desk stuff!
|
||||||
|
|
||||||
|
```
|
||||||
|
a = b
|
||||||
|
```
|
||||||
|
|
||||||
|
/label ~label1
|
||||||
|
/assign @user1
|
||||||
|
/close
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
Delivered-To: incoming+email-test-project_id-issue-@appmail.adventuretime.ooo
|
||||||
|
Return-Path: <jake@adventuretime.ooo>
|
||||||
|
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
|
||||||
|
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
|
||||||
|
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+email-test-project_id-issue-@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
|
||||||
|
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
|
||||||
|
Date: Thu, 13 Jun 2013 17:03:48 -0400
|
||||||
|
From: Finn the Human <finn@adventuretime.ooo>
|
||||||
|
Sender: Jake the Dog <jake@adventuretime.ooo>
|
||||||
|
To: support@adventuretime.ooo
|
||||||
|
Delivered-To: support@adventuretime.ooo
|
||||||
|
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
|
||||||
|
Subject: The message subject! @all
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: text/plain;
|
||||||
|
charset=ISO-8859-1
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
X-Sieve: CMU Sieve 2.2
|
||||||
|
X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
|
||||||
|
13 Jun 2013 14:03:48 -0700 (PDT)
|
||||||
|
X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
|
||||||
|
|
||||||
|
Service desk stuff!
|
||||||
|
|
||||||
|
```
|
||||||
|
a = b
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
Return-Path: <jake@adventuretime.ooo>
|
||||||
|
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
|
||||||
|
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
|
||||||
|
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
|
||||||
|
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
|
||||||
|
Date: Thu, 13 Jun 2013 17:03:48 -0400
|
||||||
|
From: Jake the Dog <jake@adventuretime.ooo>
|
||||||
|
To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
|
||||||
|
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
|
||||||
|
In-Reply-To: <issue_1@localhost>
|
||||||
|
References: <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost> <issue_1@localhost>
|
||||||
|
Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: text/plain;
|
||||||
|
charset=ISO-8859-1
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
X-Sieve: CMU Sieve 2.2
|
||||||
|
X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
|
||||||
|
13 Jun 2013 14:03:48 -0700 (PDT)
|
||||||
|
X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
|
||||||
|
|
||||||
|
I could not disagree more. I am obviously biased but adventure time is the
|
||||||
|
greatest show ever created. Everyone should watch it.
|
||||||
|
|
||||||
|
- Jake out
|
||||||
|
|
||||||
|
/close
|
||||||
|
/title test
|
||||||
|
|
||||||
|
|
||||||
|
On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
|
||||||
|
<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
|
||||||
|
>
|
||||||
|
>
|
||||||
|
>
|
||||||
|
> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
|
||||||
|
>
|
||||||
|
> ---
|
||||||
|
> hey guys everyone knows adventure time sucks!
|
||||||
|
>
|
||||||
|
> ---
|
||||||
|
> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
|
||||||
|
>
|
||||||
|
> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
|
||||||
|
>
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
export const headIssues = [
|
||||||
|
{
|
||||||
|
check_name: 'Rubocop/Lint/UselessAssignment',
|
||||||
|
description: 'Insecure Dependency',
|
||||||
|
location: {
|
||||||
|
path: 'lib/six.rb',
|
||||||
|
lines: {
|
||||||
|
begin: 6,
|
||||||
|
end: 7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fingerprint: 'e879dd9bbc0953cad5037cde7ff0f627',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
categories: ['Security'],
|
||||||
|
check_name: 'Insecure Dependency',
|
||||||
|
description: 'Insecure Dependency',
|
||||||
|
location: {
|
||||||
|
path: 'Gemfile.lock',
|
||||||
|
lines: {
|
||||||
|
begin: 22,
|
||||||
|
end: 22,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const mockParsedHeadIssues = [
|
||||||
|
{
|
||||||
|
...headIssues[1],
|
||||||
|
name: 'Insecure Dependency',
|
||||||
|
path: 'lib/six.rb',
|
||||||
|
urlPath: 'headPath/lib/six.rb#L6',
|
||||||
|
line: 6,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const baseIssues = [
|
||||||
|
{
|
||||||
|
categories: ['Security'],
|
||||||
|
check_name: 'Insecure Dependency',
|
||||||
|
description: 'Insecure Dependency',
|
||||||
|
location: {
|
||||||
|
path: 'Gemfile.lock',
|
||||||
|
lines: {
|
||||||
|
begin: 22,
|
||||||
|
end: 22,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
categories: ['Security'],
|
||||||
|
check_name: 'Insecure Dependency',
|
||||||
|
description: 'Insecure Dependency',
|
||||||
|
location: {
|
||||||
|
path: 'Gemfile.lock',
|
||||||
|
lines: {
|
||||||
|
begin: 21,
|
||||||
|
end: 21,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fingerprint: 'ca2354534dee94ae60ba2f54e3857c50e5',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const mockParsedBaseIssues = [
|
||||||
|
{
|
||||||
|
...baseIssues[1],
|
||||||
|
name: 'Insecure Dependency',
|
||||||
|
path: 'Gemfile.lock',
|
||||||
|
line: 21,
|
||||||
|
urlPath: 'basePath/Gemfile.lock#L21',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const issueDiff = [
|
||||||
|
{
|
||||||
|
categories: ['Security'],
|
||||||
|
check_name: 'Insecure Dependency',
|
||||||
|
description: 'Insecure Dependency',
|
||||||
|
fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5',
|
||||||
|
line: 6,
|
||||||
|
location: { lines: { begin: 22, end: 22 }, path: 'Gemfile.lock' },
|
||||||
|
name: 'Insecure Dependency',
|
||||||
|
path: 'lib/six.rb',
|
||||||
|
urlPath: 'headPath/lib/six.rb#L6',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
import axios from '~/lib/utils/axios_utils';
|
||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
import * as actions from '~/reports/codequality_report/store/actions';
|
||||||
|
import * as types from '~/reports/codequality_report/store/mutation_types';
|
||||||
|
import createStore from '~/reports/codequality_report/store';
|
||||||
|
import { TEST_HOST } from 'spec/test_constants';
|
||||||
|
import testAction from 'helpers/vuex_action_helper';
|
||||||
|
import { headIssues, baseIssues, mockParsedHeadIssues, mockParsedBaseIssues } from '../mock_data';
|
||||||
|
|
||||||
|
// mock codequality comparison worker
|
||||||
|
jest.mock('~/reports/codequality_report/workers/codequality_comparison_worker', () =>
|
||||||
|
jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
addEventListener: (eventName, callback) => {
|
||||||
|
callback({
|
||||||
|
data: {
|
||||||
|
newIssues: [mockParsedHeadIssues[0]],
|
||||||
|
resolvedIssues: [mockParsedBaseIssues[0]],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('Codequality Reports actions', () => {
|
||||||
|
let localState;
|
||||||
|
let localStore;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localStore = createStore();
|
||||||
|
localState = localStore.state;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setPaths', () => {
|
||||||
|
it('should commit SET_PATHS mutation', done => {
|
||||||
|
const paths = {
|
||||||
|
basePath: 'basePath',
|
||||||
|
headPath: 'headPath',
|
||||||
|
baseBlobPath: 'baseBlobPath',
|
||||||
|
headBlobPath: 'headBlobPath',
|
||||||
|
helpPath: 'codequalityHelpPath',
|
||||||
|
};
|
||||||
|
|
||||||
|
testAction(
|
||||||
|
actions.setPaths,
|
||||||
|
paths,
|
||||||
|
localState,
|
||||||
|
[{ type: types.SET_PATHS, payload: paths }],
|
||||||
|
[],
|
||||||
|
done,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchReports', () => {
|
||||||
|
let mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localState.headPath = `${TEST_HOST}/head.json`;
|
||||||
|
localState.basePath = `${TEST_HOST}/base.json`;
|
||||||
|
mock = new MockAdapter(axios);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on success', () => {
|
||||||
|
it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', done => {
|
||||||
|
mock.onGet(`${TEST_HOST}/head.json`).reply(200, headIssues);
|
||||||
|
mock.onGet(`${TEST_HOST}/base.json`).reply(200, baseIssues);
|
||||||
|
|
||||||
|
testAction(
|
||||||
|
actions.fetchReports,
|
||||||
|
null,
|
||||||
|
localState,
|
||||||
|
[{ type: types.REQUEST_REPORTS }],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
payload: {
|
||||||
|
newIssues: [mockParsedHeadIssues[0]],
|
||||||
|
resolvedIssues: [mockParsedBaseIssues[0]],
|
||||||
|
},
|
||||||
|
type: 'receiveReportsSuccess',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
done,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on error', () => {
|
||||||
|
it('commits REQUEST_REPORTS and dispatches receiveReportsError', done => {
|
||||||
|
mock.onGet(`${TEST_HOST}/head.json`).reply(500);
|
||||||
|
|
||||||
|
testAction(
|
||||||
|
actions.fetchReports,
|
||||||
|
null,
|
||||||
|
localState,
|
||||||
|
[{ type: types.REQUEST_REPORTS }],
|
||||||
|
[{ type: 'receiveReportsError' }],
|
||||||
|
done,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with no base path', () => {
|
||||||
|
it('commits REQUEST_REPORTS and dispatches receiveReportsError', done => {
|
||||||
|
localState.basePath = null;
|
||||||
|
|
||||||
|
testAction(
|
||||||
|
actions.fetchReports,
|
||||||
|
null,
|
||||||
|
localState,
|
||||||
|
[{ type: types.REQUEST_REPORTS }],
|
||||||
|
[{ type: 'receiveReportsError' }],
|
||||||
|
done,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('receiveReportsSuccess', () => {
|
||||||
|
it('commits RECEIVE_REPORTS_SUCCESS', done => {
|
||||||
|
const data = { issues: [] };
|
||||||
|
|
||||||
|
testAction(
|
||||||
|
actions.receiveReportsSuccess,
|
||||||
|
data,
|
||||||
|
localState,
|
||||||
|
[{ type: types.RECEIVE_REPORTS_SUCCESS, payload: data }],
|
||||||
|
[],
|
||||||
|
done,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('receiveReportsError', () => {
|
||||||
|
it('commits RECEIVE_REPORTS_ERROR', done => {
|
||||||
|
testAction(
|
||||||
|
actions.receiveReportsError,
|
||||||
|
null,
|
||||||
|
localState,
|
||||||
|
[{ type: types.RECEIVE_REPORTS_ERROR }],
|
||||||
|
[],
|
||||||
|
done,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import * as getters from '~/reports/codequality_report/store/getters';
|
||||||
|
import createStore from '~/reports/codequality_report/store';
|
||||||
|
import { LOADING, ERROR, SUCCESS } from '~/reports/constants';
|
||||||
|
|
||||||
|
describe('Codequality reports store getters', () => {
|
||||||
|
let localState;
|
||||||
|
let localStore;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localStore = createStore();
|
||||||
|
localState = localStore.state;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasCodequalityIssues', () => {
|
||||||
|
describe('when there are issues', () => {
|
||||||
|
it('returns true', () => {
|
||||||
|
localState.newIssues = [{ reason: 'repetitive code' }];
|
||||||
|
localState.resolvedIssues = [];
|
||||||
|
|
||||||
|
expect(getters.hasCodequalityIssues(localState)).toEqual(true);
|
||||||
|
|
||||||
|
localState.newIssues = [];
|
||||||
|
localState.resolvedIssues = [{ reason: 'repetitive code' }];
|
||||||
|
|
||||||
|
expect(getters.hasCodequalityIssues(localState)).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when there are no issues', () => {
|
||||||
|
it('returns false when there are no issues', () => {
|
||||||
|
expect(getters.hasCodequalityIssues(localState)).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('codequalityStatus', () => {
|
||||||
|
describe('when loading', () => {
|
||||||
|
it('returns loading status', () => {
|
||||||
|
localState.isLoading = true;
|
||||||
|
|
||||||
|
expect(getters.codequalityStatus(localState)).toEqual(LOADING);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on error', () => {
|
||||||
|
it('returns error status', () => {
|
||||||
|
localState.hasError = true;
|
||||||
|
|
||||||
|
expect(getters.codequalityStatus(localState)).toEqual(ERROR);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when successfully loaded', () => {
|
||||||
|
it('returns error status', () => {
|
||||||
|
expect(getters.codequalityStatus(localState)).toEqual(SUCCESS);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('codequalityText', () => {
|
||||||
|
it.each`
|
||||||
|
resolvedIssues | newIssues | expectedText
|
||||||
|
${0} | ${0} | ${'No changes to code quality'}
|
||||||
|
${0} | ${1} | ${'Code quality degraded on 1 point'}
|
||||||
|
${2} | ${0} | ${'Code quality improved on 2 points'}
|
||||||
|
${1} | ${2} | ${'Code quality improved on 1 point and degraded on 2 points'}
|
||||||
|
`(
|
||||||
|
'returns a summary containing $resolvedIssues resolved issues and $newIssues new issues',
|
||||||
|
({ newIssues, resolvedIssues, expectedText }) => {
|
||||||
|
localState.newIssues = new Array(newIssues).fill({ reason: 'Repetitive code' });
|
||||||
|
localState.resolvedIssues = new Array(resolvedIssues).fill({ reason: 'Repetitive code' });
|
||||||
|
|
||||||
|
expect(getters.codequalityText(localState)).toEqual(expectedText);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('codequalityPopover', () => {
|
||||||
|
describe('when head report is available but base report is not', () => {
|
||||||
|
it('returns a popover with a documentation link', () => {
|
||||||
|
localState.headPath = 'head.json';
|
||||||
|
localState.basePath = undefined;
|
||||||
|
localState.helpPath = 'codequality_help.html';
|
||||||
|
|
||||||
|
expect(getters.codequalityPopover(localState).title).toEqual(
|
||||||
|
'Base pipeline codequality artifact not found',
|
||||||
|
);
|
||||||
|
expect(getters.codequalityPopover(localState).content).toContain(
|
||||||
|
'Learn more about codequality reports',
|
||||||
|
'href="codequality_help.html"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import mutations from '~/reports/codequality_report/store/mutations';
|
||||||
|
import createStore from '~/reports/codequality_report/store';
|
||||||
|
|
||||||
|
describe('Codequality Reports mutations', () => {
|
||||||
|
let localState;
|
||||||
|
let localStore;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localStore = createStore();
|
||||||
|
localState = localStore.state;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SET_PATHS', () => {
|
||||||
|
it('sets paths to given values', () => {
|
||||||
|
const basePath = 'base.json';
|
||||||
|
const headPath = 'head.json';
|
||||||
|
const baseBlobPath = 'base/blob/path/';
|
||||||
|
const headBlobPath = 'head/blob/path/';
|
||||||
|
const helpPath = 'help.html';
|
||||||
|
|
||||||
|
mutations.SET_PATHS(localState, {
|
||||||
|
basePath,
|
||||||
|
headPath,
|
||||||
|
baseBlobPath,
|
||||||
|
headBlobPath,
|
||||||
|
helpPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(localState.basePath).toEqual(basePath);
|
||||||
|
expect(localState.headPath).toEqual(headPath);
|
||||||
|
expect(localState.baseBlobPath).toEqual(baseBlobPath);
|
||||||
|
expect(localState.headBlobPath).toEqual(headBlobPath);
|
||||||
|
expect(localState.helpPath).toEqual(helpPath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('REQUEST_REPORTS', () => {
|
||||||
|
it('sets isLoading to true', () => {
|
||||||
|
mutations.REQUEST_REPORTS(localState);
|
||||||
|
|
||||||
|
expect(localState.isLoading).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('RECEIVE_REPORTS_SUCCESS', () => {
|
||||||
|
it('sets isLoading to false', () => {
|
||||||
|
mutations.RECEIVE_REPORTS_SUCCESS(localState, {});
|
||||||
|
|
||||||
|
expect(localState.isLoading).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets hasError to false', () => {
|
||||||
|
mutations.RECEIVE_REPORTS_SUCCESS(localState, {});
|
||||||
|
|
||||||
|
expect(localState.hasError).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets newIssues and resolvedIssues from response data', () => {
|
||||||
|
const data = { newIssues: [{ id: 1 }], resolvedIssues: [{ id: 2 }] };
|
||||||
|
mutations.RECEIVE_REPORTS_SUCCESS(localState, data);
|
||||||
|
|
||||||
|
expect(localState.newIssues).toEqual(data.newIssues);
|
||||||
|
expect(localState.resolvedIssues).toEqual(data.resolvedIssues);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('RECEIVE_REPORTS_ERROR', () => {
|
||||||
|
it('sets isLoading to false', () => {
|
||||||
|
mutations.RECEIVE_REPORTS_ERROR(localState);
|
||||||
|
|
||||||
|
expect(localState.isLoading).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets hasError to true', () => {
|
||||||
|
mutations.RECEIVE_REPORTS_ERROR(localState);
|
||||||
|
|
||||||
|
expect(localState.hasError).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
import {
|
||||||
|
parseCodeclimateMetrics,
|
||||||
|
doCodeClimateComparison,
|
||||||
|
} from '~/reports/codequality_report/store/utils/codequality_comparison';
|
||||||
|
import { baseIssues, mockParsedHeadIssues, mockParsedBaseIssues } from '../../mock_data';
|
||||||
|
|
||||||
|
jest.mock('~/reports/codequality_report/workers/codequality_comparison_worker', () => {
|
||||||
|
let mockPostMessageCallback;
|
||||||
|
return jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
addEventListener: (_, callback) => {
|
||||||
|
mockPostMessageCallback = callback;
|
||||||
|
},
|
||||||
|
postMessage: data => {
|
||||||
|
if (!data.headIssues) return mockPostMessageCallback({ data: {} });
|
||||||
|
if (!data.baseIssues) throw new Error();
|
||||||
|
const key = 'fingerprint';
|
||||||
|
return mockPostMessageCallback({
|
||||||
|
data: {
|
||||||
|
newIssues: data.headIssues.filter(
|
||||||
|
item => !data.baseIssues.find(el => el[key] === item[key]),
|
||||||
|
),
|
||||||
|
resolvedIssues: data.baseIssues.filter(
|
||||||
|
item => !data.headIssues.find(el => el[key] === item[key]),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Codequality report store utils', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
describe('parseCodeclimateMetrics', () => {
|
||||||
|
it('should parse the received issues', () => {
|
||||||
|
[result] = parseCodeclimateMetrics(baseIssues, 'path');
|
||||||
|
|
||||||
|
expect(result.name).toEqual(baseIssues[0].check_name);
|
||||||
|
expect(result.path).toEqual(baseIssues[0].location.path);
|
||||||
|
expect(result.line).toEqual(baseIssues[0].location.lines.begin);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when an issue has no location or path', () => {
|
||||||
|
const issue = { description: 'Insecure Dependency' };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
[result] = parseCodeclimateMetrics([issue], 'path');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is parsed', () => {
|
||||||
|
expect(result.name).toEqual(issue.description);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when an issue has a path but no line', () => {
|
||||||
|
const issue = { description: 'Insecure Dependency', location: { path: 'Gemfile.lock' } };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
[result] = parseCodeclimateMetrics([issue], 'path');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is parsed', () => {
|
||||||
|
expect(result.name).toEqual(issue.description);
|
||||||
|
expect(result.path).toEqual(issue.location.path);
|
||||||
|
expect(result.urlPath).toEqual(`path/${issue.location.path}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when an issue has a line nested in positions', () => {
|
||||||
|
const issue = {
|
||||||
|
description: 'Insecure Dependency',
|
||||||
|
location: {
|
||||||
|
path: 'Gemfile.lock',
|
||||||
|
positions: { begin: { line: 84 } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
[result] = parseCodeclimateMetrics([issue], 'path');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is parsed', () => {
|
||||||
|
expect(result.name).toEqual(issue.description);
|
||||||
|
expect(result.path).toEqual(issue.location.path);
|
||||||
|
expect(result.urlPath).toEqual(
|
||||||
|
`path/${issue.location.path}#L${issue.location.positions.begin.line}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with an empty issue array', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
result = parseCodeclimateMetrics([], 'path');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty array', () => {
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('doCodeClimateComparison', () => {
|
||||||
|
describe('when the comparison worker finds changed issues', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
result = await doCodeClimateComparison(mockParsedHeadIssues, mockParsedBaseIssues);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the new and resolved issues', () => {
|
||||||
|
expect(result.resolvedIssues[0]).toEqual(mockParsedBaseIssues[0]);
|
||||||
|
expect(result.newIssues[0]).toEqual(mockParsedHeadIssues[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the comparison worker finds no changed issues', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
result = await doCodeClimateComparison([], []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the empty issue arrays', () => {
|
||||||
|
expect(result.newIssues).toEqual([]);
|
||||||
|
expect(result.resolvedIssues).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the comparison worker is given malformed data', () => {
|
||||||
|
it('rejects the promise', () => {
|
||||||
|
return expect(doCodeClimateComparison(null)).rejects.toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the comparison worker encounters an error', () => {
|
||||||
|
it('rejects the promise and throws an error', () => {
|
||||||
|
return expect(doCodeClimateComparison([], null)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -114,4 +114,18 @@ RSpec.describe EnvironmentsHelper do
|
||||||
expect(subject).to eq(true)
|
expect(subject).to eq(true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#environment_logs_data' do
|
||||||
|
it 'returns logs data' do
|
||||||
|
expected_data = {
|
||||||
|
"environment-name": environment.name,
|
||||||
|
"environments-path": project_environments_path(project, format: :json),
|
||||||
|
"environment-id": environment.id,
|
||||||
|
"cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack'),
|
||||||
|
"clusters-path": project_clusters_path(project, format: :json)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(helper.environment_logs_data(project, environment)).to eq(expected_data)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -80,16 +80,6 @@ RSpec.describe EventFilter do
|
||||||
it 'returns all events' do
|
it 'returns all events' do
|
||||||
expect(filtered_events).to eq(Event.all)
|
expect(filtered_events).to eq(Event.all)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'the :wiki_events filter is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(wiki_events: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not return wiki events' do
|
|
||||||
expect(filtered_events).to eq(Event.not_wiki_page)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with the "design" filter' do
|
context 'with the "design" filter' do
|
||||||
|
|
@ -116,16 +106,6 @@ RSpec.describe EventFilter do
|
||||||
it 'returns only wiki page events' do
|
it 'returns only wiki page events' do
|
||||||
expect(filtered_events).to contain_exactly(wiki_page_event, wiki_page_update_event)
|
expect(filtered_events).to contain_exactly(wiki_page_event, wiki_page_update_event)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'the :wiki_events filter is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(wiki_events: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not return wiki events' do
|
|
||||||
expect(filtered_events).not_to include(wiki_page_event, wiki_page_update_event)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with an unknown filter' do
|
context 'with an unknown filter' do
|
||||||
|
|
@ -134,16 +114,6 @@ RSpec.describe EventFilter do
|
||||||
it 'returns all events' do
|
it 'returns all events' do
|
||||||
expect(filtered_events).to eq(Event.all)
|
expect(filtered_events).to eq(Event.all)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'the :wiki_events filter is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(wiki_events: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not return wiki events' do
|
|
||||||
expect(filtered_events).to eq(Event.not_wiki_page)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a nil filter' do
|
context 'with a nil filter' do
|
||||||
|
|
@ -152,16 +122,6 @@ RSpec.describe EventFilter do
|
||||||
it 'returns all events' do
|
it 'returns all events' do
|
||||||
expect(filtered_events).to eq(Event.all)
|
expect(filtered_events).to eq(Event.all)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'the :wiki_events filter is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(wiki_events: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not return wiki events' do
|
|
||||||
expect(filtered_events).to eq(Event.not_wiki_page)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,17 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when prepare action is used' do
|
||||||
|
let(:config) do
|
||||||
|
{ name: 'production',
|
||||||
|
action: 'prepare' }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is valid' do
|
||||||
|
expect(entry).to be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when wrong action type is used' do
|
context 'when wrong action type is used' do
|
||||||
let(:config) do
|
let(:config) do
|
||||||
{ name: 'production',
|
{ name: 'production',
|
||||||
|
|
@ -137,7 +148,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do
|
||||||
describe '#errors' do
|
describe '#errors' do
|
||||||
it 'contains error about invalid action' do
|
it 'contains error about invalid action' do
|
||||||
expect(entry.errors)
|
expect(entry.errors)
|
||||||
.to include 'environment action should be start or stop'
|
.to include 'environment action should be start, stop or prepare'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,19 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Deployment do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when job has environment attribute with prepare action' do
|
||||||
|
let(:attributes) do
|
||||||
|
{
|
||||||
|
environment: 'production',
|
||||||
|
options: { environment: { name: 'production', action: 'prepare' } }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nothing' do
|
||||||
|
is_expected.to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when job does not have environment attribute' do
|
context 'when job does not have environment attribute' do
|
||||||
let(:attributes) { { name: 'test' } }
|
let(:attributes) { { name: 'test' } }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -242,4 +242,70 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do
|
||||||
|
|
||||||
it_behaves_like 'a reply to existing comment'
|
it_behaves_like 'a reply to existing comment'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the service desk' do
|
||||||
|
let(:project) { create(:project, :public, service_desk_enabled: true) }
|
||||||
|
let(:support_bot) { User.support_bot }
|
||||||
|
let(:noteable) { create(:issue, project: project, author: support_bot, title: 'service desk issue') }
|
||||||
|
let(:note) { create(:note, project: project, noteable: noteable) }
|
||||||
|
let(:email_raw) { fixture_file('emails/valid_reply_with_quick_actions.eml') }
|
||||||
|
|
||||||
|
let!(:sent_notification) do
|
||||||
|
SentNotification.record_note(note, support_bot.id, mail_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'is enabled' do
|
||||||
|
before do
|
||||||
|
allow(Gitlab::ServiceDesk).to receive(:enabled?).with(project: project).and_return(true)
|
||||||
|
project.project_feature.update!(issues_access_level: issues_access_level)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when issues are enabled for everyone' do
|
||||||
|
let(:issues_access_level) { ProjectFeature::ENABLED }
|
||||||
|
|
||||||
|
it 'creates a comment' do
|
||||||
|
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when quick actions are present' do
|
||||||
|
it 'encloses quick actions with code span markdown' do
|
||||||
|
receiver.execute
|
||||||
|
noteable.reload
|
||||||
|
|
||||||
|
note = Note.last
|
||||||
|
expect(note.note).to include("Jake out\n\n`/close`\n`/title test`")
|
||||||
|
expect(noteable.title).to eq('service desk issue')
|
||||||
|
expect(noteable).to be_opened
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when issues are protected members only' do
|
||||||
|
let(:issues_access_level) { ProjectFeature::PRIVATE }
|
||||||
|
|
||||||
|
it 'creates a comment' do
|
||||||
|
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when issues are disabled' do
|
||||||
|
let(:issues_access_level) { ProjectFeature::DISABLED }
|
||||||
|
|
||||||
|
it 'does not create a comment' do
|
||||||
|
expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotAuthorizedError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'is disabled' do
|
||||||
|
before do
|
||||||
|
allow(Gitlab::ServiceDesk).to receive(:enabled?).and_return(false)
|
||||||
|
allow(Gitlab::ServiceDesk).to receive(:enabled?).with(project: project).and_return(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a comment' do
|
||||||
|
expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,311 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
|
||||||
|
include_context :email_shared_context
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
|
||||||
|
stub_config_setting(host: 'localhost')
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:email_raw) { email_fixture('emails/service_desk.eml') }
|
||||||
|
let_it_be(:namespace) { create(:namespace, name: "email") }
|
||||||
|
let(:expected_description) do
|
||||||
|
"Service desk stuff!\n\n```\na = b\n```\n\n`/label ~label1`\n`/assign @user1`\n`/close`\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'service desk is enabled for the project' do
|
||||||
|
let_it_be(:project) { create(:project, :repository, :public, namespace: namespace, path: 'test', service_desk_enabled: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Gitlab::ServiceDesk).to receive(:supported?).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'a new issue request' do
|
||||||
|
before do
|
||||||
|
setup_attachment
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a new issue' do
|
||||||
|
expect { receiver.execute }.to change { Issue.count }.by(1)
|
||||||
|
|
||||||
|
new_issue = Issue.last
|
||||||
|
|
||||||
|
expect(new_issue.author).to eql(User.support_bot)
|
||||||
|
expect(new_issue.confidential?).to be true
|
||||||
|
expect(new_issue.all_references.all).to be_empty
|
||||||
|
expect(new_issue.title).to eq("Service Desk (from jake@adventuretime.ooo): The message subject! @all")
|
||||||
|
expect(new_issue.description).to eq(expected_description.strip)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends thank you email' do
|
||||||
|
expect { receiver.execute }.to have_enqueued_job.on_queue('mailers')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when everything is fine' do
|
||||||
|
it_behaves_like 'a new issue request'
|
||||||
|
|
||||||
|
context 'with legacy incoming email address' do
|
||||||
|
let(:email_raw) { fixture_file('emails/service_desk_legacy.eml') }
|
||||||
|
|
||||||
|
it_behaves_like 'a new issue request'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when using issue templates' do
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
setup_attachment
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and template is present' do
|
||||||
|
let_it_be(:settings) { create(:service_desk_setting, project: project) }
|
||||||
|
|
||||||
|
def set_template_file(file_name, content)
|
||||||
|
file_path = ".gitlab/issue_templates/#{file_name}.md"
|
||||||
|
project.repository.create_file(user, file_path, content, message: 'message', branch_name: 'master')
|
||||||
|
settings.update!(issue_template_key: file_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'appends template text to issue description' do
|
||||||
|
set_template_file('service_desk', 'text from template')
|
||||||
|
|
||||||
|
receiver.execute
|
||||||
|
|
||||||
|
issue_description = Issue.last.description
|
||||||
|
expect(issue_description).to include(expected_description)
|
||||||
|
expect(issue_description.lines.last).to eq('text from template')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when quick actions are present' do
|
||||||
|
let(:label) { create(:label, project: project, title: 'label1') }
|
||||||
|
let(:milestone) { create(:milestone, project: project) }
|
||||||
|
let!(:user) { create(:user, username: 'user1') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'applies quick action commands present on templates' do
|
||||||
|
file_content = %(Text from template \n/label ~#{label.title} \n/milestone %"#{milestone.name}"")
|
||||||
|
set_template_file('with_slash_commands', file_content)
|
||||||
|
|
||||||
|
receiver.execute
|
||||||
|
|
||||||
|
issue = Issue.last
|
||||||
|
expect(issue.description).to include('Text from template')
|
||||||
|
expect(issue.label_ids).to include(label.id)
|
||||||
|
expect(issue.milestone).to eq(milestone)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'redacts quick actions present on user email body' do
|
||||||
|
set_template_file('service_desk1', 'text from template')
|
||||||
|
|
||||||
|
receiver.execute
|
||||||
|
|
||||||
|
issue = Issue.last
|
||||||
|
expect(issue).to be_opened
|
||||||
|
expect(issue.description).to include('`/label ~label1`')
|
||||||
|
expect(issue.description).to include('`/assign @user1`')
|
||||||
|
expect(issue.description).to include('`/close`')
|
||||||
|
expect(issue.assignees).to be_empty
|
||||||
|
expect(issue.milestone).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'and template cannot be found' do
|
||||||
|
before do
|
||||||
|
service = ServiceDeskSetting.new(project_id: project.id, issue_template_key: 'unknown')
|
||||||
|
service.save!(validate: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not append template text to issue description' do
|
||||||
|
receiver.execute
|
||||||
|
|
||||||
|
new_issue = Issue.last
|
||||||
|
|
||||||
|
expect(new_issue.description).to eq(expected_description.strip)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates support bot note on issue' do
|
||||||
|
receiver.execute
|
||||||
|
|
||||||
|
note = Note.last
|
||||||
|
|
||||||
|
expect(note.note).to include("WARNING: The template file unknown.md used for service desk issues is empty or could not be found.")
|
||||||
|
expect(note.author).to eq(User.support_bot)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not send warning note email' do
|
||||||
|
ActionMailer::Base.deliveries = []
|
||||||
|
|
||||||
|
perform_enqueued_jobs do
|
||||||
|
expect { receiver.execute }.to change { ActionMailer::Base.deliveries.size }.by(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Only sends created issue email
|
||||||
|
expect(ActionMailer::Base.deliveries.last.text_part.body).to include("Thank you for your support request!")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when using service desk key' do
|
||||||
|
let_it_be(:service_desk_settings) { create(:service_desk_setting, project: project, project_key: 'mykey') }
|
||||||
|
let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml') }
|
||||||
|
let(:receiver) { Gitlab::Email::ServiceDeskReceiver.new(email_raw) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_service_desk_email_setting(enabled: true, address: 'support+%{key}@example.com')
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'a new issue request'
|
||||||
|
|
||||||
|
context 'when there is no project with the key' do
|
||||||
|
let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', key: 'some_key') }
|
||||||
|
|
||||||
|
it 'bounces the email' do
|
||||||
|
expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the project slug does not match' do
|
||||||
|
let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', slug: 'some-slug') }
|
||||||
|
|
||||||
|
it 'bounces the email' do
|
||||||
|
expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when service_desk_custom_address feature is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(service_desk_custom_address: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'bounces the email' do
|
||||||
|
expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#can_handle?' do
|
||||||
|
let(:mail) { Mail::Message.new(email_raw) }
|
||||||
|
|
||||||
|
it 'handles the new email key format' do
|
||||||
|
handler = described_class.new(mail, "h5bp-html5-boilerplate-#{project.project_id}-issue-")
|
||||||
|
|
||||||
|
expect(handler.instance_variable_get(:@project_id).to_i).to eq project.project_id
|
||||||
|
expect(handler.can_handle?).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles the legacy email key format' do
|
||||||
|
handler = described_class.new(mail, "h5bp/html5-boilerplate")
|
||||||
|
|
||||||
|
expect(handler.instance_variable_get(:@project_path)).to eq 'h5bp/html5-boilerplate'
|
||||||
|
expect(handler.can_handle?).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't handle invalid email key" do
|
||||||
|
handler = described_class.new(mail, "h5bp-html5-boilerplate-invalid")
|
||||||
|
|
||||||
|
expect(handler.can_handle?).to be_falsey
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is no from address' do
|
||||||
|
before do
|
||||||
|
allow_next_instance_of(described_class) do |instance|
|
||||||
|
allow(instance).to receive(:from_address).and_return(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "creates a new issue" do
|
||||||
|
expect { receiver.execute }.to change { Issue.count }.by(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not send thank you email' do
|
||||||
|
expect { receiver.execute }.not_to have_enqueued_job.on_queue('mailers')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is a sender address and a from address' do
|
||||||
|
let(:email_raw) { email_fixture('emails/service_desk_sender_and_from.eml') }
|
||||||
|
|
||||||
|
it 'prefers the from address' do
|
||||||
|
setup_attachment
|
||||||
|
|
||||||
|
expect { receiver.execute }.to change { Issue.count }.by(1)
|
||||||
|
|
||||||
|
new_issue = Issue.last
|
||||||
|
|
||||||
|
expect(new_issue.service_desk_reply_to).to eq('finn@adventuretime.ooo')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when service desk is not enabled for project' do
|
||||||
|
before do
|
||||||
|
allow(Gitlab::ServiceDesk).to receive(:enabled?).and_return(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create an issue' do
|
||||||
|
expect { receiver.execute rescue nil }.not_to change { Issue.count }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not send thank you email' do
|
||||||
|
expect { receiver.execute rescue nil }.not_to have_enqueued_job.on_queue('mailers')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the email is forwarded through an alias' do
|
||||||
|
let(:email_raw) { email_fixture('emails/service_desk_forwarded.eml') }
|
||||||
|
|
||||||
|
it_behaves_like 'a new issue request'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the email is forwarded' do
|
||||||
|
let(:email_raw) { email_fixture('emails/service_desk_forwarded_new_issue.eml') }
|
||||||
|
|
||||||
|
it_behaves_like 'a new issue request' do
|
||||||
|
let(:expected_description) do
|
||||||
|
<<~EOF
|
||||||
|
Service desk stuff!
|
||||||
|
|
||||||
|
---------- Forwarded message ---------
|
||||||
|
From: Jake the Dog <jake@adventuretime.ooo>
|
||||||
|
To: <jake@adventuretime.ooo>
|
||||||
|
|
||||||
|
|
||||||
|
forwarded content
|
||||||
|
|
||||||
|

|
||||||
|
EOF
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'service desk is disabled for the project' do
|
||||||
|
let(:project) { create(:project, :public, namespace: namespace, path: 'test', service_desk_enabled: false) }
|
||||||
|
|
||||||
|
it 'bounces the email' do
|
||||||
|
expect { receiver.execute }.to raise_error(Gitlab::Email::ProcessingError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't create an issue" do
|
||||||
|
expect { receiver.execute rescue nil }.not_to change { Issue.count }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def email_fixture(path)
|
||||||
|
fixture_file(path).gsub('project_id', project.project_id.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
def service_desk_fixture(path, slug: nil, key: 'mykey')
|
||||||
|
slug ||= project.full_path_slug.to_s
|
||||||
|
fixture_file(path).gsub('project_slug', slug).gsub('project_key', key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -33,12 +33,40 @@ RSpec.describe Gitlab::Email::Handler do
|
||||||
it 'returns nil if provided email is nil' do
|
it 'returns nil if provided email is nil' do
|
||||||
expect(described_class.for(nil, '')).to be_nil
|
expect(described_class.for(nil, '')).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'new issue email' do
|
||||||
|
def handler_for(fixture, mail_key)
|
||||||
|
described_class.for(fixture_file(fixture), mail_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
|
||||||
|
stub_config_setting(host: 'localhost')
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:user) { create(:user, email: 'jake@adventuretime.ooo', incoming_email_token: 'auth_token') }
|
||||||
|
|
||||||
|
context 'a Service Desk email' do
|
||||||
|
it 'uses the Service Desk handler' do
|
||||||
|
expect(handler_for('emails/service_desk.eml', 'some/project')).to be_instance_of(Gitlab::Email::Handler::ServiceDeskHandler)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'return new issue handler' do
|
||||||
|
expect(handler_for('emails/valid_new_issue.eml', 'some/project+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'regexps are set properly' do
|
describe 'regexps are set properly' do
|
||||||
let(:addresses) do
|
let(:addresses) do
|
||||||
%W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX} sent_notification_key path-to-project-123-user_email_token-merge-request path-to-project-123-user_email_token-issue) +
|
%W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX} sent_notification_key path-to-project-123-user_email_token-merge-request) +
|
||||||
%W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY} sent_notification_key path/to/project+merge-request+user_email_token path/to/project+user_email_token)
|
%W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY} sent_notification_key path-to-project-123-user_email_token-issue) +
|
||||||
|
%w(path/to/project+user_email_token path/to/project+merge-request+user_email_token some/project)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Gitlab::ServiceDesk).to receive(:supported?).and_return(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'picks each handler at least once' do
|
it 'picks each handler at least once' do
|
||||||
|
|
@ -46,12 +74,12 @@ RSpec.describe Gitlab::Email::Handler do
|
||||||
described_class.for(email, address).class
|
described_class.for(email, address).class
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(matched_handlers.uniq).to match_array(ce_handlers)
|
expect(matched_handlers.uniq).to match_array(Gitlab::Email::Handler.handlers)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'can pick exactly one handler for each address' do
|
it 'can pick exactly one handler for each address' do
|
||||||
addresses.each do |address|
|
addresses.each do |address|
|
||||||
matched_handlers = ce_handlers.select do |handler|
|
matched_handlers = Gitlab::Email::Handler.handlers.select do |handler|
|
||||||
handler.new(email, address).can_handle?
|
handler.new(email, address).can_handle?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -59,10 +87,4 @@ RSpec.describe Gitlab::Email::Handler do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def ce_handlers
|
|
||||||
@ce_handlers ||= Gitlab::Email::Handler.handlers.reject do |handler|
|
|
||||||
handler.name.start_with?('Gitlab::Email::Handler::EE::')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::Email::ServiceDeskReceiver do
|
||||||
|
let(:email) { fixture_file('emails/service_desk_custom_address.eml') }
|
||||||
|
let(:receiver) { described_class.new(email) }
|
||||||
|
|
||||||
|
context 'when the email contains a valid email address' do
|
||||||
|
before do
|
||||||
|
stub_service_desk_email_setting(enabled: true, address: 'support+%{key}@example.com')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'finds the service desk key' do
|
||||||
|
handler = double(execute: true, metrics_event: true, metrics_params: true)
|
||||||
|
expected_params = [
|
||||||
|
an_instance_of(Mail::Message), nil,
|
||||||
|
{ service_desk_key: 'project_slug-project_key' }
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(Gitlab::Email::Handler::ServiceDeskHandler)
|
||||||
|
.to receive(:new).with(*expected_params).and_return(handler)
|
||||||
|
|
||||||
|
receiver.execute
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the email does not contain a valid email address' do
|
||||||
|
before do
|
||||||
|
stub_service_desk_email_setting(enabled: true, address: 'other_support+%{key}@example.com')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises an error' do
|
||||||
|
expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'fast_spec_helper'
|
require 'fast_spec_helper'
|
||||||
require 'support/helpers/fixture_helpers'
|
|
||||||
|
|
||||||
RSpec.describe Sentry::PaginationParser do
|
RSpec.describe Sentry::PaginationParser do
|
||||||
include FixtureHelpers
|
|
||||||
|
|
||||||
describe '.parse' do
|
describe '.parse' do
|
||||||
subject { described_class.parse(headers) }
|
subject { described_class.parse(headers) }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'email_spec'
|
||||||
|
|
||||||
|
RSpec.describe Emails::ServiceDesk do
|
||||||
|
include EmailSpec::Helpers
|
||||||
|
include EmailSpec::Matchers
|
||||||
|
include EmailHelpers
|
||||||
|
|
||||||
|
include_context 'gitlab email notification'
|
||||||
|
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:project) { create(:project) }
|
||||||
|
let_it_be(:issue) { create(:issue, project: project) }
|
||||||
|
let(:template) { double(content: template_content) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_const('ServiceEmailClass', Class.new(ApplicationMailer))
|
||||||
|
|
||||||
|
ServiceEmailClass.class_eval do
|
||||||
|
include GitlabRoutingHelper
|
||||||
|
include EmailsHelper
|
||||||
|
include Emails::ServiceDesk
|
||||||
|
|
||||||
|
helper GitlabRoutingHelper
|
||||||
|
helper EmailsHelper
|
||||||
|
|
||||||
|
# this method is implemented in Notify class, we don't need to test it
|
||||||
|
def reply_key
|
||||||
|
'test-key'
|
||||||
|
end
|
||||||
|
|
||||||
|
# this method is implemented in Notify class, we don't need to test it
|
||||||
|
def sender(author_id, params = {})
|
||||||
|
author_id
|
||||||
|
end
|
||||||
|
|
||||||
|
# this method is implemented in Notify class
|
||||||
|
#
|
||||||
|
# We do not need to test the Notify method, it is already tested in notify_spec
|
||||||
|
def mail_new_thread(issue, options)
|
||||||
|
# we need to rewrite this in order to look up templates in the correct directory
|
||||||
|
self.class.mailer_name = 'notify'
|
||||||
|
|
||||||
|
# this is needed for default layout
|
||||||
|
@unsubscribe_url = 'http://unsubscribe.example.com'
|
||||||
|
|
||||||
|
mail(options)
|
||||||
|
end
|
||||||
|
alias_method :mail_answer_thread, :mail_new_thread
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'handle template content' do |template_key|
|
||||||
|
before do
|
||||||
|
expect(Gitlab::Template::ServiceDeskTemplate).to receive(:find)
|
||||||
|
.with(template_key, issue.project)
|
||||||
|
.and_return(template)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'builds the email correctly' do
|
||||||
|
aggregate_failures do
|
||||||
|
is_expected.to have_referable_subject(issue, include_project: false, reply: reply_in_subject)
|
||||||
|
is_expected.to have_body_text(expected_body)
|
||||||
|
expect(subject.content_type).to include('text/html')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'read template from repository' do |template_key|
|
||||||
|
let(:template_content) { 'custom text' }
|
||||||
|
let(:issue) { create(:issue, project: project)}
|
||||||
|
|
||||||
|
context 'when a template is in the repository' do
|
||||||
|
let(:project) { create(:project, :custom_repo, files: { ".gitlab/service_desk_templates/#{template_key}.md" => template_content }) }
|
||||||
|
|
||||||
|
it 'uses the text template from the template' do
|
||||||
|
is_expected.to have_body_text(template_content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the service_desk_templates directory does not contain correct template' do
|
||||||
|
let(:project) { create(:project, :custom_repo, files: { ".gitlab/service_desk_templates/another_file.md" => template_content }) }
|
||||||
|
|
||||||
|
it 'uses the default template' do
|
||||||
|
is_expected.to have_body_text(default_text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the service_desk_templates directory does not exist' do
|
||||||
|
let(:project) { create(:project, :custom_repo, files: { "other_directory/another_file.md" => template_content }) }
|
||||||
|
|
||||||
|
it 'uses the default template' do
|
||||||
|
is_expected.to have_body_text(default_text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the project does not have a repo' do
|
||||||
|
let(:project) { create(:project) }
|
||||||
|
|
||||||
|
it 'uses the default template' do
|
||||||
|
is_expected.to have_body_text(default_text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.service_desk_thank_you_email' do
|
||||||
|
let_it_be(:reply_in_subject) { true }
|
||||||
|
let_it_be(:default_text) do
|
||||||
|
"Thank you for your support request! We are tracking your request as ticket #{issue.to_reference}, and will respond as soon as we can."
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { ServiceEmailClass.service_desk_thank_you_email(issue.id) }
|
||||||
|
|
||||||
|
it_behaves_like 'read template from repository', 'thank_you'
|
||||||
|
|
||||||
|
context 'handling template markdown' do
|
||||||
|
context 'with a simple text' do
|
||||||
|
let(:template_content) { 'thank you, **your new issue** has been created.' }
|
||||||
|
let(:expected_body) { 'thank you, <strong>your new issue</strong> has been created.' }
|
||||||
|
|
||||||
|
it_behaves_like 'handle template content', 'thank_you'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an issue id and issue path placeholders' do
|
||||||
|
let(:template_content) { 'thank you, **your new issue:** %{ISSUE_ID}, path: %{ISSUE_PATH}' }
|
||||||
|
let(:expected_body) { "thank you, <strong>your new issue:</strong> ##{issue.iid}, path: #{project.full_path}##{issue.iid}" }
|
||||||
|
|
||||||
|
it_behaves_like 'handle template content', 'thank_you'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an issue id placeholder with whitespace' do
|
||||||
|
let(:template_content) { 'thank you, **your new issue:** %{ ISSUE_ID}' }
|
||||||
|
let(:expected_body) { "thank you, <strong>your new issue:</strong> ##{issue.iid}" }
|
||||||
|
|
||||||
|
it_behaves_like 'handle template content', 'thank_you'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with unexpected placeholder' do
|
||||||
|
let(:template_content) { 'thank you, **your new issue:** %{this is issue}' }
|
||||||
|
let(:expected_body) { "thank you, <strong>your new issue:</strong> %{this is issue}" }
|
||||||
|
|
||||||
|
it_behaves_like 'handle template content', 'thank_you'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.service_desk_new_note_email' do
|
||||||
|
let_it_be(:reply_in_subject) { false }
|
||||||
|
let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project) }
|
||||||
|
let_it_be(:default_text) { note.note }
|
||||||
|
|
||||||
|
subject { ServiceEmailClass.service_desk_new_note_email(issue.id, note.id) }
|
||||||
|
|
||||||
|
it_behaves_like 'read template from repository', 'new_note'
|
||||||
|
|
||||||
|
context 'handling template markdown' do
|
||||||
|
context 'with a simple text' do
|
||||||
|
let(:template_content) { 'thank you, **new note on issue** has been created.' }
|
||||||
|
let(:expected_body) { 'thank you, <strong>new note on issue</strong> has been created.' }
|
||||||
|
|
||||||
|
it_behaves_like 'handle template content', 'new_note'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an issue id, issue path and note placeholders' do
|
||||||
|
let(:template_content) { 'thank you, **new note on issue:** %{ISSUE_ID}, path: %{ISSUE_PATH}: %{NOTE_TEXT}' }
|
||||||
|
let(:expected_body) { "thank you, <strong>new note on issue:</strong> ##{issue.iid}, path: #{project.full_path}##{issue.iid}: #{note.note}" }
|
||||||
|
|
||||||
|
it_behaves_like 'handle template content', 'new_note'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an issue id placeholder with whitespace' do
|
||||||
|
let(:template_content) { 'thank you, **new note on issue:** %{ ISSUE_ID}: %{ NOTE_TEXT }' }
|
||||||
|
let(:expected_body) { "thank you, <strong>new note on issue:</strong> ##{issue.iid}: #{note.note}" }
|
||||||
|
|
||||||
|
it_behaves_like 'handle template content', 'new_note'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with unexpected placeholder' do
|
||||||
|
let(:template_content) { 'thank you, **new note on issue:** %{this is issue}' }
|
||||||
|
let(:expected_body) { "thank you, <strong>new note on issue:</strong> %{this is issue}" }
|
||||||
|
|
||||||
|
it_behaves_like 'handle template content', 'new_note'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1253,6 +1253,78 @@ RSpec.describe Notify do
|
||||||
it_behaves_like 'appearance header and footer not enabled'
|
it_behaves_like 'appearance header and footer not enabled'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'for service desk issues' do
|
||||||
|
before do
|
||||||
|
issue.update!(service_desk_reply_to: 'service.desk@example.com')
|
||||||
|
end
|
||||||
|
|
||||||
|
def expect_sender(username)
|
||||||
|
sender = subject.header[:from].addrs[0]
|
||||||
|
expect(sender.display_name).to eq(username)
|
||||||
|
expect(sender.address).to eq(gitlab_sender)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'thank you email' do
|
||||||
|
subject { described_class.service_desk_thank_you_email(issue.id) }
|
||||||
|
|
||||||
|
it_behaves_like 'an unsubscribeable thread'
|
||||||
|
|
||||||
|
it 'has the correct recipient' do
|
||||||
|
is_expected.to deliver_to('service.desk@example.com')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has the correct subject and body' do
|
||||||
|
aggregate_failures do
|
||||||
|
is_expected.to have_referable_subject(issue, include_project: false, reply: true)
|
||||||
|
is_expected.to have_body_text("Thank you for your support request! We are tracking your request as ticket #{issue.to_reference}, and will respond as soon as we can.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses service bot name by default' do
|
||||||
|
expect_sender(User.support_bot.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when custom outgoing name is set' do
|
||||||
|
let_it_be(:settings) { create(:service_desk_setting, project: project, outgoing_name: 'some custom name') }
|
||||||
|
|
||||||
|
it 'uses custom name in "from" header' do
|
||||||
|
expect_sender('some custom name')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when custom outgoing name is empty' do
|
||||||
|
let_it_be(:settings) { create(:service_desk_setting, project: project, outgoing_name: '') }
|
||||||
|
|
||||||
|
it 'uses service bot name' do
|
||||||
|
expect_sender(User.support_bot.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'new note email' do
|
||||||
|
let_it_be(:first_note) { create(:discussion_note_on_issue, note: 'Hello world') }
|
||||||
|
|
||||||
|
subject { described_class.service_desk_new_note_email(issue.id, first_note.id) }
|
||||||
|
|
||||||
|
it_behaves_like 'an unsubscribeable thread'
|
||||||
|
|
||||||
|
it 'has the correct recipient' do
|
||||||
|
is_expected.to deliver_to('service.desk@example.com')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses author\'s name in "from" header' do
|
||||||
|
expect_sender(first_note.author.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has the correct subject and body' do
|
||||||
|
aggregate_failures do
|
||||||
|
is_expected.to have_referable_subject(issue, include_project: false, reply: true)
|
||||||
|
is_expected.to have_body_text(first_note.note)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for a group' do
|
context 'for a group' do
|
||||||
|
|
|
||||||
|
|
@ -44,22 +44,10 @@ RSpec.describe EventCollection do
|
||||||
expect(events).to match_array(most_recent_20_events)
|
expect(events).to match_array(most_recent_20_events)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'the wiki_events feature flag is disabled' do
|
it 'includes the wiki page events when using to_a' do
|
||||||
before do
|
|
||||||
stub_feature_flags(wiki_events: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'omits the wiki page events when using to_a' do
|
|
||||||
events = described_class.new(projects).to_a
|
events = described_class.new(projects).to_a
|
||||||
|
|
||||||
expect(events).not_to include(wiki_page_event)
|
expect(events).to include(wiki_page_event)
|
||||||
end
|
|
||||||
|
|
||||||
it 'omits the wiki page events when using all_project_events' do
|
|
||||||
events = described_class.new(projects).all_project_events
|
|
||||||
|
|
||||||
expect(events).not_to include(wiki_page_event)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'the design_activity_events feature flag is disabled' do
|
context 'the design_activity_events feature flag is disabled' do
|
||||||
|
|
@ -87,23 +75,11 @@ RSpec.describe EventCollection do
|
||||||
expect(collection.all_project_events).to include(design_event)
|
expect(collection.all_project_events).to include(design_event)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'the wiki_events feature flag is enabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(wiki_events: true)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'includes the wiki page events when using to_a' do
|
|
||||||
events = described_class.new(projects).to_a
|
|
||||||
|
|
||||||
expect(events).to include(wiki_page_event)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'includes the wiki page events when using all_project_events' do
|
it 'includes the wiki page events when using all_project_events' do
|
||||||
events = described_class.new(projects).all_project_events
|
events = described_class.new(projects).all_project_events
|
||||||
|
|
||||||
expect(events).to include(wiki_page_event)
|
expect(events).to include(wiki_page_event)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
it 'applies a limit to the number of events' do
|
it 'applies a limit to the number of events' do
|
||||||
events = described_class.new(projects).to_a
|
events = described_class.new(projects).to_a
|
||||||
|
|
|
||||||
|
|
@ -661,15 +661,6 @@ RSpec.describe Event do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.not_wiki_page' do
|
|
||||||
it 'does not contain the wiki page events' do
|
|
||||||
non_wiki_events = events.reject(&:wiki_page?)
|
|
||||||
|
|
||||||
expect(events).not_to match_array(non_wiki_events)
|
|
||||||
expect(described_class.not_wiki_page).to match_array(non_wiki_events)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '.for_wiki_meta' do
|
describe '.for_wiki_meta' do
|
||||||
it 'finds events for a given wiki page metadata object' do
|
it 'finds events for a given wiki page metadata object' do
|
||||||
event = events.select(&:wiki_page?).first
|
event = events.select(&:wiki_page?).first
|
||||||
|
|
|
||||||
|
|
@ -289,4 +289,74 @@ RSpec.describe Clusters::ClusterPresenter do
|
||||||
it_behaves_like 'cluster health data'
|
it_behaves_like 'cluster health data'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#gitlab_managed_apps_logs_path' do
|
||||||
|
context 'user can read logs' do
|
||||||
|
let(:project) { cluster.project }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_maintainer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns path to logs' do
|
||||||
|
expect(presenter.gitlab_managed_apps_logs_path).to eq project_logs_path(project, cluster_id: cluster.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'group cluster' do
|
||||||
|
let(:cluster) { create(:cluster, cluster_type: :group_type, groups: [group]) }
|
||||||
|
let(:group) { create(:group, name: 'Foo') }
|
||||||
|
|
||||||
|
context 'user can read logs' do
|
||||||
|
before do
|
||||||
|
group.add_maintainer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'there are projects within group' do
|
||||||
|
let!(:project) { create(:project, namespace: group) }
|
||||||
|
|
||||||
|
it 'returns path to logs' do
|
||||||
|
expect(presenter.gitlab_managed_apps_logs_path).to eq project_logs_path(project, cluster_id: cluster.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'there are no projects within group' do
|
||||||
|
it 'returns nil' do
|
||||||
|
expect(presenter.gitlab_managed_apps_logs_path).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'instance cluster' do
|
||||||
|
let(:cluster) { create(:cluster, cluster_type: :instance_type) }
|
||||||
|
let!(:project) { create(:project) }
|
||||||
|
let(:user) { create(:admin) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_maintainer(user)
|
||||||
|
stub_feature_flags(user_mode_in_session: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'user can read logs' do
|
||||||
|
it 'returns path to logs' do
|
||||||
|
expect(presenter.gitlab_managed_apps_logs_path).to eq project_logs_path(project, cluster_id: cluster.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'user can NOT read logs' do
|
||||||
|
let(:cluster) { create(:cluster, cluster_type: :instance_type) }
|
||||||
|
let!(:project) { create(:project) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_developer(user)
|
||||||
|
stub_feature_flags(user_mode_in_session: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nil' do
|
||||||
|
expect(presenter.gitlab_managed_apps_logs_path).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1928,6 +1928,13 @@ RSpec.describe API::Projects do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'exposes service desk attributes' do
|
||||||
|
get api("/projects/#{project.id}", user)
|
||||||
|
|
||||||
|
expect(json_response).to have_key 'service_desk_enabled'
|
||||||
|
expect(json_response).to have_key 'service_desk_address'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET /projects/:id/users' do
|
describe 'GET /projects/:id/users' do
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,13 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe ClusterEntity do
|
RSpec.describe ClusterEntity do
|
||||||
|
include Gitlab::Routing.url_helpers
|
||||||
|
|
||||||
describe '#as_json' do
|
describe '#as_json' do
|
||||||
subject { described_class.new(cluster).as_json }
|
let(:user) { nil }
|
||||||
|
let(:request) { EntityRequest.new({ current_user: user }) }
|
||||||
|
|
||||||
|
subject { described_class.new(cluster, request: request).as_json }
|
||||||
|
|
||||||
context 'when provider type is gcp' do
|
context 'when provider type is gcp' do
|
||||||
let(:cluster) { create(:cluster, :instance, provider_type: :gcp, provider_gcp: provider) }
|
let(:cluster) { create(:cluster, :instance, provider_type: :gcp, provider_gcp: provider) }
|
||||||
|
|
@ -40,7 +45,7 @@ RSpec.describe ClusterEntity do
|
||||||
context 'when no application has been installed' do
|
context 'when no application has been installed' do
|
||||||
let(:cluster) { create(:cluster, :instance) }
|
let(:cluster) { create(:cluster, :instance) }
|
||||||
|
|
||||||
subject { described_class.new(cluster).as_json[:applications]}
|
subject { described_class.new(cluster, request: request).as_json[:applications]}
|
||||||
|
|
||||||
it 'contains helm as not_installable' do
|
it 'contains helm as not_installable' do
|
||||||
expect(subject).not_to be_empty
|
expect(subject).not_to be_empty
|
||||||
|
|
@ -50,5 +55,28 @@ RSpec.describe ClusterEntity do
|
||||||
expect(helm[:status]).to eq(:not_installable)
|
expect(helm[:status]).to eq(:not_installable)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'gitlab_managed_apps_logs_path' do
|
||||||
|
let(:cluster) { create(:cluster, :project) }
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
subject { described_class.new(cluster, request: request).as_json }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow_next_instance_of(Clusters::ClusterPresenter) do |presenter|
|
||||||
|
allow(presenter).to receive(:show_path).and_return(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'return projects log explorer path' do
|
||||||
|
log_explorer_path = project_logs_path(cluster.project, cluster_id: cluster.id)
|
||||||
|
|
||||||
|
expect_next_instance_of(Clusters::ClusterPresenter, cluster, current_user: user) do |presenter|
|
||||||
|
expect(presenter).to receive(:gitlab_managed_apps_logs_path).and_return(log_explorer_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(subject[:gitlab_managed_apps_logs_path]).to eq(log_explorer_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,14 @@ RSpec.describe ClusterSerializer do
|
||||||
let(:cluster) { create(:cluster, :project, provider_type: :user) }
|
let(:cluster) { create(:cluster, :project, provider_type: :user) }
|
||||||
|
|
||||||
describe '#represent_list' do
|
describe '#represent_list' do
|
||||||
subject { described_class.new.represent_list(cluster).keys }
|
subject { described_class.new(current_user: nil).represent_list(cluster).keys }
|
||||||
|
|
||||||
it 'serializes attrs correctly' do
|
it 'serializes attrs correctly' do
|
||||||
is_expected.to contain_exactly(
|
is_expected.to contain_exactly(
|
||||||
:cluster_type,
|
:cluster_type,
|
||||||
:enabled,
|
:enabled,
|
||||||
:environment_scope,
|
:environment_scope,
|
||||||
|
:gitlab_managed_apps_logs_path,
|
||||||
:name,
|
:name,
|
||||||
:nodes,
|
:nodes,
|
||||||
:path,
|
:path,
|
||||||
|
|
@ -22,7 +23,7 @@ RSpec.describe ClusterSerializer do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#represent_status' do
|
describe '#represent_status' do
|
||||||
subject { described_class.new.represent_status(cluster).keys }
|
subject { described_class.new(current_user: nil).represent_status(cluster).keys }
|
||||||
|
|
||||||
context 'when provider type is gcp and cluster is errored' do
|
context 'when provider type is gcp and cluster is errored' do
|
||||||
let(:cluster) do
|
let(:cluster) do
|
||||||
|
|
|
||||||
|
|
@ -200,16 +200,6 @@ RSpec.describe EventCreateService do
|
||||||
|
|
||||||
expect(duplicate).to eq(event)
|
expect(duplicate).to eq(event)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'the feature is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(wiki_events: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not create the event' do
|
|
||||||
expect { event }.not_to change(Event, :count)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -247,14 +247,6 @@ RSpec.describe Git::WikiPushService, services: true do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'the wiki_events feature is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(wiki_events: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'a no-op push'
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'the wiki_events_on_git_push feature is disabled' do
|
context 'the wiki_events_on_git_push feature is disabled' do
|
||||||
before do
|
before do
|
||||||
stub_feature_flags(wiki_events_on_git_push: false)
|
stub_feature_flags(wiki_events_on_git_push: false)
|
||||||
|
|
|
||||||
|
|
@ -14,21 +14,6 @@ RSpec.describe WikiPages::EventCreateService do
|
||||||
let(:action) { :created }
|
let(:action) { :created }
|
||||||
let(:response) { subject.execute(slug, page, action) }
|
let(:response) { subject.execute(slug, page, action) }
|
||||||
|
|
||||||
context 'feature flag is not enabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(wiki_events: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not error' do
|
|
||||||
expect(response).to be_success
|
|
||||||
.and have_attributes(message: /No event created/)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not create an event' do
|
|
||||||
expect { response }.not_to change(Event, :count)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'the user is nil' do
|
context 'the user is nil' do
|
||||||
subject { described_class.new(nil) }
|
subject { described_class.new(nil) }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,16 +63,6 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type|
|
||||||
include_examples 'correct event created'
|
include_examples 'correct event created'
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'the feature is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(wiki_events: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not record the activity' do
|
|
||||||
expect { service.execute }.not_to change(Event, :count)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the options are bad' do
|
context 'when the options are bad' do
|
||||||
let(:page_title) { '' }
|
let(:page_title) { '' }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,14 +37,4 @@ RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type|
|
||||||
|
|
||||||
expect { service.execute(nil) }.not_to change { counter.read(:delete) }
|
expect { service.execute(nil) }.not_to change { counter.read(:delete) }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'the feature is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(wiki_events: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not record the activity' do
|
|
||||||
expect { service.execute(page) }.not_to change(Event, :count)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -67,16 +67,6 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type|
|
||||||
include_examples 'adds activity event'
|
include_examples 'adds activity event'
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'the feature is disabled' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(wiki_events: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not record the activity' do
|
|
||||||
expect { service.execute(page) }.not_to change(Event, :count)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the options are bad' do
|
context 'when the options are bad' do
|
||||||
let(:page_title) { '' }
|
let(:page_title) { '' }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "spec_helper"
|
||||||
|
|
||||||
|
RSpec.describe ServiceDeskEmailReceiverWorker, :mailer do
|
||||||
|
describe '#perform' do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
let(:email) { fixture_file('emails/service_desk_custom_address.eml') }
|
||||||
|
|
||||||
|
context 'when service_desk_email config is enabled' do
|
||||||
|
before do
|
||||||
|
stub_service_desk_email_setting(enabled: true, address: 'foo')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not ignore the email' do
|
||||||
|
expect(Gitlab::Email::ServiceDeskReceiver).to receive(:new)
|
||||||
|
|
||||||
|
worker.perform(email)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when service desk receiver raises an exception' do
|
||||||
|
before do
|
||||||
|
allow_next_instance_of(Gitlab::Email::ServiceDeskReceiver) do |receiver|
|
||||||
|
allow(receiver).to receive(:find_handler).and_return(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends a rejection email' do
|
||||||
|
perform_enqueued_jobs do
|
||||||
|
worker.perform(email)
|
||||||
|
end
|
||||||
|
|
||||||
|
reply = ActionMailer::Base.deliveries.last
|
||||||
|
expect(reply).not_to be_nil
|
||||||
|
expect(reply.to).to eq(['jake@adventuretime.ooo'])
|
||||||
|
expect(reply.subject).to include('Rejected')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when service_desk_email config is disabled' do
|
||||||
|
before do
|
||||||
|
stub_service_desk_email_setting(enabled: false, address: 'foo')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores the email' do
|
||||||
|
expect(Gitlab::Email::ServiceDeskReceiver).not_to receive(:new)
|
||||||
|
|
||||||
|
worker.perform(email)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
16
yarn.lock
16
yarn.lock
|
|
@ -843,15 +843,15 @@
|
||||||
eslint-plugin-vue "^6.2.1"
|
eslint-plugin-vue "^6.2.1"
|
||||||
vue-eslint-parser "^7.0.0"
|
vue-eslint-parser "^7.0.0"
|
||||||
|
|
||||||
"@gitlab/svgs@1.151.0":
|
"@gitlab/svgs@1.152.0":
|
||||||
version "1.151.0"
|
version "1.152.0"
|
||||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.151.0.tgz#099905295d33eb31033f4a48eb3652da2f686239"
|
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.152.0.tgz#663c9a5f073f59b66f4241ef2d3fea2205846905"
|
||||||
integrity sha512-2PTSM8CFhUjeTFKfcq6E/YwPpOVdSVWupf3NhKO/bz/cisSBS5P7aWxaXKIaxy28ySyBKEfKaAT6b4rXTwvVgg==
|
integrity sha512-daZHOBVAwjsU6n60IycanoO/JymfQ36vrr46OUdWjHdp0ATYrgh+01LcxiSNLdlyndIRqHWGtwmuilokM9q6Vg==
|
||||||
|
|
||||||
"@gitlab/ui@17.22.1":
|
"@gitlab/ui@17.26.0":
|
||||||
version "17.22.1"
|
version "17.26.0"
|
||||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.22.1.tgz#368578d04bb49011690911599c22a7d306f5fe99"
|
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.26.0.tgz#d8efad47c3f4dc32e0586f3f5e4e2e3e0c2babf6"
|
||||||
integrity sha512-elcu2gdvt1Afz3GMrIBQR+eujlA6JetLn44T1UzPHUhlaodT/w+TIj0+uPIbPiD7Oz6uR/sYwBqlZXQdBcVv3Q==
|
integrity sha512-0QgzMK8MFGaqBB8yYntjYjUnzKFQ9a8d4mjufIyeKq6WomuMYHTFJgUj0+cEQ6uuTRtNk3MMuy3ZHBJg1wGzTw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/standalone" "^7.0.0"
|
"@babel/standalone" "^7.0.0"
|
||||||
"@gitlab/vue-toasted" "^1.3.0"
|
"@gitlab/vue-toasted" "^1.3.0"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue