Commit dab26621 authored by Sean McGivern's avatar Sean McGivern

Merge branch '31511-jira-settings' into 'master'

Simplify test&save actions when setting a service integration

Closes #31511

See merge request !11599
parents 84b9aae9 6838c16c
...@@ -7,8 +7,21 @@ window.Flash = (function() { ...@@ -7,8 +7,21 @@ window.Flash = (function() {
return $(this).fadeOut(); 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) { if (type == null) {
type = 'alert'; type = 'alert';
} }
...@@ -30,6 +43,23 @@ window.Flash = (function() { ...@@ -30,6 +43,23 @@ window.Flash = (function() {
text: message text: message
}); });
textDiv.appendTo(flash); 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')) { if (this.flashContainer.parent().hasClass('content-wrapper')) {
textDiv.addClass('container-fluid container-limited'); textDiv.addClass('container-fluid container-limited');
} }
......
...@@ -31,11 +31,15 @@ class GlFieldErrors { ...@@ -31,11 +31,15 @@ class GlFieldErrors {
* and prevents disabling of invalid submit button by application.js */ * and prevents disabling of invalid submit button by application.js */
catchInvalidFormSubmit (event) { catchInvalidFormSubmit (event) {
const $form = $(event.currentTarget);
if (!$form.attr('novalidate')) {
if (!event.currentTarget.checkValidity()) { if (!event.currentTarget.checkValidity()) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
} }
}
/* Public method for triggering validity updates manually */ /* Public method for triggering validity updates manually */
updateFormValidityState() { updateFormValidityState() {
......
/* eslint-disable no-new */
import IntegrationSettingsForm from './integration_settings_form';
$(() => {
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
});
/* 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);
});
}
}
...@@ -16,6 +16,22 @@ ...@@ -16,6 +16,22 @@
@extend .alert; @extend .alert;
@extend .alert-danger; @extend .alert-danger;
margin: 0; 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, .flash-notice,
......
...@@ -4,6 +4,7 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -4,6 +4,7 @@ class Projects::ServicesController < Projects::ApplicationController
# Authorize # Authorize
before_action :authorize_admin_project! before_action :authorize_admin_project!
before_action :service, only: [:edit, :update, :test] before_action :service, only: [:edit, :update, :test]
before_action :update_service, only: [:update, :test]
respond_to :html respond_to :html
...@@ -13,36 +14,46 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -13,36 +14,46 @@ class Projects::ServicesController < Projects::ApplicationController
end end
def update def update
@service.assign_attributes(service_params[:service])
if @service.save(context: :manual_change) if @service.save(context: :manual_change)
redirect_to( redirect_to(namespace_project_settings_integrations_path(@project.namespace, @project), notice: success_message)
edit_namespace_project_service_path(@project.namespace, @project, @service.to_param),
notice: 'Successfully updated.'
)
else else
render 'edit' render 'edit'
end end
end end
def test def test
return render_404 unless @service.can_test? message = {}
if @service.can_test?
data = @service.test_data(project, current_user) data = @service.test_data(project, current_user)
outcome = @service.test(data) outcome = @service.test(data)
if outcome[:success] unless outcome[:success]
message = { notice: 'We sent a request to the provided URL' } message = { error: true, message: 'Test failed.', service_response: outcome[:result].to_s }
end
status = :ok
else else
error_message = "We tried to send a request to the provided URL but an error occurred" status = :not_found
error_message << ": #{outcome[:result]}" if outcome[:result].present?
message = { alert: error_message }
end end
redirect_back_or_default(options: message) render json: message, status: status
end end
private 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 def service
@service ||= @project.find_or_initialize_service(params[:id]) @service ||= @project.find_or_initialize_service(params[:id])
end end
......
...@@ -34,7 +34,8 @@ http://app.asana.com/-/account_api' ...@@ -34,7 +34,8 @@ http://app.asana.com/-/account_api'
{ {
type: 'text', type: 'text',
name: 'api_key', 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', type: 'text',
......
...@@ -18,7 +18,7 @@ class AssemblaService < Service ...@@ -18,7 +18,7 @@ class AssemblaService < Service
def fields def fields
[ [
{ type: 'text', name: 'token', placeholder: '' }, { type: 'text', name: 'token', placeholder: '', required: true },
{ type: 'text', name: 'subdomain', placeholder: '' } { type: 'text', name: 'subdomain', placeholder: '' }
] ]
end end
......
...@@ -47,9 +47,9 @@ class BambooService < CiService ...@@ -47,9 +47,9 @@ class BambooService < CiService
def fields def fields
[ [
{ type: 'text', name: 'bamboo_url', { 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', { 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', { type: 'text', name: 'username',
placeholder: 'A user with API access, if applicable' }, placeholder: 'A user with API access, if applicable' },
{ type: 'password', name: 'password' } { type: 'password', name: 'password' }
......
...@@ -58,11 +58,11 @@ class BuildkiteService < CiService ...@@ -58,11 +58,11 @@ class BuildkiteService < CiService
[ [
{ type: 'text', { type: 'text',
name: 'token', name: 'token',
placeholder: 'Buildkite project GitLab token' }, placeholder: 'Buildkite project GitLab token', required: true },
{ type: 'text', { type: 'text',
name: 'project_url', name: 'project_url',
placeholder: "#{ENDPOINT}/example/project" }, placeholder: "#{ENDPOINT}/example/project", required: true },
{ type: 'checkbox', { type: 'checkbox',
name: 'enable_ssl_verification', name: 'enable_ssl_verification',
......
...@@ -18,7 +18,7 @@ class CampfireService < Service ...@@ -18,7 +18,7 @@ class CampfireService < Service
def fields def fields
[ [
{ type: 'text', name: 'token', placeholder: '' }, { type: 'text', name: 'token', placeholder: '', required: true },
{ type: 'text', name: 'subdomain', placeholder: '' }, { type: 'text', name: 'subdomain', placeholder: '' },
{ type: 'text', name: 'room', placeholder: '' } { type: 'text', name: 'room', placeholder: '' }
] ]
......
...@@ -21,10 +21,6 @@ class ChatNotificationService < Service ...@@ -21,10 +21,6 @@ class ChatNotificationService < Service
end end
end end
def can_test?
valid?
end
def self.supported_events def self.supported_events
%w[push issue confidential_issue merge_request note tag_push %w[push issue confidential_issue merge_request note tag_push
pipeline wiki_page] pipeline wiki_page]
...@@ -36,7 +32,7 @@ class ChatNotificationService < Service ...@@ -36,7 +32,7 @@ class ChatNotificationService < Service
def default_fields 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: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'checkbox', name: 'notify_only_default_branch' } { type: 'checkbox', name: 'notify_only_default_branch' }
......
...@@ -31,9 +31,9 @@ class CustomIssueTrackerService < IssueTrackerService ...@@ -31,9 +31,9 @@ class CustomIssueTrackerService < IssueTrackerService
[ [
{ type: 'text', name: 'title', placeholder: title }, { type: 'text', name: 'title', placeholder: title },
{ type: 'text', name: 'description', placeholder: description }, { type: 'text', name: 'description', placeholder: description },
{ type: 'text', name: 'project_url', placeholder: 'Project url' }, { type: 'text', name: 'project_url', placeholder: 'Project url', required: true },
{ type: 'text', name: 'issues_url', placeholder: 'Issue url' }, { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true },
{ type: 'text', name: 'new_issue_url', placeholder: 'New Issue url' } { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url', required: true }
] ]
end end
end end
...@@ -30,4 +30,8 @@ class DeploymentService < Service ...@@ -30,4 +30,8 @@ class DeploymentService < Service
def terminals(environment) def terminals(environment)
raise NotImplementedError raise NotImplementedError
end end
def can_test?
false
end
end end
...@@ -93,8 +93,8 @@ class DroneCiService < CiService ...@@ -93,8 +93,8 @@ class DroneCiService < CiService
def fields def fields
[ [
{ type: 'text', name: 'token', placeholder: 'Drone CI project specific token' }, { type: 'text', name: 'token', placeholder: 'Drone CI project specific token', required: true },
{ type: 'text', name: 'drone_url', placeholder: 'http://drone.example.com' }, { type: 'text', name: 'drone_url', placeholder: 'http://drone.example.com', required: true },
{ type: 'checkbox', name: 'enable_ssl_verification', title: "Enable SSL verification" } { type: 'checkbox', name: 'enable_ssl_verification', title: "Enable SSL verification" }
] ]
end end
......
...@@ -19,7 +19,7 @@ class ExternalWikiService < Service ...@@ -19,7 +19,7 @@ class ExternalWikiService < Service
def fields 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 end
......
...@@ -18,7 +18,7 @@ class FlowdockService < Service ...@@ -18,7 +18,7 @@ class FlowdockService < Service
def fields def fields
[ [
{ type: 'text', name: 'token', placeholder: 'Flowdock Git source token' } { type: 'text', name: 'token', placeholder: 'Flowdock Git source token', required: true }
] ]
end end
......
...@@ -18,8 +18,8 @@ class GemnasiumService < Service ...@@ -18,8 +18,8 @@ class GemnasiumService < Service
def fields def fields
[ [
{ type: 'text', name: 'api_key', placeholder: 'Your personal API KEY 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' } { type: 'text', name: 'token', placeholder: 'The project\'s slug on gemnasium.com', required: true }
] ]
end end
......
...@@ -33,7 +33,7 @@ class HipchatService < Service ...@@ -33,7 +33,7 @@ class HipchatService < Service
def fields 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: 'text', name: 'room', placeholder: 'Room name or ID' },
{ type: 'checkbox', name: 'notify' }, { type: 'checkbox', name: 'notify' },
{ type: 'select', name: 'color', choices: %w(yellow red green purple gray random) }, { type: 'select', name: 'color', choices: %w(yellow red green purple gray random) },
......
...@@ -49,7 +49,7 @@ class IrkerService < Service ...@@ -49,7 +49,7 @@ class IrkerService < Service
help: 'A default IRC URI to prepend before each recipient (optional)', help: 'A default IRC URI to prepend before each recipient (optional)',
placeholder: 'irc://irc.network.net:6697/' }, placeholder: 'irc://irc.network.net:6697/' },
{ type: 'textarea', name: 'recipients', { 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: '\ help: 'Recipients have to be specified with a full URI: '\
'irc[s]://irc.network.net[:port]/#channel. Special cases: if '\ 'irc[s]://irc.network.net[:port]/#channel. Special cases: if '\
'you want the channel to be a nickname instead, append ",isnick" to ' \ 'you want the channel to be a nickname instead, append ",isnick" to ' \
......
...@@ -32,9 +32,9 @@ class IssueTrackerService < Service ...@@ -32,9 +32,9 @@ class IssueTrackerService < Service
def fields def fields
[ [
{ type: 'text', name: 'description', placeholder: description }, { type: 'text', name: 'description', placeholder: description },
{ type: 'text', name: 'project_url', placeholder: 'Project url' }, { type: 'text', name: 'project_url', placeholder: 'Project url', required: true },
{ type: 'text', name: 'issues_url', placeholder: 'Issue url' }, { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true },
{ type: 'text', name: 'new_issue_url', placeholder: 'New Issue url' } { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url', required: true }
] ]
end end
......
...@@ -86,11 +86,11 @@ class JiraService < IssueTrackerService ...@@ -86,11 +86,11 @@ class JiraService < IssueTrackerService
def fields 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: 'api_url', title: 'JIRA API URL', placeholder: 'If different from Web URL' },
{ type: 'text', name: 'project_key', placeholder: 'Project Key' }, { type: 'text', name: 'project_key', placeholder: 'Project Key', required: true },
{ type: 'text', name: 'username', placeholder: '' }, { type: 'text', name: 'username', placeholder: '', required: true },
{ type: 'password', name: 'password', placeholder: '' }, { type: 'password', name: 'password', placeholder: '', required: true },
{ type: 'text', name: 'jira_issue_transition_id', placeholder: '' } { type: 'text', name: 'jira_issue_transition_id', placeholder: '' }
] ]
end end
...@@ -175,10 +175,6 @@ class JiraService < IssueTrackerService ...@@ -175,10 +175,6 @@ class JiraService < IssueTrackerService
{ success: result.present?, result: result } { success: result.present?, result: result }
end end
def can_test?
username.present? && password.present?
end
# JIRA does not need test data. # JIRA does not need test data.
# We are requesting the project that belongs to the project key. # We are requesting the project that belongs to the project key.
def test_data(user = nil, project = nil) def test_data(user = nil, project = nil)
......
...@@ -21,7 +21,8 @@ class MockCiService < CiService ...@@ -21,7 +21,8 @@ class MockCiService < CiService
[ [
{ type: 'text', { type: 'text',
name: 'mock_service_url', name: 'mock_service_url',
placeholder: 'http://localhost:4004' } placeholder: 'http://localhost:4004',
required: true }
] ]
end end
...@@ -79,4 +80,8 @@ class MockCiService < CiService ...@@ -79,4 +80,8 @@ class MockCiService < CiService
:error :error
end end
end end
def can_test?
false
end
end end
...@@ -14,4 +14,8 @@ class MockMonitoringService < MonitoringService ...@@ -14,4 +14,8 @@ class MockMonitoringService < MonitoringService
def metrics(environment) def metrics(environment)
JSON.parse(File.read(Rails.root + 'spec/fixtures/metrics.json')) JSON.parse(File.read(Rails.root + 'spec/fixtures/metrics.json'))
end end
def can_test?
false
end
end end
...@@ -53,7 +53,8 @@ class PipelinesEmailService < Service ...@@ -53,7 +53,8 @@ class PipelinesEmailService < Service
[ [
{ type: 'textarea', { type: 'textarea',
name: 'recipients', name: 'recipients',
placeholder: 'Emails separated by comma' }, placeholder: 'Emails separated by comma',
required: true },
{ type: 'checkbox', { type: 'checkbox',
name: 'notify_only_broken_pipelines' } name: 'notify_only_broken_pipelines' }
] ]
......
...@@ -23,7 +23,8 @@ class PivotaltrackerService < Service ...@@ -23,7 +23,8 @@ class PivotaltrackerService < Service
{ {
type: 'text', type: 'text',
name: 'token', name: 'token',
placeholder: 'Pivotal Tracker API token.' placeholder: 'Pivotal Tracker API token.',
required: true
}, },
{ {
type: 'text', type: 'text',
......
...@@ -49,7 +49,8 @@ class PrometheusService < MonitoringService ...@@ -49,7 +49,8 @@ class PrometheusService < MonitoringService
type: 'text', type: 'text',
name: 'api_url', name: 'api_url',
title: '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 end
......
...@@ -19,10 +19,10 @@ class PushoverService < Service ...@@ -19,10 +19,10 @@ class PushoverService < Service
def fields def fields
[ [
{ type: 'text', name: 'api_key', placeholder: 'Your application key' }, { type: 'text', name: 'api_key', placeholder: 'Your application key', required: true },
{ type: 'text', name: 'user_key', placeholder: 'Your user key' }, { type: 'text', name: 'user_key', placeholder: 'Your user key', required: true },
{ type: 'text', name: 'device', placeholder: 'Leave blank for all active devices' }, { 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], ['Lowest Priority', -2],
['Low Priority', -1], ['Low Priority', -1],
......
...@@ -50,9 +50,9 @@ class TeamcityService < CiService ...@@ -50,9 +50,9 @@ class TeamcityService < CiService
def fields def fields
[ [
{ type: 'text', name: 'teamcity_url', { 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', { type: 'text', name: 'build_type',
placeholder: 'Build configuration ID' }, placeholder: 'Build configuration ID', required: true },
{ type: 'text', name: 'username', { type: 'text', name: 'username',
placeholder: 'A user with permissions to trigger a manual build' }, placeholder: 'A user with permissions to trigger a manual build' },
{ type: 'password', name: 'password' } { type: 'password', name: 'password' }
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('integrations')
.row.prepend-top-default.append-bottom-default .row.prepend-top-default.append-bottom-default
.col-lg-3 .col-lg-3
%h4.prepend-top-0 %h4.prepend-top-0
...@@ -6,15 +9,17 @@ ...@@ -6,15 +9,17 @@
%p= @service.description %p= @service.description
.col-lg-9 .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 = render 'shared/service_settings', form: form, subject: @service
.footer-block.row-content-block .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; &nbsp;
- if @service.valid? && @service.activated? - if @service.valid? && @service.activated?
- unless @service.can_test? - unless @service.can_test?
- disabled_class = 'disabled' - disabled_class = 'disabled'
- disabled_title = @service.disabled_title - 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"
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
- value = @service.send(name) - value = @service.send(name)
- type = field[:type] - type = field[:type]
- placeholder = field[:placeholder] - placeholder = field[:placeholder]
- required = field[:required]
- choices = field[:choices] - choices = field[:choices]
- default_choice = field[:default_choice] - default_choice = field[:default_choice]
- help = field[:help] - help = field[:help]
...@@ -14,14 +15,14 @@ ...@@ -14,14 +15,14 @@
= form.label name, title, class: "control-label" = form.label name, title, class: "control-label"
.col-sm-10 .col-sm-10
- if type == 'text' - 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' - 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' - elsif type == 'checkbox'
= form.check_box name = form.check_box name
- elsif type == 'select' - elsif type == 'select'
= form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } = form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" }
- elsif type == 'password' - 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 - if help
%span.help-block= help %span.help-block= help
---
title: Simplify testing and saving service integrations
merge_request: 11599
author:
...@@ -67,7 +67,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -67,7 +67,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
member do member do
get :test put :test
end end
end end
......
...@@ -41,6 +41,7 @@ var config = { ...@@ -41,6 +41,7 @@ var config = {
group: './group.js', group: './group.js',
groups_list: './groups_list.js', groups_list: './groups_list.js',
issue_show: './issue_show/index.js', issue_show: './issue_show/index.js',
integrations: './integrations',
locale: './locale/index.js', locale: './locale/index.js',
main: './main.js', main: './main.js',
merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js', merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
......
...@@ -11,77 +11,77 @@ Feature: Project Services ...@@ -11,77 +11,77 @@ Feature: Project Services
When I visit project "Shop" services page When I visit project "Shop" services page
And I click hipchat service link And I click hipchat service link
And I fill hipchat settings 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 Scenario: Activate hipchat service with custom server
When I visit project "Shop" services page When I visit project "Shop" services page
And I click hipchat service link And I click hipchat service link
And I fill hipchat settings with custom server 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 Scenario: Activate pivotaltracker service
When I visit project "Shop" services page When I visit project "Shop" services page
And I click pivotaltracker service link And I click pivotaltracker service link
And I fill pivotaltracker settings 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 Scenario: Activate Flowdock service
When I visit project "Shop" services page When I visit project "Shop" services page
And I click Flowdock service link And I click Flowdock service link
And I fill Flowdock settings 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 Scenario: Activate Assembla service
When I visit project "Shop" services page When I visit project "Shop" services page
And I click Assembla service link And I click Assembla service link
And I fill Assembla settings 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 Scenario: Activate Slack notifications service
When I visit project "Shop" services page When I visit project "Shop" services page
And I click Slack notifications service link And I click Slack notifications service link
And I fill Slack notifications settings 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 Scenario: Activate Pushover service
When I visit project "Shop" services page When I visit project "Shop" services page
And I click Pushover service link And I click Pushover service link
And I fill Pushover settings 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 Scenario: Activate email on push service
When I visit project "Shop" services page When I visit project "Shop" services page
And I click email on push service link And I click email on push service link
And I fill email on push settings 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 Scenario: Activate JIRA service
When I visit project "Shop" services page When I visit project "Shop" services page
And I click jira service link And I click jira service link
And I fill jira settings 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 Scenario: Activate Irker (IRC Gateway) service
When I visit project "Shop" services page When I visit project "Shop" services page
And I click Irker service link And I click Irker service link
And I fill Irker settings 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 Scenario: Activate Atlassian Bamboo CI service
When I visit project "Shop" services page When I visit project "Shop" services page
And I click Atlassian Bamboo CI service link And I click Atlassian Bamboo CI service link
And I fill Atlassian Bamboo CI settings 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 And I should see empty field Change Password
Scenario: Activate jetBrains TeamCity CI service Scenario: Activate jetBrains TeamCity CI service
When I visit project "Shop" services page When I visit project "Shop" services page
And I click jetBrains TeamCity CI service link And I click jetBrains TeamCity CI service link
And I fill jetBrains TeamCity CI settings 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 Scenario: Activate Asana service
When I visit project "Shop" services page When I visit project "Shop" services page
And I click Asana service link And I click Asana service link
And I fill Asana settings And I fill Asana settings
Then I should see Asana service settings saved Then I should see the Asana success message
...@@ -34,8 +34,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps ...@@ -34,8 +34,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save' click_button 'Save'
end end
step 'I should see hipchat service settings saved' do step 'I should see the Hipchat success message' do
expect(find_field('Room').value).to eq 'gitlab' expect(page).to have_content 'HipChat activated.'
end end
step 'I fill hipchat settings with custom server' do step 'I fill hipchat settings with custom server' do
...@@ -46,10 +46,6 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps ...@@ -46,10 +46,6 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save' click_button 'Save'
end 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 step 'I click pivotaltracker service link' do
click_link 'PivotalTracker' click_link 'PivotalTracker'
end end
...@@ -60,8 +56,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps ...@@ -60,8 +56,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save' click_button 'Save'
end end
step 'I should see pivotaltracker service settings saved' do step 'I should see the Pivotaltracker success message' do
expect(find_field('Token').value).to eq 'verySecret' expect(page).to have_content 'PivotalTracker activated.'
end end
step 'I click Flowdock service link' do step 'I click Flowdock service link' do
...@@ -74,8 +70,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps ...@@ -74,8 +70,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save' click_button 'Save'
end end
step 'I should see Flowdock service settings saved' do step 'I should see the Flowdock success message' do
expect(find_field('Token').value).to eq 'verySecret' expect(page).to have_content 'Flowdock activated.'
end end
step 'I click Assembla service link' do step 'I click Assembla service link' do
...@@ -88,8 +84,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps ...@@ -88,8 +84,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save' click_button 'Save'
end end
step 'I should see Assembla service settings saved' do step 'I should see the Assembla success message' do
expect(find_field('Token').value).to eq 'verySecret' expect(page).to have_content 'Assembla activated.'
end end
step 'I click Asana service link' do step 'I click Asana service link' do
...@@ -103,9 +99,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps ...@@ -103,9 +99,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save' click_button 'Save'
end end
step 'I should see Asana service settings saved' do step 'I should see the Asana success message' do
expect(find_field('Api key').value).to eq 'verySecret' expect(page).to have_content 'Asana activated.'
expect(find_field('Restrict to branch').value).to eq 'master'
end end
step 'I click email on push service link' do step 'I click email on push service link' do
...@@ -113,12 +108,13 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps ...@@ -113,12 +108,13 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
end end
step 'I fill email on push settings' do step 'I fill email on push settings' do
check 'Active'
fill_in 'Recipients', with: 'qa@company.name' fill_in 'Recipients', with: 'qa@company.name'
click_button 'Save' click_button 'Save'
end end
step 'I should see email on push service settings saved' do step 'I should see the Emails on push success message' do
expect(find_field('Recipients').value).to eq 'qa@company.name' expect(page).to have_content 'Emails on push activated.'
end end
step 'I click Irker service link' do step 'I click Irker service link' do
...@@ -132,9 +128,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps ...@@ -132,9 +128,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save' click_button 'Save'
end end
step 'I should see Irker service settings saved' do step 'I should see the Irker success message' do
expect(find_field('Recipients').value).to eq 'irc://chat.freenode.net/#commits' expect(page).to have_content 'Irker (IRC gateway) activated.'
expect(find_field('Colorize messages').value).to eq '1'
end end
step 'I click Slack notifications service link' do step 'I click Slack notifications service link' do
...@@ -147,8 +142,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps ...@@ -147,8 +142,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save' click_button 'Save'
end end
step 'I should see Slack Notifications service settings saved' do step 'I should see the Slack notifications success message' do
expect(find_field('Webhook').value).to eq 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' expect(page).to have_content 'Slack notifications activated.'
end end
step 'I click Pushover service link' do step 'I click Pushover service link' do
...@@ -165,12 +160,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps ...@@ -165,12 +160,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save' click_button 'Save'
end end
step 'I should see Pushover service settings saved' do step 'I should see the Pushover success message' do
expect(find_field('Api key').value).to eq 'verySecret' expect(page).to have_content 'Pushover activated.'
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'
end end
step 'I click jira service link' do step 'I click jira service link' do
...@@ -178,6 +169,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps ...@@ -178,6 +169,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
end end
step 'I fill jira settings' do step 'I fill jira settings' do
check 'Active'
fill_in 'Web URL', with: 'http://jira.example' fill_in 'Web URL', with: 'http://jira.example'
fill_in 'JIRA API URL', with: 'http://jira.example/api' fill_in 'JIRA API URL', with: 'http://jira.example/api'
fill_in 'Username', with: 'gitlab' fill_in 'Username', with: 'gitlab'
...@@ -186,11 +179,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps ...@@ -186,11 +179,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save' click_button 'Save'
end end
step 'I should see jira service settings saved' do step 'I should see the JIRA success message' do
expect(find_field('Web URL').value).to eq 'http://jira.example' expect(page).to have_content 'JIRA activated.'
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'
end end
step 'I click Atlassian Bamboo CI service link' do step 'I click Atlassian Bamboo CI service link' do
...@@ -206,13 +196,13 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps ...@@ -206,13 +196,13 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save' click_button 'Save'
end end
step 'I should see Atlassian Bamboo CI service settings saved' do step 'I should see the Bamboo success message' do
expect(find_field('Bamboo url').value).to eq 'http://bamboo.example.com' expect(page).to have_content 'Atlassian Bamboo CI activated.'
expect(find_field('Build key').value).to eq 'KEY'
expect(find_field('Username').value).to eq 'user'
end end
step 'I should see empty field Change Password' do step 'I should see empty field Change Password' do
click_link 'Atlassian Bamboo CI'
expect(find_field('Enter new password').value).to be_nil expect(find_field('Enter new password').value).to be_nil
end end
...@@ -229,9 +219,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps ...@@ -229,9 +219,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save' click_button 'Save'
end end
step 'I should see JetBrains TeamCity CI service settings saved' do step 'I should see the JetBrains success message' do
expect(find_field('Teamcity url').value).to eq 'http://teamcity.example.com' expect(page).to have_content 'JetBrains TeamCity CI activated.'
expect(find_field('Build type').value).to eq 'GitlabTest_Build'
expect(find_field('Username').value).to eq 'user'
end end
end end
...@@ -3,7 +3,9 @@ require 'spec_helper' ...@@ -3,7 +3,9 @@ require 'spec_helper'
describe Projects::ServicesController do describe Projects::ServicesController do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:user) { create(:user) } 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 before do
sign_in(user) sign_in(user)
...@@ -13,17 +15,12 @@ describe Projects::ServicesController do ...@@ -13,17 +15,12 @@ describe Projects::ServicesController do
controller.instance_variable_set(:@service, service) controller.instance_variable_set(:@service, service)
end end
shared_examples_for 'services controller' do |referrer| describe '#test' do
before do
request.env["HTTP_REFERER"] = referrer
end
describe "#test" do
context 'when can_test? returns false' do context 'when can_test? returns false' do
it 'renders 404' do it 'renders 404' do
allow_any_instance_of(Service).to receive(:can_test?).and_return(false) 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) expect(response).to have_http_status(404)
end end
...@@ -36,74 +33,63 @@ describe Projects::ServicesController do ...@@ -36,74 +33,63 @@ describe Projects::ServicesController do
context 'with chat notification service' do context 'with chat notification service' do
let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') } let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') }
it 'redirects and show success message' do it 'returns success' do
allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true) 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(response.status).to eq(200)
expect(flash[:notice]).to eq('We sent a request to the provided URL')
end end
end end
it 'redirects and show success message' do it 'returns success' do
expect(service).to receive(:test).and_return(success: true, result: 'done') 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(response.status).to eq(200)
expect(flash[:notice]).to eq('We sent a request to the provided URL')
end end
end end
it "redirects and show success message" do it 'returns success' do
expect(service).to receive(:test).and_return(success: true, result: 'done') 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(response.status).to eq(200)
expect(flash[:notice]).to eq('We sent a request to the provided URL')
end end
end end
context 'failure' do context 'failure' do
it "redirects and show failure message" do it 'returns success status code and the error message' do
expect(service).to receive(:test).and_return(success: false, result: 'Bad test') expect(HipChat::Client).to receive(:new).with('hipchat_token_p', anything).and_raise('Bad test')
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(response.status).to eq(200)
expect(flash[:alert]).to eq('We tried to send a request to the provided URL but an error occurred: Bad test') expect(JSON.parse(response.body)).
end to eq('error' => true, 'message' => 'Test failed.', 'service_response' => 'Bad test')
end end
end end
end end
describe 'referrer defined' do describe 'PUT #update' do
it_should_behave_like 'services controller' do context 'when param `active` is set to true' do
let!(:referrer) { "/" } it 'activates the service and redirects to integrations paths' do
end put :update,
end namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: { active: true }
describe 'referrer undefined' do expect(response).to redirect_to(namespace_project_settings_integrations_path(project.namespace, project))
it_should_behave_like 'services controller' do expect(flash[:notice]).to eq 'HipChat activated.'
let!(:referrer) { nil }
end end
end end
describe 'PUT #update' do context 'when param `active` is set to false' do
context 'on successful update' do it 'does not activate the service but saves the settings' 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)
put :update, put :update,
namespace_id: project.namespace.id, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: { active: false }
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 end
end end
......
...@@ -33,4 +33,10 @@ FactoryGirl.define do ...@@ -33,4 +33,10 @@ FactoryGirl.define do
project_key: 'jira-key' project_key: 'jira-key'
) )
end end
factory :hipchat_service do
project factory: :empty_project
type 'HipchatService'
token 'test_token'
end
end end
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
...@@ -24,15 +24,25 @@ feature 'Setup Mattermost slash commands', :feature, :js do ...@@ -24,15 +24,25 @@ feature 'Setup Mattermost slash commands', :feature, :js do
expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx') expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end 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 token = ('a'..'z').to_a.join
fill_in 'service_token', with: token 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 end
it 'shows the add to mattermost button' do it 'shows the add to mattermost button' do
......
...@@ -21,13 +21,21 @@ feature 'Slack slash commands', feature: true do ...@@ -21,13 +21,21 @@ feature 'Slack slash commands', feature: true do
expect(page).to have_content('This service allows users to perform common') expect(page).to have_content('This service allows users to perform common')
end 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' fill_in 'service_token', with: 'token'
click_on 'Save' 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 end
it 'shows the correct trigger url' do it 'shows the correct trigger url' do
......
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
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);
});
});
});
...@@ -69,41 +69,6 @@ describe JiraService, models: true do ...@@ -69,41 +69,6 @@ describe JiraService, models: true do
end end
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 describe '#close_issue' do
let(:custom_base_url) { 'http://custom_url' } let(:custom_base_url) { 'http://custom_url' }
let(:user) { create(:user) } let(:user) { create(:user) }
......
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