From 9cbccb92effde25ea2d6e962006b24634c508e97 Mon Sep 17 00:00:00 2001 From: Peter Bell Date: Sat, 20 Jun 2020 10:39:56 +0100 Subject: [PATCH 01/54] This updates the REST API used to pull sprint details Was using Grasshopper. Switched to using the newer agile API. Ran all RSpecs and they pass. Does away with the need to query rapidView. --- lib/jira/resource/sprint.rb | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/lib/jira/resource/sprint.rb b/lib/jira/resource/sprint.rb index 74beffd4..3ea7caf5 100644 --- a/lib/jira/resource/sprint.rb +++ b/lib/jira/resource/sprint.rb @@ -48,7 +48,7 @@ def get_sprint_details_attribute(attribute_name) def get_sprint_details search_url = - "#{client.options[:site]}#{client.options[:client_path]}/rest/greenhopper/1.0/rapid/charts/sprintreport?rapidViewId=#{rapidview_id}&sprintId=#{id}" + "#{client.options[:site]}#{client.options[:client_path]}/rest/agile/1.0/sprint/#{id}" begin response = client.get(search_url) rescue StandardError @@ -56,24 +56,12 @@ def get_sprint_details end json = self.class.parse_json(response.body) - @start_date = Date.parse(json['sprint']['startDate']) unless json['sprint']['startDate'] == 'None' - @end_date = Date.parse(json['sprint']['endDate']) unless json['sprint']['endDate'] == 'None' - @completed_date = Date.parse(json['sprint']['completeDate']) unless json['sprint']['completeDate'] == 'None' + @start_date = json['sprint']['startDate'] && Date.parse(json['sprint']['startDate']) + @end_date = json['sprint']['endDate'] && Date.parse(json['sprint']['endDate']) + @completed_date = json['sprint']['completeDate'] && Date.parse(json['sprint']['completeDate']) @sprint_report = client.SprintReport.build(json['contents']) end - def rapidview_id - return @attrs['rapidview_id'] if @attrs['rapidview_id'] - search_url = client.options[:site] + '/secure/GHGoToBoard.jspa?sprintId=' + id.to_s - begin - response = client.get(search_url) - rescue JIRA::HTTPError => error - return unless error.response.instance_of? Net::HTTPFound - rapid_view_match = /rapidView=(\d+)&/.match(error.response['location']) - @attrs['rapidview_id'] = rapid_view_match[1] unless rapid_view_match.nil? - end - end - def save(attrs = {}, _path = nil) attrs = @attrs if attrs.empty? super(attrs, agile_path) From e3eb3d168ab4afe0d368cb6b985d7e3c34503bdb Mon Sep 17 00:00:00 2001 From: Allan Date: Thu, 13 Aug 2020 13:47:29 +1000 Subject: [PATCH 02/54] Update example.rb Created example for adding an attachment. --- example.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/example.rb b/example.rb index 5e92b808..f332c74e 100644 --- a/example.rb +++ b/example.rb @@ -166,6 +166,14 @@ # # -------------------------- # issue.comments.first.save({"body" => "an updated comment frome example.rb"}) + +# # Add attachment to Issue +# # ------------------------ +# issue = client.Issue.find('PROJ-1') +# attachment = issue.attachments.build +# attachment.save('file': '/path/to/file') +# + # List all available link types # ------------------------------ pp client.Issuelinktype.all From aa51e3c51265dc90b41ddd7a6cdd9a08fdbedce9 Mon Sep 17 00:00:00 2001 From: Jake Helbig Date: Sun, 27 Sep 2020 14:06:00 -0500 Subject: [PATCH 03/54] This change will only effect object inspections, typically used in the printing of the object to console or log. All tests ran and passed. This resolves #266. --- lib/jira/client.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/jira/client.rb b/lib/jira/client.rb index 813ecc7c..a401bac0 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -299,6 +299,11 @@ def request(http_method, path, body = '', headers = {}) @request_client.request(http_method, path, body, headers) end + # Stops sensitive client information from being displayed in logs + def inspect + "#" + end + protected def merge_default_headers(headers) From 0a2d9e6b9c5592933af86494a0ffa6035b2ea613 Mon Sep 17 00:00:00 2001 From: Arturo Herrero Date: Tue, 15 Dec 2020 12:13:35 +0100 Subject: [PATCH 04/54] Add use_cookies to the available options list --- lib/jira/client.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/jira/client.rb b/lib/jira/client.rb index a401bac0..522e8d80 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -6,7 +6,7 @@ module JIRA # This class is the main access point for all JIRA::Resource instances. # # The client must be initialized with an options hash containing - # configuration options. The available options are: + # configuration options. The available options are: # # :site => 'http://localhost:2990', # :context_path => '/jira', @@ -29,6 +29,7 @@ module JIRA # :proxy_port => nil, # :proxy_username => nil, # :proxy_password => nil, + # :use_cookies => nil, # :additional_cookies => nil, # :default_headers => {}, # :use_client_cert => false, @@ -79,6 +80,7 @@ class Client :proxy_port, :proxy_username, :proxy_password, + :use_cookies, :additional_cookies, :default_headers, :use_client_cert, From b592f0ee72ffdc16b8ad6eb468e846dd6b62da06 Mon Sep 17 00:00:00 2001 From: Simon Lacroix Date: Wed, 30 Dec 2020 00:23:47 +0100 Subject: [PATCH 05/54] Bump version to 2.1.4 --- lib/jira/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jira/version.rb b/lib/jira/version.rb index 7efe598e..b0bb1168 100644 --- a/lib/jira/version.rb +++ b/lib/jira/version.rb @@ -1,3 +1,3 @@ module JIRA - VERSION = '2.1.3'.freeze + VERSION = '2.1.4'.freeze end From 05ded69919ca377643b6111d7134d508186822a7 Mon Sep 17 00:00:00 2001 From: toda Date: Thu, 21 Jan 2021 14:13:52 +0900 Subject: [PATCH 06/54] fix user all endpoint --- lib/jira/resource/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jira/resource/user.rb b/lib/jira/resource/user.rb index 1529628f..fc2705d3 100644 --- a/lib/jira/resource/user.rb +++ b/lib/jira/resource/user.rb @@ -18,7 +18,7 @@ def self.singular_path(client, key, prefix = '/') # Cannot retrieve more than 1,000 users through the api, please see: https://jira.atlassian.com/browse/JRASERVER-65089 def self.all(client) - response = client.get("/rest/api/2/user/search?username=_&maxResults=#{MAX_RESULTS}") + response = client.get("/rest/api/2/users/search?username=_&maxResults=#{MAX_RESULTS}") all_users = JSON.parse(response.body) all_users.flatten.uniq.map do |user| From bfc142f4332d0a355c485ae4d3a1c76d1f2f85cd Mon Sep 17 00:00:00 2001 From: toda Date: Thu, 21 Jan 2021 14:14:07 +0900 Subject: [PATCH 07/54] fix test --- spec/integration/user_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/integration/user_spec.rb b/spec/integration/user_spec.rb index 930cbf7f..efe66e1f 100644 --- a/spec/integration/user_spec.rb +++ b/spec/integration/user_spec.rb @@ -21,18 +21,18 @@ describe '#all' do let(:client) do client = double(options: { rest_base_path: '/jira/rest/api/2' }) - allow(client).to receive(:get).with('/rest/api/2/user/search?username=_&maxResults=1000').and_return(JIRA::Resource::UserFactory.new(client)) + allow(client).to receive(:get).with('/rest/api/2/users/search?username=_&maxResults=1000').and_return(JIRA::Resource::UserFactory.new(client)) client end before do allow(client).to receive(:get) - .with('/rest/api/2/user/search?username=_&maxResults=1000') { OpenStruct.new(body: '["User1"]') } + .with('/rest/api/2/users/search?username=_&maxResults=1000') { OpenStruct.new(body: '["User1"]') } allow(client).to receive_message_chain(:User, :build).with('users') { [] } end it 'gets users with maxResults of 1000' do - expect(client).to receive(:get).with('/rest/api/2/user/search?username=_&maxResults=1000') + expect(client).to receive(:get).with('/rest/api/2/users/search?username=_&maxResults=1000') expect(client).to receive_message_chain(:User, :build).with('User1') JIRA::Resource::User.all(client) end From ef841063dbd6320ba14ebc327286bc9d4d51289a Mon Sep 17 00:00:00 2001 From: Simon Lacroix Date: Sat, 23 Jan 2021 15:21:08 +0100 Subject: [PATCH 08/54] Bump version to 2.1.5 --- lib/jira/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jira/version.rb b/lib/jira/version.rb index b0bb1168..bd3da133 100644 --- a/lib/jira/version.rb +++ b/lib/jira/version.rb @@ -1,3 +1,3 @@ module JIRA - VERSION = '2.1.4'.freeze + VERSION = '2.1.5'.freeze end From c26577597694dc15051862e409ada9769e08ee04 Mon Sep 17 00:00:00 2001 From: David Alpert Date: Sat, 23 Jan 2021 11:40:00 -0600 Subject: [PATCH 09/54] comment out failing specs getting latest on the main branch and running rspec had failures. Finished in 0.72355 seconds (files took 0.76971 seconds to load) 482 examples, 9 failures Failed examples: rspec ./spec/integration/issue_spec.rb[1:3:1:1] # JIRA::Resource::Issue GET all issues it should behave like a resource with a collection GET endpoint should get the collection rspec ./spec/integration/issue_spec.rb[1:8:1] # JIRA::Resource::Issue errors fails to save when fields and update are missing rspec ./spec/integration/issue_spec.rb[1:9:1:1] # JIRA::Resource::Issue GET jql issues it should behave like a resource with JQL inputs and a collection GET endpoint should get the collection rspec ./spec/integration/project_spec.rb[1:5] # JIRA::Resource::Project returns a collection of components rspec ./spec/integration/project_spec.rb[1:4:1] # JIRA::Resource::Project issues returns all the issues rspec ./spec/integration/rapidview_spec.rb[1:2:1:1] # JIRA::Resource::RapidView GET all rapidviews it should behave like a resource with a collection GET endpoint should get the collection rspec ./spec/integration/rapidview_spec.rb[1:3:1] # JIRA::Resource::RapidView issues should return all the issues rspec ./spec/integration/watcher_spec.rb[1:1:1] # JIRA::Resource::Watcher watchers should returns all the watchers rspec ./spec/integration/watcher_spec.rb[1:1:2] # JIRA::Resource::Watcher watchers should add a watcher these specs all appear to be missing an rsakey.pem file which is expected but not included in the repo and not stubbed out. Errno::ENOENT: No such file or directory @ rb_sysopen - rsakey.pem # .../gem/ruby/2.5.0/gems/oauth-0.5.5/lib/oauth/signature/rsa/sha1.rb:39:in `read' I see that this is expected and there is a :prepare rake task which would tell me how to resolve these errors. This commit adds that :prepare task as an explicit dependency of the :spec task so that anyone running rake spec will also invoke the :prepare task --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 983806e8..f12add91 100644 --- a/Rakefile +++ b/Rakefile @@ -20,7 +20,7 @@ end desc 'Run RSpec tests' # RSpec::Core::RakeTask.new(:spec) -RSpec::Core::RakeTask.new(:spec) do |task| +RSpec::Core::RakeTask.new(:spec, [] => [:prepare]) do |task| task.rspec_opts = ['--color', '--format', 'doc'] end From 4d4a907a410ac98553df918ff48cd23d1d7c039b Mon Sep 17 00:00:00 2001 From: David Alpert Date: Sat, 23 Jan 2021 11:45:55 -0600 Subject: [PATCH 10/54] invoke the jira:generate_public_cert task before running specs the test and spec targets previously asserted that an rsa keypair was avilable on disk (as a dependency of the integration tests) and raised an error if it was missing with guidance as to how to create it this commit invokes that rake task automatically if required, allowing the first run of the :test or :spec targets to complete without error. --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index f12add91..cdb06d6f 100644 --- a/Rakefile +++ b/Rakefile @@ -14,7 +14,7 @@ desc 'Prepare and run rspec tests' task :prepare do rsa_key = File.expand_path('rsakey.pem') unless File.exist?(rsa_key) - raise 'rsakey.pem does not exist, tests will fail. Run `rake jira:generate_public_cert` first' + Rake::Task['jira:generate_public_cert'].invoke end end From 2128c846534514abdff4e3cd742d739aaea338d7 Mon Sep 17 00:00:00 2001 From: Ilya Kamenko Date: Fri, 15 Oct 2021 17:43:34 +0300 Subject: [PATCH 11/54] Issue picker suggestions --- lib/jira-ruby.rb | 3 + lib/jira/client.rb | 4 + lib/jira/resource/issue_picker_suggestions.rb | 24 ++++++ .../issue_picker_suggestions_issue.rb | 10 +++ lib/jira/resource/suggested_issue.rb | 9 +++ .../resource/issue_picker_suggestions_spec.rb | 79 +++++++++++++++++++ .../jira_picker_suggestions_issue_spec.rb | 18 +++++ 7 files changed, 147 insertions(+) create mode 100644 lib/jira/resource/issue_picker_suggestions.rb create mode 100644 lib/jira/resource/issue_picker_suggestions_issue.rb create mode 100644 lib/jira/resource/suggested_issue.rb create mode 100644 spec/jira/resource/issue_picker_suggestions_spec.rb create mode 100644 spec/jira/resource/jira_picker_suggestions_issue_spec.rb diff --git a/lib/jira-ruby.rb b/lib/jira-ruby.rb index b4250cf2..1629051c 100644 --- a/lib/jira-ruby.rb +++ b/lib/jira-ruby.rb @@ -25,6 +25,9 @@ require 'jira/resource/applinks' require 'jira/resource/issuelinktype' require 'jira/resource/issuelink' +require 'jira/resource/suggested_issue' +require 'jira/resource/issue_picker_suggestions_issue' +require 'jira/resource/issue_picker_suggestions' require 'jira/resource/remotelink' require 'jira/resource/sprint' require 'jira/resource/sprint_report' diff --git a/lib/jira/client.rb b/lib/jira/client.rb index 522e8d80..8ac91b3a 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -257,6 +257,10 @@ def Issuelinktype JIRA::Resource::IssuelinktypeFactory.new(self) end + def IssuePickerSuggestions + JIRA::Resource::IssuePickerSuggestionsFactory.new(self) + end + def Remotelink JIRA::Resource::RemotelinkFactory.new(self) end diff --git a/lib/jira/resource/issue_picker_suggestions.rb b/lib/jira/resource/issue_picker_suggestions.rb new file mode 100644 index 00000000..2834c16a --- /dev/null +++ b/lib/jira/resource/issue_picker_suggestions.rb @@ -0,0 +1,24 @@ +module JIRA + module Resource + class IssuePickerSuggestionsFactory < JIRA::BaseFactory # :nodoc: + end + + class IssuePickerSuggestions < JIRA::Base + has_many :sections, class: JIRA::Resource::IssuePickerSuggestionsIssue + + def self.all(client, query = '', options = { current_jql: nil, current_issue_key: nil, current_project_id: nil, show_sub_tasks: nil, show_sub_tasks_parent: nil }) + url = client.options[:rest_base_path] + "/issue/picker?query=#{CGI.escape(query)}" + + url << "¤tJQL=#{CGI.escape(options[:current_jql])}" if options[:current_jql] + url << "¤tIssueKey=#{CGI.escape(options[:current_issue_key])}" if options[:current_issue_key] + url << "¤tProjectId=#{CGI.escape(options[:current_project_id])}" if options[:current_project_id] + url << "&showSubTasks=#{options[:show_sub_tasks]}" if options[:show_sub_tasks] + url << "&showSubTaskParent=#{options[:show_sub_task_parent]}" if options[:show_sub_task_parent] + + response = client.get(url) + json = parse_json(response.body) + client.IssuePickerSuggestions.build(json) + end + end + end +end diff --git a/lib/jira/resource/issue_picker_suggestions_issue.rb b/lib/jira/resource/issue_picker_suggestions_issue.rb new file mode 100644 index 00000000..4d54c90b --- /dev/null +++ b/lib/jira/resource/issue_picker_suggestions_issue.rb @@ -0,0 +1,10 @@ +module JIRA + module Resource + class IssuePickerSuggestionsIssueFactory < JIRA::BaseFactory # :nodoc: + end + + class IssuePickerSuggestionsIssue < JIRA::Base + has_many :issues, class: JIRA::Resource::SuggestedIssue + end + end +end diff --git a/lib/jira/resource/suggested_issue.rb b/lib/jira/resource/suggested_issue.rb new file mode 100644 index 00000000..7fe7d00d --- /dev/null +++ b/lib/jira/resource/suggested_issue.rb @@ -0,0 +1,9 @@ +module JIRA + module Resource + class SuggestedIssueFactory < JIRA::BaseFactory # :nodoc: + end + + class SuggestedIssue < JIRA::Base + end + end +end diff --git a/spec/jira/resource/issue_picker_suggestions_spec.rb b/spec/jira/resource/issue_picker_suggestions_spec.rb new file mode 100644 index 00000000..b6826a6a --- /dev/null +++ b/spec/jira/resource/issue_picker_suggestions_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe JIRA::Resource::IssuePickerSuggestions do + let(:client) do + double('client', options: { + rest_base_path: '/jira/rest/api/2' + }) + end + + describe 'relationships' do + subject do + JIRA::Resource::IssuePickerSuggestions.new(client, attrs: { + 'sections' => [{ 'id' => 'hs'}, { 'id' => 'cs' }] + }) + end + + it 'has the correct relationships' do + expect(subject).to have_many(:sections, JIRA::Resource::IssuePickerSuggestionsIssue) + expect(subject.sections.length).to eq(2) + end + end + + describe '#all' do + let(:response) { double } + let(:issue_picker_suggestions) { double } + + before do + allow(response).to receive(:body).and_return('{"sections":[{"id": "cs"}]}') + allow(client).to receive(:IssuePickerSuggestions).and_return(issue_picker_suggestions) + allow(issue_picker_suggestions).to receive(:build) + end + + it 'should autocomplete issues' do + allow(response).to receive(:body).and_return('{"sections":[{"id": "cs"}]}') + expect(client).to receive(:get).with('/jira/rest/api/2/issue/picker?query=query') + .and_return(response) + + expect(client).to receive(:IssuePickerSuggestions).and_return(issue_picker_suggestions) + expect(issue_picker_suggestions).to receive(:build).with('sections' => [{ 'id' => 'cs' }]) + + JIRA::Resource::IssuePickerSuggestions.all(client, 'query') + end + + it 'should autocomplete issues with current jql' do + expect(client).to receive(:get).with('/jira/rest/api/2/issue/picker?query=query¤tJQL=project+%3D+PR') + .and_return(response) + + JIRA::Resource::IssuePickerSuggestions.all(client, 'query', current_jql: 'project = PR') + end + + it 'should autocomplete issues with current issue jey' do + expect(client).to receive(:get).with('/jira/rest/api/2/issue/picker?query=query¤tIssueKey=PR-42') + .and_return(response) + + JIRA::Resource::IssuePickerSuggestions.all(client, 'query', current_issue_key: 'PR-42') + end + + it 'should autocomplete issues with current project id' do + expect(client).to receive(:get).with('/jira/rest/api/2/issue/picker?query=query¤tProjectId=PR') + .and_return(response) + + JIRA::Resource::IssuePickerSuggestions.all(client, 'query', current_project_id: 'PR') + end + + it 'should autocomplete issues with show sub tasks' do + expect(client).to receive(:get).with('/jira/rest/api/2/issue/picker?query=query&showSubTasks=true') + .and_return(response) + + JIRA::Resource::IssuePickerSuggestions.all(client, 'query', show_sub_tasks: true) + end + + it 'should autocomplete issues with show sub tasks parent' do + expect(client).to receive(:get).with('/jira/rest/api/2/issue/picker?query=query&showSubTaskParent=true') + .and_return(response) + + JIRA::Resource::IssuePickerSuggestions.all(client, 'query', show_sub_task_parent: true) + end + end +end diff --git a/spec/jira/resource/jira_picker_suggestions_issue_spec.rb b/spec/jira/resource/jira_picker_suggestions_issue_spec.rb new file mode 100644 index 00000000..c584b87a --- /dev/null +++ b/spec/jira/resource/jira_picker_suggestions_issue_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe JIRA::Resource::IssuePickerSuggestionsIssue do + let(:client) { double('client') } + + describe 'relationships' do + subject do + JIRA::Resource::IssuePickerSuggestionsIssue.new(client, attrs: { + 'issues' => [{ 'id' => '1'}, { 'id' => '2' }] + }) + end + + it 'has the correct relationships' do + expect(subject).to have_many(:issues, JIRA::Resource::SuggestedIssue) + expect(subject.issues.length).to eq(2) + end + end +end From 5cf40699fb63c3ba3efe3f528aef5c2df25b1415 Mon Sep 17 00:00:00 2001 From: Orgad Shaneh Date: Thu, 16 Dec 2021 13:44:34 +0200 Subject: [PATCH 12/54] Fix loading with active_support 7 Fixes #385 --- lib/jira-ruby.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/jira-ruby.rb b/lib/jira-ruby.rb index b4250cf2..2193efb6 100644 --- a/lib/jira-ruby.rb +++ b/lib/jira-ruby.rb @@ -1,5 +1,6 @@ $LOAD_PATH << __dir__ +require 'active_support' require 'active_support/inflector' ActiveSupport::Inflector.inflections do |inflector| inflector.singular /status$/, 'status' From 126e31b85a3a4cc8bba46a5e9f8ed1452a0cab7a Mon Sep 17 00:00:00 2001 From: Simon Lacroix Date: Fri, 31 Dec 2021 17:59:39 +0100 Subject: [PATCH 13/54] Bump version to 2.2.0 --- lib/jira/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jira/version.rb b/lib/jira/version.rb index bd3da133..179f2889 100644 --- a/lib/jira/version.rb +++ b/lib/jira/version.rb @@ -1,3 +1,3 @@ module JIRA - VERSION = '2.1.5'.freeze + VERSION = '2.2.0'.freeze end From 5fedad5768fde40871a4c4a54918093a5cb92ca4 Mon Sep 17 00:00:00 2001 From: Peter Ruan Date: Thu, 6 Jan 2022 20:50:39 -0800 Subject: [PATCH 14/54] added example on how to get Personal Access Token authentication to work --- README.md | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 66231eb0..41cc3552 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ defaults to HTTP Basic Auth. Jira supports cookie based authentication whereby user credentials are passed to JIRA via a JIRA REST API call. This call returns a session cookie which must -then be sent to all following JIRA REST API calls. +then be sent to all following JIRA REST API calls. To enable cookie based authentication, set `:auth_type` to `:cookie`, set `:use_cookies` to `true` and set `:username` and `:password` accordingly. @@ -114,7 +114,7 @@ options = { :context_path => '', :auth_type => :cookie, # Set cookie based authentication :use_cookies => true, # Send cookies with each request - :additional_cookies => ['AUTH=vV7uzixt0SScJKg7'] # Optional cookies to send + :additional_cookies => ['AUTH=vV7uzixt0SScJKg7'] # Optional cookies to send # with each request } @@ -134,15 +134,39 @@ cookie to add to the request. Some authentication schemes that require additional cookies ignore the username and password sent in the JIRA REST API call. For those use cases, `:username` -and `:password` may be omitted from `options`. +and `:password` may be omitted from `options`. +## Configuring JIRA to use Personal Access Tokens Auth +If your JIRA system is configured to support Personal Access Token authorization, minor modifications are needed in how credentials are communicated to the server. Specifically, the paremeters `:username` and `:password` are not needed. Also, the parameter `:default_headers` is needed to contain the api_token, which can be obtained following the official documentation from [Atlassian](https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html). Please note that the Personal Access Token can only be used as it is. If it is encoded (with base64 or any other encoding method) then the token will not work correctly and authentication will fail. + +```ruby +require 'jira-ruby' + +# NOTE: the token should not be encoded +api_token = API_TOKEN_OBTAINED_FROM_JIRA_UI + +options = { + :site => 'http://mydomain.atlassian.net:443/', + :context_path => '', + :auth_type => :basic, + :default_headers => { 'Authorization' => "Bearer #{api_token}"} +} + +client = JIRA::Client.new(options) + +project = client.Project.find('SAMPLEPROJECT') + +project.issues.each do |issue| + puts "#{issue.id} - #{issue.summary}" +end +``` ## Using the API Gem in a command line application Using HTTP Basic Authentication, configure and connect a client to your instance of JIRA. Note: If your Jira install is hosted on [atlassian.net](atlassian.net), it will have no context -path by default. If you're having issues connecting, try setting context_path +path by default. If you're having issues connecting, try setting context_path to an empty string in the options hash. ```ruby From 7b65fb5cb71f8998005f72fbe82f7f8b3321dbeb Mon Sep 17 00:00:00 2001 From: Kanstantsin Shautsou Date: Tue, 1 Feb 2022 01:27:08 +0300 Subject: [PATCH 15/54] Allow adding multiple issues --- lib/jira/resource/sprint.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/jira/resource/sprint.rb b/lib/jira/resource/sprint.rb index 74beffd4..50f76969 100644 --- a/lib/jira/resource/sprint.rb +++ b/lib/jira/resource/sprint.rb @@ -18,7 +18,11 @@ def issues(options = {}) end def add_issue(issue) - request_body = { issues: [issue.id] }.to_json + add_issues([issue.id]) + end + + def add_issues(issues) + request_body = { issues: issues }.to_json response = client.post("#{agile_path}/issue", request_body) true end From 4b46a2b0febe0f1ddacbe2e60e32728d872ea126 Mon Sep 17 00:00:00 2001 From: Peter Tseng Date: Tue, 5 Apr 2022 00:54:42 +0000 Subject: [PATCH 16/54] HttpClient: Allow specifying ca_file I'm working in an environment where we have our own certificate authority, and I need to pass a CA file. It doesn't look like there's currently a way to do so. I see a similar patch was submitted in https://github.com/sumoheavy/jira-ruby/pull/279 and it was just withdrawn because the submitter didn't need it anymore. Now that I'm seeing a need, I wonder whether there is a possibility of adding it. I see a monkey-patch solution being offered in https://github.com/sumoheavy/jira-ruby/issues/140#issuecomment-196534011 but my colleagues are leery of monkey-patching. I also see from my searches that perhaps setting the environment variable SSL_CERT_FILE may be able to achieve this, but we're also a little leery of an implicit reliance on the environment variable, and think it's better to be able to explicitly specify the ca file. --- lib/jira/client.rb | 1 + lib/jira/http_client.rb | 1 + spec/jira/http_client_spec.rb | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/lib/jira/client.rb b/lib/jira/client.rb index 8ac91b3a..b777766e 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -40,6 +40,7 @@ module JIRA # :key_path => nil, # :ssl_client_cert => nil, # :ssl_client_key => nil + # :ca_file => nil # # See the JIRA::Base class methods for all of the available methods on these accessor # objects. diff --git a/lib/jira/http_client.rb b/lib/jira/http_client.rb index e68a5371..bdd89ea6 100644 --- a/lib/jira/http_client.rb +++ b/lib/jira/http_client.rb @@ -59,6 +59,7 @@ def http_conn(uri) http_conn.verify_mode = @options[:ssl_verify_mode] http_conn.ssl_version = @options[:ssl_version] if @options[:ssl_version] http_conn.read_timeout = @options[:read_timeout] + http_conn.ca_file = @options[:ca_file] if @options[:ca_file] http_conn end diff --git a/spec/jira/http_client_spec.rb b/spec/jira/http_client_spec.rb index 11b22943..e184ea67 100644 --- a/spec/jira/http_client_spec.rb +++ b/spec/jira/http_client_spec.rb @@ -285,6 +285,11 @@ expect(basic_client_cert_client.http_conn(uri)).to eq(http_conn) end + it 'can use a certificate authority file' do + client = JIRA::HttpClient.new(JIRA::Client::DEFAULT_OPTIONS.merge(ca_file: '/opt/custom.ca.pem')) + expect(client.http_conn(client.uri).ca_file).to eql('/opt/custom.ca.pem') + end + it 'returns a http connection' do http_conn = double uri = double From da88f1ace7dee331ea105b3b78cc37fc4bbffc19 Mon Sep 17 00:00:00 2001 From: texpert Date: Sat, 21 May 2022 20:40:26 +0300 Subject: [PATCH 17/54] Fix JIRA::OauthClient.request_token block passing to get_request_token --- lib/jira/oauth_client.rb | 2 +- spec/jira/oauth_client_spec.rb | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/jira/oauth_client.rb b/lib/jira/oauth_client.rb index fcfface8..bcbed350 100644 --- a/lib/jira/oauth_client.rb +++ b/lib/jira/oauth_client.rb @@ -46,7 +46,7 @@ def init_oauth_consumer(_options) # Returns the current request token if it is set, else it creates # and sets a new token. def request_token(options = {}, *arguments, &block) - @request_token ||= get_request_token(options, *arguments, block) + @request_token ||= get_request_token(options, *arguments, &block) end # Sets the request token from a given token and secret. diff --git a/spec/jira/oauth_client_spec.rb b/spec/jira/oauth_client_spec.rb index 048b1d68..c94c3e30 100644 --- a/spec/jira/oauth_client_spec.rb +++ b/spec/jira/oauth_client_spec.rb @@ -35,6 +35,26 @@ expect(oauth_client.get_request_token).to eq(request_token) end + it 'could pre-process the response body in a block' do + response = Net::HTTPSuccess.new(1.0, '200', 'OK') + allow_any_instance_of(OAuth::Consumer).to receive(:request).and_return(response) + allow(response).to receive(:body).and_return('&oauth_token=token&oauth_token_secret=secret&password=top_secret') + + result = oauth_client.request_token do |response_body| + CGI.parse(response_body).each_with_object({}) do |(k, v), h| + next if k == 'password' + + h[k.strip.to_sym] = v.first + end + end + + expect(result).to be_an_instance_of(OAuth::RequestToken) + expect(result.consumer).to eql(oauth_client.consumer) + expect(result.params[:oauth_token]).to eql('token') + expect(result.params[:oauth_token_secret]).to eql('secret') + expect(result.params[:password]).to be_falsey + end + it 'allows setting the request token' do token = double expect(OAuth::RequestToken).to receive(:new).with(oauth_client.consumer, 'foo', 'bar').and_return(token) @@ -159,4 +179,4 @@ end end end -end \ No newline at end of file +end From 4cc02b4d27f141bde4450da285708aba2116032e Mon Sep 17 00:00:00 2001 From: Luke Duncalfe Date: Wed, 7 Sep 2022 15:25:55 +1200 Subject: [PATCH 18/54] Update specs to work with Ruby 3 --- spec/jira/oauth_client_spec.rb | 2 +- spec/jira/resource/issue_picker_suggestions_spec.rb | 2 +- spec/jira/resource/issue_spec.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/jira/oauth_client_spec.rb b/spec/jira/oauth_client_spec.rb index 048b1d68..03959b5a 100644 --- a/spec/jira/oauth_client_spec.rb +++ b/spec/jira/oauth_client_spec.rb @@ -58,7 +58,7 @@ request_token = OAuth::RequestToken.new(oauth_client.consumer) allow(oauth_client).to receive(:get_request_token).and_return(request_token) mock_access_token = double - expect(request_token).to receive(:get_access_token).with(oauth_verifier: 'abc123').and_return(mock_access_token) + expect(request_token).to receive(:get_access_token).with({ oauth_verifier: 'abc123' }).and_return(mock_access_token) oauth_client.init_access_token(oauth_verifier: 'abc123') expect(oauth_client.access_token).to eq(mock_access_token) end diff --git a/spec/jira/resource/issue_picker_suggestions_spec.rb b/spec/jira/resource/issue_picker_suggestions_spec.rb index b6826a6a..6cbc3512 100644 --- a/spec/jira/resource/issue_picker_suggestions_spec.rb +++ b/spec/jira/resource/issue_picker_suggestions_spec.rb @@ -36,7 +36,7 @@ .and_return(response) expect(client).to receive(:IssuePickerSuggestions).and_return(issue_picker_suggestions) - expect(issue_picker_suggestions).to receive(:build).with('sections' => [{ 'id' => 'cs' }]) + expect(issue_picker_suggestions).to receive(:build).with({ 'sections' => [{ 'id' => 'cs' }] }) JIRA::Resource::IssuePickerSuggestions.all(client, 'query') end diff --git a/spec/jira/resource/issue_spec.rb b/spec/jira/resource/issue_spec.rb index 585200c2..7c9c9157 100644 --- a/spec/jira/resource/issue_spec.rb +++ b/spec/jira/resource/issue_spec.rb @@ -47,7 +47,7 @@ class JIRAResourceDelegation < SimpleDelegator # :nodoc: .and_return(empty_response) expect(client).to receive(:Issue).and_return(issue) - expect(issue).to receive(:build).with('id' => '1', 'summary' => 'Bugs Everywhere') + expect(issue).to receive(:build).with({ 'id' => '1', 'summary' => 'Bugs Everywhere' }) issues = JIRA::Resource::Issue.all(client) end From e805074aedb98778c18947c0ed630cc6065ea287 Mon Sep 17 00:00:00 2001 From: Rafael Sales Date: Fri, 7 Oct 2022 21:45:55 -0300 Subject: [PATCH 19/54] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 41cc3552..eb55f720 100644 --- a/README.md +++ b/README.md @@ -148,8 +148,9 @@ api_token = API_TOKEN_OBTAINED_FROM_JIRA_UI options = { :site => 'http://mydomain.atlassian.net:443/', :context_path => '', - :auth_type => :basic, - :default_headers => { 'Authorization' => "Bearer #{api_token}"} + :username => '', + :password => api_token, + :auth_type => :basic } client = JIRA::Client.new(options) From d5c0ef38ec3a013fc0157c5c41f31ae775cdb195 Mon Sep 17 00:00:00 2001 From: Simon Lacroix Date: Mon, 23 Jan 2023 21:06:36 +0000 Subject: [PATCH 20/54] Bump to version 2.3.0 --- lib/jira/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jira/version.rb b/lib/jira/version.rb index 179f2889..09231406 100644 --- a/lib/jira/version.rb +++ b/lib/jira/version.rb @@ -1,3 +1,3 @@ module JIRA - VERSION = '2.2.0'.freeze + VERSION = '2.3.0'.freeze end From 4404aaf671ce4ca10e8593093ee0d6ccb4fa579f Mon Sep 17 00:00:00 2001 From: Spike Ilacqua Date: Tue, 14 Feb 2023 09:14:28 -0700 Subject: [PATCH 21/54] Allow overriding Net::HTTP max_retries --- lib/jira/client.rb | 2 ++ lib/jira/http_client.rb | 1 + spec/jira/http_client_spec.rb | 22 ++++++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/lib/jira/client.rb b/lib/jira/client.rb index b777766e..b1723e87 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -34,6 +34,7 @@ module JIRA # :default_headers => {}, # :use_client_cert => false, # :read_timeout => nil, + # :max_retries => nil, # :http_debug => false, # :shared_secret => nil, # :cert_path => nil, @@ -86,6 +87,7 @@ class Client :default_headers, :use_client_cert, :read_timeout, + :max_retries, :http_debug, :issuer, :base_url, diff --git a/lib/jira/http_client.rb b/lib/jira/http_client.rb index bdd89ea6..a021b7c4 100644 --- a/lib/jira/http_client.rb +++ b/lib/jira/http_client.rb @@ -59,6 +59,7 @@ def http_conn(uri) http_conn.verify_mode = @options[:ssl_verify_mode] http_conn.ssl_version = @options[:ssl_version] if @options[:ssl_version] http_conn.read_timeout = @options[:read_timeout] + http_conn.max_retries = @options[:max_retries] if @options[:max_retries] http_conn.ca_file = @options[:ca_file] if @options[:ca_file] http_conn end diff --git a/spec/jira/http_client_spec.rb b/spec/jira/http_client_spec.rb index e184ea67..abaa0ac7 100644 --- a/spec/jira/http_client_spec.rb +++ b/spec/jira/http_client_spec.rb @@ -69,6 +69,13 @@ JIRA::HttpClient.new(options) end + let(:basic_client_with_max_retries) do + options = JIRA::Client::DEFAULT_OPTIONS.merge(JIRA::HttpClient::DEFAULT_OPTIONS).merge( + max_retries: 2 + ) + JIRA::HttpClient.new(options) + end + let(:response) do response = double('response') allow(response).to receive(:kind_of?).with(Net::HTTPSuccess).and_return(true) @@ -290,6 +297,21 @@ expect(client.http_conn(client.uri).ca_file).to eql('/opt/custom.ca.pem') end + it 'allows overriding max_retries' do + http_conn = double + uri = double + host = double + port = double + expect(uri).to receive(:host).and_return(host) + expect(uri).to receive(:port).and_return(port) + expect(Net::HTTP).to receive(:new).with(host, port).and_return(http_conn) + expect(http_conn).to receive(:use_ssl=).with(basic_client.options[:use_ssl]).and_return(http_conn) + expect(http_conn).to receive(:verify_mode=).with(basic_client.options[:ssl_verify_mode]).and_return(http_conn) + expect(http_conn).to receive(:read_timeout=).with(basic_client.options[:read_timeout]).and_return(http_conn) + expect(http_conn).to receive(:max_retries=).with(basic_client_with_max_retries.options[:max_retries]).and_return(http_conn) + expect(basic_client_with_max_retries.http_conn(uri)).to eq(http_conn) + end + it 'returns a http connection' do http_conn = double uri = double From db56ded95926d0e50e9c680602d4afc388293766 Mon Sep 17 00:00:00 2001 From: Ian Chesal Date: Thu, 10 Aug 2023 09:28:33 -0400 Subject: [PATCH 22/54] chore(gems): Upgrade to OAuth 1.0.x OAuth 0.6 goes EOL around April 2024. Move to 1.0.x. From the bundler output when you install the current version of this gem you get: > You have installed oauth version 0.6.2, congratulations! > > Non-commercial support for the 0.6.x series will end by April, 2024. > Please upgrade to 1.0.x as soon as possible! The only breaking change > will be dropped support for Ruby 2.4, 2.5, and 2.6. --- jira-ruby.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jira-ruby.gemspec b/jira-ruby.gemspec index 02f01703..dc156bc4 100644 --- a/jira-ruby.gemspec +++ b/jira-ruby.gemspec @@ -22,7 +22,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'activesupport' s.add_runtime_dependency 'atlassian-jwt' s.add_runtime_dependency 'multipart-post' - s.add_runtime_dependency 'oauth', '~> 0.5', '>= 0.5.0' + s.add_runtime_dependency 'oauth', '~> 1.0' # Development Dependencies s.add_development_dependency 'guard', '~> 2.13', '>= 2.13.0' From dd36a368ea13d5e6a4c2c1c618c783ada965f88e Mon Sep 17 00:00:00 2001 From: Nicolas Buero Date: Wed, 16 Aug 2023 18:23:34 -0300 Subject: [PATCH 23/54] Remove Travis CI --- .travis.yml | 9 --------- README.md | 1 - 2 files changed, 10 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6f17a8a2..00000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: ruby -rvm: - - 2.4 - - 2.5 - - 2.6 - - 2.7 -before_script: - - rake jira:generate_public_cert -script: bundle exec rake spec diff --git a/README.md b/README.md index eb55f720..b504e216 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # JIRA API Gem [![Code Climate](https://codeclimate.com/github/sumoheavy/jira-ruby.svg)](https://codeclimate.com/github/sumoheavy/jira-ruby) -[![Build Status](https://travis-ci.org/sumoheavy/jira-ruby.svg?branch=master)](https://travis-ci.org/sumoheavy/jira-ruby) This gem provides access to the Atlassian JIRA REST API. From fa90f538fb59e492a0108c708c73b979fbf09595 Mon Sep 17 00:00:00 2001 From: Nicolas Buero Date: Wed, 16 Aug 2023 18:24:00 -0300 Subject: [PATCH 24/54] Add Github Actions CI with newer ruby versions and updated README --- .github/workflows/CI.yml | 31 +++++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 32 insertions(+) create mode 100644 .github/workflows/CI.yml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 00000000..76ff2fc2 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,31 @@ +name: Ruby + +on: [push] + +jobs: + test: + name: CI-tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby: + - '3.1' + - '3.0' + - '2.7' + - '2.6' + - '2.5' + - '2.4' + steps: + - uses: actions/checkout@v3 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - name: Run tests + run: | + bundle exec rake jira:generate_public_cert + bundle exec rake spec diff --git a/README.md b/README.md index b504e216..9fa261be 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # JIRA API Gem [![Code Climate](https://codeclimate.com/github/sumoheavy/jira-ruby.svg)](https://codeclimate.com/github/sumoheavy/jira-ruby) +[![Build Status](https://github.com/sumoheavy/jira-ruby/actions/workflows/CI.yml/badge.svg)](https://github.com/sumoheavy/jira-ruby/actions/workflows/CI.yml) This gem provides access to the Atlassian JIRA REST API. From f82e82e022cb4af0f71080b4de94703395f486a4 Mon Sep 17 00:00:00 2001 From: gat-developer <73225579+gat-developer@users.noreply.github.com> Date: Mon, 25 Sep 2023 14:49:05 +0000 Subject: [PATCH 25/54] moves jwt to headers insted of query string jwt in the query string is not supported anymore by Jira REST API --- lib/jira/jwt_client.rb | 62 +++++++++---------------------- spec/jira/client_spec.rb | 4 +- spec/jira/jwt_uri_builder_spec.rb | 59 ----------------------------- 3 files changed, 20 insertions(+), 105 deletions(-) delete mode 100644 spec/jira/jwt_uri_builder_spec.rb diff --git a/lib/jira/jwt_client.rb b/lib/jira/jwt_client.rb index 033301ff..843d810a 100644 --- a/lib/jira/jwt_client.rb +++ b/lib/jira/jwt_client.rb @@ -4,64 +4,38 @@ module JIRA class JwtClient < HttpClient def make_request(http_method, url, body = '', headers = {}) @http_method = http_method + jwt_header = build_jwt_header(url) - super(http_method, url, body, headers) + super(http_method, url, body, headers.merge(jwt_header)) end def make_multipart_request(url, data, headers = {}) @http_method = :post + jwt_header = build_jwt_header(url) - super(url, data, headers) - end - - class JwtUriBuilder - attr_reader :request_url, :http_method, :shared_secret, :site, :issuer - - def initialize(request_url, http_method, shared_secret, site, issuer) - @request_url = request_url - @http_method = http_method - @shared_secret = shared_secret - @site = site - @issuer = issuer - end - - def build - uri = URI.parse(request_url) - new_query = URI.decode_www_form(String(uri.query)) << ['jwt', jwt_header] - uri.query = URI.encode_www_form(new_query) - - return uri.to_s unless uri.is_a?(URI::HTTP) - - uri.request_uri - end - - private - - def jwt_header - claim = Atlassian::Jwt.build_claims \ - issuer, - request_url, - http_method.to_s, - site, - (Time.now - 60).to_i, - (Time.now + 86_400).to_i - - JWT.encode claim, shared_secret - end + super(url, data, headers.merge(jwt_header)) end private attr_reader :http_method - def request_path(url) - JwtUriBuilder.new( + def build_jwt_header(url) + jwt = build_jwt(url) + + {'Authorization' => "JWT #{jwt}"} + end + + def build_jwt(url) + claim = Atlassian::Jwt.build_claims \ + @options[:issuer], url, http_method.to_s, - @options[:shared_secret], @options[:site], - @options[:issuer] - ).build + (Time.now - 60).to_i, + (Time.now + 86_400).to_i + + JWT.encode claim, @options[:shared_secret] end end -end +end \ No newline at end of file diff --git a/spec/jira/client_spec.rb b/spec/jira/client_spec.rb index f8f44dee..fd5b78ef 100644 --- a/spec/jira/client_spec.rb +++ b/spec/jira/client_spec.rb @@ -232,7 +232,7 @@ before(:each) do stub_request(:get, 'https://localhost:2990/jira/rest/api/2/project') - .with(query: hash_including(:jwt)) + .with(headers: {"Authorization" => /JWT .+/}) .to_return(status: 200, body: '[]', headers: {}) end @@ -248,7 +248,7 @@ context 'with a incorrect jwt key' do before do stub_request(:get, 'https://localhost:2990/jira/rest/api/2/project') - .with(query: hash_including(:jwt)) + .with(headers: {"Authorization" => /JWT .+/}) .to_return(status: 401, body: '[]', headers: {}) end diff --git a/spec/jira/jwt_uri_builder_spec.rb b/spec/jira/jwt_uri_builder_spec.rb deleted file mode 100644 index 0e210d12..00000000 --- a/spec/jira/jwt_uri_builder_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'spec_helper' - -describe JIRA::JwtClient::JwtUriBuilder do - subject(:url_builder) do - JIRA::JwtClient::JwtUriBuilder.new(url, http_method, shared_secret, site, issuer) - end - - let(:url) { '/foo' } - let(:http_method) { :get } - let(:shared_secret) { 'shared_secret' } - let(:site) { 'http://localhost:2990' } - let(:issuer) { nil } - - describe '#build' do - subject { url_builder.build } - - it 'includes the jwt param' do - expect(subject).to include('?jwt=') - end - - context 'when the url already contains params' do - let(:url) { '/foo?expand=projects.issuetypes.fields' } - - it 'includes the jwt param' do - expect(subject).to include('&jwt=') - end - end - - context 'with a complete url' do - let(:url) { 'http://localhost:2990/rest/api/2/issue/createmeta' } - - it 'includes the jwt param' do - expect(subject).to include('?jwt=') - end - - it { is_expected.to start_with('/') } - - it 'contains only one ?' do - expect(subject.count('?')).to eq(1) - end - end - - context 'with a complete url containing a param' do - let(:url) do - 'http://localhost:2990/rest/api/2/issue/createmeta?expand=projects.issuetypes.fields' - end - - it 'includes the jwt param' do - expect(subject).to include('&jwt=') - end - - it { is_expected.to start_with('/') } - - it 'contains only one ?' do - expect(subject.count('?')).to eq(1) - end - end - end -end From b2994b41977668f84b5a30512baec9cb76341c1b Mon Sep 17 00:00:00 2001 From: Marlin Pierce Date: Sun, 29 Oct 2023 13:33:04 -0400 Subject: [PATCH 26/54] Pass client default headers to fix bug where the default headers were not passed for posting multipart. --- lib/jira/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jira/client.rb b/lib/jira/client.rb index b777766e..ba921ce1 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -291,7 +291,7 @@ def post(path, body = '', headers = {}) def post_multipart(path, file, headers = {}) puts "post multipart: #{path} - [#{file}]" if @http_debug - @request_client.request_multipart(path, file, headers) + @request_client.request_multipart(path, file, merge_default_headers(headers)) end def put(path, body = '', headers = {}) From bf7512643b27201c0023d49b4ad80712f06066f4 Mon Sep 17 00:00:00 2001 From: "R.J. Robinson" Date: Mon, 27 Nov 2023 15:11:03 -0500 Subject: [PATCH 27/54] Update README.md --- README.md | 98 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index eb55f720..b39b6b49 100644 --- a/README.md +++ b/README.md @@ -11,27 +11,103 @@ Join our Slack channel! You can find us [here](https://jira-ruby-slackin.herokua ## Example usage -```ruby -require 'rubygems' -require 'jira-ruby' +# Jira Ruby API - Sample Usage + +This sample usage demonstrates how you can interact with JIRA's API using the [jira-ruby gem](https://github.com/sumoheavy/jira-ruby). + +### Dependencies + +Before running, install the `jira-ruby` gem: + +```shell +gem install jira-ruby +``` +### Sample Usage +Connect to JIRA +Firstly, establish a connection with your JIRA instance by providing a few configuration parameters: + • private_key_file: The path to your RSA private key file. + • consumer_key: Your consumer key. + • site: The URL of your JIRA instance. + +```ruby options = { - :username => 'username', - :password => 'pass1234', - :site => 'http://mydomain.atlassian.net:443/', - :context_path => '', - :auth_type => :basic + :private_key_file => "rsakey.pem", + :context_path => '', + :consumer_key => 'your_consumer_key', + :site => 'your_jira_instance_url' } client = JIRA::Client.new(options) +``` -project = client.Project.find('SAMPLEPROJECT') +### Retrieve and Display Projects -project.issues.each do |issue| - puts "#{issue.id} - #{issue.summary}" +After establishing the connection, you can fetch all projects and display their key and name: +```ruby +projects = client.Project.all +projects.each do |project| + puts "Project -> key: #{project.key}, name: #{project.name}" end ``` +### Handling Fields by Name +The jira-ruby gem allows you to refer to fields by their custom names rather than identifiers. Make sure to map fields before using them: + +```ruby +client.Field.map_fields +old_way = issue.customfield_12345 +# Note: The methods mapped here adopt a unique style combining PascalCase and snake_case conventions. +new_way = issue.Special_Field +``` + +### JQL Queries +To find issues based on specific criteria, you can use JIRA Query Language (JQL): + +```ruby +client.Issue.jql(a_normal_jql_search, fields:[:description, :summary, :Special_field, :created]) +``` + +### Several actions can be performed on the Issue object such as create, update, transition, delete, etc: +### Creating an Issue +```ruby +issue = client.Issue.build +labels = ['label1', 'label2'] +issue.save({ + "fields" => { + "summary" => "blarg from in example.rb", + "project" => {"key" => "SAMPLEPROJECT"}, + "issuetype" => {"id" => "3"}, + "labels" => labels, + "priority" => {"id" => "1"} + } +}) +``` + +### Updating/Transitioning an Issue +```ruby +issue = client.Issue.find("10002") +issue.save({"fields"=>{"summary"=>"EVEN MOOOOOOARRR NINJAAAA ```markdown +!"}}) + +issue_transition = issue.transitions.build +issue_transition.save!('transition' => {'id' => transition_id}) +``` +### Deleting an Issue +```ruby +issue = client.Issue.find('SAMPLEPROJECT-2') +issue.delete +``` +### Other Capabilities +Apart from the operations listed above, this API wrapper supports several other capabilities like: + • Searching for a user + • Retrieving an issue's watchers + • Changing the assignee of an issue + • Adding attachments and comments to issues + • Managing issue links and much more. + +Not all examples are shown in this README, refer to the complete script example for a full overview of the capabilities supported by this API wrapper. + ## Links to JIRA REST API documentation * [Overview](https://developer.atlassian.com/display/JIRADEV/JIRA+REST+APIs) From 2fd47bc86f2657fe3eab23d5c58e20e18d241a6a Mon Sep 17 00:00:00 2001 From: "R.J. Robinson" Date: Mon, 27 Nov 2023 15:20:58 -0500 Subject: [PATCH 28/54] Update README.md --- README.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b39b6b49..43dbbc18 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,10 @@ gem install jira-ruby ### Sample Usage Connect to JIRA Firstly, establish a connection with your JIRA instance by providing a few configuration parameters: - • private_key_file: The path to your RSA private key file. - • consumer_key: Your consumer key. - • site: The URL of your JIRA instance. +There are other ways to connect to JIRA listed below | [Personal Access Token](#configuring-jira-to-use-personal-access-tokens-auth) +- private_key_file: The path to your RSA private key file. +- consumer_key: Your consumer key. +- site: The URL of your JIRA instance. ```ruby options = { @@ -46,6 +47,7 @@ client = JIRA::Client.new(options) After establishing the connection, you can fetch all projects and display their key and name: ```ruby projects = client.Project.all + projects.each do |project| puts "Project -> key: #{project.key}, name: #{project.name}" end @@ -56,7 +58,9 @@ The jira-ruby gem allows you to refer to fields by their custom names rather ```ruby client.Field.map_fields + old_way = issue.customfield_12345 + # Note: The methods mapped here adopt a unique style combining PascalCase and snake_case conventions. new_way = issue.Special_Field ``` @@ -87,17 +91,18 @@ issue.save({ ### Updating/Transitioning an Issue ```ruby issue = client.Issue.find("10002") -issue.save({"fields"=>{"summary"=>"EVEN MOOOOOOARRR NINJAAAA ```markdown -!"}}) +issue.save({"fields"=>{"summary"=>"EVEN MOOOOOOARRR NINJAAAA!"}}) issue_transition = issue.transitions.build issue_transition.save!('transition' => {'id' => transition_id}) ``` + ### Deleting an Issue ```ruby issue = client.Issue.find('SAMPLEPROJECT-2') issue.delete ``` + ### Other Capabilities Apart from the operations listed above, this API wrapper supports several other capabilities like: • Searching for a user @@ -106,7 +111,7 @@ Apart from the operations listed above, this API wrapper supports several other • Adding attachments and comments to issues • Managing issue links and much more. -Not all examples are shown in this README, refer to the complete script example for a full overview of the capabilities supported by this API wrapper. +Not all examples are shown in this README; refer to the complete script example for a full overview of the capabilities supported by this API wrapper. ## Links to JIRA REST API documentation @@ -163,7 +168,7 @@ key. > After you have entered all the information click OK and ensure OAuth authentication is > enabled. -For 2 legged oauth in server mode only, not in cloud based JIRA, make sure to `Allow 2-Legged OAuth` +For two legged oauth in server mode only, not in cloud based JIRA, make sure to `Allow 2-Legged OAuth` ## Configuring JIRA to use HTTP Basic Auth From 2a1313e275e3897d06f0d56819350bb157a5ba07 Mon Sep 17 00:00:00 2001 From: Marlin Pierce Date: Sat, 2 Dec 2023 12:17:12 -0500 Subject: [PATCH 29/54] Wrote test for `Sprint#add_issues` method. Changed `Sprint#add_issues` method to take objects parallel to `Sprint#add_issue` method. --- lib/jira/resource/sprint.rb | 7 ++-- spec/jira/resource/sprint_spec.rb | 57 +++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/lib/jira/resource/sprint.rb b/lib/jira/resource/sprint.rb index 50f76969..ee50a7a7 100644 --- a/lib/jira/resource/sprint.rb +++ b/lib/jira/resource/sprint.rb @@ -18,12 +18,13 @@ def issues(options = {}) end def add_issue(issue) - add_issues([issue.id]) + add_issues( [ issue ]) end def add_issues(issues) - request_body = { issues: issues }.to_json - response = client.post("#{agile_path}/issue", request_body) + issue_ids = issues.map{ |issue| issue.id } + request_body = { issues: issue_ids }.to_json + client.post("#{agile_path}/issue", request_body) true end diff --git a/spec/jira/resource/sprint_spec.rb b/spec/jira/resource/sprint_spec.rb index d3f4c3c0..b766abb8 100644 --- a/spec/jira/resource/sprint_spec.rb +++ b/spec/jira/resource/sprint_spec.rb @@ -86,5 +86,62 @@ end end end + + context 'an issue exists' do + let(:issue_id) { 1001 } + let(:post_issue_path) do + described_class.agile_path(client, sprint.id) + "/jira/rest/agile/1.0/sprint//issue" + end + let(:issue) do + issue = double + allow(issue).to receive(:id).and_return(issue_id) + issue + end + let(:post_issue_input) do + {"issues":[issue.id]} + end + + + describe '#add_issu' do + context 'when an issue is passed' do + + it 'posts with the issue id' do + expect(client).to receive(:post).with(post_issue_path, post_issue_input.to_json) + + sprint.add_issue(issue) + end + end + end + end + + context 'multiple issues exists' do + let(:issue_ids) { [ 1001, 1012 ] } + let(:post_issue_path) do + described_class.agile_path(client, sprint.id) + "/jira/rest/agile/1.0/sprint//issue" + end + let(:issues) do + issue_ids.map do |issue_id| + issue = double + allow(issue).to receive(:id).and_return(issue_id) + issue + end + end + let(:post_issue_input) do + {"issues": issue_ids} + end + + describe '#add_issues' do + context 'when an issue is passed' do + + it 'posts with the issue id' do + expect(client).to receive(:post).with(post_issue_path, post_issue_input.to_json) + + sprint.add_issues(issues) + end + end + end + end end end From 927ebfa18e2ca04ab31efc34105c96c33f235a18 Mon Sep 17 00:00:00 2001 From: Marlin Pierce Date: Sun, 3 Dec 2023 07:33:39 -0500 Subject: [PATCH 30/54] Replace obsolete Net::HTTP::Proxy --- lib/jira/http_client.rb | 12 +++---- spec/jira/http_client_spec.rb | 63 +++++++++++++++++++++++++++++------ 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/lib/jira/http_client.rb b/lib/jira/http_client.rb index bdd89ea6..e920e21d 100644 --- a/lib/jira/http_client.rb +++ b/lib/jira/http_client.rb @@ -45,12 +45,12 @@ def basic_auth_http_conn end def http_conn(uri) - if @options[:proxy_address] - http_class = Net::HTTP::Proxy(@options[:proxy_address], @options[:proxy_port] || 80, @options[:proxy_username], @options[:proxy_password]) - else - http_class = Net::HTTP - end - http_conn = http_class.new(uri.host, uri.port) + http_conn = + if @options[:proxy_address] + Net::HTTP.new(uri.host, uri.port, @options[:proxy_address], @options[:proxy_port] || 80, @options[:proxy_username], @options[:proxy_password]) + else + Net::HTTP.new(uri.host, uri.port) + end http_conn.use_ssl = @options[:use_ssl] if @options[:use_client_cert] http_conn.cert = @options[:ssl_client_cert] diff --git a/spec/jira/http_client_spec.rb b/spec/jira/http_client_spec.rb index e184ea67..2f2300ba 100644 --- a/spec/jira/http_client_spec.rb +++ b/spec/jira/http_client_spec.rb @@ -81,6 +81,37 @@ response end + context 'simple client' do + let(:client) do + options_local = JIRA::Client::DEFAULT_OPTIONS.merge(JIRA::HttpClient::DEFAULT_OPTIONS).merge( + proxy_address: 'proxyAddress', + proxy_port: 42, + proxy_username: 'proxyUsername', + proxy_password: 'proxyPassword' + ) + JIRA::HttpClient.new(options_local) + end + + describe 'HttpClient#basic_auth_http_conn' do + subject(:http_conn) { basic_client.basic_auth_http_conn } + + it 'creates an instance of Net:HTTP for a basic auth client' do + + expect(http_conn.class).to eq(Net::HTTP) + end + + it 'the connection created has no proxy' do + + http_conn + + expect(http_conn.proxy_address).to be_nil + expect(http_conn.proxy_port).to be_nil + expect(http_conn.proxy_user).to be_nil + expect(http_conn.proxy_pass).to be_nil + end + end + end + it 'creates an instance of Net:HTTP for a basic auth client' do expect(basic_client.basic_auth_http_conn.class).to eq(Net::HTTP) end @@ -254,19 +285,29 @@ expect(proxy_configuration.proxy_pass).to be_nil end - it 'sets up a proxied http connection when using proxy options' do - uri = double - host = double - port = double + context 'client has proxy settings' do + let(:proxy_client) do + options_local = JIRA::Client::DEFAULT_OPTIONS.merge(JIRA::HttpClient::DEFAULT_OPTIONS).merge( + proxy_address: 'proxyAddress', + proxy_port: 42, + proxy_username: 'proxyUsername', + proxy_password: 'proxyPassword' + ) + JIRA::HttpClient.new(options_local) + end + subject(:proxy_conn) { proxy_client.basic_auth_http_conn } - expect(uri).to receive(:host).and_return(host) - expect(uri).to receive(:port).and_return(port) + describe 'HttpClient#basic_auth_http_conn' do + it 'creates a Net:HTTP instance for a basic auth client setting up a proxied http connection' do - proxy_configuration = proxy_client.http_conn(uri).class - expect(proxy_configuration.proxy_address).to eq(proxy_client.options[:proxy_address]) - expect(proxy_configuration.proxy_port).to eq(proxy_client.options[:proxy_port]) - expect(proxy_configuration.proxy_user).to eq(proxy_client.options[:proxy_username]) - expect(proxy_configuration.proxy_pass).to eq(proxy_client.options[:proxy_password]) + expect(proxy_conn.class).to eq(Net::HTTP) + + expect(proxy_conn.proxy_address).to eq(proxy_client.options[:proxy_address]) + expect(proxy_conn.proxy_port).to eq(proxy_client.options[:proxy_port]) + expect(proxy_conn.proxy_user).to eq(proxy_client.options[:proxy_username]) + expect(proxy_conn.proxy_pass).to eq(proxy_client.options[:proxy_password]) + end + end end it 'can use client certificates' do From efa48fcd4409afce6a791cb717798ad5aa69cff8 Mon Sep 17 00:00:00 2001 From: Marlin Pierce Date: Sun, 3 Dec 2023 12:38:38 -0500 Subject: [PATCH 31/54] Change expect for raising to block from advice of deprecation warning. --- spec/jira/base_spec.rb | 6 +++--- spec/support/shared_examples/integration.rb | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/jira/base_spec.rb b/spec/jira/base_spec.rb index 26749bd5..573ad61c 100644 --- a/spec/jira/base_spec.rb +++ b/spec/jira/base_spec.rb @@ -301,7 +301,7 @@ class JIRA::Resource::HasManyExample < JIRA::Base # :nodoc: response = instance_double('Response', body: '{"errorMessages":["blah"]}', status: 400) allow(subject).to receive(:new_record?) { false } expect(client).to receive(:put).with('/foo/bar', '{"invalid_field":"foobar"}').and_raise(JIRA::HTTPError.new(response)) - expect(-> { subject.save!('invalid_field' => 'foobar') }).to raise_error(JIRA::HTTPError) + expect{ subject.save!('invalid_field' => 'foobar') }.to raise_error(JIRA::HTTPError) end end @@ -579,9 +579,9 @@ class JIRA::Resource::BelongsToExample < JIRA::Base end it 'raises an exception when initialized without a belongs_to instance' do - expect(lambda { + expect{ JIRA::Resource::BelongsToExample.new(client, attrs: { 'id' => '123' }) - }).to raise_exception(ArgumentError, 'Required option :deadbeef missing') + }.to raise_exception(ArgumentError, 'Required option :deadbeef missing') end it 'returns the right url' do diff --git a/spec/support/shared_examples/integration.rb b/spec/support/shared_examples/integration.rb index 36910beb..1c3e0a17 100644 --- a/spec/support/shared_examples/integration.rb +++ b/spec/support/shared_examples/integration.rb @@ -55,9 +55,9 @@ def build_receiver stub_request(:put, site_url + subject.url) .to_return(status: 405, body: 'Some HTML') expect(subject.save('foo' => 'bar')).to be_falsey - expect(lambda do + expect do expect(subject.save!('foo' => 'bar')).to be_falsey - end).to raise_error(JIRA::HTTPError) + end.to raise_error(JIRA::HTTPError) end end @@ -115,9 +115,9 @@ def build_receiver it 'handles a 404' do stub_request(:get, site_url + described_class.singular_path(client, '99999', prefix)) .to_return(status: 404, body: '{"errorMessages":["' + class_basename + ' Does Not Exist"],"errors": {}}') - expect(lambda do + expect do client.send(class_basename).find('99999', options) - end).to raise_exception(JIRA::HTTPError) + end.to raise_exception(JIRA::HTTPError) end end @@ -170,8 +170,8 @@ def build_receiver subject.fetch expect(subject.save('fields' => { 'invalid' => 'field' })).to be_falsey - expect(lambda do + expect do subject.save!('fields' => { 'invalid' => 'field' }) - end).to raise_error(JIRA::HTTPError) + end.to raise_error(JIRA::HTTPError) end end From 77a3ffb680731ac89d9bd36b17ccff2baf2b602f Mon Sep 17 00:00:00 2001 From: Marlin Pierce Date: Mon, 30 Oct 2023 05:05:24 -0400 Subject: [PATCH 32/54] Replace deprecated alias UploadIO. --- lib/jira/resource/attachment.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jira/resource/attachment.rb b/lib/jira/resource/attachment.rb index b416e69b..cffe6e47 100644 --- a/lib/jira/resource/attachment.rb +++ b/lib/jira/resource/attachment.rb @@ -24,7 +24,7 @@ def save!(attrs, path = url) mime_type = attrs[:mimeType] || 'application/binary' headers = { 'X-Atlassian-Token' => 'nocheck' } - data = { 'file' => UploadIO.new(file, mime_type, file) } + data = { 'file' => Multipart::Post::UploadIO.new(file, mime_type, file) } response = client.post_multipart(path, data , headers) From fe845dad4696747422a5e146539c8da6243448a2 Mon Sep 17 00:00:00 2001 From: Marlin Pierce Date: Sat, 2 Dec 2023 15:51:57 -0500 Subject: [PATCH 33/54] Minimum functioning spec. --- spec/jira/resource/attachment_spec.rb | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/spec/jira/resource/attachment_spec.rb b/spec/jira/resource/attachment_spec.rb index 03e0c722..a2d41def 100644 --- a/spec/jira/resource/attachment_spec.rb +++ b/spec/jira/resource/attachment_spec.rb @@ -135,4 +135,53 @@ end end end + + context 'when there is a local file' do + let(:file_name) { 'short.txt' } + let(:file_size) { 11 } + let(:file_mime_type) { 'text/plain' } + let(:path_to_file) { "./spec/data/files/#{file_name}" } + let(:response) do + double( + body: [ + { + "id": 10_001, + "self": 'http://www.example.com/jira/rest/api/2.0/attachments/10000', + "filename": file_name, + "created": '2017-07-19T12:23:06.572+0000', + "size": file_size, + "mimeType": file_mime_type + } + ].to_json + ) + end + let(:issue) { JIRA::Resource::Issue.new(client) } + + # anything + describe '#save!' do + context 'when using custom client headers' do + subject(:bearer_attachment) do + JIRA::Resource::Attachment.new( + bearer_client, + issue: JIRA::Resource::Issue.new(bearer_client), + attrs: { 'author' => { 'foo' => 'bar' } } + ) + end + let(:default_headers_given) { { 'authorization' => "Bearer 83CF8B609DE60036A8277BD0E96135751BBC07EB234256D4B65B893360651BF2" } } + let(:bearer_client) do + JIRA::Client.new(username: 'username', password: 'password', auth_type: :basic, use_ssl: false, + default_headers: default_headers_given ) + end + let(:merged_headers) do + {"Accept"=>"application/json", "X-Atlassian-Token"=>"nocheck"}.merge(default_headers_given) + end + it 'passes the custom headers' do + expect(bearer_client.request_client).to receive(:request_multipart).with("/jira/rest/api/2/issue/attachments", anything, merged_headers).and_return(response) + + bearer_attachment.save!('file' => path_to_file) + + end + end + end + end end From 4bdb1c38746b6bfb2280dfd02b7441c8c94a41c7 Mon Sep 17 00:00:00 2001 From: Marlin Pierce Date: Sat, 2 Dec 2023 15:53:16 -0500 Subject: [PATCH 34/54] Data file for spec --- spec/data/files/short.txt | 1 + spec/jira/resource/attachment_spec.rb | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 spec/data/files/short.txt diff --git a/spec/data/files/short.txt b/spec/data/files/short.txt new file mode 100644 index 00000000..97fc24bf --- /dev/null +++ b/spec/data/files/short.txt @@ -0,0 +1 @@ +short text diff --git a/spec/jira/resource/attachment_spec.rb b/spec/jira/resource/attachment_spec.rb index a2d41def..92346a49 100644 --- a/spec/jira/resource/attachment_spec.rb +++ b/spec/jira/resource/attachment_spec.rb @@ -157,7 +157,6 @@ end let(:issue) { JIRA::Resource::Issue.new(client) } - # anything describe '#save!' do context 'when using custom client headers' do subject(:bearer_attachment) do From 7b0f715bfdda34dde63ce1ea014b4f89957faa30 Mon Sep 17 00:00:00 2001 From: Marlin Pierce Date: Sat, 2 Dec 2023 16:02:46 -0500 Subject: [PATCH 35/54] Consolidate #save! --- spec/jira/resource/attachment_spec.rb | 95 +++++++++++++++------------ 1 file changed, 52 insertions(+), 43 deletions(-) diff --git a/spec/jira/resource/attachment_spec.rb b/spec/jira/resource/attachment_spec.rb index 92346a49..e4c97edd 100644 --- a/spec/jira/resource/attachment_spec.rb +++ b/spec/jira/resource/attachment_spec.rb @@ -91,73 +91,81 @@ end end - describe '#save!' do - subject { attachment.save!('file' => path_to_file) } - - let(:path_to_file) { './spec/mock_responses/issue.json' } + context 'when there is a local file' do + let(:file_name) { 'short.txt' } + let(:file_size) { 11 } + let(:file_mime_type) { 'text/plain' } + let(:path_to_file) { "./spec/data/files/#{file_name}" } let(:response) do double( body: [ { "id": 10_001, "self": 'http://www.example.com/jira/rest/api/2.0/attachments/10000', - "filename": 'picture.jpg', + "filename": file_name, "created": '2017-07-19T12:23:06.572+0000', - "size": 23_123, - "mimeType": 'image/jpeg' + "size": file_size, + "mimeType": file_mime_type } ].to_json ) end let(:issue) { JIRA::Resource::Issue.new(client) } - before do - allow(client).to receive(:post_multipart).and_return(response) - end + describe '#save' do + context 'when using custom client headers' do + subject(:bearer_attachment) do + JIRA::Resource::Attachment.new( + bearer_client, + issue: JIRA::Resource::Issue.new(bearer_client), + attrs: { 'author' => { 'foo' => 'bar' } } + ) + end + let(:default_headers_given) { { 'authorization' => "Bearer 83CF8B609DE60036A8277BD0E96135751BBC07EB234256D4B65B893360651BF2" } } + let(:bearer_client) do + JIRA::Client.new(username: 'username', password: 'password', auth_type: :basic, use_ssl: false, + default_headers: default_headers_given ) + end + let(:merged_headers) do + {"Accept"=>"application/json", "X-Atlassian-Token"=>"nocheck"}.merge(default_headers_given) + end - it 'successfully update the attachment' do - subject + it 'passes the custom headers' do + expect(bearer_client.request_client).to receive(:request_multipart).with("/jira/rest/api/2/issue//attachments", anything, merged_headers).and_return(response) - expect(attachment.filename).to eq 'picture.jpg' - expect(attachment.mimeType).to eq 'image/jpeg' - expect(attachment.size).to eq 23_123 + bearer_attachment.save('file' => path_to_file) + + end + end end - context 'when passing in a symbol as file key' do - subject { attachment.save!(file: path_to_file) } + describe '#save!' do + subject { attachment.save!('file' => path_to_file) } + + before do + allow(client).to receive(:post_multipart).and_return(response) + end it 'successfully update the attachment' do subject - expect(attachment.filename).to eq 'picture.jpg' - expect(attachment.mimeType).to eq 'image/jpeg' - expect(attachment.size).to eq 23_123 + expect(attachment.filename).to eq file_name + expect(attachment.mimeType).to eq file_mime_type + expect(attachment.size).to eq file_size end - end - end - context 'when there is a local file' do - let(:file_name) { 'short.txt' } - let(:file_size) { 11 } - let(:file_mime_type) { 'text/plain' } - let(:path_to_file) { "./spec/data/files/#{file_name}" } - let(:response) do - double( - body: [ - { - "id": 10_001, - "self": 'http://www.example.com/jira/rest/api/2.0/attachments/10000', - "filename": file_name, - "created": '2017-07-19T12:23:06.572+0000', - "size": file_size, - "mimeType": file_mime_type - } - ].to_json - ) - end - let(:issue) { JIRA::Resource::Issue.new(client) } + context 'when passing in a symbol as file key' do + subject { attachment.save!(file: path_to_file) } + + it 'successfully update the attachment' do + subject + + expect(attachment.filename).to eq file_name + expect(attachment.mimeType).to eq file_mime_type + expect(attachment.size).to eq file_size + end + end - describe '#save!' do context 'when using custom client headers' do subject(:bearer_attachment) do JIRA::Resource::Attachment.new( @@ -174,6 +182,7 @@ let(:merged_headers) do {"Accept"=>"application/json", "X-Atlassian-Token"=>"nocheck"}.merge(default_headers_given) end + it 'passes the custom headers' do expect(bearer_client.request_client).to receive(:request_multipart).with("/jira/rest/api/2/issue/attachments", anything, merged_headers).and_return(response) From 4fbf1b8739952f6d4364dcbbf1f8b187a2db53ea Mon Sep 17 00:00:00 2001 From: Marlin Pierce Date: Sat, 2 Dec 2023 16:05:37 -0500 Subject: [PATCH 36/54] Consolidated #save --- spec/jira/resource/attachment_spec.rb | 50 +++++++++------------------ 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/spec/jira/resource/attachment_spec.rb b/spec/jira/resource/attachment_spec.rb index e4c97edd..e5deed31 100644 --- a/spec/jira/resource/attachment_spec.rb +++ b/spec/jira/resource/attachment_spec.rb @@ -59,38 +59,6 @@ end end - describe '#save' do - subject { attachment.save('file' => path_to_file) } - let(:path_to_file) { './spec/mock_responses/issue.json' } - let(:response) do - double( - body: [ - { - "id": 10_001, - "self": 'http://www.example.com/jira/rest/api/2.0/attachments/10000', - "filename": 'picture.jpg', - "created": '2017-07-19T12:23:06.572+0000', - "size": 23_123, - "mimeType": 'image/jpeg' - } - ].to_json - ) - end - let(:issue) { JIRA::Resource::Issue.new(client) } - - before do - allow(client).to receive(:post_multipart).and_return(response) - end - - it 'successfully update the attachment' do - subject - - expect(attachment.filename).to eq 'picture.jpg' - expect(attachment.mimeType).to eq 'image/jpeg' - expect(attachment.size).to eq 23_123 - end - end - context 'when there is a local file' do let(:file_name) { 'short.txt' } let(:file_size) { 11 } @@ -112,6 +80,22 @@ end let(:issue) { JIRA::Resource::Issue.new(client) } + describe '#save' do + subject { attachment.save('file' => path_to_file) } + + before do + allow(client).to receive(:post_multipart).and_return(response) + end + + it 'successfully update the attachment' do + subject + + expect(attachment.filename).to eq file_name + expect(attachment.mimeType).to eq file_mime_type + expect(attachment.size).to eq file_size + end + end + describe '#save' do context 'when using custom client headers' do subject(:bearer_attachment) do @@ -137,9 +121,7 @@ end end - end - describe '#save!' do subject { attachment.save!('file' => path_to_file) } before do From 9e13662b52ff584c4f096b66094f9aca0a6397d2 Mon Sep 17 00:00:00 2001 From: Marlin Pierce Date: Tue, 19 Dec 2023 12:38:35 -0500 Subject: [PATCH 37/54] Remove change to attachment save URI. --- spec/jira/resource/attachment_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/jira/resource/attachment_spec.rb b/spec/jira/resource/attachment_spec.rb index e5deed31..32dd1dff 100644 --- a/spec/jira/resource/attachment_spec.rb +++ b/spec/jira/resource/attachment_spec.rb @@ -166,7 +166,7 @@ end it 'passes the custom headers' do - expect(bearer_client.request_client).to receive(:request_multipart).with("/jira/rest/api/2/issue/attachments", anything, merged_headers).and_return(response) + expect(bearer_client.request_client).to receive(:request_multipart).with(anything, anything, merged_headers).and_return(response) bearer_attachment.save!('file' => path_to_file) From 493758f394af87b9f5451cd05a80297dac7363c6 Mon Sep 17 00:00:00 2001 From: Marlin Pierce Date: Tue, 19 Dec 2023 12:46:06 -0500 Subject: [PATCH 38/54] Remove expectation for attachment save URI. --- spec/jira/resource/attachment_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/jira/resource/attachment_spec.rb b/spec/jira/resource/attachment_spec.rb index 32dd1dff..0bd5f039 100644 --- a/spec/jira/resource/attachment_spec.rb +++ b/spec/jira/resource/attachment_spec.rb @@ -115,7 +115,7 @@ end it 'passes the custom headers' do - expect(bearer_client.request_client).to receive(:request_multipart).with("/jira/rest/api/2/issue//attachments", anything, merged_headers).and_return(response) + expect(bearer_client.request_client).to receive(:request_multipart).with(anything, anything, merged_headers).and_return(response) bearer_attachment.save('file' => path_to_file) From f526b1e94ff0c592436287f2d948207af445b84c Mon Sep 17 00:00:00 2001 From: Marlin Pierce Date: Tue, 19 Dec 2023 12:49:53 -0500 Subject: [PATCH 39/54] Clean up nesting in attachment spec. --- spec/jira/resource/attachment_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/jira/resource/attachment_spec.rb b/spec/jira/resource/attachment_spec.rb index 0bd5f039..ee89cc1d 100644 --- a/spec/jira/resource/attachment_spec.rb +++ b/spec/jira/resource/attachment_spec.rb @@ -94,9 +94,6 @@ expect(attachment.mimeType).to eq file_mime_type expect(attachment.size).to eq file_size end - end - - describe '#save' do context 'when using custom client headers' do subject(:bearer_attachment) do JIRA::Resource::Attachment.new( @@ -122,7 +119,10 @@ end end - subject { attachment.save!('file' => path_to_file) } + end + + describe '#save!' do + subject { attachment.save!('file' => path_to_file) } before do allow(client).to receive(:post_multipart).and_return(response) From 5503d94f92296a8edf9ecddd418b92f3f63833eb Mon Sep 17 00:00:00 2001 From: Marlin Pierce Date: Fri, 22 Dec 2023 17:17:49 -0500 Subject: [PATCH 40/54] Implement from TTD. --- lib/jira/resource/attachment.rb | 12 +++++++++ spec/jira/resource/attachment_spec.rb | 37 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/lib/jira/resource/attachment.rb b/lib/jira/resource/attachment.rb index b416e69b..35094685 100644 --- a/lib/jira/resource/attachment.rb +++ b/lib/jira/resource/attachment.rb @@ -1,4 +1,5 @@ require 'net/http/post/multipart' +require 'open-uri' module JIRA module Resource @@ -19,6 +20,17 @@ def self.meta(client) parse_json(response.body) end + def download_file(headers = {}, &block) + default_headers = client.options[:default_headers] + URI.open(content, default_headers.merge(headers), &block) + end + + def download_contents(headers = {}) + download_file(headers) do |file| + file.read + end + end + def save!(attrs, path = url) file = attrs['file'] || attrs[:file] # Keep supporting 'file' parameter as a string for backward compatibility mime_type = attrs[:mimeType] || 'application/binary' diff --git a/spec/jira/resource/attachment_spec.rb b/spec/jira/resource/attachment_spec.rb index 03e0c722..fde83563 100644 --- a/spec/jira/resource/attachment_spec.rb +++ b/spec/jira/resource/attachment_spec.rb @@ -59,6 +59,43 @@ end end + context 'there is an attachment on an issue' do + let(:client) do + JIRA::Client.new(username: 'username', password: 'password', auth_type: :basic, use_ssl: false ) + end + let(:attachment_file_contents) { 'file contents' } + let(:file_target) { double(read: :attachment_file_contents) } + let(:attachment_url) { "https:jirahost/secure/attachment/32323/myfile.txt" } + subject(:attachment) do + JIRA::Resource::Attachment.new( + client, + issue: JIRA::Resource::Issue.new(client), + attrs: { 'author' => { 'foo' => 'bar' }, 'content' => attachment_url } + ) + end + + describe '.download_file' do + it 'passes file object to block' do + expect(URI).to receive(:open).with(attachment_url, anything).and_yield(file_target) + + attachment.download_file do |file| + expect(file).to eq(file_target) + end + + end + end + + describe '.download_contents' do + it 'downloads the file contents as a string' do + expect(URI).to receive(:open).with(attachment_url, anything).and_return(attachment_file_contents) + + result_str = attachment.download_contents + + expect(result_str).to eq(attachment_file_contents) + end + end + end + describe '#save' do subject { attachment.save('file' => path_to_file) } let(:path_to_file) { './spec/mock_responses/issue.json' } From 5c0e59fe4ff8fc28935d2be2fffaeb8fd7fb2e50 Mon Sep 17 00:00:00 2001 From: Marlin Pierce Date: Fri, 22 Dec 2023 17:35:04 -0500 Subject: [PATCH 41/54] Documentation. --- lib/jira/resource/attachment.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/jira/resource/attachment.rb b/lib/jira/resource/attachment.rb index 35094685..5c94756c 100644 --- a/lib/jira/resource/attachment.rb +++ b/lib/jira/resource/attachment.rb @@ -20,11 +20,22 @@ def self.meta(client) parse_json(response.body) end + # Opens a file streaming the download of the attachment. + # @example Read the file contents + # download_file(headers) do |file| + # file.read + # end + # @param [Hash] headers Any additional headers to call Jira. + # @yield |file| + # @yieldparam [IO] file The IO object streaming the download. def download_file(headers = {}, &block) default_headers = client.options[:default_headers] URI.open(content, default_headers.merge(headers), &block) end + # Downloads the file contents as a string object. + # @param [Hash] headers Any additional headers to call Jira. + # @return [String,NilClass] The file contents. def download_contents(headers = {}) download_file(headers) do |file| file.read From 2979884e13c5706e62392b4223fb506b375d4f62 Mon Sep 17 00:00:00 2001 From: Marlin Pierce Date: Sun, 7 Jan 2024 12:11:13 -0500 Subject: [PATCH 42/54] Doc change to recommend against read into string. --- lib/jira/resource/attachment.rb | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/jira/resource/attachment.rb b/lib/jira/resource/attachment.rb index 5c94756c..886702ff 100644 --- a/lib/jira/resource/attachment.rb +++ b/lib/jira/resource/attachment.rb @@ -21,10 +21,22 @@ def self.meta(client) end # Opens a file streaming the download of the attachment. - # @example Read the file contents - # download_file(headers) do |file| - # file.read + # @example Write file contents to a file. + # File.open('some-filename', 'wb') do |output| + # download_file do |file| + # IO.copy_stream(file, output) + # end # end + # @example Stream file contents for an HTTP response. + # response.headers[ "Content-Type" ] = "application/octet-stream" + # download_file do |file| + # chunk = file.read(8000) + # while chunk.present? do + # response.stream.write(chunk) + # chunk = file.read(8000) + # end + # end + # response.stream.close # @param [Hash] headers Any additional headers to call Jira. # @yield |file| # @yieldparam [IO] file The IO object streaming the download. @@ -34,6 +46,11 @@ def download_file(headers = {}, &block) end # Downloads the file contents as a string object. + # + # Note that this reads the contents into a ruby string in memory. + # A file might be very large so it is recommend to avoid this unless you are certain about doing so. + # Use the download_file method instead and avoid calling the read method without a limit. + # # @param [Hash] headers Any additional headers to call Jira. # @return [String,NilClass] The file contents. def download_contents(headers = {}) From 5fb44aedd70dc2704bc12d14c7823e8be0d955a5 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 21 Feb 2024 17:10:21 -0800 Subject: [PATCH 43/54] Support the status category resource --- lib/jira-ruby.rb | 1 + lib/jira/base.rb | 2 +- lib/jira/client.rb | 4 ++ lib/jira/resource/status.rb | 6 ++- lib/jira/resource/status_category.rb | 8 ++++ spec/integration/status_category_spec.rb | 20 ++++++++++ spec/integration/status_spec.rb | 6 +-- spec/jira/resource/status_spec.rb | 22 +++++++++++ spec/mock_responses/status.json | 45 ++++++++++++++++++++--- spec/mock_responses/status/1.json | 9 ++++- spec/mock_responses/statuscategory.json | 30 +++++++++++++++ spec/mock_responses/statuscategory/1.json | 7 ++++ 12 files changed, 147 insertions(+), 13 deletions(-) create mode 100644 lib/jira/resource/status_category.rb create mode 100644 spec/integration/status_category_spec.rb create mode 100644 spec/jira/resource/status_spec.rb create mode 100644 spec/mock_responses/statuscategory.json create mode 100644 spec/mock_responses/statuscategory/1.json diff --git a/lib/jira-ruby.rb b/lib/jira-ruby.rb index 5036e9e1..31050e9c 100644 --- a/lib/jira-ruby.rb +++ b/lib/jira-ruby.rb @@ -18,6 +18,7 @@ require 'jira/resource/issuetype' require 'jira/resource/version' require 'jira/resource/status' +require 'jira/resource/status_category' require 'jira/resource/transition' require 'jira/resource/project' require 'jira/resource/priority' diff --git a/lib/jira/base.rb b/lib/jira/base.rb index 8f98cf05..52c6dbf3 100644 --- a/lib/jira/base.rb +++ b/lib/jira/base.rb @@ -141,7 +141,7 @@ def self.collection_path(client, prefix = '/') # JIRA::Resource::Comment.singular_path('456','/issue/123/') # # => /jira/rest/api/2/issue/123/comment/456 def self.singular_path(client, key, prefix = '/') - collection_path(client, prefix) + '/' + key + collection_path(client, prefix) + '/' + key.to_s end # Returns the attribute name of the attribute used for find. diff --git a/lib/jira/client.rb b/lib/jira/client.rb index b777766e..9c882cde 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -182,6 +182,10 @@ def Status # :nodoc: JIRA::Resource::StatusFactory.new(self) end + def StatusCategory # :nodoc: + JIRA::Resource::StatusCategoryFactory.new(self) + end + def Resolution # :nodoc: JIRA::Resource::ResolutionFactory.new(self) end diff --git a/lib/jira/resource/status.rb b/lib/jira/resource/status.rb index 66c8b99b..be53507f 100644 --- a/lib/jira/resource/status.rb +++ b/lib/jira/resource/status.rb @@ -1,8 +1,12 @@ +require_relative 'status_category' + module JIRA module Resource class StatusFactory < JIRA::BaseFactory # :nodoc: end - class Status < JIRA::Base; end + class Status < JIRA::Base + has_one :status_category, class: JIRA::Resource::StatusCategory, attribute_key: 'statusCategory' + end end end diff --git a/lib/jira/resource/status_category.rb b/lib/jira/resource/status_category.rb new file mode 100644 index 00000000..c900308c --- /dev/null +++ b/lib/jira/resource/status_category.rb @@ -0,0 +1,8 @@ +module JIRA + module Resource + class StatusCategoryFactory < JIRA::BaseFactory # :nodoc: + end + + class StatusCategory < JIRA::Base; end + end +end diff --git a/spec/integration/status_category_spec.rb b/spec/integration/status_category_spec.rb new file mode 100644 index 00000000..5453f807 --- /dev/null +++ b/spec/integration/status_category_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe JIRA::Resource::StatusCategory do + with_each_client do |site_url, client| + let(:client) { client } + let(:site_url) { site_url } + + let(:key) { 1 } + + let(:expected_attributes) do + JSON.parse(File.read('spec/mock_responses/statuscategory/1.json')) + end + + let(:expected_collection_length) { 4 } + + it_should_behave_like 'a resource' + it_should_behave_like 'a resource with a collection GET endpoint' + it_should_behave_like 'a resource with a singular GET endpoint' + end +end diff --git a/spec/integration/status_spec.rb b/spec/integration/status_spec.rb index 9af17335..3a74e764 100644 --- a/spec/integration/status_spec.rb +++ b/spec/integration/status_spec.rb @@ -8,11 +8,7 @@ let(:key) { '1' } let(:expected_attributes) do - { - 'self' => 'http://localhost:2990/jira/rest/api/2/status/1', - 'id' => key, - 'name' => 'Open' - } + JSON.parse(File.read('spec/mock_responses/status/1.json')) end let(:expected_collection_length) { 5 } diff --git a/spec/jira/resource/status_spec.rb b/spec/jira/resource/status_spec.rb new file mode 100644 index 00000000..e3023dc6 --- /dev/null +++ b/spec/jira/resource/status_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe JIRA::Resource::Status do + + let(:client) do + client = double(options: { rest_base_path: '/jira/rest/api/2' }) + allow(client).to receive(:Field).and_return(JIRA::Resource::FieldFactory.new(client)) + allow(client).to receive(:cache).and_return(OpenStruct.new) + client + end + + describe '#status_category' do + subject do + JIRA::Resource::Status.new(client, attrs: JSON.parse(File.read('spec/mock_responses/status/1.json'))) + end + + it 'has a status_category relationship' do + expect(subject).to have_one(:status_category, JIRA::Resource::StatusCategory) + expect(subject.status_category.name).to eq('To Do') + end + end +end \ No newline at end of file diff --git a/spec/mock_responses/status.json b/spec/mock_responses/status.json index 835ad825..30f85e40 100644 --- a/spec/mock_responses/status.json +++ b/spec/mock_responses/status.json @@ -4,34 +4,69 @@ "description": "The issue is open and ready for the assignee to start work on it.", "iconUrl": "http://localhost:2990/jira/images/icons/status_open.gif", "name": "Open", - "id": "1" + "id": "1", + "statusCategory": { + "self": "http://localhost:2990/jira/rest/api/2/statuscategory/2", + "id": 2, + "key": "new", + "colorName": "blue-gray", + "name": "To Do" + } }, { "self": "http://localhost:2990/jira/rest/api/2/status/3", "description": "This issue is being actively worked on at the moment by the assignee.", "iconUrl": "http://localhost:2990/jira/images/icons/status_inprogress.gif", "name": "In Progress", - "id": "3" + "id": "3", + "statusCategory": { + "self": "http://localhost:2990/jira/rest/api/2/statuscategory/4", + "id": 4, + "key": "indeterminate", + "colorName": "yellow", + "name": "In Progress" + } }, { "self": "http://localhost:2990/jira/rest/api/2/status/4", "description": "This issue was once resolved, but the resolution was deemed incorrect. From here issues are either marked assigned or resolved.", "iconUrl": "http://localhost:2990/jira/images/icons/status_reopened.gif", "name": "Reopened", - "id": "4" + "id": "4", + "statusCategory": { + "self": "http://localhost:2990/jira/rest/api/2/statuscategory/2", + "id": 2, + "key": "new", + "colorName": "blue-gray", + "name": "To Do" + } }, { "self": "http://localhost:2990/jira/rest/api/2/status/5", "description": "A resolution has been taken, and it is awaiting verification by reporter. From here issues are either reopened, or are closed.", "iconUrl": "http://localhost:2990/jira/images/icons/status_resolved.gif", "name": "Resolved", - "id": "5" + "id": "5", + "statusCategory": { + "self": "http://localhost:2990/jira/rest/api/2/statuscategory/3", + "id": 3, + "key": "done", + "colorName": "green", + "name": "Done" + } }, { "self": "http://localhost:2990/jira/rest/api/2/status/6", "description": "The issue is considered finished, the resolution is correct. Issues which are closed can be reopened.", "iconUrl": "http://localhost:2990/jira/images/icons/status_closed.gif", "name": "Closed", - "id": "6" + "id": "6", + "statusCategory": { + "self": "http://localhost:2990/jira/rest/api/2/statuscategory/3", + "id": 3, + "key": "done", + "colorName": "green", + "name": "Done" + } } ] diff --git a/spec/mock_responses/status/1.json b/spec/mock_responses/status/1.json index af3f17b1..63de85cc 100644 --- a/spec/mock_responses/status/1.json +++ b/spec/mock_responses/status/1.json @@ -3,5 +3,12 @@ "description": "The issue is open and ready for the assignee to start work on it.", "iconUrl": "http://localhost:2990/jira/images/icons/status_open.gif", "name": "Open", - "id": "1" + "id": "1", + "statusCategory": { + "self": "http://localhost:2990/jira/rest/api/2/statuscategory/2", + "id": 2, + "key": "new", + "colorName": "blue-gray", + "name": "To Do" + } } diff --git a/spec/mock_responses/statuscategory.json b/spec/mock_responses/statuscategory.json new file mode 100644 index 00000000..24ef8f59 --- /dev/null +++ b/spec/mock_responses/statuscategory.json @@ -0,0 +1,30 @@ +[ + { + "self": "http://localhost:2990/jira/rest/api/2/statuscategory/1", + "id": 1, + "key": "undefined", + "colorName": "medium-gray", + "name": "No Category" + }, + { + "self": "http://localhost:2990/jira/rest/api/2/statuscategory/2", + "id": 2, + "key": "new", + "colorName": "blue-gray", + "name": "To Do" + }, + { + "self": "http://localhost:2990/jira/rest/api/2/statuscategory/4", + "id": 4, + "key": "indeterminate", + "colorName": "yellow", + "name": "In Progress" + }, + { + "self": "http://localhost:2990/jira/rest/api/2/statuscategory/3", + "id": 3, + "key": "done", + "colorName": "green", + "name": "Done" + } +] \ No newline at end of file diff --git a/spec/mock_responses/statuscategory/1.json b/spec/mock_responses/statuscategory/1.json new file mode 100644 index 00000000..ba03e5e0 --- /dev/null +++ b/spec/mock_responses/statuscategory/1.json @@ -0,0 +1,7 @@ +{ + "self": "http://localhost:2990/jira/rest/api/2/statuscategory/1", + "id": 1, + "key": "undefined", + "colorName": "medium-gray", + "name": "No Category" +} \ No newline at end of file From 83a1f2396f020e0bbcd97fbf40582279180cd68e Mon Sep 17 00:00:00 2001 From: James Couball Date: Sun, 25 Feb 2024 11:15:41 -0800 Subject: [PATCH 44/54] Add Issue#resolution relationship --- lib/jira-ruby.rb | 2 +- lib/jira/resource/issue.rb | 3 +++ spec/jira/resource/issue_spec.rb | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/jira-ruby.rb b/lib/jira-ruby.rb index 31050e9c..ae464f8e 100644 --- a/lib/jira-ruby.rb +++ b/lib/jira-ruby.rb @@ -33,11 +33,11 @@ require 'jira/resource/remotelink' require 'jira/resource/sprint' require 'jira/resource/sprint_report' +require 'jira/resource/resolution' require 'jira/resource/issue' require 'jira/resource/filter' require 'jira/resource/field' require 'jira/resource/rapidview' -require 'jira/resource/resolution' require 'jira/resource/serverinfo' require 'jira/resource/createmeta' require 'jira/resource/webhook' diff --git a/lib/jira/resource/issue.rb b/lib/jira/resource/issue.rb index a4e7a169..2d9e75b8 100644 --- a/lib/jira/resource/issue.rb +++ b/lib/jira/resource/issue.rb @@ -1,6 +1,7 @@ require 'cgi' require 'json' + module JIRA module Resource class IssueFactory < JIRA::BaseFactory # :nodoc: @@ -19,6 +20,8 @@ class Issue < JIRA::Base has_one :status, nested_under: 'fields' + has_one :resolution, nested_under: 'fields' + has_many :transitions has_many :components, nested_under: 'fields' diff --git a/spec/jira/resource/issue_spec.rb b/spec/jira/resource/issue_spec.rb index 7c9c9157..9fbcb5d2 100644 --- a/spec/jira/resource/issue_spec.rb +++ b/spec/jira/resource/issue_spec.rb @@ -180,6 +180,7 @@ class JIRAResourceDelegation < SimpleDelegator # :nodoc: 'priority' => { 'foo' => 'bar' }, 'issuetype' => { 'foo' => 'bar' }, 'status' => { 'foo' => 'bar' }, + 'resolution' => { 'foo' => 'bar' }, 'components' => [{ 'foo' => 'bar' }, { 'baz' => 'flum' }], 'versions' => [{ 'foo' => 'bar' }, { 'baz' => 'flum' }], 'comment' => { 'comments' => [{ 'foo' => 'bar' }, { 'baz' => 'flum' }] }, @@ -208,6 +209,9 @@ class JIRAResourceDelegation < SimpleDelegator # :nodoc: expect(subject).to have_one(:status, JIRA::Resource::Status) expect(subject.status.foo).to eq('bar') + expect(subject).to have_one(:resolution, JIRA::Resource::Resolution) + expect(subject.resolution.foo).to eq('bar') + expect(subject).to have_many(:components, JIRA::Resource::Component) expect(subject.components.length).to eq(2) From 3e0ddfb7d2473250e32c1d526545c9593cefbd72 Mon Sep 17 00:00:00 2001 From: Marlin Pierce Date: Fri, 5 Apr 2024 06:49:49 -0400 Subject: [PATCH 45/54] Resovle conflict. --- spec/jira/resource/attachment_spec.rb | 133 +++++++++++++++++--------- 1 file changed, 86 insertions(+), 47 deletions(-) diff --git a/spec/jira/resource/attachment_spec.rb b/spec/jira/resource/attachment_spec.rb index fde83563..13cf249d 100644 --- a/spec/jira/resource/attachment_spec.rb +++ b/spec/jira/resource/attachment_spec.rb @@ -96,79 +96,118 @@ end end - describe '#save' do - subject { attachment.save('file' => path_to_file) } - let(:path_to_file) { './spec/mock_responses/issue.json' } + context 'when there is a local file' do + let(:file_name) { 'short.txt' } + let(:file_size) { 11 } + let(:file_mime_type) { 'text/plain' } + let(:path_to_file) { "./spec/data/files/#{file_name}" } let(:response) do double( body: [ { "id": 10_001, "self": 'http://www.example.com/jira/rest/api/2.0/attachments/10000', - "filename": 'picture.jpg', + "filename": file_name, "created": '2017-07-19T12:23:06.572+0000', - "size": 23_123, - "mimeType": 'image/jpeg' + "size": file_size, + "mimeType": file_mime_type } ].to_json ) end let(:issue) { JIRA::Resource::Issue.new(client) } - before do - allow(client).to receive(:post_multipart).and_return(response) - end + describe '#save' do + subject { attachment.save('file' => path_to_file) } - it 'successfully update the attachment' do - subject + before do + allow(client).to receive(:post_multipart).and_return(response) + end - expect(attachment.filename).to eq 'picture.jpg' - expect(attachment.mimeType).to eq 'image/jpeg' - expect(attachment.size).to eq 23_123 - end - end + it 'successfully update the attachment' do + subject - describe '#save!' do - subject { attachment.save!('file' => path_to_file) } + expect(attachment.filename).to eq file_name + expect(attachment.mimeType).to eq file_mime_type + expect(attachment.size).to eq file_size + end + context 'when using custom client headers' do + subject(:bearer_attachment) do + JIRA::Resource::Attachment.new( + bearer_client, + issue: JIRA::Resource::Issue.new(bearer_client), + attrs: { 'author' => { 'foo' => 'bar' } } + ) + end + let(:default_headers_given) { { 'authorization' => "Bearer 83CF8B609DE60036A8277BD0E96135751BBC07EB234256D4B65B893360651BF2" } } + let(:bearer_client) do + JIRA::Client.new(username: 'username', password: 'password', auth_type: :basic, use_ssl: false, + default_headers: default_headers_given ) + end + let(:merged_headers) do + {"Accept"=>"application/json", "X-Atlassian-Token"=>"nocheck"}.merge(default_headers_given) + end - let(:path_to_file) { './spec/mock_responses/issue.json' } - let(:response) do - double( - body: [ - { - "id": 10_001, - "self": 'http://www.example.com/jira/rest/api/2.0/attachments/10000', - "filename": 'picture.jpg', - "created": '2017-07-19T12:23:06.572+0000', - "size": 23_123, - "mimeType": 'image/jpeg' - } - ].to_json - ) - end - let(:issue) { JIRA::Resource::Issue.new(client) } + it 'passes the custom headers' do + expect(bearer_client.request_client).to receive(:request_multipart).with(anything, anything, merged_headers).and_return(response) - before do - allow(client).to receive(:post_multipart).and_return(response) - end + bearer_attachment.save('file' => path_to_file) - it 'successfully update the attachment' do - subject + end + end - expect(attachment.filename).to eq 'picture.jpg' - expect(attachment.mimeType).to eq 'image/jpeg' - expect(attachment.size).to eq 23_123 end - context 'when passing in a symbol as file key' do - subject { attachment.save!(file: path_to_file) } + describe '#save!' do + subject { attachment.save!('file' => path_to_file) } + + before do + allow(client).to receive(:post_multipart).and_return(response) + end it 'successfully update the attachment' do subject - expect(attachment.filename).to eq 'picture.jpg' - expect(attachment.mimeType).to eq 'image/jpeg' - expect(attachment.size).to eq 23_123 + expect(attachment.filename).to eq file_name + expect(attachment.mimeType).to eq file_mime_type + expect(attachment.size).to eq file_size + end + + context 'when passing in a symbol as file key' do + subject { attachment.save!(file: path_to_file) } + + it 'successfully update the attachment' do + subject + + expect(attachment.filename).to eq file_name + expect(attachment.mimeType).to eq file_mime_type + expect(attachment.size).to eq file_size + end + end + + context 'when using custom client headers' do + subject(:bearer_attachment) do + JIRA::Resource::Attachment.new( + bearer_client, + issue: JIRA::Resource::Issue.new(bearer_client), + attrs: { 'author' => { 'foo' => 'bar' } } + ) + end + let(:default_headers_given) { { 'authorization' => "Bearer 83CF8B609DE60036A8277BD0E96135751BBC07EB234256D4B65B893360651BF2" } } + let(:bearer_client) do + JIRA::Client.new(username: 'username', password: 'password', auth_type: :basic, use_ssl: false, + default_headers: default_headers_given ) + end + let(:merged_headers) do + {"Accept"=>"application/json", "X-Atlassian-Token"=>"nocheck"}.merge(default_headers_given) + end + + it 'passes the custom headers' do + expect(bearer_client.request_client).to receive(:request_multipart).with(anything, anything, merged_headers).and_return(response) + + bearer_attachment.save!('file' => path_to_file) + + end end end end From a8a73c176aeaa1a911f3689750b1d959400b4fe7 Mon Sep 17 00:00:00 2001 From: Robert Brodie Date: Sat, 6 Apr 2024 19:43:46 -0400 Subject: [PATCH 46/54] Add supported Ruby versions, deprecate EOL --- .github/workflows/CI.yml | 11 ++++------- Gemfile | 2 +- jira-ruby.gemspec | 12 ++++++------ 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 76ff2fc2..2e1387f7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,6 +1,6 @@ name: Ruby -on: [push] +on: [push, pull_request] jobs: test: @@ -10,14 +10,11 @@ jobs: fail-fast: false matrix: ruby: + - '3.3' + - '3.2' - '3.1' - - '3.0' - - '2.7' - - '2.6' - - '2.5' - - '2.4' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Ruby uses: ruby/setup-ruby@v1 diff --git a/Gemfile b/Gemfile index 04597bf0..4912c25c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,4 @@ -source 'http://rubygems.org' +source 'https://rubygems.org' group :development do gem 'guard' diff --git a/jira-ruby.gemspec b/jira-ruby.gemspec index dc156bc4..03a7fdd8 100644 --- a/jira-ruby.gemspec +++ b/jira-ruby.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |s| s.licenses = ['MIT'] s.metadata = { 'source_code_uri' => 'https://github.com/sumoheavy/jira-ruby' } - s.required_ruby_version = '>= 1.9.3' + s.required_ruby_version = '>= 3.1.0' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") @@ -25,11 +25,11 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'oauth', '~> 1.0' # Development Dependencies - s.add_development_dependency 'guard', '~> 2.13', '>= 2.13.0' - s.add_development_dependency 'guard-rspec', '~> 4.6', '>= 4.6.5' - s.add_development_dependency 'pry', '~> 0.10', '>= 0.10.3' + s.add_development_dependency 'guard', '~> 2.18', '>= 2.18.1' + s.add_development_dependency 'guard-rspec', '~> 4.7', '>= 4.7.3' + s.add_development_dependency 'pry', '~> 0.14', '>= 0.14.3' s.add_development_dependency 'railties' - s.add_development_dependency 'rake', '~> 10.3', '>= 10.3.2' - s.add_development_dependency 'rspec', '~> 3.0', '>= 3.0.0' + s.add_development_dependency 'rake', '~> 13.2', '>= 13.2.1' + s.add_development_dependency 'rspec', '~> 3.0', '>= 3.13' s.add_development_dependency 'webmock', '~> 1.18', '>= 1.18.0' end From e4bb4655155fe5136dd6416ffe998fdba0e37856 Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 12 Apr 2024 12:28:33 -0700 Subject: [PATCH 47/54] Load Active Support's Object extensions --- lib/jira/http_error.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/jira/http_error.rb b/lib/jira/http_error.rb index 1612d78e..9733531f 100644 --- a/lib/jira/http_error.rb +++ b/lib/jira/http_error.rb @@ -1,4 +1,6 @@ require 'forwardable' +require 'active_support/core_ext/object' + module JIRA class HTTPError < StandardError extend Forwardable From 6ae421e5c690c522283f84adb1861ec34fd0e7ed Mon Sep 17 00:00:00 2001 From: Robert Brodie Date: Sun, 14 Apr 2024 23:27:23 -0400 Subject: [PATCH 48/54] Update tests for to_json --- .github/workflows/CI.yml | 2 +- spec/jira/base_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 76ff2fc2..732325a0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -1,6 +1,6 @@ name: Ruby -on: [push] +on: [push, pull_request] jobs: test: diff --git a/spec/jira/base_spec.rb b/spec/jira/base_spec.rb index 573ad61c..0833a940 100644 --- a/spec/jira/base_spec.rb +++ b/spec/jira/base_spec.rb @@ -428,7 +428,7 @@ class JIRA::Resource::HasManyExample < JIRA::Base # :nodoc: h = { 'key' => subject } h_attrs = { 'key' => subject.attrs } - expect(h.to_json).to eq(h_attrs.to_json) + expect(h['key'].to_json).to eq(h_attrs['key'].to_json) end describe 'extract attrs from response' do From 7ad98f3729ae996cff73974af9e31d23fc7fe421 Mon Sep 17 00:00:00 2001 From: Robert Brodie Date: Fri, 26 Apr 2024 17:56:20 -0400 Subject: [PATCH 49/54] #433 Remove link to deprecated Slack --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index e0c48e95..2f0cb9cd 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,6 @@ This gem provides access to the Atlassian JIRA REST API. -## Slack - -Join our Slack channel! You can find us [here](https://jira-ruby-slackin.herokuapp.com/) - ## Example usage # Jira Ruby API - Sample Usage From adf937cd9bda86551832c1abf86a2f49aa1f5086 Mon Sep 17 00:00:00 2001 From: Robert Brodie Date: Sat, 27 Apr 2024 01:14:29 -0400 Subject: [PATCH 50/54] #430 Upgrade webmock and tests --- jira-ruby.gemspec | 2 +- spec/jira/client_spec.rb | 18 ++++++++++-------- spec/support/clients_helper.rb | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/jira-ruby.gemspec b/jira-ruby.gemspec index 03a7fdd8..70ce734b 100644 --- a/jira-ruby.gemspec +++ b/jira-ruby.gemspec @@ -31,5 +31,5 @@ Gem::Specification.new do |s| s.add_development_dependency 'railties' s.add_development_dependency 'rake', '~> 13.2', '>= 13.2.1' s.add_development_dependency 'rspec', '~> 3.0', '>= 3.13' - s.add_development_dependency 'webmock', '~> 1.18', '>= 1.18.0' + s.add_development_dependency 'webmock', '~> 3.23', '>= 3.23.0' end diff --git a/spec/jira/client_spec.rb b/spec/jira/client_spec.rb index fd5b78ef..dfd04948 100644 --- a/spec/jira/client_spec.rb +++ b/spec/jira/client_spec.rb @@ -140,10 +140,12 @@ subject { JIRA::Client.new(username: 'foo', password: 'bar', auth_type: :basic) } before(:each) do - stub_request(:get, 'https://foo:bar@localhost:2990/jira/rest/api/2/project') + stub_request(:get, 'https://localhost:2990/jira/rest/api/2/project') + .with(headers: { 'Authorization' => "Basic #{Base64.strict_encode64('foo:bar').chomp}" }) .to_return(status: 200, body: '[]', headers: {}) - stub_request(:get, 'https://foo:badpassword@localhost:2990/jira/rest/api/2/project') + stub_request(:get, 'https://localhost:2990/jira/rest/api/2/project') + .with(headers: { 'Authorization' => "Basic #{Base64.strict_encode64('foo:badpassword').chomp}" }) .to_return(status: 401, headers: {}) end @@ -157,17 +159,17 @@ expect(subject.options[:password]).to eq('bar') end + it 'only returns a true for #authenticated? once we have requested some data' do + expect(subject.authenticated?).to be_nil + expect(subject.Project.all).to be_empty + expect(subject.authenticated?).to be_truthy + end + it 'fails with wrong user name and password' do bad_login = JIRA::Client.new(username: 'foo', password: 'badpassword', auth_type: :basic) expect(bad_login.authenticated?).to be_falsey expect { bad_login.Project.all }.to raise_error JIRA::HTTPError end - - it 'only returns a true for #authenticated? once we have requested some data' do - expect(subject.authenticated?).to be_falsey - expect(subject.Project.all).to be_empty - expect(subject.authenticated?).to be_truthy - end end context 'with cookie authentication' do diff --git a/spec/support/clients_helper.rb b/spec/support/clients_helper.rb index a9df0477..c022b0fe 100644 --- a/spec/support/clients_helper.rb +++ b/spec/support/clients_helper.rb @@ -7,7 +7,7 @@ def with_each_client clients['http://localhost:2990'] = oauth_client basic_client = JIRA::Client.new(username: 'foo', password: 'bar', auth_type: :basic, use_ssl: false) - clients['http://foo:bar@localhost:2990'] = basic_client + clients['http://localhost:2990'] = basic_client clients.each do |site_url, client| yield site_url, client From af3f3a4928ea1859c054158c569fef1e1bef2f53 Mon Sep 17 00:00:00 2001 From: Robert Brodie Date: Sat, 27 Apr 2024 18:18:34 -0400 Subject: [PATCH 51/54] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 20 ++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..f57ef21c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,20 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: Bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +A runnable code example to reproduce the issue. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..f24e6f6e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: Feature +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context about the feature request here. From 96cc14428b50c8fac5284087515854074b0960c4 Mon Sep 17 00:00:00 2001 From: Robert Brodie Date: Sat, 27 Apr 2024 19:53:02 -0400 Subject: [PATCH 52/54] Create codeql.yml --- .github/workflows/codeql.yml | 100 +++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..44aa3f48 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,100 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "master" ] + paths-ignore: + - '**/*.md' + - 'http-basic-example.rb' + - 'example.rb' + pull_request: + branches: [ "master" ] + paths-ignore: + - '**/*.md' + - 'http-basic-example.rb' + - 'example.rb' + schedule: + - cron: '0 13 * * *' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: ruby + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" From 5d5dc6257d5d9f2897a0beee1c8d121b4f547e47 Mon Sep 17 00:00:00 2001 From: Robert Brodie Date: Sun, 28 Apr 2024 01:33:56 -0400 Subject: [PATCH 53/54] Add tests for sprints and fix complete_date --- lib/jira-ruby.rb | 1 - lib/jira/client.rb | 4 ---- lib/jira/resource/sprint.rb | 18 +++++++----------- lib/jira/resource/sprint_report.rb | 8 -------- spec/jira/resource/sprint_spec.rb | 27 ++++++++++++++++++--------- spec/mock_responses/sprint/1.json | 13 +++++++++++++ 6 files changed, 38 insertions(+), 33 deletions(-) delete mode 100644 lib/jira/resource/sprint_report.rb create mode 100644 spec/mock_responses/sprint/1.json diff --git a/lib/jira-ruby.rb b/lib/jira-ruby.rb index ae464f8e..addb96c6 100644 --- a/lib/jira-ruby.rb +++ b/lib/jira-ruby.rb @@ -32,7 +32,6 @@ require 'jira/resource/issue_picker_suggestions' require 'jira/resource/remotelink' require 'jira/resource/sprint' -require 'jira/resource/sprint_report' require 'jira/resource/resolution' require 'jira/resource/issue' require 'jira/resource/filter' diff --git a/lib/jira/client.rb b/lib/jira/client.rb index fa631050..d80fa754 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -232,10 +232,6 @@ def Sprint JIRA::Resource::SprintFactory.new(self) end - def SprintReport - JIRA::Resource::SprintReportFactory.new(self) - end - def ServerInfo JIRA::Resource::ServerInfoFactory.new(self) end diff --git a/lib/jira/resource/sprint.rb b/lib/jira/resource/sprint.rb index 6b90710b..5664c44c 100644 --- a/lib/jira/resource/sprint.rb +++ b/lib/jira/resource/sprint.rb @@ -12,26 +12,22 @@ def self.find(client, key) # get all issues of sprint def issues(options = {}) - jql = 'sprint = ' + id.to_s + jql = "sprint = #{id.to_s}" jql += " and updated >= '#{options[:updated]}'" if options[:updated] Issue.jql(client, jql) end def add_issue(issue) - add_issues( [ issue ]) + add_issues([issue]) end def add_issues(issues) - issue_ids = issues.map{ |issue| issue.id } + issue_ids = issues.map(&:id) request_body = { issues: issue_ids }.to_json client.post("#{agile_path}/issue", request_body) true end - def sprint_report - get_sprint_details_attribute('sprint_report') - end - def start_date get_sprint_details_attribute('start_date') end @@ -47,6 +43,7 @@ def complete_date def get_sprint_details_attribute(attribute_name) attribute = instance_variable_get("@#{attribute_name}") return attribute if attribute + get_sprint_details instance_variable_get("@#{attribute_name}") end @@ -61,10 +58,9 @@ def get_sprint_details end json = self.class.parse_json(response.body) - @start_date = json['sprint']['startDate'] && Date.parse(json['sprint']['startDate']) - @end_date = json['sprint']['endDate'] && Date.parse(json['sprint']['endDate']) - @completed_date = json['sprint']['completeDate'] && Date.parse(json['sprint']['completeDate']) - @sprint_report = client.SprintReport.build(json['contents']) + @start_date = json['startDate'] && Date.parse(json['startDate']) + @end_date = json['endDate'] && Date.parse(json['endDate']) + @complete_date = json['completeDate'] && Date.parse(json['completeDate']) end def save(attrs = {}, _path = nil) diff --git a/lib/jira/resource/sprint_report.rb b/lib/jira/resource/sprint_report.rb deleted file mode 100644 index 8f179229..00000000 --- a/lib/jira/resource/sprint_report.rb +++ /dev/null @@ -1,8 +0,0 @@ -module JIRA - module Resource - class SprintReportFactory < JIRA::BaseFactory # :nodoc: - end - - class SprintReport < JIRA::Base; end - end -end diff --git a/spec/jira/resource/sprint_spec.rb b/spec/jira/resource/sprint_spec.rb index b766abb8..ca57fd3c 100644 --- a/spec/jira/resource/sprint_spec.rb +++ b/spec/jira/resource/sprint_spec.rb @@ -2,12 +2,24 @@ describe JIRA::Resource::Sprint do let(:client) do - client = double(options: { site: 'https://foo.bar.com', context_path: '/jira' }) + client = double(options: { rest_base_path: '/jira/rest/api/2', context_path: '/jira' }) allow(client).to receive(:Sprint).and_return(JIRA::Resource::SprintFactory.new(client)) client end let(:sprint) { described_class.new(client) } - let(:agile_sprint_path) { "#{sprint.client.options[:context_path]}/rest/agile/1.0/sprint/#{sprint.id}" } + let(:agile_sprint_path) { "/jira/rest/agile/1.0/sprint/#{sprint.id}" } + let(:response) { double } + + describe 'get_sprint_details' do + let(:sprint) { JIRA::Resource::Sprint.find(client, '1') } + it 'check each of the date attributes' do + allow(client).to receive(:get).and_return(double(body: get_mock_response('sprint/1.json'))) + + expect(sprint.start_date).to eq Date.parse('2024-01-01T03:20:00.000Z') + expect(sprint.end_date).to eq Date.parse('2024-01-15T03:20:00.000Z') + expect(sprint.complete_date).to eq Date.parse('2024-01-16T03:48:00.000Z') + end + end describe '::find' do let(:response) { double('Response', body: '{"some_detail":"some detail"}') } @@ -91,7 +103,7 @@ let(:issue_id) { 1001 } let(:post_issue_path) do described_class.agile_path(client, sprint.id) - "/jira/rest/agile/1.0/sprint//issue" + '/jira/rest/agile/1.0/sprint//issue' end let(:issue) do issue = double @@ -99,13 +111,11 @@ issue end let(:post_issue_input) do - {"issues":[issue.id]} + { "issues": [issue.id] } end - describe '#add_issu' do context 'when an issue is passed' do - it 'posts with the issue id' do expect(client).to receive(:post).with(post_issue_path, post_issue_input.to_json) @@ -119,7 +129,7 @@ let(:issue_ids) { [ 1001, 1012 ] } let(:post_issue_path) do described_class.agile_path(client, sprint.id) - "/jira/rest/agile/1.0/sprint//issue" + '/jira/rest/agile/1.0/sprint//issue' end let(:issues) do issue_ids.map do |issue_id| @@ -129,12 +139,11 @@ end end let(:post_issue_input) do - {"issues": issue_ids} + { "issues": issue_ids } end describe '#add_issues' do context 'when an issue is passed' do - it 'posts with the issue id' do expect(client).to receive(:post).with(post_issue_path, post_issue_input.to_json) diff --git a/spec/mock_responses/sprint/1.json b/spec/mock_responses/sprint/1.json new file mode 100644 index 00000000..4fcb02df --- /dev/null +++ b/spec/mock_responses/sprint/1.json @@ -0,0 +1,13 @@ +{ + "id": 1, + "self": "https://localhost:2990/jira/rest/agile/1.0/sprint/1", + "state": "closed", + "name": "SP Sprint 1", + "startDate": "2024-01-01T03:20:00.000Z", + "endDate": "2024-01-15T03:20:00.000Z", + "completeDate": "2024-01-16T03:48:00.000Z", + "createdDate": "2024-01-01T00:00:00.000Z", + "originBoardId": 1, + "goal": "", + "rapidview_id": 1 +} \ No newline at end of file From 1373c9c9ff510c6a38794e13ab34a8b8b38a1ce9 Mon Sep 17 00:00:00 2001 From: Robert Brodie Date: Sun, 28 Apr 2024 02:25:20 -0400 Subject: [PATCH 54/54] Bump version to prepare for the next release --- lib/jira/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jira/version.rb b/lib/jira/version.rb index 09231406..afd85d02 100644 --- a/lib/jira/version.rb +++ b/lib/jira/version.rb @@ -1,3 +1,3 @@ module JIRA - VERSION = '2.3.0'.freeze + VERSION = '3.0.0'.freeze end