jira-slurper 1.3.1 → 1.3.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.rdoc +3 -3
- data/bin/slurp +3 -3
- data/lib/jira/inflectors.rb +108 -0
- data/lib/jira.rb +121 -71
- data/lib/slurper.rb +5 -2
- data/lib/story.rb +52 -24
- data/spec/jira_spec.rb +71 -37
- data/spec/pivotal_spec.rb +2 -38
- data/spec/slurper_spec.rb +24 -24
- data/spec/story_spec.rb +64 -13
- metadata +19 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8fe9b95e6287faca051d81a53b31a96dc83bcc97
|
4
|
+
data.tar.gz: be88e9ef68afb38a263592a8305d2264762b47e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 19d143ebdf7583731ba5d8bdfb1e566bc23f535ffb1793e6d9f0bbe8ddfda09e314314812d86b77cb35a9a0c08c7aa53c2b8910e470b07a3c78d7d2cb6da205b
|
7
|
+
data.tar.gz: a755f9a35edfd10c66b3b8702222278c88656b7c869a714a528b2bb3f72201b8dd6352a6328dc8d3cb84e72cc9b6eeabe6f41ef0519857b89386f4ec4ad853b7
|
data/README.rdoc
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
-
= slurper gem
|
1
|
+
= jira-slurper gem
|
2
2
|
|
3
|
-
Slurper allows you to quickly compose your stories in a text file and import them into Pivotal Tracker.
|
3
|
+
Slurper allows you to quickly compose your stories in a text file and import them into Jira or Pivotal Tracker.
|
4
4
|
|
5
5
|
Works great with slurper.vim! (http://github.com/adamlowe/vim-slurper)
|
6
6
|
|
7
7
|
== Install
|
8
8
|
|
9
|
-
gem install slurper
|
9
|
+
gem install jira-slurper
|
10
10
|
|
11
11
|
== Config
|
12
12
|
|
data/bin/slurp
CHANGED
@@ -41,10 +41,10 @@ end.parse!
|
|
41
41
|
|
42
42
|
story_file = ARGV.empty? ? "stories.slurper" : ARGV[0]
|
43
43
|
|
44
|
-
handlers = [Jira.new, Pivotal.new]
|
44
|
+
handlers = [Jira::Handler.new, Pivotal.new]
|
45
45
|
|
46
|
-
raise "Stories file not found: '#{story_file}'"
|
47
|
-
raise "Config file not found: '#{options[:config_file]}'"
|
46
|
+
raise "Stories file not found: '#{story_file}'" unless File::exists? story_file
|
47
|
+
raise "Config file not found: '#{options[:config_file]}'" unless File::exists? options[:config_file]
|
48
48
|
|
49
49
|
if options[:dump]
|
50
50
|
Slurper.dump(options[:config_file], options[:filter], handlers, options[:reverse])
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module Jira
|
2
|
+
module Inflectors
|
3
|
+
|
4
|
+
class BaseInflector
|
5
|
+
def initialize(value)
|
6
|
+
@value = value
|
7
|
+
end
|
8
|
+
|
9
|
+
def value
|
10
|
+
@value
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class StringInflector < BaseInflector
|
15
|
+
def initialize(value)
|
16
|
+
super(value.to_s)
|
17
|
+
end
|
18
|
+
|
19
|
+
def value
|
20
|
+
super.strip
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class NumberFieldInflector < BaseInflector
|
25
|
+
def value
|
26
|
+
@value.to_f
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class LabelInflector < StringInflector
|
31
|
+
def value
|
32
|
+
super.gsub(' ', '_')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class IndexedValueInflector < BaseInflector
|
37
|
+
def initialize(value, key=nil)
|
38
|
+
super(value)
|
39
|
+
if key.nil?
|
40
|
+
key = :id if value.is_a?(Fixnum) or (value.is_a?(String) && value[/^[0-9]+$/])
|
41
|
+
end
|
42
|
+
@key = key || :value
|
43
|
+
end
|
44
|
+
|
45
|
+
def value
|
46
|
+
{@key.to_sym => super}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class CompositeInflector < BaseInflector
|
51
|
+
def initialize(*inflectors)
|
52
|
+
@inflectors = inflectors
|
53
|
+
end
|
54
|
+
|
55
|
+
def value
|
56
|
+
@inflectors.map(&:value)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.factory(type, value)
|
61
|
+
case type.to_s
|
62
|
+
when 'FreeTextField', 'TextField', 'URLField'
|
63
|
+
StringInflector.new(value)
|
64
|
+
when 'GroupPicker', 'UserPicker', 'SingleVersionPicker', 'SingleSelect'
|
65
|
+
IndexedValueInflector.new(value, :name)
|
66
|
+
when 'MultiUserPicker', 'MultiGroupPicker', 'MultiSelect'
|
67
|
+
inner = ensure_list(value) do |value|
|
68
|
+
value.map do |v|
|
69
|
+
begin
|
70
|
+
factory(type.sub('Multi', ''), v)
|
71
|
+
rescue
|
72
|
+
factory(type.sub('Multi', 'Single'), v)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
CompositeInflector.new(*inner)
|
77
|
+
when 'Labels'
|
78
|
+
ensure_list(value) do |value|
|
79
|
+
CompositeInflector.new(*(value.map { |v| LabelInflector.new(v) }))
|
80
|
+
end
|
81
|
+
when 'SelectList'
|
82
|
+
IndexedValueInflector.new(value, :value)
|
83
|
+
when 'ProjectPicker'
|
84
|
+
IndexedValueInflector.new(value, :key)
|
85
|
+
else
|
86
|
+
inflector = "#{type}Inflector".to_sym
|
87
|
+
unless const_defined?(inflector)
|
88
|
+
raise "Unrecognized field type \"#{type}\""
|
89
|
+
end
|
90
|
+
const_get(inflector).new(value)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def self.ensure_list(value)
|
97
|
+
value = case value
|
98
|
+
when String
|
99
|
+
value.split(',').map(&:strip)
|
100
|
+
when Enumerable
|
101
|
+
value
|
102
|
+
else
|
103
|
+
raise('Expected enumerable value')
|
104
|
+
end
|
105
|
+
yield value if block_given?
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/jira.rb
CHANGED
@@ -2,112 +2,162 @@ require 'faraday'
|
|
2
2
|
require 'configliere'
|
3
3
|
require 'base64'
|
4
4
|
require 'json'
|
5
|
+
require 'jira/inflectors'
|
5
6
|
|
6
|
-
|
7
|
-
class
|
7
|
+
module Jira
|
8
|
+
class Handler
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
def initialize(jira_mapper = nil, jira_api = nil)
|
11
|
+
@mapper = jira_mapper || StoryMapper.new
|
12
|
+
@api = jira_api || ApiConsumer.new
|
13
|
+
end
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
def supports?(config)
|
16
|
+
config.fetch('tracker', '').downcase == 'jira'
|
17
|
+
end
|
17
18
|
|
18
|
-
|
19
|
-
|
19
|
+
def configure!(config)
|
20
|
+
settings = Configliere::Param.new
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
settings.define :username, :required => true, :env_var => 'JIRA_USERNAME'
|
23
|
+
settings.define :password, :required => true, :env_var => 'JIRA_PASSWORD'
|
24
|
+
settings.define :url, :required => true
|
25
|
+
settings.define :project, :required => true
|
26
|
+
settings.define :mappings, :required => false
|
25
27
|
|
26
|
-
|
28
|
+
settings.defaults api_version: 'latest', default_labels: '', mappings: {}
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
+
settings.use(config)
|
31
|
+
settings.resolve!
|
30
32
|
|
31
|
-
|
32
|
-
|
33
|
+
@mapper.configure!(settings)
|
34
|
+
@config = settings
|
35
|
+
end
|
36
|
+
|
37
|
+
def handle(yaml_story)
|
38
|
+
@api.configure!(@config[:url], @config[:username], @config[:password], @config[:api_version])
|
33
39
|
|
34
|
-
|
35
|
-
|
40
|
+
yaml_story['project'] = @config[:project]
|
41
|
+
yaml_story['reporter'] = @config[:username] unless yaml_story.key?('reporter')
|
36
42
|
|
37
|
-
|
43
|
+
issue = @mapper.map(yaml_story)
|
38
44
|
|
39
|
-
|
40
|
-
|
45
|
+
success, response_body = @api.create_issue(issue)
|
46
|
+
response = JSON.parse response_body
|
41
47
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
48
|
+
if success
|
49
|
+
message = "Issue key = #{response['key']}, #{@config[:url]}/browse/#{response['key']}"
|
50
|
+
else
|
51
|
+
message = JSON.pretty_generate response
|
52
|
+
end
|
53
|
+
|
54
|
+
yield success, message
|
46
55
|
end
|
47
56
|
|
48
|
-
yield success, message
|
49
57
|
end
|
50
58
|
|
51
|
-
|
59
|
+
class ApiConsumer
|
52
60
|
|
53
|
-
|
61
|
+
def configure!(url, username, password, api_version = 'latest')
|
62
|
+
@url = url
|
63
|
+
@username = username
|
64
|
+
@password = password
|
65
|
+
@api_version = api_version
|
66
|
+
end
|
54
67
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
@api_version = api_version
|
60
|
-
end
|
68
|
+
def create_issue(jira_story)
|
69
|
+
response = post(url, jira_story.to_json, headers)
|
70
|
+
return response.success?, response.body
|
71
|
+
end
|
61
72
|
|
62
|
-
|
63
|
-
response = post(url, jira_story.to_json, headers)
|
64
|
-
return response.success?, response.body
|
65
|
-
end
|
73
|
+
protected
|
66
74
|
|
67
|
-
|
75
|
+
def headers
|
76
|
+
{'Content-Type' => 'application/json', 'Authorization' => auth_header}
|
77
|
+
end
|
68
78
|
|
69
|
-
|
70
|
-
|
71
|
-
|
79
|
+
def post(issues_url, json_data, headers)
|
80
|
+
|
81
|
+
conn = Faraday.new do |faraday|
|
82
|
+
faraday.headers = headers
|
83
|
+
faraday.adapter Faraday.default_adapter
|
84
|
+
end
|
72
85
|
|
73
|
-
|
86
|
+
conn.post do |req|
|
87
|
+
req.url issues_url
|
88
|
+
req.body = json_data
|
89
|
+
end
|
74
90
|
|
75
|
-
conn = Faraday.new do |faraday|
|
76
|
-
faraday.headers = headers
|
77
|
-
faraday.adapter Faraday.default_adapter
|
78
91
|
end
|
79
92
|
|
80
|
-
|
81
|
-
|
82
|
-
req.body = json_data
|
93
|
+
def url
|
94
|
+
"#{@url}/rest/api/#{@api_version}/issue/"
|
83
95
|
end
|
84
96
|
|
85
|
-
|
97
|
+
def auth_header
|
98
|
+
login = Base64.encode64 @username + ':' + @password
|
99
|
+
"Basic #{login}"
|
100
|
+
end
|
86
101
|
|
87
|
-
def url
|
88
|
-
"#{@url}/rest/api/#{@api_version}/issue/"
|
89
102
|
end
|
90
103
|
|
91
|
-
|
92
|
-
login = Base64.encode64 @username + ':' + @password
|
93
|
-
"Basic #{login}"
|
94
|
-
end
|
104
|
+
class StoryMapper
|
95
105
|
|
96
|
-
|
106
|
+
def initialize
|
107
|
+
@map = default_mapping
|
108
|
+
end
|
97
109
|
|
98
|
-
|
110
|
+
def configure!(config)
|
111
|
+
config[:mappings].each do |mapping|
|
112
|
+
field, map = mapping
|
113
|
+
@map[field] = map.to_hash
|
114
|
+
end
|
115
|
+
end
|
99
116
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
117
|
+
def map(story)
|
118
|
+
fields = story.to_hash.inject({}) do |hash, (key, value)|
|
119
|
+
hash.merge!(do_map(key, value))
|
120
|
+
hash
|
121
|
+
end
|
122
|
+
{:fields => fields}
|
123
|
+
end
|
106
124
|
|
107
|
-
|
108
|
-
|
125
|
+
private
|
126
|
+
|
127
|
+
def default_mapping
|
128
|
+
{
|
129
|
+
:project => {
|
130
|
+
:type => 'ProjectPicker'
|
131
|
+
},
|
132
|
+
:name => {
|
133
|
+
:field_name => :summary,
|
134
|
+
:type => 'TextField'
|
135
|
+
},
|
136
|
+
:description => {
|
137
|
+
:type => 'FreeTextField'
|
138
|
+
},
|
139
|
+
:story_type => {
|
140
|
+
:field_name => :issuetype,
|
141
|
+
:type => 'SingleSelect'
|
142
|
+
},
|
143
|
+
:labels => {
|
144
|
+
:type => 'Labels'
|
145
|
+
},
|
146
|
+
:reporter => {
|
147
|
+
:type => 'SingleSelect'
|
148
|
+
},
|
149
|
+
:assignee => {
|
150
|
+
:type => 'SingleSelect'
|
151
|
+
}
|
152
|
+
}
|
109
153
|
end
|
110
154
|
|
111
|
-
|
155
|
+
def do_map(key, value)
|
156
|
+
map = @map[key.to_sym].tap do |m|
|
157
|
+
raise "I don't know how to map '#{key}' to request. Please add it to your mappings config." if m.nil?
|
158
|
+
end
|
159
|
+
|
160
|
+
{map.fetch(:field_name, key).to_sym => Inflectors.factory(map.fetch(:type), value).value}
|
161
|
+
end
|
112
162
|
end
|
113
163
|
end
|
data/lib/slurper.rb
CHANGED
@@ -2,6 +2,9 @@ require 'yaml'
|
|
2
2
|
require 'story'
|
3
3
|
|
4
4
|
class Slurper
|
5
|
+
|
6
|
+
GLOBAL_STORY_ATTRIBUTES = ['project']
|
7
|
+
|
5
8
|
attr_accessor :story_file, :stories, :handlers
|
6
9
|
|
7
10
|
def self.dump(config_file, filter, handlers, reverse)
|
@@ -14,7 +17,7 @@ class Slurper
|
|
14
17
|
def self.slurp(story_file, config_file, handlers, reverse)
|
15
18
|
config = self.load_config(config_file)
|
16
19
|
|
17
|
-
stories = self.load_stories(story_file, config)
|
20
|
+
stories = self.load_stories(story_file, config.select {|k, _| GLOBAL_STORY_ATTRIBUTES.include?(k)})
|
18
21
|
stories.reverse! unless reverse
|
19
22
|
|
20
23
|
slurper = new(config, stories)
|
@@ -63,7 +66,7 @@ class Slurper
|
|
63
66
|
|
64
67
|
begin
|
65
68
|
handler.handle(story) do |status, message|
|
66
|
-
if status
|
69
|
+
if status
|
67
70
|
puts "Success: #{message}"
|
68
71
|
else
|
69
72
|
puts "Failed: #{message}"
|
data/lib/story.rb
CHANGED
@@ -2,47 +2,75 @@
|
|
2
2
|
# It's only concern is to store and format yaml data from stories
|
3
3
|
class YamlStory
|
4
4
|
|
5
|
-
attr_reader :name, :story_type, :requested_by
|
6
|
-
|
7
5
|
def initialize(attributes = {}, defaults = {})
|
8
|
-
# delete empty values
|
9
|
-
attributes.delete_if
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
end
|
14
|
-
|
15
|
-
@name = attributes['name']
|
16
|
-
@description = attributes['description']
|
17
|
-
@story_type = attributes['story_type']
|
18
|
-
@labels = attributes['labels']
|
19
|
-
@requested_by = attributes['requested_by']
|
6
|
+
# delete empty values
|
7
|
+
attributes.delete_if do |key, val|
|
8
|
+
val == '' && defaults.key?(key)
|
9
|
+
end
|
10
|
+
@attributes = defaults.merge(attributes)
|
20
11
|
end
|
21
12
|
|
22
13
|
def labels
|
23
|
-
|
24
|
-
@labels.split(',').map { |label| label.strip }
|
14
|
+
(@attributes['labels'] || '').split(',').map(&:strip)
|
25
15
|
end
|
26
16
|
|
27
17
|
def description
|
28
|
-
|
18
|
+
description = @attributes['description'] || ''
|
19
|
+
if description == ''
|
20
|
+
nil
|
21
|
+
else
|
22
|
+
description.gsub(' ', '').gsub(" \n", "\n")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def key?(key)
|
27
|
+
@attributes.key?(key.to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
def [](key)
|
31
|
+
raise "Attribute #{key} not defined" unless @attributes.key?(key.to_s)
|
32
|
+
|
33
|
+
if respond_to?(key.to_sym)
|
34
|
+
send(key.to_sym)
|
35
|
+
else
|
36
|
+
@attributes[key.to_s] == '' ? nil : @attributes[key.to_s]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def []=(key, value)
|
41
|
+
@attributes[key.to_s] = value
|
42
|
+
end
|
43
|
+
|
44
|
+
def method_missing(symbol)
|
45
|
+
self[symbol]
|
46
|
+
end
|
29
47
|
|
30
|
-
|
48
|
+
def to_hash
|
49
|
+
@attributes.keys.inject({}) do |hash, key|
|
50
|
+
hash[key] = self[key]
|
51
|
+
hash
|
52
|
+
end
|
31
53
|
end
|
32
54
|
|
33
55
|
def yaml_description
|
34
|
-
|
35
|
-
|
56
|
+
description = @attributes['description']
|
57
|
+
if description.nil? || description.empty?
|
58
|
+
''
|
59
|
+
else
|
60
|
+
description.gsub(/^/, ' ')
|
61
|
+
end
|
36
62
|
end
|
37
63
|
|
38
64
|
def to_yaml
|
39
|
-
|
65
|
+
<<-yaml
|
66
|
+
==
|
40
67
|
story_type: #{story_type}
|
41
|
-
name:
|
68
|
+
name: "#{name.gsub('"', "'")}"
|
42
69
|
description:
|
43
70
|
#{yaml_description}
|
44
71
|
labels: #{labels.join(', ')}
|
45
|
-
|
72
|
+
==
|
73
|
+
yaml
|
46
74
|
end
|
47
75
|
|
48
|
-
end
|
76
|
+
end
|
data/spec/jira_spec.rb
CHANGED
@@ -3,37 +3,72 @@ require 'slurper'
|
|
3
3
|
require 'story'
|
4
4
|
require 'jira'
|
5
5
|
|
6
|
-
describe
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
end
|
12
|
-
|
13
|
-
it 'should take the project key from config' do
|
14
|
-
result = @mapper.map(@story, 'AB')
|
15
|
-
result[:fields][:project][:key].should == 'AB'
|
6
|
+
describe Jira::StoryMapper do
|
7
|
+
let(:story) do
|
8
|
+
YamlStory.new('project' => 'Test', 'name' => 'Story name', 'story_type' => 'Feature',
|
9
|
+
'description' => 'Story desc', 'labels' => 'a, b',
|
10
|
+
'reporter' => 'User', 'assignee' => 'A guy')
|
16
11
|
end
|
12
|
+
let(:mapper) { described_class.new }
|
13
|
+
|
14
|
+
describe '#map' do
|
15
|
+
subject { mapper.map(story) }
|
16
|
+
|
17
|
+
context 'default mappings' do
|
18
|
+
it do
|
19
|
+
should == {
|
20
|
+
:fields => {
|
21
|
+
project: {key: 'Test'},
|
22
|
+
summary: 'Story name',
|
23
|
+
issuetype: {name: 'Feature'},
|
24
|
+
description: 'Story desc',
|
25
|
+
labels: %w(a b),
|
26
|
+
reporter: {name: 'User'},
|
27
|
+
assignee: {name: 'A guy'},
|
28
|
+
}
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
17
32
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
33
|
+
context 'extra mappings' do
|
34
|
+
before do
|
35
|
+
mapper.configure!(:mappings => {
|
36
|
+
test: {:type => 'TextField'},
|
37
|
+
something: {:field_name => 'Cici', :type => 'TextField'}
|
38
|
+
})
|
39
|
+
story.tap do |s|
|
40
|
+
s['test'] = 'Hello'
|
41
|
+
s['something'] = 'World!'
|
42
|
+
end
|
43
|
+
end
|
22
44
|
|
23
|
-
|
24
|
-
|
25
|
-
|
45
|
+
it do
|
46
|
+
should == {
|
47
|
+
:fields => {
|
48
|
+
project: {key: 'Test'},
|
49
|
+
summary: 'Story name',
|
50
|
+
issuetype: {name: 'Feature'},
|
51
|
+
description: 'Story desc',
|
52
|
+
labels: %w(a b),
|
53
|
+
reporter: {name: 'User'},
|
54
|
+
assignee: {name: 'A guy'},
|
55
|
+
test: 'Hello',
|
56
|
+
Cici: 'World!'
|
57
|
+
}
|
58
|
+
}
|
59
|
+
end
|
60
|
+
end
|
26
61
|
end
|
27
62
|
end
|
28
63
|
|
29
|
-
describe
|
64
|
+
describe Jira::ApiConsumer do
|
30
65
|
it 'should post with correctly configured the url' do
|
31
66
|
good_response = double(success?: true, body: '{"id": 1, "key": "TEST-1", "self":"http://localhost:8090/rest/api/2/issue/1"}')
|
32
67
|
|
33
|
-
api =
|
68
|
+
api = described_class.new
|
34
69
|
api.configure!('http://server', 'user', 'pass', '2')
|
35
70
|
|
36
|
-
api.
|
71
|
+
expect(api).to receive(:post).with('http://server/rest/api/2/issue/', 'json', kind_of(Hash)).and_return good_response
|
37
72
|
|
38
73
|
api.create_issue(double(to_json: 'json'))
|
39
74
|
|
@@ -43,33 +78,32 @@ describe JiraApi do
|
|
43
78
|
bad_response = double(success?: false, status: 401)
|
44
79
|
issue = double(to_json: 'json')
|
45
80
|
|
46
|
-
api =
|
81
|
+
api = described_class.new
|
47
82
|
api.configure!('http://server', 'user', 'pass', '2')
|
48
|
-
api.should_receive(:post).and_return(bad_response)
|
49
83
|
|
84
|
+
expect(api).to receive(:post).and_return(bad_response)
|
50
85
|
expect { api.create_issue(issue) }.to raise_error Exception
|
51
|
-
|
52
86
|
end
|
53
87
|
|
54
88
|
end
|
55
89
|
|
56
|
-
describe Jira do
|
90
|
+
describe Jira::Handler do
|
57
91
|
|
58
92
|
before do
|
59
|
-
@jira =
|
93
|
+
@jira = described_class.new
|
60
94
|
end
|
61
95
|
|
62
96
|
context '#supports' do
|
63
97
|
it 'should support the jira config if tracker is given' do
|
64
|
-
@jira.supports?('tracker' => 'jira').
|
98
|
+
expect(@jira.supports?('tracker' => 'jira')).to be true
|
65
99
|
end
|
66
100
|
|
67
101
|
it 'should not support if tracker is missing' do
|
68
|
-
@jira.supports?({}).
|
102
|
+
expect(@jira.supports?({})).to be false
|
69
103
|
end
|
70
104
|
|
71
105
|
it 'should not support if tracker is anything else' do
|
72
|
-
@jira.supports?('tracker' => 'pivotal').
|
106
|
+
expect(@jira.supports?('tracker' => 'pivotal')).to be false
|
73
107
|
end
|
74
108
|
end
|
75
109
|
|
@@ -83,23 +117,23 @@ describe Jira do
|
|
83
117
|
context 'good config provided' do
|
84
118
|
before do
|
85
119
|
required = [:username, :password, :project, :url]
|
86
|
-
@raw_config = Hash[required.map {|v| [v, 'value']}
|
120
|
+
@raw_config = Hash[required.map { |v| [v, 'value'] }]
|
87
121
|
end
|
88
122
|
|
89
123
|
it 'should pass if the required fields are present' do
|
90
124
|
config = @jira.configure! @raw_config
|
91
|
-
config
|
125
|
+
expect(config[:username]).to eq 'value'
|
92
126
|
end
|
93
127
|
|
94
128
|
it 'should return the default api version' do
|
95
129
|
config = @jira.configure! @raw_config
|
96
|
-
config[:api_version].
|
130
|
+
expect(config[:api_version]).to eq 'latest'
|
97
131
|
end
|
98
132
|
|
99
133
|
it 'should overwrite the default api_version' do
|
100
134
|
@raw_config[:api_version] = '2'
|
101
135
|
config = @jira.configure! @raw_config
|
102
|
-
config[:api_version].
|
136
|
+
expect(config[:api_version]).to eq '2'
|
103
137
|
end
|
104
138
|
end
|
105
139
|
end
|
@@ -118,16 +152,16 @@ describe Jira do
|
|
118
152
|
|
119
153
|
it 'should create a json issue using the api' do
|
120
154
|
issue = double(to_json: 'json')
|
121
|
-
mapper = double(map: issue)
|
155
|
+
mapper = double(map: issue, configure!: true)
|
122
156
|
|
123
157
|
api = double()
|
124
|
-
api.
|
125
|
-
api.
|
158
|
+
expect(api).to receive(:configure!).with('http://host', 'user', 'pass', 'latest')
|
159
|
+
expect(api).to receive(:create_issue).with(issue).and_return([true, '{"id": 1, "key": "TEST-1", "self":"http://localhost:8090/rest/api/2/issue/1"}'])
|
126
160
|
|
127
|
-
handler =
|
161
|
+
handler = described_class.new(mapper, api)
|
128
162
|
handler.configure! @config
|
129
163
|
handler.handle @story do |status, response|
|
130
|
-
expect(status).to
|
164
|
+
expect(status).to be true
|
131
165
|
expect(response).to include('TEST-1')
|
132
166
|
end
|
133
167
|
end
|
data/spec/pivotal_spec.rb
CHANGED
@@ -10,48 +10,12 @@ describe Pivotal do
|
|
10
10
|
|
11
11
|
describe '#supports?' do
|
12
12
|
it 'should support pivotal if project_id exists' do
|
13
|
-
@tracker.supports?({'project_id' => 12345}).
|
13
|
+
expect(@tracker.supports?({'project_id' => 12345})).to be true
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'should support pivotal if specified' do
|
17
|
-
@tracker.supports?({'tracker' => 'pivotal'}).
|
17
|
+
expect(@tracker.supports?({'tracker' => 'pivotal'})).to be true
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
21
|
end
|
22
|
-
|
23
|
-
#describe Story do
|
24
|
-
#
|
25
|
-
# before do
|
26
|
-
# fixture = File.join(File.dirname(__FILE__), "fixtures", "slurper_config_pivotal.yml")
|
27
|
-
# Story.configure(YAML.load_file fixture)
|
28
|
-
# end
|
29
|
-
#
|
30
|
-
# context "#prepare" do
|
31
|
-
#
|
32
|
-
# it "uses http by default" do
|
33
|
-
# Story.scheme.should == "http"
|
34
|
-
# Story.config['ssl'].should be_nil
|
35
|
-
# end
|
36
|
-
#
|
37
|
-
# it "uses https if set in the config" do
|
38
|
-
# Story.configure({"ssl" => true})
|
39
|
-
#
|
40
|
-
# Story.config['ssl'].should be_true
|
41
|
-
# Story.scheme.should == "https"
|
42
|
-
# Story.ssl_options[:verify_mode].should == 1
|
43
|
-
#
|
44
|
-
# # Not sure what this next line is testing
|
45
|
-
# File.open(File.expand_path('lib/cacert.pem')).readlines.find_all{ |l| l.starts_with?("Equifax") }.count.should == 4
|
46
|
-
# end
|
47
|
-
#
|
48
|
-
# it "sets the api token from config" do
|
49
|
-
# Story.headers.should == {"X-TrackerToken"=>"123abc123abc123abc123abc"}
|
50
|
-
# end
|
51
|
-
# end
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#end
|
data/spec/slurper_spec.rb
CHANGED
@@ -4,7 +4,7 @@ require 'slurper'
|
|
4
4
|
describe Slurper do
|
5
5
|
|
6
6
|
context "#create_stories" do
|
7
|
-
let(:stories) { [YamlStory.new(
|
7
|
+
let(:stories) { [YamlStory.new('name' => 'A story')] }
|
8
8
|
let(:config) { {:tracker => 'jira'} }
|
9
9
|
let(:slurper) { Slurper.new(config, stories) }
|
10
10
|
let(:handler) { double('handler') }
|
@@ -19,37 +19,37 @@ describe Slurper do
|
|
19
19
|
end
|
20
20
|
|
21
21
|
it "should detect the handler by checking the config" do
|
22
|
-
handler.
|
23
|
-
handler.
|
24
|
-
handler.
|
22
|
+
expect(handler).to receive(:supports?).with(config).and_return(true)
|
23
|
+
expect(handler).to receive(:configure!).and_return(true)
|
24
|
+
expect(handler).to receive(:handle)
|
25
25
|
|
26
26
|
slurper.create_stories
|
27
27
|
end
|
28
28
|
|
29
29
|
it "should call configure on the handler" do
|
30
|
-
handler.
|
31
|
-
handler.
|
30
|
+
expect(handler).to receive(:supports?).with(config).and_return(true)
|
31
|
+
expect(handler).to receive(:configure!).with(config)
|
32
32
|
|
33
33
|
slurper.create_stories
|
34
34
|
end
|
35
35
|
|
36
36
|
it "should delegate to the proper handler" do
|
37
37
|
jira = double
|
38
|
-
jira.
|
38
|
+
expect(jira).to receive(:supports?).with(config).and_return(false)
|
39
39
|
|
40
40
|
pivotal = double
|
41
|
-
pivotal.
|
42
|
-
pivotal.
|
43
|
-
pivotal.
|
41
|
+
expect(pivotal).to receive(:supports?).with(config).and_return(true)
|
42
|
+
expect(pivotal).to receive(:configure!).and_return(true)
|
43
|
+
expect(pivotal).to receive(:handle).with(stories[0])
|
44
44
|
|
45
45
|
slurper.handlers = [jira, pivotal]
|
46
46
|
slurper.create_stories
|
47
47
|
end
|
48
48
|
|
49
49
|
it "should fail gracefully on handler exceptions" do
|
50
|
-
handler.
|
51
|
-
handler.
|
52
|
-
handler.
|
50
|
+
allow(handler).to receive(:supports?).and_return(true)
|
51
|
+
expect(handler).to receive(:configure!).and_return(true)
|
52
|
+
allow(handler).to receive(:handle).and_raise('An error')
|
53
53
|
|
54
54
|
expect { slurper.create_stories }.not_to raise_error
|
55
55
|
end
|
@@ -61,9 +61,9 @@ describe Slurper do
|
|
61
61
|
config_file = File.join(File.dirname(__FILE__), "fixtures", "slurper_config_pivotal.yml")
|
62
62
|
|
63
63
|
handler = double
|
64
|
-
handler.
|
65
|
-
handler.
|
66
|
-
handler.
|
64
|
+
allow(handler).to receive(:supports?).and_return true
|
65
|
+
allow(handler).to receive(:configure!).and_return true
|
66
|
+
expect(handler).to receive(:handle).once
|
67
67
|
|
68
68
|
Slurper.slurp(story_file, config_file, [handler], false)
|
69
69
|
end
|
@@ -80,7 +80,7 @@ describe Slurper do
|
|
80
80
|
let(:config) { {'requested_by' => 'John Doe'} }
|
81
81
|
|
82
82
|
it 'should load default values' do
|
83
|
-
story.requested_by.
|
83
|
+
expect(story.requested_by).to eq 'John Doe'
|
84
84
|
end
|
85
85
|
end
|
86
86
|
|
@@ -88,7 +88,7 @@ describe Slurper do
|
|
88
88
|
let(:story_file) { 'whitespacey_story.slurper' }
|
89
89
|
|
90
90
|
it "strips whitespace from the name" do
|
91
|
-
story.name.
|
91
|
+
expect(story.name).to eq "Profit"
|
92
92
|
end
|
93
93
|
end
|
94
94
|
|
@@ -96,15 +96,15 @@ describe Slurper do
|
|
96
96
|
let(:story_file) { 'full_story.slurper' }
|
97
97
|
|
98
98
|
it "parses the name correctly" do
|
99
|
-
story.name.
|
99
|
+
expect(story.name).to eq 'Profit'
|
100
100
|
end
|
101
101
|
|
102
102
|
it "parses the label correctly" do
|
103
|
-
story.labels.
|
103
|
+
expect(story.labels).to eq ['money','power','fame']
|
104
104
|
end
|
105
105
|
|
106
106
|
it "parses the story type correctly" do
|
107
|
-
story.story_type.
|
107
|
+
expect(story.story_type).to eq 'feature'
|
108
108
|
end
|
109
109
|
end
|
110
110
|
|
@@ -113,7 +113,7 @@ describe Slurper do
|
|
113
113
|
let(:story_file) { 'name_only.slurper' }
|
114
114
|
|
115
115
|
it "should parse the name correctly" do
|
116
|
-
story.name.
|
116
|
+
expect(story.name).to eq "Profit"
|
117
117
|
end
|
118
118
|
|
119
119
|
end
|
@@ -123,11 +123,11 @@ describe Slurper do
|
|
123
123
|
let(:story_file) { 'empty_attributes.slurper' }
|
124
124
|
|
125
125
|
it "should not set any name" do
|
126
|
-
story.name.
|
126
|
+
expect(story.name).to be_nil
|
127
127
|
end
|
128
128
|
|
129
129
|
it "should not set any labels" do
|
130
|
-
story.labels.
|
130
|
+
expect(story.labels).to eq []
|
131
131
|
end
|
132
132
|
end
|
133
133
|
end
|
data/spec/story_spec.rb
CHANGED
@@ -6,28 +6,27 @@ describe YamlStory do
|
|
6
6
|
it "should return a default value if it's set" do
|
7
7
|
story = YamlStory.new({}, 'description' => 'Fill description', 'requested_by' => 'John Doe')
|
8
8
|
|
9
|
-
story.description.
|
10
|
-
story.requested_by.
|
9
|
+
expect(story.description).to eq 'Fill description'
|
10
|
+
expect(story.requested_by).to eq 'John Doe'
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
14
|
context "#requested_by" do
|
15
15
|
it "should return nil when value is blank" do
|
16
16
|
story = YamlStory.new('requested_by' => '')
|
17
|
-
story.requested_by.
|
17
|
+
expect(story.requested_by).to be_nil
|
18
18
|
end
|
19
|
-
|
20
19
|
end
|
21
20
|
|
22
21
|
context "#description" do
|
23
22
|
it "when the description is blank" do
|
24
23
|
story = YamlStory.new('description' => '', 'name' => 'test')
|
25
|
-
story.description.
|
24
|
+
expect(story.description).to be_nil
|
26
25
|
end
|
27
26
|
|
28
27
|
it "when there is no description given" do
|
29
28
|
story = YamlStory.new
|
30
|
-
story.description.
|
29
|
+
expect(story.description).to be_nil
|
31
30
|
end
|
32
31
|
|
33
32
|
it "when it contains quotes" do
|
@@ -35,7 +34,7 @@ describe YamlStory do
|
|
35
34
|
I have a "quote"
|
36
35
|
STRING
|
37
36
|
story = YamlStory.new('description' => desc)
|
38
|
-
story.description.
|
37
|
+
expect(story.description).to eq "I have a \"quote\"\n"
|
39
38
|
end
|
40
39
|
|
41
40
|
it "when it is full of whitespace" do
|
@@ -45,7 +44,7 @@ describe YamlStory do
|
|
45
44
|
I want to click a thingy
|
46
45
|
STRING
|
47
46
|
story = YamlStory.new('description' => desc)
|
48
|
-
story.description.
|
47
|
+
expect(story.description).to eq "In order to do something\nAs a role\nI want to click a thingy\n"
|
49
48
|
end
|
50
49
|
|
51
50
|
it "when it contains acceptance criteria" do
|
@@ -59,24 +58,76 @@ describe YamlStory do
|
|
59
58
|
- don't forget the other thing
|
60
59
|
STRING
|
61
60
|
story = YamlStory.new('description' => desc)
|
62
|
-
story.description.
|
61
|
+
expect(story.description).to eq "In order to do something\nAs a role\nI want to click a thingy\n\nAcceptance:\n- do the thing\n- don't forget the other thing\n"
|
63
62
|
end
|
64
63
|
end
|
65
64
|
|
66
65
|
context 'default attributes' do
|
67
66
|
it 'uses the default if not given one' do
|
68
67
|
story = YamlStory.new({}, {'requested_by' => 'Mr. Client'})
|
69
|
-
story.requested_by.
|
68
|
+
expect(story.requested_by).to eq 'Mr. Client'
|
70
69
|
end
|
71
70
|
|
72
71
|
it 'uses the default if given a blank requested_by' do
|
73
72
|
story = YamlStory.new({'requested_by' => ''}, {'requested_by' => 'Mr. Client'})
|
74
|
-
story.requested_by.
|
73
|
+
expect(story.requested_by).to eq 'Mr. Client'
|
75
74
|
end
|
76
75
|
|
77
76
|
it 'uses the name given in the story file if there is one' do
|
78
77
|
story = YamlStory.new({'requested_by' => 'Mr. Stakeholder'}, {'requested_by' => 'Mr. Client'})
|
79
|
-
story.requested_by.
|
78
|
+
expect(story.requested_by).to eq 'Mr. Stakeholder'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context '#key?' do
|
83
|
+
subject { YamlStory.new({'name' => 'Test', 'description' => 'Desc', 'requested_by' => 'Mr. Client'}).key?(key) }
|
84
|
+
|
85
|
+
context 'with existing key' do
|
86
|
+
let(:key) { 'name' }
|
87
|
+
it { should be true }
|
88
|
+
end
|
89
|
+
|
90
|
+
context 'with other key' do
|
91
|
+
let(:key) { 'missing' }
|
92
|
+
it { should be false }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context '#[]' do
|
97
|
+
subject { YamlStory.new({'name' => 'Test', 'description' => 'Desc', 'requested_by' => 'Mr. Client'}) }
|
98
|
+
|
99
|
+
it 'return defined attributes' do
|
100
|
+
expect(subject['name']).to eq 'Test'
|
101
|
+
expect(subject['description']).to eq 'Desc'
|
102
|
+
expect(subject['requested_by']).to eq 'Mr. Client'
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'raise exception for undefined attributes' do
|
106
|
+
expect { subject['something'] }.to raise_exception
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'attributes accessed as methods' do
|
111
|
+
subject { YamlStory.new({'name' => 'Test', 'description' => 'Desc', 'requested_by' => 'Mr. Client'}) }
|
112
|
+
|
113
|
+
it 'return defined attributes' do
|
114
|
+
expect(subject.name).to eq 'Test'
|
115
|
+
expect(subject.description).to eq 'Desc'
|
116
|
+
expect(subject.requested_by).to eq 'Mr. Client'
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'raise exception for undefined attributes' do
|
120
|
+
expect { subject.somethig }.to raise_exception
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
context '#to_hash' do
|
125
|
+
let(:attrs) do
|
126
|
+
{'name' => 'Test', 'description' => 'Desc', 'requested_by' => 'Mr. Client'}
|
80
127
|
end
|
128
|
+
|
129
|
+
subject { YamlStory.new(attrs).to_hash }
|
130
|
+
|
131
|
+
it { should == attrs }
|
81
132
|
end
|
82
|
-
end
|
133
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jira-slurper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.3.
|
4
|
+
version: 1.3.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wes Gibbs
|
@@ -12,7 +12,7 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: bin
|
14
14
|
cert_chain: []
|
15
|
-
date: 2014-
|
15
|
+
date: 2014-08-05 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: faraday
|
@@ -62,14 +62,28 @@ dependencies:
|
|
62
62
|
requirements:
|
63
63
|
- - ~>
|
64
64
|
- !ruby/object:Gem::Version
|
65
|
-
version:
|
65
|
+
version: 3.0.0
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
68
|
version_requirements: !ruby/object:Gem::Requirement
|
69
69
|
requirements:
|
70
70
|
- - ~>
|
71
71
|
- !ruby/object:Gem::Version
|
72
|
-
version:
|
72
|
+
version: 3.0.0
|
73
|
+
- !ruby/object:Gem::Dependency
|
74
|
+
name: rspec-its
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ~>
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 1.0.1
|
80
|
+
type: :development
|
81
|
+
prerelease: false
|
82
|
+
version_requirements: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ~>
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: 1.0.1
|
73
87
|
description: "\n Slurps stories from the given file (stories.slurper by default)
|
74
88
|
and creates Pivotal Tracker stories from them. Useful during story carding sessions
|
75
89
|
when you want to capture a number of stories quickly without clicking your way through
|
@@ -85,6 +99,7 @@ files:
|
|
85
99
|
- bin/slurp
|
86
100
|
- lib/cacert.pem
|
87
101
|
- lib/jira.rb
|
102
|
+
- lib/jira/inflectors.rb
|
88
103
|
- lib/pivotal.rb
|
89
104
|
- lib/slurper.rb
|
90
105
|
- lib/story.rb
|