jira-slurper 1.3.1 → 1.3.6
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 +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
|