jira-ruby 2.1.3

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 (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