Commit d454486f authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch '201855-rename-project_services_to_integrations-4' into 'master'

Move Jira integration model to `Integrations::` namespace [RUN AS-IF-FOSS] [RUN ALL RSPEC]

See merge request gitlab-org/gitlab!61743
parents 6c203b47 eade74bf
...@@ -872,7 +872,6 @@ RSpec/AnyInstanceOf: ...@@ -872,7 +872,6 @@ RSpec/AnyInstanceOf:
- 'ee/spec/features/issues/form_spec.rb' - 'ee/spec/features/issues/form_spec.rb'
- 'ee/spec/features/merge_request/user_creates_merge_request_spec.rb' - 'ee/spec/features/merge_request/user_creates_merge_request_spec.rb'
- 'ee/spec/features/projects/new_project_spec.rb' - 'ee/spec/features/projects/new_project_spec.rb'
- 'ee/spec/features/projects/services/user_activates_jira_spec.rb'
- 'ee/spec/features/registrations/welcome_spec.rb' - 'ee/spec/features/registrations/welcome_spec.rb'
- 'ee/spec/features/security/project/internal_access_spec.rb' - 'ee/spec/features/security/project/internal_access_spec.rb'
- 'ee/spec/features/security/project/private_access_spec.rb' - 'ee/spec/features/security/project/private_access_spec.rb'
...@@ -1151,6 +1150,7 @@ RSpec/AnyInstanceOf: ...@@ -1151,6 +1150,7 @@ RSpec/AnyInstanceOf:
- 'spec/models/hooks/service_hook_spec.rb' - 'spec/models/hooks/service_hook_spec.rb'
- 'spec/models/hooks/system_hook_spec.rb' - 'spec/models/hooks/system_hook_spec.rb'
- 'spec/models/hooks/web_hook_spec.rb' - 'spec/models/hooks/web_hook_spec.rb'
- 'spec/models/integrations/jira_spec.rb'
- 'spec/models/issue_spec.rb' - 'spec/models/issue_spec.rb'
- 'spec/models/key_spec.rb' - 'spec/models/key_spec.rb'
- 'spec/models/member_spec.rb' - 'spec/models/member_spec.rb'
...@@ -1158,7 +1158,6 @@ RSpec/AnyInstanceOf: ...@@ -1158,7 +1158,6 @@ RSpec/AnyInstanceOf:
- 'spec/models/merge_request_spec.rb' - 'spec/models/merge_request_spec.rb'
- 'spec/models/note_spec.rb' - 'spec/models/note_spec.rb'
- 'spec/models/project_import_state_spec.rb' - 'spec/models/project_import_state_spec.rb'
- 'spec/models/project_services/jira_service_spec.rb'
- 'spec/models/project_services/mattermost_slash_commands_service_spec.rb' - 'spec/models/project_services/mattermost_slash_commands_service_spec.rb'
- 'spec/models/project_spec.rb' - 'spec/models/project_spec.rb'
- 'spec/models/repository_spec.rb' - 'spec/models/repository_spec.rb'
...@@ -1662,7 +1661,6 @@ Gitlab/NamespacedClass: ...@@ -1662,7 +1661,6 @@ Gitlab/NamespacedClass:
- 'app/models/project_services/irker_service.rb' - 'app/models/project_services/irker_service.rb'
- 'app/models/project_services/issue_tracker_data.rb' - 'app/models/project_services/issue_tracker_data.rb'
- 'app/models/project_services/jenkins_service.rb' - 'app/models/project_services/jenkins_service.rb'
- 'app/models/project_services/jira_service.rb'
- 'app/models/project_services/jira_tracker_data.rb' - 'app/models/project_services/jira_tracker_data.rb'
- 'app/models/project_services/mattermost_service.rb' - 'app/models/project_services/mattermost_service.rb'
- 'app/models/project_services/mattermost_slash_commands_service.rb' - 'app/models/project_services/mattermost_slash_commands_service.rb'
...@@ -2918,7 +2916,6 @@ Style/RegexpLiteralMixedPreserve: ...@@ -2918,7 +2916,6 @@ Style/RegexpLiteralMixedPreserve:
- 'ee/spec/features/groups/saml_enforcement_spec.rb' - 'ee/spec/features/groups/saml_enforcement_spec.rb'
- 'ee/spec/features/markdown/metrics_spec.rb' - 'ee/spec/features/markdown/metrics_spec.rb'
- 'ee/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb' - 'ee/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb'
- 'ee/spec/models/project_services/jira_service_spec.rb'
- 'ee/spec/services/jira/requests/issues/list_service_spec.rb' - 'ee/spec/services/jira/requests/issues/list_service_spec.rb'
- 'lib/api/invitations.rb' - 'lib/api/invitations.rb'
- 'lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb' - 'lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb'
......
...@@ -15,7 +15,7 @@ module Types ...@@ -15,7 +15,7 @@ module Types
definition_methods do definition_methods do
def resolve_type(object, context) def resolve_type(object, context)
if object.is_a?(::JiraService) if object.is_a?(::Integrations::Jira)
Types::Projects::Services::JiraServiceType Types::Projects::Services::JiraServiceType
else else
Types::Projects::Services::BaseServiceType Types::Projects::Services::BaseServiceType
......
...@@ -107,7 +107,7 @@ module ServicesHelper ...@@ -107,7 +107,7 @@ module ServicesHelper
reset_path: scoped_reset_integration_path(integration, group: group) reset_path: scoped_reset_integration_path(integration, group: group)
} }
if integration.is_a?(JiraService) if integration.is_a?(Integrations::Jira)
form_data[:jira_issue_transition_automatic] = integration.jira_issue_transition_automatic form_data[:jira_issue_transition_automatic] = integration.jira_issue_transition_automatic
form_data[:jira_issue_transition_id] = integration.jira_issue_transition_id form_data[:jira_issue_transition_id] = integration.jira_issue_transition_id
end end
......
# frozen_string_literal: true
# Accessible as Project#external_issue_tracker
module Integrations
class Jira < IssueTracker
extend ::Gitlab::Utils::Override
include Gitlab::Routing
include ApplicationHelper
include ActionView::Helpers::AssetUrlHelper
include Gitlab::Utils::StrongMemoize
PROJECTS_PER_PAGE = 50
# TODO: use jira_service.deployment_type enum when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged
DEPLOYMENT_TYPES = {
server: 'SERVER',
cloud: 'CLOUD'
}.freeze
validates :url, public_url: true, presence: true, if: :activated?
validates :api_url, public_url: true, allow_blank: true
validates :username, presence: true, if: :activated?
validates :password, presence: true, if: :activated?
validates :jira_issue_transition_id,
format: { with: Gitlab::Regex.jira_transition_id_regex, message: s_("JiraService|IDs must be a list of numbers that can be split with , or ;") },
allow_blank: true
# Jira Cloud version is deprecating authentication via username and password.
# We should use username/password for Jira Server and email/api_token for Jira Cloud,
# for more information check: https://gitlab.com/gitlab-org/gitlab-foss/issues/49936.
# TODO: we can probably just delegate as part of
# https://gitlab.com/gitlab-org/gitlab/issues/29404
data_field :username, :password, :url, :api_url, :jira_issue_transition_automatic, :jira_issue_transition_id, :project_key, :issues_enabled,
:vulnerabilities_enabled, :vulnerabilities_issuetype
before_update :reset_password
after_commit :update_deployment_type, on: [:create, :update], if: :update_deployment_type?
enum comment_detail: {
standard: 1,
all_details: 2
}
alias_method :project_url, :url
# When these are false GitLab does not create cross reference
# comments on Jira except when an issue gets transitioned.
def self.supported_events
%w(commit merge_request)
end
def self.supported_event_actions
%w(comment)
end
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def self.reference_pattern(only_long: true)
@reference_pattern ||= /(?<issue>\b#{Gitlab::Regex.jira_issue_key_regex})/
end
def initialize_properties
{}
end
def data_fields
jira_tracker_data || self.build_jira_tracker_data
end
def reset_password
data_fields.password = nil if reset_password?
end
def set_default_data
return unless issues_tracker.present?
return if url
data_fields.url ||= issues_tracker['url']
data_fields.api_url ||= issues_tracker['api_url']
end
def options
url = URI.parse(client_url)
{
username: username&.strip,
password: password,
site: URI.join(url, '/').to_s, # Intended to find the root
context_path: url.path,
auth_type: :basic,
read_timeout: 120,
use_cookies: true,
additional_cookies: ['OBBasicAuth=fromDialog'],
use_ssl: url.scheme == 'https'
}
end
def client
@client ||= begin
JIRA::Client.new(options).tap do |client|
# Replaces JIRA default http client with our implementation
client.request_client = Gitlab::Jira::HttpClient.new(client.options)
end
end
end
def help
jira_doc_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_url('integration/jira/index.html') }
s_("JiraService|You need to configure Jira before enabling this integration. For more details, read the %{jira_doc_link_start}Jira integration documentation%{link_end}.") % { jira_doc_link_start: jira_doc_link_start, link_end: '</a>'.html_safe }
end
def title
'Jira'
end
def description
s_("JiraService|Use Jira as this project's issue tracker.")
end
def self.to_param
'jira'
end
def fields
[
{
type: 'text',
name: 'url',
title: s_('JiraService|Web URL'),
placeholder: 'https://jira.example.com',
help: s_('JiraService|Base URL of the Jira instance.'),
required: true
},
{
type: 'text',
name: 'api_url',
title: s_('JiraService|Jira API URL'),
help: s_('JiraService|If different from Web URL.')
},
{
type: 'text',
name: 'username',
title: s_('JiraService|Username or Email'),
help: s_('JiraService|Use a username for server version and an email for cloud version.'),
required: true
},
{
type: 'password',
name: 'password',
title: s_('JiraService|Password or API token'),
non_empty_password_title: s_('JiraService|Enter new password or API token'),
non_empty_password_help: s_('JiraService|Leave blank to use your current password or API token.'),
help: s_('JiraService|Use a password for server version and an API token for cloud version.'),
required: true
}
]
end
def issues_url
"#{url}/browse/:id"
end
def new_issue_url
"#{url}/secure/CreateIssue!default.jspa"
end
alias_method :original_url, :url
def url
original_url&.delete_suffix('/')
end
alias_method :original_api_url, :api_url
def api_url
original_api_url&.delete_suffix('/')
end
def execute(push)
# This method is a no-op, because currently Integrations::Jira does not
# support any events.
end
def find_issue(issue_key, rendered_fields: false, transitions: false)
expands = []
expands << 'renderedFields' if rendered_fields
expands << 'transitions' if transitions
options = { expand: expands.join(',') } if expands.any?
jira_request { client.Issue.find(issue_key, options || {}) }
end
def close_issue(entity, external_issue, current_user)
issue = find_issue(external_issue.iid, transitions: jira_issue_transition_automatic)
return if issue.nil? || has_resolution?(issue) || !issue_transition_enabled?
commit_id = case entity
when Commit then entity.id
when MergeRequest then entity.diff_head_sha
end
commit_url = build_entity_url(:commit, commit_id)
# Depending on the Jira project's workflow, a comment during transition
# may or may not be allowed. Refresh the issue after transition and check
# if it is closed, so we don't have one comment for every commit.
issue = find_issue(issue.key) if transition_issue(issue)
add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue)
log_usage(:close_issue, current_user)
end
def create_cross_reference_note(mentioned, noteable, author)
unless can_cross_reference?(noteable)
return s_("JiraService|Events for %{noteable_model_name} are disabled.") % { noteable_model_name: noteable.model_name.plural.humanize(capitalize: false) }
end
jira_issue = find_issue(mentioned.id)
return unless jira_issue.present?
noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id
noteable_type = noteable_name(noteable)
entity_url = build_entity_url(noteable_type, noteable_id)
entity_meta = build_entity_meta(noteable)
data = {
user: {
name: author.name,
url: resource_url(user_path(author))
},
project: {
name: project.full_path,
url: resource_url(project_path(project))
},
entity: {
id: entity_meta[:id],
name: noteable_type.humanize.downcase,
url: entity_url,
title: noteable.title,
description: entity_meta[:description],
branch: entity_meta[:branch]
}
}
add_comment(data, jira_issue).tap { log_usage(:cross_reference, author) }
end
def valid_connection?
test(nil)[:success]
end
def test(_)
result = server_info
success = result.present?
result = @error&.message unless success
{ success: success, result: result }
end
override :support_close_issue?
def support_close_issue?
true
end
override :support_cross_reference?
def support_cross_reference?
true
end
def issue_transition_enabled?
jira_issue_transition_automatic || jira_issue_transition_id.present?
end
private
def server_info
strong_memoize(:server_info) do
client_url.present? ? jira_request { client.ServerInfo.all.attrs } : nil
end
end
def can_cross_reference?(noteable)
case noteable
when Commit then commit_events
when MergeRequest then merge_requests_events
else true
end
end
# jira_issue_transition_id can have multiple values split by , or ;
# the issue is transitioned at the order given by the user
# if any transition fails it will log the error message and stop the transition sequence
def transition_issue(issue)
return transition_issue_to_done(issue) if jira_issue_transition_automatic
jira_issue_transition_id.scan(Gitlab::Regex.jira_transition_id_regex).all? do |transition_id|
transition_issue_to_id(issue, transition_id)
end
end
def transition_issue_to_id(issue, transition_id)
issue.transitions.build.save!(
transition: { id: transition_id }
)
true
rescue StandardError => error
log_error(
"Issue transition failed",
error: {
exception_class: error.class.name,
exception_message: error.message,
exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace)
},
client_url: client_url
)
false
end
def transition_issue_to_done(issue)
transitions = issue.transitions rescue []
transition = transitions.find do |transition|
status = transition&.to&.statusCategory
status && status['key'] == 'done'
end
return false unless transition
transition_issue_to_id(issue, transition.id)
end
def log_usage(action, user)
key = "i_ecosystem_jira_service_#{action}"
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user.id)
end
def add_issue_solved_comment(issue, commit_id, commit_url)
link_title = "Solved by commit #{commit_id}."
comment = "Issue solved with [#{commit_id}|#{commit_url}]."
link_props = build_remote_link_props(url: commit_url, title: link_title, resolved: true)
send_message(issue, comment, link_props)
end
def add_comment(data, issue)
entity_name = data[:entity][:name]
entity_url = data[:entity][:url]
entity_title = data[:entity][:title]
message = comment_message(data)
link_title = "#{entity_name.capitalize} - #{entity_title}"
link_props = build_remote_link_props(url: entity_url, title: link_title)
unless comment_exists?(issue, message)
send_message(issue, message, link_props)
end
end
def comment_message(data)
user_link = build_jira_link(data[:user][:name], data[:user][:url])
entity = data[:entity]
entity_ref = all_details? ? "#{entity[:name]} #{entity[:id]}" : "a #{entity[:name]}"
entity_link = build_jira_link(entity_ref, entity[:url])
project_link = build_jira_link(project.full_name, Gitlab::Routing.url_helpers.project_url(project))
branch =
if entity[:branch].present?
s_('JiraService| on branch %{branch_link}') % {
branch_link: build_jira_link(entity[:branch], project_tree_url(project, entity[:branch]))
}
end
entity_message = entity[:description].presence if all_details?
entity_message ||= entity[:title].chomp
s_('JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}') % {
user_link: user_link,
entity_link: entity_link,
project_link: project_link,
branch: branch,
entity_message: entity_message
}
end
def build_jira_link(title, url)
"[#{title}|#{url}]"
end
def has_resolution?(issue)
issue.respond_to?(:resolution) && issue.resolution.present?
end
def comment_exists?(issue, message)
comments = jira_request { issue.comments }
comments.present? && comments.any? { |comment| comment.body.include?(message) }
end
def send_message(issue, message, remote_link_props)
return unless client_url.present?
jira_request do
remote_link = find_remote_link(issue, remote_link_props[:object][:url])
create_issue_comment(issue, message) unless remote_link
remote_link ||= issue.remotelink.build
remote_link.save!(remote_link_props)
log_info("Successfully posted", client_url: client_url)
"SUCCESS: Successfully posted to #{client_url}."
end
end
def create_issue_comment(issue, message)
return unless comment_on_event_enabled
issue.comments.build.save!(body: message)
end
def find_remote_link(issue, url)
links = jira_request { issue.remotelink.all }
return unless links
links.find { |link| link.object["url"] == url }
end
def build_remote_link_props(url:, title:, resolved: false)
status = {
resolved: resolved
}
{
GlobalID: 'GitLab',
relationship: 'mentioned on',
object: {
url: url,
title: title,
status: status,
icon: {
title: 'GitLab', url16x16: asset_url(Gitlab::Favicon.main, host: gitlab_config.base_url)
}
}
}
end
def resource_url(resource)
"#{Settings.gitlab.base_url.chomp("/")}#{resource}"
end
def build_entity_url(noteable_type, entity_id)
polymorphic_url(
[
self.project,
noteable_type.to_sym
],
id: entity_id,
host: Settings.gitlab.base_url
)
end
def build_entity_meta(noteable)
if noteable.is_a?(Commit)
{
id: noteable.short_id,
description: noteable.safe_message,
branch: noteable.ref_names(project.repository).first
}
elsif noteable.is_a?(MergeRequest)
{
id: noteable.to_reference,
branch: noteable.source_branch
}
else
{}
end
end
def noteable_name(noteable)
name = noteable.model_name.singular
# ProjectSnippet inherits from Snippet class so it causes
# routing error building the URL.
name == "project_snippet" ? "snippet" : name
end
# Handle errors when doing Jira API calls
def jira_request
yield
rescue StandardError => error
@error = error
log_error("Error sending message", client_url: client_url, error: @error.message)
nil
end
def client_url
api_url.presence || url
end
def reset_password?
# don't reset the password if a new one is provided
return false if password_touched?
return true if api_url_changed?
return false if api_url.present?
url_changed?
end
def update_deployment_type?
(api_url_changed? || url_changed? || username_changed? || password_changed?) &&
can_test?
end
def update_deployment_type
clear_memoization(:server_info) # ensure we run the request when we try to update deployment type
results = server_info
return data_fields.deployment_unknown! unless results.present?
case results['deploymentType']
when 'Server'
data_fields.deployment_server!
when 'Cloud'
data_fields.deployment_cloud!
else
data_fields.deployment_unknown!
end
end
def self.event_description(event)
case event
when "merge_request", "merge_request_events"
s_("JiraService|Jira comments are created when an issue is referenced in a merge request.")
when "commit", "commit_events"
s_("JiraService|Jira comments are created when an issue is referenced in a commit.")
end
end
end
end
Integrations::Jira.prepend_mod_with('Integrations::Jira')
...@@ -193,6 +193,7 @@ class Project < ApplicationRecord ...@@ -193,6 +193,7 @@ class Project < ApplicationRecord
has_one :datadog_service, class_name: 'Integrations::Datadog' has_one :datadog_service, class_name: 'Integrations::Datadog'
has_one :emails_on_push_service, class_name: 'Integrations::EmailsOnPush' has_one :emails_on_push_service, class_name: 'Integrations::EmailsOnPush'
has_one :ewm_service, class_name: 'Integrations::Ewm' has_one :ewm_service, class_name: 'Integrations::Ewm'
has_one :jira_service, class_name: 'Integrations::Jira'
has_one :redmine_service, class_name: 'Integrations::Redmine' has_one :redmine_service, class_name: 'Integrations::Redmine'
has_one :youtrack_service, class_name: 'Integrations::Youtrack' has_one :youtrack_service, class_name: 'Integrations::Youtrack'
has_one :discord_service has_one :discord_service
...@@ -209,7 +210,6 @@ class Project < ApplicationRecord ...@@ -209,7 +210,6 @@ class Project < ApplicationRecord
has_one :teamcity_service has_one :teamcity_service
has_one :pushover_service has_one :pushover_service
has_one :jenkins_service has_one :jenkins_service
has_one :jira_service
has_one :external_wiki_service has_one :external_wiki_service
has_one :prometheus_service, inverse_of: :project has_one :prometheus_service, inverse_of: :project
has_one :mock_ci_service has_one :mock_ci_service
...@@ -560,7 +560,7 @@ class Project < ApplicationRecord ...@@ -560,7 +560,7 @@ class Project < ApplicationRecord
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).merge(Event.pushed_action) } scope :with_push, -> { joins(:events).merge(Event.pushed_action) }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :with_active_jira_services, -> { joins(:integrations).merge(::JiraService.active) } # rubocop:disable CodeReuse/ServiceClass scope :with_active_jira_services, -> { joins(:integrations).merge(::Integrations::Jira.active) } # rubocop:disable CodeReuse/ServiceClass
scope :with_jira_dvcs_cloud, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: true)) } scope :with_jira_dvcs_cloud, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: true)) }
scope :with_jira_dvcs_server, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false)) } scope :with_jira_dvcs_server, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false)) }
scope :inc_routes, -> { includes(:route, namespace: :route) } scope :inc_routes, -> { includes(:route, namespace: :route) }
......
# frozen_string_literal: true
# Accessible as Project#external_issue_tracker
class JiraService < Integrations::IssueTracker
extend ::Gitlab::Utils::Override
include Gitlab::Routing
include ApplicationHelper
include ActionView::Helpers::AssetUrlHelper
include Gitlab::Utils::StrongMemoize
PROJECTS_PER_PAGE = 50
# TODO: use jira_service.deployment_type enum when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged
DEPLOYMENT_TYPES = {
server: 'SERVER',
cloud: 'CLOUD'
}.freeze
validates :url, public_url: true, presence: true, if: :activated?
validates :api_url, public_url: true, allow_blank: true
validates :username, presence: true, if: :activated?
validates :password, presence: true, if: :activated?
validates :jira_issue_transition_id,
format: { with: Gitlab::Regex.jira_transition_id_regex, message: s_("JiraService|transition ids can have only numbers which can be split with , or ;") },
allow_blank: true
# Jira Cloud version is deprecating authentication via username and password.
# We should use username/password for Jira Server and email/api_token for Jira Cloud,
# for more information check: https://gitlab.com/gitlab-org/gitlab-foss/issues/49936.
# TODO: we can probably just delegate as part of
# https://gitlab.com/gitlab-org/gitlab/issues/29404
data_field :username, :password, :url, :api_url, :jira_issue_transition_automatic, :jira_issue_transition_id, :project_key, :issues_enabled,
:vulnerabilities_enabled, :vulnerabilities_issuetype
before_update :reset_password
after_commit :update_deployment_type, on: [:create, :update], if: :update_deployment_type?
enum comment_detail: {
standard: 1,
all_details: 2
}
alias_method :project_url, :url
# When these are false GitLab does not create cross reference
# comments on Jira except when an issue gets transitioned.
def self.supported_events
%w(commit merge_request)
end
def self.supported_event_actions
%w(comment)
end
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def self.reference_pattern(only_long: true)
@reference_pattern ||= /(?<issue>\b#{Gitlab::Regex.jira_issue_key_regex})/
end
def initialize_properties
{}
end
def data_fields
jira_tracker_data || self.build_jira_tracker_data
end
def reset_password
data_fields.password = nil if reset_password?
end
def set_default_data
return unless issues_tracker.present?
return if url
data_fields.url ||= issues_tracker['url']
data_fields.api_url ||= issues_tracker['api_url']
end
def options
url = URI.parse(client_url)
{
username: username&.strip,
password: password,
site: URI.join(url, '/').to_s, # Intended to find the root
context_path: url.path,
auth_type: :basic,
read_timeout: 120,
use_cookies: true,
additional_cookies: ['OBBasicAuth=fromDialog'],
use_ssl: url.scheme == 'https'
}
end
def client
@client ||= begin
JIRA::Client.new(options).tap do |client|
# Replaces JIRA default http client with our implementation
client.request_client = Gitlab::Jira::HttpClient.new(client.options)
end
end
end
def help
jira_doc_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_url('integration/jira/index.html') }
s_("JiraService|You need to configure Jira before enabling this integration. For more details, read the %{jira_doc_link_start}Jira integration documentation%{link_end}.") % { jira_doc_link_start: jira_doc_link_start, link_end: '</a>'.html_safe }
end
def title
'Jira'
end
def description
s_("JiraService|Use Jira as this project's issue tracker.")
end
def self.to_param
'jira'
end
def fields
[
{
type: 'text',
name: 'url',
title: s_('JiraService|Web URL'),
placeholder: 'https://jira.example.com',
help: s_('JiraService|Base URL of the Jira instance.'),
required: true
},
{
type: 'text',
name: 'api_url',
title: s_('JiraService|Jira API URL'),
help: s_('JiraService|If different from Web URL.')
},
{
type: 'text',
name: 'username',
title: s_('JiraService|Username or Email'),
help: s_('JiraService|Use a username for server version and an email for cloud version.'),
required: true
},
{
type: 'password',
name: 'password',
title: s_('JiraService|Password or API token'),
non_empty_password_title: s_('JiraService|Enter new password or API token'),
non_empty_password_help: s_('JiraService|Leave blank to use your current password or API token.'),
help: s_('JiraService|Use a password for server version and an API token for cloud version.'),
required: true
}
]
end
def issues_url
"#{url}/browse/:id"
end
def new_issue_url
"#{url}/secure/CreateIssue!default.jspa"
end
alias_method :original_url, :url
def url
original_url&.delete_suffix('/')
end
alias_method :original_api_url, :api_url
def api_url
original_api_url&.delete_suffix('/')
end
def execute(push)
# This method is a no-op, because currently JiraService does not
# support any events.
end
def find_issue(issue_key, rendered_fields: false, transitions: false)
expands = []
expands << 'renderedFields' if rendered_fields
expands << 'transitions' if transitions
options = { expand: expands.join(',') } if expands.any?
jira_request { client.Issue.find(issue_key, options || {}) }
end
def close_issue(entity, external_issue, current_user)
issue = find_issue(external_issue.iid, transitions: jira_issue_transition_automatic)
return if issue.nil? || has_resolution?(issue) || !issue_transition_enabled?
commit_id = case entity
when Commit then entity.id
when MergeRequest then entity.diff_head_sha
end
commit_url = build_entity_url(:commit, commit_id)
# Depending on the Jira project's workflow, a comment during transition
# may or may not be allowed. Refresh the issue after transition and check
# if it is closed, so we don't have one comment for every commit.
issue = find_issue(issue.key) if transition_issue(issue)
add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue)
log_usage(:close_issue, current_user)
end
def create_cross_reference_note(mentioned, noteable, author)
unless can_cross_reference?(noteable)
return s_("JiraService|Events for %{noteable_model_name} are disabled.") % { noteable_model_name: noteable.model_name.plural.humanize(capitalize: false) }
end
jira_issue = find_issue(mentioned.id)
return unless jira_issue.present?
noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id
noteable_type = noteable_name(noteable)
entity_url = build_entity_url(noteable_type, noteable_id)
entity_meta = build_entity_meta(noteable)
data = {
user: {
name: author.name,
url: resource_url(user_path(author))
},
project: {
name: project.full_path,
url: resource_url(project_path(project))
},
entity: {
id: entity_meta[:id],
name: noteable_type.humanize.downcase,
url: entity_url,
title: noteable.title,
description: entity_meta[:description],
branch: entity_meta[:branch]
}
}
add_comment(data, jira_issue).tap { log_usage(:cross_reference, author) }
end
def valid_connection?
test(nil)[:success]
end
def test(_)
result = server_info
success = result.present?
result = @error&.message unless success
{ success: success, result: result }
end
override :support_close_issue?
def support_close_issue?
true
end
override :support_cross_reference?
def support_cross_reference?
true
end
def issue_transition_enabled?
jira_issue_transition_automatic || jira_issue_transition_id.present?
end
private
def server_info
strong_memoize(:server_info) do
client_url.present? ? jira_request { client.ServerInfo.all.attrs } : nil
end
end
def can_cross_reference?(noteable)
case noteable
when Commit then commit_events
when MergeRequest then merge_requests_events
else true
end
end
# jira_issue_transition_id can have multiple values split by , or ;
# the issue is transitioned at the order given by the user
# if any transition fails it will log the error message and stop the transition sequence
def transition_issue(issue)
return transition_issue_to_done(issue) if jira_issue_transition_automatic
jira_issue_transition_id.scan(Gitlab::Regex.jira_transition_id_regex).all? do |transition_id|
transition_issue_to_id(issue, transition_id)
end
end
def transition_issue_to_id(issue, transition_id)
issue.transitions.build.save!(
transition: { id: transition_id }
)
true
rescue StandardError => error
log_error(
"Issue transition failed",
error: {
exception_class: error.class.name,
exception_message: error.message,
exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace)
},
client_url: client_url
)
false
end
def transition_issue_to_done(issue)
transitions = issue.transitions rescue []
transition = transitions.find do |transition|
status = transition&.to&.statusCategory
status && status['key'] == 'done'
end
return false unless transition
transition_issue_to_id(issue, transition.id)
end
def log_usage(action, user)
key = "i_ecosystem_jira_service_#{action}"
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user.id)
end
def add_issue_solved_comment(issue, commit_id, commit_url)
link_title = "Solved by commit #{commit_id}."
comment = "Issue solved with [#{commit_id}|#{commit_url}]."
link_props = build_remote_link_props(url: commit_url, title: link_title, resolved: true)
send_message(issue, comment, link_props)
end
def add_comment(data, issue)
entity_name = data[:entity][:name]
entity_url = data[:entity][:url]
entity_title = data[:entity][:title]
message = comment_message(data)
link_title = "#{entity_name.capitalize} - #{entity_title}"
link_props = build_remote_link_props(url: entity_url, title: link_title)
unless comment_exists?(issue, message)
send_message(issue, message, link_props)
end
end
def comment_message(data)
user_link = build_jira_link(data[:user][:name], data[:user][:url])
entity = data[:entity]
entity_ref = all_details? ? "#{entity[:name]} #{entity[:id]}" : "a #{entity[:name]}"
entity_link = build_jira_link(entity_ref, entity[:url])
project_link = build_jira_link(project.full_name, Gitlab::Routing.url_helpers.project_url(project))
branch =
if entity[:branch].present?
s_('JiraService| on branch %{branch_link}') % {
branch_link: build_jira_link(entity[:branch], project_tree_url(project, entity[:branch]))
}
end
entity_message = entity[:description].presence if all_details?
entity_message ||= entity[:title].chomp
s_('JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}') % {
user_link: user_link,
entity_link: entity_link,
project_link: project_link,
branch: branch,
entity_message: entity_message
}
end
def build_jira_link(title, url)
"[#{title}|#{url}]"
end
def has_resolution?(issue)
issue.respond_to?(:resolution) && issue.resolution.present?
end
def comment_exists?(issue, message)
comments = jira_request { issue.comments }
comments.present? && comments.any? { |comment| comment.body.include?(message) }
end
def send_message(issue, message, remote_link_props)
return unless client_url.present?
jira_request do
remote_link = find_remote_link(issue, remote_link_props[:object][:url])
create_issue_comment(issue, message) unless remote_link
remote_link ||= issue.remotelink.build
remote_link.save!(remote_link_props)
log_info("Successfully posted", client_url: client_url)
"SUCCESS: Successfully posted to #{client_url}."
end
end
def create_issue_comment(issue, message)
return unless comment_on_event_enabled
issue.comments.build.save!(body: message)
end
def find_remote_link(issue, url)
links = jira_request { issue.remotelink.all }
return unless links
links.find { |link| link.object["url"] == url }
end
def build_remote_link_props(url:, title:, resolved: false)
status = {
resolved: resolved
}
{
GlobalID: 'GitLab',
relationship: 'mentioned on',
object: {
url: url,
title: title,
status: status,
icon: {
title: 'GitLab', url16x16: asset_url(Gitlab::Favicon.main, host: gitlab_config.base_url)
}
}
}
end
def resource_url(resource)
"#{Settings.gitlab.base_url.chomp("/")}#{resource}"
end
def build_entity_url(noteable_type, entity_id)
polymorphic_url(
[
self.project,
noteable_type.to_sym
],
id: entity_id,
host: Settings.gitlab.base_url
)
end
def build_entity_meta(noteable)
if noteable.is_a?(Commit)
{
id: noteable.short_id,
description: noteable.safe_message,
branch: noteable.ref_names(project.repository).first
}
elsif noteable.is_a?(MergeRequest)
{
id: noteable.to_reference,
branch: noteable.source_branch
}
else
{}
end
end
def noteable_name(noteable)
name = noteable.model_name.singular
# ProjectSnippet inherits from Snippet class so it causes
# routing error building the URL.
name == "project_snippet" ? "snippet" : name
end
# Handle errors when doing Jira API calls
def jira_request
yield
rescue StandardError => error
@error = error
log_error("Error sending message", client_url: client_url, error: @error.message)
nil
end
def client_url
api_url.presence || url
end
def reset_password?
# don't reset the password if a new one is provided
return false if password_touched?
return true if api_url_changed?
return false if api_url.present?
url_changed?
end
def update_deployment_type?
(api_url_changed? || url_changed? || username_changed? || password_changed?) &&
can_test?
end
def update_deployment_type
clear_memoization(:server_info) # ensure we run the request when we try to update deployment type
results = server_info
return data_fields.deployment_unknown! unless results.present?
case results['deploymentType']
when 'Server'
data_fields.deployment_server!
when 'Cloud'
data_fields.deployment_cloud!
else
data_fields.deployment_unknown!
end
end
def self.event_description(event)
case event
when "merge_request", "merge_request_events"
s_("JiraService|Jira comments will be created when an issue gets referenced in a merge request.")
when "commit", "commit_events"
s_("JiraService|Jira comments will be created when an issue gets referenced in a commit.")
end
end
end
JiraService.prepend_mod_with('JiraService')
...@@ -43,9 +43,9 @@ module JiraImport ...@@ -43,9 +43,9 @@ module JiraImport
def user_mapper_service_factory def user_mapper_service_factory
# TODO: use deployment_type enum from jira service when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged # TODO: use deployment_type enum from jira service when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged
case deployment_type.upcase case deployment_type.upcase
when JiraService::DEPLOYMENT_TYPES[:server] when Integrations::Jira::DEPLOYMENT_TYPES[:server]
ServerUsersMapperService.new(user, project, start_at) ServerUsersMapperService.new(user, project, start_at)
when JiraService::DEPLOYMENT_TYPES[:cloud] when Integrations::Jira::DEPLOYMENT_TYPES[:cloud]
CloudUsersMapperService.new(user, project, start_at) CloudUsersMapperService.new(user, project, start_at)
else else
raise ArgumentError raise ArgumentError
......
...@@ -302,7 +302,7 @@ It contains information about [integrations](../user/project/integrations/overvi ...@@ -302,7 +302,7 @@ It contains information about [integrations](../user/project/integrations/overvi
{ {
"severity":"ERROR", "severity":"ERROR",
"time":"2018-09-06T14:56:20.439Z", "time":"2018-09-06T14:56:20.439Z",
"service_class":"JiraService", "service_class":"Integrations::Jira",
"project_id":8, "project_id":8,
"project_path":"h5bp/html5-boilerplate", "project_path":"h5bp/html5-boilerplate",
"message":"Error sending message", "message":"Error sending message",
...@@ -312,7 +312,7 @@ It contains information about [integrations](../user/project/integrations/overvi ...@@ -312,7 +312,7 @@ It contains information about [integrations](../user/project/integrations/overvi
{ {
"severity":"INFO", "severity":"INFO",
"time":"2018-09-06T17:15:16.365Z", "time":"2018-09-06T17:15:16.365Z",
"service_class":"JiraService", "service_class":"Integrations::Jira",
"project_id":3, "project_id":3,
"project_path":"namespace2/project2", "project_path":"namespace2/project2",
"message":"Successfully posted", "message":"Successfully posted",
......
...@@ -54,13 +54,13 @@ module Projects ...@@ -54,13 +54,13 @@ module Projects
total_count: finder.total_count total_count: finder.total_count
) )
::Integrations::Jira::IssueSerializer.new ::Integrations::JiraSerializers::IssueSerializer.new
.with_pagination(request, response) .with_pagination(request, response)
.represent(jira_issues, project: project) .represent(jira_issues, project: project)
end end
def issue_json def issue_json
::Integrations::Jira::IssueDetailSerializer.new ::Integrations::JiraSerializers::IssueDetailSerializer.new
.represent(project.jira_service.find_issue(params[:id], rendered_fields: true), project: project) .represent(project.jira_service.find_issue(params[:id], rendered_fields: true), project: project)
end end
......
...@@ -34,7 +34,7 @@ module Resolvers ...@@ -34,7 +34,7 @@ module Resolvers
def serialize_external_issue(external_issue, external_type) def serialize_external_issue(external_issue, external_type)
case external_type case external_type
when 'jira' when 'jira'
::Integrations::Jira::IssueSerializer ::Integrations::JiraSerializers::IssueSerializer
.new .new
.represent(external_issue, project: object.vulnerability.project, only: %i[title references status external_tracker web_url created_at updated_at] ) .represent(external_issue, project: object.vulnerability.project, only: %i[title references status external_tracker web_url created_at updated_at] )
end end
......
...@@ -13,7 +13,7 @@ module EE ...@@ -13,7 +13,7 @@ module EE
def integration_form_data(integration, group: nil) def integration_form_data(integration, group: nil)
form_data = super form_data = super
if integration.is_a?(JiraService) if integration.is_a?(Integrations::Jira)
form_data.merge!( form_data.merge!(
show_jira_issues_integration: @project&.jira_issues_integration_available?.to_s, show_jira_issues_integration: @project&.jira_issues_integration_available?.to_s,
show_jira_vulnerabilities_integration: integration.jira_vulnerabilities_integration_available?.to_s, show_jira_vulnerabilities_integration: integration.jira_vulnerabilities_integration_available?.to_s,
......
...@@ -15,7 +15,7 @@ module VulnerabilitiesHelper ...@@ -15,7 +15,7 @@ module VulnerabilitiesHelper
new_issue_url: new_issue_url_for(vulnerability), new_issue_url: new_issue_url_for(vulnerability),
create_jira_issue_url: create_jira_issue_url_for(vulnerability), create_jira_issue_url: create_jira_issue_url_for(vulnerability),
related_jira_issues_path: project_integrations_jira_issues_path(vulnerability.project, vulnerability_ids: [vulnerability.id]), related_jira_issues_path: project_integrations_jira_issues_path(vulnerability.project, vulnerability_ids: [vulnerability.id]),
jira_integration_settings_path: edit_project_service_path(vulnerability.project, ::JiraService), jira_integration_settings_path: edit_project_service_path(vulnerability.project, ::Integrations::Jira),
has_mr: !!vulnerability.finding.merge_request_feedback.try(:merge_request_id), has_mr: !!vulnerability.finding.merge_request_feedback.try(:merge_request_id),
create_mr_url: create_vulnerability_feedback_merge_request_path(vulnerability.finding.project), create_mr_url: create_vulnerability_feedback_merge_request_path(vulnerability.finding.project),
discussions_url: discussions_project_security_vulnerability_path(vulnerability.project, vulnerability), discussions_url: discussions_project_security_vulnerability_path(vulnerability.project, vulnerability),
......
# frozen_string_literal: true
module EE
module Integrations
module Jira
extend ActiveSupport::Concern
MAX_URL_LENGTH = 4000
prepended do
validates :project_key, presence: true, if: :project_key_required?
validates :vulnerabilities_issuetype, presence: true, if: :vulnerabilities_enabled
end
def jira_vulnerabilities_integration_available?
parent.present? ? parent.licensed_feature_available?(:jira_vulnerabilities_integration) : License.feature_available?(:jira_vulnerabilities_integration)
end
def jira_vulnerabilities_integration_enabled?
jira_vulnerabilities_integration_available? && vulnerabilities_enabled
end
def configured_to_create_issues_from_vulnerabilities?
strong_memoize(:configured_to_create_issues_from_vulnerabilities) do
active? && project_key.present? && vulnerabilities_issuetype.present? && jira_vulnerabilities_integration_enabled?
end
end
def test(_)
super.then do |result|
next result unless result[:success]
next result unless project.jira_vulnerabilities_integration_enabled?
result.merge(data: { issuetypes: issue_types })
end
end
def new_issue_url_with_predefined_fields(summary, description)
escaped_summary = CGI.escape(summary)
escaped_description = CGI.escape(description)
"#{url}/secure/CreateIssueDetails!init.jspa?pid=#{jira_project_id}&issuetype=#{vulnerabilities_issuetype}&summary=#{escaped_summary}&description=#{escaped_description}"[0..MAX_URL_LENGTH]
end
def create_issue(summary, description, current_user)
return if client_url.blank?
jira_request do
issue = client.Issue.build
issue.save(
fields: {
project: { id: jira_project_id },
issuetype: { id: vulnerabilities_issuetype },
summary: summary,
description: description
}
)
log_usage(:create_issue, current_user)
issue
end
end
private
def project_key_required?
strong_memoize(:project_key_required) do
issues_enabled || vulnerabilities_enabled
end
end
# Returns internal JIRA Project ID
#
# @return [String, nil] the internal JIRA ID of the Project
def jira_project_id
jira_project&.id
end
# Returns JIRA Project for selected Project Key
#
# @return [JIRA::Resource::Project, nil] the object that represents JIRA Projects
def jira_project
strong_memoize(:jira_project) do
client_url.present? ? jira_request { client.Project.find(project_key) } : nil
end
end
# Returns list of Issue Type Scheme IDs in selected JIRA Project
#
# @return [Array] the array of IDs
def project_issuetype_scheme_ids
raise NotImplementedError unless data_fields.deployment_cloud?
query_url = Addressable::URI.join("#{client.options[:rest_base_path]}/", 'issuetypescheme/', 'project')
query_url.query_values = { 'projectId' => jira_project_id }
client
.get(query_url.to_s)
.fetch('values', [])
.map { |schemes| schemes.dig('issueTypeScheme', 'id') }
end
# Returns list of Issue Type IDs available in active Issue Type Scheme in selected JIRA Project
#
# @return [Array] the array of IDs
def project_issuetype_ids
strong_memoize(:project_issuetype_ids) do
if data_fields.deployment_server?
query_url = Addressable::URI.join("#{client.options[:rest_base_path]}/", 'project/', project_key)
client
.get(query_url.to_s)
.fetch('issueTypes', [])
.map { |issue_type| issue_type['id'] }
elsif data_fields.deployment_cloud?
query_url = Addressable::URI.join("#{client.options[:rest_base_path]}/", 'issuetypescheme/', 'mapping')
query_url.query_values = { 'issueTypeSchemeId' => project_issuetype_scheme_ids }
client
.get(query_url.to_s)
.fetch('values', [])
.map { |schemes| schemes['issueTypeId'] }
else
raise NotImplementedError
end
end
end
# Returns list of available Issue tTpes in selected JIRA Project
#
# @return [Array] the array of objects with JIRA Issuetype ID, Name and Description
def issue_types
return [] if jira_project.blank?
client
.Issuetype
.all
.select { |issue_type| issue_type.id.in?(project_issuetype_ids) }
.reject { |issue_type| issue_type.subtask }
.map { |issue_type| { id: issue_type.id, name: issue_type.name, description: issue_type.description } }
end
end
end
end
# frozen_string_literal: true
module EE
module JiraService
extend ActiveSupport::Concern
MAX_URL_LENGTH = 4000
prepended do
validates :project_key, presence: true, if: :project_key_required?
validates :vulnerabilities_issuetype, presence: true, if: :vulnerabilities_enabled
end
def jira_vulnerabilities_integration_available?
parent.present? ? parent.licensed_feature_available?(:jira_vulnerabilities_integration) : License.feature_available?(:jira_vulnerabilities_integration)
end
def jira_vulnerabilities_integration_enabled?
jira_vulnerabilities_integration_available? && vulnerabilities_enabled
end
def configured_to_create_issues_from_vulnerabilities?
strong_memoize(:configured_to_create_issues_from_vulnerabilities) do
active? && project_key.present? && vulnerabilities_issuetype.present? && jira_vulnerabilities_integration_enabled?
end
end
def test(_)
super.then do |result|
next result unless result[:success]
next result unless project.jira_vulnerabilities_integration_enabled?
result.merge(data: { issuetypes: issue_types })
end
end
def new_issue_url_with_predefined_fields(summary, description)
escaped_summary = CGI.escape(summary)
escaped_description = CGI.escape(description)
"#{url}/secure/CreateIssueDetails!init.jspa?pid=#{jira_project_id}&issuetype=#{vulnerabilities_issuetype}&summary=#{escaped_summary}&description=#{escaped_description}"[0..MAX_URL_LENGTH]
end
def create_issue(summary, description, current_user)
return if client_url.blank?
jira_request do
issue = client.Issue.build
issue.save(
fields: {
project: { id: jira_project_id },
issuetype: { id: vulnerabilities_issuetype },
summary: summary,
description: description
}
)
log_usage(:create_issue, current_user)
issue
end
end
private
def project_key_required?
strong_memoize(:project_key_required) do
issues_enabled || vulnerabilities_enabled
end
end
# Returns internal JIRA Project ID
#
# @return [String, nil] the internal JIRA ID of the Project
def jira_project_id
jira_project&.id
end
# Returns JIRA Project for selected Project Key
#
# @return [JIRA::Resource::Project, nil] the object that represents JIRA Projects
def jira_project
strong_memoize(:jira_project) do
client_url.present? ? jira_request { client.Project.find(project_key) } : nil
end
end
# Returns list of Issue Type Scheme IDs in selected JIRA Project
#
# @return [Array] the array of IDs
def project_issuetype_scheme_ids
raise NotImplementedError unless data_fields.deployment_cloud?
query_url = Addressable::URI.join("#{client.options[:rest_base_path]}/", 'issuetypescheme/', 'project')
query_url.query_values = { 'projectId' => jira_project_id }
client
.get(query_url.to_s)
.fetch('values', [])
.map { |schemes| schemes.dig('issueTypeScheme', 'id') }
end
# Returns list of Issue Type IDs available in active Issue Type Scheme in selected JIRA Project
#
# @return [Array] the array of IDs
def project_issuetype_ids
strong_memoize(:project_issuetype_ids) do
if data_fields.deployment_server?
query_url = Addressable::URI.join("#{client.options[:rest_base_path]}/", 'project/', project_key)
client
.get(query_url.to_s)
.fetch('issueTypes', [])
.map { |issue_type| issue_type['id'] }
elsif data_fields.deployment_cloud?
query_url = Addressable::URI.join("#{client.options[:rest_base_path]}/", 'issuetypescheme/', 'mapping')
query_url.query_values = { 'issueTypeSchemeId' => project_issuetype_scheme_ids }
client
.get(query_url.to_s)
.fetch('values', [])
.map { |schemes| schemes['issueTypeId'] }
else
raise NotImplementedError
end
end
end
# Returns list of available Issue tTpes in selected JIRA Project
#
# @return [Array] the array of objects with JIRA Issuetype ID, Name and Description
def issue_types
return [] if jira_project.blank?
client
.Issuetype
.all
.select { |issue_type| issue_type.id.in?(project_issuetype_ids) }
.reject { |issue_type| issue_type.subtask }
.map { |issue_type| { id: issue_type.id, name: issue_type.name, description: issue_type.description } }
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
module Integrations module Integrations
module Jira module JiraSerializers
class IssueDetailEntity < ::Integrations::Jira::IssueEntity class IssueDetailEntity < ::Integrations::JiraSerializers::IssueEntity
expose :description_html do |jira_issue| expose :description_html do |jira_issue|
jira_gfm_pipeline(jira_issue.renderedFields['description']) jira_gfm_pipeline(jira_issue.renderedFields['description'])
end end
......
# frozen_string_literal: true # frozen_string_literal: true
module Integrations module Integrations
module Jira module JiraSerializers
class IssueDetailSerializer < BaseSerializer class IssueDetailSerializer < BaseSerializer
entity ::Integrations::Jira::IssueDetailEntity entity ::Integrations::JiraSerializers::IssueDetailEntity
end end
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
module Integrations module Integrations
module Jira module JiraSerializers
class IssueEntity < Grape::Entity class IssueEntity < Grape::Entity
include RequestAwareEntity include RequestAwareEntity
......
# frozen_string_literal: true # frozen_string_literal: true
module Integrations module Integrations
module Jira module JiraSerializers
class IssueSerializer < BaseSerializer class IssueSerializer < BaseSerializer
include WithPagination include WithPagination
entity ::Integrations::Jira::IssueEntity entity ::Integrations::JiraSerializers::IssueEntity
end end
end end
end end
...@@ -567,7 +567,7 @@ module EE ...@@ -567,7 +567,7 @@ module EE
min_id = minimum_id(JiraTrackerData.where(issues_enabled: true), :service_id) min_id = minimum_id(JiraTrackerData.where(issues_enabled: true), :service_id)
max_id = maximum_id(JiraTrackerData.where(issues_enabled: true), :service_id) max_id = maximum_id(JiraTrackerData.where(issues_enabled: true), :service_id)
# rubocop: enable UsageData/LargeTable: # rubocop: enable UsageData/LargeTable:
count(::JiraService.active.includes(:jira_tracker_data).where(jira_tracker_data: { issues_enabled: true }), start: min_id, finish: max_id) count(::Integrations::Jira.active.includes(:jira_tracker_data).where(jira_tracker_data: { issues_enabled: true }), start: min_id, finish: max_id)
end end
# rubocop:enable CodeReuse/ActiveRecord # rubocop:enable CodeReuse/ActiveRecord
......
...@@ -46,7 +46,7 @@ module Sidebars ...@@ -46,7 +46,7 @@ module Sidebars
override :render? override :render?
def render? def render?
external_issue_tracker.is_a?(JiraService) && context.jira_issues_integration external_issue_tracker.is_a?(Integrations::Jira) && context.jira_issues_integration
end end
private private
......
...@@ -71,7 +71,7 @@ RSpec.describe Projects::Integrations::Jira::IssuesController do ...@@ -71,7 +71,7 @@ RSpec.describe Projects::Integrations::Jira::IssuesController do
expect(finder).to receive(:execute).and_return(jira_issues) expect(finder).to receive(:execute).and_return(jira_issues)
end end
expect_next_instance_of(Integrations::Jira::IssueSerializer) do |serializer| expect_next_instance_of(Integrations::JiraSerializers::IssueSerializer) do |serializer|
expect(serializer).to receive(:represent).with(jira_issues, project: project) expect(serializer).to receive(:represent).with(jira_issues, project: project)
end end
...@@ -203,11 +203,11 @@ RSpec.describe Projects::Integrations::Jira::IssuesController do ...@@ -203,11 +203,11 @@ RSpec.describe Projects::Integrations::Jira::IssuesController do
before do before do
stub_licensed_features(jira_issues_integration: true) stub_licensed_features(jira_issues_integration: true)
expect_next_found_instance_of(JiraService) do |service| expect_next_found_instance_of(Integrations::Jira) do |service|
expect(service).to receive(:find_issue).with('1', rendered_fields: true).and_return(jira_issue) expect(service).to receive(:find_issue).with('1', rendered_fields: true).and_return(jira_issue)
end end
expect_next_instance_of(Integrations::Jira::IssueDetailSerializer) do |serializer| expect_next_instance_of(Integrations::JiraSerializers::IssueDetailSerializer) do |serializer|
expect(serializer).to receive(:represent).with(jira_issue, project: project).and_return(issue_json) expect(serializer).to receive(:represent).with(jira_issue, project: project).and_return(issue_json)
end end
end end
......
...@@ -15,7 +15,9 @@ RSpec.describe 'User activates Jira', :js do ...@@ -15,7 +15,9 @@ RSpec.describe 'User activates Jira', :js do
context 'when Jira connection test succeeds' do context 'when Jira connection test succeeds' do
before do before do
stub_licensed_features(jira_issues_integration: true) stub_licensed_features(jira_issues_integration: true)
allow_any_instance_of(JiraService).to receive(:issues_enabled) { true } allow_next_instance_of(Integrations::Jira) do |instance|
allow(instance).to receive(:issues_enabled) { true }
end
visit_project_integration('Jira') visit_project_integration('Jira')
fill_form fill_form
......
...@@ -184,7 +184,7 @@ RSpec.describe VulnerabilitiesHelper do ...@@ -184,7 +184,7 @@ RSpec.describe VulnerabilitiesHelper do
describe '#create_jira_issue_url_for' do describe '#create_jira_issue_url_for' do
subject { helper.vulnerability_details(vulnerability, pipeline) } subject { helper.vulnerability_details(vulnerability, pipeline) }
let(:jira_service) { double('JiraService', new_issue_url_with_predefined_fields: 'https://jira.example.com/new') } let(:jira_service) { double('Integrations::Jira', new_issue_url_with_predefined_fields: 'https://jira.example.com/new') }
before do before do
allow(helper).to receive(:can?).and_return(true) allow(helper).to receive(:can?).and_return(true)
...@@ -231,7 +231,7 @@ RSpec.describe VulnerabilitiesHelper do ...@@ -231,7 +231,7 @@ RSpec.describe VulnerabilitiesHelper do
subject subject
end end
it 'delegates rendering URL to JiraService' do it 'delegates rendering URL to Integrations::Jira' do
expect(jira_service).to receive(:new_issue_url_with_predefined_fields).with("Investigate vulnerability: #{vulnerability.title}", expected_jira_issue_description) expect(jira_service).to receive(:new_issue_url_with_predefined_fields).with("Investigate vulnerability: #{vulnerability.title}", expected_jira_issue_description)
subject subject
......
...@@ -12,7 +12,7 @@ RSpec.describe Sidebars::Projects::Menus::JiraMenu do ...@@ -12,7 +12,7 @@ RSpec.describe Sidebars::Projects::Menus::JiraMenu do
subject { described_class.new(context) } subject { described_class.new(context) }
describe 'render?' do describe 'render?' do
context 'when issue tracker is not a JiraService' do context 'when issue tracker is not Jira' do
it 'returns false' do it 'returns false' do
create(:custom_issue_tracker_service, active: true, project: project, project_url: 'http://test.com') create(:custom_issue_tracker_service, active: true, project: project, project_url: 'http://test.com')
...@@ -20,7 +20,7 @@ RSpec.describe Sidebars::Projects::Menus::JiraMenu do ...@@ -20,7 +20,7 @@ RSpec.describe Sidebars::Projects::Menus::JiraMenu do
end end
end end
context 'when issue tracker is a JiraService' do context 'when issue tracker is Jira' do
let!(:jira) { create(:jira_service, project: project, project_key: 'GL') } let!(:jira) { create(:jira_service, project: project, project_key: 'GL') }
context 'when issues integration is disabled' do context 'when issues integration is disabled' do
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe JiraService do RSpec.describe Integrations::Jira do
let(:jira_service) { build(:jira_service, **options) } let(:jira_service) { build(:jira_service, **options) }
let(:headers) { { 'Content-Type' => 'application/json' } } let(:headers) { { 'Content-Type' => 'application/json' } }
...@@ -167,11 +167,11 @@ RSpec.describe JiraService do ...@@ -167,11 +167,11 @@ RSpec.describe JiraService do
end end
before do before do
WebMock.stub_request(:get, /api\/2\/project\/GL/).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).to_return(body: project_info_result.to_json ) WebMock.stub_request(:get, %r{api/2/project/GL}).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).to_return(body: project_info_result.to_json )
WebMock.stub_request(:get, /api\/2\/project\/GL\z/).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).to_return(body: { 'id' => '10000' }.to_json, headers: headers) WebMock.stub_request(:get, %r{api/2/project/GL\z}).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).to_return(body: { 'id' => '10000' }.to_json, headers: headers)
WebMock.stub_request(:get, /api\/2\/issuetype\z/).to_return(body: issue_types_response.to_json, headers: headers) WebMock.stub_request(:get, %r{api/2/issuetype\z}).to_return(body: issue_types_response.to_json, headers: headers)
WebMock.stub_request(:get, /api\/2\/issuetypescheme\/project\?projectId\=10000\z/).to_return(body: issue_type_scheme_response.to_json, headers: headers) WebMock.stub_request(:get, %r{api/2/issuetypescheme/project\?projectId\=10000\z}).to_return(body: issue_type_scheme_response.to_json, headers: headers)
WebMock.stub_request(:get, /api\/2\/issuetypescheme\/mapping\?issueTypeSchemeId\=10126\z/).to_return(body: issue_type_mapping_response.to_json, headers: headers) WebMock.stub_request(:get, %r{api/2/issuetypescheme/mapping\?issueTypeSchemeId\=10126\z}).to_return(body: issue_type_mapping_response.to_json, headers: headers)
end end
it { is_expected.to eq(success: true, result: { jira: true }, data: { issuetypes: [{ id: '10001', name: 'Bug', description: 'Jira Bug' }] }) } it { is_expected.to eq(success: true, result: { jira: true }, data: { issuetypes: [{ id: '10001', name: 'Bug', description: 'Jira Bug' }] }) }
...@@ -236,8 +236,8 @@ RSpec.describe JiraService do ...@@ -236,8 +236,8 @@ RSpec.describe JiraService do
allow(jira_service.data_fields).to receive(:deployment_cloud?).and_return(false) allow(jira_service.data_fields).to receive(:deployment_cloud?).and_return(false)
allow(jira_service.data_fields).to receive(:deployment_server?).and_return(true) allow(jira_service.data_fields).to receive(:deployment_server?).and_return(true)
WebMock.stub_request(:get, /api\/2\/project\/GL/).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).to_return(body: project_info_result.to_json, headers: headers) WebMock.stub_request(:get, %r{api/2/project/GL}).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).to_return(body: project_info_result.to_json, headers: headers)
WebMock.stub_request(:get, /api\/2\/issuetype\z/).to_return(body: issue_types_response.to_json, headers: headers) WebMock.stub_request(:get, %r{api/2/issuetype\z}).to_return(body: issue_types_response.to_json, headers: headers)
end end
it { is_expected.to eq(success: true, result: { jira: true }, data: { issuetypes: [{ description: "A task that needs to be done.", id: "10003", name: "Task" }, { description: "Created by Jira Software - do not edit or delete. Issue type for a user story.", id: "10002", name: "Story" }, { description: "A problem which impairs or prevents the functions of the product.", id: "10004", name: "Bug" }, { description: "Created by Jira Software - do not edit or delete. Issue type for a big user story that needs to be broken down.", id: "10001", name: "Epic" }] }) } it { is_expected.to eq(success: true, result: { jira: true }, data: { issuetypes: [{ description: "A task that needs to be done.", id: "10003", name: "Task" }, { description: "Created by Jira Software - do not edit or delete. Issue type for a user story.", id: "10002", name: "Story" }, { description: "A problem which impairs or prevents the functions of the product.", id: "10004", name: "Bug" }, { description: "Created by Jira Software - do not edit or delete. Issue type for a big user story that needs to be broken down.", id: "10001", name: "Epic" }] }) }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Integrations::Jira::IssueDetailEntity do RSpec.describe Integrations::JiraSerializers::IssueDetailEntity do
include JiraServiceHelper include JiraServiceHelper
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Integrations::Jira::IssueEntity do RSpec.describe Integrations::JiraSerializers::IssueEntity do
include JiraServiceHelper include JiraServiceHelper
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Integrations::Jira::IssueSerializer do RSpec.describe Integrations::JiraSerializers::IssueSerializer do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:jira_service) { create(:jira_service, project: project) } let_it_be(:jira_service) { create(:jira_service, project: project) }
......
...@@ -102,7 +102,7 @@ RSpec.describe Vulnerabilities::FindingEntity do ...@@ -102,7 +102,7 @@ RSpec.describe Vulnerabilities::FindingEntity do
before do before do
stub_licensed_features(jira_vulnerabilities_integration: true) stub_licensed_features(jira_vulnerabilities_integration: true)
allow_next_found_instance_of(JiraService) do |jira| allow_next_found_instance_of(Integrations::Jira) do |jira|
allow(jira).to receive(:jira_project_id).and_return('11223') allow(jira).to receive(:jira_project_id).and_return('11223')
end end
end end
......
...@@ -784,6 +784,7 @@ module API ...@@ -784,6 +784,7 @@ module API
::Integrations::Datadog, ::Integrations::Datadog,
::Integrations::EmailsOnPush, ::Integrations::EmailsOnPush,
::Integrations::Ewm, ::Integrations::Ewm,
::Integrations::Jira,
::Integrations::Redmine, ::Integrations::Redmine,
::Integrations::Youtrack, ::Integrations::Youtrack,
::BuildkiteService, ::BuildkiteService,
...@@ -794,7 +795,6 @@ module API ...@@ -794,7 +795,6 @@ module API
::HangoutsChatService, ::HangoutsChatService,
::IrkerService, ::IrkerService,
::JenkinsService, ::JenkinsService,
::JiraService,
::MattermostSlashCommandsService, ::MattermostSlashCommandsService,
::SlackSlashCommandsService, ::SlackSlashCommandsService,
::PackagistService, ::PackagistService,
......
...@@ -5,7 +5,7 @@ module Gitlab ...@@ -5,7 +5,7 @@ module Gitlab
class StiType < ActiveRecord::Type::String class StiType < ActiveRecord::Type::String
NAMESPACED_INTEGRATIONS = Set.new(%w( NAMESPACED_INTEGRATIONS = Set.new(%w(
Asana Assembla Bamboo Bugzilla Campfire Confluence CustomIssueTracker Datadog Asana Assembla Bamboo Bugzilla Campfire Confluence CustomIssueTracker Datadog
EmailsOnPush Ewm IssueTracker Redmine Youtrack EmailsOnPush Ewm IssueTracker Jira Redmine Youtrack
)).freeze )).freeze
def cast(value) def cast(value)
......
...@@ -227,7 +227,7 @@ module Gitlab ...@@ -227,7 +227,7 @@ module Gitlab
} }
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
JiraService.active.includes(:jira_tracker_data).find_in_batches(batch_size: 100) do |services| ::Integrations::Jira.active.includes(:jira_tracker_data).find_in_batches(batch_size: 100) do |services|
counts = services.group_by do |service| counts = services.group_by do |service|
# TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404 # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
service_url = service.data_fields&.url || (service.properties && service.properties['url']) service_url = service.data_fields&.url || (service.properties && service.properties['url'])
......
...@@ -18662,6 +18662,9 @@ msgstr "" ...@@ -18662,6 +18662,9 @@ msgstr ""
msgid "JiraService|GitLab for Jira Configuration" msgid "JiraService|GitLab for Jira Configuration"
msgstr "" msgstr ""
msgid "JiraService|IDs must be a list of numbers that can be split with , or ;"
msgstr ""
msgid "JiraService|If different from Web URL." msgid "JiraService|If different from Web URL."
msgstr "" msgstr ""
...@@ -18677,10 +18680,10 @@ msgstr "" ...@@ -18677,10 +18680,10 @@ msgstr ""
msgid "JiraService|Jira Issues" msgid "JiraService|Jira Issues"
msgstr "" msgstr ""
msgid "JiraService|Jira comments will be created when an issue gets referenced in a commit." msgid "JiraService|Jira comments are created when an issue is referenced in a commit."
msgstr "" msgstr ""
msgid "JiraService|Jira comments will be created when an issue gets referenced in a merge request." msgid "JiraService|Jira comments are created when an issue is referenced in a merge request."
msgstr "" msgstr ""
msgid "JiraService|Jira issue type" msgid "JiraService|Jira issue type"
...@@ -18770,9 +18773,6 @@ msgstr "" ...@@ -18770,9 +18773,6 @@ msgstr ""
msgid "JiraService|You need to configure Jira before enabling this integration. For more details, read the %{jira_doc_link_start}Jira integration documentation%{link_end}." msgid "JiraService|You need to configure Jira before enabling this integration. For more details, read the %{jira_doc_link_start}Jira integration documentation%{link_end}."
msgstr "" msgstr ""
msgid "JiraService|transition ids can have only numbers which can be split with , or ;"
msgstr ""
msgid "Job" msgid "Job"
msgstr "" msgstr ""
......
...@@ -93,8 +93,8 @@ RSpec.describe Admin::IntegrationsController do ...@@ -93,8 +93,8 @@ RSpec.describe Admin::IntegrationsController do
end end
it 'deletes the integration and all inheriting integrations' do it 'deletes the integration and all inheriting integrations' do
expect { subject }.to change { JiraService.for_instance.count }.by(-1) expect { subject }.to change { Integrations::Jira.for_instance.count }.by(-1)
.and change { JiraService.inherit_from_id(integration.id).count }.by(-1) .and change { Integrations::Jira.inherit_from_id(integration.id).count }.by(-1)
end end
end end
end end
...@@ -124,8 +124,8 @@ RSpec.describe Groups::Settings::IntegrationsController do ...@@ -124,8 +124,8 @@ RSpec.describe Groups::Settings::IntegrationsController do
end end
it 'deletes the integration and all inheriting integrations' do it 'deletes the integration and all inheriting integrations' do
expect { subject }.to change { JiraService.for_group(group.id).count }.by(-1) expect { subject }.to change { Integrations::Jira.for_group(group.id).count }.by(-1)
.and change { JiraService.inherit_from_id(integration.id).count }.by(-1) .and change { Integrations::Jira.inherit_from_id(integration.id).count }.by(-1)
end end
end end
end end
......
...@@ -45,7 +45,7 @@ FactoryBot.define do ...@@ -45,7 +45,7 @@ FactoryBot.define do
token { 'test' } token { 'test' }
end end
factory :jira_service do factory :jira_service, class: 'Integrations::Jira' do
project project
active { true } active { true }
type { 'JiraService' } type { 'JiraService' }
......
...@@ -43,7 +43,7 @@ RSpec.describe 'Edit Project Settings' do ...@@ -43,7 +43,7 @@ RSpec.describe 'Edit Project Settings' do
context 'When external issue tracker is enabled and issues enabled on project settings' do context 'When external issue tracker is enabled and issues enabled on project settings' do
it 'does not hide issues tab and hides labels tab' do it 'does not hide issues tab and hides labels tab' do
allow_next_instance_of(Project) do |instance| allow_next_instance_of(Project) do |instance|
allow(instance).to receive(:external_issue_tracker).and_return(JiraService.new) allow(instance).to receive(:external_issue_tracker).and_return(Integrations::Jira.new)
end end
visit project_path(project) visit project_path(project)
...@@ -58,7 +58,7 @@ RSpec.describe 'Edit Project Settings' do ...@@ -58,7 +58,7 @@ RSpec.describe 'Edit Project Settings' do
project.issues_enabled = false project.issues_enabled = false
project.save! project.save!
allow_next_instance_of(Project) do |instance| allow_next_instance_of(Project) do |instance|
allow(instance).to receive(:external_issue_tracker).and_return(JiraService.new) allow(instance).to receive(:external_issue_tracker).and_return(Integrations::Jira.new)
end end
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe JiraService do RSpec.describe Integrations::Jira do
include AssetsHelpers include AssetsHelpers
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
...@@ -493,7 +493,7 @@ RSpec.describe JiraService do ...@@ -493,7 +493,7 @@ RSpec.describe JiraService do
before do before do
jira_service.jira_issue_transition_id = '999' jira_service.jira_issue_transition_id = '999'
# These stubs are needed to test JiraService#close_issue. # These stubs are needed to test Integrations::Jira#close_issue.
# We close the issue then do another request to API to check if it got closed. # We close the issue then do another request to API to check if it got closed.
# Here is stubbed the API return with a closed and an opened issues. # Here is stubbed the API return with a closed and an opened issues.
open_issue = JIRA::Resource::Issue.new(jira_service.client, attrs: issue_fields.deep_stringify_keys) open_issue = JIRA::Resource::Issue.new(jira_service.client, attrs: issue_fields.deep_stringify_keys)
...@@ -829,7 +829,7 @@ RSpec.describe JiraService do ...@@ -829,7 +829,7 @@ RSpec.describe JiraService do
context 'when disabled' do context 'when disabled' do
before do before do
allow_next_instance_of(JiraService) do |instance| allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:commit_events) { false } allow(instance).to receive(:commit_events) { false }
end end
end end
...@@ -847,7 +847,7 @@ RSpec.describe JiraService do ...@@ -847,7 +847,7 @@ RSpec.describe JiraService do
context 'when disabled' do context 'when disabled' do
before do before do
allow_next_instance_of(JiraService) do |instance| allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:merge_requests_events) { false } allow(instance).to receive(:merge_requests_events) { false }
end end
end end
......
...@@ -100,7 +100,7 @@ RSpec.describe DataFields do ...@@ -100,7 +100,7 @@ RSpec.describe DataFields do
context 'when service and data_fields are not persisted' do context 'when service and data_fields are not persisted' do
let(:service) do let(:service) do
JiraService.new Integrations::Jira.new
end end
describe 'data_fields_present?' do describe 'data_fields_present?' do
......
...@@ -17,14 +17,14 @@ RSpec.describe BulkUpdateIntegrationService do ...@@ -17,14 +17,14 @@ RSpec.describe BulkUpdateIntegrationService do
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) } let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:group_integration) do let_it_be(:group_integration) do
JiraService.create!( Integrations::Jira.create!(
group: group, group: group,
url: 'http://group.jira.com' url: 'http://group.jira.com'
) )
end end
let_it_be(:subgroup_integration) do let_it_be(:subgroup_integration) do
JiraService.create!( Integrations::Jira.create!(
inherit_from_id: group_integration.id, inherit_from_id: group_integration.id,
group: subgroup, group: subgroup,
url: 'http://subgroup.jira.com', url: 'http://subgroup.jira.com',
...@@ -33,7 +33,7 @@ RSpec.describe BulkUpdateIntegrationService do ...@@ -33,7 +33,7 @@ RSpec.describe BulkUpdateIntegrationService do
end end
let_it_be(:excluded_integration) do let_it_be(:excluded_integration) do
JiraService.create!( Integrations::Jira.create!(
group: create(:group), group: create(:group),
url: 'http://another.jira.com', url: 'http://another.jira.com',
push_events: false push_events: false
...@@ -41,7 +41,7 @@ RSpec.describe BulkUpdateIntegrationService do ...@@ -41,7 +41,7 @@ RSpec.describe BulkUpdateIntegrationService do
end end
let_it_be(:integration) do let_it_be(:integration) do
JiraService.create!( Integrations::Jira.create!(
project: create(:project, group: subgroup), project: create(:project, group: subgroup),
inherit_from_id: subgroup_integration.id, inherit_from_id: subgroup_integration.id,
url: 'http://project.jira.com', url: 'http://project.jira.com',
......
...@@ -181,7 +181,7 @@ RSpec.describe MergeRequests::MergeService do ...@@ -181,7 +181,7 @@ RSpec.describe MergeRequests::MergeService do
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}") commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit]) allow(merge_request).to receive(:commits).and_return([commit])
expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue, user).once expect_any_instance_of(Integrations::Jira).to receive(:close_issue).with(merge_request, jira_issue, user).once
service.execute(merge_request) service.execute(merge_request)
end end
...@@ -193,7 +193,7 @@ RSpec.describe MergeRequests::MergeService do ...@@ -193,7 +193,7 @@ RSpec.describe MergeRequests::MergeService do
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}") commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit]) allow(merge_request).to receive(:commits).and_return([commit])
expect_any_instance_of(JiraService).not_to receive(:close_issue) expect_any_instance_of(Integrations::Jira).not_to receive(:close_issue)
service.execute(merge_request) service.execute(merge_request)
end end
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
RSpec.describe ProjectServiceWorker, '#perform' do RSpec.describe ProjectServiceWorker, '#perform' do
let(:worker) { described_class.new } let(:worker) { described_class.new }
let(:service) { JiraService.new } let(:service) { Integrations::Jira.new }
before do before do
allow(Integration).to receive(:find).and_return(service) allow(Integration).to receive(:find).and_return(service)
......
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