Commit cb9cdd16 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-to-ee-2018-08-21' into 'master'

CE upstream - 2018-08-21 21:21 UTC

Closes gitlab-ce#40550

See merge request gitlab-org/gitlab-ee!6962
parents 66066270 abf0d4b3
...@@ -330,7 +330,7 @@ export default { ...@@ -330,7 +330,7 @@ export default {
<pipelines-artifacts-component <pipelines-artifacts-component
v-if="pipeline.details.artifacts.length" v-if="pipeline.details.artifacts.length"
:artifacts="pipeline.details.artifacts" :artifacts="pipeline.details.artifacts"
class="d-none d-sm-none d-md-block" class="d-md-block"
/> />
<loading-button <loading-button
......
...@@ -108,11 +108,15 @@ module IssuableCollections ...@@ -108,11 +108,15 @@ module IssuableCollections
end end
def set_sort_order_from_cookie def set_sort_order_from_cookie
key = 'issuable_sort' cookies[remember_sorting_key] = params[:sort] if params[:sort].present?
# fallback to legacy cookie value for backward compatibility
cookies[remember_sorting_key] ||= cookies['issuable_sort']
cookies[remember_sorting_key] = update_cookie_value(cookies[remember_sorting_key])
params[:sort] = cookies[remember_sorting_key]
end
cookies[key] = params[:sort] if params[:sort].present? def remember_sorting_key
cookies[key] = update_cookie_value(cookies[key]) @remember_sorting_key ||= "#{collection_type.downcase}_sort"
params[:sort] = cookies[key]
end end
def default_sort_order def default_sort_order
...@@ -141,16 +145,14 @@ module IssuableCollections ...@@ -141,16 +145,14 @@ module IssuableCollections
end end
def finder def finder
strong_memoize(:finder) do @finder ||= issuable_finder_for(finder_type)
issuable_finder_for(finder_type)
end
end end
def collection_type def collection_type
@collection_type ||= case finder @collection_type ||= case finder_type.name
when IssuesFinder when 'IssuesFinder'
'Issue' 'Issue'
when MergeRequestsFinder when 'MergeRequestsFinder'
'MergeRequest' 'MergeRequest'
end end
end end
......
---
title: Allow spaces in wiki markdown links when using CommonMark
merge_request: 20417
author:
type: fixed
---
title: Split remembering sorting for issues and merge requests
merge_request: 21153
author: Jacopo Beschi @jacopo-beschi
type: fixed
---
title: Shows download artifacts button for pipelines on small screens
merge_request:
author:
type: changed
---
title: Don't use arguments keyword in gettext script
merge_request: 21296
author: gfyoung
type: fixed
# frozen_string_literal: true
require 'uri'
module Banzai
module Filter
# HTML Filter for markdown links with spaces in the URLs
#
# Based on Banzai::Filter::AutolinkFilter
#
# CommonMark does not allow spaces in the url portion of a link.
# For example, `[example](page slug)` is not valid. However,
# in our wikis, we support (via RedCarpet) this type of link, allowing
# wiki pages to be easily linked by their title. This filter adds that functionality.
# The intent is for this to only be used in Wikis - in general, we want
# to adhere to CommonMark's spec.
#
class SpacedLinkFilter < HTML::Pipeline::Filter
include ActionView::Helpers::TagHelper
# Pattern to match a standard markdown link
#
# Rubular: http://rubular.com/r/z9EAHxYmKI
LINK_PATTERN = /\[([^\]]+)\]\(([^)"]+)(?: \"([^\"]+)\")?\)/
# Text matching LINK_PATTERN inside these elements will not be linked
IGNORE_PARENTS = %w(a code kbd pre script style).to_set
# The XPath query to use for finding text nodes to parse.
TEXT_QUERY = %Q(descendant-or-self::text()[
not(#{IGNORE_PARENTS.map { |p| "ancestor::#{p}" }.join(' or ')})
and contains(., ']\(')
]).freeze
def call
return doc if context[:markdown_engine] == :redcarpet
doc.xpath(TEXT_QUERY).each do |node|
content = node.to_html
next unless content.match(LINK_PATTERN)
html = spaced_link_filter(content)
next if html == content
node.replace(html)
end
doc
end
private
def spaced_link_match(link)
match = LINK_PATTERN.match(link)
return link unless match && match[1] && match[2]
# escape the spaces in the url so that it's a valid markdown link,
# then run it through the markdown processor again, let it do its magic
text = match[1]
new_link = match[2].gsub(' ', '%20')
title = match[3] ? " \"#{match[3]}\"" : ''
html = Banzai::Filter::MarkdownFilter.call("[#{text}](#{new_link}#{title})", context)
# link is wrapped in a <p>, so strip that off
html.sub('<p>', '').chomp('</p>')
end
def spaced_link_filter(text)
Gitlab::StringRegexMarker.new(CGI.unescapeHTML(text), text.html_safe).mark(LINK_PATTERN) do |link, left:, right:|
spaced_link_match(link)
end
end
end
end
end
...@@ -5,6 +5,7 @@ module Banzai ...@@ -5,6 +5,7 @@ module Banzai
@filters ||= begin @filters ||= begin
super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter) super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter)
.insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter) .insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter)
.insert_before(Filter::WikiLinkFilter, Filter::SpacedLinkFilter)
end end
end end
end end
......
...@@ -23,7 +23,13 @@ module QA ...@@ -23,7 +23,13 @@ module QA
# After we fill the key, JS would generate another field so # After we fill the key, JS would generate another field so
# we need to use the same index to find the corresponding one. # we need to use the same index to find the corresponding one.
keys[index].set(key) keys[index].set(key)
all_elements(:ci_variable_input_value)[index].set(value) node = all_elements(:ci_variable_input_value)[index]
# Simply run `node.set(value)` is too slow for long text here,
# so we need to run JavaScript directly to set the value.
# The code was inspired from:
# https://github.com/teamcapybara/capybara/blob/679548cea10773d45e32808f4d964377cfe5e892/lib/capybara/selenium/node.rb#L217
execute_script("arguments[0].value = #{value.to_json}", node)
end end
def save_variables def save_variables
......
...@@ -7,7 +7,7 @@ const { ...@@ -7,7 +7,7 @@ const {
} = require('gettext-extractor-vue'); } = require('gettext-extractor-vue');
const ensureSingleLine = require('../../app/assets/javascripts/locale/ensure_single_line.js'); const ensureSingleLine = require('../../app/assets/javascripts/locale/ensure_single_line.js');
const arguments = argumentsParser const args = argumentsParser
.option('-f, --file <file>', 'Extract message from one single file') .option('-f, --file <file>', 'Extract message from one single file')
.option('-a, --all', 'Extract message from all js/vue files') .option('-a, --all', 'Extract message from all js/vue files')
.parse(process.argv); .parse(process.argv);
...@@ -61,12 +61,12 @@ function printJson() { ...@@ -61,12 +61,12 @@ function printJson() {
console.log(JSON.stringify(messages)); console.log(JSON.stringify(messages));
} }
if (arguments.file) { if (args.file) {
vueParser.parseFile(arguments.file).then(() => printJson()); vueParser.parseFile(args.file).then(() => printJson());
} else if (arguments.all) { } else if (args.all) {
vueParser.parseFilesGlob('{ee/app,app}/assets/javascripts/**/*.{js,vue}').then(() => printJson()); vueParser.parseFilesGlob('{ee/app,app}/assets/javascripts/**/*.{js,vue}').then(() => printJson());
} else { } else {
console.warn('ERROR: Please use the script correctly:'); console.warn('ERROR: Please use the script correctly:');
arguments.outputHelp(); args.outputHelp();
process.exit(1); process.exit(1);
} }
require "spec_helper" require "spec_helper"
describe "User sorts issues" do describe "User sorts issues" do
set(:project) { create(:project_empty_repo, :public) } set(:user) { create(:user) }
set(:group) { create(:group) }
set(:project) { create(:project_empty_repo, :public, group: group) }
set(:issue1) { create(:issue, project: project) } set(:issue1) { create(:issue, project: project) }
set(:issue2) { create(:issue, project: project) } set(:issue2) { create(:issue, project: project) }
set(:issue3) { create(:issue, project: project) } set(:issue3) { create(:issue, project: project) }
...@@ -12,7 +14,29 @@ describe "User sorts issues" do ...@@ -12,7 +14,29 @@ describe "User sorts issues" do
create(:award_emoji, :downvote, awardable: issue1) create(:award_emoji, :downvote, awardable: issue1)
create(:award_emoji, :upvote, awardable: issue2) create(:award_emoji, :upvote, awardable: issue2)
sign_in(user)
visit(project_issues_path(project))
end
it 'keeps the sort option' do
find('button.dropdown-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link('Milestone')
end
visit(issues_dashboard_path(assignee_id: user.id))
expect(find('.issues-filters a.is-active')).to have_content('Milestone')
visit(project_issues_path(project)) visit(project_issues_path(project))
expect(find('.issues-filters a.is-active')).to have_content('Milestone')
visit(issues_group_path(group))
expect(find('.issues-filters a.is-active')).to have_content('Milestone')
end end
it "sorts by popularity" do it "sorts by popularity" do
......
require 'spec_helper' require 'spec_helper'
describe 'User sorts merge requests' do describe 'User sorts merge requests' do
include CookieHelper
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let!(:merge_request2) do let!(:merge_request2) do
create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test') create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
end end
let(:project) { create(:project, :public, :repository) } set(:user) { create(:user) }
let(:user) { create(:user) } set(:group) { create(:group) }
set(:group_member) { create(:group_member, :maintainer, user: user, group: group) }
set(:project) { create(:project, :public, group: group) }
before do before do
project.add_maintainer(user)
sign_in(user) sign_in(user)
visit(project_merge_requests_path(project)) visit(project_merge_requests_path(project))
...@@ -19,16 +22,42 @@ describe 'User sorts merge requests' do ...@@ -19,16 +22,42 @@ describe 'User sorts merge requests' do
find('button.dropdown-toggle').click find('button.dropdown-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-right li') do page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link('Last updated') click_link('Milestone')
end end
visit(merge_requests_dashboard_path(assignee_id: user.id)) visit(merge_requests_dashboard_path(assignee_id: user.id))
expect(find('.issues-filters')).to have_content('Last updated') expect(find('.issues-filters a.is-active')).to have_content('Milestone')
visit(project_merge_requests_path(project)) visit(project_merge_requests_path(project))
expect(find('.issues-filters')).to have_content('Last updated') expect(find('.issues-filters a.is-active')).to have_content('Milestone')
visit(merge_requests_group_path(group))
expect(find('.issues-filters a.is-active')).to have_content('Milestone')
end
it 'fallbacks to issuable_sort cookie key when remembering the sorting option' do
set_cookie('issuable_sort', 'milestone')
visit(merge_requests_dashboard_path(assignee_id: user.id))
expect(find('.issues-filters a.is-active')).to have_content('Milestone')
end
it 'separates remember sorting with issues' do
create(:issue, project: project)
find('button.dropdown-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link('Milestone')
end
visit(project_issues_path(project))
expect(find('.issues-filters a.is-active')).not_to have_content('Milestone')
end end
context 'when merge requests have awards' do context 'when merge requests have awards' do
......
import Vue from 'vue'; import Vue from 'vue';
import { getTimeago } from '~/lib/utils/datetime_utility'; import { getTimeago } from '~/lib/utils/datetime_utility';
import component from '~/jobs/components/artifacts_block.vue'; import component from '~/jobs/components/artifacts_block.vue';
import mountComponent from '../helpers/vue_mount_component_helper'; import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Artifacts block', () => { describe('Artifacts block', () => {
const Component = Vue.extend(component); const Component = Vue.extend(component);
......
import Vue from 'vue'; import Vue from 'vue';
import component from '~/jobs/components/commit_block.vue'; import component from '~/jobs/components/commit_block.vue';
import mountComponent from '../helpers/vue_mount_component_helper'; import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Commit block', () => { describe('Commit block', () => {
const Component = Vue.extend(component); const Component = Vue.extend(component);
......
import Vue from 'vue'; import Vue from 'vue';
import component from '~/jobs/components/empty_state.vue'; import component from '~/jobs/components/empty_state.vue';
import mountComponent from '../helpers/vue_mount_component_helper'; import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Empty State', () => { describe('Empty State', () => {
const Component = Vue.extend(component); const Component = Vue.extend(component);
......
import Vue from 'vue'; import Vue from 'vue';
import { getTimeago } from '~/lib/utils/datetime_utility'; import { getTimeago } from '~/lib/utils/datetime_utility';
import component from '~/jobs/components/erased_block.vue'; import component from '~/jobs/components/erased_block.vue';
import mountComponent from '../helpers/vue_mount_component_helper'; import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Erased block', () => { describe('Erased block', () => {
const Component = Vue.extend(component); const Component = Vue.extend(component);
......
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import JobMediator from '~/jobs/job_details_mediator'; import JobMediator from '~/jobs/job_details_mediator';
import job from './mock_data'; import job from '../mock_data';
describe('JobMediator', () => { describe('JobMediator', () => {
let mediator; let mediator;
......
import Vue from 'vue'; import Vue from 'vue';
import component from '~/jobs/components/job_log.vue'; import component from '~/jobs/components/job_log.vue';
import mountComponent from '../helpers/vue_mount_component_helper'; import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Job Log', () => { describe('Job Log', () => {
const Component = Vue.extend(component); const Component = Vue.extend(component);
......
import JobStore from '~/jobs/stores/job_store'; import JobStore from '~/jobs/stores/job_store';
import job from './mock_data'; import job from '../mock_data';
describe('Job Store', () => { describe('Job Store', () => {
let store; let store;
......
import Vue from 'vue'; import Vue from 'vue';
import component from '~/jobs/components/jobs_container.vue'; import component from '~/jobs/components/jobs_container.vue';
import mountComponent from '../helpers/vue_mount_component_helper'; import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Artifacts block', () => { describe('Artifacts block', () => {
const Component = Vue.extend(component); const Component = Vue.extend(component);
......
import Vue from 'vue'; import Vue from 'vue';
import sidebarDetailsBlock from '~/jobs/components/sidebar_details_block.vue'; import sidebarDetailsBlock from '~/jobs/components/sidebar_details_block.vue';
import job from './mock_data'; import job from '../mock_data';
import mountComponent from '../helpers/vue_mount_component_helper'; import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Sidebar details block', () => { describe('Sidebar details block', () => {
let SidebarComponent; let SidebarComponent;
......
import Vue from 'vue'; import Vue from 'vue';
import component from '~/jobs/components/stages_dropdown.vue'; import component from '~/jobs/components/stages_dropdown.vue';
import mountComponent from '../helpers/vue_mount_component_helper'; import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Artifacts block', () => { describe('Artifacts block', () => {
const Component = Vue.extend(component); const Component = Vue.extend(component);
......
import Vue from 'vue'; import Vue from 'vue';
import component from '~/jobs/components/stuck_block.vue'; import component from '~/jobs/components/stuck_block.vue';
import mountComponent from '../helpers/vue_mount_component_helper'; import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Stuck Block Job component', () => { describe('Stuck Block Job component', () => {
const Component = Vue.extend(component); const Component = Vue.extend(component);
......
import Vue from 'vue'; import Vue from 'vue';
import component from '~/jobs/components/trigger_block.vue'; import component from '~/jobs/components/trigger_block.vue';
import mountComponent from '../helpers/vue_mount_component_helper'; import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Trigger block', () => { describe('Trigger block', () => {
const Component = Vue.extend(component); const Component = Vue.extend(component);
......
require 'spec_helper'
describe Banzai::Filter::SpacedLinkFilter do
include FilterSpecHelper
let(:link) { '[example](page slug)' }
it 'converts slug with spaces to a link' do
doc = filter("See #{link}")
expect(doc.at_css('a').text).to eq 'example'
expect(doc.at_css('a')['href']).to eq 'page%20slug'
expect(doc.at_css('p')).to eq nil
end
it 'converts slug with spaces and a title to a link' do
link = '[example](page slug "title")'
doc = filter("See #{link}")
expect(doc.at_css('a').text).to eq 'example'
expect(doc.at_css('a')['href']).to eq 'page%20slug'
expect(doc.at_css('a')['title']).to eq 'title'
expect(doc.at_css('p')).to eq nil
end
it 'does nothing when markdown_engine is redcarpet' do
exp = act = link
expect(filter(act, markdown_engine: :redcarpet).to_html).to eq exp
end
it 'does nothing with empty text' do
link = '[](page slug)'
doc = filter("See #{link}")
expect(doc.at_css('a')).to eq nil
end
it 'does nothing with an empty slug' do
link = '[example]()'
doc = filter("See #{link}")
expect(doc.at_css('a')).to eq nil
end
it 'converts multiple URLs' do
link1 = '[first](slug one)'
link2 = '[second](http://example.com/slug two)'
doc = filter("See #{link1} and #{link2}")
found_links = doc.css('a')
expect(found_links.size).to eq(2)
expect(found_links[0].text).to eq 'first'
expect(found_links[0]['href']).to eq 'slug%20one'
expect(found_links[1].text).to eq 'second'
expect(found_links[1]['href']).to eq 'http://example.com/slug%20two'
end
described_class::IGNORE_PARENTS.each do |elem|
it "ignores valid links contained inside '#{elem}' element" do
exp = act = "<#{elem}>See #{link}</#{elem}>"
expect(filter(act).to_html).to eq exp
end
end
end
...@@ -27,13 +27,13 @@ describe JiraService do ...@@ -27,13 +27,13 @@ describe JiraService do
end end
end end
describe "Associations" do describe 'Associations' do
it { is_expected.to belong_to :project } it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook } it { is_expected.to have_one :service_hook }
it { is_expected.to allow_value(nil).for(:jira_issue_transition_id) } it { is_expected.to allow_value(nil).for(:jira_issue_transition_id) }
it { is_expected.to allow_value("1,2,3").for(:jira_issue_transition_id) } it { is_expected.to allow_value('1,2,3').for(:jira_issue_transition_id) }
it { is_expected.to allow_value("1;2;3").for(:jira_issue_transition_id) } it { is_expected.to allow_value('1;2;3').for(:jira_issue_transition_id) }
it { is_expected.not_to allow_value("a,b,cd").for(:jira_issue_transition_id) } it { is_expected.not_to allow_value('a,b,cd').for(:jira_issue_transition_id) }
end end
describe 'Validations' do describe 'Validations' do
...@@ -116,143 +116,142 @@ describe JiraService do ...@@ -116,143 +116,142 @@ describe JiraService do
describe '#close_issue' do describe '#close_issue' do
let(:custom_base_url) { 'http://custom_url' } let(:custom_base_url) { 'http://custom_url' }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request) }
before do
@jira_service = described_class.new
allow(@jira_service).to receive_messages(
project_id: project.id,
project: project,
service_hook: true,
url: 'http://jira.example.com',
username: 'gitlab_jira_username',
password: 'gitlab_jira_password',
jira_issue_transition_id: "999"
)
# These stubs are needed to test JiraService#close_issue. shared_examples 'close_issue' do
# We close the issue then do another request to API to check if it got closed. before do
# Here is stubbed the API return with a closed and an opened issues. @jira_service = described_class.new
open_issue = JIRA::Resource::Issue.new(@jira_service.client, attrs: { "id" => "JIRA-123" }) allow(@jira_service).to receive_messages(
closed_issue = open_issue.dup project_id: project.id,
allow(open_issue).to receive(:resolution).and_return(false) project: project,
allow(closed_issue).to receive(:resolution).and_return(true) service_hook: true,
allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue) url: 'http://jira.example.com',
username: 'gitlab_jira_username',
allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return("JIRA-123") password: 'gitlab_jira_password',
allow(JIRA::Resource::Remotelink).to receive(:all).and_return([]) jira_issue_transition_id: '999'
)
@jira_service.save
project_issues_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123'
@transitions_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/transitions'
@comment_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/comment'
@remote_link_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/remotelink'
WebMock.stub_request(:get, project_issues_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
WebMock.stub_request(:post, @comment_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
WebMock.stub_request(:post, @remote_link_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
end
it "calls JIRA API" do # These stubs are needed to test JiraService#close_issue.
@jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) # 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: { 'id' => 'JIRA-123' })
closed_issue = open_issue.dup
allow(open_issue).to receive(:resolution).and_return(false)
allow(closed_issue).to receive(:resolution).and_return(true)
allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue)
expect(WebMock).to have_requested(:post, @comment_url).with( allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return('JIRA-123')
body: /Issue solved with/ allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
).once
end
# Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links @jira_service.save
# for more information
it "creates Remote Link reference in JIRA for comment" do
@jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
favicon_path = "http://localhost/assets/#{find_asset('favicon.png').digest_path}"
# Creates comment
expect(WebMock).to have_requested(:post, @comment_url)
# Creates Remote Link in JIRA issue fields
expect(WebMock).to have_requested(:post, @remote_link_url).with(
body: hash_including(
GlobalID: "GitLab",
object: {
url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}",
title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.",
icon: { title: "GitLab", url16x16: favicon_path },
status: { resolved: true }
}
)
).once
end
it "does not send comment or remote links to issues already closed" do project_issues_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123'
allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(true) @transitions_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/transitions'
@comment_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/comment'
@remote_link_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/remotelink'
@jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) WebMock.stub_request(:get, project_issues_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
WebMock.stub_request(:post, @comment_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
WebMock.stub_request(:post, @remote_link_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
end
expect(WebMock).not_to have_requested(:post, @comment_url) it 'calls JIRA API' do
expect(WebMock).not_to have_requested(:post, @remote_link_url) @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
end
it "does not send comment or remote links to issues with unknown resolution" do expect(WebMock).to have_requested(:post, @comment_url).with(
allow_any_instance_of(JIRA::Resource::Issue).to receive(:respond_to?).with(:resolution).and_return(false) body: /Issue solved with/
).once
end
@jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) # Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links
# for more information
it 'creates Remote Link reference in JIRA for comment' do
@jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
favicon_path = "http://localhost/assets/#{find_asset('favicon.png').digest_path}"
# Creates comment
expect(WebMock).to have_requested(:post, @comment_url)
# Creates Remote Link in JIRA issue fields
expect(WebMock).to have_requested(:post, @remote_link_url).with(
body: hash_including(
GlobalID: 'GitLab',
object: {
url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{commit_id}",
title: "GitLab: Solved by commit #{commit_id}.",
icon: { title: 'GitLab', url16x16: favicon_path },
status: { resolved: true }
}
)
).once
end
expect(WebMock).not_to have_requested(:post, @comment_url) it 'does not send comment or remote links to issues already closed' do
expect(WebMock).not_to have_requested(:post, @remote_link_url) allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(true)
end
it "references the GitLab commit/merge request" do @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
stub_config_setting(base_url: custom_base_url)
@jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) expect(WebMock).not_to have_requested(:post, @comment_url)
expect(WebMock).not_to have_requested(:post, @remote_link_url)
end
expect(WebMock).to have_requested(:post, @comment_url).with( it 'does not send comment or remote links to issues with unknown resolution' do
body: %r{#{custom_base_url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}} allow_any_instance_of(JIRA::Resource::Issue).to receive(:respond_to?).with(:resolution).and_return(false)
).once
end
it "references the GitLab commit/merge request (relative URL)" do @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
stub_config_setting(relative_url_root: '/gitlab')
stub_config_setting(url: Settings.send(:build_gitlab_url))
allow(described_class).to receive(:default_url_options) do expect(WebMock).not_to have_requested(:post, @comment_url)
{ script_name: '/gitlab' } expect(WebMock).not_to have_requested(:post, @remote_link_url)
end end
@jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) it 'references the GitLab commit' do
stub_config_setting(base_url: custom_base_url)
expect(WebMock).to have_requested(:post, @comment_url).with( @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
body: %r{#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}}
).once expect(WebMock).to have_requested(:post, @comment_url).with(
end body: %r{#{custom_base_url}/#{project.full_path}/commit/#{commit_id}}
).once
end
it 'references the GitLab commit' do
stub_config_setting(relative_url_root: '/gitlab')
stub_config_setting(url: Settings.send(:build_gitlab_url))
allow(described_class).to receive(:default_url_options) do
{ script_name: '/gitlab' }
end
@jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
context '#close_issue' do expect(WebMock).to have_requested(:post, @comment_url).with(
it "logs exception when transition id is not valid" do body: %r{#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{commit_id}}
).once
end
it 'logs exception when transition id is not valid' do
allow(Rails.logger).to receive(:info) allow(Rails.logger).to receive(:info)
WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise("Bad Request") WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise('Bad Request')
@jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
expect(Rails.logger).to have_received(:info).with("JiraService Issue Transition failed message ERROR: http://jira.example.com - Bad Request") expect(Rails.logger).to have_received(:info).with('JiraService Issue Transition failed message ERROR: http://jira.example.com - Bad Request')
end end
it "calls the api with jira_issue_transition_id" do it 'calls the api with jira_issue_transition_id' do
@jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
expect(WebMock).to have_requested(:post, @transitions_url).with( expect(WebMock).to have_requested(:post, @transitions_url).with(
body: /999/ body: /999/
).once ).once
end end
context "when have multiple transition ids" do context 'when have multiple transition ids' do
it "calls the api with transition ids separated by comma" do it 'calls the api with transition ids separated by comma' do
allow(@jira_service).to receive_messages(jira_issue_transition_id: "1,2,3") allow(@jira_service).to receive_messages(jira_issue_transition_id: '1,2,3')
@jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
1.upto(3) do |transition_id| 1.upto(3) do |transition_id|
expect(WebMock).to have_requested(:post, @transitions_url).with( expect(WebMock).to have_requested(:post, @transitions_url).with(
...@@ -261,10 +260,10 @@ describe JiraService do ...@@ -261,10 +260,10 @@ describe JiraService do
end end
end end
it "calls the api with transition ids separated by semicolon" do it 'calls the api with transition ids separated by semicolon' do
allow(@jira_service).to receive_messages(jira_issue_transition_id: "1;2;3") allow(@jira_service).to receive_messages(jira_issue_transition_id: '1;2;3')
@jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
1.upto(3) do |transition_id| 1.upto(3) do |transition_id|
expect(WebMock).to have_requested(:post, @transitions_url).with( expect(WebMock).to have_requested(:post, @transitions_url).with(
...@@ -274,6 +273,20 @@ describe JiraService do ...@@ -274,6 +273,20 @@ describe JiraService do
end end
end end
end end
context 'when resource is a merge request' do
let(:resource) { create(:merge_request) }
let(:commit_id) { resource.diff_head_sha }
it_behaves_like 'close_issue'
end
context 'when resource is a commit' do
let(:resource) { project.commit('master') }
let(:commit_id) { resource.id }
it_behaves_like 'close_issue'
end
end end
describe '#test_settings' do describe '#test_settings' do
...@@ -321,17 +334,17 @@ describe JiraService do ...@@ -321,17 +334,17 @@ describe JiraService do
end end
end end
describe "Stored password invalidation" do describe 'Stored password invalidation' do
let(:project) { create(:project) } let(:project) { create(:project) }
context "when a password was previously set" do context 'when a password was previously set' do
before do before do
@jira_service = described_class.create!( @jira_service = described_class.create!(
project: project, project: project,
properties: { properties: {
url: 'http://jira.example.com/web', url: 'http://jira.example.com/web',
username: 'mic', username: 'mic',
password: "password" password: 'password'
} }
) )
end end
...@@ -370,10 +383,10 @@ describe JiraService do ...@@ -370,10 +383,10 @@ describe JiraService do
@jira_service.url = 'http://jira_edited.example.com/rweb' @jira_service.url = 'http://jira_edited.example.com/rweb'
@jira_service.save @jira_service.save
expect(@jira_service.password).to eq("password") expect(@jira_service.password).to eq('password')
end end
it 'reset password if api url set to ""' do it 'reset password if api url set to empty' do
@jira_service.api_url = '' @jira_service.api_url = ''
@jira_service.save @jira_service.save
...@@ -440,7 +453,7 @@ describe JiraService do ...@@ -440,7 +453,7 @@ describe JiraService do
it 'is initialized' do it 'is initialized' do
expect(@service.title).to eq('JIRA') expect(@service.title).to eq('JIRA')
expect(@service.description).to eq("Jira issue tracker") expect(@service.description).to eq('Jira issue tracker')
end end
end end
...@@ -454,7 +467,7 @@ describe JiraService do ...@@ -454,7 +467,7 @@ describe JiraService do
@service.destroy! @service.destroy!
end end
it "is correct" do it 'is correct' do
expect(@service.title).to eq('Jira One') expect(@service.title).to eq('Jira One')
expect(@service.description).to eq('Jira One issue tracker') expect(@service.description).to eq('Jira One issue tracker')
end end
...@@ -476,7 +489,7 @@ describe JiraService do ...@@ -476,7 +489,7 @@ describe JiraService do
it 'is initialized' do it 'is initialized' do
expect(@service.options[:use_cookies]).to eq(true) expect(@service.options[:use_cookies]).to eq(true)
expect(@service.options[:additional_cookies]).to eq(["OBBasicAuth=fromDialog"]) expect(@service.options[:additional_cookies]).to eq(['OBBasicAuth=fromDialog'])
end end
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