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'
EventCollection.new(current_user.authorized_projects).all_project_events
else
# EventCollection is responsible for applying the feature flag
apply_feature_flags(source.events)
source.events
end
end
def apply_feature_flags(events)
return events if ::Feature.enabled?(:wiki_events)
events.not_wiki_page
end
# rubocop: disable CodeReuse/ActiveRecord
def by_current_user_access(events)
events.merge(Project.public_or_visible_to_user(current_user))

View File

@ -36,7 +36,8 @@ module EnvironmentsHelper
"environment-name": environment.name,
"environments-path": project_environments_path(project, format: :json),
"environment-id": environment.id,
"cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack')
"cluster-applications-documentation-path" => help_page_path('user/clusters/applications.md', anchor: 'elastic-stack'),
"clusters-path": project_clusters_path(project, format: :json)
}
end

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::Groups
include Emails::Reviews
include Emails::ServiceDesk
helper TimeboxesHelper
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
end
def service_desk_new_note_email
cleanup do
note = create_note(noteable_type: 'Issue', noteable_id: issue.id, note: 'Issue note content')
Notify.service_desk_new_note_email(issue.id, note.id).message
end
end
def service_desk_thank_you_email
Notify.service_desk_thank_you_email(issue.id).message
end
private
def project

View File

@ -84,7 +84,6 @@ class Event < ApplicationRecord
scope :for_design, -> { where(target_type: 'DesignManagement::Design') }
# Needed to implement feature flag: can be removed when feature flag is removed
scope :not_wiki_page, -> { where('target_type IS NULL or target_type <> ?', 'WikiPage::Meta') }
scope :not_design, -> { where('target_type IS NULL or target_type <> ?', 'DesignManagement::Design') }
scope :with_associations, -> do

View File

@ -45,7 +45,6 @@ class EventCollection
private
def apply_feature_flags(events)
events = events.not_wiki_page unless ::Feature.enabled?(:wiki_events)
events = events.not_design unless ::Feature.enabled?(:design_activity_events)
events

View File

@ -2,6 +2,7 @@
module Clusters
class ClusterPresenter < Gitlab::View::Presenter::Delegated
include ::Gitlab::Utils::StrongMemoize
include ActionView::Helpers::SanitizeHelper
include ActionView::Helpers::UrlHelper
include IconsHelper
@ -60,6 +61,12 @@ module Clusters
end
end
def gitlab_managed_apps_logs_path
return unless logs_project && can_read_cluster?
project_logs_path(logs_project, cluster_id: cluster.id)
end
def read_only_kubernetes_platform_fields?
!cluster.provided_by_user?
end
@ -85,6 +92,16 @@ module Clusters
ActionController::Base.helpers.image_path(path)
end
# currently log explorer is only available in the scope of the project
# for group and instance level cluster selected project does not affects
# fetching logs from gitlab managed apps namespace, therefore any project
# available to user will be sufficient.
def logs_project
strong_memoize(:logs_project) do
cluster.all_projects.first
end
end
def clusterable
if cluster.group_type?
cluster.group

View File

@ -16,4 +16,8 @@ class ClusterEntity < Grape::Entity
expose :path do |cluster|
Clusters::ClusterPresenter.new(cluster).show_path # rubocop: disable CodeReuse/Presenter
end
expose :gitlab_managed_apps_logs_path do |cluster|
Clusters::ClusterPresenter.new(cluster, current_user: request.current_user).gitlab_managed_apps_logs_path # rubocop: disable CodeReuse/Presenter
end
end

View File

@ -10,6 +10,7 @@ class ClusterSerializer < BaseSerializer
:cluster_type,
:enabled,
:environment_scope,
:gitlab_managed_apps_logs_path,
:name,
:nodes,
:path,

View File

@ -120,8 +120,6 @@ class EventCreateService
#
# @return a tuple of event and either :found or :created
def wiki_event(wiki_page_meta, author, action)
return unless Feature.enabled?(:wiki_events)
raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action)
if duplicate = existing_wiki_event(wiki_page_meta, action)

View File

@ -23,7 +23,7 @@ module Git
end
def can_process_wiki_events?
Feature.enabled?(:wiki_events) && Feature.enabled?(:wiki_events_on_git_push, project)
Feature.enabled?(:wiki_events_on_git_push, project)
end
def push_changes

View File

@ -44,8 +44,6 @@ module WikiPages
end
def create_wiki_event(page)
return unless ::Feature.enabled?(:wiki_events)
response = WikiPages::EventCreateService.new(current_user).execute(slug_for_page(page), page, event_action)
log_error(response.message) if response.error?

View File

@ -10,8 +10,6 @@ module WikiPages
end
def execute(slug, page, action)
return ServiceResponse.success(message: 'No event created as `wiki_events` feature is disabled') unless ::Feature.enabled?(:wiki_events)
event = Event.transaction do
wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)

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'
- if comments_visible?
= event_filter_link EventFilter::COMMENTS, _('Comments'), s_('EventFilterBy|Filter by comments')
- if Feature.enabled?(:wiki_events) && (@project.nil? || @project.has_wiki?)
- if @project.nil? || @project.has_wiki?
= event_filter_link EventFilter::WIKI, _('Wiki'), s_('EventFilterBy|Filter by wiki')
- if event_filter_visible(:designs)
= event_filter_link EventFilter::DESIGNS, _('Designs'), s_('EventFilterBy|Filter by designs')

View File

@ -1660,6 +1660,14 @@
:weight: 2
:idempotent:
:tags: []
- :name: service_desk_email_receiver
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
:tags: []
- :name: system_hook_push
:feature_category: :source_code_management
:has_external_dependencies:

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%.
NOTE: **Note:**
Stickiness (consistent application behavior for the same user) is guaranteed for logged-in users, but not anonymous users.
CAUTION: **Caution:**
If this strategy is selected, then the Unleash client **must** be given a user
ID for the feature to be enabled. See the [Ruby example](#ruby-application-example) below.
@ -120,6 +123,9 @@ activation strategy.
Enter user IDs as a comma-separated list of values. For example,
`user@example.com, user2@example.com`, or `username1,username2,username3`, and so on.
NOTE: **Note:**
User IDs are identifiers for your application users. They do not need to be GitLab users.
CAUTION: **Caution:**
The Unleash client **must** be given a user ID for the feature to be enabled for
target users. See the [Ruby example](#ruby-application-example) below.

View File

@ -10,14 +10,14 @@ You can find the full documentation for the AsciiDoc syntax at <https://asciidoc
### Paragraphs
```asciidoc
```plaintext
A normal paragraph.
Line breaks are not preserved.
```
Line comments, which are lines that start with `//`, are skipped:
```asciidoc
```plaintext
// this is a comment
```
@ -25,7 +25,7 @@ A blank line separates paragraphs.
A paragraph with the `[%hardbreaks]` option will preserve line breaks:
```asciidoc
```plaintext
[%hardbreaks]
This paragraph carries the `hardbreaks` option.
Notice how line breaks are now preserved.
@ -35,7 +35,7 @@ An indented (literal) paragraph disables text formatting,
preserves spaces and line breaks, and is displayed in a
monospaced font:
```asciidoc
```plaintext
This literal paragraph is indented with one space.
As a consequence, *text formatting*, spaces,
and lines breaks will be preserved.
@ -43,7 +43,7 @@ monospaced font:
An admonition paragraph grabs the reader's attention:
```asciidoc
```plaintext
NOTE: This is a brief reference, please read the full documentation at https://asciidoctor.org/docs/.
TIP: Lists can be indented. Leading whitespace is not significant.
@ -53,7 +53,7 @@ TIP: Lists can be indented. Leading whitespace is not significant.
**Constrained (applied at word boundaries)**
```asciidoc
```plaintext
*strong importance* (aka bold)
_stress emphasis_ (aka italic)
`monospaced` (aka typewriter text)
@ -64,7 +64,7 @@ _stress emphasis_ (aka italic)
**Unconstrained (applied anywhere)**
```asciidoc
```plaintext
**C**reate+**R**ead+**U**pdate+**D**elete
fan__freakin__tastic
``mono``culture
@ -72,7 +72,7 @@ fan__freakin__tastic
**Replacements**
```asciidoc
```plaintext
A long time ago in a galaxy far, far away...
(C) 1976 Arty Artisan
I believe I shall--no, actually I won't.
@ -80,7 +80,7 @@ I believe I shall--no, actually I won't.
**Macros**
```asciidoc
```plaintext
// where c=specialchars, q=quotes, a=attributes, r=replacements, m=macros, p=post_replacements, etc.
The European icon:flag[role=blue] is blue & contains pass:[************] arranged in a icon:circle-o[role=yellow].
The pass:c[->] operator is often referred to as the stabby lambda.
@ -93,12 +93,12 @@ stem:[sqrt(4) = 2]
**User-defined attributes**
```asciidoc
```plaintext
// define attributes in the document header
:name: value
```
```asciidoc
```plaintext
:url-gem: https://rubygems.org/gems/asciidoctor
You can download and install Asciidoctor {asciidoctor-version} from {url-gem}.
@ -117,7 +117,7 @@ GitLab sets the following environment attributes:
### Links
```asciidoc
```plaintext
https://example.org/page[A webpage]
link:../path/to/file.txt[A local file]
xref:document.adoc[A sibling document]
@ -126,7 +126,7 @@ mailto:hello@example.org[Email to say hello!]
### Anchors
```asciidoc
```plaintext
[[idname,reference text]]
// or written using normal block attributes as `[#idname,reftext=reference text]`
A paragraph (or any block) with an anchor (aka ID) and reftext.
@ -142,7 +142,7 @@ This paragraph has a footnote.footnote:[This is the text of the footnote.]
#### Unordered
```asciidoc
```plaintext
* level 1
** level 2
*** level 3
@ -161,7 +161,7 @@ Attach a block or paragraph to a list item using a list continuation (which you
#### Ordered
```asciidoc
```plaintext
. Step 1
. Step 2
.. Step 2a
@ -177,14 +177,14 @@ Attach a block or paragraph to a list item using a list continuation (which you
#### Checklist
```asciidoc
```plaintext
* [x] checked
* [ ] not checked
```
#### Callout
```asciidoc
```plaintext
// enable callout bubbles by adding `:icons: font` to the document header
[,ruby]
----
@ -195,7 +195,7 @@ puts 'Hello, World!' # <1>
#### Description
```asciidoc
```plaintext
first term:: description of first term
second term::
description of second term
@ -205,7 +205,7 @@ description of second term
#### Header
```asciidoc
```plaintext
= Document Title
Author Name <author@example.org>
v1.0, 2019-01-01
@ -213,7 +213,7 @@ v1.0, 2019-01-01
#### Sections
```asciidoc
```plaintext
= Document Title (Level 0)
== Level 1
=== Level 2
@ -225,7 +225,7 @@ v1.0, 2019-01-01
#### Includes
```asciidoc
```plaintext
include::basics.adoc[]
// define -a allow-uri-read to allow content to be read from URI
@ -239,13 +239,13 @@ included, a number that is inclusive of transitive dependencies.
### Blocks
```asciidoc
```plaintext
--
open - a general-purpose content wrapper; useful for enclosing content to attach to a list item
--
```
```asciidoc
```plaintext
// recognized types include CAUTION, IMPORTANT, NOTE, TIP, and WARNING
// enable admonition icons by setting `:icons: font` in the document header
[NOTE]
@ -254,13 +254,13 @@ admonition - a notice for the reader, ranging in severity from a tip to an alert
====
```
```asciidoc
```plaintext
====
example - a demonstration of the concept being documented
====
```
```asciidoc
```plaintext
.Toggle Me
[%collapsible]
====
@ -268,58 +268,58 @@ collapsible - these details are revealed by clicking the title
====
```
```asciidoc
```plaintext
****
sidebar - auxiliary content that can be read independently of the main content
****
```
```asciidoc
```plaintext
....
literal - an exhibit that features program output
....
```
```asciidoc
```plaintext
----
listing - an exhibit that features program input, source code, or the contents of a file
----
```
```asciidoc
```plaintext
[,language]
----
source - a listing that is embellished with (colorized) syntax highlighting
----
```
````asciidoc
````plaintext
\```language
fenced code - a shorthand syntax for the source block
\```
````
```asciidoc
```plaintext
[,attribution,citetitle]
____
quote - a quotation or excerpt; attribution with title of source are optional
____
```
```asciidoc
```plaintext
[verse,attribution,citetitle]
____
verse - a literary excerpt, often a poem; attribution with title of source are optional
____
```
```asciidoc
```plaintext
++++
pass - content passed directly to the output document; often raw HTML
++++
```
```asciidoc
```plaintext
// activate stem support by adding `:stem:` to the document header
[stem]
++++
@ -327,7 +327,7 @@ x = y^2
++++
```
```asciidoc
```plaintext
////
comment - content which is not included in the output document
////
@ -335,7 +335,7 @@ comment - content which is not included in the output document
### Tables
```asciidoc
```plaintext
.Table Attributes
[cols=>1h;2d,width=50%,frame=topbot]
|===
@ -366,7 +366,7 @@ comment - content which is not included in the output document
### Multimedia
```asciidoc
```plaintext
image::screenshot.png[block image,800,450]
Press image:reload.svg[reload,16,opts=interactive] to reload the page.
@ -380,12 +380,12 @@ video::300817511[vimeo]
### Breaks
```asciidoc
```plaintext
// thematic break (aka horizontal rule)
---
```
```asciidoc
```plaintext
// page break
<<<
```

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)**
> [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
> Support for `.gitlab.ci.yml` validation [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218472) in GitLab 13.2.
As expected from an IDE, syntax highlighting for many languages within
the Web IDE will make your direct editing even easier.
@ -35,6 +37,13 @@ The Web IDE currently provides:
- IntelliSense and validation support (displaying errors and warnings, providing
smart completions, formatting, and outlining) for some languages. For example:
TypeScript, JavaScript, CSS, LESS, SCSS, JSON, and HTML.
- Validation support for certain JSON and YAML files using schemas based on the
[JSON Schema Store](https://www.schemastore.org/json/). This feature
is only supported for the `.gitlab-ci.yml` file.
NOTE: **Note:** Validation support based on schemas is hidden behind
the feature flag `:schema_linting` on self-managed installations. To enable the
feature, you can [turn on the feature flag in Rails console](../../../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags).
Because the Web IDE is based on the [Monaco Editor](https://microsoft.github.io/monaco-editor/),
you can find a more complete list of supported languages in the

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
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14902) in GitLab 12.10.
> - It's deployed behind a feature flag, disabled by default.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14902) in **GitLab 12.10.**
> - Git events were [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216014) in **GitLab 13.0.**
> - It's enabled on GitLab.com.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-wiki-events-core-only). **(CORE ONLY)**
> - Git access activity creation is managed by a feature flag.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-wiki-events-in-git-core-only). **(CORE ONLY)**
Wiki events (creation, deletion, and updates) are tracked by GitLab and
displayed on the [user profile](../../profile/index.md#user-profile),
[group](../../group/index.md#view-group-activity),
and [project](../index.md#project-activity) activity pages.
### Limitations
### Enable or disable Wiki events in Git **(CORE ONLY)**
Only edits made in the browser or through the API have their activity recorded.
Edits made and pushed through Git are not currently listed in the activity list.
### Enable or disable Wiki Events **(CORE ONLY)**
Wiki event activity is under development and not ready for production use. It is
Tracking wiki events through Git is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session)
can enable it for your instance. You're welcome to test it, but use it at your
own risk.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can enable it for your instance.
To enable it:
```ruby
Feature.enable(:wiki_events)
Feature.enable(:wiki_events_on_git_push)
```
To enable for just a particular project:
```ruby
project = Project.find_by_full_path('your-group/your-project')
Feature.enable(:wiki_events_on_git_push, project)
```
To disable it:
```ruby
Feature.disable(:wiki_events)
Feature.disable(:wiki_events_on_git_push)
```
To disable for just a particular project:
```ruby
project = Project.find_by_full_path('your-group/your-project')
Feature.disable(:wiki_events_on_git_push, project)
```
## Adding and editing wiki pages locally

View File

@ -52,15 +52,12 @@ class EventFilter
private
def apply_feature_flags(events)
events = events.not_wiki_page unless Feature.enabled?(:wiki_events)
events = events.not_design unless can_view_design_activity?
events
end
def wiki_events(events)
return events unless Feature.enabled?(:wiki_events)
events.for_wiki_page
end

View File

@ -44,7 +44,7 @@ module Gitlab
validates :action,
type: String,
inclusion: { in: %w[start stop], message: 'should be start or stop' },
inclusion: { in: %w[start stop prepare], message: 'should be start, stop or prepare' },
allow_nil: true
validates :on_stop, type: String, allow_nil: true

View File

@ -12,7 +12,8 @@ module Gitlab
CreateNoteHandler,
CreateIssueHandler,
UnsubscribeHandler,
CreateMergeRequestHandler
CreateMergeRequestHandler,
ServiceDeskHandler
]
end
@ -25,5 +26,3 @@ module Gitlab
end
end
end
Gitlab::Email::Handler.prepend_if_ee('::EE::Gitlab::Email::Handler')

View File

@ -37,7 +37,11 @@ module Gitlab
def process_message(**kwargs)
message = ReplyParser.new(mail, **kwargs).execute.strip
add_attachments(message)
message_with_attachments = add_attachments(message)
# Support bot is specifically forbidden
# from using slash commands.
strip_quick_actions(message_with_attachments)
end
def add_attachments(reply)
@ -82,6 +86,15 @@ module Gitlab
def valid_project_slug?(found_project)
project_slug == found_project.full_path_slug
end
def strip_quick_actions(content)
return content unless author.support_bot?
command_definitions = ::QuickActions::InterpretService.command_definitions
extractor = ::Gitlab::QuickActions::Extractor.new(command_definitions)
extractor.redact_commands(content)
end
end
end
end

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/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.151.0",
"@gitlab/ui": "17.22.1",
"@gitlab/svgs": "1.152.0",
"@gitlab/ui": "17.26.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-1",
"@sentry/browser": "^5.10.2",

View File

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

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

View File

@ -80,16 +80,6 @@ RSpec.describe EventFilter do
it 'returns all events' do
expect(filtered_events).to eq(Event.all)
end
context 'the :wiki_events filter is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not return wiki events' do
expect(filtered_events).to eq(Event.not_wiki_page)
end
end
end
context 'with the "design" filter' do
@ -116,16 +106,6 @@ RSpec.describe EventFilter do
it 'returns only wiki page events' do
expect(filtered_events).to contain_exactly(wiki_page_event, wiki_page_update_event)
end
context 'the :wiki_events filter is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not return wiki events' do
expect(filtered_events).not_to include(wiki_page_event, wiki_page_update_event)
end
end
end
context 'with an unknown filter' do
@ -134,16 +114,6 @@ RSpec.describe EventFilter do
it 'returns all events' do
expect(filtered_events).to eq(Event.all)
end
context 'the :wiki_events filter is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not return wiki events' do
expect(filtered_events).to eq(Event.not_wiki_page)
end
end
end
context 'with a nil filter' do
@ -152,16 +122,6 @@ RSpec.describe EventFilter do
it 'returns all events' do
expect(filtered_events).to eq(Event.all)
end
context 'the :wiki_events filter is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not return wiki events' do
expect(filtered_events).to eq(Event.not_wiki_page)
end
end
end
end

View File

@ -102,6 +102,17 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do
end
end
context 'when prepare action is used' do
let(:config) do
{ name: 'production',
action: 'prepare' }
end
it 'is valid' do
expect(entry).to be_valid
end
end
context 'when wrong action type is used' do
let(:config) do
{ name: 'production',
@ -137,7 +148,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Environment do
describe '#errors' do
it 'contains error about invalid action' do
expect(entry.errors)
.to include 'environment action should be start or stop'
.to include 'environment action should be start, stop or prepare'
end
end
end

View File

@ -102,6 +102,19 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Deployment do
end
end
context 'when job has environment attribute with prepare action' do
let(:attributes) do
{
environment: 'production',
options: { environment: { name: 'production', action: 'prepare' } }
}
end
it 'returns nothing' do
is_expected.to be_nil
end
end
context 'when job does not have environment attribute' do
let(:attributes) { { name: 'test' } }

View File

@ -242,4 +242,70 @@ RSpec.describe Gitlab::Email::Handler::CreateNoteHandler do
it_behaves_like 'a reply to existing comment'
end
context 'when the service desk' do
let(:project) { create(:project, :public, service_desk_enabled: true) }
let(:support_bot) { User.support_bot }
let(:noteable) { create(:issue, project: project, author: support_bot, title: 'service desk issue') }
let(:note) { create(:note, project: project, noteable: noteable) }
let(:email_raw) { fixture_file('emails/valid_reply_with_quick_actions.eml') }
let!(:sent_notification) do
SentNotification.record_note(note, support_bot.id, mail_key)
end
context 'is enabled' do
before do
allow(Gitlab::ServiceDesk).to receive(:enabled?).with(project: project).and_return(true)
project.project_feature.update!(issues_access_level: issues_access_level)
end
context 'when issues are enabled for everyone' do
let(:issues_access_level) { ProjectFeature::ENABLED }
it 'creates a comment' do
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
end
context 'when quick actions are present' do
it 'encloses quick actions with code span markdown' do
receiver.execute
noteable.reload
note = Note.last
expect(note.note).to include("Jake out\n\n`/close`\n`/title test`")
expect(noteable.title).to eq('service desk issue')
expect(noteable).to be_opened
end
end
end
context 'when issues are protected members only' do
let(:issues_access_level) { ProjectFeature::PRIVATE }
it 'creates a comment' do
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
end
end
context 'when issues are disabled' do
let(:issues_access_level) { ProjectFeature::DISABLED }
it 'does not create a comment' do
expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotAuthorizedError)
end
end
end
context 'is disabled' do
before do
allow(Gitlab::ServiceDesk).to receive(:enabled?).and_return(false)
allow(Gitlab::ServiceDesk).to receive(:enabled?).with(project: project).and_return(false)
end
it 'does not create a comment' do
expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
end
end
end
end

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
expect(described_class.for(nil, '')).to be_nil
end
context 'new issue email' do
def handler_for(fixture, mail_key)
described_class.for(fixture_file(fixture), mail_key)
end
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
stub_config_setting(host: 'localhost')
end
let!(:user) { create(:user, email: 'jake@adventuretime.ooo', incoming_email_token: 'auth_token') }
context 'a Service Desk email' do
it 'uses the Service Desk handler' do
expect(handler_for('emails/service_desk.eml', 'some/project')).to be_instance_of(Gitlab::Email::Handler::ServiceDeskHandler)
end
end
it 'return new issue handler' do
expect(handler_for('emails/valid_new_issue.eml', 'some/project+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
end
end
end
describe 'regexps are set properly' do
let(:addresses) do
%W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX} sent_notification_key path-to-project-123-user_email_token-merge-request path-to-project-123-user_email_token-issue) +
%W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY} sent_notification_key path/to/project+merge-request+user_email_token path/to/project+user_email_token)
%W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX} sent_notification_key path-to-project-123-user_email_token-merge-request) +
%W(sent_notification_key#{Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY} sent_notification_key path-to-project-123-user_email_token-issue) +
%w(path/to/project+user_email_token path/to/project+merge-request+user_email_token some/project)
end
before do
allow(Gitlab::ServiceDesk).to receive(:supported?).and_return(true)
end
it 'picks each handler at least once' do
@ -46,12 +74,12 @@ RSpec.describe Gitlab::Email::Handler do
described_class.for(email, address).class
end
expect(matched_handlers.uniq).to match_array(ce_handlers)
expect(matched_handlers.uniq).to match_array(Gitlab::Email::Handler.handlers)
end
it 'can pick exactly one handler for each address' do
addresses.each do |address|
matched_handlers = ce_handlers.select do |handler|
matched_handlers = Gitlab::Email::Handler.handlers.select do |handler|
handler.new(email, address).can_handle?
end
@ -59,10 +87,4 @@ RSpec.describe Gitlab::Email::Handler do
end
end
end
def ce_handlers
@ce_handlers ||= Gitlab::Email::Handler.handlers.reject do |handler|
handler.name.start_with?('Gitlab::Email::Handler::EE::')
end
end
end

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
require 'fast_spec_helper'
require 'support/helpers/fixture_helpers'
RSpec.describe Sentry::PaginationParser do
include FixtureHelpers
describe '.parse' do
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'
end
end
context 'for service desk issues' do
before do
issue.update!(service_desk_reply_to: 'service.desk@example.com')
end
def expect_sender(username)
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(username)
expect(sender.address).to eq(gitlab_sender)
end
describe 'thank you email' do
subject { described_class.service_desk_thank_you_email(issue.id) }
it_behaves_like 'an unsubscribeable thread'
it 'has the correct recipient' do
is_expected.to deliver_to('service.desk@example.com')
end
it 'has the correct subject and body' do
aggregate_failures do
is_expected.to have_referable_subject(issue, include_project: false, reply: true)
is_expected.to have_body_text("Thank you for your support request! We are tracking your request as ticket #{issue.to_reference}, and will respond as soon as we can.")
end
end
it 'uses service bot name by default' do
expect_sender(User.support_bot.name)
end
context 'when custom outgoing name is set' do
let_it_be(:settings) { create(:service_desk_setting, project: project, outgoing_name: 'some custom name') }
it 'uses custom name in "from" header' do
expect_sender('some custom name')
end
end
context 'when custom outgoing name is empty' do
let_it_be(:settings) { create(:service_desk_setting, project: project, outgoing_name: '') }
it 'uses service bot name' do
expect_sender(User.support_bot.name)
end
end
end
describe 'new note email' do
let_it_be(:first_note) { create(:discussion_note_on_issue, note: 'Hello world') }
subject { described_class.service_desk_new_note_email(issue.id, first_note.id) }
it_behaves_like 'an unsubscribeable thread'
it 'has the correct recipient' do
is_expected.to deliver_to('service.desk@example.com')
end
it 'uses author\'s name in "from" header' do
expect_sender(first_note.author.name)
end
it 'has the correct subject and body' do
aggregate_failures do
is_expected.to have_referable_subject(issue, include_project: false, reply: true)
is_expected.to have_body_text(first_note.note)
end
end
end
end
end
context 'for a group' do

View File

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

View File

@ -661,15 +661,6 @@ RSpec.describe Event do
end
end
describe '.not_wiki_page' do
it 'does not contain the wiki page events' do
non_wiki_events = events.reject(&:wiki_page?)
expect(events).not_to match_array(non_wiki_events)
expect(described_class.not_wiki_page).to match_array(non_wiki_events)
end
end
describe '.for_wiki_meta' do
it 'finds events for a given wiki page metadata object' do
event = events.select(&:wiki_page?).first

View File

@ -289,4 +289,74 @@ RSpec.describe Clusters::ClusterPresenter do
it_behaves_like 'cluster health data'
end
end
describe '#gitlab_managed_apps_logs_path' do
context 'user can read logs' do
let(:project) { cluster.project }
before do
project.add_maintainer(user)
end
it 'returns path to logs' do
expect(presenter.gitlab_managed_apps_logs_path).to eq project_logs_path(project, cluster_id: cluster.id)
end
end
context 'group cluster' do
let(:cluster) { create(:cluster, cluster_type: :group_type, groups: [group]) }
let(:group) { create(:group, name: 'Foo') }
context 'user can read logs' do
before do
group.add_maintainer(user)
end
context 'there are projects within group' do
let!(:project) { create(:project, namespace: group) }
it 'returns path to logs' do
expect(presenter.gitlab_managed_apps_logs_path).to eq project_logs_path(project, cluster_id: cluster.id)
end
end
context 'there are no projects within group' do
it 'returns nil' do
expect(presenter.gitlab_managed_apps_logs_path).to be_nil
end
end
end
end
context 'instance cluster' do
let(:cluster) { create(:cluster, cluster_type: :instance_type) }
let!(:project) { create(:project) }
let(:user) { create(:admin) }
before do
project.add_maintainer(user)
stub_feature_flags(user_mode_in_session: false)
end
context 'user can read logs' do
it 'returns path to logs' do
expect(presenter.gitlab_managed_apps_logs_path).to eq project_logs_path(project, cluster_id: cluster.id)
end
end
end
context 'user can NOT read logs' do
let(:cluster) { create(:cluster, cluster_type: :instance_type) }
let!(:project) { create(:project) }
before do
project.add_developer(user)
stub_feature_flags(user_mode_in_session: false)
end
it 'returns nil' do
expect(presenter.gitlab_managed_apps_logs_path).to be_nil
end
end
end
end

View File

@ -1928,6 +1928,13 @@ RSpec.describe API::Projects do
end
end
end
it 'exposes service desk attributes' do
get api("/projects/#{project.id}", user)
expect(json_response).to have_key 'service_desk_enabled'
expect(json_response).to have_key 'service_desk_address'
end
end
describe 'GET /projects/:id/users' do

View File

@ -3,8 +3,13 @@
require 'spec_helper'
RSpec.describe ClusterEntity do
include Gitlab::Routing.url_helpers
describe '#as_json' do
subject { described_class.new(cluster).as_json }
let(:user) { nil }
let(:request) { EntityRequest.new({ current_user: user }) }
subject { described_class.new(cluster, request: request).as_json }
context 'when provider type is gcp' do
let(:cluster) { create(:cluster, :instance, provider_type: :gcp, provider_gcp: provider) }
@ -40,7 +45,7 @@ RSpec.describe ClusterEntity do
context 'when no application has been installed' do
let(:cluster) { create(:cluster, :instance) }
subject { described_class.new(cluster).as_json[:applications]}
subject { described_class.new(cluster, request: request).as_json[:applications]}
it 'contains helm as not_installable' do
expect(subject).not_to be_empty
@ -50,5 +55,28 @@ RSpec.describe ClusterEntity do
expect(helm[:status]).to eq(:not_installable)
end
end
context 'gitlab_managed_apps_logs_path' do
let(:cluster) { create(:cluster, :project) }
let(:user) { create(:user) }
subject { described_class.new(cluster, request: request).as_json }
before do
allow_next_instance_of(Clusters::ClusterPresenter) do |presenter|
allow(presenter).to receive(:show_path).and_return(nil)
end
end
it 'return projects log explorer path' do
log_explorer_path = project_logs_path(cluster.project, cluster_id: cluster.id)
expect_next_instance_of(Clusters::ClusterPresenter, cluster, current_user: user) do |presenter|
expect(presenter).to receive(:gitlab_managed_apps_logs_path).and_return(log_explorer_path)
end
expect(subject[:gitlab_managed_apps_logs_path]).to eq(log_explorer_path)
end
end
end
end

View File

@ -6,13 +6,14 @@ RSpec.describe ClusterSerializer do
let(:cluster) { create(:cluster, :project, provider_type: :user) }
describe '#represent_list' do
subject { described_class.new.represent_list(cluster).keys }
subject { described_class.new(current_user: nil).represent_list(cluster).keys }
it 'serializes attrs correctly' do
is_expected.to contain_exactly(
:cluster_type,
:enabled,
:environment_scope,
:gitlab_managed_apps_logs_path,
:name,
:nodes,
:path,
@ -22,7 +23,7 @@ RSpec.describe ClusterSerializer do
end
describe '#represent_status' do
subject { described_class.new.represent_status(cluster).keys }
subject { described_class.new(current_user: nil).represent_status(cluster).keys }
context 'when provider type is gcp and cluster is errored' do
let(:cluster) do

View File

@ -200,16 +200,6 @@ RSpec.describe EventCreateService do
expect(duplicate).to eq(event)
end
context 'the feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not create the event' do
expect { event }.not_to change(Event, :count)
end
end
end
end

View File

@ -247,14 +247,6 @@ RSpec.describe Git::WikiPushService, services: true do
end
end
context 'the wiki_events feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it_behaves_like 'a no-op push'
end
context 'the wiki_events_on_git_push feature is disabled' do
before do
stub_feature_flags(wiki_events_on_git_push: false)

View File

@ -14,21 +14,6 @@ RSpec.describe WikiPages::EventCreateService do
let(:action) { :created }
let(:response) { subject.execute(slug, page, action) }
context 'feature flag is not enabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not error' do
expect(response).to be_success
.and have_attributes(message: /No event created/)
end
it 'does not create an event' do
expect { response }.not_to change(Event, :count)
end
end
context 'the user is nil' do
subject { described_class.new(nil) }

View File

@ -63,16 +63,6 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type|
include_examples 'correct event created'
end
context 'the feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not record the activity' do
expect { service.execute }.not_to change(Event, :count)
end
end
context 'when the options are bad' do
let(:page_title) { '' }

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

View File

@ -67,16 +67,6 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type|
include_examples 'adds activity event'
end
context 'the feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not record the activity' do
expect { service.execute(page) }.not_to change(Event, :count)
end
end
context 'when the options are bad' do
let(:page_title) { '' }

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"
vue-eslint-parser "^7.0.0"
"@gitlab/svgs@1.151.0":
version "1.151.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.151.0.tgz#099905295d33eb31033f4a48eb3652da2f686239"
integrity sha512-2PTSM8CFhUjeTFKfcq6E/YwPpOVdSVWupf3NhKO/bz/cisSBS5P7aWxaXKIaxy28ySyBKEfKaAT6b4rXTwvVgg==
"@gitlab/svgs@1.152.0":
version "1.152.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.152.0.tgz#663c9a5f073f59b66f4241ef2d3fea2205846905"
integrity sha512-daZHOBVAwjsU6n60IycanoO/JymfQ36vrr46OUdWjHdp0ATYrgh+01LcxiSNLdlyndIRqHWGtwmuilokM9q6Vg==
"@gitlab/ui@17.22.1":
version "17.22.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.22.1.tgz#368578d04bb49011690911599c22a7d306f5fe99"
integrity sha512-elcu2gdvt1Afz3GMrIBQR+eujlA6JetLn44T1UzPHUhlaodT/w+TIj0+uPIbPiD7Oz6uR/sYwBqlZXQdBcVv3Q==
"@gitlab/ui@17.26.0":
version "17.26.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.26.0.tgz#d8efad47c3f4dc32e0586f3f5e4e2e3e0c2babf6"
integrity sha512-0QgzMK8MFGaqBB8yYntjYjUnzKFQ9a8d4mjufIyeKq6WomuMYHTFJgUj0+cEQ6uuTRtNk3MMuy3ZHBJg1wGzTw==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"