jiraby 0.0.1

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.
Files changed (51) hide show
  1. data/.gitignore +9 -0
  2. data/.rspec +3 -0
  3. data/.travis.yml +4 -0
  4. data/.yardopts +8 -0
  5. data/Gemfile +7 -0
  6. data/README.md +132 -0
  7. data/Rakefile +5 -0
  8. data/docs/development.md +20 -0
  9. data/docs/history.md +5 -0
  10. data/docs/ideas.md +54 -0
  11. data/docs/index.md +11 -0
  12. data/docs/usage.md +64 -0
  13. data/jiraby.gemspec +31 -0
  14. data/lib/jiraby.rb +8 -0
  15. data/lib/jiraby/entity.rb +21 -0
  16. data/lib/jiraby/exceptions.rb +8 -0
  17. data/lib/jiraby/issue.rb +109 -0
  18. data/lib/jiraby/jira.rb +319 -0
  19. data/lib/jiraby/json_resource.rb +136 -0
  20. data/lib/jiraby/project.rb +19 -0
  21. data/spec/data/field.json +32 -0
  22. data/spec/data/issue_10002.json +187 -0
  23. data/spec/data/issue_createmeta.json +35 -0
  24. data/spec/data/jira_issues.rb +265 -0
  25. data/spec/data/jira_projects.rb +117 -0
  26. data/spec/data/project_TST.json +97 -0
  27. data/spec/data/search_results.json +26 -0
  28. data/spec/entity_spec.rb +20 -0
  29. data/spec/issue_spec.rb +289 -0
  30. data/spec/jira_spec.rb +314 -0
  31. data/spec/json_resource_spec.rb +222 -0
  32. data/spec/mockapp/config.ru +6 -0
  33. data/spec/mockapp/index.html +10 -0
  34. data/spec/mockapp/jira.rb +61 -0
  35. data/spec/mockapp/views/auth/login_failed.erb +1 -0
  36. data/spec/mockapp/views/auth/login_success.erb +7 -0
  37. data/spec/mockapp/views/error.erb +3 -0
  38. data/spec/mockapp/views/field.erb +32 -0
  39. data/spec/mockapp/views/issue/TST-1.erb +186 -0
  40. data/spec/mockapp/views/issue/createmeta.erb +35 -0
  41. data/spec/mockapp/views/issue/err_nonexistent.erb +1 -0
  42. data/spec/mockapp/views/project/TST.erb +97 -0
  43. data/spec/mockapp/views/project/err_nonexistent.erb +4 -0
  44. data/spec/mockapp/views/search.erb +26 -0
  45. data/spec/project_spec.rb +20 -0
  46. data/spec/spec_helper.rb +26 -0
  47. data/tasks/mockjira.rake +10 -0
  48. data/tasks/pry.rake +28 -0
  49. data/tasks/spec.rake +9 -0
  50. data/tasks/test.rake +8 -0
  51. metadata +288 -0
data/spec/jira_spec.rb ADDED
@@ -0,0 +1,314 @@
1
+ require_relative 'spec_helper'
2
+ require_relative 'data/jira_issues'
3
+
4
+ describe Jiraby::Jira do
5
+ before(:each) do
6
+ @jira = Jiraby::Jira.new('localhost:9292', 'username', 'password')
7
+ todo_stub = RuntimeError.new("RestClient call needs a stub")
8
+ RestClient.stub(:get).and_raise(todo_stub)
9
+ RestClient.stub(:post).and_raise(todo_stub)
10
+ end
11
+
12
+ describe '#initialize' do
13
+ before(:each) do
14
+ end
15
+
16
+ it "raises an error for unknown API version" do
17
+ lambda do
18
+ Jiraby::Jira.new('jira.example.com', nil, nil, '1.0')
19
+ end.should raise_error
20
+ end
21
+
22
+ it "accepts valid API versions" do
23
+ jira = Jiraby::Jira.new('jira.example.com', nil, nil, '2')
24
+ jira.api_version.should == '2'
25
+ end
26
+
27
+ it "accepts URL beginning with http://" do
28
+ jira = Jiraby::Jira.new('http://jira.example.com', nil, nil)
29
+ jira.url.should == 'http://jira.example.com'
30
+ end
31
+
32
+ it "accepts URL beginning with https://" do
33
+ jira = Jiraby::Jira.new('https://jira.example.com', nil, nil)
34
+ jira.url.should == 'https://jira.example.com'
35
+ end
36
+
37
+ it "prepends http:// to the URL if needed" do
38
+ jira = Jiraby::Jira.new('jira.example.com', nil, nil)
39
+ jira.url.should == 'http://jira.example.com'
40
+ end
41
+ end #initialize
42
+
43
+ describe '#auth_url' do
44
+ it "returns the full REST authorization URL" do
45
+ jira = Jiraby::Jira.new('jira.example.com', nil, nil)
46
+ jira.auth_url.should == 'http://jira.example.com/rest/auth/1/session'
47
+ end
48
+ end #auth_url
49
+
50
+ describe '#not_implemented_in' do
51
+ it "raises an exception when API version is one of those listed" do
52
+ jira = Jiraby::Jira.new('jira.example.com', nil, nil, '2')
53
+ lambda do
54
+ jira.not_implemented_in('Issue creation', '2')
55
+ end.should raise_error
56
+ end
57
+
58
+ it "returns nil when API version is not one of those listed" do
59
+ jira = Jiraby::Jira.new('jira.example.com', nil, nil, '2')
60
+ jira.not_implemented_in('Issue creation', '2.0.alpha1').should be_nil
61
+ end
62
+ end #not_implemented_in
63
+
64
+ context "REST wrappers" do
65
+ before(:each) do
66
+ @path = 'fake/path'
67
+ @resource = Jiraby::JSONResource.new(@jira.base_url)
68
+ @jira.rest.stub(:[]).with(@path).and_return(@resource)
69
+ end
70
+
71
+ describe "#_path_with_query" do
72
+ it "returns path as-is if query is empty" do
73
+ @jira._path_with_query("user/search").should == "user/search"
74
+ end
75
+
76
+ it "returns path with query parameters appended" do
77
+ path = "user/search"
78
+ query = {:username => "someone", :startAt => 0, :maxResults => 10}
79
+ expect_path = "user/search?username=someone&startAt=0&maxResults=10"
80
+ @jira._path_with_query(path, query).should == expect_path
81
+ end
82
+ end
83
+
84
+ describe "#get" do
85
+ it "sends a GET request" do
86
+ @resource.should_receive(:get)
87
+ @jira.get(@path)
88
+ end
89
+ end
90
+
91
+ describe "#put" do
92
+ it "sends a PUT request" do
93
+ @resource.should_receive(:put)
94
+ @jira.put(@path, {})
95
+ end
96
+ end
97
+
98
+ describe "#post" do
99
+ it "sends a POST request" do
100
+ @resource.should_receive(:post)
101
+ @jira.post(@path, {})
102
+ end
103
+ end
104
+
105
+ describe "#delete" do
106
+ it "sends a DELETE request" do
107
+ @resource.should_receive(:delete)
108
+ @jira.delete(@path)
109
+ end
110
+ end
111
+ end # REST wrappers
112
+
113
+ describe '#issue' do
114
+ it "returns an Issue for valid issue key" do
115
+ @jira.issue('TST-1').should be_a Jiraby::Issue
116
+ end
117
+
118
+ it "raises ArgumentError if key is nil" do
119
+ lambda do
120
+ @jira.issue(nil)
121
+ end.should raise_error(ArgumentError, /Issue key is required/)
122
+ end
123
+
124
+ it "raises ArgumentError if key is empty" do
125
+ lambda do
126
+ @jira.issue(' ')
127
+ end.should raise_error(ArgumentError, /Issue key is required/)
128
+ end
129
+
130
+ it "raises IssueNotFound for invalid issue key" do
131
+ lambda do
132
+ @jira.issue('BOGUS-429')
133
+ end.should raise_error(Jiraby::IssueNotFound, /Issue 'BOGUS-429' not found/)
134
+ end
135
+ end #issue
136
+
137
+ describe '#create_issue' do
138
+ before(:each) do
139
+ @response = {
140
+ "id" => "10000",
141
+ "key" => "TST-24",
142
+ "self" => "http://www.example.com/jira/rest/api/2/issue/10000"
143
+ }
144
+ @response_json = Yajl::Encoder.encode(@response)
145
+ end
146
+
147
+ it "sends a POST request to the Jira API" do
148
+ #@jira.resource.should_receive(:post).and_return(@response_json)
149
+ @jira.create_issue('TST', 'Bug')
150
+ end
151
+
152
+ it "returns a Jiraby::Issue" do
153
+ RestClient.stub(:post => @response_json)
154
+ end
155
+ end #create_issue
156
+
157
+ describe "#enumerator" do
158
+ it "returns an Enumerator instance" do
159
+ enum = @jira.enumerator(:get, 'user/search')
160
+ enum.should be_an Enumerator
161
+ end
162
+
163
+ it "gets multiple pages by incrementing `startAt`" do
164
+ page1 = (1..50).map { |num| Jiraby::Entity.new(:key => "TST-#{num}") }
165
+ page2 = (51..100).map { |num| Jiraby::Entity.new(:key => "TST-#{num}") }
166
+ page3 = (101..129).map { |num| Jiraby::Entity.new(:key => "TST-#{num}") }
167
+ jql = 'project=TST'
168
+ params = {:jql => jql, :maxResults => 50}
169
+ @jira.should_receive(:post).
170
+ with('search', params.merge(:startAt => 0)).
171
+ once.and_return(page1)
172
+ @jira.should_receive(:post).
173
+ with('search', params.merge(:startAt => 50)).
174
+ once.and_return(page2)
175
+ @jira.should_receive(:post).
176
+ with('search', params.merge(:startAt => 100)).
177
+ once.and_return(page3)
178
+
179
+ items = @jira.enumerator(:post, 'search', {:jql => jql}).to_a
180
+ end
181
+
182
+ it "works when REST method returns an Entity" do
183
+ entity = Jiraby::Entity.new(
184
+ :issues => [
185
+ {:key => 'TST-1'},
186
+ {:key => 'TST-2'},
187
+ {:key => 'TST-3'},
188
+ ]
189
+ )
190
+ @jira.stub(:post).and_return(entity)
191
+
192
+ enum = @jira.enumerator(:post, 'fake_search', {}, 'issues')
193
+ enum.count.should == 3
194
+ end
195
+
196
+ it "works when REST method returns an Array of Entity" do
197
+ issue_keys = ['TST-1', 'TST-2', 'TST-3']
198
+ entities = issue_keys.map {|key| Jiraby::Entity.new(:key => key)}
199
+ @jira.stub(:post).and_return(entities)
200
+
201
+ enum = @jira.enumerator(:post, 'fake_search')
202
+ enum.count.should == 3
203
+ enum.to_a.should == entities
204
+ end
205
+
206
+ it "supports the .next method" do
207
+ # FIXME: For some reason, .next works fine in this test, but when
208
+ # connected to an actual Jira instance, it blows up with
209
+ # SystemStackError: stack level too deep
210
+ issue_keys = ['TST-1', 'TST-2', 'TST-3']
211
+ entities = issue_keys.map {|key| Jiraby::Entity.new(:key => key)}
212
+ @jira.stub(:post).and_return(entities)
213
+
214
+ enum = @jira.enumerator(:post, 'fake_search')
215
+ enum.next.key.should == 'TST-1'
216
+ enum.next.key.should == 'TST-2'
217
+ enum.next.key.should == 'TST-3'
218
+ end
219
+
220
+ it "raises an exception if response is not Entity or Array" do
221
+ @jira.stub(:post).and_return('this string')
222
+ enum = @jira.enumerator(:post, 'fake_search')
223
+ lambda do
224
+ enum.first
225
+ end.should raise_error(RuntimeError, /Unexpected data: this string/)
226
+ end
227
+ end
228
+
229
+ # TODO: Populate some more test issues in order to properly test this
230
+ describe '#search' do
231
+ before(:each) do
232
+ @jira.stub(:issue => Jiraby::Issue.new(@jira))
233
+ end
234
+
235
+ it "something or other" do
236
+ response = @jira.post(:search, {:jql => 'project=FOO'})
237
+ end
238
+
239
+ it "returns an Enumerator" do
240
+ require 'enumerator'
241
+ @jira.search('project=TST').should be_an Enumerator
242
+ end
243
+
244
+ it "yields one Issue instance for each issue key" do
245
+ @jira.search('project=TST').each do |issue|
246
+ issue.should be_a Jiraby::Issue
247
+ end
248
+ end
249
+ end #search
250
+
251
+ # FIXME: Test this using the fake Jira instance
252
+ describe '#count' do
253
+ it "returns the number of issues matching a JQL query" do
254
+ search_results = Jiraby::Entity.new({'total' => 5})
255
+ @jira.should_receive(:post).
256
+ with('search', anything).
257
+ and_return(search_results)
258
+ @jira.count('key = TST-1').should == 5
259
+ end
260
+
261
+ it "returns a count of all issues when JQL is empty" do
262
+ search_results = Jiraby::Entity.new({'total' => 15})
263
+ @jira.stub(:post).and_return(search_results)
264
+
265
+ @jira.count('').should == 15
266
+ end
267
+ end #count
268
+
269
+ describe '#project' do
270
+ before(:each) do
271
+ #@jira.resource.stub(:get).and_return({})
272
+ #@jira.resource.stub(:get).with('project/TST').and_return(json_data('project_TST.json'))
273
+ end
274
+
275
+ it "returns project data" do
276
+ project = @jira.project('TST')
277
+ project.should be_a Jiraby::Project
278
+ # TODO: Verify attributes (requires fleshing out Project class)
279
+ end
280
+
281
+ it "raises ProjectNotFound if the project is not found" do
282
+ lambda do
283
+ @jira.project('BOGUS')
284
+ end.should raise_error(Jiraby::ProjectNotFound, /Project 'BOGUS' not found/)
285
+ end
286
+ end #project
287
+
288
+ describe '#project_meta' do
289
+ it "returns the project createmeta info if the project exists" do
290
+ meta = @jira.project_meta('TST')
291
+ expect_keys = ['name', 'self', 'issuetypes', 'id', 'avatarUrls', 'key']
292
+ meta.keys.sort.should == expect_keys.sort
293
+ end
294
+
295
+ it "raises ProjectNotFound if the project doesn't exist" do
296
+ RestClient.stub(:get).and_raise(RestClient::ResourceNotFound)
297
+ lambda do
298
+ @jira.project_meta('BOGUS')
299
+ end.should raise_error(Jiraby::ProjectNotFound, /Project 'BOGUS' not found/)
300
+ end
301
+ end #project_meta
302
+
303
+ describe '#field_mapping' do
304
+ it "returns a mapping of field IDs to names" do
305
+ @jira.field_mapping.should == {
306
+ 'description' => "Description",
307
+ 'summary' => "Summary",
308
+ 'customfield_123' => "My Field",
309
+ }
310
+ end
311
+ end #field_mapping
312
+
313
+ end
314
+
@@ -0,0 +1,222 @@
1
+ require_relative 'spec_helper'
2
+ require 'jiraby/jira'
3
+ require 'jiraby/json_resource'
4
+
5
+ describe Jiraby::JSONResource do
6
+ before(:each) do
7
+ @jr = Jiraby::JSONResource.new('http://example.com')
8
+ end
9
+
10
+ describe "#initialize" do
11
+ # Ensure that `options` contains appropriate JSON headers
12
+ def should_include_json_headers(options)
13
+ options.should include(:headers)
14
+ options[:headers].should include(:content_type)
15
+ options[:headers][:content_type].should == :json
16
+ options[:headers].should include(:accept)
17
+ options[:headers][:accept].should == :json
18
+ end
19
+
20
+ it "returns a JSONResource instance" do
21
+ jr = Jiraby::JSONResource.new('http://example.com')
22
+ jr.should be_a Jiraby::JSONResource
23
+ end
24
+
25
+ it "sets options[:headers] to send/receive JSON" do
26
+ jr = Jiraby::JSONResource.new('http://example.com')
27
+ should_include_json_headers(jr.options)
28
+ end
29
+
30
+ it "merges additional options" do
31
+ options = {:headers => {:foo => 'bar'}, :other => 'x'}
32
+ jr = Jiraby::JSONResource.new('http://example.com', options)
33
+ should_include_json_headers(jr.options)
34
+ jr.options.should include(:other)
35
+ jr.options[:other].should == 'x'
36
+ jr.options[:headers].should include(:foo)
37
+ jr.options[:headers][:foo].should == 'bar'
38
+ end
39
+ end #initialize
40
+
41
+ describe "#[]" do
42
+ it "returns a JSONResource instance" do
43
+ @jr['subpath'].should be_a Jiraby::JSONResource
44
+ end
45
+ end #[]
46
+
47
+ describe "#get" do
48
+ it "invokes #wrap with :_head" do
49
+ @jr.should_receive(:wrap).with(:_get, {})
50
+ @jr.get({})
51
+ end
52
+ end #get
53
+
54
+ describe "#delete" do
55
+ it "invokes #wrap with :_delete" do
56
+ @jr.should_receive(:wrap).with(:_delete, {})
57
+ @jr.delete({})
58
+ end
59
+ end #delete
60
+
61
+ describe "#head" do
62
+ it "invokes #wrap with :_head" do
63
+ @jr.should_receive(:wrap).with(:_head, {})
64
+ @jr.head({})
65
+ end
66
+ end #head
67
+
68
+ describe "#post" do
69
+ it "invokes #wrap_with_payload with :_post" do
70
+ @jr.should_receive(:wrap_with_payload).with(:_post, {}, {})
71
+ @jr.post({}, {})
72
+ end
73
+ end #post
74
+
75
+ describe "#put" do
76
+ it "invokes #wrap_with_payload with :_put" do
77
+ @jr.should_receive(:wrap_with_payload).with(:_put, {}, {})
78
+ @jr.put({}, {})
79
+ end
80
+ end #put
81
+
82
+ describe "#patch" do
83
+ it "invokes #wrap_with_payload with :_patch" do
84
+ @jr.should_receive(:wrap_with_payload).with(:_patch, {}, {})
85
+ @jr.patch({}, {})
86
+ end
87
+ end #patch
88
+
89
+ describe "#wrap" do
90
+ before(:each) do
91
+ @headers = {}
92
+ end
93
+
94
+ it "invokes a REST method with additional headers and block" do
95
+ @jr.should_receive(:_get).with(@headers).and_return('{}')
96
+ @jr.wrap(:_get, @headers)
97
+ end
98
+
99
+ it "returns the parsed JSON response as a hash" do
100
+ response_hash = {"status" => "ok"}
101
+ @jr.should_receive(:_get).
102
+ with(@headers).
103
+ and_return(response_hash.to_json)
104
+ result = @jr.wrap(:_get, @headers)
105
+ result.should == response_hash
106
+ end
107
+
108
+ it "when RestClient::Exception occurs, returns exception response as a hash" do
109
+ error_hash = {"error" => "Error message"}
110
+ exception = RestClient::Exception.new
111
+ exception.response = error_hash.to_json
112
+ got_response = @jr.wrap(:_get, {}) do
113
+ raise exception
114
+ end
115
+ got_response.should == error_hash
116
+ end
117
+ end #wrap
118
+
119
+ describe "#wrap_with_payload" do
120
+ before(:each) do
121
+ @payload = {"name" => "Foo"}
122
+ @headers = {}
123
+ end
124
+
125
+ it "when payload is a hash, it's encoded as JSON" do
126
+ @jr.should_receive(:_put).with(@payload.to_json, @headers).and_return('{}')
127
+ @jr.wrap_with_payload(:_put, @payload, @headers)
128
+ end
129
+
130
+ it "when payload is already a JSON string, it's sent as-is" do
131
+ json_payload = @payload.to_json
132
+ @jr.should_receive(:_put).with(json_payload, @headers).and_return('{}')
133
+ @jr.wrap_with_payload(:_put, json_payload, @headers)
134
+ end
135
+
136
+ it "invokes a REST method with additional headers and block" do
137
+ @headers['extra'] = 'something'
138
+ @jr.should_receive(:_put).with(@payload.to_json, @headers).and_return('{}')
139
+ @jr.wrap_with_payload(:_put, @payload, @headers)
140
+ end
141
+
142
+ it "returns the parsed JSON response as a hash" do
143
+ json_payload = @payload.to_json
144
+ response_hash = {"status" => "ok"}
145
+ @jr.should_receive(:_put).
146
+ with(json_payload, @headers).
147
+ and_return(response_hash.to_json)
148
+ result = @jr.wrap_with_payload(:_put, json_payload, @headers)
149
+ result.should == response_hash
150
+ end
151
+
152
+ it "when RestClient::Exception occurs, returns exception response as a hash" do
153
+ error_hash = {"error" => "Error message"}
154
+ exception = RestClient::Exception.new
155
+ exception.response = error_hash.to_json
156
+ got_response = @jr.wrap_with_payload(:_put, {}, {}) do
157
+ raise exception
158
+ end
159
+ got_response.should == error_hash
160
+ end
161
+ end #wrap_with_payload
162
+
163
+ describe "#parsed_response" do
164
+ it "returns a Hash if the response is a JSON object" do
165
+ hash = {
166
+ 'foo' => 'bar',
167
+ 'nested' => {
168
+ 'a' => 'z',
169
+ 'x' => 'y',
170
+ }
171
+ }
172
+ json = Yajl::Encoder.encode(hash)
173
+ @jr.parsed_response(json).should == hash
174
+ end
175
+
176
+ it "returns an Array of Hashes if the response is a JSON object array" do
177
+ hash_array = [{'foo' => 'bar'}, {'goo' => 'car'}]
178
+ json = Yajl::Encoder.encode(hash_array)
179
+ @jr.parsed_response(json).should == hash_array
180
+ end
181
+
182
+ it "returns nil if the response is an empty string" do
183
+ @jr.parsed_response("").should be_nil
184
+ end
185
+
186
+ it "raises JSONParseError when parsing fails" do
187
+ lambda do
188
+ @jr.parsed_response('bogus json')
189
+ end.should raise_error(Jiraby::JSONParseError)
190
+ end
191
+ end #parsed_response
192
+
193
+ describe "#maybe_error_response" do
194
+ it "yields the block return value if no exception occurs" do
195
+ expect_response = "The normalexpected response"
196
+ got_response = @jr.maybe_error_response do
197
+ expect_response
198
+ end
199
+ got_response.should == expect_response
200
+ end
201
+
202
+ it "re-raises RestClient::RequestTimeout" do
203
+ exception = RestClient::RequestTimeout.new
204
+ lambda do
205
+ @jr.maybe_error_response do
206
+ raise exception
207
+ end
208
+ end.should raise_error(exception)
209
+ end
210
+
211
+ it "yields the exception's response if a RestClient::Excception occurs" do
212
+ exception = RestClient::Exception.new
213
+ exception.response = "The exception response"
214
+ got_response = @jr.maybe_error_response do
215
+ raise exception
216
+ end
217
+ got_response.should == exception.response
218
+ end
219
+ end #maybe_error_response
220
+
221
+ end # describe Jiraby::JSONResource
222
+