zk-jira-ruby 2.1.5

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 +232 -0
  10. data/http-basic-example.rb +113 -0
  11. data/jira-ruby.gemspec +35 -0
  12. data/lib/jira/base.rb +525 -0
  13. data/lib/jira/base_factory.rb +46 -0
  14. data/lib/jira/client.rb +315 -0
  15. data/lib/jira/has_many_proxy.rb +42 -0
  16. data/lib/jira/http_client.rb +112 -0
  17. data/lib/jira/http_error.rb +14 -0
  18. data/lib/jira/jwt_client.rb +67 -0
  19. data/lib/jira/oauth_client.rb +114 -0
  20. data/lib/jira/railtie.rb +10 -0
  21. data/lib/jira/request_client.rb +31 -0
  22. data/lib/jira/resource/agile.rb +79 -0
  23. data/lib/jira/resource/applinks.rb +39 -0
  24. data/lib/jira/resource/attachment.rb +50 -0
  25. data/lib/jira/resource/board.rb +91 -0
  26. data/lib/jira/resource/board_configuration.rb +9 -0
  27. data/lib/jira/resource/comment.rb +12 -0
  28. data/lib/jira/resource/component.rb +8 -0
  29. data/lib/jira/resource/createmeta.rb +44 -0
  30. data/lib/jira/resource/field.rb +83 -0
  31. data/lib/jira/resource/filter.rb +15 -0
  32. data/lib/jira/resource/issue.rb +141 -0
  33. data/lib/jira/resource/issuelink.rb +20 -0
  34. data/lib/jira/resource/issuelinktype.rb +14 -0
  35. data/lib/jira/resource/issuetype.rb +8 -0
  36. data/lib/jira/resource/priority.rb +8 -0
  37. data/lib/jira/resource/project.rb +41 -0
  38. data/lib/jira/resource/rapidview.rb +67 -0
  39. data/lib/jira/resource/remotelink.rb +26 -0
  40. data/lib/jira/resource/resolution.rb +8 -0
  41. data/lib/jira/resource/serverinfo.rb +18 -0
  42. data/lib/jira/resource/sprint.rb +105 -0
  43. data/lib/jira/resource/sprint_report.rb +8 -0
  44. data/lib/jira/resource/status.rb +8 -0
  45. data/lib/jira/resource/transition.rb +29 -0
  46. data/lib/jira/resource/user.rb +30 -0
  47. data/lib/jira/resource/version.rb +66 -0
  48. data/lib/jira/resource/watcher.rb +35 -0
  49. data/lib/jira/resource/webhook.rb +37 -0
  50. data/lib/jira/resource/worklog.rb +14 -0
  51. data/lib/jira/tasks.rb +0 -0
  52. data/lib/jira/version.rb +3 -0
  53. data/lib/jira-ruby.rb +49 -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/10000.invalid.put.json +5 -0
  98. data/spec/mock_responses/component/10000.json +39 -0
  99. data/spec/mock_responses/component/10000.put.json +39 -0
  100. data/spec/mock_responses/component.post.json +28 -0
  101. data/spec/mock_responses/empty_issues.json +8 -0
  102. data/spec/mock_responses/field/1.json +15 -0
  103. data/spec/mock_responses/field.json +32 -0
  104. data/spec/mock_responses/issue/10002/attachments/10000.json +20 -0
  105. data/spec/mock_responses/issue/10002/comment/10000.json +29 -0
  106. data/spec/mock_responses/issue/10002/comment/10000.put.json +29 -0
  107. data/spec/mock_responses/issue/10002/comment.json +65 -0
  108. data/spec/mock_responses/issue/10002/comment.post.json +29 -0
  109. data/spec/mock_responses/issue/10002/transitions.json +49 -0
  110. data/spec/mock_responses/issue/10002/transitions.post.json +1 -0
  111. data/spec/mock_responses/issue/10002/watchers.json +13 -0
  112. data/spec/mock_responses/issue/10002/worklog/10000.json +31 -0
  113. data/spec/mock_responses/issue/10002/worklog/10000.put.json +30 -0
  114. data/spec/mock_responses/issue/10002/worklog.json +98 -0
  115. data/spec/mock_responses/issue/10002/worklog.post.json +30 -0
  116. data/spec/mock_responses/issue/10002.invalid.put.json +6 -0
  117. data/spec/mock_responses/issue/10002.json +126 -0
  118. data/spec/mock_responses/issue/10002.put.missing_field_update.json +6 -0
  119. data/spec/mock_responses/issue.json +1108 -0
  120. data/spec/mock_responses/issue.post.json +5 -0
  121. data/spec/mock_responses/issueLinkType/10000.json +7 -0
  122. data/spec/mock_responses/issueLinkType.json +25 -0
  123. data/spec/mock_responses/issuetype/5.json +8 -0
  124. data/spec/mock_responses/issuetype.json +42 -0
  125. data/spec/mock_responses/jira/rest/webhooks/1.0/webhook/2.json +11 -0
  126. data/spec/mock_responses/jira/rest/webhooks/1.0/webhook.json +11 -0
  127. data/spec/mock_responses/priority/1.json +8 -0
  128. data/spec/mock_responses/priority.json +42 -0
  129. data/spec/mock_responses/project/SAMPLEPROJECT.issues.json +1108 -0
  130. data/spec/mock_responses/project/SAMPLEPROJECT.json +84 -0
  131. data/spec/mock_responses/project.json +12 -0
  132. data/spec/mock_responses/rapidview/SAMPLEPROJECT.issues.full.json +276 -0
  133. data/spec/mock_responses/rapidview/SAMPLEPROJECT.issues.json +111 -0
  134. data/spec/mock_responses/rapidview/SAMPLEPROJECT.json +6 -0
  135. data/spec/mock_responses/rapidview.json +10 -0
  136. data/spec/mock_responses/resolution/1.json +7 -0
  137. data/spec/mock_responses/resolution.json +15 -0
  138. data/spec/mock_responses/sprint/1_issues.json +125 -0
  139. data/spec/mock_responses/status/1.json +7 -0
  140. data/spec/mock_responses/status.json +37 -0
  141. data/spec/mock_responses/user_username=admin.json +17 -0
  142. data/spec/mock_responses/version/10000.invalid.put.json +5 -0
  143. data/spec/mock_responses/version/10000.json +11 -0
  144. data/spec/mock_responses/version/10000.put.json +7 -0
  145. data/spec/mock_responses/version.post.json +7 -0
  146. data/spec/mock_responses/webhook/webhook.json +11 -0
  147. data/spec/mock_responses/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,46 @@
1
+ module JIRA
2
+ # This is the base class for all the JIRA resource factory instances.
3
+ class BaseFactory
4
+ attr_reader :client
5
+
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # Return the name of the class which this factory generates, i.e.
11
+ # JIRA::Resource::FooFactory creates JIRA::Resource::Foo instances.
12
+ def target_class
13
+ # Need to do a little bit of work here as Module.const_get doesn't work
14
+ # with nested class names, i.e. JIRA::Resource::Foo.
15
+ #
16
+ # So create a method chain from the class components. This code will
17
+ # unroll to:
18
+ # Module.const_get('JIRA').const_get('Resource').const_get('Foo')
19
+ #
20
+ target_class_name = self.class.name.sub(/Factory$/, '')
21
+ class_components = target_class_name.split('::')
22
+
23
+ class_components.inject(Module) do |mod, const_name|
24
+ mod.const_get(const_name)
25
+ end
26
+ end
27
+
28
+ def self.delegate_to_target_class(*method_names)
29
+ method_names.each do |method_name|
30
+ define_method method_name do |*args|
31
+ target_class.send(method_name, @client, *args)
32
+ end
33
+ end
34
+ end
35
+
36
+ # The principle purpose of this class is to delegate methods to the corresponding
37
+ # non-factory class and automatically prepend the client argument to the argument
38
+ # list.
39
+ delegate_to_target_class :all, :find, :collection_path, :singular_path, :jql, :get_backlog_issues, :get_board_issues, :get_sprints, :get_sprint_issues, :get_projects, :get_projects_full
40
+
41
+ # This method needs special handling as it has a default argument value
42
+ def build(attrs = {})
43
+ target_class.build(@client, attrs)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,315 @@
1
+ require 'json'
2
+ require 'forwardable'
3
+ require 'ostruct'
4
+
5
+ module JIRA
6
+ # This class is the main access point for all JIRA::Resource instances.
7
+ #
8
+ # The client must be initialized with an options hash containing
9
+ # configuration options. The available options are:
10
+ #
11
+ # :site => 'http://localhost:2990',
12
+ # :context_path => '/jira',
13
+ # :signature_method => 'RSA-SHA1',
14
+ # :request_token_path => "/plugins/servlet/oauth/request-token",
15
+ # :authorize_path => "/plugins/servlet/oauth/authorize",
16
+ # :access_token_path => "/plugins/servlet/oauth/access-token",
17
+ # :private_key => nil,
18
+ # :private_key_file => "rsakey.pem",
19
+ # :rest_base_path => "/rest/api/2",
20
+ # :consumer_key => nil,
21
+ # :consumer_secret => nil,
22
+ # :ssl_verify_mode => OpenSSL::SSL::VERIFY_PEER,
23
+ # :ssl_version => nil,
24
+ # :use_ssl => true,
25
+ # :username => nil,
26
+ # :password => nil,
27
+ # :auth_type => :oauth,
28
+ # :proxy_address => nil,
29
+ # :proxy_port => nil,
30
+ # :proxy_username => nil,
31
+ # :proxy_password => nil,
32
+ # :use_cookies => nil,
33
+ # :additional_cookies => nil,
34
+ # :default_headers => {},
35
+ # :use_client_cert => false,
36
+ # :read_timeout => nil,
37
+ # :http_debug => false,
38
+ # :shared_secret => nil,
39
+ # :cert_path => nil,
40
+ # :key_path => nil,
41
+ # :ssl_client_cert => nil,
42
+ # :ssl_client_key => nil
43
+ #
44
+ # See the JIRA::Base class methods for all of the available methods on these accessor
45
+ # objects.
46
+
47
+ class Client
48
+ extend Forwardable
49
+
50
+ # The OAuth::Consumer instance returned by the OauthClient
51
+ #
52
+ # The authenticated client instance returned by the respective client type
53
+ # (Oauth, Basic)
54
+ attr_accessor :consumer, :request_client, :http_debug, :cache
55
+
56
+ # The configuration options for this client instance
57
+ attr_reader :options
58
+
59
+ def_delegators :@request_client, :init_access_token, :set_access_token, :set_request_token, :request_token, :access_token, :authenticated?
60
+
61
+ DEFINED_OPTIONS = [
62
+ :site,
63
+ :context_path,
64
+ :signature_method,
65
+ :request_token_path,
66
+ :authorize_path,
67
+ :access_token_path,
68
+ :private_key,
69
+ :private_key_file,
70
+ :rest_base_path,
71
+ :consumer_key,
72
+ :consumer_secret,
73
+ :ssl_verify_mode,
74
+ :ssl_version,
75
+ :use_ssl,
76
+ :username,
77
+ :password,
78
+ :auth_type,
79
+ :proxy_address,
80
+ :proxy_port,
81
+ :proxy_username,
82
+ :proxy_password,
83
+ :use_cookies,
84
+ :additional_cookies,
85
+ :default_headers,
86
+ :use_client_cert,
87
+ :read_timeout,
88
+ :http_debug,
89
+ :issuer,
90
+ :base_url,
91
+ :shared_secret,
92
+ :cert_path,
93
+ :key_path,
94
+ :ssl_client_cert,
95
+ :ssl_client_key
96
+ ].freeze
97
+
98
+ DEFAULT_OPTIONS = {
99
+ site: 'http://localhost:2990',
100
+ context_path: '/jira',
101
+ rest_base_path: '/rest/api/2',
102
+ ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER,
103
+ use_ssl: true,
104
+ use_client_cert: false,
105
+ auth_type: :oauth,
106
+ http_debug: false,
107
+ default_headers: {}
108
+ }.freeze
109
+
110
+ def initialize(options = {})
111
+ options = DEFAULT_OPTIONS.merge(options)
112
+ @options = options
113
+ @options[:rest_base_path] = @options[:context_path] + @options[:rest_base_path]
114
+
115
+ unknown_options = options.keys.reject { |o| DEFINED_OPTIONS.include?(o) }
116
+ raise ArgumentError, "Unknown option(s) given: #{unknown_options}" unless unknown_options.empty?
117
+
118
+ if options[:use_client_cert]
119
+ @options[:ssl_client_cert] = OpenSSL::X509::Certificate.new(File.read(@options[:cert_path])) if @options[:cert_path]
120
+ @options[:ssl_client_key] = OpenSSL::PKey::RSA.new(File.read(@options[:key_path])) if @options[:key_path]
121
+
122
+ raise ArgumentError, 'Options: :cert_path or :ssl_client_cert must be set when :use_client_cert is true' unless @options[:ssl_client_cert]
123
+ raise ArgumentError, 'Options: :key_path or :ssl_client_key must be set when :use_client_cert is true' unless @options[:ssl_client_key]
124
+ end
125
+
126
+ case options[:auth_type]
127
+ when :oauth, :oauth_2legged
128
+ @request_client = OauthClient.new(@options)
129
+ @consumer = @request_client.consumer
130
+ when :jwt
131
+ @request_client = JwtClient.new(@options)
132
+ when :basic
133
+ @request_client = HttpClient.new(@options)
134
+ when :cookie
135
+ raise ArgumentError, 'Options: :use_cookies must be true for :cookie authorization type' if @options.key?(:use_cookies) && !@options[:use_cookies]
136
+ @options[:use_cookies] = true
137
+ @request_client = HttpClient.new(@options)
138
+ @request_client.make_cookie_auth_request
139
+ @options.delete(:username)
140
+ @options.delete(:password)
141
+ else
142
+ raise ArgumentError, 'Options: ":auth_type" must be ":oauth",":oauth_2legged", ":cookie" or ":basic"'
143
+ end
144
+
145
+ @http_debug = @options[:http_debug]
146
+
147
+ @options.freeze
148
+
149
+ @cache = OpenStruct.new
150
+ end
151
+
152
+ def Project # :nodoc:
153
+ JIRA::Resource::ProjectFactory.new(self)
154
+ end
155
+
156
+ def Issue # :nodoc:
157
+ JIRA::Resource::IssueFactory.new(self)
158
+ end
159
+
160
+ def Filter # :nodoc:
161
+ JIRA::Resource::FilterFactory.new(self)
162
+ end
163
+
164
+ def Component # :nodoc:
165
+ JIRA::Resource::ComponentFactory.new(self)
166
+ end
167
+
168
+ def User # :nodoc:
169
+ JIRA::Resource::UserFactory.new(self)
170
+ end
171
+
172
+ def Issuetype # :nodoc:
173
+ JIRA::Resource::IssuetypeFactory.new(self)
174
+ end
175
+
176
+ def Priority # :nodoc:
177
+ JIRA::Resource::PriorityFactory.new(self)
178
+ end
179
+
180
+ def Status # :nodoc:
181
+ JIRA::Resource::StatusFactory.new(self)
182
+ end
183
+
184
+ def Resolution # :nodoc:
185
+ JIRA::Resource::ResolutionFactory.new(self)
186
+ end
187
+
188
+ def Comment # :nodoc:
189
+ JIRA::Resource::CommentFactory.new(self)
190
+ end
191
+
192
+ def Attachment # :nodoc:
193
+ JIRA::Resource::AttachmentFactory.new(self)
194
+ end
195
+
196
+ def Worklog # :nodoc:
197
+ JIRA::Resource::WorklogFactory.new(self)
198
+ end
199
+
200
+ def Version # :nodoc:
201
+ JIRA::Resource::VersionFactory.new(self)
202
+ end
203
+
204
+ def Transition # :nodoc:
205
+ JIRA::Resource::TransitionFactory.new(self)
206
+ end
207
+
208
+ def Field # :nodoc:
209
+ JIRA::Resource::FieldFactory.new(self)
210
+ end
211
+
212
+ def Board
213
+ JIRA::Resource::BoardFactory.new(self)
214
+ end
215
+
216
+ def BoardConfiguration
217
+ JIRA::Resource::BoardConfigurationFactory.new(self)
218
+ end
219
+
220
+ def RapidView
221
+ JIRA::Resource::RapidViewFactory.new(self)
222
+ end
223
+
224
+ def Sprint
225
+ JIRA::Resource::SprintFactory.new(self)
226
+ end
227
+
228
+ def SprintReport
229
+ JIRA::Resource::SprintReportFactory.new(self)
230
+ end
231
+
232
+ def ServerInfo
233
+ JIRA::Resource::ServerInfoFactory.new(self)
234
+ end
235
+
236
+ def Createmeta
237
+ JIRA::Resource::CreatemetaFactory.new(self)
238
+ end
239
+
240
+ def ApplicationLink
241
+ JIRA::Resource::ApplicationLinkFactory.new(self)
242
+ end
243
+
244
+ def Watcher
245
+ JIRA::Resource::WatcherFactory.new(self)
246
+ end
247
+
248
+ def Webhook
249
+ JIRA::Resource::WebhookFactory.new(self)
250
+ end
251
+
252
+ def Issuelink
253
+ JIRA::Resource::IssuelinkFactory.new(self)
254
+ end
255
+
256
+ def Issuelinktype
257
+ JIRA::Resource::IssuelinktypeFactory.new(self)
258
+ end
259
+
260
+ def Remotelink
261
+ JIRA::Resource::RemotelinkFactory.new(self)
262
+ end
263
+
264
+ def Agile
265
+ JIRA::Resource::AgileFactory.new(self)
266
+ end
267
+
268
+ # HTTP methods without a body
269
+ def delete(path, headers = {})
270
+ request(:delete, path, nil, merge_default_headers(headers))
271
+ end
272
+
273
+ def get(path, headers = {})
274
+ request(:get, path, nil, merge_default_headers(headers))
275
+ end
276
+
277
+ def head(path, headers = {})
278
+ request(:head, path, nil, merge_default_headers(headers))
279
+ end
280
+
281
+ # HTTP methods with a body
282
+ def post(path, body = '', headers = {})
283
+ headers = { 'Content-Type' => 'application/json' }.merge(headers)
284
+ request(:post, path, body, merge_default_headers(headers))
285
+ end
286
+
287
+ def post_multipart(path, file, headers = {})
288
+ puts "post multipart: #{path} - [#{file}]" if @http_debug
289
+ @request_client.request_multipart(path, file, headers)
290
+ end
291
+
292
+ def put(path, body = '', headers = {})
293
+ headers = { 'Content-Type' => 'application/json' }.merge(headers)
294
+ request(:put, path, body, merge_default_headers(headers))
295
+ end
296
+
297
+ # Sends the specified HTTP request to the REST API through the
298
+ # appropriate method (oauth, basic).
299
+ def request(http_method, path, body = '', headers = {})
300
+ puts "#{http_method}: #{path} - [#{body}]" if @http_debug
301
+ @request_client.request(http_method, path, body, headers)
302
+ end
303
+
304
+ # Stops sensitive client information from being displayed in logs
305
+ def inspect
306
+ "#<JIRA::Client:#{object_id}>"
307
+ end
308
+
309
+ protected
310
+
311
+ def merge_default_headers(headers)
312
+ { 'Accept' => 'application/json' }.merge(@options[:default_headers]).merge(headers)
313
+ end
314
+ end
315
+ end
@@ -0,0 +1,42 @@
1
+ #
2
+ # Whenever a collection from a has_many relationship is accessed, an instance
3
+ # of this class is returned. This instance wraps the Array of instances in
4
+ # the collection with an extra build method, which allows new instances to be
5
+ # built on the collection with the correct properties.
6
+ #
7
+ # In practice, instances of this class behave exactly like an Array.
8
+ #
9
+ class JIRA::HasManyProxy
10
+ attr_reader :target_class, :parent
11
+ attr_accessor :collection
12
+
13
+ def initialize(parent, target_class, collection = [])
14
+ @parent = parent
15
+ @target_class = target_class
16
+ @collection = collection
17
+ end
18
+
19
+ # Builds an instance of this class with the correct parent.
20
+ # For example, issue.comments.build(attrs) will initialize a
21
+ # comment as follows:
22
+ #
23
+ # JIRA::Resource::Comment.new(issue.client,
24
+ # :attrs => attrs,
25
+ # :issue => issue)
26
+ def build(attrs = {})
27
+ resource = target_class.new(parent.client, :attrs => attrs, parent.to_sym => parent)
28
+ collection << resource
29
+ resource
30
+ end
31
+
32
+ # Forces an HTTP request to fetch all instances of the target class that
33
+ # are associated with the parent
34
+ def all
35
+ target_class.all(parent.client, parent.to_sym => parent)
36
+ end
37
+
38
+ # Delegate any missing methods to the collection that this proxy wraps
39
+ def method_missing(method_name, *args, &block)
40
+ collection.send(method_name, *args, &block)
41
+ end
42
+ end
@@ -0,0 +1,112 @@
1
+ require 'json'
2
+ require 'net/https'
3
+ require 'cgi/cookie'
4
+ require 'uri'
5
+
6
+ module JIRA
7
+ class HttpClient < RequestClient
8
+ DEFAULT_OPTIONS = {
9
+ username: nil,
10
+ password: nil
11
+ }.freeze
12
+
13
+ attr_reader :options
14
+
15
+ def initialize(options)
16
+ @options = DEFAULT_OPTIONS.merge(options)
17
+ @cookies = {}
18
+ end
19
+
20
+ def make_cookie_auth_request
21
+ body = { username: @options[:username].to_s, password: @options[:password].to_s }.to_json
22
+ @options.delete(:username)
23
+ @options.delete(:password)
24
+ make_request(:post, @options[:context_path] + '/rest/auth/1/session', body, 'Content-Type' => 'application/json')
25
+ end
26
+
27
+ def make_request(http_method, url, body = '', headers = {})
28
+ # When a proxy is enabled, Net::HTTP expects that the request path omits the domain name
29
+ path = request_path(url)
30
+ request = Net::HTTP.const_get(http_method.to_s.capitalize).new(path, headers)
31
+ request.body = body unless body.nil?
32
+
33
+ execute_request(request)
34
+ end
35
+
36
+ def make_multipart_request(url, body, headers = {})
37
+ path = request_path(url)
38
+ request = Net::HTTP::Post::Multipart.new(path, body, headers)
39
+
40
+ execute_request(request)
41
+ end
42
+
43
+ def basic_auth_http_conn
44
+ http_conn(uri)
45
+ end
46
+
47
+ def http_conn(uri)
48
+ if @options[:proxy_address]
49
+ http_class = Net::HTTP::Proxy(@options[:proxy_address], @options[:proxy_port] || 80, @options[:proxy_username], @options[:proxy_password])
50
+ else
51
+ http_class = Net::HTTP
52
+ end
53
+ http_conn = http_class.new(uri.host, uri.port)
54
+ http_conn.use_ssl = @options[:use_ssl]
55
+ if @options[:use_client_cert]
56
+ http_conn.cert = @options[:ssl_client_cert]
57
+ http_conn.key = @options[:ssl_client_key]
58
+ end
59
+ http_conn.verify_mode = @options[:ssl_verify_mode]
60
+ http_conn.ssl_version = @options[:ssl_version] if @options[:ssl_version]
61
+ http_conn.read_timeout = @options[:read_timeout]
62
+ http_conn
63
+ end
64
+
65
+ def uri
66
+ URI.parse(@options[:site])
67
+ end
68
+
69
+ def authenticated?
70
+ @authenticated
71
+ end
72
+
73
+ private
74
+
75
+ def execute_request(request)
76
+ add_cookies(request) if options[:use_cookies]
77
+ request.basic_auth(@options[:username], @options[:password]) if @options[:username] && @options[:password]
78
+
79
+ response = basic_auth_http_conn.request(request)
80
+ @authenticated = response.is_a? Net::HTTPOK
81
+ store_cookies(response) if options[:use_cookies]
82
+
83
+ response
84
+ end
85
+
86
+ def request_path(url)
87
+ parsed_uri = URI(url)
88
+
89
+ return url unless parsed_uri.is_a?(URI::HTTP)
90
+
91
+ parsed_uri.request_uri
92
+ end
93
+
94
+ def store_cookies(response)
95
+ cookies = response.get_fields('set-cookie')
96
+ if cookies
97
+ cookies.each do |cookie|
98
+ data = CGI::Cookie.parse(cookie)
99
+ data.delete('Path')
100
+ @cookies.merge!(data)
101
+ end
102
+ end
103
+ end
104
+
105
+ def add_cookies(request)
106
+ cookie_array = @cookies.values.map { |cookie| "#{cookie.name}=#{cookie.value[0]}" }
107
+ cookie_array += Array(@options[:additional_cookies]) if @options.key?(:additional_cookies)
108
+ request.add_field('Cookie', cookie_array.join('; ')) if cookie_array.any?
109
+ request
110
+ end
111
+ end
112
+ end