Commit 78eff43f authored by Tetiana Chupryna's avatar Tetiana Chupryna

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

Feature(integrate zentao): part of models

See merge request gitlab-org/gitlab!67941
parents 509e71a5 372a33ab
......@@ -46,6 +46,7 @@ module Integrations
has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::IssueTrackerData'
has_one :jira_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::JiraTrackerData'
has_one :open_project_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::OpenProjectTrackerData'
has_one :zentao_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::ZentaoTrackerData'
def data_fields
raise NotImplementedError
......
# frozen_string_literal: true
module Integrations
class Zentao < Integration
data_field :url, :api_url, :api_token, :zentao_product_xid
validates :url, public_url: true, presence: true, if: :activated?
validates :api_url, public_url: true, allow_blank: true
validates :api_token, presence: true, if: :activated?
validates :zentao_product_xid, presence: true, if: :activated?
def data_fields
zentao_tracker_data || self.build_zentao_tracker_data
end
def title
self.class.name.demodulize
end
def description
s_("ZentaoIntegration|Use Zentao as this project's issue tracker.")
end
def self.to_param
name.demodulize.downcase
end
def test(*_args)
client.ping
end
def self.supported_events
%w()
end
def self.supported_event_actions
%w()
end
def fields
[
{
type: 'text',
name: 'url',
title: s_('ZentaoIntegration|Zentao Web URL'),
placeholder: 'https://www.zentao.net',
help: s_('ZentaoIntegration|Base URL of the Zentao instance.'),
required: true
},
{
type: 'text',
name: 'api_url',
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'),
non_empty_password_title: s_('ZentaoIntegration|Enter API token'),
required: true
},
{
type: 'text',
name: 'zentao_product_xid',
title: s_('ZentaoIntegration|Zentao Product ID'),
required: true
}
]
end
private
def client
@client ||= ::Gitlab::Zentao::Client.new(self)
end
end
end
# frozen_string_literal: true
module Integrations
class ZentaoTrackerData < ApplicationRecord
belongs_to :integration, inverse_of: :zentao_tracker_data, foreign_key: :integration_id
delegate :activated?, to: :integration
validates :integration, presence: true
scope :encryption_options, -> do
{
key: Settings.attr_encrypted_db_key_base_32,
encode: true,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm'
}
end
attr_encrypted :url, encryption_options
attr_encrypted :api_url, encryption_options
attr_encrypted :zentao_product_xid, encryption_options
attr_encrypted :api_token, encryption_options
end
end
......@@ -209,6 +209,7 @@ class Project < ApplicationRecord
has_one :unify_circuit_integration, class_name: 'Integrations::UnifyCircuit'
has_one :webex_teams_integration, class_name: 'Integrations::WebexTeams'
has_one :youtrack_integration, class_name: 'Integrations::Youtrack'
has_one :zentao_integration, class_name: 'Integrations::Zentao'
has_one :root_of_fork_network,
foreign_key: 'root_project_id',
......@@ -1455,7 +1456,7 @@ class Project < ApplicationRecord
end
def disabled_integrations
[]
[:zentao]
end
def find_or_initialize_integration(name)
......
......@@ -387,6 +387,10 @@ 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
......
......@@ -137,6 +137,7 @@ class License < ApplicationRecord
oncall_schedules
escalation_policies
export_user_permissions
zentao_issues_integration
]
EEP_FEATURES.freeze
......
......@@ -7,7 +7,7 @@ module Gitlab
Asana Assembla Bamboo Bugzilla Buildkite Campfire Confluence CustomIssueTracker Datadog
Discord DroneCi EmailsOnPush Ewm ExternalWiki Flowdock HangoutsChat Irker Jenkins Jira Mattermost
MattermostSlashCommands MicrosoftTeams MockCi MockMonitoring Packagist PipelinesEmail Pivotaltracker
Prometheus Pushover Redmine Slack SlackSlashCommands Teamcity UnifyCircuit WebexTeams Youtrack
Prometheus Pushover Redmine Slack SlackSlashCommands Teamcity UnifyCircuit WebexTeams Youtrack Zentao
)).freeze
def self.namespaced_integrations
......
# frozen_string_literal: true
module Gitlab
module Zentao
class Client
Error = Class.new(StandardError)
ConfigError = Class.new(Error)
attr_reader :integration
def initialize(integration)
raise ConfigError, 'Please check your integration configuration.' unless integration
@integration = integration
end
def ping
response = fetch_product(zentao_product_xid)
active = response.fetch('deleted') == '0' rescue false
if active
{ success: true }
else
{ success: false, message: 'Not Found' }
end
end
def fetch_product(product_id)
get("products/#{product_id}")
end
def fetch_issues(params = {})
get("products/#{zentao_product_xid}/issues",
params.reverse_merge(page: 1, limit: 20))
end
def fetch_issue(issue_id)
get("issues/#{issue_id}")
end
private
def get(path, params = {})
options = { headers: headers, query: params }
response = Gitlab::HTTP.get(url(path), options)
return {} unless response.success?
Gitlab::Json.parse(response.body)
rescue JSON::ParserError
{}
end
def url(path)
host = integration.api_url.presence || integration.url
URI.join(host, '/api.php/v1/', path)
end
def headers
{
'Content-Type': 'application/json',
'Token': integration.api_token
}
end
def zentao_product_xid
integration.zentao_product_xid
end
end
end
end
......@@ -38925,6 +38925,30 @@ msgstr ""
msgid "Your username is %{username}."
msgstr ""
msgid "ZentaoIntegration|Base URL of the Zentao instance."
msgstr ""
msgid "ZentaoIntegration|Enter API token"
msgstr ""
msgid "ZentaoIntegration|If different from Web URL."
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 Product ID"
msgstr ""
msgid "ZentaoIntegration|Zentao Web URL"
msgstr ""
msgid "Zoom meeting added"
msgstr ""
......
......@@ -7,13 +7,21 @@ FactoryBot.define do
integration factory: :jira_integration
end
factory :zentao_tracker_data, class: 'Integrations::ZentaoTrackerData' do
integration factory: :zentao_integration
url { 'https://jihudemo.zentao.net' }
api_url { '' }
api_token { 'ZENTAO_TOKEN' }
zentao_product_xid { '3' }
end
factory :issue_tracker_data, class: 'Integrations::IssueTrackerData' do
integration
end
factory :open_project_tracker_data, class: 'Integrations::OpenProjectTrackerData' do
integration factory: :open_project_service
url { 'http://openproject.example.com'}
url { 'http://openproject.example.com' }
token { 'supersecret' }
project_identifier_code { 'PRJ-1' }
closed_status_id { '15' }
......
......@@ -85,6 +85,32 @@ FactoryBot.define do
end
end
factory :zentao_integration, class: 'Integrations::Zentao' do
project
active { true }
type { 'ZentaoService' }
transient do
create_data { true }
url { 'https://jihudemo.zentao.net' }
api_url { '' }
api_token { 'ZENTAO_TOKEN' }
zentao_product_xid { '3' }
end
after(:build) do |integration, evaluator|
if evaluator.create_data
integration.zentao_tracker_data = build(:zentao_tracker_data,
integration: integration,
url: evaluator.url,
api_url: evaluator.api_url,
api_token: evaluator.api_token,
zentao_product_xid: evaluator.zentao_product_xid
)
end
end
end
factory :confluence_integration, class: 'Integrations::Confluence' do
project
active { true }
......
......@@ -320,6 +320,7 @@ integrations:
- project
- service_hook
- jira_tracker_data
- zentao_tracker_data
- issue_tracker_data
- open_project_tracker_data
hooks:
......@@ -399,6 +400,7 @@ project:
- teamcity_integration
- pushover_integration
- jira_integration
- zentao_integration
- redmine_integration
- youtrack_integration
- custom_issue_tracker_integration
......
# frozen_string_literal: true
require 'spec_helper'
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}") }
describe '#new' do
context 'if integration is nil' do
let(:zentao_integration) { nil }
it 'raises ConfigError' do
expect { integration }.to raise_error(described_class::ConfigError)
end
end
context 'integration is provided' do
it 'is initialized successfully' do
expect { integration }.not_to raise_error
end
end
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 } }
before do
WebMock.stub_request(:get, mock_get_products_url)
.with(mock_headers).to_return(status: 200, body: mock_response.to_json)
end
it 'fetches the product' do
expect(integration.fetch_product(zentao_integration.zentao_product_xid)).to eq mock_response
end
end
context 'with invalid product' do
before do
WebMock.stub_request(:get, mock_get_products_url)
.with(mock_headers).to_return(status: 404, body: {}.to_json)
end
it 'fetches the empty product' do
expect(integration.fetch_product(zentao_integration.zentao_product_xid)).to eq({})
end
end
context 'with invalid response' do
before do
WebMock.stub_request(:get, mock_get_products_url)
.with(mock_headers).to_return(status: 200, body: '[invalid json}')
end
it 'fetches the empty product' do
expect(integration.fetch_product(zentao_integration.zentao_product_xid)).to eq({})
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)
.with(mock_headers).to_return(status: 200, body: { 'deleted' => '0' }.to_json)
end
it 'responds with success' do
expect(integration.ping[:success]).to eq true
end
end
context 'with deleted resource' do
before do
WebMock.stub_request(:get, mock_get_products_url)
.with(mock_headers).to_return(status: 200, body: { 'deleted' => '1' }.to_json)
end
it 'responds with unsuccess' do
expect(integration.ping[:success]).to eq false
end
end
end
end
......@@ -8,6 +8,18 @@ RSpec.describe AddTriggersToIntegrationsTypeNew do
let(:migration) { described_class.new }
let(:integrations) { table(:integrations) }
# This matches Gitlab::Integrations::StiType at the time the trigger was added
let(:namespaced_integrations) do
%w[
Asana Assembla Bamboo Bugzilla Buildkite Campfire Confluence CustomIssueTracker Datadog
Discord DroneCi EmailsOnPush Ewm ExternalWiki Flowdock HangoutsChat Irker Jenkins Jira Mattermost
MattermostSlashCommands MicrosoftTeams MockCi MockMonitoring Packagist PipelinesEmail Pivotaltracker
Prometheus Pushover Redmine Slack SlackSlashCommands Teamcity UnifyCircuit WebexTeams Youtrack
Github GitlabSlackApplication
]
end
describe '#up' do
before do
migrate!
......@@ -15,7 +27,7 @@ RSpec.describe AddTriggersToIntegrationsTypeNew do
describe 'INSERT trigger' do
it 'sets `type_new` to the transformed `type` class name' do
Gitlab::Integrations::StiType.namespaced_integrations.each do |type|
namespaced_integrations.each do |type|
integration = integrations.create!(type: "#{type}Service")
expect(integration.reload).to have_attributes(
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Integrations::Zentao do
let(:url) { 'https://jihudemo.zentao.net' }
let(:api_url) { 'https://jihudemo.zentao.net' }
let(:api_token) { 'ZENTAO_TOKEN' }
let(:zentao_product_xid) { '3' }
let(:zentao_integration) { create(:zentao_integration) }
describe '#create' do
let(:project) { create(:project, :repository) }
let(:params) do
{
project: project,
url: url,
api_url: api_url,
api_token: api_token,
zentao_product_xid: zentao_product_xid
}
end
it 'stores data in data_fields correctly' do
tracker_data = described_class.create!(params).zentao_tracker_data
expect(tracker_data.url).to eq(url)
expect(tracker_data.api_url).to eq(api_url)
expect(tracker_data.api_token).to eq(api_token)
expect(tracker_data.zentao_product_xid).to eq(zentao_product_xid)
end
end
describe '#fields' do
it 'returns custom fields' do
expect(zentao_integration.fields.pluck(:name)).to eq(%w[url api_url api_token zentao_product_xid])
end
end
describe '#test' do
let(:test_response) { { success: true } }
before do
allow_next_instance_of(Gitlab::Zentao::Client) do |client|
allow(client).to receive(:ping).and_return(test_response)
end
end
it 'gets response from Gitlab::Zentao::Client#ping' do
expect(zentao_integration.test).to eq(test_response)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Integrations::ZentaoTrackerData do
describe 'factory available' do
let(:zentao_tracker_data) { create(:zentao_tracker_data) }
it { expect(zentao_tracker_data.valid?).to eq true }
end
describe 'associations' do
it { is_expected.to belong_to(:integration) }
end
describe 'encrypted attributes' do
subject { described_class.encrypted_attributes.keys }
it { is_expected.to contain_exactly(:url, :api_url, :zentao_product_xid, :api_token) }
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