Commit cea9f01e authored by Luke Duncalfe's avatar Luke Duncalfe

Merge branch 'feature/integrate_zentao/controller2' into 'master'

Feature(integrate zentao): part of Controllers

See merge request gitlab-org/gitlab!69602
parents 6be08f08 398fd4f0
......@@ -9,6 +9,7 @@ module Integrations
:add_pusher,
:alert_events,
:api_key,
:api_token,
:api_url,
:bamboo_url,
:branches_to_be_notified,
......@@ -74,7 +75,8 @@ module Integrations
:url,
:user_key,
:username,
:webhook
:webhook,
:zentao_product_xid
].freeze
def integration_params
......
......@@ -132,6 +132,20 @@ module IntegrationsHelper
end
end
def zentao_issue_breadcrumb_link(issue)
link_to issue[:web_url], { target: '_blank', rel: 'noopener noreferrer', class: 'gl-display-flex gl-align-items-center gl-white-space-nowrap' } do
icon = image_tag image_path('logos/zentao.svg'), width: 15, height: 15, class: 'gl-mr-2'
[icon, html_escape(issue[:id])].join.html_safe
end
end
def zentao_issues_show_data
{
issues_show_path: project_integrations_zentao_issue_path(@project, params[:id], format: :json),
issues_list_path: project_integrations_zentao_issues_path(@project)
}
end
extend self
private
......
......@@ -14,7 +14,7 @@ class Integration < ApplicationRecord
asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord
drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack
pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack zentao
].freeze
PROJECT_SPECIFIC_INTEGRATION_NAMES = %w[
......
......@@ -9,16 +9,25 @@ module Integrations
validates :api_token, presence: true, if: :activated?
validates :zentao_product_xid, presence: true, if: :activated?
def self.feature_flag_enabled?(project)
Feature.enabled?(:zentao_issues_integration, project)
end
# License Level: EEP_FEATURES
def self.issues_license_available?(project)
project&.licensed_feature_available?(:zentao_issues_integration)
end
def data_fields
zentao_tracker_data || self.build_zentao_tracker_data
end
def title
self.class.name.demodulize
'ZenTao'
end
def description
s_("ZentaoIntegration|Use Zentao as this project's issue tracker.")
s_("ZentaoIntegration|Use ZenTao as this project's issue tracker.")
end
def self.to_param
......@@ -42,28 +51,28 @@ module Integrations
{
type: 'text',
name: 'url',
title: s_('ZentaoIntegration|Zentao Web URL'),
title: s_('ZentaoIntegration|ZenTao Web URL'),
placeholder: 'https://www.zentao.net',
help: s_('ZentaoIntegration|Base URL of the Zentao instance.'),
help: s_('ZentaoIntegration|Base URL of the ZenTao instance.'),
required: true
},
{
type: 'text',
name: 'api_url',
title: s_('ZentaoIntegration|Zentao API URL (optional)'),
title: s_('ZentaoIntegration|ZenTao API URL (optional)'),
help: s_('ZentaoIntegration|If different from Web URL.')
},
{
type: 'password',
name: 'api_token',
title: s_('ZentaoIntegration|Zentao API token'),
title: s_('ZentaoIntegration|ZenTao API token'),
non_empty_password_title: s_('ZentaoIntegration|Enter API token'),
required: true
},
{
type: 'text',
name: 'zentao_product_xid',
title: s_('ZentaoIntegration|Zentao Product ID'),
title: s_('ZentaoIntegration|ZenTao Product ID'),
required: true
}
]
......
......@@ -1453,7 +1453,10 @@ class Project < ApplicationRecord
end
def disabled_integrations
[:zentao]
disabled_integrations = []
disabled_integrations << :zentao unless ::Integrations::Zentao.feature_flag_enabled?(self)
disabled_integrations
end
def find_or_initialize_integration(name)
......
---
name: zentao_issues_integration
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69602
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338775
milestone: '14.4'
type: development
group: group::integrations
default_enabled: false
---
key_path: counts.projects_zentao_active
name: count_all_projects_zentao_active
description: Count of projects with active Zentao integrations
product_section: dev
product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
status: active
milestone: "14.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338178
time_frame: all
data_source: database
data_category: Operational
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
---
key_path: counts.groups_zentao_active
name: count_all_groups_zentao_active
description: Count of groups with active Zentao integrations
product_section: dev
product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
status: active
milestone: "14.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338178
time_frame: all
data_source: database
data_category: Operational
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
---
key_path: counts.instances_zentao_active
name: count_all_instances_zentao_active
description: Count of instances with active Zentao integrations
product_section: dev
product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
status: active
milestone: "14.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338178
time_frame: all
data_source: database
data_category: Operational
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
---
key_path: counts.projects_inheriting_zentao_active
name: count_all_projects_inheriting_zentao_active
description: Count of projects that inherit active Zentao integrations
product_section: dev
product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
status: active
milestone: "14.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338178
time_frame: all
data_source: database
data_category: Operational
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
---
key_path: counts.groups_inheriting_zentao_active
name: count_all_groups_inheriting_zentao_active
description: Count of groups that inherit active Zentao integrations
product_section: dev
product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
status: active
milestone: "14.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338178
time_frame: all
data_source: database
data_category: Operational
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
......@@ -16578,6 +16578,7 @@ State of a Sentry error.
| <a id="servicetypeunify_circuit_service"></a>`UNIFY_CIRCUIT_SERVICE` | UnifyCircuitService type. |
| <a id="servicetypewebex_teams_service"></a>`WEBEX_TEAMS_SERVICE` | WebexTeamsService type. |
| <a id="servicetypeyoutrack_service"></a>`YOUTRACK_SERVICE` | YoutrackService type. |
| <a id="servicetypezentao_service"></a>`ZENTAO_SERVICE` | ZentaoService type. |
### `SharedRunnersSetting`
......
# frozen_string_literal: true
module Projects
module Integrations
module Zentao
class IssuesController < Projects::ApplicationController
include RecordUserLastActivity
before_action :check_feature_enabled!
rescue_from ::Gitlab::Zentao::Client::Error, with: :render_error
feature_category :integrations
def index
respond_to do |format|
format.html
format.json do
render json: issues_json
end
end
end
def show
@issue_json = issue_json
respond_to do |format|
format.html
format.json do
render json: @issue_json
end
end
end
private
def query_params
params.permit(:id, :page, :limit, :search, :sort, :state, :labels)
end
def query
::Gitlab::Zentao::Query.new(project.zentao_integration, query_params)
end
def issue_json
::Integrations::ZentaoSerializers::IssueDetailSerializer.new
.represent(query.issue, project: project)
end
def issues_json
::Integrations::ZentaoSerializers::IssueSerializer.new
.with_pagination(request, response)
.represent(query.issues, project: project)
end
def check_feature_enabled!
return render_404 unless ::Integrations::Zentao.feature_flag_enabled?(project)
return render_404 unless ::Integrations::Zentao.issues_license_available?(project) && project.zentao_integration&.active?
end
def render_error(exception)
log_exception(exception)
render json: { errors: [s_('ZentaoIntegration|An error occurred while requesting data from the ZenTao service.')] },
status: :bad_request
end
end
end
end
end
......@@ -404,10 +404,6 @@ module EE
feature_available?(:jira_issues_integration)
end
def zentao_issues_integration_available?
feature_available?(:zentao_issues_integration)
end
def multiple_approval_rules_available?
feature_available?(:multiple_approval_rules)
end
......
# frozen_string_literal: true
module Integrations
module ZentaoSerializers
class IssueDetailEntity < IssueEntity
expose :description_html do |item|
sanitize(item['desc'])
end
expose :comments do |item|
item['comments'].map do |comment|
{
id: comment['id']&.to_i,
created_at: comment['date']&.to_datetime&.utc,
body_html: body_html(comment),
author: user_info(comment['actor'])
}
end
end
private
def body_html(comment)
content = [comment['title'], comment['body_html']].join('<br>')
sanitize(content)
end
end
end
end
# frozen_string_literal: true
module Integrations
module ZentaoSerializers
class IssueDetailSerializer < BaseSerializer
entity ::Integrations::ZentaoSerializers::IssueDetailEntity
end
end
end
# frozen_string_literal: true
module Integrations
module ZentaoSerializers
class IssueEntity < Grape::Entity
include ActionView::Helpers::SanitizeHelper
include RequestAwareEntity
expose :id do |item|
sanitize(item['id'])
end
expose :project_id do |item|
project.id
end
expose :title do |item|
sanitize(item['title'])
end
expose :created_at do |item|
item['openedDate']&.to_datetime&.utc
end
expose :updated_at do |item|
item['lastEditedDate']&.to_datetime&.utc
end
expose :closed_at do |item|
item['lastEditedDate']&.to_datetime&.utc if item['status'] == 'closed'
end
expose :status do |item|
sanitize(item['status'])
end
expose :state do |item|
sanitize(item['status'])
end
expose :labels do |item|
item['labels'].compact.map do |label|
name = sanitize(label)
{
id: name,
title: name,
name: name,
color: '#0052CC',
text_color: '#FFFFFF'
}
end
end
expose :author do |item|
user_info(item['openedBy'])
end
expose :assignees do |item|
item['assignedTo'].compact.map do |user|
user_info(user)
end
end
expose :web_url do |item|
item['url']
end
expose :gitlab_web_url do |item|
project_integrations_zentao_issue_path(project, item['id'])
end
private
def project
@project ||= options[:project]
end
def user_info(user)
return {} unless user.present?
{
"name": sanitize(user['realname'].presence || user['account']),
"web_url": user['url'],
"avatar_url": user['avatar']
}
end
end
end
end
# frozen_string_literal: true
module Integrations
module ZentaoSerializers
class IssueSerializer < BaseSerializer
include WithPagination
entity ::Integrations::ZentaoSerializers::IssueEntity
end
end
end
- page_title _('ZentaoIntegration|Zentao issues')
- add_page_specific_style 'page_bundles/issues_list'
.js-zentao-issues-list{ data: { issues_fetch_path: project_integrations_zentao_issues_path(@project, format: :json),
page: params[:page],
initial_state: params[:state],
initial_sort_by: params[:sort],
project_full_path: @project.full_path,
issue_create_url: @project.zentao_integration.url,
empty_state_path: image_path('illustrations/issues.svg') } }
- add_to_breadcrumbs _('Zentao issues'), project_integrations_zentao_issues_path(@project)
- breadcrumb_title zentao_issue_breadcrumb_link(@issue_json)
- page_title html_escape(@issue_json[:title])
.js-zentao-issues-show-app{ data: zentao_issues_show_data }
......@@ -119,6 +119,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
end
namespace :zentao do
resources :issues, only: [:index, :show]
end
end
# Added for backward compatibility with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39543
......
# frozen_string_literal: true
module EE
module Sidebars
module Projects
module Menus
module ZentaoMenu
extend ::Gitlab::Utils::Override
override :link
def link
return super unless feature_available?
project_integrations_zentao_issues_path(context.project)
end
override :add_items
def add_items
add_item(issue_list_menu_item) if feature_available?
super
end
private
def feature_available?
::Integrations::Zentao.issues_license_available?(context.project)
end
def issue_list_menu_item
::Sidebars::MenuItem.new(
title: s_('ZentaoIntegration|Issue list'),
link: project_integrations_zentao_issues_path(context.project),
active_routes: { controller: 'projects/integrations/zentao/issues' },
item_id: :issue_list
)
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::Integrations::Zentao::IssuesController do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, developer_projects: [project]) }
let_it_be(:zentao_integration) { create(:zentao_integration, project: project) }
before do
stub_licensed_features(zentao_issues_integration: true)
sign_in(user)
end
describe 'GET #index' do
context 'when zentao_issues_integration licensed feature is not available' do
before do
stub_licensed_features(zentao_issues_integration: false)
end
it 'returns 404 status' do
get :index, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(:not_found)
end
end
it_behaves_like 'unauthorized when external service denies access' do
subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
end
it 'renders the "index" template' do
get :index, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
context 'json request' do
let(:zentao_issue) { [] }
it 'returns a list of serialized zentao issues' do
expect_next_instance_of(::Gitlab::Zentao::Query) do |query|
expect(query).to receive(:issues).and_return(zentao_issue)
end
expect_next_instance_of(Integrations::ZentaoSerializers::IssueSerializer) do |serializer|
expect(serializer).to receive(:represent).with(zentao_issue, project: project)
end
get :index, params: { namespace_id: project.namespace, project_id: project }, format: :json
end
it 'renders bad request for Error' do
expect_next_instance_of(::Gitlab::Zentao::Query) do |query|
expect(query).to receive(:issues).and_raise(::Gitlab::Zentao::Client::Error)
end
expect(Gitlab::ErrorTracking).to receive(:track_exception)
get :index, params: { namespace_id: project.namespace, project_id: project }, format: :json
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['errors']).to match_array [s_('ZentaoIntegration|An error occurred while requesting data from the ZenTao service.')]
end
end
end
describe 'GET #show' do
context 'when zentao_issues_integration licensed feature is not available' do
before do
stub_licensed_features(zentao_issues_integration: false)
end
it 'returns 404 status' do
get :show, params: { namespace_id: project.namespace, project_id: project, id: 1 }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when zentao_issues_integration licensed feature is available' do
let(:zentao_issue) { { 'from' => 'zentao' } }
let(:issue_json) { { 'from' => 'backend' } }
before do
stub_licensed_features(zentao_issues_integration: true)
expect_next_instance_of(::Gitlab::Zentao::Query) do |query|
allow(query).to receive(:issue).and_return(zentao_issue)
end
allow_next_instance_of(Integrations::ZentaoSerializers::IssueDetailSerializer) do |serializer|
allow(serializer).to receive(:represent).with(zentao_issue, project: project).and_return(issue_json)
end
end
it 'renders `show` template' do
get :show, params: { namespace_id: project.namespace, project_id: project, id: 'story-1' }
expect(assigns(:issue_json)).to eq(issue_json)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:show)
end
it 'returns JSON response' do
get :show, params: { namespace_id: project.namespace, project_id: project, id: 'story-1', format: :json }
expect(json_response).to eq(issue_json)
end
context 'when the JSON fetched from ZenTao contains HTML' do
let(:payload) { "<script>alert('XSS')</script>" }
let(:issue_json) { { id: payload, title: payload, status: payload, labels: [payload] } }
render_views
it 'escapes the HTML in issue' do
get :show, params: { namespace_id: project.namespace, project_id: project, id: 'story-1' }
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).not_to include(payload)
expect(response.body).to include(html_escape(payload))
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::ZentaoMenu do
let(:project) { create(:project, has_external_issue_tracker: true) }
let(:user) { project.owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
let(:zentao_integration) { create(:zentao_integration, project: project) }
subject { described_class.new(context) }
describe 'when feature is not licensed' do
before do
stub_licensed_features(zentao_issues_integration: false)
end
it_behaves_like 'ZenTao menu with CE version'
end
describe 'when feature is licensed' do
before do
stub_licensed_features(zentao_issues_integration: true)
end
context 'when issues integration is disabled' do
before do
zentao_integration.update!(active: false)
end
it 'returns false' do
expect(subject.render?).to eq false
end
end
context 'when issues integration is enabled' do
before do
zentao_integration.update!(active: true)
end
it 'returns true' do
expect(subject.render?).to eq true
end
it 'renders menu link' do
expect(subject.link).to include('/-/integrations/zentao/issues')
end
it 'contains issue list and open ZenTao menu items' do
expect(subject.renderable_items.map(&:item_id)).to match_array [:issue_list, :open_zentao]
end
end
end
end
......@@ -254,7 +254,7 @@ module API
type: Boolean,
desc: 'DEPRECATED: This parameter has no effect since SSL verification will always be enabled'
}
],
],
'campfire' => [
{
required: true,
......@@ -768,7 +768,33 @@ module API
desc: 'The Webex Teams webhook. For example, https://api.ciscospark.com/v1/webhooks/incoming/...'
},
chat_notification_events
].flatten
].flatten,
'zentao' => [
{
required: true,
name: :url,
type: String,
desc: 'The base URL to the ZenTao instance web interface which is being linked to this GitLab project. For example, https://www.zentao.net'
},
{
required: false,
name: :api_url,
type: String,
desc: 'The base URL to the ZenTao instance API. Web URL value will be used if not set. For example, https://www.zentao.net'
},
{
required: true,
name: :api_token,
type: String,
desc: 'The API token created from ZenTao dashboard'
},
{
required: true,
name: :zentao_product_xid,
type: String,
desc: 'The product ID of ZenTao project'
}
]
}
end
......@@ -805,7 +831,8 @@ module API
::Integrations::Slack,
::Integrations::SlackSlashCommands,
::Integrations::Teamcity,
::Integrations::Youtrack
::Integrations::Youtrack,
::Integrations::Zentao
]
end
......
......@@ -15,10 +15,8 @@ module Gitlab
end
def ping
response = fetch_product(zentao_product_xid)
response = fetch_product(zentao_product_xid) rescue {}
active = response.fetch('deleted') == '0' rescue false
if active
{ success: true }
else
......@@ -31,25 +29,30 @@ module Gitlab
end
def fetch_issues(params = {})
get("products/#{zentao_product_xid}/issues",
params.reverse_merge(page: 1, limit: 20))
get("products/#{zentao_product_xid}/issues", params)
end
def fetch_issue(issue_id)
raise Gitlab::Zentao::Client::Error unless issue_id_pattern.match(issue_id)
get("issues/#{issue_id}")
end
private
def issue_id_pattern
/\A\S+-\d+\z/
end
def get(path, params = {})
options = { headers: headers, query: params }
response = Gitlab::HTTP.get(url(path), options)
return {} unless response.success?
raise Gitlab::Zentao::Client::Error unless response.success?
Gitlab::Json.parse(response.body)
rescue JSON::ParserError
{}
raise Gitlab::Zentao::Client::Error
end
def url(path)
......
# frozen_string_literal: true
module Gitlab
module Zentao
class Query
STATUSES = %w[all opened closed].freeze
ISSUES_DEFAULT_LIMIT = 20
ISSUES_MAX_LIMIT = 50
attr_reader :client, :params
def initialize(integration, params)
@client = Client.new(integration)
@params = params
end
def issues
issues_response = client.fetch_issues(query_options)
return [] if issues_response.blank?
Kaminari.paginate_array(
issues_response['issues'],
limit: issues_response['limit'],
total_count: issues_response['total']
)
end
def issue
issue_response = client.fetch_issue(params[:id])
issue_response['issue']
end
private
def query_options
{
order: query_order,
status: query_status,
labels: query_labels,
page: query_page,
limit: query_limit,
search: query_search
}
end
def query_page
params[:page].presence || 1
end
def query_limit
limit = params[:limit].presence || ISSUES_DEFAULT_LIMIT
[limit.to_i, ISSUES_MAX_LIMIT].min
end
def query_search
params[:search] || ''
end
def query_order
key, order = params['sort'].to_s.split('_', 2)
zentao_key = (key == 'created' ? 'openedDate' : 'lastEditedDate')
zentao_order = (order == 'asc' ? 'asc' : 'desc')
"#{zentao_key}_#{zentao_order}"
end
def query_status
return params[:state] if params[:state].present? && params[:state].in?(STATUSES)
'opened'
end
def query_labels
(params[:labels].presence || []).join(',')
end
end
end
end
# frozen_string_literal: true
module Sidebars
module Projects
module Menus
class ZentaoMenu < ::Sidebars::Menu
override :configure_menu_items
def configure_menu_items
render?.tap do |render|
break unless render
add_items
end
end
override :link
def link
zentao_integration.url
end
override :title
def title
s_('ZentaoIntegration|ZenTao issues')
end
override :title_html_options
def title_html_options
{
id: 'js-onboarding-settings-link'
}
end
override :image_path
def image_path
'logos/zentao.svg'
end
# Hardcode sizes so image doesn't flash before CSS loads https://gitlab.com/gitlab-org/gitlab/-/issues/321022
override :image_html_options
def image_html_options
{
size: 16
}
end
override :render?
def render?
return false if zentao_integration.blank?
zentao_integration.active?
end
def add_items
add_item(open_zentao_menu_item)
end
private
def zentao_integration
@zentao_integration ||= context.project.zentao_integration
end
def open_zentao_menu_item
::Sidebars::MenuItem.new(
title: s_('ZentaoIntegration|Open ZenTao'),
link: zentao_integration.url,
active_routes: {},
item_id: :open_zentao,
sprite_icon: 'external-link',
container_html_options: {
target: '_blank',
rel: 'noopener noreferrer'
}
)
end
end
end
end
end
::Sidebars::Projects::Menus::ZentaoMenu.prepend_mod
......@@ -23,6 +23,7 @@ module Sidebars
add_menu(Sidebars::Projects::Menus::RepositoryMenu.new(context))
add_menu(Sidebars::Projects::Menus::IssuesMenu.new(context))
add_menu(Sidebars::Projects::Menus::ExternalIssueTrackerMenu.new(context))
add_menu(Sidebars::Projects::Menus::ZentaoMenu.new(context)) if ::Integrations::Zentao.feature_flag_enabled?(context.project)
add_menu(Sidebars::Projects::Menus::MergeRequestsMenu.new(context))
add_menu(Sidebars::Projects::Menus::CiCdMenu.new(context))
add_menu(Sidebars::Projects::Menus::SecurityComplianceMenu.new(context))
......
......@@ -39678,7 +39678,13 @@ msgstr ""
msgid "ZenTaoIntegration|ZenTao user"
msgstr ""
msgid "ZentaoIntegration|Base URL of the Zentao instance."
msgid "Zentao issues"
msgstr ""
msgid "ZentaoIntegration|An error occurred while requesting data from the ZenTao service."
msgstr ""
msgid "ZentaoIntegration|Base URL of the ZenTao instance."
msgstr ""
msgid "ZentaoIntegration|Enter API token"
......@@ -39687,19 +39693,31 @@ msgstr ""
msgid "ZentaoIntegration|If different from Web URL."
msgstr ""
msgid "ZentaoIntegration|Use Zentao as this project's issue tracker."
msgid "ZentaoIntegration|Issue list"
msgstr ""
msgid "ZentaoIntegration|Open ZenTao"
msgstr ""
msgid "ZentaoIntegration|Use ZenTao as this project's issue tracker."
msgstr ""
msgid "ZentaoIntegration|ZenTao API URL (optional)"
msgstr ""
msgid "ZentaoIntegration|ZenTao API token"
msgstr ""
msgid "ZentaoIntegration|Zentao API URL (optional)"
msgid "ZentaoIntegration|ZenTao Product ID"
msgstr ""
msgid "ZentaoIntegration|Zentao API token"
msgid "ZentaoIntegration|ZenTao Web URL"
msgstr ""
msgid "ZentaoIntegration|Zentao Product ID"
msgid "ZentaoIntegration|ZenTao issues"
msgstr ""
msgid "ZentaoIntegration|Zentao Web URL"
msgid "ZentaoIntegration|Zentao issues"
msgstr ""
msgid "Zoom meeting added"
......
......@@ -6,7 +6,23 @@ RSpec.describe Gitlab::Zentao::Client do
subject(:integration) { described_class.new(zentao_integration) }
let(:zentao_integration) { create(:zentao_integration) }
let(:mock_get_products_url) { integration.send(:url, "products/#{zentao_integration.zentao_product_xid}") }
def mock_get_products_url
integration.send(:url, "products/#{zentao_integration.zentao_product_xid}")
end
def mock_fetch_issue_url(issue_id)
integration.send(:url, "issues/#{issue_id}")
end
let(:mock_headers) do
{
headers: {
'Content-Type' => 'application/json',
'Token' => zentao_integration.api_token
}
}
end
describe '#new' do
context 'if integration is nil' do
......@@ -25,15 +41,6 @@ RSpec.describe Gitlab::Zentao::Client do
end
describe '#fetch_product' do
let(:mock_headers) do
{
headers: {
'Content-Type' => 'application/json',
'Token' => zentao_integration.api_token
}
}
end
context 'with valid product' do
let(:mock_response) { { 'id' => zentao_integration.zentao_product_xid } }
......@@ -54,7 +61,9 @@ RSpec.describe Gitlab::Zentao::Client do
end
it 'fetches the empty product' do
expect(integration.fetch_product(zentao_integration.zentao_product_xid)).to eq({})
expect do
integration.fetch_product(zentao_integration.zentao_product_xid)
end.to raise_error(Gitlab::Zentao::Client::Error)
end
end
......@@ -65,21 +74,14 @@ RSpec.describe Gitlab::Zentao::Client do
end
it 'fetches the empty product' do
expect(integration.fetch_product(zentao_integration.zentao_product_xid)).to eq({})
expect do
integration.fetch_product(zentao_integration.zentao_product_xid)
end.to raise_error(Gitlab::Zentao::Client::Error)
end
end
end
describe '#ping' do
let(:mock_headers) do
{
headers: {
'Content-Type' => 'application/json',
'Token' => zentao_integration.api_token
}
}
end
context 'with valid resource' do
before do
WebMock.stub_request(:get, mock_get_products_url)
......@@ -102,4 +104,29 @@ RSpec.describe Gitlab::Zentao::Client do
end
end
end
describe '#fetch_issue' do
context 'with invalid id' do
let(:invalid_ids) { ['story', 'story-', '-', '123', ''] }
it 'returns empty object' do
invalid_ids.each do |id|
expect { integration.fetch_issue(id) }.to raise_error(Gitlab::Zentao::Client::Error)
end
end
end
context 'with valid id' do
let(:valid_ids) { %w[story-1 bug-23] }
it 'fetches current issue' do
valid_ids.each do |id|
WebMock.stub_request(:get, mock_fetch_issue_url(id))
.with(mock_headers).to_return(status: 200, body: { issue: { id: id } }.to_json)
expect(integration.fetch_issue(id).dig('issue', 'id')).to eq id
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Zentao::Query do
let(:zentao_integration) { create(:zentao_integration) }
let(:params) { {} }
subject(:query) { described_class.new(zentao_integration, ActionController::Parameters.new(params)) }
describe '#issues' do
let(:response) { { 'page' => 1, 'total' => 0, 'limit' => 20, 'issues' => [] } }
def expect_query_option_include(expected_params)
expect_next_instance_of(Gitlab::Zentao::Client) do |client|
expect(client).to receive(:fetch_issues)
.with(hash_including(expected_params))
.and_return(response)
end
query.issues
end
context 'when params are empty' do
it 'fills default params' do
expect_query_option_include(status: 'opened', order: 'lastEditedDate_desc', labels: '')
end
end
context 'when params contain valid options' do
let(:params) { { state: 'closed', sort: 'created_asc', labels: %w[Bugs Features] } }
it 'fills params with standard of ZenTao' do
expect_query_option_include(status: 'closed', order: 'openedDate_asc', labels: 'Bugs,Features')
end
end
context 'when params contain invalid options' do
let(:params) { { state: 'xxx', sort: 'xxx', labels: %w[xxx] } }
it 'fills default params with standard of ZenTao' do
expect_query_option_include(status: 'opened', order: 'lastEditedDate_desc', labels: 'xxx')
end
end
end
describe '#issue' do
let(:response) { { 'issue' => { 'id' => 'story-1' } } }
before do
expect_next_instance_of(Gitlab::Zentao::Client) do |client|
expect(client).to receive(:fetch_issue)
.and_return(response)
end
end
it 'returns issue object by client' do
expect(query.issue).to include('id' => 'story-1')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::ZentaoMenu do
it_behaves_like 'ZenTao menu with CE version'
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'ZenTao menu with CE version' do
let(:project) { create(:project, has_external_issue_tracker: true) }
let(:user) { project.owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
let(:zentao_integration) { create(:zentao_integration, project: project) }
subject { described_class.new(context) }
describe '#render?' do
context 'when issues integration is disabled' do
before do
zentao_integration.update!(active: false)
end
it 'returns false' do
expect(subject.render?).to eq false
end
end
context 'when issues integration is enabled' do
before do
zentao_integration.update!(active: true)
end
it 'returns true' do
expect(subject.render?).to eq true
end
it 'renders menu link' do
expect(subject.link).to eq zentao_integration.url
end
it 'contains only open ZenTao item' do
expect(subject.renderable_items.map(&:item_id)).to match_array [:open_zentao]
end
end
end
end
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