From 0698255e2ecee5c187e10e3e016f7df7139989fd Mon Sep 17 00:00:00 2001 From: Ryan Sonnek Date: Mon, 18 Feb 2019 13:45:51 -0600 Subject: [PATCH 01/94] Add BoardConfiguration API Why? Allow for lookup of configuration information for a specific board. See JIRA API documentation: https://developer.atlassian.com/cloud/jira/software/rest/#api-rest-agile-1-0-board-boardId-configuration-get Completes #316 --- lib/jira-ruby.rb | 1 + lib/jira/client.rb | 4 ++++ lib/jira/resource/board.rb | 7 +++++++ lib/jira/resource/board_configuration.rb | 9 +++++++++ 4 files changed, 21 insertions(+) create mode 100644 lib/jira/resource/board_configuration.rb diff --git a/lib/jira-ruby.rb b/lib/jira-ruby.rb index 4bfccf00..b4250cf2 100644 --- a/lib/jira-ruby.rb +++ b/lib/jira-ruby.rb @@ -38,6 +38,7 @@ require 'jira/resource/webhook' require 'jira/resource/agile' require 'jira/resource/board' +require 'jira/resource/board_configuration' require 'jira/request_client' require 'jira/oauth_client' diff --git a/lib/jira/client.rb b/lib/jira/client.rb index d9f88b06..1f619695 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -159,6 +159,10 @@ def Board JIRA::Resource::BoardFactory.new(self) end + def BoardConfiguration + JIRA::Resource::BoardConfigurationFactory.new(self) + end + def RapidView JIRA::Resource::RapidViewFactory.new(self) end diff --git a/lib/jira/resource/board.rb b/lib/jira/resource/board.rb index 8459560b..d78ec68a 100644 --- a/lib/jira/resource/board.rb +++ b/lib/jira/resource/board.rb @@ -46,6 +46,13 @@ def issues(params = {}) results.map { |issue| client.Issue.build(issue) } end + def configuration(params = {}) + path = path_base(client) + "/board/#{id}/configuration" + response = client.get(url_with_query_params(path, params)) + json = self.class.parse_json(response.body) + client.BoardConfiguration.build(json) + end + # options # - state ~ future, active, closed, you can define multiple states separated by commas, e.g. state=active,closed # - maxResults ~ default: 50 (JIRA API), 1000 (this library) diff --git a/lib/jira/resource/board_configuration.rb b/lib/jira/resource/board_configuration.rb new file mode 100644 index 00000000..fa152f8b --- /dev/null +++ b/lib/jira/resource/board_configuration.rb @@ -0,0 +1,9 @@ +module JIRA + module Resource + class BoardConfigurationFactory < JIRA::BaseFactory # :nodoc: + end + + class BoardConfiguration < JIRA::Base + end + end +end From c0ee6f829db735ca11b22e7ff6c33afccd3b76a2 Mon Sep 17 00:00:00 2001 From: Jean-Michel Lacroix Date: Thu, 28 Feb 2019 17:45:24 -0500 Subject: [PATCH 02/94] Fix save method not working on attachments --- lib/jira/resource/attachment.rb | 4 ++-- spec/jira/resource/attachment_spec.rb | 30 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/jira/resource/attachment.rb b/lib/jira/resource/attachment.rb index 2f2e5983..b79eaf27 100644 --- a/lib/jira/resource/attachment.rb +++ b/lib/jira/resource/attachment.rb @@ -19,11 +19,11 @@ def self.meta(client) parse_json(response.body) end - def save!(attrs) + def save!(attrs, path = url) headers = { 'X-Atlassian-Token' => 'nocheck' } data = { 'file' => UploadIO.new(attrs['file'], 'application/binary', attrs['file']) } - request = Net::HTTP::Post::Multipart.new url, data, headers + request = Net::HTTP::Post::Multipart.new path, data, headers request.basic_auth(client.request_client.options[:username], client.request_client.options[:password]) diff --git a/spec/jira/resource/attachment_spec.rb b/spec/jira/resource/attachment_spec.rb index a8526c56..ab17c750 100644 --- a/spec/jira/resource/attachment_spec.rb +++ b/spec/jira/resource/attachment_spec.rb @@ -49,6 +49,36 @@ end end + describe '#save' do + it 'successfully update the attachment' do + basic_auth_http_conn = double + response = 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 + ) + + allow(client.request_client).to receive(:basic_auth_http_conn).and_return(basic_auth_http_conn) + allow(basic_auth_http_conn).to receive(:request).and_return(response) + + issue = JIRA::Resource::Issue.new(client) + path_to_file = './spec/mock_responses/issue.json' + attachment = JIRA::Resource::Attachment.new(client, issue: issue) + attachment.save('file' => path_to_file) + + expect(attachment.filename).to eq 'picture.jpg' + expect(attachment.mimeType).to eq 'image/jpeg' + expect(attachment.size).to eq 23_123 + end + end + describe '#save!' do it 'successfully update the attachment' do basic_auth_http_conn = double From 9a5f68a14a585940db9e2aaa8ad3c29539a0d3c9 Mon Sep 17 00:00:00 2001 From: Jean-Michel Lacroix Date: Fri, 1 Mar 2019 10:11:51 -0500 Subject: [PATCH 03/94] Add mimeType parameter to make JIRA image preview work --- lib/jira/resource/attachment.rb | 4 +++- spec/jira/resource/attachment_spec.rb | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/jira/resource/attachment.rb b/lib/jira/resource/attachment.rb index 2f2e5983..f6601f16 100644 --- a/lib/jira/resource/attachment.rb +++ b/lib/jira/resource/attachment.rb @@ -20,8 +20,10 @@ def self.meta(client) end def save!(attrs) + file = attrs['file'] || attrs[:file] # Keep supporting 'file' parameter as a string for backward compatibility + mime_type = attrs[:mimeType] || 'application/binary' headers = { 'X-Atlassian-Token' => 'nocheck' } - data = { 'file' => UploadIO.new(attrs['file'], 'application/binary', attrs['file']) } + data = { 'file' => UploadIO.new(file, mime_type, file) } request = Net::HTTP::Post::Multipart.new url, data, headers request.basic_auth(client.request_client.options[:username], diff --git a/spec/jira/resource/attachment_spec.rb b/spec/jira/resource/attachment_spec.rb index a8526c56..2126c34b 100644 --- a/spec/jira/resource/attachment_spec.rb +++ b/spec/jira/resource/attachment_spec.rb @@ -71,7 +71,7 @@ issue = JIRA::Resource::Issue.new(client) path_to_file = './spec/mock_responses/issue.json' attachment = JIRA::Resource::Attachment.new(client, issue: issue) - attachment.save!('file' => path_to_file) + attachment.save!('file' => path_to_file, mimeType: 'image/jpeg') expect(attachment.filename).to eq 'picture.jpg' expect(attachment.mimeType).to eq 'image/jpeg' From 06dc638b7d2ecc787e33986cd17c4c9cedd0b560 Mon Sep 17 00:00:00 2001 From: Kevin Jin Date: Sat, 16 Mar 2019 06:40:12 -0700 Subject: [PATCH 04/94] added save method and wrote tests --- lib/jira/resource/watcher.rb | 7 +++++++ spec/integration/watcher_spec.rb | 21 +++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/jira/resource/watcher.rb b/lib/jira/resource/watcher.rb index a8f7778b..be9f16da 100644 --- a/lib/jira/resource/watcher.rb +++ b/lib/jira/resource/watcher.rb @@ -23,6 +23,13 @@ def self.all(client, options = {}) issue.watchers.build(watcher) end end + + def save!(user_id, path = nil) + path ||= new_record? ? url : patched_url + response = client.post(path, user_id.to_json) + true + end + end end end diff --git a/spec/integration/watcher_spec.rb b/spec/integration/watcher_spec.rb index 20b8b82a..3edb5813 100644 --- a/spec/integration/watcher_spec.rb +++ b/spec/integration/watcher_spec.rb @@ -33,21 +33,30 @@ end describe 'watchers' do - it 'should returns all the watchers' do - stub_request(:get, - site_url + '/jira/rest/api/2/issue/10002') + before(:each) do + stub_request(:get, site_url + '/jira/rest/api/2/issue/10002') .to_return(status: 200, body: get_mock_response('issue/10002.json')) - stub_request(:get, - site_url + '/jira/rest/api/2/issue/10002/watchers') + stub_request(:get, site_url + '/jira/rest/api/2/issue/10002/watchers') .to_return(status: 200, body: get_mock_response('issue/10002/watchers.json')) + stub_request(:post, site_url + '/jira/rest/api/2/issue/10002/watchers') + .to_return(status: 204, body: nil) + end + + it 'should returns all the watchers' do issue = client.Issue.find('10002') watchers = client.Watcher.all(options = { issue: issue }) expect(watchers.length).to eq(1) end + + it 'should add a watcher' do + issue = client.Issue.find('10002') + watcher = JIRA::Resource::Watcher.new(client, issue: issue) + user_id = "tester" + watcher.save!(user_id) + end end - it_should_behave_like 'a resource' end end From 0302694ae20bb8e8e37e8b16d11ece4a0c027e9a Mon Sep 17 00:00:00 2001 From: Rutger Gelling Date: Mon, 18 Mar 2019 13:35:58 +0100 Subject: [PATCH 05/94] Fix a bug whith multiple params for JTW clients --- lib/jira/jwt_client.rb | 50 +++++++++++++++++++++----- spec/jira/jwt_uri_builder_spec.rb | 59 +++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 spec/jira/jwt_uri_builder_spec.rb diff --git a/lib/jira/jwt_client.rb b/lib/jira/jwt_client.rb index 9ae3e50f..ea2a50a0 100644 --- a/lib/jira/jwt_client.rb +++ b/lib/jira/jwt_client.rb @@ -4,7 +4,7 @@ module JIRA class JwtClient < HttpClient def make_request(http_method, url, body = '', headers = {}) # When a proxy is enabled, Net::HTTP expects that the request path omits the domain name - path = request_path(url) + "?jwt=#{jwt_header(http_method, url)}" + path = request_path(http_method, url) request = Net::HTTP.const_get(http_method.to_s.capitalize).new(path, headers) request.body = body unless body.nil? @@ -15,18 +15,52 @@ def make_request(http_method, url, body = '', headers = {}) response 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 + end + private - def jwt_header(http_method, url) - claim = Atlassian::Jwt.build_claims \ - @options[:issuer], + def request_path(http_method, url) + JwtUriBuilder.new( url, http_method.to_s, + @options[:shared_secret], @options[:site], - (Time.now - 60).to_i, - (Time.now + (86400)).to_i - - JWT.encode claim, @options[:shared_secret] + @options[:issuer] + ).build end end end diff --git a/spec/jira/jwt_uri_builder_spec.rb b/spec/jira/jwt_uri_builder_spec.rb new file mode 100644 index 00000000..0e210d12 --- /dev/null +++ b/spec/jira/jwt_uri_builder_spec.rb @@ -0,0 +1,59 @@ +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 f44bb6654fcbe99efc0e3718df8131bdc2bf48d4 Mon Sep 17 00:00:00 2001 From: Kevin Butterfield Date: Tue, 16 Apr 2019 16:20:32 -0400 Subject: [PATCH 06/94] Add support for proxy usernames/passwords --- lib/jira/client.rb | 2 ++ lib/jira/http_client.rb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/jira/client.rb b/lib/jira/client.rb index d9f88b06..d5ba04e2 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -25,6 +25,8 @@ module JIRA # :auth_type => :oauth, # :proxy_address => nil, # :proxy_port => nil, + # :proxy_username => nil, + # :proxy_password => nil, # :additional_cookies => nil, # :default_headers => {} # diff --git a/lib/jira/http_client.rb b/lib/jira/http_client.rb index d99c6f59..aead2929 100644 --- a/lib/jira/http_client.rb +++ b/lib/jira/http_client.rb @@ -43,7 +43,7 @@ def basic_auth_http_conn def http_conn(uri) if @options[:proxy_address] - http_class = Net::HTTP::Proxy(@options[:proxy_address], @options[:proxy_port] || 80) + http_class = Net::HTTP::Proxy(@options[:proxy_address], @options[:proxy_port] || 80, @options[:proxy_username], @options[:proxy_password]) else http_class = Net::HTTP end From 458586274bff48ad73ddd6c7346f8d4e184784c1 Mon Sep 17 00:00:00 2001 From: Simon Lacroix Date: Thu, 11 Jul 2019 17:23:11 +0200 Subject: [PATCH 07/94] Bump version to 1.7.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 82910e63..9e7c2395 100644 --- a/lib/jira/version.rb +++ b/lib/jira/version.rb @@ -1,3 +1,3 @@ module JIRA - VERSION = '1.6.0'.freeze + VERSION = '1.7.0'.freeze end From 9156d7261764a184c8082785cbbb7fd2885ac7a6 Mon Sep 17 00:00:00 2001 From: Olle Jonsson Date: Sun, 14 Jul 2019 14:01:29 +0200 Subject: [PATCH 08/94] gemspec: drop rubyforge_project, it is EOL --- jira-ruby.gemspec | 2 -- 1 file changed, 2 deletions(-) diff --git a/jira-ruby.gemspec b/jira-ruby.gemspec index c898f811..02f01703 100644 --- a/jira-ruby.gemspec +++ b/jira-ruby.gemspec @@ -13,8 +13,6 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 1.9.3' - s.rubyforge_project = 'jira-ruby' - s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } From a41072fdb801514d2ec9401316c6b409f242fd1f Mon Sep 17 00:00:00 2001 From: Simon Lacroix Date: Mon, 19 Aug 2019 17:54:09 +0200 Subject: [PATCH 09/94] Bump version to 1.7.1 --- 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 9e7c2395..f92bb78c 100644 --- a/lib/jira/version.rb +++ b/lib/jira/version.rb @@ -1,3 +1,3 @@ module JIRA - VERSION = '1.7.0'.freeze + VERSION = '1.7.1'.freeze end From 581bde208631d1e64c32b4cac30a1b57ee431bd4 Mon Sep 17 00:00:00 2001 From: Kevin Butterfield Date: Tue, 20 Aug 2019 15:53:57 -0400 Subject: [PATCH 10/94] Add tests --- spec/jira/http_client_spec.rb | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/spec/jira/http_client_spec.rb b/spec/jira/http_client_spec.rb index 4c81150e..a486b15f 100644 --- a/spec/jira/http_client_spec.rb +++ b/spec/jira/http_client_spec.rb @@ -36,6 +36,16 @@ JIRA::HttpClient.new(options) end + let(:proxy_client) do + options = 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) + end + let(:response) do response = double('response') allow(response).to receive(:kind_of?).with(Net::HTTPSuccess).and_return(true) @@ -178,6 +188,36 @@ expect(basic_client.http_conn(uri)).to eq(http_conn) end + it 'sets up a non-proxied http connection by default' do + uri = double + host = double + port = double + + expect(uri).to receive(:host).and_return(host) + expect(uri).to receive(:port).and_return(port) + + proxy_configuration = basic_client.http_conn(uri).class + expect(proxy_configuration.proxy_address).to be_nil + expect(proxy_configuration.proxy_port).to be_nil + expect(proxy_configuration.proxy_user).to be_nil + 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 + + expect(uri).to receive(:host).and_return(host) + expect(uri).to receive(:port).and_return(port) + + 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]) + end + it 'can use client certificates' do http_conn = double uri = double From 3d06b8204cc19fa3c63fbe0f4955fce148778a3c Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Fri, 30 Aug 2019 16:26:51 +0900 Subject: [PATCH 11/94] Ensure Ruby 2.5/2.6-based tests on CI process Signed-off-by: Takuya Noguchi --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 1bab3706..f4c50dcf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,8 @@ language: ruby rvm: - 2.3 - 2.4 + - 2.5 + - 2.6 - ruby-head before_script: - rake jira:generate_public_cert From b387f5cc8df2e5ec725857de45bfedf746ff4783 Mon Sep 17 00:00:00 2001 From: Simon Lacroix Date: Fri, 30 Aug 2019 10:57:31 +0200 Subject: [PATCH 12/94] Drop CI for ruby 2.3 Support of Ruby 2.3 has ended https://www.ruby-lang.org/en/news/2019/03/31/support-of-ruby-2-3-has-ended/ --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1bab3706..860cad40 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: ruby rvm: - - 2.3 - 2.4 - ruby-head before_script: From 35aeac3e35ec850b826e3280b280c9b2eb1d6757 Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Tue, 8 Oct 2019 17:48:58 +0200 Subject: [PATCH 13/94] Add the :ssl_version option to the JIRA::Client class Allow callers to specify a fixed SSL Version for the HTTP connections. --- lib/jira/client.rb | 1 + lib/jira/http_client.rb | 1 + spec/jira/http_client_spec.rb | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/lib/jira/client.rb b/lib/jira/client.rb index d9f88b06..014c8a4f 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -19,6 +19,7 @@ module JIRA # :consumer_key => nil, # :consumer_secret => nil, # :ssl_verify_mode => OpenSSL::SSL::VERIFY_PEER, + # :ssl_version => nil, # :use_ssl => true, # :username => nil, # :password => nil, diff --git a/lib/jira/http_client.rb b/lib/jira/http_client.rb index d99c6f59..4d5b460e 100644 --- a/lib/jira/http_client.rb +++ b/lib/jira/http_client.rb @@ -54,6 +54,7 @@ def http_conn(uri) http_conn.key = @options[:key] end 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 end diff --git a/spec/jira/http_client_spec.rb b/spec/jira/http_client_spec.rb index 4c81150e..708dc3b4 100644 --- a/spec/jira/http_client_spec.rb +++ b/spec/jira/http_client_spec.rb @@ -11,6 +11,11 @@ JIRA::HttpClient.new(options) end + let(:custom_ssl_version_client) do + options = JIRA::Client::DEFAULT_OPTIONS.merge(JIRA::HttpClient::DEFAULT_OPTIONS).merge(ssl_version: :TLSv1_2) + JIRA::HttpClient.new(options) + end + let(:basic_cookie_client_with_context_path) do options = JIRA::Client::DEFAULT_OPTIONS.merge(JIRA::HttpClient::DEFAULT_OPTIONS).merge( use_cookies: true, @@ -178,6 +183,21 @@ expect(basic_client.http_conn(uri)).to eq(http_conn) end + it 'sets the SSL version when one is provided' 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(:ssl_version=).with(custom_ssl_version_client.options[:ssl_version]).and_return(http_conn) + expect(http_conn).to receive(:read_timeout=).with(basic_client.options[:read_timeout]).and_return(http_conn) + expect(custom_ssl_version_client.http_conn(uri)).to eq(http_conn) + end + it 'can use client certificates' do http_conn = double uri = double From d80005306216114d30fa41b470a47951895f4a5f Mon Sep 17 00:00:00 2001 From: Dan Callaghan Date: Fri, 11 Oct 2019 08:11:28 +1100 Subject: [PATCH 14/94] Default HttpClient basic auth credentials to nil --- lib/jira/http_client.rb | 6 ++--- spec/jira/http_client_spec.rb | 43 ++++++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/lib/jira/http_client.rb b/lib/jira/http_client.rb index d99c6f59..02062ffa 100644 --- a/lib/jira/http_client.rb +++ b/lib/jira/http_client.rb @@ -6,8 +6,8 @@ module JIRA class HttpClient < RequestClient DEFAULT_OPTIONS = { - username: '', - password: '' + username: nil, + password: nil }.freeze attr_reader :options @@ -18,7 +18,7 @@ def initialize(options) end def make_cookie_auth_request - body = { username: @options[:username], password: @options[:password] }.to_json + body = { username: @options[:username].to_s, password: @options[:password].to_s }.to_json @options.delete(:username) @options.delete(:password) make_request(:post, @options[:context_path] + '/rest/auth/1/session', body, 'Content-Type' => 'application/json') diff --git a/spec/jira/http_client_spec.rb b/spec/jira/http_client_spec.rb index 4c81150e..b2ed9f2a 100644 --- a/spec/jira/http_client_spec.rb +++ b/spec/jira/http_client_spec.rb @@ -2,12 +2,17 @@ describe JIRA::HttpClient do let(:basic_client) do - options = JIRA::Client::DEFAULT_OPTIONS.merge(JIRA::HttpClient::DEFAULT_OPTIONS) + options = JIRA::Client::DEFAULT_OPTIONS + .merge(JIRA::HttpClient::DEFAULT_OPTIONS) + .merge(basic_auth_credentials) JIRA::HttpClient.new(options) end let(:basic_cookie_client) do - options = JIRA::Client::DEFAULT_OPTIONS.merge(JIRA::HttpClient::DEFAULT_OPTIONS).merge(use_cookies: true) + options = JIRA::Client::DEFAULT_OPTIONS + .merge(JIRA::HttpClient::DEFAULT_OPTIONS) + .merge(use_cookies: true) + .merge(basic_auth_credentials) JIRA::HttpClient.new(options) end @@ -20,10 +25,13 @@ end let(:basic_cookie_client_with_additional_cookies) do - options = JIRA::Client::DEFAULT_OPTIONS.merge(JIRA::HttpClient::DEFAULT_OPTIONS).merge( - use_cookies: true, - additional_cookies: ['sessionToken=abc123', 'internal=true'] - ) + options = JIRA::Client::DEFAULT_OPTIONS + .merge(JIRA::HttpClient::DEFAULT_OPTIONS) + .merge( + use_cookies: true, + additional_cookies: ['sessionToken=abc123', 'internal=true'] + ) + .merge(basic_auth_credentials) JIRA::HttpClient.new(options) end @@ -36,6 +44,16 @@ JIRA::HttpClient.new(options) end + let(:basic_client_with_no_auth_credentials) do + options = JIRA::Client::DEFAULT_OPTIONS + .merge(JIRA::HttpClient::DEFAULT_OPTIONS) + JIRA::HttpClient.new(options) + end + + let(:basic_auth_credentials) do + { username: 'donaldduck', password: 'supersecret' } + end + let(:response) do response = double('response') allow(response).to receive(:kind_of?).with(Net::HTTPSuccess).and_return(true) @@ -159,6 +177,19 @@ basic_client.make_request(:get, 'http://mydomain.com/foo', body, headers) end + it 'does not try to use basic auth if the credentials are not set' do + body = nil + headers = double + basic_auth_http_conn = double + http_request = double + expect(Net::HTTP::Get).to receive(:new).with('/foo', headers).and_return(http_request) + + expect(basic_auth_http_conn).to receive(:request).with(http_request).and_return(response) + expect(http_request).not_to receive(:basic_auth) + allow(basic_client_with_no_auth_credentials).to receive(:basic_auth_http_conn).and_return(basic_auth_http_conn) + basic_client_with_no_auth_credentials.make_request(:get, '/foo', body, headers) + end + it 'returns a URI' do uri = URI.parse(basic_client.options[:site]) expect(basic_client.uri).to eq(uri) From eff84e77bbb4ec87c0c51f33224b02b085065e18 Mon Sep 17 00:00:00 2001 From: Karam EL HABOUTI Date: Fri, 11 Oct 2019 08:59:04 +0200 Subject: [PATCH 15/94] Remove duplicate method definition in client.rb --- lib/jira/client.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/jira/client.rb b/lib/jira/client.rb index d9f88b06..7dcae9cd 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -203,10 +203,6 @@ def Remotelink JIRA::Resource::RemotelinkFactory.new(self) end - def Sprint - JIRA::Resource::SprintFactory.new(self) - end - def Agile JIRA::Resource::AgileFactory.new(self) end From 9dbdb768299783ec161ae2f15b6713c622b12894 Mon Sep 17 00:00:00 2001 From: Rutger Gelling Date: Wed, 30 Jan 2019 10:48:34 +0100 Subject: [PATCH 16/94] Implement multipart requests for all client types --- lib/jira/client.rb | 5 +++++ lib/jira/http_client.rb | 28 +++++++++++++++++++++------- lib/jira/jwt_client.rb | 15 +++++++-------- lib/jira/oauth_client.rb | 20 +++++++++++++++----- lib/jira/request_client.rb | 18 +++++++++++++++--- lib/jira/resource/attachment.rb | 31 +++++++++++++++++-------------- 6 files changed, 80 insertions(+), 37 deletions(-) diff --git a/lib/jira/client.rb b/lib/jira/client.rb index ea245dc5..db57c5d0 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -227,6 +227,11 @@ def post(path, body = '', headers = {}) request(:post, path, body, merge_default_headers(headers)) end + def post_multipart(path, file, headers = {}) + puts "post multipart: #{path} - [#{file}]" if @http_debug + @request_client.request_multipart(path, file, headers) + end + def put(path, body = '', headers = {}) headers = { 'Content-Type' => 'application/json' }.merge(headers) request(:put, path, body, merge_default_headers(headers)) diff --git a/lib/jira/http_client.rb b/lib/jira/http_client.rb index b2e8dc1a..d8b4ff6d 100644 --- a/lib/jira/http_client.rb +++ b/lib/jira/http_client.rb @@ -29,12 +29,15 @@ def make_request(http_method, url, body = '', headers = {}) path = request_path(url) request = Net::HTTP.const_get(http_method.to_s.capitalize).new(path, headers) request.body = body unless body.nil? - add_cookies(request) if options[:use_cookies] - request.basic_auth(@options[:username], @options[:password]) if @options[:username] && @options[:password] - response = basic_auth_http_conn.request(request) - @authenticated = response.is_a? Net::HTTPOK - store_cookies(response) if options[:use_cookies] - response + + execute_request(request) + end + + def make_multipart_request(url, data, headers = {}) + path = request_path(url) + request = Net::HTTP::Post::Multipart.new path, data, headers + + execute_request(request) end def basic_auth_http_conn @@ -60,7 +63,7 @@ def http_conn(uri) end def uri - uri = URI.parse(@options[:site]) + URI.parse(@options[:site]) end def authenticated? @@ -69,6 +72,17 @@ def authenticated? private + def execute_request(request) + add_cookies(request) if options[:use_cookies] + request.basic_auth(@options[:username], @options[:password]) if @options[:username] && @options[:password] + + response = basic_auth_http_conn.request(request) + @authenticated = response.is_a? Net::HTTPOK + store_cookies(response) if options[:use_cookies] + + response + end + def request_path(url) parsed_uri = URI(url) diff --git a/lib/jira/jwt_client.rb b/lib/jira/jwt_client.rb index ea2a50a0..d363ae66 100644 --- a/lib/jira/jwt_client.rb +++ b/lib/jira/jwt_client.rb @@ -3,16 +3,15 @@ module JIRA class JwtClient < HttpClient def make_request(http_method, url, body = '', headers = {}) - # When a proxy is enabled, Net::HTTP expects that the request path omits the domain name - path = request_path(http_method, url) + @http_method = http_method - request = Net::HTTP.const_get(http_method.to_s.capitalize).new(path, headers) - request.body = body unless body.nil? + super(http_method, url, body, headers) + end + + def make_multipart_request(url, data, headers = {}) + @http_method = :post - response = basic_auth_http_conn.request(request) - @authenticated = response.is_a? Net::HTTPOK - store_cookies(response) if options[:use_cookies] - response + super(url, data, headers) end class JwtUriBuilder diff --git a/lib/jira/oauth_client.rb b/lib/jira/oauth_client.rb index 4a047810..f6ed6132 100644 --- a/lib/jira/oauth_client.rb +++ b/lib/jira/oauth_client.rb @@ -72,29 +72,39 @@ def access_token @access_token end - def make_request(http_method, path, body = '', headers = {}) + def make_request(http_method, url, body = '', headers = {}) # When using oauth_2legged we need to add an empty oauth_token parameter to every request. if @options[:auth_type] == :oauth_2legged oauth_params_str = 'oauth_token=' - uri = URI.parse(path) + uri = URI.parse(url) uri.query = if uri.query.to_s == '' oauth_params_str else uri.query + '&' + oauth_params_str end - path = uri.to_s + url = uri.to_s end case http_method when :delete, :get, :head - response = access_token.send http_method, path, headers + response = access_token.send http_method, url, headers when :post, :put - response = access_token.send http_method, path, body, headers + response = access_token.send http_method, url, body, headers end @authenticated = true response end + def make_multipart_request(url, data, headers = {}) + request = Net::HTTP::Post::Multipart.new url, data, headers + + access_token.sign! request + + response = consumer.http.request(request) + @authenticated = true + response + end + def authenticated? @authenticated end diff --git a/lib/jira/request_client.rb b/lib/jira/request_client.rb index de5b875c..d90ae9d6 100644 --- a/lib/jira/request_client.rb +++ b/lib/jira/request_client.rb @@ -1,7 +1,6 @@ require 'oauth' require 'json' require 'net/https' -# require 'pry' module JIRA class RequestClient @@ -11,9 +10,22 @@ class RequestClient def request(*args) response = make_request(*args) - # binding.pry unless response.kind_of?(Net::HTTPSuccess) raise HTTPError, response unless response.is_a?(Net::HTTPSuccess) response end + + def request_multipart(*args) + response = make_multipart_request(*args) + raise HTTPError, response unless response.is_a?(Net::HTTPSuccess) + response + end + + def make_request(*args) + raise NotImplementedError + end + + def make_multipart_request(*args) + raise NotImplementedError + end end -end +end \ No newline at end of file diff --git a/lib/jira/resource/attachment.rb b/lib/jira/resource/attachment.rb index 2f2e5983..17041416 100644 --- a/lib/jira/resource/attachment.rb +++ b/lib/jira/resource/attachment.rb @@ -20,26 +20,29 @@ def self.meta(client) end def save!(attrs) - headers = { 'X-Atlassian-Token' => 'nocheck' } - data = { 'file' => UploadIO.new(attrs['file'], 'application/binary', attrs['file']) } + headers = { 'X-Atlassian-Token' => 'no-check' } + data = { 'file' => UploadIO.new(attrs[:file], 'application/binary', attrs[:file]) } - request = Net::HTTP::Post::Multipart.new url, data, headers - request.basic_auth(client.request_client.options[:username], - client.request_client.options[:password]) + # Execute the multipart post here + response = client.post_multipart(url, data , headers) - response = client.request_client.basic_auth_http_conn.request(request) - - set_attrs(attrs, false) - unless response.body.nil? || response.body.length < 2 - json = self.class.parse_json(response.body) - attachment = json[0] - - set_attrs(attachment) - end + set_attributes(attrs, response) @expanded = false true end + + private + + def set_attributes(attributes, response) + set_attrs(attributes, false) + return if response.body.nil? || response.body.length < 2 + + json = self.class.parse_json(response.body) + attachment = json[0] + + set_attrs(attachment) + end end end end From fdcbadc39205b9699c7af2e7c4d2b79871712da6 Mon Sep 17 00:00:00 2001 From: Rutger Gelling Date: Wed, 30 Jan 2019 10:51:15 +0100 Subject: [PATCH 17/94] Add specs for new multipart requests And refactor some specs to be more structured and easier to handle --- lib/jira/http_error.rb | 2 +- spec/jira/http_client_spec.rb | 31 ++++++++++++++ spec/jira/oauth_client_spec.rb | 37 ++++++++++++----- spec/jira/request_client_spec.rb | 47 ++++++++++++++++----- spec/jira/resource/attachment_spec.rb | 59 ++++++++++++++++----------- 5 files changed, 132 insertions(+), 44 deletions(-) diff --git a/lib/jira/http_error.rb b/lib/jira/http_error.rb index e48cada5..1612d78e 100644 --- a/lib/jira/http_error.rb +++ b/lib/jira/http_error.rb @@ -8,7 +8,7 @@ class HTTPError < StandardError def initialize(response) @response = response - @message = response.try(:message) || response.try(:body) + @message = response.try(:message).presence || response.try(:body) end end end diff --git a/spec/jira/http_client_spec.rb b/spec/jira/http_client_spec.rb index ab1684b1..e99006be 100644 --- a/spec/jira/http_client_spec.rb +++ b/spec/jira/http_client_spec.rb @@ -252,4 +252,35 @@ expect(basic_client).to receive(:http_conn).and_return(http_conn) expect(basic_client.basic_auth_http_conn).to eq(http_conn) end + + describe '#make_multipart_request' do + subject { JIRA::HttpClient.new(client_options).make_multipart_request(path, data, headers) } + + let(:client_options) { JIRA::Client::DEFAULT_OPTIONS.merge(JIRA::HttpClient::DEFAULT_OPTIONS) } + let(:headers) { { 'X-Atlassian-Token' => 'no-check' } } + let(:data) { {} } + let(:path) { '/foo' } + let(:basic_auth_http_conn) { double } + let(:request){ double } + let(:response) { double('response') } + + before do + allow(request).to receive(:basic_auth) + allow(response).to receive(:get_fields).with('set-cookie') + allow(Net::HTTP::Post::Multipart).to receive(:new).with(path, data, headers).and_return(request) + end + + it 'performs a basic http client request' do + expect(request).to receive(:basic_auth).with(client.options[:username], client.options[:password]).and_return(request) + + subject + end + + it 'makes a correct HTTP request' do + expect(basic_auth_http_conn).to receive(:request).with(request).and_return(response) + expect(response).to receive(:is_a?).with(Net::HTTPOK) + + subject + end + end end diff --git a/spec/jira/oauth_client_spec.rb b/spec/jira/oauth_client_spec.rb index 82355b85..048b1d68 100644 --- a/spec/jira/oauth_client_spec.rb +++ b/spec/jira/oauth_client_spec.rb @@ -82,28 +82,45 @@ end describe 'http' do + let(:headers) { double } + let(:access_token) { double } + let(:body) { nil } + + before do + allow(oauth_client).to receive(:access_token).and_return(access_token) + end + it 'responds to the http methods' do - headers = double - mock_access_token = double - allow(oauth_client).to receive(:access_token).and_return(mock_access_token) %i[delete get head].each do |method| - expect(mock_access_token).to receive(method).with('/path', headers).and_return(response) + expect(access_token).to receive(method).with('/path', headers).and_return(response) oauth_client.make_request(method, '/path', '', headers) end %i[post put].each do |method| - expect(mock_access_token).to receive(method).with('/path', '', headers).and_return(response) + expect(access_token).to receive(method).with('/path', '', headers).and_return(response) oauth_client.make_request(method, '/path', '', headers) end end it 'performs a request' do - body = nil - headers = double - access_token = double expect(access_token).to receive(:send).with(:get, '/foo', headers).and_return(response) - allow(oauth_client).to receive(:access_token).and_return(access_token) + + oauth_client.request(:get, '/foo', body, headers) end + + context 'for a multipart request' do + subject { oauth_client.make_multipart_request('/path', data, headers) } + + let(:data) { {} } + let(:headers) { {} } + + it 'signs the access_token and performs the request' do + expect(access_token).to receive(:sign!).with(an_instance_of(Net::HTTP::Post::Multipart)) + expect(oauth_client.consumer).to receive_message_chain(:http, :request).with(an_instance_of(Net::HTTP::Post::Multipart)) + + subject + end + end end describe 'auth type is oauth_2legged' do @@ -142,4 +159,4 @@ end end end -end +end \ No newline at end of file diff --git a/spec/jira/request_client_spec.rb b/spec/jira/request_client_spec.rb index 5453e216..1de1fcee 100644 --- a/spec/jira/request_client_spec.rb +++ b/spec/jira/request_client_spec.rb @@ -1,14 +1,41 @@ require 'spec_helper' describe JIRA::RequestClient do - it 'raises an exception for non success responses' do - response = double - allow(response).to receive(:kind_of?).with(Net::HTTPSuccess).and_return(false) - rc = JIRA::RequestClient.new - expect(rc).to receive(:make_request).with(:get, '/foo', '', {}).and_return(response) - - expect do - rc.request(:get, '/foo', '', {}) - end.to raise_exception(JIRA::HTTPError) + let(:request_client) { JIRA::RequestClient.new } + + describe '#request' do + subject(:request) { request_client.request(:get, '/foo', '', {}) } + + context 'when doing a request fails' do + let(:response) { double } + + before do + allow(response).to receive(:kind_of?).with(Net::HTTPSuccess).and_return(false) + allow(request_client).to receive(:make_request).with(:get, '/foo', '', {}).and_return(response) + end + + it 'raises an exception' do + expect{ subject }.to raise_exception(JIRA::HTTPError) + end + end + end + + describe '#request_multipart' do + subject(:request) { request_client.request_multipart('/foo', data, {}) } + + let(:data) { double } + + context 'when doing a request fails' do + let(:response) { double } + + before do + allow(response).to receive(:kind_of?).with(Net::HTTPSuccess).and_return(false) + allow(request_client).to receive(:make_multipart_request).with('/foo', data, {}).and_return(response) + end + + it 'raises an exception' do + expect{ subject }.to raise_exception(JIRA::HTTPError) + end + end end -end +end \ No newline at end of file diff --git a/spec/jira/resource/attachment_spec.rb b/spec/jira/resource/attachment_spec.rb index a8526c56..7f31e1fe 100644 --- a/spec/jira/resource/attachment_spec.rb +++ b/spec/jira/resource/attachment_spec.rb @@ -1,6 +1,14 @@ require 'spec_helper' describe JIRA::Resource::Attachment do + subject(:attachment) do + JIRA::Resource::Attachment.new( + client, + issue: JIRA::Resource::Issue.new(client), + attrs: { 'author' => { 'foo' => 'bar' } } + ) + end + let(:client) do double( 'client', @@ -17,42 +25,46 @@ end describe 'relationships' do - subject do - JIRA::Resource::Attachment.new(client, - issue: JIRA::Resource::Issue.new(client), - attrs: { 'author' => { 'foo' => 'bar' } }) + it 'has an author' do + expect(subject).to have_one(:author, JIRA::Resource::User) end - it 'has the correct relationships' do - expect(subject).to have_one(:author, JIRA::Resource::User) + it 'has the correct author name' do expect(subject.author.foo).to eq('bar') end end - describe '#meta' do + describe '.meta' do + subject { JIRA::Resource::Attachment.meta(client) } + let(:response) do double( - 'response', - body: '{"enabled":true,"uploadLimit":10485760}' + 'response', + body: '{"enabled":true,"uploadLimit":10485760}' ) end it 'returns meta information about attachment upload' do expect(client).to receive(:get).with('/jira/rest/api/2/attachment/meta').and_return(response) - JIRA::Resource::Attachment.meta(client) + + subject end - subject { JIRA::Resource::AttachmentFactory.new(client) } + context 'the factory delegates correctly' do + subject { JIRA::Resource::AttachmentFactory.new(client) } - it 'delegates #meta to to target class' do - expect(subject).to respond_to(:meta) + it 'delegates #meta to to target class' do + expect(subject).to respond_to(:meta) + end end end describe '#save!' do - it 'successfully update the attachment' do - basic_auth_http_conn = double - response = double( + subject { attachment.save!(:file => path_to_file) } + + let(:path_to_file) { './spec/mock_responses/issue.json' } + let(:response) do + double( body: [ { "id": 10_001, @@ -64,18 +76,19 @@ } ].to_json ) + end + let(:issue) { JIRA::Resource::Issue.new(client) } - allow(client.request_client).to receive(:basic_auth_http_conn).and_return(basic_auth_http_conn) - allow(basic_auth_http_conn).to receive(:request).and_return(response) + before do + allow(client).to receive(:post_multipart).and_return(response) + end - issue = JIRA::Resource::Issue.new(client) - path_to_file = './spec/mock_responses/issue.json' - attachment = JIRA::Resource::Attachment.new(client, issue: issue) - attachment.save!('file' => path_to_file) + 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 -end +end \ No newline at end of file From 03cf2cd467367e34e843863c93b32213afbea61e Mon Sep 17 00:00:00 2001 From: Rutger Gelling Date: Mon, 4 Mar 2019 13:25:56 +0100 Subject: [PATCH 18/94] Don't break functionality for existing integrations Since for other Jira Resources accept both symbols and strings as key for files i think it makes sense to support both here too. --- lib/jira/http_client.rb | 4 ++-- lib/jira/jwt_client.rb | 4 +++- lib/jira/resource/attachment.rb | 3 ++- spec/jira/http_client_spec.rb | 16 +++++++++------- spec/jira/resource/attachment_spec.rb | 14 +++++++++++++- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/lib/jira/http_client.rb b/lib/jira/http_client.rb index d8b4ff6d..11523e0c 100644 --- a/lib/jira/http_client.rb +++ b/lib/jira/http_client.rb @@ -33,9 +33,9 @@ def make_request(http_method, url, body = '', headers = {}) execute_request(request) end - def make_multipart_request(url, data, headers = {}) + def make_multipart_request(url, body, headers = {}) path = request_path(url) - request = Net::HTTP::Post::Multipart.new path, data, headers + request = Net::HTTP::Post::Multipart.new(path, body, headers) execute_request(request) end diff --git a/lib/jira/jwt_client.rb b/lib/jira/jwt_client.rb index d363ae66..033301ff 100644 --- a/lib/jira/jwt_client.rb +++ b/lib/jira/jwt_client.rb @@ -52,7 +52,9 @@ def jwt_header private - def request_path(http_method, url) + attr_reader :http_method + + def request_path(url) JwtUriBuilder.new( url, http_method.to_s, diff --git a/lib/jira/resource/attachment.rb b/lib/jira/resource/attachment.rb index 17041416..ca7963b7 100644 --- a/lib/jira/resource/attachment.rb +++ b/lib/jira/resource/attachment.rb @@ -21,7 +21,8 @@ def self.meta(client) def save!(attrs) headers = { 'X-Atlassian-Token' => 'no-check' } - data = { 'file' => UploadIO.new(attrs[:file], 'application/binary', attrs[:file]) } + file = attrs['file'] || attrs[:file] + data = { 'file' => UploadIO.new(file, 'application/binary', file) } # Execute the multipart post here response = client.post_multipart(url, data , headers) diff --git a/spec/jira/http_client_spec.rb b/spec/jira/http_client_spec.rb index e99006be..47c36767 100644 --- a/spec/jira/http_client_spec.rb +++ b/spec/jira/http_client_spec.rb @@ -254,24 +254,26 @@ end describe '#make_multipart_request' do - subject { JIRA::HttpClient.new(client_options).make_multipart_request(path, data, headers) } + subject do + basic_client.make_multipart_request(path, data, headers) + end - let(:client_options) { JIRA::Client::DEFAULT_OPTIONS.merge(JIRA::HttpClient::DEFAULT_OPTIONS) } - let(:headers) { { 'X-Atlassian-Token' => 'no-check' } } - let(:data) { {} } let(:path) { '/foo' } + let(:data) { {} } + let(:headers) { { 'X-Atlassian-Token' => 'no-check' } } let(:basic_auth_http_conn) { double } - let(:request){ double } + let(:request) { double('Http Request', path: path) } let(:response) { double('response') } before do allow(request).to receive(:basic_auth) - allow(response).to receive(:get_fields).with('set-cookie') allow(Net::HTTP::Post::Multipart).to receive(:new).with(path, data, headers).and_return(request) + allow(basic_client).to receive(:basic_auth_http_conn).and_return(basic_auth_http_conn) + allow(basic_auth_http_conn).to receive(:request).with(request).and_return(response) end it 'performs a basic http client request' do - expect(request).to receive(:basic_auth).with(client.options[:username], client.options[:password]).and_return(request) + expect(request).to receive(:basic_auth).with(basic_client.options[:username], basic_client.options[:password]).and_return(request) subject end diff --git a/spec/jira/resource/attachment_spec.rb b/spec/jira/resource/attachment_spec.rb index 7f31e1fe..24d4917a 100644 --- a/spec/jira/resource/attachment_spec.rb +++ b/spec/jira/resource/attachment_spec.rb @@ -60,7 +60,7 @@ end describe '#save!' do - subject { attachment.save!(:file => path_to_file) } + subject { attachment.save!('file' => path_to_file) } let(:path_to_file) { './spec/mock_responses/issue.json' } let(:response) do @@ -90,5 +90,17 @@ 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) } + + 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 end end \ No newline at end of file From b9edcf51e452dd1fff0d68db287e1e318a4dddce Mon Sep 17 00:00:00 2001 From: Kevin Butterfield Date: Wed, 4 Mar 2020 15:59:45 -0500 Subject: [PATCH 19/94] Re-add missing test helper --- spec/jira/http_client_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/jira/http_client_spec.rb b/spec/jira/http_client_spec.rb index 458c8c90..4334d80c 100644 --- a/spec/jira/http_client_spec.rb +++ b/spec/jira/http_client_spec.rb @@ -59,6 +59,16 @@ { username: 'donaldduck', password: 'supersecret' } end + let(:proxy_client) do + options = 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) + end + let(:response) do response = double('response') allow(response).to receive(:kind_of?).with(Net::HTTPSuccess).and_return(true) From f405972ad271c643904c737207cbef1df9aa3b18 Mon Sep 17 00:00:00 2001 From: Ryan Sonnek Date: Fri, 6 Mar 2020 13:37:38 -0600 Subject: [PATCH 20/94] Add specs for board configuration --- spec/jira/resource/board_spec.rb | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/spec/jira/resource/board_spec.rb b/spec/jira/resource/board_spec.rb index eaefe655..288889e6 100644 --- a/spec/jira/resource/board_spec.rb +++ b/spec/jira/resource/board_spec.rb @@ -172,4 +172,53 @@ class JIRAResourceDelegation < SimpleDelegator # :nodoc: expect(client).to receive(:Sprint).twice.and_return(JIRA::Resource::SprintFactory.new(client)) expect(board.sprints.size).to be(2) end + + it 'should get board configuration for a board' do + response = double + + api_json = <<-eos + { + "id":1, + "name":"My Board", + "type":"kanban", + "self":"https://mycompany.atlassian.net/rest/agile/1.0/board/1/configuration", + "location":{ + "type":"project", + "key":"MYPROJ", + "id":"10000", + "self":"https://mycompany.atlassian.net/rest/api/2/project/10000", + "name":"My Project" + }, + "filter":{ + "id":"10000", + "self":"https://mycompany.atlassian.net/rest/api/2/filter/10000" + }, + "subQuery":{ + "query":"resolution = EMPTY OR resolution != EMPTY AND resolutiondate >= -5d" + }, + "columnConfig":{ + "columns":[ + { + "name":"Backlog", + "statuses":[ + { + "id":"10000", + "self":"https://mycompany.atlassian.net/rest/api/2/status/10000" + } + ] + } + ], + "constraintType":"issueCount" + }, + "ranking":{ + "rankCustomFieldId":10011 + } + } + eos + allow(response).to receive(:body).and_return(api_json) + allow(board).to receive(:id).and_return(84) + expect(client).to receive(:get).with('/rest/agile/1.0/board/84/configuration').and_return(response) + expect(client).to receive(:BoardConfiguration).and_return(JIRA::Resource::BoardConfigurationFactory.new(client)) + expect(board.configuration).not_to be(nil) + end end From 39f860db4d0f463750b69960be0e038ade61da46 Mon Sep 17 00:00:00 2001 From: Simon Lacroix Date: Sat, 14 Mar 2020 16:53:33 +0100 Subject: [PATCH 21/94] Bump version to 1.8.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 f92bb78c..c70ffcb6 100644 --- a/lib/jira/version.rb +++ b/lib/jira/version.rb @@ -1,3 +1,3 @@ module JIRA - VERSION = '1.7.1'.freeze + VERSION = '1.8.0'.freeze end From d79299f0675f1abef7152ec782612168d1c0e590 Mon Sep 17 00:00:00 2001 From: Fabien Garcia Date: Mon, 16 Mar 2020 11:29:11 +0100 Subject: [PATCH 22/94] Unify and fix test for save --- spec/jira/resource/attachment_spec.rb | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/spec/jira/resource/attachment_spec.rb b/spec/jira/resource/attachment_spec.rb index 61bdd232..03e0c722 100644 --- a/spec/jira/resource/attachment_spec.rb +++ b/spec/jira/resource/attachment_spec.rb @@ -60,9 +60,10 @@ end describe '#save' do - it 'successfully update the attachment' do - basic_auth_http_conn = double - response = double( + subject { attachment.save('file' => path_to_file) } + let(:path_to_file) { './spec/mock_responses/issue.json' } + let(:response) do + double( body: [ { "id": 10_001, @@ -74,14 +75,15 @@ } ].to_json ) + end + let(:issue) { JIRA::Resource::Issue.new(client) } - allow(client.request_client).to receive(:basic_auth_http_conn).and_return(basic_auth_http_conn) - allow(basic_auth_http_conn).to receive(:request).and_return(response) + before do + allow(client).to receive(:post_multipart).and_return(response) + end - issue = JIRA::Resource::Issue.new(client) - path_to_file = './spec/mock_responses/issue.json' - attachment = JIRA::Resource::Attachment.new(client, issue: issue) - attachment.save('file' => path_to_file) + it 'successfully update the attachment' do + subject expect(attachment.filename).to eq 'picture.jpg' expect(attachment.mimeType).to eq 'image/jpeg' @@ -133,4 +135,4 @@ end end end -end \ No newline at end of file +end From b2a13db569bbfab2940f560ffd406faf120de34c Mon Sep 17 00:00:00 2001 From: Fabien Garcia Date: Mon, 16 Mar 2020 11:37:54 +0100 Subject: [PATCH 23/94] Bump version number to 2.0.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 c70ffcb6..76c7f935 100644 --- a/lib/jira/version.rb +++ b/lib/jira/version.rb @@ -1,3 +1,3 @@ module JIRA - VERSION = '1.8.0'.freeze + VERSION = '2.0.0'.freeze end From 0e957cf95d5c5405258f75a0c76aa219b8b1e7fc Mon Sep 17 00:00:00 2001 From: Simon Lacroix Date: Mon, 16 Mar 2020 13:00:10 +0100 Subject: [PATCH 24/94] Set explicit ruby versions for travis ruby-head is currently at `2.8.0dev` when 2.7 has just been released, I think we should explicit the versions we want to test. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f353a9d7..6f17a8a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ rvm: - 2.4 - 2.5 - 2.6 - - ruby-head + - 2.7 before_script: - rake jira:generate_public_cert script: bundle exec rake spec From 62f14d9923da1cd77d5763d219eaffd464160d4f Mon Sep 17 00:00:00 2001 From: Peter Bell Date: Wed, 27 May 2020 04:46:53 +0100 Subject: [PATCH 25/94] Fixes Sprint::find to use context_path correctly --- .gitignore | 2 ++ lib/jira/resource/sprint.rb | 20 ++++++++++-------- spec/jira/resource/sprint_spec.rb | 34 +++++++++++++++++++++---------- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index dc5f2747..cc85860d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ pkg/* .DS_STORE doc .ruby-version + +.rakeTasks diff --git a/lib/jira/resource/sprint.rb b/lib/jira/resource/sprint.rb index 85c49de5..74beffd4 100644 --- a/lib/jira/resource/sprint.rb +++ b/lib/jira/resource/sprint.rb @@ -5,7 +5,7 @@ class SprintFactory < JIRA::BaseFactory # :nodoc: class Sprint < JIRA::Base def self.find(client, key) - response = client.get("#{client.options[:site]}/rest/agile/1.0/sprint/#{key}") + response = client.get(agile_path(client, key)) json = parse_json(response.body) client.Sprint.build(json) end @@ -19,7 +19,7 @@ def issues(options = {}) def add_issue(issue) request_body = { issues: [issue.id] }.to_json - response = client.post(client.options[:site] + "/rest/agile/1.0/sprint/#{id}/issue", request_body) + response = client.post("#{agile_path}/issue", request_body) true end @@ -47,8 +47,8 @@ def get_sprint_details_attribute(attribute_name) end def get_sprint_details - search_url = client.options[:site] + '/rest/greenhopper/1.0/rapid/charts/sprintreport?rapidViewId=' + - rapidview_id.to_s + '&sprintId=' + id.to_s + search_url = + "#{client.options[:site]}#{client.options[:client_path]}/rest/greenhopper/1.0/rapid/charts/sprintreport?rapidViewId=#{rapidview_id}&sprintId=#{id}" begin response = client.get(search_url) rescue StandardError @@ -76,12 +76,12 @@ def rapidview_id def save(attrs = {}, _path = nil) attrs = @attrs if attrs.empty? - super(attrs, agile_url) + super(attrs, agile_path) end def save!(attrs = {}, _path = nil) attrs = @attrs if attrs.empty? - super(attrs, agile_url) + super(attrs, agile_path) end # WORK IN PROGRESS @@ -93,8 +93,12 @@ def complete private - def agile_url - "#{client.options[:site]}/rest/agile/1.0/sprint/#{id}" + def agile_path + self.class.agile_path(client, id) + end + + def self.agile_path(client, key) + "#{client.options[:context_path]}/rest/agile/1.0/sprint/#{key}" end end end diff --git a/spec/jira/resource/sprint_spec.rb b/spec/jira/resource/sprint_spec.rb index 05904389..d3f4c3c0 100644 --- a/spec/jira/resource/sprint_spec.rb +++ b/spec/jira/resource/sprint_spec.rb @@ -1,12 +1,25 @@ require 'spec_helper' describe JIRA::Resource::Sprint do - describe 'peristence' do - let(:sprint) { described_class.new(client) } - let(:client) { double('Client', options: { site: 'https://foo.bar.com' }) } + let(:client) do + client = double(options: { site: 'https://foo.bar.com', 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}" } + describe '::find' do + let(:response) { double('Response', body: '{"some_detail":"some detail"}') } + + it 'fetches the sprint from JIRA' do + expect(client).to receive(:get).with('/jira/rest/agile/1.0/sprint/111').and_return(response) + expect(JIRA::Resource::Sprint.find(client, '111')).to be_a(JIRA::Resource::Sprint) + end + end + + describe 'peristence' do describe '#save' do - let(:agile_sprint_url) { "#{sprint.client.options[:site]}/rest/agile/1.0/sprint/#{sprint.id}" } let(:instance_attrs) { { start_date: '2016-06-01' } } before do @@ -17,7 +30,7 @@ let(:given_attrs) { { start_date: '2016-06-10' } } it 'calls save on the super class with the given attributes & agile url' do - expect_any_instance_of(JIRA::Base).to receive(:save).with(given_attrs, agile_sprint_url) + expect_any_instance_of(JIRA::Base).to receive(:save).with(given_attrs, agile_sprint_path) sprint.save(given_attrs) end @@ -25,7 +38,7 @@ context 'when attributes are not specified' do it 'calls save on the super class with the instance attributes & agile url' do - expect_any_instance_of(JIRA::Base).to receive(:save).with(instance_attrs, agile_sprint_url) + expect_any_instance_of(JIRA::Base).to receive(:save).with(instance_attrs, agile_sprint_path) sprint.save end @@ -33,7 +46,7 @@ context 'when providing the path argument' do it 'ignores it' do - expect_any_instance_of(JIRA::Base).to receive(:save).with(instance_attrs, agile_sprint_url) + expect_any_instance_of(JIRA::Base).to receive(:save).with(instance_attrs, agile_sprint_path) sprint.save({}, 'mavenlink.com') end @@ -41,7 +54,6 @@ end describe '#save!' do - let(:agile_sprint_url) { "#{sprint.client.options[:site]}/rest/agile/1.0/sprint/#{sprint.id}" } let(:instance_attrs) { { start_date: '2016-06-01' } } before do @@ -52,7 +64,7 @@ let(:given_attrs) { { start_date: '2016-06-10' } } it 'calls save! on the super class with the given attributes & agile url' do - expect_any_instance_of(JIRA::Base).to receive(:save!).with(given_attrs, agile_sprint_url) + expect_any_instance_of(JIRA::Base).to receive(:save!).with(given_attrs, agile_sprint_path) sprint.save!(given_attrs) end @@ -60,7 +72,7 @@ context 'when attributes are not specified' do it 'calls save! on the super class with the instance attributes & agile url' do - expect_any_instance_of(JIRA::Base).to receive(:save!).with(instance_attrs, agile_sprint_url) + expect_any_instance_of(JIRA::Base).to receive(:save!).with(instance_attrs, agile_sprint_path) sprint.save! end @@ -68,7 +80,7 @@ context 'when providing the path argument' do it 'ignores it' do - expect_any_instance_of(JIRA::Base).to receive(:save!).with(instance_attrs, agile_sprint_url) + expect_any_instance_of(JIRA::Base).to receive(:save!).with(instance_attrs, agile_sprint_path) sprint.save!({}, 'mavenlink.com') end From 9cbccb92effde25ea2d6e962006b24634c508e97 Mon Sep 17 00:00:00 2001 From: Peter Bell Date: Sat, 20 Jun 2020 10:39:56 +0100 Subject: [PATCH 26/94] 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 731ecd881ff6a333b09f316a76ea80230daed243 Mon Sep 17 00:00:00 2001 From: David Kinzer Date: Tue, 30 Jun 2020 22:58:13 -0400 Subject: [PATCH 27/94] Throw exception when passing unknown option. When creating an instance of a JIRA::CLient, raise ArgumentError if the option is unknown. Fixes sumoheavy/jira-ruby#356 --- lib/jira/client.rb | 38 +++++++++++++++++++++++++++++++++++++- spec/jira/client_spec.rb | 9 +++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/lib/jira/client.rb b/lib/jira/client.rb index 3cde011b..d472e37b 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -29,7 +29,10 @@ module JIRA # :proxy_username => nil, # :proxy_password => nil, # :additional_cookies => nil, - # :default_headers => {} + # :default_headers => {}, + # :use_client_cert => false, + # :http_debug => false, + # :shared_secret => nil # # See the JIRA::Base class methods for all of the available methods on these accessor # objects. @@ -48,6 +51,36 @@ class Client def_delegators :@request_client, :init_access_token, :set_access_token, :set_request_token, :request_token, :access_token, :authenticated? + DEFINED_OPTIONS = [ + :site, + :context_path, + :signature_method, + :request_token_path, + :authorize_path, + :access_token_path, + :private_key_file, + :rest_base_path, + :consumer_key, + :consumer_secret, + :ssl_verify_mode, + :ssl_version, + :use_ssl, + :username, + :password, + :auth_type, + :proxy_address, + :proxy_port, + :proxy_username, + :proxy_password, + :additional_cookies, + :default_headers, + :use_client_cert, + :http_debug, + :issuer, + :base_url, + :shared_secret + ].freeze + DEFAULT_OPTIONS = { site: 'http://localhost:2990', context_path: '/jira', @@ -65,6 +98,9 @@ def initialize(options = {}) @options = options @options[:rest_base_path] = @options[:context_path] + @options[:rest_base_path] + unknown_options = options.keys.reject { |o| DEFINED_OPTIONS.include?(o) } + raise ArgumentError, "Unknown option(s) given: #{unknown_options}" unless unknown_options.empty? + if options[:use_client_cert] raise ArgumentError, 'Options: :cert_path must be set when :use_client_cert is true' unless @options[:cert_path] raise ArgumentError, 'Options: :key_path must be set when :use_client_cert is true' unless @options[:key_path] diff --git a/spec/jira/client_spec.rb b/spec/jira/client_spec.rb index 51a16c20..df6c6a94 100644 --- a/spec/jira/client_spec.rb +++ b/spec/jira/client_spec.rb @@ -266,4 +266,13 @@ include_examples 'OAuth Common Tests' end + + context 'with unknown options' do + let(:options) { { 'username' => 'foo', 'password' => 'bar', auth_type: :basic } } + subject { JIRA::Client.new(options) } + + it 'raises an ArgumentError' do + expect { subject }.to raise_exception(ArgumentError, 'Unknown option(s) given: ["username", "password"]') + end + end end From c360a3fc2fcb9e92b00fd465c838d1c91ddcbc08 Mon Sep 17 00:00:00 2001 From: Yuri Kanivetsky Date: Fri, 3 Jul 2020 15:44:31 +0300 Subject: [PATCH 28/94] Make client tolerate trailing slashes in site option --- lib/jira/base.rb | 2 +- spec/jira/base_spec.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/jira/base.rb b/lib/jira/base.rb index 8cea4fa5..8f98cf05 100644 --- a/lib/jira/base.rb +++ b/lib/jira/base.rb @@ -424,7 +424,7 @@ def url end if @attrs['self'] the_url = @attrs['self'] - the_url = the_url.sub(@client.options[:site], '') if @client.options[:site] + the_url = the_url.sub(@client.options[:site].chomp('/'), '') if @client.options[:site] the_url elsif key_value self.class.singular_path(client, key_value.to_s, prefix) diff --git a/spec/jira/base_spec.rb b/spec/jira/base_spec.rb index 4a0fd202..26749bd5 100644 --- a/spec/jira/base_spec.rb +++ b/spec/jira/base_spec.rb @@ -369,6 +369,18 @@ class JIRA::Resource::HasManyExample < JIRA::Base # :nodoc: expect(subject.url).to eq('http://foo/bar') end + it 'returns path as the URL if set and site options is specified' do + allow(client).to receive(:options) { { site: 'http://foo' } } + attrs['self'] = 'http://foo/bar' + expect(subject.url).to eq('/bar') + end + + it 'returns path as the URL if set and site options is specified and ends with a slash' do + allow(client).to receive(:options) { { site: 'http://foo/' } } + attrs['self'] = 'http://foo/bar' + expect(subject.url).to eq('/bar') + end + it 'generates the URL from id if self not set' do attrs['self'] = nil attrs['id'] = '98765' From 263543264d83d38765e9e1eb802458adde2665b3 Mon Sep 17 00:00:00 2001 From: Yuri Kanivetsky Date: Fri, 3 Jul 2020 15:44:55 +0300 Subject: [PATCH 29/94] README.md: add trailing slashes (for consistency) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 386b912a..66231eb0 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ api_token = "myApiToken" options = { :username => username, :password => api_token, - :site => 'http://localhost:8080/', # or 'https://.atlassian.net' + :site => 'http://localhost:8080/', # or 'https://.atlassian.net/' :context_path => '/myjira', # often blank :auth_type => :basic, :read_timeout => 120 @@ -307,7 +307,7 @@ class App < Sinatra::Base # site uri, and the request token, access token, and authorize paths before do options = { - :site => 'http://localhost:2990', + :site => 'http://localhost:2990/', :context_path => '/jira', :signature_method => 'RSA-SHA1', :request_token_path => "/plugins/servlet/oauth/request-token", @@ -405,7 +405,7 @@ require 'pp' require 'jira-ruby' options = { - :site => 'http://localhost:2990', + :site => 'http://localhost:2990/', :context_path => '/jira', :signature_method => 'RSA-SHA1', :private_key_file => "rsakey.pem", From c6bd701ad9b8f13572b96c9f3a3b39ce3736dbe0 Mon Sep 17 00:00:00 2001 From: Simon Lacroix Date: Fri, 3 Jul 2020 15:36:01 +0200 Subject: [PATCH 30/94] Bump version to 2.1.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 76c7f935..fa7ba25e 100644 --- a/lib/jira/version.rb +++ b/lib/jira/version.rb @@ -1,3 +1,3 @@ module JIRA - VERSION = '2.0.0'.freeze + VERSION = '2.1.0'.freeze end From 2404ad0192d2538e8935cd46bc8b146256703a8d Mon Sep 17 00:00:00 2001 From: Orgad Shaneh Date: Thu, 9 Jul 2020 12:06:41 +0300 Subject: [PATCH 31/94] Add read_timeout to accepted options It is used in http_client, and the example in README uses it. --- lib/jira/client.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/jira/client.rb b/lib/jira/client.rb index d472e37b..9d2c9596 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -31,6 +31,7 @@ module JIRA # :additional_cookies => nil, # :default_headers => {}, # :use_client_cert => false, + # :read_timeout => nil, # :http_debug => false, # :shared_secret => nil # @@ -75,6 +76,7 @@ class Client :additional_cookies, :default_headers, :use_client_cert, + :read_timeout, :http_debug, :issuer, :base_url, From c7aedd822473972b62d36b15230f27dc9c645829 Mon Sep 17 00:00:00 2001 From: Simon Lacroix Date: Thu, 9 Jul 2020 11:14:55 +0200 Subject: [PATCH 32/94] Bump version to 2.1.1 --- 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 fa7ba25e..0da07642 100644 --- a/lib/jira/version.rb +++ b/lib/jira/version.rb @@ -1,3 +1,3 @@ module JIRA - VERSION = '2.1.0'.freeze + VERSION = '2.1.1'.freeze end From fdfc4bd51b3e8514401e490e5d4695d2bc6415eb Mon Sep 17 00:00:00 2001 From: dicksongonzalez Date: Thu, 9 Jul 2020 11:47:09 -0600 Subject: [PATCH 33/94] Adding private_key option for server oauth --- lib/jira/client.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/jira/client.rb b/lib/jira/client.rb index 9d2c9596..e2103580 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -14,6 +14,7 @@ module JIRA # :request_token_path => "/plugins/servlet/oauth/request-token", # :authorize_path => "/plugins/servlet/oauth/authorize", # :access_token_path => "/plugins/servlet/oauth/access-token", + # :private_key => nil, # :private_key_file => "rsakey.pem", # :rest_base_path => "/rest/api/2", # :consumer_key => nil, @@ -59,6 +60,7 @@ class Client :request_token_path, :authorize_path, :access_token_path, + :private_key, :private_key_file, :rest_base_path, :consumer_key, From 9be920fb8525cf8f71009bb8fde788115d47693a Mon Sep 17 00:00:00 2001 From: dicksongonzalez Date: Thu, 9 Jul 2020 11:32:43 -0600 Subject: [PATCH 34/94] Supporting proxy option pass to oauth gem Giving a more meaningful name to the ssl client cert and key --- lib/jira/client.rb | 4 ++-- lib/jira/http_client.rb | 6 ++++-- spec/jira/http_client_spec.rb | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/jira/client.rb b/lib/jira/client.rb index 9d2c9596..cf05f953 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -106,8 +106,8 @@ def initialize(options = {}) if options[:use_client_cert] raise ArgumentError, 'Options: :cert_path must be set when :use_client_cert is true' unless @options[:cert_path] raise ArgumentError, 'Options: :key_path must be set when :use_client_cert is true' unless @options[:key_path] - @options[:cert] = OpenSSL::X509::Certificate.new(File.read(@options[:cert_path])) - @options[:key] = OpenSSL::PKey::RSA.new(File.read(@options[:key_path])) + @options[:ssl_client_cert] = OpenSSL::X509::Certificate.new(File.read(@options[:cert_path])) + @options[:ssl_client_key] = OpenSSL::PKey::RSA.new(File.read(@options[:key_path])) end case options[:auth_type] diff --git a/lib/jira/http_client.rb b/lib/jira/http_client.rb index 5d81166f..fe4ae378 100644 --- a/lib/jira/http_client.rb +++ b/lib/jira/http_client.rb @@ -46,6 +46,8 @@ def basic_auth_http_conn def http_conn(uri) if @options[:proxy_address] + # proxy_address does not exist in oauth's gem context but proxy does + @options[:proxy] = @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 @@ -53,8 +55,8 @@ def http_conn(uri) http_conn = http_class.new(uri.host, uri.port) http_conn.use_ssl = @options[:use_ssl] if @options[:use_client_cert] - http_conn.cert = @options[:cert] - http_conn.key = @options[:key] + http_conn.cert = @options[:ssl_client_cert] + http_conn.key = @options[:ssl_client_key] end http_conn.verify_mode = @options[:ssl_verify_mode] http_conn.ssl_version = @options[:ssl_version] if @options[:ssl_version] diff --git a/spec/jira/http_client_spec.rb b/spec/jira/http_client_spec.rb index 01e3ae12..11b22943 100644 --- a/spec/jira/http_client_spec.rb +++ b/spec/jira/http_client_spec.rb @@ -280,8 +280,8 @@ expect(http_conn).to receive(:use_ssl=).with(basic_client.options[:use_ssl]) expect(http_conn).to receive(:verify_mode=).with(basic_client.options[:ssl_verify_mode]) expect(http_conn).to receive(:read_timeout=).with(basic_client.options[:read_timeout]) - expect(http_conn).to receive(:cert=).with(basic_client_cert_client.options[:cert]) - expect(http_conn).to receive(:key=).with(basic_client_cert_client.options[:key]) + expect(http_conn).to receive(:cert=).with(basic_client_cert_client.options[:ssl_client_cert]) + expect(http_conn).to receive(:key=).with(basic_client_cert_client.options[:ssl_client_key]) expect(basic_client_cert_client.http_conn(uri)).to eq(http_conn) end From d3937a1d637ecf9da270ccff226e5aedc9feafd8 Mon Sep 17 00:00:00 2001 From: Dickson Date: Thu, 9 Jul 2020 17:31:14 -0600 Subject: [PATCH 35/94] Reverting rename on cert and key --- lib/jira/client.rb | 4 ++-- lib/jira/http_client.rb | 4 ++-- spec/jira/http_client_spec.rb | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/jira/client.rb b/lib/jira/client.rb index cf05f953..9d2c9596 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -106,8 +106,8 @@ def initialize(options = {}) if options[:use_client_cert] raise ArgumentError, 'Options: :cert_path must be set when :use_client_cert is true' unless @options[:cert_path] raise ArgumentError, 'Options: :key_path must be set when :use_client_cert is true' unless @options[:key_path] - @options[:ssl_client_cert] = OpenSSL::X509::Certificate.new(File.read(@options[:cert_path])) - @options[:ssl_client_key] = OpenSSL::PKey::RSA.new(File.read(@options[:key_path])) + @options[:cert] = OpenSSL::X509::Certificate.new(File.read(@options[:cert_path])) + @options[:key] = OpenSSL::PKey::RSA.new(File.read(@options[:key_path])) end case options[:auth_type] diff --git a/lib/jira/http_client.rb b/lib/jira/http_client.rb index fe4ae378..afdf2df5 100644 --- a/lib/jira/http_client.rb +++ b/lib/jira/http_client.rb @@ -55,8 +55,8 @@ def http_conn(uri) http_conn = http_class.new(uri.host, uri.port) http_conn.use_ssl = @options[:use_ssl] if @options[:use_client_cert] - http_conn.cert = @options[:ssl_client_cert] - http_conn.key = @options[:ssl_client_key] + http_conn.cert = @options[:cert] + http_conn.key = @options[:key] end http_conn.verify_mode = @options[:ssl_verify_mode] http_conn.ssl_version = @options[:ssl_version] if @options[:ssl_version] diff --git a/spec/jira/http_client_spec.rb b/spec/jira/http_client_spec.rb index 11b22943..01e3ae12 100644 --- a/spec/jira/http_client_spec.rb +++ b/spec/jira/http_client_spec.rb @@ -280,8 +280,8 @@ expect(http_conn).to receive(:use_ssl=).with(basic_client.options[:use_ssl]) expect(http_conn).to receive(:verify_mode=).with(basic_client.options[:ssl_verify_mode]) expect(http_conn).to receive(:read_timeout=).with(basic_client.options[:read_timeout]) - expect(http_conn).to receive(:cert=).with(basic_client_cert_client.options[:ssl_client_cert]) - expect(http_conn).to receive(:key=).with(basic_client_cert_client.options[:ssl_client_key]) + expect(http_conn).to receive(:cert=).with(basic_client_cert_client.options[:cert]) + expect(http_conn).to receive(:key=).with(basic_client_cert_client.options[:key]) expect(basic_client_cert_client.http_conn(uri)).to eq(http_conn) end From 02ee2bd9729b3334a4f2da92a79bafb1d0fea28f Mon Sep 17 00:00:00 2001 From: Dickson Date: Fri, 10 Jul 2020 15:19:52 -0600 Subject: [PATCH 36/94] Add_Cert_and_key_options --- lib/jira/client.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/jira/client.rb b/lib/jira/client.rb index e2103580..d47d228d 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -34,7 +34,9 @@ module JIRA # :use_client_cert => false, # :read_timeout => nil, # :http_debug => false, - # :shared_secret => nil + # :shared_secret => nil, + # :cert_path => nil, + # :key_path => nil # # See the JIRA::Base class methods for all of the available methods on these accessor # objects. @@ -82,7 +84,9 @@ class Client :http_debug, :issuer, :base_url, - :shared_secret + :shared_secret, + :cert_path, + :key_path ].freeze DEFAULT_OPTIONS = { From ed34eec019fceaa03cd78bee954e37903219ba39 Mon Sep 17 00:00:00 2001 From: Dickson Date: Mon, 13 Jul 2020 10:51:25 -0600 Subject: [PATCH 37/94] Adding support for ssl client certificate and key text based Renaming cert and key options in a more meaningful way --- lib/jira/client.rb | 17 +++++++++++------ lib/jira/http_client.rb | 4 ++-- spec/jira/client_spec.rb | 13 +++++++++++++ spec/jira/http_client_spec.rb | 4 ++-- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/jira/client.rb b/lib/jira/client.rb index e2103580..c3b854a7 100644 --- a/lib/jira/client.rb +++ b/lib/jira/client.rb @@ -34,7 +34,9 @@ module JIRA # :use_client_cert => false, # :read_timeout => nil, # :http_debug => false, - # :shared_secret => nil + # :shared_secret => nil, + # :ssl_client_cert => nil, + # :ssl_client_key => nil # # See the JIRA::Base class methods for all of the available methods on these accessor # objects. @@ -82,7 +84,9 @@ class Client :http_debug, :issuer, :base_url, - :shared_secret + :shared_secret, + :ssl_client_cert, + :ssl_client_key ].freeze DEFAULT_OPTIONS = { @@ -106,10 +110,11 @@ def initialize(options = {}) raise ArgumentError, "Unknown option(s) given: #{unknown_options}" unless unknown_options.empty? if options[:use_client_cert] - raise ArgumentError, 'Options: :cert_path must be set when :use_client_cert is true' unless @options[:cert_path] - raise ArgumentError, 'Options: :key_path must be set when :use_client_cert is true' unless @options[:key_path] - @options[:cert] = OpenSSL::X509::Certificate.new(File.read(@options[:cert_path])) - @options[:key] = OpenSSL::PKey::RSA.new(File.read(@options[:key_path])) + @options[:ssl_client_cert] = OpenSSL::X509::Certificate.new(File.read(@options[:cert_path])) if @options[:cert_path] + @options[:ssl_client_key] = OpenSSL::PKey::RSA.new(File.read(@options[:key_path])) if @options[:key_path] + + raise ArgumentError, 'Options: :cert_path or :ssl_client_cert must be set when :use_client_cert is true' unless @options[:ssl_client_cert] + raise ArgumentError, 'Options: :key_path or :ssl_client_key must be set when :use_client_cert is true' unless @options[:ssl_client_key] end case options[:auth_type] diff --git a/lib/jira/http_client.rb b/lib/jira/http_client.rb index 5d81166f..e68a5371 100644 --- a/lib/jira/http_client.rb +++ b/lib/jira/http_client.rb @@ -53,8 +53,8 @@ def http_conn(uri) http_conn = http_class.new(uri.host, uri.port) http_conn.use_ssl = @options[:use_ssl] if @options[:use_client_cert] - http_conn.cert = @options[:cert] - http_conn.key = @options[:key] + http_conn.cert = @options[:ssl_client_cert] + http_conn.key = @options[:ssl_client_key] end http_conn.verify_mode = @options[:ssl_verify_mode] http_conn.ssl_version = @options[:ssl_version] if @options[:ssl_version] diff --git a/spec/jira/client_spec.rb b/spec/jira/client_spec.rb index df6c6a94..f8f44dee 100644 --- a/spec/jira/client_spec.rb +++ b/spec/jira/client_spec.rb @@ -59,6 +59,19 @@ expect(subject.Project.find('123')).to eq(find_result) end end + + describe 'SSL client options' do + context 'without certificate and key' do + let(:options) { { use_client_cert: true } } + subject { JIRA::Client.new(options) } + + it 'raises an ArgumentError' do + expect { subject }.to raise_exception(ArgumentError, 'Options: :cert_path or :ssl_client_cert must be set when :use_client_cert is true') + options[:ssl_client_cert] = '' + expect { subject }.to raise_exception(ArgumentError, 'Options: :key_path or :ssl_client_key must be set when :use_client_cert is true') + end + end + end end RSpec.shared_examples 'HttpClient tests' do diff --git a/spec/jira/http_client_spec.rb b/spec/jira/http_client_spec.rb index 01e3ae12..11b22943 100644 --- a/spec/jira/http_client_spec.rb +++ b/spec/jira/http_client_spec.rb @@ -280,8 +280,8 @@ expect(http_conn).to receive(:use_ssl=).with(basic_client.options[:use_ssl]) expect(http_conn).to receive(:verify_mode=).with(basic_client.options[:ssl_verify_mode]) expect(http_conn).to receive(:read_timeout=).with(basic_client.options[:read_timeout]) - expect(http_conn).to receive(:cert=).with(basic_client_cert_client.options[:cert]) - expect(http_conn).to receive(:key=).with(basic_client_cert_client.options[:key]) + expect(http_conn).to receive(:cert=).with(basic_client_cert_client.options[:ssl_client_cert]) + expect(http_conn).to receive(:key=).with(basic_client_cert_client.options[:ssl_client_key]) expect(basic_client_cert_client.http_conn(uri)).to eq(http_conn) end From e33799f565f72ff9a269532164e079127471d723 Mon Sep 17 00:00:00 2001 From: Simon Lacroix Date: Tue, 14 Jul 2020 11:10:58 +0200 Subject: [PATCH 38/94] Bump version to 2.1.2 --- 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 0da07642..7d37db82 100644 --- a/lib/jira/version.rb +++ b/lib/jira/version.rb @@ -1,3 +1,3 @@ module JIRA - VERSION = '2.1.1'.freeze + VERSION = '2.1.2'.freeze end From 0fb356f328c276f618e481ee1e0e2f60992a4c3c Mon Sep 17 00:00:00 2001 From: Dickson Date: Tue, 14 Jul 2020 13:27:17 -0600 Subject: [PATCH 39/94] moving proxy option to initializer --- lib/jira/http_client.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jira/http_client.rb b/lib/jira/http_client.rb index afdf2df5..30d58846 100644 --- a/lib/jira/http_client.rb +++ b/lib/jira/http_client.rb @@ -14,6 +14,8 @@ class HttpClient < RequestClient def initialize(options) @options = DEFAULT_OPTIONS.merge(options) + # proxy_address does not exist in oauth's gem context but proxy does + @options[:proxy] = @options[:proxy_address] if @options[:proxy_address] @cookies = {} end @@ -46,8 +48,6 @@ def basic_auth_http_conn def http_conn(uri) if @options[:proxy_address] - # proxy_address does not exist in oauth's gem context but proxy does - @options[:proxy] = @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 From a36cdd81349fc7d6e1ce267b836c672d9961b28e Mon Sep 17 00:00:00 2001 From: Dickson Date: Tue, 14 Jul 2020 13:59:53 -0600 Subject: [PATCH 40/94] moving proxy option to oauth_client initializer --- lib/jira/http_client.rb | 2 -- lib/jira/oauth_client.rb | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jira/http_client.rb b/lib/jira/http_client.rb index 30d58846..5d81166f 100644 --- a/lib/jira/http_client.rb +++ b/lib/jira/http_client.rb @@ -14,8 +14,6 @@ class HttpClient < RequestClient def initialize(options) @options = DEFAULT_OPTIONS.merge(options) - # proxy_address does not exist in oauth's gem context but proxy does - @options[:proxy] = @options[:proxy_address] if @options[:proxy_address] @cookies = {} end diff --git a/lib/jira/oauth_client.rb b/lib/jira/oauth_client.rb index f6ed6132..fcfface8 100644 --- a/lib/jira/oauth_client.rb +++ b/lib/jira/oauth_client.rb @@ -38,6 +38,8 @@ def init_oauth_consumer(_options) @options[:request_token_path] = @options[:context_path] + @options[:request_token_path] @options[:authorize_path] = @options[:context_path] + @options[:authorize_path] @options[:access_token_path] = @options[:context_path] + @options[:access_token_path] + # proxy_address does not exist in oauth's gem context but proxy does + @options[:proxy] = @options[:proxy_address] if @options[:proxy_address] OAuth::Consumer.new(@options[:consumer_key], @options[:consumer_secret], @options) end From 342755e4cdecc3ee5a7305e7dd2a2782c56dfad9 Mon Sep 17 00:00:00 2001 From: Simon Lacroix Date: Tue, 21 Jul 2020 17:52:59 +0200 Subject: [PATCH 41/94] Bump version to 2.1.3 --- 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 7d37db82..7efe598e 100644 --- a/lib/jira/version.rb +++ b/lib/jira/version.rb @@ -1,3 +1,3 @@ module JIRA - VERSION = '2.1.2'.freeze + VERSION = '2.1.3'.freeze end From e3eb3d168ab4afe0d368cb6b985d7e3c34503bdb Mon Sep 17 00:00:00 2001 From: Allan Date: Thu, 13 Aug 2020 13:47:29 +1000 Subject: [PATCH 42/94] 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 43/94] 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 44/94] 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 45/94] 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 46/94] 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 47/94] 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 48/94] 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 49/94] 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 50/94] 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 51/94] 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 52/94] 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 53/94] 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 54/94] 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 55/94] 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 56/94] 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 57/94] 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 58/94] 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 59/94] 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 60/94] 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 61/94] 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 62/94] 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 63/94] 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 64/94] 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 65/94] 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 66/94] 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 67/94] 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 68/94] 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 69/94] 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 70/94] 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 71/94] 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 72/94] 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 73/94] 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 74/94] 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 75/94] 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 76/94] 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 77/94] 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 78/94] 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 79/94] 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 80/94] 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 81/94] 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 82/94] 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 83/94] 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 84/94] 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 85/94] 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 86/94] 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 87/94] 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 88/94] 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 89/94] #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 90/94] #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 91/94] 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 92/94] 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 93/94] 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 94/94] 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