diff --git a/lib/connection.rb b/lib/connection.rb index 0b3a2a3..0dbcdde 100644 --- a/lib/connection.rb +++ b/lib/connection.rb @@ -1,87 +1,87 @@ # frozen_string_literal: true # # Copyright (C) 2016 Harald Sitter # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) version 3, or any # later version accepted by the membership of KDE e.V. (or its # successor approved by the membership of KDE e.V.), which shall # act as a proxy defined in Section 6 of version 3 of the license. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . require 'faraday' require 'json' require 'uri' require 'logger' -require(File.expand_path('representation', File.dirname(__FILE__))) +require_relative 'representation' module Conduit # Connection logic wrapper. class Connection # FinerStruct wrapper around returned hash. class ConduitRepsonse < FinerStruct::Immutable def initialize(response) super(JSON.parse(response.body, symbolize_names: true)) end def to_error RuntimeError.new("ConduitError #{error_code}: #{error_info}") end def error? !error_code.nil? end end class << self # Somewhat hackish since we have no global config. attr_accessor :api_token end attr_accessor :uri def initialize @uri = URI.parse('https://phabricator.kde.org/api') end - def call(meth, kwords = {}) + def call(meth, **kwords) response = get(meth, kwords) raise "HTTPError #{response.status}" if response.status != 200 response = ConduitRepsonse.new(response) raise response.to_error if response.error? response.result end private - def get(meth, kwords = {}) + def get(meth, **kwords) client.get do |req| req.url meth req.headers['Content-Type'] = 'application/json' req.params = kwords.merge('api.token' => api_token) end end def api_token @api_token ||= ENV.fetch('PHABRICATOR_API_TOKEN', self.class.api_token) end def client @client ||= Faraday.new(@uri.to_s) do |faraday| faraday.request :url_encoded # faraday.response :logger, ::Logger.new(STDOUT), bodies: true faraday.adapter Faraday.default_adapter end end end end diff --git a/lib/differential.rb b/lib/differential.rb index e794b6c..cdee852 100644 --- a/lib/differential.rb +++ b/lib/differential.rb @@ -1,35 +1,35 @@ # frozen_string_literal: true # # Copyright (C) 2016 Harald Sitter # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) version 3, or any # later version accepted by the membership of KDE e.V. (or its # successor approved by the membership of KDE e.V.), which shall # act as a proxy defined in Section 6 of version 3 of the license. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . -require(File.expand_path('connection', File.dirname(__FILE__))) -require(File.expand_path('representation', File.dirname(__FILE__))) +require_relative 'connection' +require_relative 'representation' module Conduit # A code review class Differential < Representation class << self def get(id, connection = Connection.new) data = connection.call('differential.query', ids: [id]) raise 'Empty response from Phabricator' if !data || data.empty? new(connection, data[0]) end end end end diff --git a/lib/maniphest.rb b/lib/maniphest.rb index a284564..2df1c74 100644 --- a/lib/maniphest.rb +++ b/lib/maniphest.rb @@ -1,33 +1,33 @@ # frozen_string_literal: true # # Copyright (C) 2016 Harald Sitter # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) version 3, or any # later version accepted by the membership of KDE e.V. (or its # successor approved by the membership of KDE e.V.), which shall # act as a proxy defined in Section 6 of version 3 of the license. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . -require(File.expand_path('connection', File.dirname(__FILE__))) -require(File.expand_path('representation', File.dirname(__FILE__))) +require_relative 'connection' +require_relative 'representation' module Conduit # A task class Maniphest < Representation class << self def get(task_id, connection = Connection.new) new(connection, connection.call('maniphest.info', task_id: task_id)) end end end end diff --git a/lib/project.rb b/lib/project.rb index e6cec74..bad5e5b 100644 --- a/lib/project.rb +++ b/lib/project.rb @@ -1,38 +1,38 @@ # frozen_string_literal: true # # Copyright (C) 2016 Harald Sitter # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) version 3, or any # later version accepted by the membership of KDE e.V. (or its # successor approved by the membership of KDE e.V.), which shall # act as a proxy defined in Section 6 of version 3 of the license. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . -require(File.expand_path('connection', File.dirname(__FILE__))) -require(File.expand_path('representation', File.dirname(__FILE__))) +require_relative 'connection' +require_relative 'representation' module Conduit # A project class Project < Representation class << self def find_by_phids(phids, connection = Connection.new) return [] if !phids || phids.empty? page = connection.call('project.query', phids: phids) # In the data: keys are the phids, values are the actual objects page[:data].values.collect do |hash| new(connection, hash) end end end end end diff --git a/phabricator.rb b/phabricator.rb index 5aae209..1479396 100644 --- a/phabricator.rb +++ b/phabricator.rb @@ -1,107 +1,105 @@ # encoding: utf-8 # frozen_string_literal: true # # Copyright (C) 2016 Harald Sitter # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 3 of # the License or any later version accepted by the membership of # KDE e.V. (or its successor approved by the membership of KDE # e.V.), which shall act as a proxy defined in Section 14 of # version 3 of the license. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -require(File.expand_path('lib/differential', File.dirname(__FILE__))) -require(File.expand_path('lib/maniphest', File.dirname(__FILE__))) -require(File.expand_path('lib/project', File.dirname(__FILE__))) +require_relative 'lib/differential' +require_relative 'lib/maniphest' +require_relative 'lib/project' # Phabricator Plugin class PhabricatorPlugin < Plugin Config.register Config::StringValue.new('phabricator.api_token', :requires_rescan => true, :default => '', :desc => 'Phabricator API token to use for conduit.') Config.register Config::ArrayValue.new('phabricator.blacklist', :default => %w[#kde-bugs-activity], :desc => 'Disables all message handling in the listed channels.') Config.register Config::ArrayValue.new('phabricator.url_blacklist', :default => [], :desc => 'Disables message handling on URLs only in the listed channels. Trigger words still get handled.') def initialize super Conduit::Connection.api_token = bot.config['phabricator.api_token'] end def unreplied(m, _ = {}) return if skip?(m) maybe_task(m) unless m.replied? maybe_diff(m) unless m.replied? end def maybe_task(m) return unless (match = m.message.scan(/\bT(\d+)\b+/)) match.flatten.each do |number| task(m, number: number) end end def maybe_diff(m) return unless (match = m.message.scan(/\bD(\d+)\b/)) match.flatten.each do |number| diff(m, number: number) end end - def task(m, options = {}) - number = options.fetch(:number) + def task(m, number:) task = Conduit::Maniphest.get(number) projects = Conduit::Project.find_by_phids(task.projectPHIDs) m.reply "Task #{task.id} \"#{task.title}\" [#{task.statusName},#{task.priority}] {#{projects.collect(&:name).join(',')}} #{task.uri}" rescue => e m.notify "Task not found ¯\\_(ツ)_/¯ #{e}" end - def diff(m, options = {}) - number = options.fetch(:number) + def diff(m, number:) diff = Conduit::Differential.get(number) m.reply "Diff #{diff.id} \"#{diff.title}\" [#{diff.statusName}] #{diff.uri}" rescue => e m.notify "Diff not found ¯\\_(ツ)_/¯ #{e}" end private def skip?(m) skip_handling?(m) || skip_url_handling?(m) end def skip_handling?(m) bot.config['phabricator.blacklist'].any? do |exclude| m.channel && m.channel.name == exclude end end def skip_url_handling?(m) # This isn't technically the most reliable check. We'd have to find # matches, determine their urlyness and then skip the urly ones while # processing the nonurly ones, I really can't be bothered with that. is_url = m.message.include?('https://phabricator.kde.org/') return false unless is_url bot.config['phabricator.url_blacklist'].any? do |exclude| m.channel && m.channel.name == exclude end end end PhabricatorPlugin.new unless ENV['DONT_TEST_INIT'] diff --git a/test/differential_test.rb b/test/differential_test.rb index 8d3e9c5..b7ceec9 100644 --- a/test/differential_test.rb +++ b/test/differential_test.rb @@ -1,12 +1,12 @@ -require(File.expand_path('test_helper', File.dirname(__FILE__))) +require_relative 'test_helper' require 'lib/differential' class DifferentialTest < Test::Unit::TestCase def test_get_success VCR.use_cassette("#{self.class}/#{__method__}") do diff = Conduit::Differential.get(2372) assert_equal('Make powerdevil normal executable instead of kded module', diff.title) end end end diff --git a/test/maniphest_test.rb b/test/maniphest_test.rb index 7bf8eac..1977402 100644 --- a/test/maniphest_test.rb +++ b/test/maniphest_test.rb @@ -1,12 +1,12 @@ -require(File.expand_path('test_helper', File.dirname(__FILE__))) +require_relative 'test_helper' require 'lib/maniphest' class ManiphestTest < Test::Unit::TestCase def test_get_success VCR.use_cassette("#{self.class}/#{__method__}") do task = Conduit::Maniphest.get(3192) assert_equal('Move Mediacentre to Extragear', task.title) end end end diff --git a/test/plugin_test.rb b/test/plugin_test.rb index cdeb4e1..00dbb78 100644 --- a/test/plugin_test.rb +++ b/test/plugin_test.rb @@ -1,165 +1,165 @@ # encoding: utf-8 -require(File.expand_path('test_helper', File.dirname(__FILE__))) +require_relative 'test_helper' # Dud base class. We mocha this for functionality later. class Plugin class Config class StringValue - def initialize(_, _ = {}) + def initialize(_, **) end end class ArrayValue @@kdebugsactivity_defaulted = false def self.kdebugsactivity_defaulted # At least one ArrayValue must have had a default value with the bugs # activity channel. This is technically a very specific value but # asserting any had it should be good enough. @@kdebugsactivity_defaulted end - def initialize(_, params = {}) + def initialize(_, **params) return unless params[:default] == %w[#kde-bugs-activity] @@kdebugsactivity_defaulted = true end end def self.register(_) end end def map(*args) end def bot end end require 'phabricator' class PluginTest < Test::Unit::TestCase # NB: mocha is stupid with the quotes and can't tell single from double! def setup assert(Plugin::Config::ArrayValue.kdebugsactivity_defaulted, 'blacklist ArrayValue should have had the bugs activity channel blacklisted') config = mock('config') # Do not give an api_token as we need the environment to take over. config.stubs(:[]).with('phabricator.api_token').returns(nil) # This default value is also set in the rb and asserted via our duds config.stubs(:[]).with('phabricator.blacklist').returns(%w[#kde-bugs-activity]) config.stubs(:[]).with('phabricator.url_blacklist').returns([]) bot = mock('bot') bot.stubs(:config).returns(config) Plugin.any_instance.stubs(:bot).returns(bot) @config = config end def teardown end def message_double channel = mock('message-channel') channel.stubs(:name).returns('#message-double-channel') mock('message').tap { |m| m.stubs(:channel).returns(channel) } end def test_get_task_unreplied message = message_double message.stubs(:message).returns('yolo T123 T456 meow T789') message.stubs(:replied?).returns(false) plugin = PhabricatorPlugin.new plugin.expects(:task).with(message, { :number => '123' }) plugin.expects(:task).with(message, { :number => '456' }) plugin.expects(:task).with(message, { :number => '789' }) plugin.unreplied(message) end def test_get_diff_unreplied message = message_double message.stubs(:message).returns('yolo D123 D456 meow D789') message.stubs(:replied?).returns(false) plugin = PhabricatorPlugin.new plugin.expects(:diff).with(message, { :number => '123' }) plugin.expects(:diff).with(message, { :number => '456' }) plugin.expects(:diff).with(message, { :number => '789' }) plugin.unreplied(message) end def test_task message = message_double message.expects(:reply).with('Task 2970 "aptly sftp publishing to files.kde" [Open,Normal] {Neon} https://phabricator.kde.org/T2970') VCR.use_cassette("#{self.class}/#{__method__}") do plugin = PhabricatorPlugin.new plugin.task(message, :number => 2970) end end def test_task_fail message = message_double message.expects(:notify).with('Task not found ¯\_(ツ)_/¯ ConduitError ERR_BAD_TASK: No such Maniphest task exists.') VCR.use_cassette(__method__) do plugin = PhabricatorPlugin.new plugin.task(message, :number => -1) end end def test_diff message = message_double message.expects(:reply).with('Diff 2300 "always load about-distro in ctor" [Closed] https://phabricator.kde.org/D2300') VCR.use_cassette("#{self.class}/#{__method__}") do plugin = PhabricatorPlugin.new plugin.diff(message, :number => 2300) end end def test_diff_fail message = message_double message.expects(:notify).with('Diff not found ¯\_(ツ)_/¯ Empty response from Phabricator') VCR.use_cassette(__method__) do plugin = PhabricatorPlugin.new plugin.diff(message, :number => -1) end end def test_skip message = message_double message.channel.stubs(:name).returns('#kde-bugs-activity') plugin = PhabricatorPlugin.new plugin.unreplied(message) end def test_skip_url @config.stubs(:[]).with('phabricator.url_blacklist').returns(%w[#message-double-channel]) message = message_double message.stubs(:message).returns('yolo https://phabricator.kde.org/D123') message.stubs(:replied?).returns(false) plugin = PhabricatorPlugin.new plugin.expects(:diff).never plugin.unreplied(message) end def test_task_with_no_projects message = message_double message.expects(:reply).with('Task 8149 "Rewritten Dragon player UI in Kirigami (QML)" [Open,Wishlist] {} https://phabricator.kde.org/T8149') VCR.use_cassette("#{self.class}/#{__method__}") do plugin = PhabricatorPlugin.new plugin.task(message, :number => 8149) end end end diff --git a/test/project_test.rb b/test/project_test.rb index bc09e76..88a3eb0 100644 --- a/test/project_test.rb +++ b/test/project_test.rb @@ -1,17 +1,17 @@ -require(File.expand_path('test_helper', File.dirname(__FILE__))) +require_relative 'test_helper' require 'lib/project' class ProjectTest < Test::Unit::TestCase def test_get_success VCR.use_cassette("#{self.class}/#{__method__}") do projects = Conduit::Project.find_by_phids(%w(PHID-PROJ-o2ghhdqrnnhccudwekzd PHID-PROJ-ic6son3yl5tcvafnqbn6)) names = projects.collect(&:name) assert_equal(['Neon', 'Neon Jenkins Administrators'].sort, names.sort) end end def test_get_no_phids assert_equal([], Conduit::Project.find_by_phids([])) end end diff --git a/test/test_helper.rb b/test/test_helper.rb index d929e40..9cdb806 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,45 +1,41 @@ begin require 'simplecov' SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new( [ SimpleCov::Formatter::HTMLFormatter ] ) SimpleCov.start rescue LoadError warn 'SimpleCov not loaded' end -def __dir__ - File.dirname(File.realpath(__FILE__)) -end - require 'vcr' VCR.configure do |config| config.cassette_library_dir = "#{__dir__}/fixtures/vcr_casettes" config.hook_into :webmock config.filter_sensitive_data('API_TOKEN') do |interaction| # Prevent recording the actual token! uri = URI.parse(interaction.request.uri) query = CGI.parse(uri.query) query.fetch('api.token').join('') end # Make sure to ignore api.token from matching. Otherwise we'd have to # meddle with filtering and restoration, where latter is tricky to do # since we don't know what the relevant token would be. config.default_cassette_options = { match_requests_on: [ :method, VCR.request_matchers.uri_without_param('api.token') ] } end $LOAD_PATH.unshift(File.absolute_path('../', __dir__)) # ../ $LOAD_PATH.unshift(File.absolute_path(__dir__)) # test/ ENV['DONT_TEST_INIT'] = 'true' # Do not allow plugin to init. require 'test/unit' require 'mocha/test_unit' # Patch mocha in require 'mocha/setup' # Make sure it is set up (ruby 1.9) diff --git a/test/test_run.rb b/test/test_run.rb index 302e243..e454129 100644 --- a/test/test_run.rb +++ b/test/test_run.rb @@ -1,5 +1,5 @@ # This is a fancy wrapper around test_helper to prevent the collector from # loading the helper twice as it would occur if we ran the helper directly. -require(File.expand_path('test_helper', File.dirname(__FILE__))) +require_relative 'test_helper' Test::Unit::AutoRunner.run(true, File.absolute_path(__dir__))