jira_dependency_visualizer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +129 -0
- data/Guardfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +67 -0
- data/Rakefile +12 -0
- data/bin/jira_dependency_visualizer +112 -0
- data/issue_graph.svg +339 -0
- data/jira_dependency_visualizer.gemspec +35 -0
- data/lib/jira_dependency_visualizer.rb +7 -0
- data/lib/jira_dependency_visualizer/graph.rb +118 -0
- data/lib/jira_dependency_visualizer/jira.rb +55 -0
- data/lib/jira_dependency_visualizer/version.rb +4 -0
- data/spec/factories/issue.rb +80 -0
- data/spec/factories/node.rb +4 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/unit/graph_spec.rb +79 -0
- data/spec/unit/jira_dependency_visualizer_spec.rb +7 -0
- data/spec/unit/jira_spec.rb +69 -0
- metadata +237 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
require 'rake'
|
|
3
|
+
|
|
4
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
6
|
+
require 'jira_dependency_visualizer/version'
|
|
7
|
+
|
|
8
|
+
Gem::Specification.new do |s|
|
|
9
|
+
s.authors = ['Alejandro Figueroa']
|
|
10
|
+
s.bindir = 'bin'
|
|
11
|
+
s.description = 'Creates a graphviz file from a JIRA ticket\'s dependencies'
|
|
12
|
+
s.email = ['alejandro@ideasftw.com']
|
|
13
|
+
s.executables << 'jira_dependency_visualizer'
|
|
14
|
+
s.files = FileList['lib/**/*.rb', 'bin/*', '[A-Z]*', 'spec/**/*'].to_a
|
|
15
|
+
s.homepage = 'https://github.com/thejandroman/jira_dependency_visualizer'
|
|
16
|
+
s.license = 'MIT'
|
|
17
|
+
s.name = 'jira_dependency_visualizer'
|
|
18
|
+
s.summary = 'Creates a graphviz file from a JIRA ticket\'s dependencies'
|
|
19
|
+
s.required_ruby_version = ['>= 2.1.0', '<= 2.3.0']
|
|
20
|
+
s.version = JiraDependencyVisualizer::VERSION
|
|
21
|
+
|
|
22
|
+
s.add_dependency 'jira-ruby', '~> 0.1'
|
|
23
|
+
s.add_dependency 'ruby-graphviz', '~> 1.2'
|
|
24
|
+
s.add_dependency 'trollop', '~> 2.1'
|
|
25
|
+
|
|
26
|
+
s.add_development_dependency 'bundler', '~> 1.11'
|
|
27
|
+
s.add_development_dependency 'coveralls', '~> 0.8'
|
|
28
|
+
s.add_development_dependency 'factory_girl', '~> 4.5'
|
|
29
|
+
s.add_development_dependency 'guard-rspec', '~> 4.6'
|
|
30
|
+
s.add_development_dependency 'pry', '~> 0.10'
|
|
31
|
+
s.add_development_dependency 'rake', '~> 10.5'
|
|
32
|
+
s.add_development_dependency 'rspec', '~> 3.4'
|
|
33
|
+
s.add_development_dependency 'rubocop', '~> 0.37'
|
|
34
|
+
s.add_development_dependency 'yard', '~> 0.8'
|
|
35
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
require 'graphviz'
|
|
2
|
+
|
|
3
|
+
module JiraDependencyVisualizer
|
|
4
|
+
# Gets Jira issue dependencies and creates a Graphviz graph based on
|
|
5
|
+
# those dependencies
|
|
6
|
+
class Graph
|
|
7
|
+
# @param [String] the initial issue to build the dependency graph
|
|
8
|
+
# from
|
|
9
|
+
# @param [JiraDependencyVisualizer::Jira]
|
|
10
|
+
# @param [Array] list of issue link types to exclude from the
|
|
11
|
+
# graph
|
|
12
|
+
# @param [Hash] colors for graph; see `./config/colors.yaml`
|
|
13
|
+
# @param [Graphviz]
|
|
14
|
+
def initialize(start_issue_key, jira, excludes = [], colors = {}, graph = nil)
|
|
15
|
+
@start_issue_key = start_issue_key
|
|
16
|
+
@jira = jira
|
|
17
|
+
@excludes = excludes
|
|
18
|
+
@colors = colors
|
|
19
|
+
@graph = graph || GraphViz.new(start_issue_key.to_sym, type: :digraph)
|
|
20
|
+
@seen = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Retrieve a list of all tickets dependent to `@start_issue_key`
|
|
24
|
+
# and add them to a Graphviz graph.
|
|
25
|
+
def walk
|
|
26
|
+
issue_walker(@start_issue_key)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param [String] filename the output filename
|
|
30
|
+
# @param [String] format the graphviz output format for the graph
|
|
31
|
+
def write_graph(filename = './issue_graph.svg', format = 'svg')
|
|
32
|
+
@graph.output(format.to_sym => filename)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def status_color(issue)
|
|
38
|
+
return {} unless @colors.key?('status')
|
|
39
|
+
@colors['status'][issue['fields']['status']['name']] || {}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def get_key(issue)
|
|
43
|
+
issue.attrs['key']
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def process_link(issue_key, link)
|
|
47
|
+
return unless link.key?('outwardIssue')
|
|
48
|
+
|
|
49
|
+
direction = 'outward'
|
|
50
|
+
linked_issue_key = link[direction + 'Issue']['key']
|
|
51
|
+
link_type = link['type'][direction]
|
|
52
|
+
|
|
53
|
+
return linked_issue_key if @excludes.include?(link_type)
|
|
54
|
+
|
|
55
|
+
attrs = { label: link_type }
|
|
56
|
+
attrs[:color] = 'red' if link_type == 'blocks'
|
|
57
|
+
node_edge_builder(issue_key, linked_issue_key, attrs)
|
|
58
|
+
# node_attr_builder(linked_issue_key, status_color(link['outwardIssue']))
|
|
59
|
+
|
|
60
|
+
linked_issue_key
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def node_edge_builder(node1_key, node2_key, edge_attrs)
|
|
64
|
+
@graph.add_edges(node1_key, node2_key, edge_attrs)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def node_attr_builder(issue)
|
|
68
|
+
issue_key = get_key(issue)
|
|
69
|
+
attrs = status_color(issue.attrs).merge(
|
|
70
|
+
'URL' => "#{@jira.base_url}/browse/#{issue_key}",
|
|
71
|
+
'tooltip' => issue.attrs['fields']['summary']
|
|
72
|
+
)
|
|
73
|
+
node = @graph.search_node(issue_key)
|
|
74
|
+
attrs.each { |k, v| node[k] = v }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def epic_walker(issue_key, children)
|
|
78
|
+
issues = @jira.query("'Epic Link' = '#{issue_key}'")
|
|
79
|
+
issues.each do |subtask|
|
|
80
|
+
subtask_key = get_key(subtask)
|
|
81
|
+
node_edge_builder(issue_key, subtask_key, color: 'orange')
|
|
82
|
+
node_attr_builder(subtask)
|
|
83
|
+
children.push(subtask_key)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def issue_walker(issue_key)
|
|
88
|
+
issue = @jira.get_issue(issue_key)
|
|
89
|
+
@seen.push(issue_key)
|
|
90
|
+
fields = issue.attrs['fields']
|
|
91
|
+
fields.reject! { |_, v| v.nil? || v.empty? if v.is_a? Array }
|
|
92
|
+
children = []
|
|
93
|
+
|
|
94
|
+
epic_walker(issue_key, children) if fields['issuetype']['name'] == 'Epic'
|
|
95
|
+
subtasks_walker(fields['subtasks'], issue_key, children) if fields.key?('subtasks')
|
|
96
|
+
issuelinks_walker(fields['issuelinks'], issue_key, children) if fields.key?('issuelinks')
|
|
97
|
+
|
|
98
|
+
(children - @seen).each { |child| issue_walker(child) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def issuelinks_walker(issuelinks, issue_key, children)
|
|
102
|
+
issuelinks.each do |other_link|
|
|
103
|
+
result = process_link(issue_key, other_link)
|
|
104
|
+
next if result.nil?
|
|
105
|
+
children.append(result)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def subtasks_walker(subtasks, issue_key, children)
|
|
110
|
+
subtasks.each do |subtask|
|
|
111
|
+
subtask_key = get_key(subtask)
|
|
112
|
+
node_edge_builder(issue_key, subtask_key, color: 'blue', label: 'subtask')
|
|
113
|
+
node_attr_builder(subtask)
|
|
114
|
+
children.append(subtask_key)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require 'jira'
|
|
2
|
+
|
|
3
|
+
module JiraDependencyVisualizer
|
|
4
|
+
# Creates a Jira client and exposes functions to get Jira issues
|
|
5
|
+
class Jira
|
|
6
|
+
# @param [Hash] opts the options to create a Jira client
|
|
7
|
+
# @option opts [String] :context_path The Jira application's
|
|
8
|
+
# context path
|
|
9
|
+
# @option opts [String] :password The Jira user's password
|
|
10
|
+
# @option opts [String] :proxy_address The proxy address
|
|
11
|
+
# @option opts [Integer] :proxy_port The proxy port
|
|
12
|
+
# @option opts [Integer] :read_timeout Number of seconds to wait
|
|
13
|
+
# for data to be read
|
|
14
|
+
# @option opts [String] :rest_base_path The Jira rest API base
|
|
15
|
+
# path
|
|
16
|
+
# @option opts [String] :site URL for Jira
|
|
17
|
+
# @option opts [Boolean] :use_ssl Whether to use SSL
|
|
18
|
+
# @option opts [String] :username The Jira user's username
|
|
19
|
+
def initialize(opts = {})
|
|
20
|
+
@options = opts
|
|
21
|
+
client
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param [String] issue the Jira issue ID
|
|
25
|
+
# @param [JIRA::Client] client a Jira client instance
|
|
26
|
+
# @return [JIRA::Issue] the matched Jira issue object
|
|
27
|
+
def get_issue(issue, client = @client)
|
|
28
|
+
client.Issue.find(issue)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param [String] query a Jira JQL query string
|
|
32
|
+
# @param [JIRA::Client] client a Jira client instance
|
|
33
|
+
# @return [Array[JIRA::Issue]] list of matching Jira issue objects
|
|
34
|
+
def query(query, client = @client)
|
|
35
|
+
client.Issue.jql(query)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [String] the base Jira URL without a trailing slash
|
|
39
|
+
def base_url
|
|
40
|
+
site = @options[:site].dup
|
|
41
|
+
context_path = @options[:context_path].dup
|
|
42
|
+
|
|
43
|
+
site.chop! if site.ends_with?('/')
|
|
44
|
+
context_path.chop! if context_path.ends_with?('/')
|
|
45
|
+
|
|
46
|
+
URI.join(site, context_path).to_s
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def client
|
|
52
|
+
@client ||= JIRA::Client.new(@options)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
FactoryGirl.define do
|
|
2
|
+
factory :subtask_issue, class: OpenStruct do
|
|
3
|
+
attrs do
|
|
4
|
+
{
|
|
5
|
+
'key' => 'test3',
|
|
6
|
+
'fields' => {
|
|
7
|
+
'issuetype' => {
|
|
8
|
+
'name' => 'notEpic'
|
|
9
|
+
},
|
|
10
|
+
'status' => {
|
|
11
|
+
'name' => 'To Do'
|
|
12
|
+
},
|
|
13
|
+
'subtasks' => [build(:generic_issue)]
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
factory :generic_issue, class: OpenStruct do
|
|
20
|
+
attrs do
|
|
21
|
+
{ 'key' => 'test5',
|
|
22
|
+
'fields' => {
|
|
23
|
+
'status' => {
|
|
24
|
+
'name' => 'Blocked'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
factory :issuelinks_generic_issue, class: Hash do
|
|
32
|
+
outwardIssue do
|
|
33
|
+
{ 'key' => 'test3',
|
|
34
|
+
'fields' => {
|
|
35
|
+
'status' => {
|
|
36
|
+
'name' => 'Done'
|
|
37
|
+
}
|
|
38
|
+
} }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
type do
|
|
42
|
+
{ 'outward' => 'blocks' }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
initialize_with { attributes.stringify_keys }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
factory :issuelinks_issue, class: OpenStruct do
|
|
49
|
+
attrs do
|
|
50
|
+
{
|
|
51
|
+
'key' => 'test4',
|
|
52
|
+
'fields' => {
|
|
53
|
+
'issuetype' => {
|
|
54
|
+
'name' => 'notEpic'
|
|
55
|
+
},
|
|
56
|
+
'status' => {
|
|
57
|
+
'name' => 'To Do'
|
|
58
|
+
},
|
|
59
|
+
'issuelinks' => [build(:issuelinks_generic_issue)]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
factory :epic_issue, class: OpenStruct do
|
|
66
|
+
attrs do
|
|
67
|
+
{
|
|
68
|
+
'key' => 'test',
|
|
69
|
+
'fields' => {
|
|
70
|
+
'issuetype' => {
|
|
71
|
+
'name' => 'Epic'
|
|
72
|
+
},
|
|
73
|
+
'status' => {
|
|
74
|
+
'name' => 'Planning'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'coveralls'
|
|
2
|
+
require 'simplecov'
|
|
3
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
|
|
4
|
+
[SimpleCov::Formatter::HTMLFormatter,
|
|
5
|
+
Coveralls::SimpleCov::Formatter]
|
|
6
|
+
)
|
|
7
|
+
SimpleCov.start do
|
|
8
|
+
add_filter 'spec'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
|
12
|
+
|
|
13
|
+
require 'jira_dependency_visualizer'
|
|
14
|
+
require 'pry'
|
|
15
|
+
require 'factory_girl'
|
|
16
|
+
|
|
17
|
+
RSpec.configure do |config|
|
|
18
|
+
config.expect_with :rspec do |c|
|
|
19
|
+
c.syntax = :expect
|
|
20
|
+
end
|
|
21
|
+
config.include FactoryGirl::Syntax::Methods
|
|
22
|
+
config.before(:suite) do
|
|
23
|
+
FactoryGirl.find_definitions
|
|
24
|
+
# FactoryGirl.lint
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe JiraDependencyVisualizer::Graph do
|
|
4
|
+
subject { JiraDependencyVisualizer::Graph.new('issue id', jira, [], colors, graph) }
|
|
5
|
+
|
|
6
|
+
let(:epic_issue) { build(:epic_issue) }
|
|
7
|
+
let(:issues) { build_pair(:subtask_issue) }
|
|
8
|
+
|
|
9
|
+
let(:jira) do
|
|
10
|
+
response = double('jira')
|
|
11
|
+
allow(response).to receive(:get_issue).and_return(epic_issue)
|
|
12
|
+
allow(response).to receive(:query).and_return(issues)
|
|
13
|
+
allow(response).to receive(:base_url).and_return('http://localhost')
|
|
14
|
+
response
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
let(:node) { build(:node) }
|
|
18
|
+
|
|
19
|
+
let(:graph) do
|
|
20
|
+
response = double('graph')
|
|
21
|
+
allow(response).to receive(:add_edges).and_return(true)
|
|
22
|
+
allow(response).to receive(:search_node).and_return(node)
|
|
23
|
+
allow(response).to receive(:output).and_return(nil)
|
|
24
|
+
response
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
let(:colors) do
|
|
28
|
+
{ 'status' =>
|
|
29
|
+
{ 'Incoming' =>
|
|
30
|
+
{ 'fillcolor' => 'purple',
|
|
31
|
+
'style' => 'filled' },
|
|
32
|
+
'To Do' =>
|
|
33
|
+
{ 'fillcolor' => 'purple',
|
|
34
|
+
'style' => 'filled',
|
|
35
|
+
'fontcolor' => 'white' }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
context '#new' do
|
|
41
|
+
it { is_expected.to be_instance_of JiraDependencyVisualizer::Graph }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
context '#walk subtasks' do
|
|
45
|
+
let(:issues) { build_pair(:subtask_issue) }
|
|
46
|
+
|
|
47
|
+
let(:jira) do
|
|
48
|
+
response = double('jira')
|
|
49
|
+
allow(response).to receive(:get_issue).and_return(epic_issue, issues[0])
|
|
50
|
+
allow(response).to receive(:query).and_return(issues)
|
|
51
|
+
allow(response).to receive(:base_url).and_return('http://localhost')
|
|
52
|
+
response
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it { expect(subject.walk).to eq(%w(test3 test3)) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
context '#walk issuelinks' do
|
|
59
|
+
let(:issues) { build_pair(:issuelinks_issue) }
|
|
60
|
+
|
|
61
|
+
let(:jira) do
|
|
62
|
+
response = double('jira')
|
|
63
|
+
allow(response).to receive(:get_issue).and_return(epic_issue, issues[0])
|
|
64
|
+
allow(response).to receive(:query).and_return(issues)
|
|
65
|
+
allow(response).to receive(:base_url).and_return('http://localhost')
|
|
66
|
+
response
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it { expect(subject.walk).to eq(%w(test4 test4)) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
context '#write_graph' do
|
|
73
|
+
it { is_expected.to respond_to(:write_graph) }
|
|
74
|
+
it 'write a graph' do
|
|
75
|
+
written_graph = subject.write_graph
|
|
76
|
+
expect(written_graph).to eq(nil)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe JiraDependencyVisualizer::Jira do
|
|
4
|
+
context '#new' do
|
|
5
|
+
it { is_expected.to be_instance_of JiraDependencyVisualizer::Jira }
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
let(:issue_class) do
|
|
9
|
+
class_double('JIRA::Issue').as_stubbed_const
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
let(:issue) do
|
|
13
|
+
response = double('issue')
|
|
14
|
+
allow(response).to receive(:find).and_return(issue_class)
|
|
15
|
+
allow(response).to receive(:jql).and_return(issue_class)
|
|
16
|
+
response
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
let(:client) do
|
|
20
|
+
response = double('client')
|
|
21
|
+
allow(response).to receive(:Issue).and_return(issue)
|
|
22
|
+
response
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
context '#get_issue' do
|
|
26
|
+
it { is_expected.to respond_to(:get_issue) }
|
|
27
|
+
it 'get an issue' do
|
|
28
|
+
issue = subject.get_issue('issue id', client)
|
|
29
|
+
expect(issue).to eq(JIRA::Issue)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
context '#query' do
|
|
34
|
+
it { is_expected.to respond_to(:query) }
|
|
35
|
+
it 'get a query' do
|
|
36
|
+
query = subject.query('query', client)
|
|
37
|
+
expect(query).to eq(JIRA::Issue)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
context '#base_url' do
|
|
42
|
+
subject do
|
|
43
|
+
JiraDependencyVisualizer::Jira.new(
|
|
44
|
+
site: site,
|
|
45
|
+
context_path: context_path
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
context 'no trailing slash, no context path' do
|
|
49
|
+
let(:site) { 'http://localhost' }
|
|
50
|
+
let(:context_path) { '' }
|
|
51
|
+
it { expect(subject.base_url).to eq('http://localhost') }
|
|
52
|
+
end
|
|
53
|
+
context 'no trailing slash, context path' do
|
|
54
|
+
let(:site) { 'http://localhost' }
|
|
55
|
+
let(:context_path) { 'jira' }
|
|
56
|
+
it { expect(subject.base_url).to eq('http://localhost/jira') }
|
|
57
|
+
end
|
|
58
|
+
context 'trailing slash, no context path' do
|
|
59
|
+
let(:site) { 'http://localhost/' }
|
|
60
|
+
let(:context_path) { '' }
|
|
61
|
+
it { expect(subject.base_url).to eq('http://localhost') }
|
|
62
|
+
end
|
|
63
|
+
context 'trailing slash, context path' do
|
|
64
|
+
let(:site) { 'http://localhost/' }
|
|
65
|
+
let(:context_path) { 'jira' }
|
|
66
|
+
it { expect(subject.base_url).to eq('http://localhost/jira') }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|