Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-14 15:09:05 +00:00
parent 49089d4fb1
commit 66bd1f0fdc
83 changed files with 2412 additions and 270 deletions

View File

@ -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);
};

View File

@ -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 {};
};

View File

@ -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),
});

View File

@ -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';

View File

@ -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;
},
};

View File

@ -0,0 +1,15 @@
export default () => ({
basePath: null,
headPath: null,
baseBlobPath: null,
headBlobPath: null,
isLoading: false,
hasError: false,
newIssues: [],
resolvedIssues: [],
helpPath: null,
});

View File

@ -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,
});
});
};

View File

@ -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();
});

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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?

View File

@ -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)

View File

@ -0,0 +1,24 @@
%html{ lang: "en" }
%head
%meta{ content: "text/html; charset=utf-8", "http-equiv" => "Content-Type" }
-# haml-lint:disable NoPlainNodes
%title
GitLab
-# haml-lint:enable NoPlainNodes
= stylesheet_link_tag 'notify'
= yield :head
%body
.content
= yield
.footer{ style: "margin-top: 10px;" }
%p
&mdash;
%br
= link_to "Unsubscribe", @unsubscribe_url
-# EE-specific start
- if Gitlab::CurrentSettings.email_additional_text.present?
%br
%br
= Gitlab::Utils.nlbr(Gitlab::CurrentSettings.email_additional_text)
-# EE-specific end

View File

@ -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)

View File

@ -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 %>

View File

@ -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.

View File

@ -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 %>

View File

@ -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')

View File

@ -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:

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Introduce prepare environment action to annotate non-deployment jobs
merge_request: 35642
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Enable display of wiki events in activity streams
merge_request: 32475
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add docs for Alert trigger test alerts
merge_request: 36647
author:
type: added

View File

@ -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.

View File

@ -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
<<< <<<
``` ```

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -66,29 +66,13 @@ RSpec.describe EventsFinder do
end end
end end
describe 'wiki events feature flag' do describe 'wiki events' do
let_it_be(:events) { create_list(:wiki_page_event, 3, project: public_project) } let_it_be(:events) { create_list(:wiki_page_event, 3, project: public_project) }
subject(:finder) { described_class.new(source: public_project, target_type: 'wiki', current_user: user) } subject(:finder) { described_class.new(source: public_project, target_type: 'wiki', current_user: user) }
context 'the wiki_events feature flag is disabled' do it 'can find the wiki events' do
before do expect(finder.execute).to match_array(events)
stub_feature_flags(wiki_events: false)
end
it 'omits the wiki page events' do
expect(finder.execute).to be_empty
end
end
context 'the wiki_events feature flag is enabled' do
before do
stub_feature_flags(wiki_events: true)
end
it 'can find the wiki events' do
expect(finder.execute).to match_array(events)
end
end end
end end

28
spec/fixtures/emails/service_desk.eml vendored Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
```

View File

@ -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).
>

View File

@ -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',
},
];

View File

@ -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,
);
});
});
});

View File

@ -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"',
);
});
});
});
});

View File

@ -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);
});
});
});

View File

@ -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();
});
});
});
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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' } }

View File

@ -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

View File

@ -0,0 +1,311 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
include_context :email_shared_context
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
stub_config_setting(host: 'localhost')
end
let(:email_raw) { email_fixture('emails/service_desk.eml') }
let_it_be(:namespace) { create(:namespace, name: "email") }
let(:expected_description) do
"Service desk stuff!\n\n```\na = b\n```\n\n`/label ~label1`\n`/assign @user1`\n`/close`\n![image](uploads/image.png)"
end
context 'service desk is enabled for the project' do
let_it_be(:project) { create(:project, :repository, :public, namespace: namespace, path: 'test', service_desk_enabled: true) }
before do
allow(Gitlab::ServiceDesk).to receive(:supported?).and_return(true)
end
shared_examples 'a new issue request' do
before do
setup_attachment
end
it 'creates a new issue' do
expect { receiver.execute }.to change { Issue.count }.by(1)
new_issue = Issue.last
expect(new_issue.author).to eql(User.support_bot)
expect(new_issue.confidential?).to be true
expect(new_issue.all_references.all).to be_empty
expect(new_issue.title).to eq("Service Desk (from jake@adventuretime.ooo): The message subject! @all")
expect(new_issue.description).to eq(expected_description.strip)
end
it 'sends thank you email' do
expect { receiver.execute }.to have_enqueued_job.on_queue('mailers')
end
end
context 'when everything is fine' do
it_behaves_like 'a new issue request'
context 'with legacy incoming email address' do
let(:email_raw) { fixture_file('emails/service_desk_legacy.eml') }
it_behaves_like 'a new issue request'
end
context 'when using issue templates' do
let_it_be(:user) { create(:user) }
before do
setup_attachment
end
context 'and template is present' do
let_it_be(:settings) { create(:service_desk_setting, project: project) }
def set_template_file(file_name, content)
file_path = ".gitlab/issue_templates/#{file_name}.md"
project.repository.create_file(user, file_path, content, message: 'message', branch_name: 'master')
settings.update!(issue_template_key: file_name)
end
it 'appends template text to issue description' do
set_template_file('service_desk', 'text from template')
receiver.execute
issue_description = Issue.last.description
expect(issue_description).to include(expected_description)
expect(issue_description.lines.last).to eq('text from template')
end
context 'when quick actions are present' do
let(:label) { create(:label, project: project, title: 'label1') }
let(:milestone) { create(:milestone, project: project) }
let!(:user) { create(:user, username: 'user1') }
before do
project.add_developer(user)
end
it 'applies quick action commands present on templates' do
file_content = %(Text from template \n/label ~#{label.title} \n/milestone %"#{milestone.name}"")
set_template_file('with_slash_commands', file_content)
receiver.execute
issue = Issue.last
expect(issue.description).to include('Text from template')
expect(issue.label_ids).to include(label.id)
expect(issue.milestone).to eq(milestone)
end
it 'redacts quick actions present on user email body' do
set_template_file('service_desk1', 'text from template')
receiver.execute
issue = Issue.last
expect(issue).to be_opened
expect(issue.description).to include('`/label ~label1`')
expect(issue.description).to include('`/assign @user1`')
expect(issue.description).to include('`/close`')
expect(issue.assignees).to be_empty
expect(issue.milestone).to be_nil
end
end
end
context 'and template cannot be found' do
before do
service = ServiceDeskSetting.new(project_id: project.id, issue_template_key: 'unknown')
service.save!(validate: false)
end
it 'does not append template text to issue description' do
receiver.execute
new_issue = Issue.last
expect(new_issue.description).to eq(expected_description.strip)
end
it 'creates support bot note on issue' do
receiver.execute
note = Note.last
expect(note.note).to include("WARNING: The template file unknown.md used for service desk issues is empty or could not be found.")
expect(note.author).to eq(User.support_bot)
end
it 'does not send warning note email' do
ActionMailer::Base.deliveries = []
perform_enqueued_jobs do
expect { receiver.execute }.to change { ActionMailer::Base.deliveries.size }.by(1)
end
# Only sends created issue email
expect(ActionMailer::Base.deliveries.last.text_part.body).to include("Thank you for your support request!")
end
end
end
context 'when using service desk key' do
let_it_be(:service_desk_settings) { create(:service_desk_setting, project: project, project_key: 'mykey') }
let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml') }
let(:receiver) { Gitlab::Email::ServiceDeskReceiver.new(email_raw) }
before do
stub_service_desk_email_setting(enabled: true, address: 'support+%{key}@example.com')
end
it_behaves_like 'a new issue request'
context 'when there is no project with the key' do
let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', key: 'some_key') }
it 'bounces the email' do
expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
end
end
context 'when the project slug does not match' do
let(:email_raw) { service_desk_fixture('emails/service_desk_custom_address.eml', slug: 'some-slug') }
it 'bounces the email' do
expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
end
end
context 'when service_desk_custom_address feature is disabled' do
before do
stub_feature_flags(service_desk_custom_address: false)
end
it 'bounces the email' do
expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
end
end
end
end
describe '#can_handle?' do
let(:mail) { Mail::Message.new(email_raw) }
it 'handles the new email key format' do
handler = described_class.new(mail, "h5bp-html5-boilerplate-#{project.project_id}-issue-")
expect(handler.instance_variable_get(:@project_id).to_i).to eq project.project_id
expect(handler.can_handle?).to be_truthy
end
it 'handles the legacy email key format' do
handler = described_class.new(mail, "h5bp/html5-boilerplate")
expect(handler.instance_variable_get(:@project_path)).to eq 'h5bp/html5-boilerplate'
expect(handler.can_handle?).to be_truthy
end
it "doesn't handle invalid email key" do
handler = described_class.new(mail, "h5bp-html5-boilerplate-invalid")
expect(handler.can_handle?).to be_falsey
end
end
context 'when there is no from address' do
before do
allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:from_address).and_return(nil)
end
end
it "creates a new issue" do
expect { receiver.execute }.to change { Issue.count }.by(1)
end
it 'does not send thank you email' do
expect { receiver.execute }.not_to have_enqueued_job.on_queue('mailers')
end
end
context 'when there is a sender address and a from address' do
let(:email_raw) { email_fixture('emails/service_desk_sender_and_from.eml') }
it 'prefers the from address' do
setup_attachment
expect { receiver.execute }.to change { Issue.count }.by(1)
new_issue = Issue.last
expect(new_issue.service_desk_reply_to).to eq('finn@adventuretime.ooo')
end
end
context 'when service desk is not enabled for project' do
before do
allow(Gitlab::ServiceDesk).to receive(:enabled?).and_return(false)
end
it 'does not create an issue' do
expect { receiver.execute rescue nil }.not_to change { Issue.count }
end
it 'does not send thank you email' do
expect { receiver.execute rescue nil }.not_to have_enqueued_job.on_queue('mailers')
end
end
context 'when the email is forwarded through an alias' do
let(:email_raw) { email_fixture('emails/service_desk_forwarded.eml') }
it_behaves_like 'a new issue request'
end
context 'when the email is forwarded' do
let(:email_raw) { email_fixture('emails/service_desk_forwarded_new_issue.eml') }
it_behaves_like 'a new issue request' do
let(:expected_description) do
<<~EOF
Service desk stuff!
---------- Forwarded message ---------
From: Jake the Dog <jake@adventuretime.ooo>
To: <jake@adventuretime.ooo>
forwarded content
![image](uploads/image.png)
EOF
end
end
end
end
context 'service desk is disabled for the project' do
let(:project) { create(:project, :public, namespace: namespace, path: 'test', service_desk_enabled: false) }
it 'bounces the email' do
expect { receiver.execute }.to raise_error(Gitlab::Email::ProcessingError)
end
it "doesn't create an issue" do
expect { receiver.execute rescue nil }.not_to change { Issue.count }
end
end
def email_fixture(path)
fixture_file(path).gsub('project_id', project.project_id.to_s)
end
def service_desk_fixture(path, slug: nil, key: 'mykey')
slug ||= project.full_path_slug.to_s
fixture_file(path).gsub('project_slug', slug).gsub('project_key', key)
end
end

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -44,22 +44,10 @@ RSpec.describe EventCollection do
expect(events).to match_array(most_recent_20_events) expect(events).to match_array(most_recent_20_events)
end end
context 'the wiki_events feature flag is disabled' do it 'includes the wiki page events when using to_a' do
before do events = described_class.new(projects).to_a
stub_feature_flags(wiki_events: false)
end
it 'omits the wiki page events when using to_a' do expect(events).to include(wiki_page_event)
events = described_class.new(projects).to_a
expect(events).not_to include(wiki_page_event)
end
it 'omits the wiki page events when using all_project_events' do
events = described_class.new(projects).all_project_events
expect(events).not_to include(wiki_page_event)
end
end end
context 'the design_activity_events feature flag is disabled' do context 'the design_activity_events feature flag is disabled' do
@ -87,22 +75,10 @@ RSpec.describe EventCollection do
expect(collection.all_project_events).to include(design_event) expect(collection.all_project_events).to include(design_event)
end end
context 'the wiki_events feature flag is enabled' do it 'includes the wiki page events when using all_project_events' do
before do events = described_class.new(projects).all_project_events
stub_feature_flags(wiki_events: true)
end
it 'includes the wiki page events when using to_a' do expect(events).to include(wiki_page_event)
events = described_class.new(projects).to_a
expect(events).to include(wiki_page_event)
end
it 'includes the wiki page events when using all_project_events' do
events = described_class.new(projects).all_project_events
expect(events).to include(wiki_page_event)
end
end end
it 'applies a limit to the number of events' do it 'applies a limit to the number of events' do

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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) }

View File

@ -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) { '' }

View File

@ -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

View File

@ -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) { '' }

View File

@ -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

View File

@ -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"