summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean McGivern <sean@mcgivern.me.uk>2017-06-02 14:03:15 +0000
committerSean McGivern <sean@mcgivern.me.uk>2017-06-02 14:03:15 +0000
commitdab266219441144a74b86fa27f4e0528810482d1 (patch)
tree1ac4d6a649cc568c38a1fff2a999811d432c5378
parent84b9aae9cfb8c319d06bcce1e93c2b1bc6f44877 (diff)
parent6838c16c49185fb72aa58a9c6591da4ec0fc7acb (diff)
downloadgitlab-ce-dab266219441144a74b86fa27f4e0528810482d1.tar.gz
Merge branch '31511-jira-settings' into 'master'
Simplify test&save actions when setting a service integration Closes #31511 See merge request !11599
-rw-r--r--app/assets/javascripts/flash.js34
-rw-r--r--app/assets/javascripts/gl_field_errors.js10
-rw-r--r--app/assets/javascripts/integrations/index.js7
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js123
-rw-r--r--app/assets/stylesheets/framework/flash.scss16
-rw-r--r--app/controllers/projects/services_controller.rb39
-rw-r--r--app/models/project_services/asana_service.rb3
-rw-r--r--app/models/project_services/assembla_service.rb2
-rw-r--r--app/models/project_services/bamboo_service.rb4
-rw-r--r--app/models/project_services/buildkite_service.rb4
-rw-r--r--app/models/project_services/campfire_service.rb4
-rw-r--r--app/models/project_services/chat_notification_service.rb6
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb6
-rw-r--r--app/models/project_services/deployment_service.rb4
-rw-r--r--app/models/project_services/drone_ci_service.rb4
-rw-r--r--app/models/project_services/external_wiki_service.rb2
-rw-r--r--app/models/project_services/flowdock_service.rb2
-rw-r--r--app/models/project_services/gemnasium_service.rb4
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/project_services/irker_service.rb2
-rw-r--r--app/models/project_services/issue_tracker_service.rb6
-rw-r--r--app/models/project_services/jira_service.rb12
-rw-r--r--app/models/project_services/mock_ci_service.rb7
-rw-r--r--app/models/project_services/mock_monitoring_service.rb4
-rw-r--r--app/models/project_services/pipelines_email_service.rb3
-rw-r--r--app/models/project_services/pivotaltracker_service.rb3
-rw-r--r--app/models/project_services/prometheus_service.rb3
-rw-r--r--app/models/project_services/pushover_service.rb6
-rw-r--r--app/models/project_services/teamcity_service.rb4
-rw-r--r--app/views/projects/services/_form.html.haml13
-rw-r--r--app/views/shared/_field.html.haml7
-rw-r--r--changelogs/unreleased/31511-jira-settings.yml4
-rw-r--r--config/routes/project.rb2
-rw-r--r--config/webpack.config.js1
-rw-r--r--features/project/service.feature26
-rw-r--r--features/steps/project/services.rb70
-rw-r--r--spec/controllers/projects/services_controller_spec.rb112
-rw-r--r--spec/factories/services.rb6
-rw-r--r--spec/features/projects/services/jira_service_spec.rb92
-rw-r--r--spec/features/projects/services/mattermost_slash_command_spec.rb18
-rw-r--r--spec/features/projects/services/slack_slash_command_spec.rb14
-rw-r--r--spec/javascripts/fixtures/services.rb31
-rw-r--r--spec/javascripts/integrations/integration_settings_form_spec.js199
-rw-r--r--spec/models/project_services/jira_service_spec.rb35
44 files changed, 726 insertions, 230 deletions
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index eec30624ff2..ccff8f0ace7 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -7,8 +7,21 @@ window.Flash = (function() {
return $(this).fadeOut();
};
- function Flash(message, type, parent) {
- var flash, textDiv;
+ /**
+ * Flash banner supports different types of Flash configurations
+ * along with ability to provide actionConfig which can be used to show
+ * additional action or link on banner next to message
+ *
+ * @param {String} message Flash message
+ * @param {String} type Type of Flash, it can be `notice` or `alert` (default)
+ * @param {Object} parent Reference to Parent element under which Flash needs to appear
+ * @param {Object} actionConfig Map of config to show action on banner
+ * @param {String} href URL to which action link should point (default '#')
+ * @param {String} title Title of action
+ * @param {Function} clickHandler Method to call when action is clicked on
+ */
+ function Flash(message, type, parent, actionConfig) {
+ var flash, textDiv, actionLink;
if (type == null) {
type = 'alert';
}
@@ -30,6 +43,23 @@ window.Flash = (function() {
text: message
});
textDiv.appendTo(flash);
+
+ if (actionConfig) {
+ const actionLinkConfig = {
+ class: 'flash-action',
+ href: actionConfig.href || '#',
+ text: actionConfig.title
+ };
+
+ if (!actionConfig.href) {
+ actionLinkConfig.role = 'button';
+ }
+
+ actionLink = $('<a/>', actionLinkConfig);
+
+ actionLink.appendTo(flash);
+ this.flashContainer.on('click', '.flash-action', actionConfig.clickHandler);
+ }
if (this.flashContainer.parent().hasClass('content-wrapper')) {
textDiv.addClass('container-fluid container-limited');
}
diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js
index 4f226ff96ea..4bef60264bb 100644
--- a/app/assets/javascripts/gl_field_errors.js
+++ b/app/assets/javascripts/gl_field_errors.js
@@ -31,9 +31,13 @@ class GlFieldErrors {
* and prevents disabling of invalid submit button by application.js */
catchInvalidFormSubmit (event) {
- if (!event.currentTarget.checkValidity()) {
- event.preventDefault();
- event.stopPropagation();
+ const $form = $(event.currentTarget);
+
+ if (!$form.attr('novalidate')) {
+ if (!event.currentTarget.checkValidity()) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
}
}
diff --git a/app/assets/javascripts/integrations/index.js b/app/assets/javascripts/integrations/index.js
new file mode 100644
index 00000000000..10fe6bac0e8
--- /dev/null
+++ b/app/assets/javascripts/integrations/index.js
@@ -0,0 +1,7 @@
+/* eslint-disable no-new */
+import IntegrationSettingsForm from './integration_settings_form';
+
+$(() => {
+ const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ integrationSettingsForm.init();
+});
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
new file mode 100644
index 00000000000..ddd3a6aab99
--- /dev/null
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -0,0 +1,123 @@
+/* global Flash */
+
+export default class IntegrationSettingsForm {
+ constructor(formSelector) {
+ this.$form = $(formSelector);
+
+ // Form Metadata
+ this.canTestService = this.$form.data('can-test');
+ this.testEndPoint = this.$form.data('test-url');
+
+ // Form Child Elements
+ this.$serviceToggle = this.$form.find('#service_active');
+ this.$submitBtn = this.$form.find('button[type="submit"]');
+ this.$submitBtnLoader = this.$submitBtn.find('.js-btn-spinner');
+ this.$submitBtnLabel = this.$submitBtn.find('.js-btn-label');
+ }
+
+ init() {
+ // Initialize View
+ this.toggleServiceState(this.$serviceToggle.is(':checked'));
+
+ // Bind Event Listeners
+ this.$serviceToggle.on('change', e => this.handleServiceToggle(e));
+ this.$submitBtn.on('click', e => this.handleSettingsSave(e));
+ }
+
+ handleSettingsSave(e) {
+ // Check if Service is marked active, as if not marked active,
+ // We can skip testing it and directly go ahead to allow form to
+ // be submitted
+ if (!this.$serviceToggle.is(':checked')) {
+ return;
+ }
+
+ // Service was marked active so now we check;
+ // 1) If form contents are valid
+ // 2) If this service can be tested
+ // If both conditions are true, we override form submission
+ // and test the service using provided configuration.
+ if (this.$form.get(0).checkValidity() && this.canTestService) {
+ e.preventDefault();
+ this.testSettings(this.$form.serialize());
+ }
+ }
+
+ handleServiceToggle(e) {
+ this.toggleServiceState($(e.currentTarget).is(':checked'));
+ }
+
+ /**
+ * Change Form's validation enforcement based on service status (active/inactive)
+ */
+ toggleServiceState(serviceActive) {
+ this.toggleSubmitBtnLabel(serviceActive);
+ if (serviceActive) {
+ this.$form.removeAttr('novalidate');
+ } else if (!this.$form.attr('novalidate')) {
+ this.$form.attr('novalidate', 'novalidate');
+ }
+ }
+
+ /**
+ * Toggle Submit button label based on Integration status and ability to test service
+ */
+ toggleSubmitBtnLabel(serviceActive) {
+ let btnLabel = 'Save changes';
+
+ if (serviceActive && this.canTestService) {
+ btnLabel = 'Test settings and save changes';
+ }
+
+ this.$submitBtnLabel.text(btnLabel);
+ }
+
+ /**
+ * Toggle Submit button state based on provided boolean value of `saveTestActive`
+ * When enabled, it does two things, and reverts back when disabled
+ *
+ * 1. It shows load spinner on submit button
+ * 2. Makes submit button disabled
+ */
+ toggleSubmitBtnState(saveTestActive) {
+ if (saveTestActive) {
+ this.$submitBtn.disable();
+ this.$submitBtnLoader.removeClass('hidden');
+ } else {
+ this.$submitBtn.enable();
+ this.$submitBtnLoader.addClass('hidden');
+ }
+ }
+
+ /* eslint-disable promise/catch-or-return, no-new */
+ /**
+ * Test Integration config
+ */
+ testSettings(formData) {
+ this.toggleSubmitBtnState(true);
+ $.ajax({
+ type: 'PUT',
+ url: this.testEndPoint,
+ data: formData,
+ })
+ .done((res) => {
+ if (res.error) {
+ new Flash(res.message, null, null, {
+ title: 'Save anyway',
+ clickHandler: (e) => {
+ e.preventDefault();
+ this.$form.submit();
+ },
+ });
+ } else {
+ this.$form.submit();
+ }
+ })
+ .fail(() => {
+ new Flash('Something went wrong on our end.');
+ })
+ .always(() => {
+ this.toggleSubmitBtnState(false);
+ });
+ }
+}
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 25b4feca3c3..38d884bc7eb 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -16,6 +16,22 @@
@extend .alert;
@extend .alert-danger;
margin: 0;
+
+ .flash-text,
+ .flash-action {
+ display: inline-block;
+ }
+
+ a.flash-action {
+ margin-left: 5px;
+ text-decoration: none;
+ font-weight: normal;
+ border-bottom: 1px solid;
+
+ &:hover {
+ border-color: transparent;
+ }
+ }
}
.flash-notice,
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index f9d798d0455..704f8cc8a79 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -4,6 +4,7 @@ class Projects::ServicesController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project!
before_action :service, only: [:edit, :update, :test]
+ before_action :update_service, only: [:update, :test]
respond_to :html
@@ -13,36 +14,46 @@ class Projects::ServicesController < Projects::ApplicationController
end
def update
- @service.assign_attributes(service_params[:service])
if @service.save(context: :manual_change)
- redirect_to(
- edit_namespace_project_service_path(@project.namespace, @project, @service.to_param),
- notice: 'Successfully updated.'
- )
+ redirect_to(namespace_project_settings_integrations_path(@project.namespace, @project), notice: success_message)
else
render 'edit'
end
end
def test
- return render_404 unless @service.can_test?
+ message = {}
+
+ if @service.can_test?
+ data = @service.test_data(project, current_user)
+ outcome = @service.test(data)
- data = @service.test_data(project, current_user)
- outcome = @service.test(data)
+ unless outcome[:success]
+ message = { error: true, message: 'Test failed.', service_response: outcome[:result].to_s }
+ end
- if outcome[:success]
- message = { notice: 'We sent a request to the provided URL' }
+ status = :ok
else
- error_message = "We tried to send a request to the provided URL but an error occurred"
- error_message << ": #{outcome[:result]}" if outcome[:result].present?
- message = { alert: error_message }
+ status = :not_found
end
- redirect_back_or_default(options: message)
+ render json: message, status: status
end
private
+ def success_message
+ if @service.active?
+ "#{@service.title} activated."
+ else
+ "#{@service.title} settings saved, but not activated."
+ end
+ end
+
+ def update_service
+ @service.assign_attributes(service_params[:service])
+ end
+
def service
@service ||= @project.find_or_initialize_service(params[:id])
end
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index 3728f5642e4..9ce2d1153a7 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -34,7 +34,8 @@ http://app.asana.com/-/account_api'
{
type: 'text',
name: 'api_key',
- placeholder: 'User Personal Access Token. User must have access to task, all comments will be attributed to this user.'
+ placeholder: 'User Personal Access Token. User must have access to task, all comments will be attributed to this user.',
+ required: true
},
{
type: 'text',
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
index aeeff8917bf..ae6af732ed4 100644
--- a/app/models/project_services/assembla_service.rb
+++ b/app/models/project_services/assembla_service.rb
@@ -18,7 +18,7 @@ class AssemblaService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: '' },
+ { type: 'text', name: 'token', placeholder: '', required: true },
{ type: 'text', name: 'subdomain', placeholder: '' }
]
end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index 3f5b3eb159b..42939ea0ec8 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -47,9 +47,9 @@ class BambooService < CiService
def fields
[
{ type: 'text', name: 'bamboo_url',
- placeholder: 'Bamboo root URL like https://bamboo.example.com' },
+ placeholder: 'Bamboo root URL like https://bamboo.example.com', required: true },
{ type: 'text', name: 'build_key',
- placeholder: 'Bamboo build plan key like KEY' },
+ placeholder: 'Bamboo build plan key like KEY', required: true },
{ type: 'text', name: 'username',
placeholder: 'A user with API access, if applicable' },
{ type: 'password', name: 'password' }
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 5fb95050b83..fc30f6e3365 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -58,11 +58,11 @@ class BuildkiteService < CiService
[
{ type: 'text',
name: 'token',
- placeholder: 'Buildkite project GitLab token' },
+ placeholder: 'Buildkite project GitLab token', required: true },
{ type: 'text',
name: 'project_url',
- placeholder: "#{ENDPOINT}/example/project" },
+ placeholder: "#{ENDPOINT}/example/project", required: true },
{ type: 'checkbox',
name: 'enable_ssl_verification',
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index 0de59af5652..c3f5b310619 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -18,7 +18,7 @@ class CampfireService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: '' },
+ { type: 'text', name: 'token', placeholder: '', required: true },
{ type: 'text', name: 'subdomain', placeholder: '' },
{ type: 'text', name: 'room', placeholder: '' }
]
@@ -76,7 +76,7 @@ class CampfireService < Service
# Returns a list of rooms, or [].
# https://github.com/basecamp/campfire-api/blob/master/sections/rooms.md#get-rooms
def rooms(auth)
- res = self.class.get("/rooms.json", auth)
+ res = self.class.get("/rooms.json", auth)
res.code == 200 ? res["rooms"] : []
end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 779ef54cfcb..6d1a321f651 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -21,10 +21,6 @@ class ChatNotificationService < Service
end
end
- def can_test?
- valid?
- end
-
def self.supported_events
%w[push issue confidential_issue merge_request note tag_push
pipeline wiki_page]
@@ -36,7 +32,7 @@ class ChatNotificationService < Service
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
+ { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}", required: true },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'checkbox', name: 'notify_only_default_branch' }
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index dea915a4d05..b9e3e982b64 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -31,9 +31,9 @@ class CustomIssueTrackerService < IssueTrackerService
[
{ type: 'text', name: 'title', placeholder: title },
{ type: 'text', name: 'description', placeholder: description },
- { type: 'text', name: 'project_url', placeholder: 'Project url' },
- { type: 'text', name: 'issues_url', placeholder: 'Issue url' },
- { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url' }
+ { type: 'text', name: 'project_url', placeholder: 'Project url', required: true },
+ { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true },
+ { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url', required: true }
]
end
end
diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb
index 91a55514a9a..5b8320158fc 100644
--- a/app/models/project_services/deployment_service.rb
+++ b/app/models/project_services/deployment_service.rb
@@ -30,4 +30,8 @@ class DeploymentService < Service
def terminals(environment)
raise NotImplementedError
end
+
+ def can_test?
+ false
+ end
end
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 2717c240f05..f6cade9c290 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -93,8 +93,8 @@ class DroneCiService < CiService
def fields
[
- { type: 'text', name: 'token', placeholder: 'Drone CI project specific token' },
- { type: 'text', name: 'drone_url', placeholder: 'http://drone.example.com' },
+ { type: 'text', name: 'token', placeholder: 'Drone CI project specific token', required: true },
+ { type: 'text', name: 'drone_url', placeholder: 'http://drone.example.com', required: true },
{ type: 'checkbox', name: 'enable_ssl_verification', title: "Enable SSL verification" }
]
end
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index b4d7c977ce4..720ad61162e 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -19,7 +19,7 @@ class ExternalWikiService < Service
def fields
[
- { type: 'text', name: 'external_wiki_url', placeholder: 'The URL of the external Wiki' }
+ { type: 'text', name: 'external_wiki_url', placeholder: 'The URL of the external Wiki', required: true }
]
end
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index 2a05d757eb4..2db95b9aaa3 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -18,7 +18,7 @@ class FlowdockService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: 'Flowdock Git source token' }
+ { type: 'text', name: 'token', placeholder: 'Flowdock Git source token', required: true }
]
end
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index f271e1f1739..017a9b2df6e 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -18,8 +18,8 @@ class GemnasiumService < Service
def fields
[
- { type: 'text', name: 'api_key', placeholder: 'Your personal API KEY on gemnasium.com ' },
- { type: 'text', name: 'token', placeholder: 'The project\'s slug on gemnasium.com' }
+ { type: 'text', name: 'api_key', placeholder: 'Your personal API KEY on gemnasium.com ', required: true },
+ { type: 'text', name: 'token', placeholder: 'The project\'s slug on gemnasium.com', required: true }
]
end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index c19fed339ba..e3906943ecd 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -33,7 +33,7 @@ class HipchatService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: 'Room token' },
+ { type: 'text', name: 'token', placeholder: 'Room token', required: true },
{ type: 'text', name: 'room', placeholder: 'Room name or ID' },
{ type: 'checkbox', name: 'notify' },
{ type: 'select', name: 'color', choices: %w(yellow red green purple gray random) },
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index a51d43adcb9..19357f90810 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -49,7 +49,7 @@ class IrkerService < Service
help: 'A default IRC URI to prepend before each recipient (optional)',
placeholder: 'irc://irc.network.net:6697/' },
{ type: 'textarea', name: 'recipients',
- placeholder: 'Recipients/channels separated by whitespaces',
+ placeholder: 'Recipients/channels separated by whitespaces', required: true,
help: 'Recipients have to be specified with a full URI: '\
'irc[s]://irc.network.net[:port]/#channel. Special cases: if '\
'you want the channel to be a nickname instead, append ",isnick" to ' \
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index eddf308eae3..ff138b9066d 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -32,9 +32,9 @@ class IssueTrackerService < Service
def fields
[
{ type: 'text', name: 'description', placeholder: description },
- { type: 'text', name: 'project_url', placeholder: 'Project url' },
- { type: 'text', name: 'issues_url', placeholder: 'Issue url' },
- { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url' }
+ { type: 'text', name: 'project_url', placeholder: 'Project url', required: true },
+ { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true },
+ { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url', required: true }
]
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 25d098b63c0..2450fb43212 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -86,11 +86,11 @@ class JiraService < IssueTrackerService
def fields
[
- { type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com' },
+ { type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com', required: true },
{ type: 'text', name: 'api_url', title: 'JIRA API URL', placeholder: 'If different from Web URL' },
- { type: 'text', name: 'project_key', placeholder: 'Project Key' },
- { type: 'text', name: 'username', placeholder: '' },
- { type: 'password', name: 'password', placeholder: '' },
+ { type: 'text', name: 'project_key', placeholder: 'Project Key', required: true },
+ { type: 'text', name: 'username', placeholder: '', required: true },
+ { type: 'password', name: 'password', placeholder: '', required: true },
{ type: 'text', name: 'jira_issue_transition_id', placeholder: '' }
]
end
@@ -175,10 +175,6 @@ class JiraService < IssueTrackerService
{ success: result.present?, result: result }
end
- def can_test?
- username.present? && password.present?
- end
-
# JIRA does not need test data.
# We are requesting the project that belongs to the project key.
def test_data(user = nil, project = nil)
diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb
index 546b6e0a498..72ddf9a4be3 100644
--- a/app/models/project_services/mock_ci_service.rb
+++ b/app/models/project_services/mock_ci_service.rb
@@ -21,7 +21,8 @@ class MockCiService < CiService
[
{ type: 'text',
name: 'mock_service_url',
- placeholder: 'http://localhost:4004' }
+ placeholder: 'http://localhost:4004',
+ required: true }
]
end
@@ -79,4 +80,8 @@ class MockCiService < CiService
:error
end
end
+
+ def can_test?
+ false
+ end
end
diff --git a/app/models/project_services/mock_monitoring_service.rb b/app/models/project_services/mock_monitoring_service.rb
index dd04e04e198..ed0318c6b27 100644
--- a/app/models/project_services/mock_monitoring_service.rb
+++ b/app/models/project_services/mock_monitoring_service.rb
@@ -14,4 +14,8 @@ class MockMonitoringService < MonitoringService
def metrics(environment)
JSON.parse(File.read(Rails.root + 'spec/fixtures/metrics.json'))
end
+
+ def can_test?
+ false
+ end
end
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index f824171ad09..9d37184be2c 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -53,7 +53,8 @@ class PipelinesEmailService < Service
[
{ type: 'textarea',
name: 'recipients',
- placeholder: 'Emails separated by comma' },
+ placeholder: 'Emails separated by comma',
+ required: true },
{ type: 'checkbox',
name: 'notify_only_broken_pipelines' }
]
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index d86f4f6f448..f9dfa2e91c3 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -23,7 +23,8 @@ class PivotaltrackerService < Service
{
type: 'text',
name: 'token',
- placeholder: 'Pivotal Tracker API token.'
+ placeholder: 'Pivotal Tracker API token.',
+ required: true
},
{
type: 'text',
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index ec72cb6856d..110b8bc209b 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -49,7 +49,8 @@ class PrometheusService < MonitoringService
type: 'text',
name: 'api_url',
title: 'API URL',
- placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/'
+ placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/',
+ required: true
}
]
end
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index fc29a5277bb..aa7bd4c3c84 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -19,10 +19,10 @@ class PushoverService < Service
def fields
[
- { type: 'text', name: 'api_key', placeholder: 'Your application key' },
- { type: 'text', name: 'user_key', placeholder: 'Your user key' },
+ { type: 'text', name: 'api_key', placeholder: 'Your application key', required: true },
+ { type: 'text', name: 'user_key', placeholder: 'Your user key', required: true },
{ type: 'text', name: 'device', placeholder: 'Leave blank for all active devices' },
- { type: 'select', name: 'priority', choices:
+ { type: 'select', name: 'priority', required: true, choices:
[
['Lowest Priority', -2],
['Low Priority', -1],
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index b16beb406b9..cbe137452bd 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -50,9 +50,9 @@ class TeamcityService < CiService
def fields
[
{ type: 'text', name: 'teamcity_url',
- placeholder: 'TeamCity root URL like https://teamcity.example.com' },
+ placeholder: 'TeamCity root URL like https://teamcity.example.com', required: true },
{ type: 'text', name: 'build_type',
- placeholder: 'Build configuration ID' },
+ placeholder: 'Build configuration ID', required: true },
{ type: 'text', name: 'username',
placeholder: 'A user with permissions to trigger a manual build' },
{ type: 'password', name: 'password' }
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index f1a80f1d5e1..9167789a69d 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -1,3 +1,6 @@
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag('integrations')
+
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
@@ -6,15 +9,17 @@
%p= @service.description
.col-lg-9
- = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form|
+ = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors form-horizontal js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_namespace_project_service_path } }) do |form|
= render 'shared/service_settings', form: form, subject: @service
.footer-block.row-content-block
- = form.submit 'Save changes', class: 'btn btn-save'
+ %button.btn.btn-save{ type: 'submit' }
+ = icon('spinner spin', class: 'hidden js-btn-spinner')
+ %span.js-btn-label
+ Save changes
&nbsp;
- if @service.valid? && @service.activated?
- unless @service.can_test?
- disabled_class = 'disabled'
- disabled_title = @service.disabled_title
- = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title
- = link_to "Cancel", namespace_project_settings_integrations_path(@project.namespace, @project), class: "btn btn-cancel"
+ = link_to 'Cancel', namespace_project_settings_integrations_path(@project.namespace, @project), class: 'btn btn-cancel'
diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml
index d74b0043949..795447a9ca6 100644
--- a/app/views/shared/_field.html.haml
+++ b/app/views/shared/_field.html.haml
@@ -3,6 +3,7 @@
- value = @service.send(name)
- type = field[:type]
- placeholder = field[:placeholder]
+- required = field[:required]
- choices = field[:choices]
- default_choice = field[:default_choice]
- help = field[:help]
@@ -14,14 +15,14 @@
= form.label name, title, class: "control-label"
.col-sm-10
- if type == 'text'
- = form.text_field name, class: "form-control", placeholder: placeholder
+ = form.text_field name, class: "form-control", placeholder: placeholder, required: required
- elsif type == 'textarea'
- = form.text_area name, rows: 5, class: "form-control", placeholder: placeholder
+ = form.text_area name, rows: 5, class: "form-control", placeholder: placeholder, required: required
- elsif type == 'checkbox'
= form.check_box name
- elsif type == 'select'
= form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" }
- elsif type == 'password'
- = form.password_field name, autocomplete: "new-password", class: "form-control"
+ = form.password_field name, autocomplete: "new-password", class: "form-control", required: value.blank? && :required
- if help
%span.help-block= help
diff --git a/changelogs/unreleased/31511-jira-settings.yml b/changelogs/unreleased/31511-jira-settings.yml
new file mode 100644
index 00000000000..4f9ddb13ef6
--- /dev/null
+++ b/changelogs/unreleased/31511-jira-settings.yml
@@ -0,0 +1,4 @@
+---
+title: Simplify testing and saving service integrations
+merge_request: 11599
+author:
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 5aac44fce10..14718e2f3c4 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -67,7 +67,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
member do
- get :test
+ put :test
end
end
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 89ef30a972e..c99298239b2 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -41,6 +41,7 @@ var config = {
group: './group.js',
groups_list: './groups_list.js',
issue_show: './issue_show/index.js',
+ integrations: './integrations',
locale: './locale/index.js',
main: './main.js',
merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
diff --git a/features/project/service.feature b/features/project/service.feature
index cce5f58adec..54f07ebca92 100644
--- a/features/project/service.feature
+++ b/features/project/service.feature
@@ -11,77 +11,77 @@ Feature: Project Services
When I visit project "Shop" services page
And I click hipchat service link
And I fill hipchat settings
- Then I should see hipchat service settings saved
+ Then I should see the Hipchat success message
Scenario: Activate hipchat service with custom server
When I visit project "Shop" services page
And I click hipchat service link
And I fill hipchat settings with custom server
- Then I should see hipchat service settings with custom server saved
+ Then I should see the Hipchat success message
Scenario: Activate pivotaltracker service
When I visit project "Shop" services page
And I click pivotaltracker service link
And I fill pivotaltracker settings
- Then I should see pivotaltracker service settings saved
+ Then I should see the Pivotaltracker success message
Scenario: Activate Flowdock service
When I visit project "Shop" services page
And I click Flowdock service link
And I fill Flowdock settings
- Then I should see Flowdock service settings saved
+ Then I should see the Flowdock success message
Scenario: Activate Assembla service
When I visit project "Shop" services page
And I click Assembla service link
And I fill Assembla settings
- Then I should see Assembla service settings saved
+ Then I should see the Assembla success message
Scenario: Activate Slack notifications service
When I visit project "Shop" services page
And I click Slack notifications service link
And I fill Slack notifications settings
- Then I should see Slack Notifications service settings saved
+ Then I should see the Slack notifications success message
Scenario: Activate Pushover service
When I visit project "Shop" services page
And I click Pushover service link
And I fill Pushover settings
- Then I should see Pushover service settings saved
+ Then I should see the Pushover success message
Scenario: Activate email on push service
When I visit project "Shop" services page
And I click email on push service link
And I fill email on push settings
- Then I should see email on push service settings saved
+ Then I should see the Emails on push success message
Scenario: Activate JIRA service
When I visit project "Shop" services page
And I click jira service link
And I fill jira settings
- Then I should see jira service settings saved
+ Then I should see the JIRA success message
Scenario: Activate Irker (IRC Gateway) service
When I visit project "Shop" services page
And I click Irker service link
And I fill Irker settings
- Then I should see Irker service settings saved
+ Then I should see the Irker success message
Scenario: Activate Atlassian Bamboo CI service
When I visit project "Shop" services page
And I click Atlassian Bamboo CI service link
And I fill Atlassian Bamboo CI settings
- Then I should see Atlassian Bamboo CI service settings saved
+ Then I should see the Bamboo success message
And I should see empty field Change Password
Scenario: Activate jetBrains TeamCity CI service
When I visit project "Shop" services page
And I click jetBrains TeamCity CI service link
And I fill jetBrains TeamCity CI settings
- Then I should see jetBrains TeamCity CI service settings saved
+ Then I should see the JetBrains success message
Scenario: Activate Asana service
When I visit project "Shop" services page
And I click Asana service link
And I fill Asana settings
- Then I should see Asana service settings saved
+ Then I should see the Asana success message
diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb
index 66368a159ec..6bac4df16f8 100644
--- a/features/steps/project/services.rb
+++ b/features/steps/project/services.rb
@@ -34,8 +34,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see hipchat service settings saved' do
- expect(find_field('Room').value).to eq 'gitlab'
+ step 'I should see the Hipchat success message' do
+ expect(page).to have_content 'HipChat activated.'
end
step 'I fill hipchat settings with custom server' do
@@ -46,10 +46,6 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see hipchat service settings with custom server saved' do
- expect(find_field('Server').value).to eq 'https://chat.example.com'
- end
-
step 'I click pivotaltracker service link' do
click_link 'PivotalTracker'
end
@@ -60,8 +56,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see pivotaltracker service settings saved' do
- expect(find_field('Token').value).to eq 'verySecret'
+ step 'I should see the Pivotaltracker success message' do
+ expect(page).to have_content 'PivotalTracker activated.'
end
step 'I click Flowdock service link' do
@@ -74,8 +70,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Flowdock service settings saved' do
- expect(find_field('Token').value).to eq 'verySecret'
+ step 'I should see the Flowdock success message' do
+ expect(page).to have_content 'Flowdock activated.'
end
step 'I click Assembla service link' do
@@ -88,8 +84,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Assembla service settings saved' do
- expect(find_field('Token').value).to eq 'verySecret'
+ step 'I should see the Assembla success message' do
+ expect(page).to have_content 'Assembla activated.'
end
step 'I click Asana service link' do
@@ -103,9 +99,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Asana service settings saved' do
- expect(find_field('Api key').value).to eq 'verySecret'
- expect(find_field('Restrict to branch').value).to eq 'master'
+ step 'I should see the Asana success message' do
+ expect(page).to have_content 'Asana activated.'
end
step 'I click email on push service link' do
@@ -113,12 +108,13 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
end
step 'I fill email on push settings' do
+ check 'Active'
fill_in 'Recipients', with: 'qa@company.name'
click_button 'Save'
end
- step 'I should see email on push service settings saved' do
- expect(find_field('Recipients').value).to eq 'qa@company.name'
+ step 'I should see the Emails on push success message' do
+ expect(page).to have_content 'Emails on push activated.'
end
step 'I click Irker service link' do
@@ -132,9 +128,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Irker service settings saved' do
- expect(find_field('Recipients').value).to eq 'irc://chat.freenode.net/#commits'
- expect(find_field('Colorize messages').value).to eq '1'
+ step 'I should see the Irker success message' do
+ expect(page).to have_content 'Irker (IRC gateway) activated.'
end
step 'I click Slack notifications service link' do
@@ -147,8 +142,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Slack Notifications service settings saved' do
- expect(find_field('Webhook').value).to eq 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685'
+ step 'I should see the Slack notifications success message' do
+ expect(page).to have_content 'Slack notifications activated.'
end
step 'I click Pushover service link' do
@@ -165,12 +160,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Pushover service settings saved' do
- expect(find_field('Api key').value).to eq 'verySecret'
- expect(find_field('User key').value).to eq 'verySecret'
- expect(find_field('Device').value).to eq 'myDevice'
- expect(find_field('Priority').find('option[selected]').value).to eq '1'
- expect(find_field('Sound').find('option[selected]').value).to eq 'bike'
+ step 'I should see the Pushover success message' do
+ expect(page).to have_content 'Pushover activated.'
end
step 'I click jira service link' do
@@ -178,6 +169,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
end
step 'I fill jira settings' do
+ check 'Active'
+
fill_in 'Web URL', with: 'http://jira.example'
fill_in 'JIRA API URL', with: 'http://jira.example/api'
fill_in 'Username', with: 'gitlab'
@@ -186,11 +179,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see jira service settings saved' do
- expect(find_field('Web URL').value).to eq 'http://jira.example'
- expect(find_field('JIRA API URL').value).to eq 'http://jira.example/api'
- expect(find_field('Username').value).to eq 'gitlab'
- expect(find_field('Project Key').value).to eq 'GITLAB'
+ step 'I should see the JIRA success message' do
+ expect(page).to have_content 'JIRA activated.'
end
step 'I click Atlassian Bamboo CI service link' do
@@ -206,13 +196,13 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Atlassian Bamboo CI service settings saved' do
- expect(find_field('Bamboo url').value).to eq 'http://bamboo.example.com'
- expect(find_field('Build key').value).to eq 'KEY'
- expect(find_field('Username').value).to eq 'user'
+ step 'I should see the Bamboo success message' do
+ expect(page).to have_content 'Atlassian Bamboo CI activated.'
end
step 'I should see empty field Change Password' do
+ click_link 'Atlassian Bamboo CI'
+
expect(find_field('Enter new password').value).to be_nil
end
@@ -229,9 +219,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see JetBrains TeamCity CI service settings saved' do
- expect(find_field('Teamcity url').value).to eq 'http://teamcity.example.com'
- expect(find_field('Build type').value).to eq 'GitlabTest_Build'
- expect(find_field('Username').value).to eq 'user'
+ step 'I should see the JetBrains success message' do
+ expect(page).to have_content 'JetBrains TeamCity CI activated.'
end
end
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index 2d892f4a2b7..23b463c0082 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -3,7 +3,9 @@ require 'spec_helper'
describe Projects::ServicesController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
- let(:service) { create(:service, project: project) }
+ let(:service) { create(:hipchat_service, project: project) }
+ let(:hipchat_client) { { '#room' => double(send: true) } }
+ let(:service_params) { { token: 'hipchat_token_p', room: '#room' } }
before do
sign_in(user)
@@ -13,97 +15,81 @@ describe Projects::ServicesController do
controller.instance_variable_set(:@service, service)
end
- shared_examples_for 'services controller' do |referrer|
- before do
- request.env["HTTP_REFERER"] = referrer
- end
-
- describe "#test" do
- context 'when can_test? returns false' do
- it 'renders 404' do
- allow_any_instance_of(Service).to receive(:can_test?).and_return(false)
+ describe '#test' do
+ context 'when can_test? returns false' do
+ it 'renders 404' do
+ allow_any_instance_of(Service).to receive(:can_test?).and_return(false)
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id
- expect(response).to have_http_status(404)
- end
+ expect(response).to have_http_status(404)
end
+ end
- context 'success' do
- context 'with empty project' do
- let(:project) { create(:empty_project) }
-
- context 'with chat notification service' do
- let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') }
-
- it 'redirects and show success message' do
- allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
-
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ context 'success' do
+ context 'with empty project' do
+ let(:project) { create(:empty_project) }
- expect(response).to redirect_to(root_path)
- expect(flash[:notice]).to eq('We sent a request to the provided URL')
- end
- end
+ context 'with chat notification service' do
+ let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') }
- it 'redirects and show success message' do
- expect(service).to receive(:test).and_return(success: true, result: 'done')
+ it 'returns success' do
+ allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id
- expect(response).to redirect_to(root_path)
- expect(flash[:notice]).to eq('We sent a request to the provided URL')
+ expect(response.status).to eq(200)
end
end
- it "redirects and show success message" do
- expect(service).to receive(:test).and_return(success: true, result: 'done')
+ it 'returns success' do
+ expect(HipChat::Client).to receive(:new).with('hipchat_token_p', anything).and_return(hipchat_client)
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: service_params
- expect(response).to redirect_to(root_path)
- expect(flash[:notice]).to eq('We sent a request to the provided URL')
+ expect(response.status).to eq(200)
end
end
- context 'failure' do
- it "redirects and show failure message" do
- expect(service).to receive(:test).and_return(success: false, result: 'Bad test')
+ it 'returns success' do
+ expect(HipChat::Client).to receive(:new).with('hipchat_token_p', anything).and_return(hipchat_client)
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: service_params
- expect(response).to redirect_to(root_path)
- expect(flash[:alert]).to eq('We tried to send a request to the provided URL but an error occurred: Bad test')
- end
+ expect(response.status).to eq(200)
end
end
- end
- describe 'referrer defined' do
- it_should_behave_like 'services controller' do
- let!(:referrer) { "/" }
- end
- end
+ context 'failure' do
+ it 'returns success status code and the error message' do
+ expect(HipChat::Client).to receive(:new).with('hipchat_token_p', anything).and_raise('Bad test')
- describe 'referrer undefined' do
- it_should_behave_like 'services controller' do
- let!(:referrer) { nil }
+ put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: service_params
+
+ expect(response.status).to eq(200)
+ expect(JSON.parse(response.body)).
+ to eq('error' => true, 'message' => 'Test failed.', 'service_response' => 'Bad test')
+ end
end
end
describe 'PUT #update' do
- context 'on successful update' do
- it 'sets the flash' do
- expect(service).to receive(:to_param).and_return('hipchat')
- expect(service).to receive(:event_names).and_return(HipchatService.event_names)
+ context 'when param `active` is set to true' do
+ it 'activates the service and redirects to integrations paths' do
+ put :update,
+ namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: { active: true }
+
+ expect(response).to redirect_to(namespace_project_settings_integrations_path(project.namespace, project))
+ expect(flash[:notice]).to eq 'HipChat activated.'
+ end
+ end
+ context 'when param `active` is set to false' do
+ it 'does not activate the service but saves the settings' do
put :update,
- namespace_id: project.namespace.id,
- project_id: project.id,
- id: service.id,
- service: { active: false }
+ namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: { active: false }
- expect(flash[:notice]).to eq 'Successfully updated.'
+ expect(flash[:notice]).to eq 'HipChat settings saved, but not activated.'
end
end
end
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index 3fad4d2d658..e7366a7fd1c 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -33,4 +33,10 @@ FactoryGirl.define do
project_key: 'jira-key'
)
end
+
+ factory :hipchat_service do
+ project factory: :empty_project
+ type 'HipchatService'
+ token 'test_token'
+ end
end
diff --git a/spec/features/projects/services/jira_service_spec.rb b/spec/features/projects/services/jira_service_spec.rb
new file mode 100644
index 00000000000..c96d87e5708
--- /dev/null
+++ b/spec/features/projects/services/jira_service_spec.rb
@@ -0,0 +1,92 @@
+require 'spec_helper'
+
+feature 'Setup Jira service', :feature, :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:service) { project.create_jira_service }
+
+ let(:url) { 'http://jira.example.com' }
+ let(:project_url) { 'http://username:password@jira.example.com/rest/api/2/project/GitLabProject' }
+
+ def fill_form(active = true)
+ check 'Active' if active
+
+ fill_in 'service_url', with: url
+ fill_in 'service_project_key', with: 'GitLabProject'
+ fill_in 'service_username', with: 'username'
+ fill_in 'service_password', with: 'password'
+ fill_in 'service_jira_issue_transition_id', with: '25'
+ end
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+
+ visit namespace_project_settings_integrations_path(project.namespace, project)
+ end
+
+ describe 'user sets and activates Jira Service' do
+ context 'when Jira connection test succeeds' do
+ before do
+ WebMock.stub_request(:get, project_url)
+ end
+
+ it 'activates the JIRA service' do
+ click_link('JIRA')
+ fill_form
+ click_button('Test settings and save changes')
+ wait_for_requests
+
+ expect(page).to have_content('JIRA activated.')
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ end
+ end
+
+ context 'when Jira connection test fails' do
+ before do
+ WebMock.stub_request(:get, project_url).to_return(status: 401)
+ end
+
+ it 'shows errors when some required fields are not filled in' do
+ click_link('JIRA')
+
+ check 'Active'
+ fill_in 'service_password', with: 'password'
+ click_button('Test settings and save changes')
+
+ page.within('.service-settings') do
+ expect(page).to have_content('This field is required.')
+ end
+ end
+
+ it 'activates the JIRA service' do
+ click_link('JIRA')
+ fill_form
+ click_button('Test settings and save changes')
+ wait_for_requests
+
+ expect(find('.flash-container-page')).to have_content 'Test failed.'
+ expect(find('.flash-container-page')).to have_content 'Save anyway'
+
+ find('.flash-alert .flash-action').trigger('click')
+ wait_for_requests
+
+ expect(page).to have_content('JIRA activated.')
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ end
+ end
+ end
+
+ describe 'user sets Jira Service but keeps it disabled' do
+ context 'when Jira connection test succeeds' do
+ it 'activates the JIRA service' do
+ click_link('JIRA')
+ fill_form(false)
+ click_button('Save changes')
+
+ expect(page).to have_content('JIRA settings saved, but not activated.')
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb
index dc3854262e7..1fe82222e59 100644
--- a/spec/features/projects/services/mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/mattermost_slash_command_spec.rb
@@ -24,15 +24,25 @@ feature 'Setup Mattermost slash commands', :feature, :js do
expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end
- it 'shows the token after saving' do
+ it 'redirects to the integrations page after saving but not activating' do
token = ('a'..'z').to_a.join
fill_in 'service_token', with: token
- click_on 'Save'
+ click_on 'Save changes'
- value = find_field('service_token').value
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ expect(page).to have_content('Mattermost slash commands settings saved, but not activated.')
+ end
+
+ it 'redirects to the integrations page after activating' do
+ token = ('a'..'z').to_a.join
+
+ fill_in 'service_token', with: token
+ check 'service_active'
+ click_on 'Save changes'
- expect(value).to eq(token)
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ expect(page).to have_content('Mattermost slash commands activated.')
end
it 'shows the add to mattermost button' do
diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/slack_slash_command_spec.rb
index db903a0c8f0..f53b820c460 100644
--- a/spec/features/projects/services/slack_slash_command_spec.rb
+++ b/spec/features/projects/services/slack_slash_command_spec.rb
@@ -21,13 +21,21 @@ feature 'Slack slash commands', feature: true do
expect(page).to have_content('This service allows users to perform common')
end
- it 'shows the token after saving' do
+ it 'redirects to the integrations page after saving but not activating' do
fill_in 'service_token', with: 'token'
click_on 'Save'
- value = find_field('service_token').value
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ expect(page).to have_content('Slack slash commands settings saved, but not activated.')
+ end
+
+ it 'redirects to the integrations page after activating' do
+ fill_in 'service_token', with: 'token'
+ check 'service_active'
+ click_on 'Save'
- expect(value).to eq('token')
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ expect(page).to have_content('Slack slash commands activated.')
end
it 'shows the correct trigger url' do
diff --git a/spec/javascripts/fixtures/services.rb b/spec/javascripts/fixtures/services.rb
new file mode 100644
index 00000000000..554451d1bbf
--- /dev/null
+++ b/spec/javascripts/fixtures/services.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') }
+ let!(:service) { create(:custom_issue_tracker_service, project: project, title: 'Custom Issue Tracker') }
+
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('services/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'services/edit_service.html.raw' do |example|
+ get :edit,
+ namespace_id: namespace,
+ project_id: project,
+ id: service.to_param
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js
new file mode 100644
index 00000000000..45909d4e70e
--- /dev/null
+++ b/spec/javascripts/integrations/integration_settings_form_spec.js
@@ -0,0 +1,199 @@
+import IntegrationSettingsForm from '~/integrations/integration_settings_form';
+
+describe('IntegrationSettingsForm', () => {
+ const FIXTURE = 'services/edit_service.html.raw';
+ preloadFixtures(FIXTURE);
+
+ beforeEach(() => {
+ loadFixtures(FIXTURE);
+ });
+
+ describe('contructor', () => {
+ let integrationSettingsForm;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ spyOn(integrationSettingsForm, 'init');
+ });
+
+ it('should initialize form element refs on class object', () => {
+ // Form Reference
+ expect(integrationSettingsForm.$form).toBeDefined();
+ expect(integrationSettingsForm.$form.prop('nodeName')).toEqual('FORM');
+
+ // Form Child Elements
+ expect(integrationSettingsForm.$serviceToggle).toBeDefined();
+ expect(integrationSettingsForm.$submitBtn).toBeDefined();
+ expect(integrationSettingsForm.$submitBtnLoader).toBeDefined();
+ expect(integrationSettingsForm.$submitBtnLabel).toBeDefined();
+ });
+
+ it('should initialize form metadata on class object', () => {
+ expect(integrationSettingsForm.testEndPoint).toBeDefined();
+ expect(integrationSettingsForm.canTestService).toBeDefined();
+ });
+ });
+
+ describe('toggleServiceState', () => {
+ let integrationSettingsForm;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ });
+
+ it('should remove `novalidate` attribute to form when called with `true`', () => {
+ integrationSettingsForm.toggleServiceState(true);
+
+ expect(integrationSettingsForm.$form.attr('novalidate')).not.toBeDefined();
+ });
+
+ it('should set `novalidate` attribute to form when called with `false`', () => {
+ integrationSettingsForm.toggleServiceState(false);
+
+ expect(integrationSettingsForm.$form.attr('novalidate')).toBeDefined();
+ });
+ });
+
+ describe('toggleSubmitBtnLabel', () => {
+ let integrationSettingsForm;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ });
+
+ it('should set Save button label to "Test settings and save changes" when serviceActive & canTestService are `true`', () => {
+ integrationSettingsForm.canTestService = true;
+
+ integrationSettingsForm.toggleSubmitBtnLabel(true);
+ expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Test settings and save changes');
+ });
+
+ it('should set Save button label to "Save changes" when either serviceActive or canTestService (or both) is `false`', () => {
+ integrationSettingsForm.canTestService = false;
+
+ integrationSettingsForm.toggleSubmitBtnLabel(false);
+ expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
+
+ integrationSettingsForm.toggleSubmitBtnLabel(true);
+ expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
+
+ integrationSettingsForm.canTestService = true;
+
+ integrationSettingsForm.toggleSubmitBtnLabel(false);
+ expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
+ });
+ });
+
+ describe('toggleSubmitBtnState', () => {
+ let integrationSettingsForm;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ });
+
+ it('should disable Save button and show loader animation when called with `true`', () => {
+ integrationSettingsForm.toggleSubmitBtnState(true);
+
+ expect(integrationSettingsForm.$submitBtn.is(':disabled')).toBeTruthy();
+ expect(integrationSettingsForm.$submitBtnLoader.hasClass('hidden')).toBeFalsy();
+ });
+
+ it('should enable Save button and hide loader animation when called with `false`', () => {
+ integrationSettingsForm.toggleSubmitBtnState(false);
+
+ expect(integrationSettingsForm.$submitBtn.is(':disabled')).toBeFalsy();
+ expect(integrationSettingsForm.$submitBtnLoader.hasClass('hidden')).toBeTruthy();
+ });
+ });
+
+ describe('testSettings', () => {
+ let integrationSettingsForm;
+ let formData;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ formData = integrationSettingsForm.$form.serialize();
+ });
+
+ it('should make an ajax request with provided `formData`', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ expect($.ajax).toHaveBeenCalledWith({
+ type: 'PUT',
+ url: integrationSettingsForm.testEndPoint,
+ data: formData,
+ });
+ });
+
+ it('should show error Flash with `Save anyway` action if ajax request responds with error in test', () => {
+ const errorMessage = 'Test failed.';
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ deferred.resolve({ error: true, message: errorMessage });
+
+ const $flashContainer = $('.flash-container');
+ expect($flashContainer.find('.flash-text').text()).toEqual(errorMessage);
+ expect($flashContainer.find('.flash-action')).toBeDefined();
+ expect($flashContainer.find('.flash-action').text()).toEqual('Save anyway');
+ });
+
+ it('should submit form if ajax request responds without any error in test', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ spyOn(integrationSettingsForm.$form, 'submit');
+ deferred.resolve({ error: false });
+
+ expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
+ });
+
+ it('should submit form when clicked on `Save anyway` action of error Flash', () => {
+ const errorMessage = 'Test failed.';
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ deferred.resolve({ error: true, message: errorMessage });
+
+ const $flashAction = $('.flash-container .flash-action');
+ expect($flashAction).toBeDefined();
+
+ spyOn(integrationSettingsForm.$form, 'submit');
+ $flashAction.trigger('click');
+ expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
+ });
+
+ it('should show error Flash if ajax request failed', () => {
+ const errorMessage = 'Something went wrong on our end.';
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ deferred.reject();
+
+ expect($('.flash-container .flash-text').text()).toEqual(errorMessage);
+ });
+
+ it('should always call `toggleSubmitBtnState` with `false` once request is completed', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ spyOn(integrationSettingsForm, 'toggleSubmitBtnState');
+ deferred.reject();
+
+ expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false);
+ });
+ });
+});
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 1920b5bf42b..0ee050196e4 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -69,41 +69,6 @@ describe JiraService, models: true do
end
end
- describe '#can_test?' do
- let(:jira_service) { described_class.new }
-
- it 'returns false if username is blank' do
- allow(jira_service).to receive_messages(
- url: 'http://jira.example.com',
- username: '',
- password: '12345678'
- )
-
- expect(jira_service.can_test?).to be_falsy
- end
-
- it 'returns false if password is blank' do
- allow(jira_service).to receive_messages(
- url: 'http://jira.example.com',
- username: 'tester',
- password: ''
- )
-
- expect(jira_service.can_test?).to be_falsy
- end
-
- it 'returns true if password and username are present' do
- jira_service = described_class.new
- allow(jira_service).to receive_messages(
- url: 'http://jira.example.com',
- username: 'tester',
- password: '12345678'
- )
-
- expect(jira_service.can_test?).to be_truthy
- end
- end
-
describe '#close_issue' do
let(:custom_base_url) { 'http://custom_url' }
let(:user) { create(:user) }