Commit 7770878c authored by Sean McGivern's avatar Sean McGivern

Merge branch 'feature/service-desk-be' into 'master'

Feature/service desk be

See merge request !1508
parents 6969cd97 95eca6fd
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */
import Clipboard from 'vendor/clipboard';
import Clipboard from 'clipboard';
var genericError, genericSuccess, showTooltip;
......
......@@ -49,6 +49,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import GeoNodes from './geo_nodes';
import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root';
const ShortcutsBlob = require('./shortcuts_blob');
......@@ -228,6 +229,13 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:activity':
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:edit':
const el = document.querySelector('.js-service-desk-setting-root');
if (el) {
const serviceDeskRoot = new ServiceDeskRoot(el);
serviceDeskRoot.init();
}
break;
case 'projects:show':
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
......
import eventHub from '../event_hub';
export default {
name: 'ServiceDeskSetting',
props: {
isEnabled: {
type: Boolean,
required: true,
},
incomingEmail: {
type: String,
required: false,
default: '',
},
fetchError: {
type: Error,
required: false,
default: null,
},
},
methods: {
onCheckboxToggle(e) {
const isChecked = e.target.checked;
eventHub.$emit('serviceDeskEnabledCheckboxToggled', isChecked);
},
},
template: `
<div>
<div class="checkbox">
<label for="service-desk-enabled-checkbox">
<input
type="checkbox"
id="service-desk-enabled-checkbox"
:checked="isEnabled"
@change="onCheckboxToggle($event)">
<span class="descr">
Activate service desk
</span>
</label>
</div>
<template v-if="isEnabled">
<div
class="panel-slim panel-default">
<div class="panel-heading">
<h3 class="panel-title">
Forward external support email address to:
</h3>
</div>
<div class="panel-body">
<template v-if="fetchError">
<i class="fa fa-exclamation-circle" aria-hidden="true" />
An error occurred while fetching the incoming email
</template>
<template v-else-if="incomingEmail">
<span ref="service-desk-incoming-email">
{{ incomingEmail }}
</span>
<button
class="btn btn-clipboard btn-transparent has-tooltip"
title="Copy incoming email address to clipboard"
:data-clipboard-text="incomingEmail"
@click.prevent>
<i class="fa fa-clipboard" aria-hidden="true" />
</button>
</template>
<template v-else>
<i class="fa fa-spinner fa-spin" aria-hidden="true" />
<span class="sr-only">
Fetching incoming email
</span>
</template>
</div>
</div>
<p class="settings-message">
We recommend you protect the external support email address.
Unblocked email spam would result in many spam issues being created,
and may disrupt your GitLab service.
</p>
</template>
</div>
`,
};
import Vue from 'vue';
export default new Vue();
/* eslint-disable no-new */
import Vue from 'vue';
import ServiceDeskSetting from './components/service_desk_setting';
import ServiceDeskStore from './stores/service_desk_store';
import ServiceDeskService from './services/service_desk_service';
import eventHub from './event_hub';
class ServiceDeskRoot {
constructor(wrapperElement) {
this.wrapperElement = wrapperElement;
const isEnabled = this.wrapperElement.dataset.enabled !== undefined &&
this.wrapperElement.dataset.enabled !== 'false';
const incomingEmail = this.wrapperElement.dataset.incomingEmail;
const endpoint = this.wrapperElement.dataset.endpoint;
this.store = new ServiceDeskStore({
isEnabled,
incomingEmail,
});
this.service = new ServiceDeskService(endpoint);
}
init() {
this.bindEvents();
if (this.store.state.isEnabled && !this.store.state.incomingEmail) {
this.fetchIncomingEmail();
}
this.render();
}
bindEvents() {
this.onEnableToggledWrapper = this.onEnableToggled.bind(this);
eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggledWrapper);
}
unbindEvents() {
eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggledWrapper);
}
render() {
this.vm = new Vue({
el: this.wrapperElement,
data: this.store.state,
template: `
<service-desk-setting
:isEnabled="isEnabled"
:incomingEmail="incomingEmail"
:fetchError="fetchError" />
`,
components: {
'service-desk-setting': ServiceDeskSetting,
},
});
}
fetchIncomingEmail() {
this.service.fetchIncomingEmail()
.then((incomingEmail) => {
this.store.setIncomingEmail(incomingEmail);
})
.catch((err) => {
this.store.setFetchError(err);
});
}
onEnableToggled(isChecked) {
this.store.setIsActivated(isChecked);
this.store.setIncomingEmail('');
this.store.setFetchError(null);
this.service.toggleServiceDesk(isChecked)
.then((incomingEmail) => {
this.store.setIncomingEmail(incomingEmail);
})
.catch((err) => {
this.store.setFetchError(err);
});
}
destroy() {
this.unbindEvents();
if (this.vm) {
this.vm.$destroy();
}
}
}
export default ServiceDeskRoot;
import Vue from 'vue';
import vueResource from 'vue-resource';
import '../../../vue_shared/vue_resource_interceptor';
Vue.use(vueResource);
class ServiceDeskService {
constructor(endpoint) {
this.serviceDeskResource = Vue.resource(`${endpoint}`);
}
fetchIncomingEmail() {
return this.serviceDeskResource.get()
.then((res) => {
const email = res.data.service_desk_address;
if (!email) {
throw new Error('Response didn\'t include `service_desk_address`');
}
return email;
});
}
toggleServiceDesk(enable) {
return this.serviceDeskResource.update({
service_desk_enabled: enable,
})
.then((res) => {
const email = res.data.service_desk_address;
if (enable && !email) {
throw new Error('Response didn\'t include `service_desk_address`');
}
return email;
});
}
}
export default ServiceDeskService;
class ServiceDeskStore {
constructor(initialState = {}) {
this.state = Object.assign({
isEnabled: false,
incomingEmail: '',
fetchError: null,
}, initialState);
}
setIsActivated(value) {
this.state.isEnabled = value;
}
setIncomingEmail(value) {
this.state.incomingEmail = value;
}
setFetchError(value) {
this.state.fetchError = value;
}
}
export default ServiceDeskStore;
.panel {
margin-bottom: $gl-padding;
@mixin panel {
.panel-heading {
padding: $gl-vert-padding $gl-padding;
line-height: 36px;
......@@ -48,3 +46,14 @@
line-height: inherit;
}
}
.panel {
@include panel;
margin-bottom: $gl-padding;
}
.panel-slim {
@extend .panel;
@include panel;
margin-bottom: $gl-vert-padding;
}
class Projects::ServiceDeskController < Projects::ApplicationController
before_action :authorize_admin_instance!, only: :update
before_action :authorize_admin_project!, only: :show
def show
json_response
end
def update
Projects::UpdateService.new(project, current_user, { service_desk_enabled: params[:service_desk_enabled] }).execute
json_response
end
private
def json_response
respond_to do |format|
service_desk_attributes =
{ service_desk_address: project.service_desk_address, service_desk_enabled: project.service_desk_enabled }
format.json { render json: service_desk_attributes }
end
end
def authorize_admin_instance!
return render_404 unless current_user.is_admin?
end
end
......@@ -349,6 +349,7 @@ class ProjectsController < Projects::ApplicationController
mirror_user_id
repository_size_limit
reset_approvals_on_push
service_desk_enabled
]
end
......
module Emails
module EE
module ServiceDesk
def service_desk_thank_you_email(issue_id)
setup_service_desk_mail(issue_id)
mail_new_thread(@issue, service_desk_options(@support_bot.id))
end
def service_desk_new_note_email(issue_id, note_id)
@note = Note.find(note_id)
setup_service_desk_mail(issue_id)
mail_answer_thread(@issue, service_desk_options(@note.author_id))
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(author_id)
{
from: sender(author_id),
to: @issue.service_desk_reply_to,
subject: "Re: #{@issue.title} (##{@issue.iid})"
}
end
end
end
end
......@@ -11,6 +11,8 @@ class Notify < BaseMailer
include Emails::Pipelines
include Emails::Members
include Emails::EE::ServiceDesk
helper MergeRequestsHelper
helper DiffHelper
helper BlobHelper
......
module EE
module Issue
# override
def check_for_spam?
author.support_bot? || super
end
# override
def subscribed_without_subscriptions?(user, *)
# TODO: this really shouldn't be necessary, because the support
# bot should be a participant (which is what the superclass
# method checks for). However, the support bot gets filtered out
# at the end of Participable#raw_participants as not being able
# to read the project. Overriding *that* behavior is problematic
# because it doesn't use the Policy framework, and instead uses a
# custom-coded Ability.users_that_can_read_project, which is...
# a pain to override in EE. So... here we say, the support bot
# is subscribed by default, until an unsubscribed record appears,
# even though it's not *technically* a participant in this issue.
# Making the support bot subscribed to every issue is not as bad as it
# seems, though, since it isn't permitted to :receive_notifications,
# and doesn't actually show up in the participants list.
user.support_bot? || super
end
end
end
......@@ -14,6 +14,8 @@ module EE
delegate :actual_shared_runners_minutes_limit,
:shared_runners_minutes_used?, to: :namespace
before_validation :auto_refresh_service_desk_key
end
def shared_runners_available?
......@@ -23,5 +25,33 @@ module EE
def shared_runners_minutes_limit_enabled?
!public? && shared_runners_enabled? && namespace.shared_runners_minutes_limit_enabled?
end
def service_desk_address
return nil unless service_desk_available?
refresh_service_desk_key! if service_desk_mail_key.blank?
from = "service_desk+#{service_desk_mail_key}"
::Gitlab::IncomingEmail.reply_address(from)
end
def refresh_service_desk_key!
return unless service_desk_available?
self.service_desk_mail_key = SentNotification.reply_key
end
private
def service_desk_available?
@service_desk_available ||=
EE::Gitlab::ServiceDesk.enabled? && service_desk_enabled?
end
def auto_refresh_service_desk_key
if service_desk_mail_key.blank? || service_desk_enabled_changed?
refresh_service_desk_key!
end
end
end
end
......@@ -17,6 +17,22 @@ module EE
validate :cannot_be_admin_and_auditor
end
module ClassMethods
def support_bot
email_pattern = "support%s@#{Settings.gitlab.host}"
unique_internal(where(support_bot: true), 'support-bot', email_pattern) do |u|
u.bio = 'The GitLab support bot used for Service Desk'
u.name = 'GitLab Support Bot'
end
end
# override
def internal_attributes
super + [:support_bot]
end
end
def cannot_be_admin_and_auditor
if admin? && auditor?
errors.add(:admin, "user cannot also be an Auditor.")
......
require 'carrierwave/orm/activerecord'
class Issue < ActiveRecord::Base
prepend EE::Issue
include InternalId
include Issuable
include Referable
......
module EE
module ProjectPolicy
def rules
super
guest_access! if user.support_bot?
end
def disabled_features!
raise NotImplementedError unless defined?(super)
......@@ -11,6 +17,11 @@ module EE
cannot! :push_code
cannot! :push_code_to_protected_branches
end
if @user&.support_bot? && !@subject.service_desk_enabled?
cannot! :create_note
cannot! :read_project
end
end
end
end
......@@ -10,6 +10,7 @@ class GlobalPolicy < BasePolicy
can! :access_api
can! :access_git
can! :receive_notifications
can! :use_slash_commands
end
end
end
require 'ee/gitlab/service_desk'
module EE
module NotificationService
# override
def send_new_note_notifications(note)
super
send_service_desk_notification(note)
end
def send_service_desk_notification(note)
return unless EE::Gitlab::ServiceDesk.enabled?
return unless note.noteable_type == 'Issue'
issue = note.noteable
support_bot = ::User.support_bot
return unless issue.service_desk_reply_to.present?
return unless issue.project.service_desk_enabled?
return if note.author == support_bot
return unless issue.subscribed?(support_bot, issue.project)
Notify.service_desk_new_note_email(issue.id, note.id).deliver_later
end
end
end
......@@ -6,6 +6,8 @@
# NotificationService.new.new_issue(issue, current_user)
#
class NotificationService
prepend EE::NotificationService
# Always notify user about ssh key added
# only if ssh key is not deploy key
#
......@@ -190,6 +192,10 @@ class NotificationService
# ignore gitlab service messages
return true if note.cross_reference? && note.system?
send_new_note_notifications(note)
end
def send_new_note_notifications(note)
notify_method = "note_#{note.to_ability_name}_email".to_sym
recipients = NotificationRecipientService.new(note.project).build_new_note_recipients(note)
......
......@@ -7,6 +7,8 @@ module SlashCommands
# Takes a text and interprets the commands that are extracted from it.
# Returns the content without commands, and hash of changes to be applied to a record.
def execute(content, issuable)
return [content, {}] unless current_user.can?(:use_slash_commands)
@issuable = issuable
@updates = {}
......
New response for issue #<%= @issue.iid %>:
Author: <%= @note.author_name %>
<%= @note.note %>
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:
<%= @sent_notification_url %>
= render "projects/settings/head"
.project-edit-container
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
......@@ -125,6 +126,16 @@
= render 'merge_request_settings', form: f
- if EE::Gitlab::ServiceDesk.enabled?
%hr
%fieldset.js-service-desk-setting-wrapper.features.append-bottom-default
%h5.prepend-top-0
Service Desk
= link_to icon('question-circle'), help_page_path("TODO")
.js-service-desk-setting-root{ data: { endpoint: namespace_project_service_desk_path(@project.namespace, @project),
enabled: @project.service_desk_enabled,
incoming_email: (@project.service_desk_address if @project.service_desk_enabled) } }
%hr
%fieldset.features.append-bottom-default
%h5.prepend-top-0
......
---
title: Add Service Desk feature
merge_request: 1508
author:
......@@ -155,6 +155,10 @@ constraints(ProjectUrlConstrainer.new) do
end
end
## EE-specific
get '/service_desk' => 'service_desk#show', as: :service_desk
put '/service_desk' => 'service_desk#update', as: :service_desk_refresh
resources :protected_branches, only: [:index, :show, :create, :update, :destroy, :patch], constraints: { id: Gitlab::Regex.git_reference_regex } do
scope module: :protected_branches do
resources :merge_access_levels, only: [:destroy]
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddServiceDeskSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
disable_ddl_transaction!
def up
add_column :users, :support_bot, :boolean
add_column :projects, :service_desk_enabled, :boolean
add_column :projects, :service_desk_mail_key, :string
add_column :issues, :service_desk_reply_to, :string
add_concurrent_index :users, :support_bot
add_concurrent_index :projects, :service_desk_mail_key, unique: true
end
def down
remove_column :users, :support_bot
remove_column :projects, :service_desk_enabled
remove_column :projects, :service_desk_mail_key
remove_column :issues, :service_desk_reply_to
end
end
......@@ -544,6 +544,7 @@ ActiveRecord::Schema.define(version: 20170405080720) do
t.integer "time_estimate"
t.integer "relative_position"
t.datetime "closed_at"
t.string "service_desk_reply_to"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
......@@ -1084,6 +1085,8 @@ ActiveRecord::Schema.define(version: 20170405080720) do
t.integer "sync_time", default: 60, null: false
t.boolean "printing_merge_request_link_enabled", default: true, null: false
t.string "import_jid"
t.boolean "service_desk_enabled"
t.string "service_desk_mail_key"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
......@@ -1099,6 +1102,7 @@ ActiveRecord::Schema.define(version: 20170405080720) do
add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree
add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree
add_index "projects", ["service_desk_mail_key"], name: "index_projects_on_service_desk_mail_key", unique: true, using: :btree
add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree
add_index "projects", ["sync_time"], name: "index_projects_on_sync_time", using: :btree
add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
......@@ -1459,6 +1463,7 @@ ActiveRecord::Schema.define(version: 20170405080720) do
t.boolean "auditor", default: false, null: false
t.boolean "ghost"
t.boolean "notified_of_own_activity"
t.boolean "support_bot"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
......@@ -1473,6 +1478,7 @@ ActiveRecord::Schema.define(version: 20170405080720) do
add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
add_index "users", ["state"], name: "index_users_on_state", using: :btree
add_index "users", ["support_bot"], name: "index_users_on_support_bot", using: :btree
add_index "users", ["username"], name: "index_users_on_username", using: :btree
add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"}
......
module Banzai
module ReferenceParser
module EE
module BaseParser
# override
# TODO: this override would make more sense in
# the policies framework, but CE currently
# manually checks for team membership and the like.
def nodes_user_can_reference(user, nodes)
return [] if user.support_bot?
super
end
end
end
end
end
module EE
module Gitlab
module ServiceDesk
def self.enabled?
::License.current && ::License.current.add_on?('GitLab_ServiceDesk')
end
end
end
end
......@@ -2,10 +2,17 @@ require 'gitlab/email/handler/create_note_handler'
require 'gitlab/email/handler/create_issue_handler'
require 'gitlab/email/handler/unsubscribe_handler'
require 'gitlab/email/handler/ee/service_desk_handler'
module Gitlab
module Email
module Handler
HANDLERS = [UnsubscribeHandler, CreateNoteHandler, CreateIssueHandler].freeze
HANDLERS = [
EE::ServiceDeskHandler,
UnsubscribeHandler,
CreateNoteHandler,
CreateIssueHandler,
].freeze
def self.for(mail, mail_key)
HANDLERS.find do |klass|
......
module Gitlab
module Email
module Handler
module EE
class ServiceDeskHandler < BaseHandler
include ReplyProcessing
def can_handle?
::EE::Gitlab::ServiceDesk.enabled? && service_desk_key.present?
end
def execute
raise ProjectNotFound if project.nil?
create_issue!
send_thank_you_email! if from_address
end
private
def service_desk_key
@service_desk_key ||=
begin
mail_key =~ /\Aservice_desk[+](\w+)\z/
$1
end
end
def project
return @project if instance_variable_defined?(:@project)
@project = Project.find_by(
service_desk_enabled: true,
service_desk_mail_key: service_desk_key
)
end
def create_issue!
# NB: the support bot is specifically forbidden
# from mentioning any entities, or from using
# slash commands.
@issue = Issues::CreateService.new(
project,
User.support_bot,
title: issue_title,
description: message,
confidential: true,
service_desk_reply_to: from_address,
).execute
raise InvalidIssueError unless @issue.persisted?
end
def send_thank_you_email!
Notify.service_desk_thank_you_email(@issue.id).deliver_later!
end
def from_address
(mail.reply_to || []).first || mail.sender || mail.from.first
end
def issue_title
from = "(from #{from_address})" if from_address
"Service Desk #{from}: #{mail.subject}"
end
end
end
end
end
end
......@@ -5,7 +5,7 @@
module Gitlab
module LDAP
class Adapter
prepend EE::Gitlab::LDAP::Adapter
prepend ::EE::Gitlab::LDAP::Adapter
attr_reader :provider, :ldap
......
module Gitlab
module LDAP
class Person
include EE::Gitlab::LDAP::Person
include ::EE::Gitlab::LDAP::Person
# Active Directory-specific LDAP filter that checks if bit 2 of the
# userAccountControl attribute is set.
......
require 'spec_helper'
describe Projects::ServiceDeskController do
let(:project) { create(:project_empty_repo, :private) }
let(:user) { create(:user, admin: true) }
before do
allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true }
project.update(service_desk_enabled: true)
project.add_master(user)
sign_in(user)
end
describe 'GET service desk properties' do
it 'returns service_desk JSON data' do
get :show, namespace_id: project.namespace.to_param, project_id: project, format: :json
body = JSON.parse(response.body)
expect(body["service_desk_address"]).to match(/\A[^@]+@[^@]+\z/)
expect(body["service_desk_enabled"]).to be_truthy
expect(response.status).to eq(200)
end
context 'when user is not project master' do
let(:guest) { create(:user) }
it 'renders 404' do
project.add_guest(guest)
sign_in(guest)
get :show, namespace_id: project.namespace.to_param, project_id: project, format: :json
expect(response.status).to eq(404)
end
end
end
describe 'PUT service desk properties' do
it 'toggles services desk incoming email' do
project.update(service_desk_enabled: true)
old_address = project.service_desk_address
project.update(service_desk_enabled: false)
put :update, namespace_id: project.namespace.to_param, project_id: project, service_desk_enabled: true, format: :json
body = JSON.parse(response.body)
expect(body["service_desk_address"]).to be_present
expect(body["service_desk_address"]).not_to eq(old_address)
expect(body["service_desk_enabled"]).to be_truthy
expect(response.status).to eq(200)
end
context 'when user is not admin' do
before { user.update(admin: false) }
it 'renders 404' do
put :update, namespace_id: project.namespace.to_param, project_id: project, service_desk_enabled: true, format: :json
expect(response.status).to eq(404)
end
end
end
end
require 'spec_helper'
describe 'Service Desk Setting', js: true, feature: true do
include WaitForAjax
let(:project) { create(:project_empty_repo, :private) }
let(:user) { create(:user) }
before do
project.add_master(user)
login_as(user)
allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true }
visit edit_namespace_project_path(project.namespace, project)
end
it 'shows service desk activation checkbox' do
expect(page).to have_selector("#service-desk-enabled-checkbox")
end
it 'shows incoming email after activating' do
find("#service-desk-enabled-checkbox").click
wait_for_ajax
expect(find('.js-service-desk-setting-wrapper .panel-body')).to have_content(project.service_desk_address)
end
end
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+service_desk+somemailkey@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
```
import Vue from 'vue';
import eventHub from '~/projects/settings_service_desk/event_hub';
import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting';
const createComponent = (propsData) => {
const Component = Vue.extend(ServiceDeskSetting);
return new Component({
el: document.createElement('div'),
propsData,
});
};
describe('ServiceDeskSetting', () => {
let vm;
afterEach(() => {
if (vm) {
vm.$destroy();
}
});
describe('when isEnabled=true', () => {
let el;
describe('only isEnabled', () => {
beforeEach(() => {
vm = createComponent({
isEnabled: true,
});
el = vm.$el;
});
it('see main panel with the email info', () => {
expect(el.querySelector('.panel')).toBeDefined();
});
it('see loading spinner', () => {
expect(el.querySelector('.fa-spinner')).toBeDefined();
expect(el.querySelector('.fa-exclamation-circle')).toBeNull();
expect(vm.$refs['service-desk-incoming-email']).toBeUndefined();
});
it('see warning message', () => {
expect(el.querySelector('.settings-message')).toBeDefined();
});
});
describe('with incomingEmail', () => {
beforeEach(() => {
vm = createComponent({
isEnabled: true,
incomingEmail: 'foo@bar.com',
});
el = vm.$el;
});
it('see email', () => {
expect(vm.$refs['service-desk-incoming-email'].textContent.trim()).toEqual('foo@bar.com');
expect(el.querySelector('.fa-spinner')).toBeNull();
expect(el.querySelector('.fa-exclamation-circle')).toBeNull();
});
});
describe('with fetchError', () => {
beforeEach(() => {
vm = createComponent({
isEnabled: true,
fetchError: new Error('some-fake-failure'),
});
el = vm.$el;
});
it('see error message', () => {
expect(el.querySelector('.fa-exclamation-circle')).toBeDefined();
expect(el.querySelector('.panel-body').textContent.trim()).toEqual('An error occurred while fetching the incoming email');
expect(el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$refs['service-desk-incoming-email']).toBeUndefined();
});
});
});
describe('when isEnabled=false', () => {
let el;
beforeEach(() => {
vm = createComponent({
isEnabled: false,
});
el = vm.$el;
});
it('should not see panel', () => {
expect(el.querySelector('.panel')).toBeNull();
});
it('should not see warning message', () => {
expect(el.querySelector('.settings-message')).toBeNull();
});
});
describe('methods', () => {
describe('onCheckboxToggle', () => {
let onCheckboxToggleSpy;
beforeEach(() => {
onCheckboxToggleSpy = jasmine.createSpy('spy');
eventHub.$on('serviceDeskEnabledCheckboxToggled', onCheckboxToggleSpy);
vm = createComponent({
isEnabled: false,
});
});
afterEach(() => {
eventHub.$off('serviceDeskEnabledCheckboxToggled', onCheckboxToggleSpy);
});
it('when getting checked', () => {
expect(onCheckboxToggleSpy).not.toHaveBeenCalled();
vm.onCheckboxToggle({
target: {
checked: true,
},
});
expect(onCheckboxToggleSpy).toHaveBeenCalledWith(true);
});
it('when getting unchecked', () => {
expect(onCheckboxToggleSpy).not.toHaveBeenCalled();
vm.onCheckboxToggle({
target: {
checked: false,
},
});
expect(onCheckboxToggleSpy).toHaveBeenCalledWith(false);
});
});
});
});
import ServiceDeskService from '~/projects/settings_service_desk/services/service_desk_service';
describe('ServiceDeskService', () => {
let service;
beforeEach(() => {
service = new ServiceDeskService('');
});
it('fetchIncomingEmail', (done) => {
spyOn(service.serviceDeskResource, 'get').and.returnValue(Promise.resolve({
data: {
service_desk_enabled: true,
service_desk_address: 'foo@bar.com',
},
}));
service.fetchIncomingEmail()
.then((incomingEmail) => {
expect(incomingEmail).toEqual('foo@bar.com');
done();
})
.catch((err) => {
done.fail(`Failed to fetch incoming email:\n${err}`);
});
});
describe('toggleServiceDesk', () => {
it('enable service desk', (done) => {
spyOn(service.serviceDeskResource, 'update').and.returnValue(Promise.resolve({
data: {
service_desk_enabled: true,
service_desk_address: 'foo@bar.com',
},
}));
service.toggleServiceDesk(true)
.then((incomingEmail) => {
expect(incomingEmail).toEqual('foo@bar.com');
done();
})
.catch((err) => {
done.fail(`Failed to enable service desk and fetch incoming email:\n${err}`);
});
});
it('disable service desk', (done) => {
spyOn(service.serviceDeskResource, 'update').and.returnValue(Promise.resolve({
data: {
service_desk_enabled: false,
service_desk_address: null,
},
}));
service.toggleServiceDesk(false)
.then((incomingEmail) => {
expect(incomingEmail).toEqual(null);
done();
})
.catch((err) => {
done.fail(`Failed to disable service desk and reset incoming email:\n${err}`);
});
});
});
});
import ServiceDeskStore from '~/projects/settings_service_desk/stores/service_desk_store';
describe('ServiceDeskStore', () => {
let store;
beforeEach(() => {
store = new ServiceDeskStore();
});
describe('setIsActivated', () => {
it('defaults to false', () => {
expect(store.state.isEnabled).toEqual(false);
});
it('set true', () => {
store.setIsActivated(true);
expect(store.state.isEnabled).toEqual(true);
});
it('set false', () => {
store.setIsActivated(false);
expect(store.state.isEnabled).toEqual(false);
});
});
describe('setIncomingEmail', () => {
it('defaults to an empty string', () => {
expect(store.state.incomingEmail).toEqual('');
});
it('set true', () => {
const email = 'foo@bar.com';
store.setIncomingEmail(email);
expect(store.state.incomingEmail).toEqual(email);
});
});
describe('setFetchError', () => {
it('defaults to null', () => {
expect(store.state.fetchError).toEqual(null);
});
it('set true', () => {
const err = new Error('some-fake-failure');
store.setFetchError(err);
expect(store.state.fetchError).toEqual(err);
});
});
});
require 'spec_helper'
require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::EE::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) { fixture_file('emails/service_desk.eml') }
let(:project) { create(:project, :public) }
context 'when service desk is enabled' do
before do
project.update(service_desk_enabled: true)
project.update(service_desk_mail_key: 'somemailkey')
allow(Notify).to receive(:service_desk_thank_you_email)
.with(kind_of(Integer)).and_return(double(deliver_later!: true))
allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true }
end
it 'sends thank you the email and creates issue' do
setup_attachment
expect(Notify).to receive(:service_desk_thank_you_email).with(kind_of(Integer))
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("Service desk stuff!\n\n```\na = b\n```\n\n![image](uploads/image.png)")
end
context 'when there is no from address' do
before do
allow_any_instance_of(described_class).to receive(:from_address)
.and_return(nil)
end
it "does not send thank you email but create an issue" do
expect(Notify).not_to receive(:service_desk_thank_you_email)
expect { receiver.execute }.to change { Issue.count }.by(1)
end
end
context 'when license does not support service desk' do
before do
allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { false }
end
it 'does not create an issue or send email' do
expect(Notify).not_to receive(:service_desk_thank_you_email)
expect { receiver.execute rescue nil }.not_to change { Issue.count }
end
end
end
context 'when service desk is not enabled' do
before do
project.update_attributes(
service_desk_enabled: false,
service_desk_mail_key: 'somemailkey',
)
end
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
end
require 'spec_helper'
describe Gitlab::Email::Handler, lib: true do
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
stub_config_setting(host: 'localhost')
end
describe '.for' do
def handler_for(fixture, mail_key)
described_class.for(fixture_file(fixture), mail_key)
end
context 'a Service Desk email' do
it 'uses the Service Desk handler when Service Desk is enabled' do
allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk').and_return(true)
expect(handler_for('emails/service_desk.eml', 'service_desk+auth_token')).to be_instance_of(Gitlab::Email::Handler::EE::ServiceDeskHandler)
end
it 'uses the create issue handler when Service Desk is disabled' do
allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk').and_return(false)
expect(handler_for('emails/service_desk.eml', 'service_desk+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
end
end
context 'a new issue email' do
let!(:user) { create(:user, email: 'jake@adventuretime.ooo', incoming_email_token: 'auth_token') }
it 'uses the create issue handler when Service Desk is enabled' do
allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk').and_return(true)
expect(handler_for('emails/valid_new_issue.eml', 'incoming+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
end
it 'uses the create issue handler when Service Desk is disabled' do
allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk').and_return(false)
expect(handler_for('emails/valid_new_issue.eml', 'incoming+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
end
end
end
end
......@@ -23,6 +23,7 @@ Issue:
- weight
- time_estimate
- relative_position
- service_desk_reply_to
Event:
- id
- target_type
......
......@@ -115,4 +115,38 @@ describe Project, models: true do
it { is_expected.to be_falsey }
end
end
describe '#regenerate_service_desk_key' do
before do
allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true }
end
subject { create(:project) }
it 'leaves it blank by default' do
expect(subject.service_desk_mail_key).to be_blank
end
it 'updates when enabled' do
subject.service_desk_enabled = true
subject.validate
expect(subject.service_desk_mail_key).not_to be_blank
end
it 'changes when enabled' do
subject.update!(service_desk_mail_key: '12345')
subject.service_desk_enabled = true
expect { subject.validate }.to change { subject.service_desk_mail_key }
end
it 'ensures mail key is never nil when enabled' do
subject.update!(service_desk_enabled: true)
expect { subject.update!(service_desk_mail_key: nil) }
.to change { subject.service_desk_mail_key }
expect(subject.service_desk_mail_key).not_to be_blank
end
end
end
require 'spec_helper'
describe EE::NotificationService do
let(:subject) { NotificationService.new }
before do
allow(Notify).to receive(:service_desk_new_note_email)
.with(kind_of(Integer), kind_of(Integer)).and_return(double(deliver_later: true))
allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true }
end
def should_email!
expect(Notify).to receive(:service_desk_new_note_email).with(issue.id, kind_of(Integer))
end
def should_not_email!
expect(Notify).not_to receive(:service_desk_new_note_email)
end
def execute!
subject.send_service_desk_notification(note)
end
def self.it_should_email!
it 'sends the email' do
should_email!
execute!
end
end
def self.it_should_not_email!
it 'doesn\'t send the email' do
should_not_email!
execute!
end
end
let(:issue) { create(:issue, author: User.support_bot) }
let(:project) { issue.project }
let(:note) { create(:note, noteable: issue, project: project) }
context 'a non-service-desk issue' do
it_should_not_email!
end
context 'a service-desk issue' do
before do
issue.update!(service_desk_reply_to: 'service.desk@example.com')
project.update!(service_desk_enabled: true)
end
it_should_email!
context 'where the project has disabled the feature' do
before do
project.update(service_desk_enabled: false)
end
it_should_not_email!
end
context 'when the license doesn\'t allow service desk' do
before do
expect(EE::Gitlab::ServiceDesk).to receive(:enabled?).and_return(false)
end
it_should_not_email!
end
context 'when the support bot has unsubscribed' do
before do
issue.unsubscribe(User.support_bot, project)
end
it_should_not_email!
end
end
end
This diff is collapsed.
......@@ -1074,6 +1074,14 @@ cli-width@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
clipboard@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.6.1.tgz#65c5b654812466b0faab82dc6ba0f1d2f8e4be53"
dependencies:
good-listener "^1.2.0"
select "^1.1.2"
tiny-emitter "^1.0.0"
cliui@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
......@@ -1383,6 +1391,10 @@ delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
delegate@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.1.2.tgz#1e1bc6f5cadda6cb6cbf7e6d05d0bcdd5712aebe"
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
......@@ -2180,6 +2192,12 @@ globby@^5.0.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
good-listener@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
dependencies:
delegate "^3.1.2"
graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
......@@ -3923,6 +3941,10 @@ select2@3.5.2-browserify:
version "3.5.2-browserify"
resolved "https://registry.yarnpkg.com/select2/-/select2-3.5.2-browserify.tgz#dc4dafda38d67a734e8a97a46f0d3529ae05391d"
select@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@~5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
......@@ -4341,6 +4363,10 @@ timers-browserify@^2.0.2:
dependencies:
setimmediate "^1.0.4"
tiny-emitter@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.1.0.tgz#ab405a21ffed814a76c19739648093d70654fecb"
tmp@0.0.28, tmp@0.0.x:
version "0.0.28"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment