Commit eade74bf authored by Luke Duncalfe's avatar Luke Duncalfe Committed by Mayra Cabrera

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

parent 6c203b47
......@@ -872,7 +872,6 @@ RSpec/AnyInstanceOf:
- 'ee/spec/features/issues/form_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/services/user_activates_jira_spec.rb'
- 'ee/spec/features/registrations/welcome_spec.rb'
- 'ee/spec/features/security/project/internal_access_spec.rb'
- 'ee/spec/features/security/project/private_access_spec.rb'
......@@ -1151,6 +1150,7 @@ RSpec/AnyInstanceOf:
- 'spec/models/hooks/service_hook_spec.rb'
- 'spec/models/hooks/system_hook_spec.rb'
- 'spec/models/hooks/web_hook_spec.rb'
- 'spec/models/integrations/jira_spec.rb'
- 'spec/models/issue_spec.rb'
- 'spec/models/key_spec.rb'
- 'spec/models/member_spec.rb'
......@@ -1158,7 +1158,6 @@ RSpec/AnyInstanceOf:
- 'spec/models/merge_request_spec.rb'
- 'spec/models/note_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_spec.rb'
- 'spec/models/repository_spec.rb'
......@@ -1662,7 +1661,6 @@ Gitlab/NamespacedClass:
- 'app/models/project_services/irker_service.rb'
- 'app/models/project_services/issue_tracker_data.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/mattermost_service.rb'
- 'app/models/project_services/mattermost_slash_commands_service.rb'
......@@ -2918,7 +2916,6 @@ Style/RegexpLiteralMixedPreserve:
- 'ee/spec/features/groups/saml_enforcement_spec.rb'
- 'ee/spec/features/markdown/metrics_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'
- 'lib/api/invitations.rb'
- 'lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb'
......
......@@ -15,7 +15,7 @@ module Types
definition_methods do
def resolve_type(object, context)
if object.is_a?(::JiraService)
if object.is_a?(::Integrations::Jira)
Types::Projects::Services::JiraServiceType
else
Types::Projects::Services::BaseServiceType
......
......@@ -107,7 +107,7 @@ module ServicesHelper
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_id] = integration.jira_issue_transition_id
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
has_one :datadog_service, class_name: 'Integrations::Datadog'
has_one :emails_on_push_service, class_name: 'Integrations::EmailsOnPush'
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 :youtrack_service, class_name: 'Integrations::Youtrack'
has_one :discord_service
......@@ -209,7 +210,6 @@ class Project < ApplicationRecord
has_one :teamcity_service
has_one :pushover_service
has_one :jenkins_service
has_one :jira_service
has_one :external_wiki_service
has_one :prometheus_service, inverse_of: :project
has_one :mock_ci_service
......@@ -560,7 +560,7 @@ class Project < ApplicationRecord
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
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_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_server, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false)) }
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
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
case deployment_type.upcase
when JiraService::DEPLOYMENT_TYPES[:server]
when Integrations::Jira::DEPLOYMENT_TYPES[:server]
ServerUsersMapperService.new(user, project, start_at)
when JiraService::DEPLOYMENT_TYPES[:cloud]
when Integrations::Jira::DEPLOYMENT_TYPES[:cloud]
CloudUsersMapperService.new(user, project, start_at)
else
raise ArgumentError
......
......@@ -302,7 +302,7 @@ It contains information about [integrations](../user/project/integrations/overvi
{
"severity":"ERROR",
"time":"2018-09-06T14:56:20.439Z",
"service_class":"JiraService",
"service_class":"Integrations::Jira",
"project_id":8,
"project_path":"h5bp/html5-boilerplate",
"message":"Error sending message",
......@@ -312,7 +312,7 @@ It contains information about [integrations](../user/project/integrations/overvi
{
"severity":"INFO",
"time":"2018-09-06T17:15:16.365Z",
"service_class":"JiraService",
"service_class":"Integrations::Jira",
"project_id":3,
"project_path":"namespace2/project2",
"message":"Successfully posted",
......
......@@ -54,13 +54,13 @@ module Projects
total_count: finder.total_count
)
::Integrations::Jira::IssueSerializer.new
::Integrations::JiraSerializers::IssueSerializer.new
.with_pagination(request, response)
.represent(jira_issues, project: project)
end
def issue_json
::Integrations::Jira::IssueDetailSerializer.new
::Integrations::JiraSerializers::IssueDetailSerializer.new
.represent(project.jira_service.find_issue(params[:id], rendered_fields: true), project: project)
end
......
......@@ -34,7 +34,7 @@ module Resolvers
def serialize_external_issue(external_issue, external_type)
case external_type
when 'jira'
::Integrations::Jira::IssueSerializer
::Integrations::JiraSerializers::IssueSerializer
.new
.represent(external_issue, project: object.vulnerability.project, only: %i[title references status external_tracker web_url created_at updated_at] )
end
......
......@@ -13,7 +13,7 @@ module EE
def integration_form_data(integration, group: nil)
form_data = super
if integration.is_a?(JiraService)
if integration.is_a?(Integrations::Jira)
form_data.merge!(
show_jira_issues_integration: @project&.jira_issues_integration_available?.to_s,
show_jira_vulnerabilities_integration: integration.jira_vulnerabilities_integration_available?.to_s,
......
......@@ -15,7 +15,7 @@ module VulnerabilitiesHelper
new_issue_url: new_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]),
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),
create_mr_url: create_vulnerability_feedback_merge_request_path(vulnerability.finding.project),
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
module Integrations
module Jira
class IssueDetailEntity < ::Integrations::Jira::IssueEntity
module JiraSerializers
class IssueDetailEntity < ::Integrations::JiraSerializers::IssueEntity
expose :description_html do |jira_issue|
jira_gfm_pipeline(jira_issue.renderedFields['description'])
end
......
# frozen_string_literal: true
module Integrations
module Jira
module JiraSerializers
class IssueDetailSerializer < BaseSerializer
entity ::Integrations::Jira::IssueDetailEntity
entity ::Integrations::JiraSerializers::IssueDetailEntity
end
end
end
# frozen_string_literal: true
module Integrations
module Jira
module JiraSerializers
class IssueEntity < Grape::Entity
include RequestAwareEntity
......
# frozen_string_literal: true
module Integrations
module Jira
module JiraSerializers
class IssueSerializer < BaseSerializer
include WithPagination
entity ::Integrations::Jira::IssueEntity
entity ::Integrations::JiraSerializers::IssueEntity
end
end
end
......@@ -567,7 +567,7 @@ module EE
min_id = minimum_id(JiraTrackerData.where(issues_enabled: true), :service_id)
max_id = maximum_id(JiraTrackerData.where(issues_enabled: true), :service_id)
# 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
# rubocop:enable CodeReuse/ActiveRecord
......
......@@ -46,7 +46,7 @@ module Sidebars
override :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
private
......
......@@ -71,7 +71,7 @@ RSpec.describe Projects::Integrations::Jira::IssuesController do
expect(finder).to receive(:execute).and_return(jira_issues)
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)
end
......@@ -203,11 +203,11 @@ RSpec.describe Projects::Integrations::Jira::IssuesController do
before do
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)
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)
end
end
......
......@@ -15,7 +15,9 @@ RSpec.describe 'User activates Jira', :js do
context 'when Jira connection test succeeds' do
before do
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')
fill_form
......
......@@ -184,7 +184,7 @@ RSpec.describe VulnerabilitiesHelper do
describe '#create_jira_issue_url_for' do
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
allow(helper).to receive(:can?).and_return(true)
......@@ -231,7 +231,7 @@ RSpec.describe VulnerabilitiesHelper do
subject
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)
subject
......
......@@ -12,7 +12,7 @@ RSpec.describe Sidebars::Projects::Menus::JiraMenu do
subject { described_class.new(context) }
describe 'render?' do
context 'when issue tracker is not a JiraService' do
context 'when issue tracker is not Jira' do
it 'returns false' do
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
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') }
context 'when issues integration is disabled' do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe JiraService do
RSpec.describe Integrations::Jira do
let(:jira_service) { build(:jira_service, **options) }
let(:headers) { { 'Content-Type' => 'application/json' } }
......@@ -167,11 +167,11 @@ RSpec.describe JiraService do
end
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, /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, /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/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\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/issuetype\z}).to_return(body: issue_types_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, %r{api/2/issuetypescheme/mapping\?issueTypeSchemeId\=10126\z}).to_return(body: issue_type_mapping_response.to_json, headers: headers)
end
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
allow(jira_service.data_fields).to receive(:deployment_cloud?).and_return(false)
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, /api\/2\/issuetype\z/).to_return(body: issue_types_response.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, %r{api/2/issuetype\z}).to_return(body: issue_types_response.to_json, headers: headers)
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" }] }) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Integrations::Jira::IssueDetailEntity do
RSpec.describe Integrations::JiraSerializers::IssueDetailEntity do
include JiraServiceHelper
let_it_be(:project) { create(:project) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Integrations::Jira::IssueEntity do
RSpec.describe Integrations::JiraSerializers::IssueEntity do
include JiraServiceHelper
let_it_be(:project) { create(:project) }
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Integrations::Jira::IssueSerializer do
RSpec.describe Integrations::JiraSerializers::IssueSerializer do
let_it_be(:project) { create(:project) }
let_it_be(:jira_service) { create(:jira_service, project: project) }
......
......@@ -102,7 +102,7 @@ RSpec.describe Vulnerabilities::FindingEntity do
before do
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')
end
end
......
......@@ -784,6 +784,7 @@ module API
::Integrations::Datadog,
::Integrations::EmailsOnPush,
::Integrations::Ewm,
::Integrations::Jira,
::Integrations::Redmine,
::Integrations::Youtrack,
::BuildkiteService,
......@@ -794,7 +795,6 @@ module API
::HangoutsChatService,
::IrkerService,
::JenkinsService,
::JiraService,
::MattermostSlashCommandsService,
::SlackSlashCommandsService,
::PackagistService,
......
......@@ -5,7 +5,7 @@ module Gitlab
class StiType < ActiveRecord::Type::String
NAMESPACED_INTEGRATIONS = Set.new(%w(
Asana Assembla Bamboo Bugzilla Campfire Confluence CustomIssueTracker Datadog
EmailsOnPush Ewm IssueTracker Redmine Youtrack
EmailsOnPush Ewm IssueTracker Jira Redmine Youtrack
)).freeze
def cast(value)
......
......@@ -227,7 +227,7 @@ module Gitlab
}
# 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|
# TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404
service_url = service.data_fields&.url || (service.properties && service.properties['url'])
......
......@@ -18662,6 +18662,9 @@ msgstr ""
msgid "JiraService|GitLab for Jira Configuration"
msgstr ""
msgid "JiraService|IDs must be a list of numbers that can be split with , or ;"
msgstr ""
msgid "JiraService|If different from Web URL."
msgstr ""
......@@ -18677,10 +18680,10 @@ msgstr ""
msgid "JiraService|Jira Issues"
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 ""
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 ""
msgid "JiraService|Jira issue type"
......@@ -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}."
msgstr ""
msgid "JiraService|transition ids can have only numbers which can be split with , or ;"
msgstr ""
msgid "Job"
msgstr ""
......
......@@ -93,8 +93,8 @@ RSpec.describe Admin::IntegrationsController do
end
it 'deletes the integration and all inheriting integrations' do
expect { subject }.to change { JiraService.for_instance.count }.by(-1)
.and change { JiraService.inherit_from_id(integration.id).count }.by(-1)
expect { subject }.to change { Integrations::Jira.for_instance.count }.by(-1)
.and change { Integrations::Jira.inherit_from_id(integration.id).count }.by(-1)
end
end
end
......@@ -124,8 +124,8 @@ RSpec.describe Groups::Settings::IntegrationsController do
end
it 'deletes the integration and all inheriting integrations' do
expect { subject }.to change { JiraService.for_group(group.id).count }.by(-1)
.and change { JiraService.inherit_from_id(integration.id).count }.by(-1)
expect { subject }.to change { Integrations::Jira.for_group(group.id).count }.by(-1)
.and change { Integrations::Jira.inherit_from_id(integration.id).count }.by(-1)
end
end
end
......
......@@ -45,7 +45,7 @@ FactoryBot.define do
token { 'test' }
end
factory :jira_service do
factory :jira_service, class: 'Integrations::Jira' do
project
active { true }
type { 'JiraService' }
......
......@@ -43,7 +43,7 @@ RSpec.describe 'Edit 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
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
visit project_path(project)
......@@ -58,7 +58,7 @@ RSpec.describe 'Edit Project Settings' do
project.issues_enabled = false
project.save!
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
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe JiraService do
RSpec.describe Integrations::Jira do
include AssetsHelpers
let_it_be(:project) { create(:project, :repository) }
......@@ -493,7 +493,7 @@ RSpec.describe JiraService do
before do
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.
# 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)
......@@ -829,7 +829,7 @@ RSpec.describe JiraService do
context 'when disabled' 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 }
end
end
......@@ -847,7 +847,7 @@ RSpec.describe JiraService do
context 'when disabled' 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 }
end
end
......
......@@ -100,7 +100,7 @@ RSpec.describe DataFields do
context 'when service and data_fields are not persisted' do
let(:service) do
JiraService.new
Integrations::Jira.new
end
describe 'data_fields_present?' do
......
......@@ -17,14 +17,14 @@ RSpec.describe BulkUpdateIntegrationService do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:group_integration) do
JiraService.create!(
Integrations::Jira.create!(
group: group,
url: 'http://group.jira.com'
)
end
let_it_be(:subgroup_integration) do
JiraService.create!(
Integrations::Jira.create!(
inherit_from_id: group_integration.id,
group: subgroup,
url: 'http://subgroup.jira.com',
......@@ -33,7 +33,7 @@ RSpec.describe BulkUpdateIntegrationService do
end
let_it_be(:excluded_integration) do
JiraService.create!(
Integrations::Jira.create!(
group: create(:group),
url: 'http://another.jira.com',
push_events: false
......@@ -41,7 +41,7 @@ RSpec.describe BulkUpdateIntegrationService do
end
let_it_be(:integration) do
JiraService.create!(
Integrations::Jira.create!(
project: create(:project, group: subgroup),
inherit_from_id: subgroup_integration.id,
url: 'http://project.jira.com',
......
......@@ -181,7 +181,7 @@ RSpec.describe MergeRequests::MergeService do
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
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)
end
......@@ -193,7 +193,7 @@ RSpec.describe MergeRequests::MergeService do
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
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)
end
......
......@@ -3,7 +3,7 @@ require 'spec_helper'
RSpec.describe ProjectServiceWorker, '#perform' do
let(:worker) { described_class.new }
let(:service) { JiraService.new }
let(:service) { Integrations::Jira.new }
before do
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