Commit 94e664de authored by Luke Duncalfe's avatar Luke Duncalfe Committed by Arturo Herrero

Initial namespacing services in `Integrations`

parent 25c73c95
......@@ -1673,9 +1673,6 @@ Gitlab/NamespacedClass:
- 'app/models/project_repository_storage_move.rb'
- 'app/models/project_services/alerts_service.rb'
- 'app/models/project_services/alerts_service_data.rb'
- 'app/models/project_services/asana_service.rb'
- 'app/models/project_services/assembla_service.rb'
- 'app/models/project_services/bamboo_service.rb'
- 'app/models/project_services/bugzilla_service.rb'
- 'app/models/project_services/buildkite_service.rb'
- 'app/models/project_services/builds_email_service.rb'
......
# frozen_string_literal: true
require 'asana'
module Integrations
class Asana < Service
include ActionView::Helpers::UrlHelper
prop_accessor :api_key, :restrict_to_branch
validates :api_key, presence: true, if: :activated?
def title
'Asana'
end
def description
s_('AsanaService|Add commit messages as comments to Asana tasks.')
end
def help
docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/asana'), target: '_blank', rel: 'noopener noreferrer'
s_('Add commit messages as comments to Asana tasks. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
'asana'
end
def fields
[
{
type: 'text',
name: 'api_key',
title: 'API key',
help: s_('AsanaService|User Personal Access Token. User must have access to the task. All comments are attributed to this user.'),
# Example Personal Access Token from Asana docs
placeholder: '0/68a9e79b868c6789e79a124c30b0',
required: true
},
{
type: 'text',
name: 'restrict_to_branch',
title: 'Restrict to branch (optional)',
help: s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.')
}
]
end
def self.supported_events
%w(push)
end
def client
@_client ||= begin
::Asana::Client.new do |c|
c.authentication :access_token, api_key
end
end
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
# check the branch restriction is poplulated and branch is not included
branch = Gitlab::Git.ref_name(data[:ref])
branch_restriction = restrict_to_branch.to_s
if branch_restriction.present? && branch_restriction.index(branch).nil?
return
end
user = data[:user_name]
project_name = project.full_name
data[:commits].each do |commit|
push_msg = s_("AsanaService|%{user} pushed to branch %{branch} of %{project_name} ( %{commit_url} ):") % { user: user, branch: branch, project_name: project_name, commit_url: commit[:url] }
check_commit(commit[:message], push_msg)
end
end
def check_commit(message, push_msg)
# matches either:
# - #1234
# - https://app.asana.com/0/{project_gid}/{task_gid}
# optionally preceded with:
# - fix/ed/es/ing
# - close/s/d
# - closing
issue_finder = %r{(fix\w*|clos[ei]\w*+)?\W*(?:https://app\.asana\.com/\d+/\w+/(\w+)|#(\w+))}i
message.scan(issue_finder).each do |tuple|
# tuple will be
# [ 'fix', 'id_from_url', 'id_from_pound' ]
taskid = tuple[2] || tuple[1]
begin
task = ::Asana::Resources::Task.find_by_id(client, taskid)
task.add_comment(text: "#{push_msg} #{message}")
if tuple[0]
task.update(completed: true)
end
rescue StandardError => e
log_error(e.message)
next
end
end
end
end
end
# frozen_string_literal: true
module Integrations
class Assembla < Service
prop_accessor :token, :subdomain
validates :token, presence: true, if: :activated?
def title
'Assembla'
end
def description
_('Manage projects.')
end
def self.to_param
'assembla'
end
def fields
[
{ type: 'text', name: 'token', placeholder: '', required: true },
{ type: 'text', name: 'subdomain', placeholder: '' }
]
end
def self.supported_events
%w(push)
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}"
Gitlab::HTTP.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' })
end
end
end
# frozen_string_literal: true
module Integrations
class Bamboo < CiService
include ActionView::Helpers::UrlHelper
include ReactiveService
prop_accessor :bamboo_url, :build_key, :username, :password
validates :bamboo_url, presence: true, public_url: true, if: :activated?
validates :build_key, presence: true, if: :activated?
validates :username,
presence: true,
if: ->(service) { service.activated? && service.password }
validates :password,
presence: true,
if: ->(service) { service.activated? && service.username }
attr_accessor :response
after_save :compose_service_hook, if: :activated?
before_update :reset_password
def compose_service_hook
hook = service_hook || build_service_hook
hook.save
end
def reset_password
if bamboo_url_changed? && !password_touched?
self.password = nil
end
end
def title
s_('BambooService|Atlassian Bamboo')
end
def description
s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo.')
end
def help
docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'), target: '_blank', rel: 'noopener noreferrer'
s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo. You must set up automatic revision labeling and a repository trigger in Bamboo. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
'bamboo'
end
def fields
[
{
type: 'text',
name: 'bamboo_url',
title: s_('BambooService|Bamboo URL'),
placeholder: s_('https://bamboo.example.com'),
help: s_('BambooService|Bamboo service root URL.'),
required: true
},
{
type: 'text',
name: 'build_key',
placeholder: s_('KEY'),
help: s_('BambooService|Bamboo build plan key.'),
required: true
},
{
type: 'text',
name: 'username',
help: s_('BambooService|The user with API access to the Bamboo server.')
},
{
type: 'password',
name: 'password',
non_empty_password_title: s_('ProjectService|Enter new password'),
non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
}
]
end
def build_page(sha, ref)
with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end
def commit_status(sha, ref)
with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
get_path("updateAndBuild.action", { buildKey: build_key })
end
def calculate_reactive_cache(sha, ref)
response = try_get_path("rest/api/latest/result/byChangeset/#{sha}")
{ build_page: read_build_page(response), commit_status: read_commit_status(response) }
end
private
def get_build_result(response)
return if response&.code != 200
# May be nil if no result, a single result hash, or an array if multiple results for a given changeset.
result = response.dig('results', 'results', 'result')
# In case of multiple results, arbitrarily assume the last one is the most relevant.
return result.last if result.is_a?(Array)
result
end
def read_build_page(response)
result = get_build_result(response)
key =
if result.blank?
# If actual build link can't be determined, send user to build summary page.
build_key
else
# If actual build link is available, go to build result page.
result.dig('planResultKey', 'key')
end
build_url("browse/#{key}")
end
def read_commit_status(response)
return :error unless response && (response.code == 200 || response.code == 404)
result = get_build_result(response)
status =
if result.blank?
'Pending'
else
result.dig('buildState')
end
return :error unless status.present?
if status.include?('Success')
'success'
elsif status.include?('Failed')
'failed'
elsif status.include?('Pending')
'pending'
else
:error
end
end
def try_get_path(path, query_params = {})
params = build_get_params(query_params)
params[:extra_log_info] = { project_id: project_id }
Gitlab::HTTP.try_get(build_url(path), params)
end
def get_path(path, query_params = {})
Gitlab::HTTP.get(build_url(path), build_get_params(query_params))
end
def build_url(path)
Gitlab::Utils.append_path(bamboo_url, path)
end
def build_get_params(query_params)
params = { verify: false, query: query_params }
return params if username.blank? && password.blank?
query_params[:os_authType] = 'basic'
params[:basic_auth] = basic_auth
params
end
def basic_auth
{ username: username, password: password }
end
end
end
......@@ -149,7 +149,10 @@ class Project < ApplicationRecord
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
has_many :boards
# Project services
# Project integrations
has_one :asana_service, class_name: 'Integrations::Asana'
has_one :assembla_service, class_name: 'Integrations::Assembla'
has_one :bamboo_service, class_name: 'Integrations::Bamboo'
has_one :campfire_service
has_one :datadog_service
has_one :discord_service
......@@ -160,14 +163,11 @@ class Project < ApplicationRecord
has_one :irker_service
has_one :pivotaltracker_service
has_one :flowdock_service
has_one :assembla_service
has_one :asana_service
has_one :mattermost_slash_commands_service
has_one :mattermost_service
has_one :slack_slash_commands_service
has_one :slack_service
has_one :buildkite_service
has_one :bamboo_service
has_one :teamcity_service
has_one :pushover_service
has_one :jenkins_service
......@@ -2603,7 +2603,7 @@ class Project < ApplicationRecord
end
def build_service(name)
"#{name}_service".classify.constantize.new(project_id: id)
Service.service_name_to_model(name).new(project_id: id)
end
def services_templates
......
# frozen_string_literal: true
require 'asana'
class AsanaService < Service
include ActionView::Helpers::UrlHelper
prop_accessor :api_key, :restrict_to_branch
validates :api_key, presence: true, if: :activated?
def title
'Asana'
end
def description
s_('AsanaService|Add commit messages as comments to Asana tasks.')
end
def help
docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/asana'), target: '_blank', rel: 'noopener noreferrer'
s_('Add commit messages as comments to Asana tasks. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
'asana'
end
def fields
[
{
type: 'text',
name: 'api_key',
title: 'API key',
help: s_('AsanaService|User Personal Access Token. User must have access to the task. All comments are attributed to this user.'),
# Example Personal Access Token from Asana docs
placeholder: '0/68a9e79b868c6789e79a124c30b0',
required: true
},
{
type: 'text',
name: 'restrict_to_branch',
title: 'Restrict to branch (optional)',
help: s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.')
}
]
end
def self.supported_events
%w(push)
end
def client
@_client ||= begin
Asana::Client.new do |c|
c.authentication :access_token, api_key
end
end
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
# check the branch restriction is poplulated and branch is not included
branch = Gitlab::Git.ref_name(data[:ref])
branch_restriction = restrict_to_branch.to_s
if branch_restriction.present? && branch_restriction.index(branch).nil?
return
end
user = data[:user_name]
project_name = project.full_name
data[:commits].each do |commit|
push_msg = s_("AsanaService|%{user} pushed to branch %{branch} of %{project_name} ( %{commit_url} ):") % { user: user, branch: branch, project_name: project_name, commit_url: commit[:url] }
check_commit(commit[:message], push_msg)
end
end
def check_commit(message, push_msg)
# matches either:
# - #1234
# - https://app.asana.com/0/{project_gid}/{task_gid}
# optionally preceded with:
# - fix/ed/es/ing
# - close/s/d
# - closing
issue_finder = %r{(fix\w*|clos[ei]\w*+)?\W*(?:https://app\.asana\.com/\d+/\w+/(\w+)|#(\w+))}i
message.scan(issue_finder).each do |tuple|
# tuple will be
# [ 'fix', 'id_from_url', 'id_from_pound' ]
taskid = tuple[2] || tuple[1]
begin
task = Asana::Resources::Task.find_by_id(client, taskid)
task.add_comment(text: "#{push_msg} #{message}")
if tuple[0]
task.update(completed: true)
end
rescue StandardError => e
log_error(e.message)
next
end
end
end
end
# frozen_string_literal: true
class AssemblaService < Service
prop_accessor :token, :subdomain
validates :token, presence: true, if: :activated?
def title
'Assembla'
end
def description
_('Manage projects.')
end
def self.to_param
'assembla'
end
def fields
[
{ type: 'text', name: 'token', placeholder: '', required: true },
{ type: 'text', name: 'subdomain', placeholder: '' }
]
end
def self.supported_events
%w(push)
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}"
Gitlab::HTTP.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' })
end
end
# frozen_string_literal: true
class BambooService < CiService
include ActionView::Helpers::UrlHelper
include ReactiveService
prop_accessor :bamboo_url, :build_key, :username, :password
validates :bamboo_url, presence: true, public_url: true, if: :activated?
validates :build_key, presence: true, if: :activated?
validates :username,
presence: true,
if: ->(service) { service.activated? && service.password }
validates :password,
presence: true,
if: ->(service) { service.activated? && service.username }
attr_accessor :response
after_save :compose_service_hook, if: :activated?
before_update :reset_password
def compose_service_hook
hook = service_hook || build_service_hook
hook.save
end
def reset_password
if bamboo_url_changed? && !password_touched?
self.password = nil
end
end
def title
s_('BambooService|Atlassian Bamboo')
end
def description
s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo.')
end
def help
docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'), target: '_blank', rel: 'noopener noreferrer'
s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo. You must set up automatic revision labeling and a repository trigger in Bamboo. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
'bamboo'
end
def fields
[
{
type: 'text',
name: 'bamboo_url',
title: s_('BambooService|Bamboo URL'),
placeholder: s_('https://bamboo.example.com'),
help: s_('BambooService|Bamboo service root URL.'),
required: true
},
{
type: 'text',
name: 'build_key',
placeholder: s_('KEY'),
help: s_('BambooService|Bamboo build plan key.'),
required: true
},
{
type: 'text',
name: 'username',
help: s_('BambooService|The user with API access to the Bamboo server.')
},
{
type: 'password',
name: 'password',
non_empty_password_title: s_('ProjectService|Enter new password'),
non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
}
]
end
def build_page(sha, ref)
with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end
def commit_status(sha, ref)
with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
get_path("updateAndBuild.action", { buildKey: build_key })
end
def calculate_reactive_cache(sha, ref)
response = try_get_path("rest/api/latest/result/byChangeset/#{sha}")
{ build_page: read_build_page(response), commit_status: read_commit_status(response) }
end
private
def get_build_result(response)
return if response&.code != 200
# May be nil if no result, a single result hash, or an array if multiple results for a given changeset.
result = response.dig('results', 'results', 'result')
# In case of multiple results, arbitrarily assume the last one is the most relevant.
return result.last if result.is_a?(Array)
result
end
def read_build_page(response)
result = get_build_result(response)
key =
if result.blank?
# If actual build link can't be determined, send user to build summary page.
build_key
else
# If actual build link is available, go to build result page.
result.dig('planResultKey', 'key')
end
build_url("browse/#{key}")
end
def read_commit_status(response)
return :error unless response && (response.code == 200 || response.code == 404)
result = get_build_result(response)
status =
if result.blank?
'Pending'
else
result.dig('buildState')
end
return :error unless status.present?
if status.include?('Success')
'success'
elsif status.include?('Failed')
'failed'
elsif status.include?('Pending')
'pending'
else
:error
end
end
def try_get_path(path, query_params = {})
params = build_get_params(query_params)
params[:extra_log_info] = { project_id: project_id }
Gitlab::HTTP.try_get(build_url(path), params)
end
def get_path(path, query_params = {})
Gitlab::HTTP.get(build_url(path), build_get_params(query_params))
end
def build_url(path)
Gitlab::Utils.append_path(bamboo_url, path)
end
def build_get_params(query_params)
params = { verify: false, query: query_params }
return params if username.blank? && password.blank?
query_params[:os_authType] = 'basic'
params[:basic_auth] = basic_auth
params
end
def basic_auth
{ username: username, password: password }
end
end
......@@ -28,6 +28,8 @@ class Service < ApplicationRecord
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
attribute :type, Gitlab::Integrations::StiType.new
default_value_for :active, false
default_value_for :alert_events, true
default_value_for :category, 'common'
......@@ -164,22 +166,23 @@ class Service < ApplicationRecord
end
def self.create_nonexistent_templates
nonexistent_services = list_nonexistent_services_for(for_template)
nonexistent_services = build_nonexistent_services_for(for_template)
return if nonexistent_services.empty?
# Create within a transaction to perform the lowest possible SQL queries.
transaction do
nonexistent_services.each do |service_type|
service_type.constantize.create(template: true)
nonexistent_services.each do |service|
service.template = true
service.save
end
end
end
private_class_method :create_nonexistent_templates
def self.find_or_initialize_non_project_specific_integration(name, instance: false, group_id: nil)
if name.in?(available_services_names(include_project_specific: false))
"#{name}_service".camelize.constantize.find_or_initialize_by(instance: instance, group_id: group_id)
end
return unless name.in?(available_services_names(include_project_specific: false))
service_name_to_model(name).find_or_initialize_by(instance: instance, group_id: group_id)
end
def self.find_or_initialize_all_non_project_specific(scope)
......@@ -187,19 +190,23 @@ class Service < ApplicationRecord
end
def self.build_nonexistent_services_for(scope)
list_nonexistent_services_for(scope).map do |service_type|
service_type.constantize.new
nonexistent_services_types_for(scope).map do |service_type|
service_type_to_model(service_type).new
end
end
private_class_method :build_nonexistent_services_for
def self.list_nonexistent_services_for(scope)
# Returns a list of service types that do not exist in the given scope.
# Example: ["AsanaService", ...]
def self.nonexistent_services_types_for(scope)
# Using #map instead of #pluck to save one query count. This is because
# ActiveRecord loaded the object here, so we don't need to query again later.
available_services_types(include_project_specific: false) - scope.map(&:type)
end
private_class_method :list_nonexistent_services_for
private_class_method :nonexistent_services_types_for
# Returns a list of available service names.
# Example: ["asana", ...]
def self.available_services_names(include_project_specific: true, include_dev: true)
service_names = services_names
service_names += project_specific_services_names if include_project_specific
......@@ -222,12 +229,34 @@ class Service < ApplicationRecord
PROJECT_SPECIFIC_SERVICE_NAMES
end
# Returns a list of available service types.
# Example: ["AsanaService", ...]
def self.available_services_types(include_project_specific: true, include_dev: true)
available_services_names(include_project_specific: include_project_specific, include_dev: include_dev).map do |service_name|
"#{service_name}_service".camelize
service_name_to_type(service_name)
end
end
# Returns the model for the given service name.
# Example: "asana" => Integrations::Asana
def self.service_name_to_model(name)
type = service_name_to_type(name)
service_type_to_model(type)
end
# Returns the STI type for the given service name.
# Example: "asana" => "AsanaService"
def self.service_name_to_type(name)
"#{name}_service".camelize
end
# Returns the model for the given STI type.
# Example: "AsanaService" => Integrations::Asana
def self.service_type_to_model(type)
Gitlab::Integrations::StiType.new.cast(type).constantize
end
private_class_method :service_type_to_model
def self.build_from_integration(integration, project_id: nil, group_id: nil)
service = integration.dup
......
......@@ -47,6 +47,7 @@ module Gitlab
config.eager_load_paths.push(*%W[#{config.root}/lib
#{config.root}/app/models/badges
#{config.root}/app/models/hooks
#{config.root}/app/models/integrations
#{config.root}/app/models/members
#{config.root}/app/models/project_services
#{config.root}/app/graphql/resolvers/concerns
......
......@@ -774,9 +774,9 @@ module API
def self.service_classes
[
::AsanaService,
::AssemblaService,
::BambooService,
::Integrations::Asana,
::Integrations::Assembla,
::Integrations::Bamboo,
::BugzillaService,
::BuildkiteService,
::ConfluenceService,
......
# frozen_string_literal: true
module Gitlab
module Integrations
class StiType < ActiveRecord::Type::String
NAMESPACED_INTEGRATIONS = Set.new(%w(
Asana Assembla Bamboo
)).freeze
def cast(value)
new_cast(value) || super
end
def serialize(value)
new_serialize(value) || super
end
def deserialize(value)
value
end
def changed?(original_value, value, _new_value_before_type_cast)
original_value != serialize(value)
end
def changed_in_place?(original_value_for_database, value)
original_value_for_database != serialize(value)
end
private
def new_cast(value)
value = prepare_value(value)
return unless value
stripped_name = value.delete_suffix('Service')
return unless NAMESPACED_INTEGRATIONS.include?(stripped_name)
"Integrations::#{stripped_name}"
end
def new_serialize(value)
value = prepare_value(value)
return unless value&.starts_with?('Integrations::')
"#{value.delete_prefix('Integrations::')}Service"
end
# Returns value cast to a `String`, or `nil` if value is `nil`.
def prepare_value(value)
return value if value.nil? || value.is_a?(String)
value.to_s
end
end
end
end
......@@ -427,12 +427,14 @@ module Gitlab
def services_usage
# rubocop: disable UsageData/LargeTable:
Service.available_services_names.each_with_object({}) do |service_name, response|
response["projects_#{service_name}_active".to_sym] = count(Service.active.where.not(project: nil).where(type: "#{service_name}_service".camelize))
response["groups_#{service_name}_active".to_sym] = count(Service.active.where.not(group: nil).where(type: "#{service_name}_service".camelize))
response["templates_#{service_name}_active".to_sym] = count(Service.active.where(template: true, type: "#{service_name}_service".camelize))
response["instances_#{service_name}_active".to_sym] = count(Service.active.where(instance: true, type: "#{service_name}_service".camelize))
response["projects_inheriting_#{service_name}_active".to_sym] = count(Service.active.where.not(project: nil).where.not(inherit_from_id: nil).where(type: "#{service_name}_service".camelize))
response["groups_inheriting_#{service_name}_active".to_sym] = count(Service.active.where.not(group: nil).where.not(inherit_from_id: nil).where(type: "#{service_name}_service".camelize))
service_type = Service.service_name_to_type(service_name)
response["projects_#{service_name}_active".to_sym] = count(Service.active.where.not(project: nil).where(type: service_type))
response["groups_#{service_name}_active".to_sym] = count(Service.active.where.not(group: nil).where(type: service_type))
response["templates_#{service_name}_active".to_sym] = count(Service.active.where(template: true, type: service_type))
response["instances_#{service_name}_active".to_sym] = count(Service.active.where(instance: true, type: service_type))
response["projects_inheriting_#{service_name}_active".to_sym] = count(Service.active.where.not(project: nil).where.not(inherit_from_id: nil).where(type: service_type))
response["groups_inheriting_#{service_name}_active".to_sym] = count(Service.active.where.not(group: nil).where.not(inherit_from_id: nil).where(type: service_type))
end.merge(jira_usage, jira_import_usage)
# rubocop: enable UsageData/LargeTable:
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Integrations::StiType do
let(:types) { ['AsanaService', 'Integrations::Asana', Integrations::Asana] }
describe '#serialize' do
context 'SQL SELECT' do
let(:expected_sql) do
<<~SQL.strip
SELECT "services".* FROM "services" WHERE "services"."type" = 'AsanaService'
SQL
end
it 'forms SQL SELECT statements correctly' do
sql_statements = types.map do |type|
Service.where(type: type).to_sql
end
expect(sql_statements).to all(eq(expected_sql))
end
end
context 'SQL CREATE' do
let(:expected_sql) do
<<~SQL.strip
INSERT INTO "services" ("type") VALUES ('AsanaService')
SQL
end
it 'forms SQL CREATE statements correctly' do
sql_statements = types.map do |type|
record = ActiveRecord::QueryRecorder.new { Service.insert({ type: type }) }
record.log.first
end
expect(sql_statements).to all(include(expected_sql))
end
end
context 'SQL UPDATE' do
let(:expected_sql) do
<<~SQL.strip
UPDATE "services" SET "type" = 'AsanaService'
SQL
end
let_it_be(:service) { create(:service) }
it 'forms SQL UPDATE statements correctly' do
sql_statements = types.map do |type|
record = ActiveRecord::QueryRecorder.new { service.update_column(:type, type) }
record.log.first
end
expect(sql_statements).to all(include(expected_sql))
end
end
context 'SQL DELETE' do
let(:expected_sql) do
<<~SQL.strip
DELETE FROM "services" WHERE "services"."type" = 'AsanaService'
SQL
end
let(:service) { create(:service) }
it 'forms SQL DELETE statements correctly' do
sql_statements = types.map do |type|
record = ActiveRecord::QueryRecorder.new { Service.delete_by(type: type) }
record.log.first
end
expect(sql_statements).to all(match(expected_sql))
end
end
end
describe '#deserialize' do
specify 'it deserializes type correctly', :aggregate_failures do
types.each do |type|
service = create(:service, type: type)
expect(service.type).to eq('AsanaService')
end
end
end
describe '#cast' do
it 'casts type as model correctly', :aggregate_failures do
create(:service, type: 'AsanaService')
types.each do |type|
expect(Service.find_by(type: type)).to be_kind_of(Integrations::Asana)
end
end
end
describe '#changed?' do
it 'detects changes correctly', :aggregate_failures do
service = create(:service, type: 'AsanaService')
types.each do |type|
service.type = type
expect(service).not_to be_changed
end
service.type = 'NewType'
expect(service).to be_changed
end
end
end
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe AsanaService do
RSpec.describe Integrations::Asana do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
......@@ -54,7 +54,7 @@ RSpec.describe AsanaService do
d1 = double('Asana::Resources::Task')
expect(d1).to receive(:add_comment).with(text: expected_message)
expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, gid).once.and_return(d1)
expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, gid).once.and_return(d1)
@asana.execute(data)
end
......@@ -64,7 +64,7 @@ RSpec.describe AsanaService do
d1 = double('Asana::Resources::Task')
expect(d1).to receive(:add_comment)
expect(d1).to receive(:update).with(completed: true)
expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1)
expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1)
@asana.execute(data)
end
......@@ -74,7 +74,7 @@ RSpec.describe AsanaService do
d1 = double('Asana::Resources::Task')
expect(d1).to receive(:add_comment)
expect(d1).to receive(:update).with(completed: true)
expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1)
expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1)
@asana.execute(data)
end
......@@ -88,25 +88,25 @@ RSpec.describe AsanaService do
d1 = double('Asana::Resources::Task')
expect(d1).to receive(:add_comment)
expect(d1).to receive(:update).with(completed: true)
expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1)
expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1)
d2 = double('Asana::Resources::Task')
expect(d2).to receive(:add_comment)
expect(d2).to receive(:update).with(completed: true)
expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2)
expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2)
d3 = double('Asana::Resources::Task')
expect(d3).to receive(:add_comment)
expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3)
expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3)
d4 = double('Asana::Resources::Task')
expect(d4).to receive(:add_comment)
expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4)
expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4)
d5 = double('Asana::Resources::Task')
expect(d5).to receive(:add_comment)
expect(d5).to receive(:update).with(completed: true)
expect(Asana::Resources::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5)
expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5)
@asana.execute(data)
end
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe AssemblaService do
RSpec.describe Integrations::Assembla do
include StubRequests
describe "Associations" do
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe BambooService, :use_clean_rails_memory_store_caching do
RSpec.describe Integrations::Bamboo, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
include StubRequests
......
......@@ -248,7 +248,7 @@ RSpec.describe Service do
describe '.find_or_initialize_all_non_project_specific' do
shared_examples 'service instances' do
it 'returns the available service instances' do
expect(Service.find_or_initialize_all_non_project_specific(Service.for_instance).pluck(:type)).to match_array(Service.available_services_types(include_project_specific: false))
expect(Service.find_or_initialize_all_non_project_specific(Service.for_instance).map(&:to_param)).to match_array(Service.available_services_names(include_project_specific: false))
end
it 'does not create service instances' do
......@@ -666,9 +666,22 @@ RSpec.describe Service do
end
end
describe '.service_name_to_model' do
it 'returns the model for the given service name', :aggregate_failures do
expect(described_class.service_name_to_model('asana')).to eq(Integrations::Asana)
# TODO We can remove this test when all models have been namespaced:
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60968#note_570994955
expect(described_class.service_name_to_model('youtrack')).to eq(YoutrackService)
end
it 'raises an error if service name is invalid' do
expect { described_class.service_name_to_model('foo') }.to raise_exception(NameError, /uninitialized constant FooService/)
end
end
describe "{property}_changed?" do
let(:service) do
BambooService.create(
Integrations::Bamboo.create(
project: project,
properties: {
bamboo_url: 'http://gitlab.com',
......@@ -708,7 +721,7 @@ RSpec.describe Service do
describe "{property}_touched?" do
let(:service) do
BambooService.create(
Integrations::Bamboo.create(
project: project,
properties: {
bamboo_url: 'http://gitlab.com',
......@@ -748,7 +761,7 @@ RSpec.describe Service do
describe "{property}_was" do
let(:service) do
BambooService.create(
Integrations::Bamboo.create(
project: project,
properties: {
bamboo_url: 'http://gitlab.com',
......
......@@ -29,7 +29,7 @@ RSpec.describe Admin::PropagateServiceTemplate do
context 'with a project that has another service' do
before do
BambooService.create!(
Integrations::Bamboo.create!(
active: true,
project: project,
properties: {
......
......@@ -6,7 +6,7 @@ Service.available_services_names.each do |service|
let(:dashed_service) { service.dasherize }
let(:service_method) { "#{service}_service".to_sym }
let(:service_klass) { "#{service}_service".classify.constantize }
let(:service_klass) { Service.service_name_to_model(service) }
let(:service_instance) { service_klass.new }
let(:service_fields) { service_instance.fields }
let(:service_attrs_list) { service_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } }
......
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