jira-ruby 2.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (154) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.travis.yml +9 -0
  4. data/Gemfile +14 -0
  5. data/Guardfile +14 -0
  6. data/LICENSE +19 -0
  7. data/README.md +427 -0
  8. data/Rakefile +31 -0
  9. data/example.rb +224 -0
  10. data/http-basic-example.rb +113 -0
  11. data/jira-ruby.gemspec +35 -0
  12. data/lib/jira-ruby.rb +49 -0
  13. data/lib/jira/base.rb +525 -0
  14. data/lib/jira/base_factory.rb +46 -0
  15. data/lib/jira/client.rb +308 -0
  16. data/lib/jira/has_many_proxy.rb +42 -0
  17. data/lib/jira/http_client.rb +112 -0
  18. data/lib/jira/http_error.rb +14 -0
  19. data/lib/jira/jwt_client.rb +67 -0
  20. data/lib/jira/oauth_client.rb +114 -0
  21. data/lib/jira/railtie.rb +10 -0
  22. data/lib/jira/request_client.rb +31 -0
  23. data/lib/jira/resource/agile.rb +79 -0
  24. data/lib/jira/resource/applinks.rb +39 -0
  25. data/lib/jira/resource/attachment.rb +50 -0
  26. data/lib/jira/resource/board.rb +91 -0
  27. data/lib/jira/resource/board_configuration.rb +9 -0
  28. data/lib/jira/resource/comment.rb +12 -0
  29. data/lib/jira/resource/component.rb +8 -0
  30. data/lib/jira/resource/createmeta.rb +44 -0
  31. data/lib/jira/resource/field.rb +83 -0
  32. data/lib/jira/resource/filter.rb +15 -0
  33. data/lib/jira/resource/issue.rb +141 -0
  34. data/lib/jira/resource/issuelink.rb +20 -0
  35. data/lib/jira/resource/issuelinktype.rb +14 -0
  36. data/lib/jira/resource/issuetype.rb +8 -0
  37. data/lib/jira/resource/priority.rb +8 -0
  38. data/lib/jira/resource/project.rb +41 -0
  39. data/lib/jira/resource/rapidview.rb +67 -0
  40. data/lib/jira/resource/remotelink.rb +26 -0
  41. data/lib/jira/resource/resolution.rb +8 -0
  42. data/lib/jira/resource/serverinfo.rb +18 -0
  43. data/lib/jira/resource/sprint.rb +105 -0
  44. data/lib/jira/resource/sprint_report.rb +8 -0
  45. data/lib/jira/resource/status.rb +8 -0
  46. data/lib/jira/resource/transition.rb +29 -0
  47. data/lib/jira/resource/user.rb +30 -0
  48. data/lib/jira/resource/version.rb +8 -0
  49. data/lib/jira/resource/watcher.rb +35 -0
  50. data/lib/jira/resource/webhook.rb +37 -0
  51. data/lib/jira/resource/worklog.rb +14 -0
  52. data/lib/jira/tasks.rb +0 -0
  53. data/lib/jira/version.rb +3 -0
  54. data/lib/tasks/generate.rake +18 -0
  55. data/spec/integration/attachment_spec.rb +32 -0
  56. data/spec/integration/comment_spec.rb +52 -0
  57. data/spec/integration/component_spec.rb +39 -0
  58. data/spec/integration/field_spec.rb +32 -0
  59. data/spec/integration/issue_spec.rb +93 -0
  60. data/spec/integration/issuelinktype_spec.rb +26 -0
  61. data/spec/integration/issuetype_spec.rb +24 -0
  62. data/spec/integration/priority_spec.rb +24 -0
  63. data/spec/integration/project_spec.rb +49 -0
  64. data/spec/integration/rapidview_spec.rb +74 -0
  65. data/spec/integration/resolution_spec.rb +26 -0
  66. data/spec/integration/status_spec.rb +24 -0
  67. data/spec/integration/transition_spec.rb +49 -0
  68. data/spec/integration/user_spec.rb +41 -0
  69. data/spec/integration/version_spec.rb +39 -0
  70. data/spec/integration/watcher_spec.rb +62 -0
  71. data/spec/integration/webhook.rb +25 -0
  72. data/spec/integration/worklog_spec.rb +51 -0
  73. data/spec/jira/base_factory_spec.rb +45 -0
  74. data/spec/jira/base_spec.rb +598 -0
  75. data/spec/jira/client_spec.rb +291 -0
  76. data/spec/jira/has_many_proxy_spec.rb +46 -0
  77. data/spec/jira/http_client_spec.rb +328 -0
  78. data/spec/jira/http_error_spec.rb +24 -0
  79. data/spec/jira/jwt_uri_builder_spec.rb +59 -0
  80. data/spec/jira/oauth_client_spec.rb +162 -0
  81. data/spec/jira/request_client_spec.rb +41 -0
  82. data/spec/jira/resource/agile_spec.rb +135 -0
  83. data/spec/jira/resource/attachment_spec.rb +138 -0
  84. data/spec/jira/resource/board_spec.rb +224 -0
  85. data/spec/jira/resource/createmeta_spec.rb +258 -0
  86. data/spec/jira/resource/field_spec.rb +85 -0
  87. data/spec/jira/resource/filter_spec.rb +97 -0
  88. data/spec/jira/resource/issue_spec.rb +227 -0
  89. data/spec/jira/resource/issuelink_spec.rb +14 -0
  90. data/spec/jira/resource/project_factory_spec.rb +11 -0
  91. data/spec/jira/resource/project_spec.rb +123 -0
  92. data/spec/jira/resource/sprint_spec.rb +90 -0
  93. data/spec/jira/resource/user_factory_spec.rb +31 -0
  94. data/spec/jira/resource/worklog_spec.rb +22 -0
  95. data/spec/mock_responses/board/1.json +33 -0
  96. data/spec/mock_responses/board/1_issues.json +62 -0
  97. data/spec/mock_responses/component.post.json +28 -0
  98. data/spec/mock_responses/component/10000.invalid.put.json +5 -0
  99. data/spec/mock_responses/component/10000.json +39 -0
  100. data/spec/mock_responses/component/10000.put.json +39 -0
  101. data/spec/mock_responses/empty_issues.json +8 -0
  102. data/spec/mock_responses/field.json +32 -0
  103. data/spec/mock_responses/field/1.json +15 -0
  104. data/spec/mock_responses/issue.json +1108 -0
  105. data/spec/mock_responses/issue.post.json +5 -0
  106. data/spec/mock_responses/issue/10002.invalid.put.json +6 -0
  107. data/spec/mock_responses/issue/10002.json +126 -0
  108. data/spec/mock_responses/issue/10002.put.missing_field_update.json +6 -0
  109. data/spec/mock_responses/issue/10002/attachments/10000.json +20 -0
  110. data/spec/mock_responses/issue/10002/comment.json +65 -0
  111. data/spec/mock_responses/issue/10002/comment.post.json +29 -0
  112. data/spec/mock_responses/issue/10002/comment/10000.json +29 -0
  113. data/spec/mock_responses/issue/10002/comment/10000.put.json +29 -0
  114. data/spec/mock_responses/issue/10002/transitions.json +49 -0
  115. data/spec/mock_responses/issue/10002/transitions.post.json +1 -0
  116. data/spec/mock_responses/issue/10002/watchers.json +13 -0
  117. data/spec/mock_responses/issue/10002/worklog.json +98 -0
  118. data/spec/mock_responses/issue/10002/worklog.post.json +30 -0
  119. data/spec/mock_responses/issue/10002/worklog/10000.json +31 -0
  120. data/spec/mock_responses/issue/10002/worklog/10000.put.json +30 -0
  121. data/spec/mock_responses/issueLinkType.json +25 -0
  122. data/spec/mock_responses/issueLinkType/10000.json +7 -0
  123. data/spec/mock_responses/issuetype.json +42 -0
  124. data/spec/mock_responses/issuetype/5.json +8 -0
  125. data/spec/mock_responses/jira/rest/webhooks/1.0/webhook.json +11 -0
  126. data/spec/mock_responses/jira/rest/webhooks/1.0/webhook/2.json +11 -0
  127. data/spec/mock_responses/priority.json +42 -0
  128. data/spec/mock_responses/priority/1.json +8 -0
  129. data/spec/mock_responses/project.json +12 -0
  130. data/spec/mock_responses/project/SAMPLEPROJECT.issues.json +1108 -0
  131. data/spec/mock_responses/project/SAMPLEPROJECT.json +84 -0
  132. data/spec/mock_responses/rapidview.json +10 -0
  133. data/spec/mock_responses/rapidview/SAMPLEPROJECT.issues.full.json +276 -0
  134. data/spec/mock_responses/rapidview/SAMPLEPROJECT.issues.json +111 -0
  135. data/spec/mock_responses/rapidview/SAMPLEPROJECT.json +6 -0
  136. data/spec/mock_responses/resolution.json +15 -0
  137. data/spec/mock_responses/resolution/1.json +7 -0
  138. data/spec/mock_responses/sprint/1_issues.json +125 -0
  139. data/spec/mock_responses/status.json +37 -0
  140. data/spec/mock_responses/status/1.json +7 -0
  141. data/spec/mock_responses/user_username=admin.json +17 -0
  142. data/spec/mock_responses/version.post.json +7 -0
  143. data/spec/mock_responses/version/10000.invalid.put.json +5 -0
  144. data/spec/mock_responses/version/10000.json +11 -0
  145. data/spec/mock_responses/version/10000.put.json +7 -0
  146. data/spec/mock_responses/webhook.json +11 -0
  147. data/spec/mock_responses/webhook/webhook.json +11 -0
  148. data/spec/spec_helper.rb +21 -0
  149. data/spec/support/clients_helper.rb +16 -0
  150. data/spec/support/matchers/have_attributes.rb +11 -0
  151. data/spec/support/matchers/have_many.rb +9 -0
  152. data/spec/support/matchers/have_one.rb +5 -0
  153. data/spec/support/shared_examples/integration.rb +177 -0
  154. metadata +491 -0
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe JIRA::Resource::User do
4
+ with_each_client do |site_url, client|
5
+ let(:client) { client }
6
+ let(:site_url) { site_url }
7
+
8
+ let(:key) { 'admin' }
9
+
10
+ let(:expected_attributes) do
11
+ {
12
+ 'self' => 'http://localhost:2990/jira/rest/api/2/user?username=admin',
13
+ 'name' => key,
14
+ 'emailAddress' => 'admin@example.com'
15
+ }
16
+ end
17
+
18
+ it_should_behave_like 'a resource'
19
+ it_should_behave_like 'a resource with a singular GET endpoint'
20
+
21
+ describe '#all' do
22
+ let(:client) do
23
+ client = double(options: { rest_base_path: '/jira/rest/api/2' })
24
+ allow(client).to receive(:get).with('/rest/api/2/user/search?username=_&maxResults=1000').and_return(JIRA::Resource::UserFactory.new(client))
25
+ client
26
+ end
27
+
28
+ before do
29
+ allow(client).to receive(:get)
30
+ .with('/rest/api/2/user/search?username=_&maxResults=1000') { OpenStruct.new(body: '["User1"]') }
31
+ allow(client).to receive_message_chain(:User, :build).with('users') { [] }
32
+ end
33
+
34
+ it 'gets users with maxResults of 1000' do
35
+ expect(client).to receive(:get).with('/rest/api/2/user/search?username=_&maxResults=1000')
36
+ expect(client).to receive_message_chain(:User, :build).with('User1')
37
+ JIRA::Resource::User.all(client)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe JIRA::Resource::Version do
4
+ with_each_client do |site_url, client|
5
+ let(:client) { client }
6
+ let(:site_url) { site_url }
7
+
8
+ let(:key) { '10000' }
9
+
10
+ let(:expected_attributes) do
11
+ {
12
+ 'self' => 'http://localhost:2990/jira/rest/api/2/version/10000',
13
+ 'id' => key,
14
+ 'description' => 'Initial version'
15
+ }
16
+ end
17
+
18
+ let(:attributes_for_post) do
19
+ { 'name' => '2.0', 'project' => 'SAMPLEPROJECT' }
20
+ end
21
+ let(:expected_attributes_from_post) do
22
+ { 'id' => '10001', 'name' => '2.0' }
23
+ end
24
+
25
+ let(:attributes_for_put) do
26
+ { 'name' => '2.0.0' }
27
+ end
28
+ let(:expected_attributes_from_put) do
29
+ { 'id' => '10000', 'name' => '2.0.0' }
30
+ end
31
+
32
+ it_should_behave_like 'a resource'
33
+ it_should_behave_like 'a resource with a singular GET endpoint'
34
+ it_should_behave_like 'a resource with a DELETE endpoint'
35
+ it_should_behave_like 'a resource with a POST endpoint'
36
+ it_should_behave_like 'a resource with a PUT endpoint'
37
+ it_should_behave_like 'a resource with a PUT endpoint that rejects invalid fields'
38
+ end
39
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ describe JIRA::Resource::Watcher do
4
+ with_each_client do |site_url, client|
5
+ let(:client) { client }
6
+ let(:site_url) { site_url }
7
+
8
+ let(:target) { JIRA::Resource::Watcher.new(client, attrs: { 'id' => '99999' }, issue_id: '10002') }
9
+
10
+ let(:belongs_to) do
11
+ JIRA::Resource::Issue.new(client, attrs: {
12
+ 'id' => '10002',
13
+ 'fields' => {
14
+ 'comment' => { 'comments' => [] }
15
+ }
16
+ })
17
+ end
18
+
19
+ let(:expected_attributes) do
20
+ {
21
+ 'self' => 'http://localhost:2990/jira/rest/api/2/issue/10002/watchers',
22
+ "isWatching": false,
23
+ "watchCount": 1,
24
+ "watchers": [
25
+ {
26
+ "self": 'http://www.example.com/jira/rest/api/2/user?username=admin',
27
+ "name": 'admin',
28
+ "displayName": 'admin',
29
+ "active": false
30
+ }
31
+ ]
32
+ }
33
+ end
34
+
35
+ describe 'watchers' do
36
+ before(:each) do
37
+ stub_request(:get, site_url + '/jira/rest/api/2/issue/10002')
38
+ .to_return(status: 200, body: get_mock_response('issue/10002.json'))
39
+
40
+ stub_request(:get, site_url + '/jira/rest/api/2/issue/10002/watchers')
41
+ .to_return(status: 200, body: get_mock_response('issue/10002/watchers.json'))
42
+
43
+ stub_request(:post, site_url + '/jira/rest/api/2/issue/10002/watchers')
44
+ .to_return(status: 204, body: nil)
45
+ end
46
+
47
+ it 'should returns all the watchers' do
48
+ issue = client.Issue.find('10002')
49
+ watchers = client.Watcher.all(options = { issue: issue })
50
+ expect(watchers.length).to eq(1)
51
+ end
52
+
53
+ it 'should add a watcher' do
54
+ issue = client.Issue.find('10002')
55
+ watcher = JIRA::Resource::Watcher.new(client, issue: issue)
56
+ user_id = "tester"
57
+ watcher.save!(user_id)
58
+ end
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe JIRA::Resource::Webhook do
4
+ with_each_client do |site_url, client|
5
+ let(:client) { client }
6
+ let(:site_url) { site_url }
7
+
8
+ let(:key) { '2' }
9
+
10
+ let(:expected_attributes) do
11
+ { 'name' => 'from API', 'url' => 'http://localhost:3000/webhooks/1', 'excludeBody' => false, 'filters' => { 'issue-related-events-section' => '' }, 'events' => [], 'enabled' => true, 'self' => 'http://localhost:2990/jira/rest/webhooks/1.0/webhook/2', 'lastUpdatedUser' => 'admin', 'lastUpdatedDisplayName' => 'admin', 'lastUpdated' => 1_453_306_520_188 }
12
+ end
13
+
14
+ let(:expected_collection_length) { 1 }
15
+
16
+ it_should_behave_like 'a resource'
17
+ it_should_behave_like 'a resource with a collection GET endpoint'
18
+ it_should_behave_like 'a resource with a singular GET endpoint'
19
+
20
+ it 'returns a collection of components' do
21
+ stub_request(:get, site_url + described_class.singular_path(client, key))
22
+ .to_return(status: 200, body: get_mock_response('webhook/webhook.json'))
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe JIRA::Resource::Worklog do
4
+ with_each_client do |site_url, client|
5
+ let(:client) { client }
6
+ let(:site_url) { site_url }
7
+
8
+ let(:key) { '10000' }
9
+
10
+ let(:target) { JIRA::Resource::Worklog.new(client, attrs: { 'id' => '99999' }, issue_id: '54321') }
11
+
12
+ let(:expected_collection_length) { 3 }
13
+
14
+ let(:belongs_to) do
15
+ JIRA::Resource::Issue.new(client, attrs: {
16
+ 'id' => '10002', 'fields' => {
17
+ 'comment' => { 'comments' => [] }
18
+ }
19
+ })
20
+ end
21
+
22
+ let(:expected_attributes) do
23
+ {
24
+ 'self' => 'http://localhost:2990/jira/rest/api/2/issue/10002/worklog/10000',
25
+ 'id' => key,
26
+ 'comment' => 'Some epic work.'
27
+ }
28
+ end
29
+
30
+ let(:attributes_for_post) do
31
+ { 'timeSpent' => '2d' }
32
+ end
33
+ let(:expected_attributes_from_post) do
34
+ { 'id' => '10001', 'timeSpent' => '2d' }
35
+ end
36
+
37
+ let(:attributes_for_put) do
38
+ { 'timeSpent' => '2d' }
39
+ end
40
+ let(:expected_attributes_from_put) do
41
+ { 'id' => '10001', 'timeSpent' => '4d' }
42
+ end
43
+
44
+ it_should_behave_like 'a resource'
45
+ it_should_behave_like 'a resource with a collection GET endpoint'
46
+ it_should_behave_like 'a resource with a singular GET endpoint'
47
+ it_should_behave_like 'a resource with a DELETE endpoint'
48
+ it_should_behave_like 'a resource with a POST endpoint'
49
+ it_should_behave_like 'a resource with a PUT endpoint'
50
+ end
51
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe JIRA::BaseFactory do
4
+ class JIRA::Resource::FooFactory < JIRA::BaseFactory; end
5
+ class JIRA::Resource::Foo; end
6
+
7
+ let(:client) { double }
8
+ subject { JIRA::Resource::FooFactory.new(client) }
9
+
10
+ it 'initializes correctly' do
11
+ expect(subject.class).to eq(JIRA::Resource::FooFactory)
12
+ expect(subject.client).to eq(client)
13
+ expect(subject.target_class).to eq(JIRA::Resource::Foo)
14
+ end
15
+
16
+ it 'proxies all to the target class' do
17
+ expect(JIRA::Resource::Foo).to receive(:all).with(client)
18
+ subject.all
19
+ end
20
+
21
+ it 'proxies find to the target class' do
22
+ expect(JIRA::Resource::Foo).to receive(:find).with(client, 'FOO')
23
+ subject.find('FOO')
24
+ end
25
+
26
+ it 'returns the target class' do
27
+ expect(subject.target_class).to eq(JIRA::Resource::Foo)
28
+ end
29
+
30
+ it 'proxies build to the target class' do
31
+ attrs = double
32
+ expect(JIRA::Resource::Foo).to receive(:build).with(client, attrs)
33
+ subject.build(attrs)
34
+ end
35
+
36
+ it 'proxies collection path to the target class' do
37
+ expect(JIRA::Resource::Foo).to receive(:collection_path).with(client)
38
+ subject.collection_path
39
+ end
40
+
41
+ it 'proxies singular path to the target class' do
42
+ expect(JIRA::Resource::Foo).to receive(:singular_path).with(client, 'FOO')
43
+ subject.singular_path('FOO')
44
+ end
45
+ end
@@ -0,0 +1,598 @@
1
+ require 'spec_helper'
2
+
3
+ describe JIRA::Base do
4
+ class JIRADelegation < SimpleDelegator # :nodoc:
5
+ end
6
+
7
+ class JIRA::Resource::Deadbeef < JIRA::Base # :nodoc:
8
+ end
9
+
10
+ class JIRA::Resource::HasOneExample < JIRA::Base # :nodoc:
11
+ has_one :deadbeef
12
+ has_one :muffin, class: JIRA::Resource::Deadbeef
13
+ has_one :brunchmuffin, class: JIRA::Resource::Deadbeef,
14
+ nested_under: 'nested'
15
+ has_one :breakfastscone,
16
+ class: JIRA::Resource::Deadbeef,
17
+ nested_under: %w[nested breakfastscone]
18
+ has_one :irregularly_named_thing,
19
+ class: JIRA::Resource::Deadbeef,
20
+ attribute_key: 'irregularlyNamedThing'
21
+ end
22
+
23
+ class JIRA::Resource::HasManyExample < JIRA::Base # :nodoc:
24
+ has_many :deadbeefs
25
+ has_many :brunchmuffins, class: JIRA::Resource::Deadbeef,
26
+ nested_under: 'nested'
27
+ has_many :breakfastscones,
28
+ class: JIRA::Resource::Deadbeef,
29
+ nested_under: %w[nested breakfastscone]
30
+ has_many :irregularly_named_things,
31
+ class: JIRA::Resource::Deadbeef,
32
+ attribute_key: 'irregularlyNamedThings'
33
+ end
34
+
35
+ let(:client) { double('client') }
36
+ let(:attrs) { {} }
37
+
38
+ subject { JIRA::Resource::Deadbeef.new(client, attrs: attrs) }
39
+
40
+ let(:decorated) { JIRADelegation.new(subject) }
41
+
42
+ describe '#respond_to?' do
43
+ describe 'when decorated using SimpleDelegator' do
44
+ it 'responds to client' do
45
+ expect(decorated.respond_to?(:client)).to eq(true)
46
+ end
47
+ it 'does not raise an error' do
48
+ expect do
49
+ decorated.respond_to?(:client)
50
+ end.not_to raise_error
51
+ end
52
+ end
53
+ end
54
+
55
+ it 'assigns the client and attrs' do
56
+ expect(subject.client).to eq(client)
57
+ expect(subject.attrs).to eq(attrs)
58
+ end
59
+
60
+ it 'returns all the deadbeefs' do
61
+ response = double
62
+ expect(response).to receive(:body).and_return('[{"self":"http://deadbeef/","id":"98765"}]')
63
+ expect(client).to receive(:get).with('/jira/rest/api/2/deadbeef').and_return(response)
64
+ expect(JIRA::Resource::Deadbeef).to receive(:collection_path).and_return('/jira/rest/api/2/deadbeef')
65
+ deadbeefs = JIRA::Resource::Deadbeef.all(client)
66
+ expect(deadbeefs.length).to eq(1)
67
+ first = deadbeefs.first
68
+ expect(first.class).to eq(JIRA::Resource::Deadbeef)
69
+ expect(first.attrs['self']).to eq('http://deadbeef/')
70
+ expect(first.attrs['id']).to eq('98765')
71
+ expect(first.expanded?).to be_falsey
72
+ end
73
+
74
+ it 'finds a deadbeef by id' do
75
+ response = instance_double('Response', body: '{"self":"http://deadbeef/","id":"98765"}')
76
+ expect(client).to receive(:get).with('/jira/rest/api/2/deadbeef/98765').and_return(response)
77
+ expect(JIRA::Resource::Deadbeef).to receive(:collection_path).and_return('/jira/rest/api/2/deadbeef')
78
+ deadbeef = JIRA::Resource::Deadbeef.find(client, '98765')
79
+ expect(deadbeef.client).to eq(client)
80
+ expect(deadbeef.attrs['self']).to eq('http://deadbeef/')
81
+ expect(deadbeef.attrs['id']).to eq('98765')
82
+ expect(deadbeef.expanded?).to be_truthy
83
+ end
84
+
85
+ it 'finds a deadbeef containing changelog by id' do
86
+ response = instance_double(
87
+ 'Response',
88
+ body: '{"self":"http://deadbeef/","id":"98765","changelog":{"histories":[]}}'
89
+ )
90
+ expect(client).to receive(:get).with('/jira/rest/api/2/deadbeef/98765?expand=changelog').and_return(response)
91
+
92
+ expect(JIRA::Resource::Deadbeef).to receive(:collection_path).and_return('/jira/rest/api/2/deadbeef')
93
+
94
+ deadbeef = JIRA::Resource::Deadbeef.find(client, '98765', expand: 'changelog')
95
+ expect(deadbeef.client).to eq(client)
96
+ expect(deadbeef.attrs['self']).to eq('http://deadbeef/')
97
+ expect(deadbeef.attrs['id']).to eq('98765')
98
+ expect(deadbeef.expanded?).to be_truthy
99
+ expect(deadbeef.attrs['changelog']['histories']).to eq([])
100
+ end
101
+
102
+ it 'builds a deadbeef' do
103
+ deadbeef = JIRA::Resource::Deadbeef.build(client, 'id' => '98765')
104
+ expect(deadbeef.expanded?).to be_falsey
105
+
106
+ expect(deadbeef.client).to eq(client)
107
+ expect(deadbeef.attrs['id']).to eq('98765')
108
+ end
109
+
110
+ it 'returns the endpoint name' do
111
+ expect(subject.class.endpoint_name).to eq('deadbeef')
112
+ end
113
+
114
+ it 'returns the path_component' do
115
+ attrs['id'] = '123'
116
+ expect(subject.path_component).to eq('/deadbeef/123')
117
+ end
118
+
119
+ it 'returns the path component for unsaved instances' do
120
+ expect(subject.path_component).to eq('/deadbeef')
121
+ end
122
+
123
+ it 'converts to a symbol' do
124
+ expect(subject.to_sym).to eq(:deadbeef)
125
+ end
126
+
127
+ describe 'collection_path' do
128
+ before(:each) do
129
+ expect(client).to receive(:options).and_return(rest_base_path: '/deadbeef/bar')
130
+ end
131
+
132
+ it 'returns the collection_path' do
133
+ expect(subject.collection_path).to eq('/deadbeef/bar/deadbeef')
134
+ end
135
+
136
+ it 'returns the collection_path with a prefix' do
137
+ expect(subject.collection_path('/baz/')).to eq('/deadbeef/bar/baz/deadbeef')
138
+ end
139
+
140
+ it 'has a class method that returns the collection_path' do
141
+ expect(subject.class.collection_path(client)).to eq('/deadbeef/bar/deadbeef')
142
+ end
143
+ end
144
+
145
+ it 'parses json' do
146
+ expect(described_class.parse_json('{"foo":"bar"}')).to eq('foo' => 'bar')
147
+ end
148
+
149
+ describe 'dynamic instance methods' do
150
+ let(:attrs) { { 'foo' => 'bar', 'flum' => 'goo', 'object_id' => 'dummy' } }
151
+ subject { JIRA::Resource::Deadbeef.new(client, attrs: attrs) }
152
+
153
+ it 'responds to each of the top level attribute names' do
154
+ expect(subject).to respond_to(:foo)
155
+ expect(subject).to respond_to('flum')
156
+ expect(subject).to respond_to(:object_id)
157
+
158
+ expect(subject.foo).to eq('bar')
159
+ expect(subject.flum).to eq('goo')
160
+
161
+ # Should not override existing method names, but should still allow
162
+ # access to their values via the attrs[] hash
163
+ expect(subject.object_id).not_to eq('dummy')
164
+ expect(subject.attrs['object_id']).to eq('dummy')
165
+ end
166
+ end
167
+
168
+ describe 'fetch' do
169
+ subject { JIRA::Resource::Deadbeef.new(client, attrs: { 'id' => '98765' }) }
170
+
171
+ describe 'not cached' do
172
+ before(:each) do
173
+ response = instance_double('Response', body: '{"self":"http://deadbeef/","id":"98765"}')
174
+ expect(client).to receive(:get).with('/jira/rest/api/2/deadbeef/98765').and_return(response)
175
+ expect(JIRA::Resource::Deadbeef).to receive(:collection_path).and_return('/jira/rest/api/2/deadbeef')
176
+ end
177
+
178
+ it 'sets expanded to true after fetch' do
179
+ expect(subject.expanded?).to be_falsey
180
+ subject.fetch
181
+ expect(subject.expanded?).to be_truthy
182
+ end
183
+
184
+ it 'performs a fetch' do
185
+ expect(subject.expanded?).to be_falsey
186
+ subject.fetch
187
+ expect(subject.self).to eq('http://deadbeef/')
188
+ expect(subject.id).to eq('98765')
189
+ end
190
+
191
+ it 'performs a fetch if already fetched and force flag is true' do
192
+ subject.expanded = true
193
+ subject.fetch(true)
194
+ end
195
+ end
196
+
197
+ describe 'cached' do
198
+ it "doesn't perform a fetch if already fetched" do
199
+ subject.expanded = true
200
+ expect(client).not_to receive(:get)
201
+ subject.fetch
202
+ end
203
+ end
204
+
205
+ context "with expand parameter 'changelog'" do
206
+ it "fetchs changelogs '" do
207
+ response = instance_double(
208
+ 'Response',
209
+ body: '{"self":"http://deadbeef/","id":"98765","changelog":{"histories":[]}}'
210
+ )
211
+ expect(client).to receive(:get).with('/jira/rest/api/2/deadbeef/98765?expand=changelog').and_return(response)
212
+
213
+ expect(JIRA::Resource::Deadbeef).to receive(:collection_path).and_return('/jira/rest/api/2/deadbeef')
214
+
215
+ subject.fetch(false, expand: 'changelog')
216
+
217
+ expect(subject.self).to eq('http://deadbeef/')
218
+ expect(subject.id).to eq('98765')
219
+ expect(subject.changelog['histories']).to eq([])
220
+ end
221
+ end
222
+ end
223
+
224
+ describe 'save' do
225
+ let(:response) { double }
226
+
227
+ subject { JIRA::Resource::Deadbeef.new(client) }
228
+
229
+ before(:each) do
230
+ expect(subject).to receive(:url).and_return('/foo/bar')
231
+ end
232
+
233
+ it 'POSTs a new record' do
234
+ response = instance_double('Response', body: '{"id":"123"}')
235
+ allow(subject).to receive(:new_record?) { true }
236
+ expect(client).to receive(:post).with('/foo/bar', '{"foo":"bar"}').and_return(response)
237
+ expect(subject.save('foo' => 'bar')).to be_truthy
238
+ expect(subject.id).to eq('123')
239
+ expect(subject.expanded).to be_falsey
240
+ end
241
+
242
+ it 'PUTs an existing record' do
243
+ response = instance_double('Response', body: nil)
244
+ allow(subject).to receive(:new_record?) { false }
245
+ expect(client).to receive(:put).with('/foo/bar', '{"foo":"bar"}').and_return(response)
246
+ expect(subject.save('foo' => 'bar')).to be_truthy
247
+ expect(subject.expanded).to be_falsey
248
+ end
249
+
250
+ it 'merges attrs on save' do
251
+ response = instance_double('Response', body: nil)
252
+ expect(client).to receive(:post).with('/foo/bar', '{"foo":{"fum":"dum"}}').and_return(response)
253
+ subject.attrs = { 'foo' => { 'bar' => 'baz' } }
254
+ subject.save('foo' => { 'fum' => 'dum' })
255
+ expect(subject.foo).to eq('bar' => 'baz', 'fum' => 'dum')
256
+ end
257
+
258
+ it 'returns false when an invalid field is set' do # The JIRA REST API apparently ignores fields that you aren't allowed to set manually
259
+ response = instance_double('Response', body: '{"errorMessages":["blah"]}', status: 400)
260
+ allow(subject).to receive(:new_record?) { false }
261
+ expect(client).to receive(:put).with('/foo/bar', '{"invalid_field":"foobar"}').and_raise(JIRA::HTTPError.new(response))
262
+ expect(subject.save('invalid_field' => 'foobar')).to be_falsey
263
+ end
264
+
265
+ it 'returns false with exception details when non json response body (unauthorized)' do # Unauthorized requests return a non-json body. This makes sure we can handle non-json bodies on HTTPError
266
+ response = double('Response', body: 'totally invalid json', code: 401, message: 'Unauthorized')
267
+ expect(client).to receive(:post).with('/foo/bar', '{"foo":"bar"}').and_raise(JIRA::HTTPError.new(response))
268
+ expect(subject.save('foo' => 'bar')).to be_falsey
269
+ expect(subject.attrs['exception']['code']).to eq(401)
270
+ expect(subject.attrs['exception']['message']).to eq('Unauthorized')
271
+ end
272
+ end
273
+
274
+ describe 'save!' do
275
+ let(:response) { double }
276
+
277
+ subject { JIRA::Resource::Deadbeef.new(client) }
278
+
279
+ before(:each) do
280
+ expect(subject).to receive(:url).and_return('/foo/bar')
281
+ end
282
+
283
+ it 'POSTs a new record' do
284
+ response = instance_double('Response', body: '{"id":"123"}')
285
+ allow(subject).to receive(:new_record?) { true }
286
+ expect(client).to receive(:post).with('/foo/bar', '{"foo":"bar"}').and_return(response)
287
+ expect(subject.save!('foo' => 'bar')).to be_truthy
288
+ expect(subject.id).to eq('123')
289
+ expect(subject.expanded).to be_falsey
290
+ end
291
+
292
+ it 'PUTs an existing record' do
293
+ response = instance_double('Response', body: nil)
294
+ allow(subject).to receive(:new_record?) { false }
295
+ expect(client).to receive(:put).with('/foo/bar', '{"foo":"bar"}').and_return(response)
296
+ expect(subject.save!('foo' => 'bar')).to be_truthy
297
+ expect(subject.expanded).to be_falsey
298
+ end
299
+
300
+ it 'throws an exception when an invalid field is set' do
301
+ response = instance_double('Response', body: '{"errorMessages":["blah"]}', status: 400)
302
+ allow(subject).to receive(:new_record?) { false }
303
+ expect(client).to receive(:put).with('/foo/bar', '{"invalid_field":"foobar"}').and_raise(JIRA::HTTPError.new(response))
304
+ expect(-> { subject.save!('invalid_field' => 'foobar') }).to raise_error(JIRA::HTTPError)
305
+ end
306
+ end
307
+
308
+ describe 'set_attrs' do
309
+ it 'merges hashes correctly when clobber is true (default)' do
310
+ subject.attrs = { 'foo' => { 'bar' => 'baz' } }
311
+ subject.set_attrs('foo' => { 'fum' => 'dum' })
312
+ expect(subject.foo).to eq('fum' => 'dum')
313
+ end
314
+
315
+ it 'merges hashes correctly when clobber is false' do
316
+ subject.attrs = { 'foo' => { 'bar' => 'baz' } }
317
+ subject.set_attrs({ 'foo' => { 'fum' => 'dum' } }, false)
318
+ expect(subject.foo).to eq('bar' => 'baz', 'fum' => 'dum')
319
+ end
320
+ end
321
+
322
+ describe 'delete' do
323
+ before(:each) do
324
+ expect(client).to receive(:delete).with('/foo/bar')
325
+ allow(subject).to receive(:url) { '/foo/bar' }
326
+ end
327
+
328
+ it 'flags itself as deleted' do
329
+ expect(subject.deleted?).to be_falsey
330
+ subject.delete
331
+ expect(subject.deleted?).to be_truthy
332
+ end
333
+
334
+ it 'sends a DELETE request' do
335
+ subject.delete
336
+ end
337
+ end
338
+
339
+ describe 'new_record?' do
340
+ it 'returns true for new_record? when new object' do
341
+ subject.attrs['id'] = nil
342
+ expect(subject.new_record?).to be_truthy
343
+ end
344
+
345
+ it 'returns false for new_record? when id is set' do
346
+ subject.attrs['id'] = '123'
347
+ expect(subject.new_record?).to be_falsey
348
+ end
349
+ end
350
+
351
+ describe 'has_errors?' do
352
+ it 'returns true when the response contains errors' do
353
+ attrs['errors'] = { 'invalid' => 'Field invalid' }
354
+ expect(subject.has_errors?).to be_truthy
355
+ end
356
+
357
+ it 'returns false when the response does not contain any errors' do
358
+ expect(subject.has_errors?).to be_falsey
359
+ end
360
+ end
361
+
362
+ describe 'url' do
363
+ before(:each) do
364
+ allow(client).to receive(:options) { { rest_base_path: '/foo/bar' } }
365
+ end
366
+
367
+ it 'returns self as the URL if set' do
368
+ attrs['self'] = 'http://foo/bar'
369
+ expect(subject.url).to eq('http://foo/bar')
370
+ end
371
+
372
+ it 'returns path as the URL if set and site options is specified' do
373
+ allow(client).to receive(:options) { { site: 'http://foo' } }
374
+ attrs['self'] = 'http://foo/bar'
375
+ expect(subject.url).to eq('/bar')
376
+ end
377
+
378
+ it 'returns path as the URL if set and site options is specified and ends with a slash' do
379
+ allow(client).to receive(:options) { { site: 'http://foo/' } }
380
+ attrs['self'] = 'http://foo/bar'
381
+ expect(subject.url).to eq('/bar')
382
+ end
383
+
384
+ it 'generates the URL from id if self not set' do
385
+ attrs['self'] = nil
386
+ attrs['id'] = '98765'
387
+ expect(subject.url).to eq('/foo/bar/deadbeef/98765')
388
+ end
389
+
390
+ it 'generates the URL from collection_path if self and id not set' do
391
+ attrs['self'] = nil
392
+ attrs['id'] = nil
393
+ expect(subject.url).to eq('/foo/bar/deadbeef')
394
+ end
395
+
396
+ it 'has a class method for the collection path' do
397
+ expect(JIRA::Resource::Deadbeef.collection_path(client)).to eq('/foo/bar/deadbeef')
398
+ # Should accept an optional prefix (flum in this case)
399
+ expect(JIRA::Resource::Deadbeef.collection_path(client, '/flum/')).to eq('/foo/bar/flum/deadbeef')
400
+ end
401
+
402
+ it 'has a class method for the singular path' do
403
+ expect(JIRA::Resource::Deadbeef.singular_path(client, 'abc123')).to eq('/foo/bar/deadbeef/abc123')
404
+ # Should accept an optional prefix (flum in this case)
405
+ expect(JIRA::Resource::Deadbeef.singular_path(client, 'abc123', '/flum/')).to eq('/foo/bar/flum/deadbeef/abc123')
406
+ end
407
+ end
408
+
409
+ it 'returns the formatted attrs from to_s' do
410
+ subject.attrs['foo'] = 'bar'
411
+ subject.attrs['dead'] = 'beef'
412
+
413
+ expect(subject.to_s).to match(/#<JIRA::Resource::Deadbeef:\d+ @attrs=#{Regexp.quote(attrs.inspect)}>/)
414
+ end
415
+
416
+ it 'returns the key attribute' do
417
+ expect(subject.class.key_attribute).to eq(:id)
418
+ end
419
+
420
+ it 'returns the key value' do
421
+ subject.attrs['id'] = '123'
422
+ expect(subject.key_value).to eq('123')
423
+ end
424
+
425
+ it 'converts to json' do
426
+ subject.attrs = { 'foo' => 'bar', 'dead' => 'beef' }
427
+ expect(subject.to_json).to eq(subject.attrs.to_json)
428
+
429
+ h = { 'key' => subject }
430
+ h_attrs = { 'key' => subject.attrs }
431
+ expect(h.to_json).to eq(h_attrs.to_json)
432
+ end
433
+
434
+ describe 'extract attrs from response' do
435
+ subject { JIRA::Resource::Deadbeef.new(client, attrs: {}) }
436
+
437
+ it 'sets the attrs from a response' do
438
+ response = instance_double('Response', body: '{"foo":"bar"}')
439
+
440
+ expect(subject.set_attrs_from_response(response)).to eq('foo' => 'bar')
441
+ expect(subject.foo).to eq('bar')
442
+ end
443
+
444
+ it "doesn't clobber existing attrs not in response" do
445
+ response = instance_double('Response', body: '{"foo":"bar"}')
446
+
447
+ subject.attrs = { 'flum' => 'flar' }
448
+ expect(subject.set_attrs_from_response(response)).to eq('foo' => 'bar')
449
+ expect(subject.foo).to eq('bar')
450
+ expect(subject.flum).to eq('flar')
451
+ end
452
+
453
+ it 'handles nil response body' do
454
+ response = instance_double('Response', body: nil)
455
+
456
+ subject.attrs = { 'flum' => 'flar' }
457
+ expect(subject.set_attrs_from_response(response)).to be_nil
458
+ expect(subject.flum).to eq('flar')
459
+ end
460
+ end
461
+
462
+ describe 'nesting' do
463
+ it 'defaults collection_attributes_are_nested to false' do
464
+ expect(JIRA::Resource::Deadbeef.collection_attributes_are_nested).to be_falsey
465
+ end
466
+
467
+ it 'allows collection_attributes_are_nested to be set' do
468
+ JIRA::Resource::Deadbeef.nested_collections true
469
+ expect(JIRA::Resource::Deadbeef.collection_attributes_are_nested).to be_truthy
470
+ end
471
+ end
472
+
473
+ describe 'has_many' do
474
+ subject { JIRA::Resource::HasManyExample.new(client, attrs: { 'deadbeefs' => [{ 'id' => '123' }] }) }
475
+
476
+ it 'returns a collection of instances for has_many relationships' do
477
+ expect(subject.deadbeefs.class).to eq(JIRA::HasManyProxy)
478
+ expect(subject.deadbeefs.length).to eq(1)
479
+ subject.deadbeefs.each do |deadbeef|
480
+ expect(deadbeef.class).to eq(JIRA::Resource::Deadbeef)
481
+ end
482
+ end
483
+
484
+ it 'returns an empty collection for empty has_many relationships' do
485
+ subject = JIRA::Resource::HasManyExample.new(client)
486
+ expect(subject.deadbeefs.length).to eq(0)
487
+ end
488
+
489
+ it 'allows the has_many attributes to be nested inside another attribute' do
490
+ subject = JIRA::Resource::HasManyExample.new(client, attrs: { 'nested' => { 'brunchmuffins' => [{ 'id' => '123' }, { 'id' => '456' }] } })
491
+ expect(subject.brunchmuffins.length).to eq(2)
492
+ subject.brunchmuffins.each do |brunchmuffin|
493
+ expect(brunchmuffin.class).to eq(JIRA::Resource::Deadbeef)
494
+ end
495
+ end
496
+
497
+ it 'allows it to be deeply nested' do
498
+ subject = JIRA::Resource::HasManyExample.new(client, attrs: { 'nested' => {
499
+ 'breakfastscone' => { 'breakfastscones' => [{ 'id' => '123' }, { 'id' => '456' }] }
500
+ } })
501
+ expect(subject.breakfastscones.length).to eq(2)
502
+ subject.breakfastscones.each do |breakfastscone|
503
+ expect(breakfastscone.class).to eq(JIRA::Resource::Deadbeef)
504
+ end
505
+ end
506
+
507
+ it 'short circuits missing deeply nested attrs' do
508
+ subject = JIRA::Resource::HasManyExample.new(client, attrs: {
509
+ 'nested' => {}
510
+ })
511
+ expect(subject.breakfastscones.length).to eq(0)
512
+ end
513
+
514
+ it 'allows the attribute key to be specified' do
515
+ subject = JIRA::Resource::HasManyExample.new(client, attrs: { 'irregularlyNamedThings' => [{ 'id' => '123' }, { 'id' => '456' }] })
516
+ expect(subject.irregularly_named_things.length).to eq(2)
517
+ subject.irregularly_named_things.each do |thing|
518
+ expect(thing.class).to eq(JIRA::Resource::Deadbeef)
519
+ end
520
+ end
521
+
522
+ it 'can build child instances' do
523
+ deadbeef = subject.deadbeefs.build
524
+ expect(deadbeef.class).to eq(JIRA::Resource::Deadbeef)
525
+ end
526
+ end
527
+
528
+ describe 'has_one' do
529
+ subject { JIRA::Resource::HasOneExample.new(client, attrs: { 'deadbeef' => { 'id' => '123' } }) }
530
+
531
+ it 'returns an instance for a has one relationship' do
532
+ expect(subject.deadbeef.class).to eq(JIRA::Resource::Deadbeef)
533
+ expect(subject.deadbeef.id).to eq('123')
534
+ end
535
+
536
+ it 'returns nil when resource attribute is nonexistent' do
537
+ subject = JIRA::Resource::HasOneExample.new(client)
538
+ expect(subject.deadbeef).to be_nil
539
+ end
540
+
541
+ it 'returns an instance with a different class name to the attribute name' do
542
+ subject = JIRA::Resource::HasOneExample.new(client, attrs: { 'muffin' => { 'id' => '123' } })
543
+ expect(subject.muffin.class).to eq(JIRA::Resource::Deadbeef)
544
+ expect(subject.muffin.id).to eq('123')
545
+ end
546
+
547
+ it 'allows the has_one attributes to be nested inside another attribute' do
548
+ subject = JIRA::Resource::HasOneExample.new(client, attrs: { 'nested' => { 'brunchmuffin' => { 'id' => '123' } } })
549
+ expect(subject.brunchmuffin.class).to eq(JIRA::Resource::Deadbeef)
550
+ expect(subject.brunchmuffin.id).to eq('123')
551
+ end
552
+
553
+ it 'allows it to be deeply nested' do
554
+ subject = JIRA::Resource::HasOneExample.new(client, attrs: { 'nested' => {
555
+ 'breakfastscone' => { 'breakfastscone' => { 'id' => '123' } }
556
+ } })
557
+ expect(subject.breakfastscone.class).to eq(JIRA::Resource::Deadbeef)
558
+ expect(subject.breakfastscone.id).to eq('123')
559
+ end
560
+
561
+ it 'allows the attribute key to be specified' do
562
+ subject = JIRA::Resource::HasOneExample.new(client, attrs: { 'irregularlyNamedThing' => { 'id' => '123' } })
563
+ expect(subject.irregularly_named_thing.class).to eq(JIRA::Resource::Deadbeef)
564
+ expect(subject.irregularly_named_thing.id).to eq('123')
565
+ end
566
+ end
567
+
568
+ describe 'belongs_to' do
569
+ class JIRA::Resource::BelongsToExample < JIRA::Base
570
+ belongs_to :deadbeef
571
+ end
572
+
573
+ let(:deadbeef) { JIRA::Resource::Deadbeef.new(client, attrs: { 'id' => '999' }) }
574
+
575
+ subject { JIRA::Resource::BelongsToExample.new(client, attrs: { 'id' => '123' }, deadbeef: deadbeef) }
576
+
577
+ it 'sets up an accessor for the belongs to relationship' do
578
+ expect(subject.deadbeef).to eq(deadbeef)
579
+ end
580
+
581
+ it 'raises an exception when initialized without a belongs_to instance' do
582
+ expect(lambda {
583
+ JIRA::Resource::BelongsToExample.new(client, attrs: { 'id' => '123' })
584
+ }).to raise_exception(ArgumentError, 'Required option :deadbeef missing')
585
+ end
586
+
587
+ it 'returns the right url' do
588
+ allow(client).to receive(:options) { { rest_base_path: '/foo' } }
589
+ expect(subject.url).to eq('/foo/deadbeef/999/belongstoexample/123')
590
+ end
591
+
592
+ it 'can be initialized with an instance or a key value' do
593
+ allow(client).to receive(:options) { { rest_base_path: '/foo' } }
594
+ subject = JIRA::Resource::BelongsToExample.new(client, attrs: { 'id' => '123' }, deadbeef_id: '987')
595
+ expect(subject.url).to eq('/foo/deadbeef/987/belongstoexample/123')
596
+ end
597
+ end
598
+ end