jira-ruby 3.0.0.beta1 → 3.0.0

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/CI.yml +1 -0
  3. data/.github/workflows/codeql.yml +0 -4
  4. data/.gitignore +3 -1
  5. data/.rubocop.yml +5 -70
  6. data/.yardopts +4 -0
  7. data/lib/jira/base.rb +5 -13
  8. data/lib/jira/client.rb +59 -4
  9. data/lib/jira/has_many_proxy.rb +30 -28
  10. data/lib/jira/http_client.rb +64 -1
  11. data/lib/jira/oauth_client.rb +62 -0
  12. data/lib/jira/request_client.rb +26 -1
  13. data/lib/jira/resource/attachment.rb +88 -3
  14. data/lib/jira/resource/field.rb +4 -8
  15. data/lib/jira/resource/issue.rb +80 -11
  16. data/lib/jira/resource/issue_picker_suggestions.rb +1 -1
  17. data/lib/jira/resource/issuelink.rb +4 -3
  18. data/lib/jira/resource/project.rb +1 -1
  19. data/lib/jira/resource/sprint.rb +2 -2
  20. data/lib/jira/resource/watcher.rb +1 -1
  21. data/lib/jira/resource/webhook.rb +5 -1
  22. data/lib/jira/version.rb +1 -1
  23. data/lib/tasks/generate.rake +1 -1
  24. data/spec/integration/issue_spec.rb +2 -2
  25. data/spec/integration/project_spec.rb +2 -2
  26. data/spec/integration/rapidview_spec.rb +3 -3
  27. data/spec/integration/user_spec.rb +12 -3
  28. data/spec/integration/watcher_spec.rb +6 -2
  29. data/spec/integration/{webhook.rb → webhook_spec.rb} +8 -1
  30. data/spec/jira/base_factory_spec.rb +11 -2
  31. data/spec/jira/base_spec.rb +80 -57
  32. data/spec/jira/client_spec.rb +29 -27
  33. data/spec/jira/http_client_spec.rb +2 -2
  34. data/spec/jira/oauth_client_spec.rb +8 -4
  35. data/spec/jira/resource/agile_spec.rb +4 -4
  36. data/spec/jira/resource/attachment_spec.rb +36 -13
  37. data/spec/jira/resource/board_spec.rb +5 -5
  38. data/spec/jira/resource/field_spec.rb +23 -24
  39. data/spec/jira/resource/filter_spec.rb +3 -2
  40. data/spec/jira/resource/issue_spec.rb +103 -81
  41. data/spec/jira/resource/project_spec.rb +8 -8
  42. data/spec/jira/resource/sprint_spec.rb +23 -11
  43. data/spec/jira/resource/status_spec.rb +1 -1
  44. data/spec/jira/resource/user_factory_spec.rb +2 -2
  45. data/spec/jira/resource/worklog_spec.rb +1 -1
  46. data/spec/mock_responses/board/1_issues.json +2 -1
  47. data/spec/mock_responses/issue.json +1 -0
  48. data/spec/mock_responses/rapidview/SAMPLEPROJECT.issues.full.json +2 -1
  49. data/spec/mock_responses/rapidview/SAMPLEPROJECT.issues.json +2 -1
  50. data/spec/support/clients_helper.rb +2 -2
  51. data/spec/support/mock_client.rb +9 -0
  52. data/spec/support/mock_response.rb +8 -0
  53. data/spec/support/shared_examples/integration.rb +1 -1
  54. metadata +9 -10
@@ -5,7 +5,16 @@ require 'json'
5
5
  require 'forwardable'
6
6
 
7
7
  module JIRA
8
+ # Client using OAuth 1.0
9
+ #
10
+ # @!attribute [rw] consumer
11
+ # @return [OAuth::Consumer] The oauth consumer object
12
+ # @!attribute [r] options
13
+ # @return [Hash] The oauth connection options
14
+ # @!attribute [r] access_token
15
+ # @return [OAuth::AccessToken] The oauth access token
8
16
  class OauthClient < RequestClient
17
+ # @private
9
18
  DEFAULT_OPTIONS = {
10
19
  signature_method: 'RSA-SHA1',
11
20
  request_token_path: '/plugins/servlet/oauth/request-token',
@@ -31,11 +40,29 @@ module JIRA
31
40
 
32
41
  def_instance_delegators :@consumer, :key, :secret, :get_request_token
33
42
 
43
+ # Generally not used directly, but through JIRA::Client.
44
+ # @param [Hash] options Options as passed from JIRA::Client constructor.
45
+ # @option options [String] :signature_method The signature method to use (defaults to 'RSA-SHA1')
46
+ # @option options [String] :request_token_path The path to request a token (defaults to '/plugins/servlet/oauth/request-token')
47
+ # @option options [String] :authorize_path The path to authorize a token (defaults to '/plugins/servlet/oauth/authorize')
48
+ # @option options [String] :access_token_path The path to access a token (defaults to '/plugins/servlet/oauth/access-token')
49
+ # @option options [String] :private_key_file The path to the private key file
50
+ # @option options [String] :consumer_key The OAuth 1.0 consumer key
51
+ # @option options [String] :consumer_secret The OAuth 1.0 consumer secret
52
+ # @option options [Hash] :default_headers Additional headers for requests
53
+ # @option options [String] :proxy_uri Proxy URI
54
+ # @option options [String] :proxy_user Proxy user
55
+ # @option options [String] :proxy_password Proxy Password
34
56
  def initialize(options)
35
57
  @options = DEFAULT_OPTIONS.merge(options)
36
58
  @consumer = init_oauth_consumer(@options)
37
59
  end
38
60
 
61
+ # @private
62
+ # Initialises the OAuth consumer object.
63
+ # Generally you should not call this method directly, it is called by the constructor.
64
+ # @param [Hash] _options The options hash
65
+ # @return [OAuth::Consumer] The OAuth consumer object
39
66
  def init_oauth_consumer(_options)
40
67
  @options[:request_token_path] = @options[:context_path] + @options[:request_token_path]
41
68
  @options[:authorize_path] = @options[:context_path] + @options[:authorize_path]
@@ -47,22 +74,31 @@ module JIRA
47
74
 
48
75
  # Returns the current request token if it is set, else it creates
49
76
  # and sets a new token.
77
+ # @param [Hash] options
50
78
  def request_token(options = {}, ...)
51
79
  @request_token ||= get_request_token(options, ...)
52
80
  end
53
81
 
54
82
  # Sets the request token from a given token and secret.
83
+ # @param [String] token The request token
84
+ # @param [String] secret The request token secret
85
+ # @return [OAuth::RequestToken] The request token object
55
86
  def set_request_token(token, secret)
56
87
  @request_token = OAuth::RequestToken.new(@consumer, token, secret)
57
88
  end
58
89
 
59
90
  # Initialises and returns a new access token from the params hash
60
91
  # returned by the OAuth transaction.
92
+ # @param [Hash] params The params hash returned by the OAuth transaction
93
+ # @return [OAuth::AccessToken] The access token object
61
94
  def init_access_token(params)
62
95
  @access_token = request_token.get_access_token(params)
63
96
  end
64
97
 
65
98
  # Sets the access token from a preexisting token and secret.
99
+ # @param [String] token The access token
100
+ # @param [String] secret The access token secret
101
+ # @return [OAuth::AccessToken] The access token object
66
102
  def set_access_token(token, secret)
67
103
  @access_token = OAuth::AccessToken.new(@consumer, token, secret)
68
104
  @authenticated = true
@@ -71,12 +107,25 @@ module JIRA
71
107
 
72
108
  # Returns the current access token. Raises an
73
109
  # JIRA::Client::UninitializedAccessTokenError exception if it is not set.
110
+ # @return [OAuth::AccessToken] The access token object
74
111
  def access_token
75
112
  raise UninitializedAccessTokenError unless @access_token
76
113
 
77
114
  @access_token
78
115
  end
79
116
 
117
+ # Makes a request to the JIRA server using the oauth gem.
118
+ #
119
+ # Generally you should not call this method directly, but use the helper methods in JIRA::Client.
120
+ #
121
+ # File uploads are not supported with this method. Use make_multipart_request instead.
122
+ #
123
+ # @param [Symbol] http_method The HTTP method to use
124
+ # @param [String] url The JIRA REST URL to call
125
+ # @param [String] body The body of the request
126
+ # @param [Hash] headers The headers to send with the request
127
+ # @return [Net::HTTPResponse] The response object
128
+ # @raise [JIRA::HTTPError] If the response is not an HTTP success code
80
129
  def make_request(http_method, url, body = '', headers = {})
81
130
  # When using oauth_2legged we need to add an empty oauth_token parameter to every request.
82
131
  if @options[:auth_type] == :oauth_2legged
@@ -100,6 +149,17 @@ module JIRA
100
149
  response
101
150
  end
102
151
 
152
+ # Makes a multipart request to the JIRA server using the oauth gem.
153
+ #
154
+ # This is used for file uploads.
155
+ #
156
+ # Generally you should not call this method directly, but use the helper methods in JIRA::Client.
157
+ #
158
+ # @param [String] url The JIRA REST URL to call
159
+ # @param [Hash] data The Net::HTTP::Post::Multipart data to send with the request
160
+ # @param [Hash] headers The headers to send with the request
161
+ # @return [Net::HTTPResponse] The response object
162
+ # @raise [JIRA::HTTPError] If the response is not an HTTP success code
103
163
  def make_multipart_request(url, data, headers = {})
104
164
  request = Net::HTTP::Post::Multipart.new url, data, headers
105
165
 
@@ -110,6 +170,8 @@ module JIRA
110
170
  response
111
171
  end
112
172
 
173
+ # Returns true if the client is authenticated.
174
+ # @return [Boolean] True if the client is authenticated
113
175
  def authenticated?
114
176
  @authenticated
115
177
  end
@@ -5,11 +5,21 @@ require 'json'
5
5
  require 'net/https'
6
6
 
7
7
  module JIRA
8
+ # Base class for request clients specific to a particular authentication method.
8
9
  class RequestClient
10
+ # Makes the JIRA REST API call.
11
+ #
9
12
  # Returns the response if the request was successful (HTTP::2xx) and
10
13
  # raises a JIRA::HTTPError if it was not successful, with the response
11
14
  # attached.
12
-
15
+ #
16
+ # Generally you should not call this method directly, but use derived classes.
17
+ #
18
+ # File uploads are not supported with this method. Use request_multipart instead.
19
+ #
20
+ # @param [Array] args Arguments to pass to the request method
21
+ # @return [Net::HTTPResponse] The response from the server
22
+ # @raise [JIRA::HTTPError] if it was not successful
13
23
  def request(*args)
14
24
  response = make_request(*args)
15
25
  raise HTTPError, response unless response.is_a?(Net::HTTPSuccess)
@@ -17,6 +27,15 @@ module JIRA
17
27
  response
18
28
  end
19
29
 
30
+ # Makes a multipart request to the JIRA server.
31
+ #
32
+ # This is used for file uploads.
33
+ #
34
+ # Generally you should not call this method directly, but use derived classes.
35
+ #
36
+ # @param [Array] args Arguments to pass to the request method
37
+ # @return [Net::HTTPResponse] The response from the server
38
+ # @raise [JIRA::HTTPError] if it was not successful
20
39
  def request_multipart(*args)
21
40
  response = make_multipart_request(*args)
22
41
  raise HTTPError, response unless response.is_a?(Net::HTTPSuccess)
@@ -24,10 +43,16 @@ module JIRA
24
43
  response
25
44
  end
26
45
 
46
+ # Abstract method to make a request to the JIRA server.
47
+ # @abstract
48
+ # @param [Array] args Arguments to pass to the request method
27
49
  def make_request(*args)
28
50
  raise NotImplementedError
29
51
  end
30
52
 
53
+ # Abstract method to make a request to the JIRA server with a file upload.
54
+ # @abstract
55
+ # @param [Array] args Arguments to pass to the request method
31
56
  def make_multipart_request(*args)
32
57
  raise NotImplementedError
33
58
  end
@@ -9,14 +9,85 @@ module JIRA
9
9
  delegate_to_target_class :meta
10
10
  end
11
11
 
12
+ # This class provides the Attachment object <-> REST mapping for JIRA::Resource::Attachment derived class,
13
+ # i.e. the Create, Retrieve, Update, Delete lifecycle methods.
14
+ #
15
+ # == Lifecycle methods
16
+ #
17
+ # === Retrieving a single attachment by Issue and attachment id
18
+ #
19
+ # # Find attachment with id 30076 on issue SUP-3000.
20
+ # issue = JIRA::Resource::Issue.find(client, 'SUP-3000', { fields: 'attachment' } )
21
+ # attachment = issue.attachments.find do |attachment_curr|
22
+ # 30076 == attachment_curr.id.to_i
23
+ # end
24
+ #
25
+ # === Retrieving meta information for an attachments
26
+ #
27
+ # attachment.meta
28
+ #
29
+ # === Retrieving file contents of attachment
30
+ #
31
+ # content = URI.open(attachment.content).read
32
+ # content = attachment.download_contents
33
+ # content = attachment.download_file { |file| file.read }
34
+ #
35
+ # === Adding an attachment to an issue
36
+ #
37
+ # Dir.mktmpdir do |dir|
38
+ # path = File.join(dir, filename)
39
+ # IO.copy_stream(file.path, path)
40
+ #
41
+ # issue = JIRA::Resource::Issue.find(client, 'SUP-3000', { fields: 'attachment' } )
42
+ # attachment = issue.attachments.build
43
+ # attachment.save!( { file: path, mimeType: content_type } )
44
+ # end
45
+ #
46
+ # === Deleting an attachment
47
+ #
48
+ # attachment.delete
49
+ #
50
+ # @!method save(attrs, path = url)
51
+ # Uploads a file as an attachment to its issue.
52
+ #
53
+ # Filename used will be the basename of the given file.
54
+ #
55
+ # @param [Hash] attrs the options to create a message with.
56
+ # @option attrs [IO,String] :file The file to upload, either a file object or a file path to find the file.
57
+ # @option attrs [String] :mimeType The MIME type of the file.
58
+ # @return [Boolean] True if successful, false if failed.
59
+ #
60
+ # @!attribute [r] self
61
+ # @return [String] URL to JSON of this attachment
62
+ # @!attribute [r] filename
63
+ # @return [String] the filename
64
+ # @!attribute [r] author
65
+ # @return [JIRA::Resource::User] the user who created the attachment
66
+ # @!attribute [r] created
67
+ # @return [String] timestamp when the attachment was created
68
+ # @!attribute [r] size
69
+ # @return [Integer] the file size
70
+ # @!attribute [r] mimeType
71
+ # @return [String] MIME of the content type
72
+ # @!attribute [r] content
73
+ # @return [String] URL (not the contents) to download the contents of the attachment
74
+ # @!attribute [r] thumbnail
75
+ # @return [String] URL to download the thumbnail of the attachment
76
+ #
12
77
  class Attachment < JIRA::Base
13
78
  belongs_to :issue
14
79
  has_one :author, class: JIRA::Resource::User
15
80
 
81
+ # @private
16
82
  def self.endpoint_name
17
83
  'attachments'
18
84
  end
19
85
 
86
+ # Gets metadata about attachments on the server.
87
+ # @example Return metadata
88
+ # Attachment.meta(client)
89
+ # => { "enabled" => true, "uploadLimit" => 1000000 }
90
+ # @return [Hash] The metadata for attachments. (See example.)
20
91
  def self.meta(client)
21
92
  response = client.get("#{client.options[:rest_base_path]}/attachment/meta")
22
93
  parse_json(response.body)
@@ -42,23 +113,37 @@ module JIRA
42
113
  # @param [Hash] headers Any additional headers to call Jira.
43
114
  # @yield |file|
44
115
  # @yieldparam [IO] file The IO object streaming the download.
45
- def download_file(headers = {}, &block)
116
+ def download_file(headers = {}, &)
46
117
  default_headers = client.options[:default_headers]
47
- URI.parse(content).open(default_headers.merge(headers), &block)
118
+ URI.parse(content).open(default_headers.merge(headers), &)
48
119
  end
49
120
 
50
121
  # Downloads the file contents as a string object.
51
122
  #
52
123
  # Note that this reads the contents into a ruby string in memory.
53
- # A file might be very large so it is recommend to avoid this unless you are certain about doing so.
124
+ # A file might be very large so it is recommended to avoid this unless you are certain about doing so.
54
125
  # Use the download_file method instead and avoid calling the read method without a limit.
55
126
  #
127
+ # @example Save file contents to a string.
128
+ # content = download_contents
56
129
  # @param [Hash] headers Any additional headers to call Jira.
57
130
  # @return [String,NilClass] The file contents.
58
131
  def download_contents(headers = {})
59
132
  download_file(headers, &:read)
60
133
  end
61
134
 
135
+ # Uploads a file as an attachment to its issue.
136
+ #
137
+ # Filename used will be the basename of the given file.
138
+ #
139
+ # @example Save a file as an attachment
140
+ # issue = JIRA::Resource::Issue.find(client, 'SUP-3000', { fields: 'attachment' } )
141
+ # attachment = issue.attachments.build
142
+ # attachment.save!( { file: path, mimeType: 'text/plain' } )
143
+ # @param [Hash] attrs the options to create a message with.
144
+ # @option attrs [IO,String] :file The file to upload, either a file object or a file path to find the file.
145
+ # @option attrs [String] :mimeType The MIME type of the file.
146
+ # @raise [JIRA::HTTPError] if failed
62
147
  def save!(attrs, path = url)
63
148
  file = attrs['file'] || attrs[:file] # Keep supporting 'file' parameter as a string for backward compatibility
64
149
  mime_type = attrs[:mimeType] || 'application/binary'
@@ -21,7 +21,6 @@ module JIRA
21
21
 
22
22
  def self.map_fields(client)
23
23
  field_map = {}
24
- field_map_reverse = {}
25
24
  fields = client.Field.all
26
25
 
27
26
  # two pass approach, so that a custom field with the same name
@@ -30,7 +29,6 @@ module JIRA
30
29
  next if f.custom
31
30
 
32
31
  name = safe_name(f.name)
33
- field_map_reverse[f.id] = [f.name, name] # capture both the official name, and the mapped name
34
32
  field_map[name] = f.id
35
33
  end
36
34
 
@@ -44,23 +42,21 @@ module JIRA
44
42
  else
45
43
  safe_name(f.name)
46
44
  end
47
- field_map_reverse[f.id] = [f.name, name] # capture both the official name, and the mapped name
48
45
  field_map[name] = f.id
49
46
  end
50
47
 
51
- client.cache.field_map_reverse = field_map_reverse # not sure where this will be used yet, but sure to be useful
52
- client.cache.field_map = field_map
48
+ client.field_map_cache = field_map
53
49
  end
54
50
 
55
51
  def self.field_map(client)
56
- client.cache.field_map
52
+ client.field_map_cache
57
53
  end
58
54
 
59
55
  def self.name_to_id(client, field_name)
60
56
  field_name = field_name.to_s
61
- return field_name unless client.cache.field_map && client.cache.field_map[field_name]
57
+ return field_name unless client.field_map_cache && client.field_map_cache[field_name]
62
58
 
63
- client.cache.field_map[field_name]
59
+ client.field_map_cache[field_name]
64
60
  end
65
61
 
66
62
  def respond_to?(method_name, _include_all = false)
@@ -8,6 +8,35 @@ module JIRA
8
8
  class IssueFactory < JIRA::BaseFactory # :nodoc:
9
9
  end
10
10
 
11
+ # This class provides the Issue object <-> REST mapping for JIRA::Resource::Issue derived class,
12
+ # i.e. the Create, Retrieve, Update, Delete lifecycle methods.
13
+ #
14
+ # == Lifecycle methods
15
+ #
16
+ # === Retrieving all issues
17
+ #
18
+ # client.Issue.all
19
+ #
20
+ # === Retrieving a single issue
21
+ #
22
+ # options = { expand: 'editmeta' }
23
+ # issue = client.Issue.find("SUP-3000", options)
24
+ #
25
+ # === Creating a new issue
26
+ #
27
+ # issue = client.Issue.build(fields: { summary: 'New issue', project: { key: 'SUP' }, issuetype: { name: 'Bug' } })
28
+ # issue.save
29
+ #
30
+ # === Updating an issue
31
+ #
32
+ # issue = client.Issue.find("SUP-3000")
33
+ # issue.save(fields: { summary: 'Updated issue' })
34
+ #
35
+ # === Deleting an issue
36
+ #
37
+ # issue = client.Issue.find("SUP-3000")
38
+ # issue.delete
39
+ #
11
40
  class Issue < JIRA::Base
12
41
  has_one :reporter, class: JIRA::Resource::User, nested_under: 'fields'
13
42
  has_one :assignee, class: JIRA::Resource::User, nested_under: 'fields'
@@ -29,13 +58,16 @@ module JIRA
29
58
  has_many :remotelink, class: JIRA::Resource::Remotelink
30
59
  has_many :watchers, attribute_key: 'watches', nested_under: %w[fields watches]
31
60
 
61
+ # Get collection of issues.
62
+ # @param client [JIRA::Client]
63
+ # @return [Array<JIRA::Resource::Issue>]
32
64
  def self.all(client)
33
65
  start_at = 0
34
66
  max_results = 1000
35
67
  result = []
36
68
  loop do
37
69
  url = client.options[:rest_base_path] +
38
- "/search?expand=transitions.fields&maxResults=#{max_results}&startAt=#{start_at}"
70
+ "/search/jql?expand=transitions.fields&maxResults=#{max_results}&startAt=#{start_at}"
39
71
  response = client.get(url)
40
72
  json = parse_json(response.body)
41
73
  json['issues'].map do |issue|
@@ -48,36 +80,51 @@ module JIRA
48
80
  result
49
81
  end
50
82
 
51
- def self.jql(client, jql, options = { fields: nil, start_at: nil, max_results: nil, expand: nil,
52
- validate_query: true })
53
- url = client.options[:rest_base_path] + "/search?jql=#{CGI.escape(jql)}"
83
+ def self.jql(client, jql, options = { fields: nil, max_results: nil, expand: nil, reconcile_issues: nil })
84
+ url = client.options[:rest_base_path] + "/search/jql?jql=#{CGI.escape(jql)}"
54
85
 
55
86
  if options[:fields]
56
87
  url << "&fields=#{options[:fields].map do |value|
57
88
  CGI.escape(client.Field.name_to_id(value))
58
89
  end.join(',')}"
59
90
  end
60
- url << "&startAt=#{CGI.escape(options[:start_at].to_s)}" if options[:start_at]
61
91
  url << "&maxResults=#{CGI.escape(options[:max_results].to_s)}" if options[:max_results]
62
- url << '&validateQuery=false' if options[:validate_query] === false
92
+ url << "&reconcileIssues=#{CGI.escape(options[:reconcile_issues].to_s)}" if options[:reconcile_issues]
63
93
 
64
94
  if options[:expand]
65
95
  options[:expand] = [options[:expand]] if options[:expand].is_a?(String)
66
96
  url << "&expand=#{options[:expand].to_a.map { |value| CGI.escape(value.to_s) }.join(',')}"
67
97
  end
68
98
 
69
- response = client.get(url)
70
- json = parse_json(response.body)
71
- return json['total'] if options[:max_results] && (options[:max_results]).zero?
99
+ issues = []
100
+ next_page_token = nil
101
+ json = {}
102
+ while json['isLast'] != true
103
+ page_url = url.dup
104
+ page_url << "&nextPageToken=#{next_page_token}" if next_page_token
72
105
 
73
- json['issues'].map do |issue|
74
- client.Issue.build(issue)
106
+ response = client.get(page_url)
107
+ json = parse_json(response.body)
108
+ return json['total'] if options[:max_results]&.zero?
109
+
110
+ next_page_token = json['nextPageToken']
111
+ json['issues'].map do |issue|
112
+ issues << client.Issue.build(issue)
113
+ end
75
114
  end
115
+ issues
76
116
  end
77
117
 
78
118
  # Fetches the attributes for the specified resource from JIRA unless
79
119
  # the resource is already expanded and the optional force reload flag
80
120
  # is not set
121
+ # @param [Boolean] reload
122
+ # @param [Hash] query_params
123
+ # @option query_params [String] :fields
124
+ # @option query_params [String] :expand
125
+ # @option query_params [Integer] :startAt
126
+ # @option query_params [Integer] :maxResults
127
+ # @return [void]
81
128
  def fetch(reload = false, query_params = {})
82
129
  return if expanded? && !reload
83
130
 
@@ -101,6 +148,7 @@ validate_query: true })
101
148
  json['fields']
102
149
  end
103
150
 
151
+ # @private
104
152
  def respond_to?(method_name, _include_all = false)
105
153
  if attrs.key?('fields') && [method_name.to_s, client.Field.name_to_id(method_name)].any? do |k|
106
154
  attrs['fields'].key?(k)
@@ -111,6 +159,7 @@ validate_query: true })
111
159
  end
112
160
  end
113
161
 
162
+ # @private
114
163
  def method_missing(method_name, *args, &)
115
164
  if attrs.key?('fields')
116
165
  if attrs['fields'].key?(method_name.to_s)
@@ -127,6 +176,26 @@ validate_query: true })
127
176
  super
128
177
  end
129
178
  end
179
+
180
+ # @!method self.find(client, key, options = {})
181
+ # Gets/fetches an issue from JIRA.
182
+ #
183
+ # Note: attachments are not fetched by default.
184
+ #
185
+ # @param [JIRA::Client] client
186
+ # @param [String] key the key of the issue to find
187
+ # @param [Hash] options the options to find the issue with
188
+ # @option options [String] :fields the fields to include in the response
189
+ # @return [JIRA::Resource::Issue] the found issue
190
+ # @example Find an issue
191
+ # JIRA::Resource::Issue.find(client, "SUP-3000", { fields: %w[summary description attachment created ] } )
192
+ #
193
+ # @!method self.build(attrs = {})
194
+ # Constructs a new issue object.
195
+ # @param [Hash] attrs the attributes to initialize the issue with
196
+ # @return [JIRA::Resource::Issue] the new issue
197
+ #
198
+ # .
130
199
  end
131
200
  end
132
201
  end
@@ -9,7 +9,7 @@ module JIRA
9
9
  has_many :sections, class: JIRA::Resource::IssuePickerSuggestionsIssue
10
10
 
11
11
  def self.all(client, query = '', options = { current_jql: nil, current_issue_key: nil, current_project_id: nil,
12
- show_sub_tasks: nil, show_sub_tasks_parent: nil })
12
+ show_sub_tasks: nil, show_sub_tasks_parent: nil })
13
13
  url = client.options[:rest_base_path] + "/issue/picker?query=#{CGI.escape(query)}"
14
14
 
15
15
  url << "&currentJQL=#{CGI.escape(options[:current_jql])}" if options[:current_jql]
@@ -5,9 +5,10 @@ module JIRA
5
5
  class IssuelinkFactory < JIRA::BaseFactory # :nodoc:
6
6
  end
7
7
 
8
- # Because of circular dependency Issue->IssueLink->Issue
9
- # we have to declare JIRA::Resource::Issue class.
10
- class Issue < JIRA::Base; end
8
+ class Issue < JIRA::Base
9
+ # Because of circular dependency Issue->IssueLink->Issue
10
+ # we have to declare JIRA::Resource::Issue class.
11
+ end
11
12
 
12
13
  class Issuelink < JIRA::Base
13
14
  has_one :type, class: JIRA::Resource::Issuelinktype
@@ -17,7 +17,7 @@ module JIRA
17
17
 
18
18
  # Returns all the issues for this project
19
19
  def issues(options = {})
20
- search_url = "#{client.options[:rest_base_path]}/search"
20
+ search_url = "#{client.options[:rest_base_path]}/search/jql"
21
21
  query_params = { jql: "project=\"#{key}\"" }
22
22
  query_params.update Base.query_params_for_search(options)
23
23
  response = client.get(url_with_query_params(search_url, query_params))
@@ -20,14 +20,14 @@ module JIRA
20
20
  end
21
21
 
22
22
  def add_issue(issue)
23
- add_issues([issue])
23
+ add_issues([issue]).first
24
24
  end
25
25
 
26
26
  def add_issues(issues)
27
27
  issue_ids = issues.map(&:id)
28
28
  request_body = { issues: issue_ids }.to_json
29
29
  client.post("#{agile_path}/issue", request_body)
30
- true
30
+ issues
31
31
  end
32
32
 
33
33
  def start_date
@@ -28,7 +28,7 @@ module JIRA
28
28
 
29
29
  def save!(user_id, path = nil)
30
30
  path ||= new_record? ? url : patched_url
31
- response = client.post(path, user_id.to_json)
31
+ client.post(path, user_id.to_json)
32
32
  true
33
33
  end
34
34
  end
@@ -16,8 +16,12 @@ module JIRA
16
16
  client.options[:context_path] + REST_BASE_PATH
17
17
  end
18
18
 
19
+ def self.singular_path(client, key, prefix = '/')
20
+ "#{full_url(client)}#{prefix}/#{endpoint_name}/#{key}"
21
+ end
22
+
19
23
  def self.collection_path(client, prefix = '/')
20
- full_url(client) + prefix + endpoint_name
24
+ "#{full_url(client)}#{prefix}/#{endpoint_name}"
21
25
  end
22
26
 
23
27
  def self.all(client, options = {})
data/lib/jira/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JIRA
4
- VERSION = '3.0.0.beta1'
4
+ VERSION = '3.0.0'
5
5
  end
@@ -12,7 +12,7 @@ namespace :jira do
12
12
  desc 'Run the system call to generate a RSA public certificate'
13
13
  task :generate_public_cert do
14
14
  puts "Executing 'openssl req -x509 -nodes -newkey rsa:1024 -sha1 -keyout rsakey.pem -out rsacert.pem'"
15
- system('openssl req -x509 -subj "/C=US/ST=New York/L=New York/O=SUMO Heavy Industries/CN=www.sumoheavy.com" -nodes -newkey rsa:1024 -sha1 -keyout rsakey.pem -out rsacert.pem') # rubocop:disable Layout/LineLength
15
+ system('openssl req -x509 -subj "/C=US/ST=New York/L=New York/O=SUMO Heavy Industries/CN=www.sumoheavy.com" -nodes -newkey rsa:1024 -sha1 -keyout rsakey.pem -out rsacert.pem')
16
16
  puts "Done. The RSA-SHA1 private keyfile is in the current directory: 'rsakey.pem'."
17
17
  puts 'You will need to copy the following certificate into your application link configuration in Jira:'
18
18
  system('cat rsacert.pem')
@@ -45,10 +45,10 @@ describe JIRA::Resource::Issue do
45
45
  end
46
46
 
47
47
  before do
48
- stub_request(:get, "#{site_url}/jira/rest/api/2/search?expand=transitions.fields&maxResults=1000&startAt=0")
48
+ stub_request(:get, "#{site_url}/jira/rest/api/2/search/jql?expand=transitions.fields&maxResults=1000&startAt=0")
49
49
  .to_return(status: 200, body: get_mock_response('issue.json'))
50
50
 
51
- stub_request(:get, "#{site_url}/jira/rest/api/2/search?expand=transitions.fields&maxResults=1000&startAt=11")
51
+ stub_request(:get, "#{site_url}/jira/rest/api/2/search/jql?expand=transitions.fields&maxResults=1000&startAt=11")
52
52
  .to_return(status: 200, body: get_mock_response('empty_issues.json'))
53
53
  end
54
54
 
@@ -23,14 +23,14 @@ describe JIRA::Resource::Project do
23
23
 
24
24
  describe 'issues' do
25
25
  it 'returns all the issues' do
26
- stub_request(:get, "#{site_url}/jira/rest/api/2/search?jql=project=\"SAMPLEPROJECT\"")
26
+ stub_request(:get, "#{site_url}/jira/rest/api/2/search/jql?jql=project=\"SAMPLEPROJECT\"")
27
27
  .to_return(status: 200, body: get_mock_response('project/SAMPLEPROJECT.issues.json'))
28
28
  subject = client.Project.build('key' => key)
29
29
  issues = subject.issues
30
30
  expect(issues.length).to eq(11)
31
31
  issues.each do |issue|
32
32
  expect(issue.class).to eq(JIRA::Resource::Issue)
33
- expect(issue.expanded?).to be_falsey
33
+ expect(issue).not_to be_expanded
34
34
  end
35
35
  end
36
36
  end
@@ -46,7 +46,7 @@ describe JIRA::Resource::RapidView do
46
46
 
47
47
  stub_request(
48
48
  :get,
49
- "#{site_url}/jira/rest/api/2/search?jql=id IN(10001, 10000)"
49
+ "#{site_url}/jira/rest/api/2/search/jql?jql=id IN(10001, 10000)"
50
50
  ).to_return(
51
51
  status: 200,
52
52
  body: get_mock_response('rapidview/SAMPLEPROJECT.issues.full.json')
@@ -54,7 +54,7 @@ describe JIRA::Resource::RapidView do
54
54
 
55
55
  stub_request(
56
56
  :get,
57
- "#{site_url}/jira/rest/api/2/search?jql=id IN(10000, 10001) AND sprint IS NOT EMPTY"
57
+ "#{site_url}/jira/rest/api/2/search/jql?jql=id IN(10000, 10001) AND sprint IS NOT EMPTY"
58
58
  ).to_return(
59
59
  status: 200,
60
60
  body: get_mock_response('rapidview/SAMPLEPROJECT.issues.full.json')
@@ -66,7 +66,7 @@ describe JIRA::Resource::RapidView do
66
66
 
67
67
  issues.each do |issue|
68
68
  expect(issue.class).to eq(JIRA::Resource::Issue)
69
- expect(issue.expanded?).to be_falsey
69
+ expect(issue).not_to be_expanded
70
70
  end
71
71
  end
72
72
  end