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
@@ -0,0 +1,117 @@
1
+ JIRA_2_PROJECT = {
2
+ "key" => "TST",
3
+ "name" => "Test",
4
+ "id" => "10000",
5
+ "self" => "http://localhost:8080/rest/api/2/project/TST",
6
+ "assigneeType" => "PROJECT_LEAD",
7
+ "components" => [],
8
+ "versions" => [],
9
+ "issueTypes" => [
10
+ {
11
+ "name" => "Bug",
12
+ "iconUrl" => "http://localhost:8080/images/icons/bug.gif",
13
+ "self" => "http://localhost:8080/rest/api/2/issuetype/1",
14
+ "id" => "1",
15
+ "subtask" => false,
16
+ "description" => "A problem which impairs or prevents the functions of the product.",
17
+ },
18
+ {
19
+ "name" => "New Feature",
20
+ "iconUrl" => "http://localhost:8080/images/icons/newfeature.gif",
21
+ "self" => "http://localhost:8080/rest/api/2/issuetype/2",
22
+ "id" => "2",
23
+ "subtask" => false,
24
+ "description" => "A new feature of the product, which has yet to be developed.",
25
+ },
26
+ {
27
+ "name" => "Task",
28
+ "iconUrl" => "http://localhost:8080/images/icons/task.gif",
29
+ "self" => "http://localhost:8080/rest/api/2/issuetype/3",
30
+ "id" => "3",
31
+ "subtask" => false,
32
+ "description" => "A task that needs to be done.",
33
+ },
34
+ {
35
+ "name" => "Improvement",
36
+ "iconUrl" => "http://localhost:8080/images/icons/improvement.gif",
37
+ "self" => "http://localhost:8080/rest/api/2/issuetype/4",
38
+ "id" => "4",
39
+ "subtask" => false,
40
+ "description" => "An improvement or enhancement to an existing feature or task.",
41
+ },
42
+ {
43
+ "name" => "Sub-task",
44
+ "iconUrl" => "http://localhost:8080/images/icons/issue_subtask.gif",
45
+ "self" => "http://localhost:8080/rest/api/2/issuetype/5",
46
+ "id" => "5",
47
+ "subtask" => true,
48
+ "description" => "The sub-task of the issue",
49
+ },
50
+ ],
51
+ "avatarUrls" => {
52
+ "48x48" => "http://localhost:8080/secure/projectavatar?pid=10000&avatarId=10011",
53
+ "16x16" => "http://localhost:8080/secure/projectavatar?size=small&pid=10000&avatarId=10011",
54
+ },
55
+ "roles" => {
56
+ "Developers" => "http://localhost:8080/rest/api/2/project/TST/role/10001",
57
+ "Administrators" => "http://localhost:8080/rest/api/2/project/TST/role/10002",
58
+ "Users" => "http://localhost:8080/rest/api/2/project/TST/role/10000",
59
+ },
60
+ "lead" => {
61
+ "name" => "admin",
62
+ "self" => "http://localhost:8080/rest/api/2/user?username=admin",
63
+ "displayName" => "Administrator",
64
+ "active" => true,
65
+ "avatarUrls" => {
66
+ "48x48" => "http://localhost:8080/secure/useravatar?avatarId=10122",
67
+ "16x16" => "http://localhost:8080/secure/useravatar?size=small&avatarId=10122",
68
+ },
69
+ },
70
+ }
71
+
72
+ JIRA_2_ALPHA_PROJECT = {
73
+ "key" => "TST",
74
+ "name" => "Test",
75
+ "self" => "http://localhost:8080/rest/api/2.0.alpha1/project/TST",
76
+ "assigneeType" => "PROJECT_LEAD",
77
+ "components" => [],
78
+ "versions" => [],
79
+ "issueTypes" => [
80
+ {
81
+ "name" => "Bug",
82
+ "self" => "http://localhost:8080/rest/api/2.0.alpha1/issueType/1",
83
+ "subtask" => false,
84
+ },
85
+ {
86
+ "name" => "New Feature",
87
+ "self" => "http://localhost:8080/rest/api/2.0.alpha1/issueType/2",
88
+ "subtask" => false,
89
+ },
90
+ {
91
+ "name" => "Task",
92
+ "self" => "http://localhost:8080/rest/api/2.0.alpha1/issueType/3",
93
+ "subtask" => false,
94
+ },
95
+ {
96
+ "name" => "Improvement",
97
+ "self" => "http://localhost:8080/rest/api/2.0.alpha1/issueType/4",
98
+ "subtask" => false,
99
+ },
100
+ {
101
+ "name" => "Sub-task",
102
+ "self" => "http://localhost:8080/rest/api/2.0.alpha1/issueType/5",
103
+ "subtask" => true,
104
+ },
105
+ ],
106
+ "roles" => {
107
+ "Developers" => "http://localhost:8080/rest/api/2.0.alpha1/project/TST/role/10001",
108
+ "Administrators" => "http://localhost:8080/rest/api/2.0.alpha1/project/TST/role/10002",
109
+ "Users" => "http://localhost:8080/rest/api/2.0.alpha1/project/TST/role/10000",
110
+ },
111
+ "lead" => {
112
+ "name" => "admin",
113
+ "self" => "http://localhost:8080/rest/api/2.0.alpha1/user?username=admin",
114
+ "displayName" => "Administrator",
115
+ "active" => true,
116
+ },
117
+ }
@@ -0,0 +1,97 @@
1
+ {
2
+ "self": "http://www.example.com/jira/rest/api/2/project/TST",
3
+ "id": "10000",
4
+ "key": "TST",
5
+ "description": "This project was created as an example for REST.",
6
+ "lead": {
7
+ "self": "http://www.example.com/jira/rest/api/2/user?username=fred",
8
+ "name": "fred",
9
+ "avatarUrls": {
10
+ "24x24": "http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred",
11
+ "16x16": "http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred",
12
+ "32x32": "http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred",
13
+ "48x48": "http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred"
14
+ },
15
+ "displayName": "Fred F. User",
16
+ "active": false
17
+ },
18
+ "components": [
19
+ {
20
+ "self": "http://www.example.com/jira/rest/api/2/component/10000",
21
+ "id": "10000",
22
+ "name": "Component 1",
23
+ "description": "This is a JIRA component",
24
+ "lead": {
25
+ "self": "http://www.example.com/jira/rest/api/2/user?username=fred",
26
+ "name": "fred",
27
+ "avatarUrls": {
28
+ "24x24": "http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred",
29
+ "16x16": "http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred",
30
+ "32x32": "http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred",
31
+ "48x48": "http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred"
32
+ },
33
+ "displayName": "Fred F. User",
34
+ "active": false
35
+ },
36
+ "assigneeType": "PROJECT_LEAD",
37
+ "assignee": {
38
+ "self": "http://www.example.com/jira/rest/api/2/user?username=fred",
39
+ "name": "fred",
40
+ "avatarUrls": {
41
+ "24x24": "http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred",
42
+ "16x16": "http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred",
43
+ "32x32": "http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred",
44
+ "48x48": "http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred"
45
+ },
46
+ "displayName": "Fred F. User",
47
+ "active": false
48
+ },
49
+ "realAssigneeType": "PROJECT_LEAD",
50
+ "realAssignee": {
51
+ "self": "http://www.example.com/jira/rest/api/2/user?username=fred",
52
+ "name": "fred",
53
+ "avatarUrls": {
54
+ "24x24": "http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred",
55
+ "16x16": "http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred",
56
+ "32x32": "http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred",
57
+ "48x48": "http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred"
58
+ },
59
+ "displayName": "Fred F. User",
60
+ "active": false
61
+ },
62
+ "isAssigneeTypeValid": false
63
+ }
64
+ ],
65
+ "issueTypes": [
66
+ {
67
+ "self": "http://localhost:8090/jira/rest/api/2.0/issueType/3",
68
+ "id": "3",
69
+ "description": "A task that needs to be done.",
70
+ "iconUrl": "http://localhost:8090/jira/images/icons/issuetypes/task.png",
71
+ "name": "Task",
72
+ "subtask": false
73
+ },
74
+ {
75
+ "self": "http://localhost:8090/jira/rest/api/2.0/issueType/1",
76
+ "id": "1",
77
+ "description": "A problem with the software.",
78
+ "iconUrl": "http://localhost:8090/jira/images/icons/issuetypes/bug.png",
79
+ "name": "Bug",
80
+ "subtask": false
81
+ }
82
+ ],
83
+ "url": "http://www.example.com/jira/browse/TST",
84
+ "email": "from-jira@example.com",
85
+ "assigneeType": "PROJECT_LEAD",
86
+ "versions": [],
87
+ "name": "Example",
88
+ "roles": {
89
+ "Developers": "http://www.example.com/jira/rest/api/2/project/TST/role/10000"
90
+ },
91
+ "avatarUrls": {
92
+ "24x24": "http://www.example.com/jira/secure/projectavatar?size=small&pid=10000",
93
+ "16x16": "http://www.example.com/jira/secure/projectavatar?size=xsmall&pid=10000",
94
+ "32x32": "http://www.example.com/jira/secure/projectavatar?size=medium&pid=10000",
95
+ "48x48": "http://www.example.com/jira/secure/projectavatar?size=large&pid=10000"
96
+ }
97
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "expand": "names,schema",
3
+ "startAt": 0,
4
+ "maxResults": 50,
5
+ "total": 3,
6
+ "issues": [
7
+ {
8
+ "key": "TST-1",
9
+ "id": "1001",
10
+ "self": "http://www.example.com/jira/rest/api/2/issue/1001",
11
+ "expand": ""
12
+ },
13
+ {
14
+ "key": "TST-2",
15
+ "id": "1002",
16
+ "self": "http://www.example.com/jira/rest/api/2/issue/1002",
17
+ "expand": ""
18
+ },
19
+ {
20
+ "key": "TST-3",
21
+ "id": "1003",
22
+ "self": "http://www.example.com/jira/rest/api/2/issue/1003",
23
+ "expand": ""
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,20 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe Jiraby::Entity do
4
+ before(:each) do
5
+ @ent = Jiraby::Entity.new(json_data('issue_10002.json'))
6
+ end
7
+
8
+ describe '#key' do
9
+ it "passes through to Hash#key if arguments are included" do
10
+ @ent.key('10002').should == 'id'
11
+ @ent.key('TST-1').should == 'key'
12
+ end
13
+
14
+ it "returns the value in the issue's `key` field if argument is omitted" do
15
+ @ent.key.should == 'TST-1'
16
+ end
17
+ end
18
+
19
+ end
20
+
@@ -0,0 +1,289 @@
1
+ require_relative 'spec_helper'
2
+ require_relative 'data/jira_issues'
3
+
4
+ describe Jiraby::Issue do
5
+ before(:each) do
6
+ @field_mapping = {
7
+ 'description' => 'Description',
8
+ 'customfield_10001' => 'Custom Field',
9
+ }
10
+ @jira = Jiraby::Jira.new('jira.example.com', 'username', 'password')
11
+ @jira.stub(:field_mapping => @field_mapping)
12
+ @issue = Jiraby::Issue.new(@jira, json_data('issue_10002.json'))
13
+ end
14
+
15
+ context "Class methods" do
16
+ end # Class methods
17
+
18
+ context "Instance methods" do
19
+ describe '#initialize' do
20
+ it "accepts a JSON hash structure" do
21
+ json = json_data('issue_10002.json')
22
+ issue = Jiraby::Issue.new(@jira, json)
23
+ issue.data.should == json
24
+ end
25
+ end
26
+
27
+ describe "#[]" do
28
+ it "accepts field `id`" do
29
+ @issue['description'].should == "example bug report"
30
+ end
31
+
32
+ it "accepts field `name`" do
33
+ @issue['Custom Field'].should == "custom field value"
34
+ end
35
+
36
+ it "returns the value set by `#[]=`" do
37
+ @issue['description'] = "Foobar"
38
+ @issue['description'].should == "Foobar"
39
+ end
40
+
41
+ it "raises an exception on invalid field name" do
42
+ lambda do
43
+ @issue['bogus']
44
+ end.should raise_error(
45
+ Jiraby::InvalidField, /Invalid field name or ID: bogus/)
46
+ end
47
+ end
48
+
49
+ describe "#[]=" do
50
+ it "accepts field `id`" do
51
+ @issue['description'] = "Foobar"
52
+ @issue.pending_changes['description'].should == "Foobar"
53
+ end
54
+
55
+ it "accepts field `name`" do
56
+ @issue['Custom Field'] = "Modified"
57
+ @issue.pending_changes[@field_mapping.key('Custom Field')].should == "Modified"
58
+ end
59
+
60
+ it "raises an exception on invalid field name" do
61
+ lambda do
62
+ @issue['bogus'] = "Modified"
63
+ end.should raise_error(
64
+ Jiraby::InvalidField, /Invalid field name or ID: bogus/)
65
+ end
66
+ end
67
+
68
+ describe "#field_id" do
69
+ before(:each) do
70
+ @ids_and_names = {
71
+ 'description' => 'Description',
72
+ 'sub-tasks' => 'Sub-Tasks',
73
+ 'project' => 'Project',
74
+ }
75
+ @jira.stub(:field_mapping => @ids_and_names)
76
+ end
77
+
78
+ it "returns ID as-is" do
79
+ @ids_and_names.keys.each do |id|
80
+ @issue.field_id(id).should == id
81
+ end
82
+ end
83
+
84
+ it "returns the ID for a given name" do
85
+ @ids_and_names.each do |id, name|
86
+ @issue.field_id(name).should == id
87
+ end
88
+ end
89
+
90
+ it "raises an exception when invalid name or ID is given" do
91
+ lambda do
92
+ @issue.field_id("Completely Bogus")
93
+ end.should raise_error(/Invalid field name or ID/)
94
+ end
95
+ end
96
+
97
+ describe "#key" do
98
+ it "returns nil if the key is undefined" do
99
+ issue = Jiraby::Issue.new(@jira, {})
100
+ issue.key.should be_nil
101
+ end
102
+
103
+ it "returns the key field from the issue's data" do
104
+ @issue.key.should == @issue.data['key']
105
+ end
106
+ end
107
+
108
+ describe "#editmeta" do
109
+ it "requests editmeta for the current issue" do
110
+ @jira.should_receive(:get).
111
+ with("issue/#{@issue.key}/editmeta").
112
+ and_return({})
113
+ @issue.editmeta
114
+ end
115
+ end
116
+
117
+ describe "#pending_changes?" do
118
+ it "returns true if updates are pending" do
119
+ @issue["description"] = "Foo"
120
+ @issue.pending_changes?.should be_true
121
+ end
122
+
123
+ it "returns false if no updates are pending" do
124
+ @issue.pending_changes?.should be_false
125
+ end
126
+ end
127
+
128
+ describe "#save!" do
129
+ it "sends a PUT request to Jira with updates" do
130
+ @issue['description'] = "Modified description"
131
+ expect_fields = {
132
+ 'fields' => @issue.pending_changes
133
+ }
134
+ @jira.should_receive(:put).
135
+ with("issue/#{@issue.key}", expect_fields)
136
+ @issue.save!
137
+ end
138
+
139
+ it "updates the data attribute with the new changes" do
140
+ @jira.stub(:put)
141
+ original_description = @issue['description']
142
+ modified_description = "Modified description"
143
+
144
+ @issue.data.fields.description.should == original_description
145
+ @issue['description'] = modified_description
146
+ @issue.save!
147
+ @issue.data.fields.description.should == modified_description
148
+ end
149
+
150
+ it "resets pending_changes" do
151
+ @jira.stub(:put => nil)
152
+ @issue['description'] = "Modified description"
153
+ @issue.pending_changes.should_not be_empty
154
+ @issue.pending_changes?.should be_true
155
+ @issue.save!
156
+ @issue.pending_changes.should be_empty
157
+ @issue.pending_changes?.should be_false
158
+ end
159
+ end
160
+
161
+ describe "#has_field?" do
162
+ before(:each) do
163
+ @ids_and_names = {
164
+ 'description' => 'Description',
165
+ 'sub-tasks' => 'Sub-Tasks',
166
+ 'project' => 'Project',
167
+ }
168
+ @jira.stub(:field_mapping => @ids_and_names)
169
+ end
170
+
171
+ it "true when issue has a field with the given ID" do
172
+ @ids_and_names.keys.each do |id|
173
+ @issue.has_field?(id).should be_true
174
+ end
175
+ end
176
+
177
+ it "true when issue has a field with the given name" do
178
+ @ids_and_names.values.each do |name|
179
+ @issue.has_field?(name).should be_true
180
+ end
181
+ end
182
+
183
+ it "false when issue has a no field with the given ID or name" do
184
+ @issue.has_field?("Completely Bogus").should be_false
185
+ end
186
+ end #has_field?
187
+
188
+ describe "#is_subtask?" do
189
+ it "true when issuetype.subtask is true" do
190
+ data = { 'fields' => { 'issuetype' => {'subtask' => true} } }
191
+ issue = Jiraby::Issue.new(@jira, data)
192
+ issue.is_subtask?.should be_true
193
+ end
194
+
195
+ it "false when issuetype.subtask is false" do
196
+ data = { 'fields' => { 'issuetype' => {'subtask' => false} } }
197
+ issue = Jiraby::Issue.new(@jira, data)
198
+ issue.is_subtask?.should be_false
199
+ end
200
+
201
+ it "false when issuetype.subtask is not set" do
202
+ data = { 'fields' => { 'issuetype' => {} } }
203
+ issue = Jiraby::Issue.new(@jira, data)
204
+ issue.is_subtask?.should be_false
205
+ end
206
+ end #is_subtask?
207
+
208
+ describe "#is_assigned?" do
209
+ it "true when assignee is set" do
210
+ data = { 'fields' => { 'assignee' => {'name' => 'someone'} } }
211
+ issue = Jiraby::Issue.new(@jira, data)
212
+ issue.is_assigned?.should be_true
213
+ end
214
+
215
+ it "false when assignee is nil" do
216
+ data = { 'fields' => { 'assignee' => nil } }
217
+ issue = Jiraby::Issue.new(@jira, data)
218
+ issue.is_assigned?.should be_false
219
+ end
220
+ end #is_assigned?
221
+
222
+ describe "#parent" do
223
+ it "returns the parent key when issue is a subtask" do
224
+ parent_key = 'FOO-234'
225
+ data = {
226
+ 'fields' => {
227
+ 'parent' => {'key' => parent_key},
228
+ 'issuetype' => {'subtask' => true},
229
+ }
230
+ }
231
+ issue = Jiraby::Issue.new(@jira, data)
232
+ issue.parent.should == parent_key
233
+ end
234
+
235
+ it "returns nil when issue is not a subtask" do
236
+ data = {
237
+ 'fields' => {
238
+ 'issuetype' => {'subtask' => false},
239
+ }
240
+ }
241
+ issue = Jiraby::Issue.new(@jira, data)
242
+ issue.parent.should be_nil
243
+ end
244
+ end #parent
245
+
246
+ describe "#subtasks" do
247
+ it "returns an array of subtask keys" do
248
+ subtask_keys = ['ST-01', 'ST-02', 'ST-03']
249
+ data = {
250
+ 'fields' => {
251
+ 'subtasks' => [
252
+ {'key' => 'ST-01'},
253
+ {'key' => 'ST-02'},
254
+ {'key' => 'ST-03'},
255
+ ]
256
+ }
257
+ }
258
+ issue = Jiraby::Issue.new(@jira, data)
259
+ issue.subtasks.should == subtask_keys
260
+ end
261
+
262
+ it "returns an empty array if the issue has no subtasks" do
263
+ data = {
264
+ 'fields' => {
265
+ 'subtasks' => []
266
+ }
267
+ }
268
+ issue = Jiraby::Issue.new(@jira, data)
269
+ issue.subtasks.should == []
270
+ end
271
+ end #subtasks
272
+
273
+ describe "#field_ids" do
274
+ it "returns a sorted array of the issue's field IDs" do
275
+ data = {
276
+ 'fields' => {
277
+ 'foo' => 'x',
278
+ 'bar' => 'y',
279
+ 'baz' => 'z',
280
+ }
281
+ }
282
+ issue = Jiraby::Issue.new(@jira, data)
283
+ issue.field_ids.should == ['bar', 'baz', 'foo']
284
+ end
285
+ end #field_ids
286
+
287
+ end # Instance methods"
288
+ end
289
+